From d638762f309d7aa6d81b48a87c1e588893dd6914 Mon Sep 17 00:00:00 2001 From: Benoit de Chezelles Date: Sat, 10 Jan 2026 13:07:21 +0100 Subject: [PATCH 1/6] util: Move copy3 in utils with types, to avoid code duplication --- lua/luasnip/extras/fmt.lua | 26 +++----------------------- lua/luasnip/nodes/snippet.lua | 22 +--------------------- lua/luasnip/util/util.lua | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/lua/luasnip/extras/fmt.lua b/lua/luasnip/extras/fmt.lua index 62cf9aff7..5ca461d5c 100644 --- a/lua/luasnip/extras/fmt.lua +++ b/lua/luasnip/extras/fmt.lua @@ -1,29 +1,9 @@ local text_node = require("luasnip.nodes.textNode").T -local wrap_nodes = require("luasnip.util.util").wrap_nodes +local util = require("luasnip.util.util") local extend_decorator = require("luasnip.util.extend_decorator") local Str = require("luasnip.util.str") local rp = require("luasnip.extras").rep --- https://gist.github.com/tylerneylon/81333721109155b2d244 -local function copy3(obj, seen) - -- Handle non-tables and previously-seen tables. - if type(obj) ~= "table" then - return obj - end - if seen and seen[obj] then - return seen[obj] - end - - -- New table; mark it as seen an copy recursively. - local s = seen or {} - local res = {} - s[obj] = res - for k, v in next, obj do - res[copy3(k, s)] = copy3(v, s) - end - return setmetatable(res, getmetatable(obj)) -end - -- Interpolate elements from `args` into format string with placeholders. -- -- The placeholder syntax for selecting from `args` is similar to fmtlib and @@ -102,7 +82,7 @@ local function interpolate(fmt, args, opts) if used_keys[key] then local jump_index = args[key]:get_jump_index() -- For nodes that don't have a jump index, copy it instead if not opts.repeat_duplicates or jump_index == nil then - table.insert(elements, copy3(args[key])) + table.insert(elements, util.copy3(args[key])) else table.insert(elements, rp(jump_index)) end @@ -198,7 +178,7 @@ local function format_nodes(str, nodes, opts) opts = vim.tbl_extend("force", defaults, opts or {}) -- allow to pass a single node - nodes = wrap_nodes(nodes) + nodes = util.wrap_nodes(nodes) -- optimization: avoid splitting multiple times local lines = nil diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 933556dd7..b293eafd8 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -859,28 +859,8 @@ function Snippet:matches(line_to_cursor, opts) return expand_params end --- https://gist.github.com/tylerneylon/81333721109155b2d244 -local function copy3(obj, seen) - -- Handle non-tables and previously-seen tables. - if type(obj) ~= "table" then - return obj - end - if seen and seen[obj] then - return seen[obj] - end - - -- New table; mark it as seen an copy recursively. - local s = seen or {} - local res = {} - s[obj] = res - for k, v in next, obj do - res[copy3(k, s)] = copy3(v, s) - end - return setmetatable(res, getmetatable(obj)) -end - function Snippet:copy() - return copy3(self) + return util.copy3(self) end function Snippet:del_marks() diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index 144dc71d2..b9bb1f38f 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -444,6 +444,32 @@ local function shallow_copy(t) return t end +--- Deepcopy given table, with support for recursive tables & metatable. +--- Taken from: https://gist.github.com/tylerneylon/81333721109155b2d244 +--- +---@generic T: table +---@param obj T +---@param seen? table +---@return T +local function copy3(obj, seen) + -- Handle non-tables and previously-seen tables. + if type(obj) ~= "table" then + return obj + end + if seen and seen[obj] then + return seen[obj] + end + + -- New table; mark it as seen an copy recursively. + local s = seen or {} + local res = {} + s[obj] = res + for k, v in next, obj do + res[copy3(k, s)] = copy3(v, s) + end + return setmetatable(res, getmetatable(obj)) +end + return { get_cursor_0ind = get_cursor_0ind, set_cursor_0ind = set_cursor_0ind, @@ -489,4 +515,5 @@ return { pos_offset = pos_offset, pos_from_offset = pos_from_offset, shallow_copy = shallow_copy, + copy3 = copy3, } From 05ef900505fb1b93150d4ea02337224ea1498d52 Mon Sep 17 00:00:00 2001 From: Benoit de Chezelles Date: Sat, 10 Jan 2026 13:18:42 +0100 Subject: [PATCH 2/6] Add types & update docs for util extend_decorator This is required for proper type deduction after extend_decorator.apply is used. --- lua/luasnip/util/extend_decorator.lua | 74 ++++++++++++++++----------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/lua/luasnip/util/extend_decorator.lua b/lua/luasnip/util/extend_decorator.lua index f0723819b..80a31a5de 100644 --- a/lua/luasnip/util/extend_decorator.lua +++ b/lua/luasnip/util/extend_decorator.lua @@ -1,26 +1,46 @@ local M = {} --- map fn -> {arg_indx = int, extend = fn}[] +---@alias LuaSnip.Opts.Util.ExtendDecoratorFn fun(arg: any[], extend_value: any[]): any[] + +---@class LuaSnip.Opts.Util.ExtendDecoratorRegister +---@field arg_indx integer The position of the parameter to override +---@field extend? LuaSnip.Opts.Util.ExtendDecoratorFn A function used to extend +--- the args passed to the decorated function. +--- Defaults to a function which extends the arg-table with the extend-table. +--- This extend-behaviour is adaptable to accomodate `s`, where the first +--- argument may be string or table. + +---@type {[fun(...): any]: LuaSnip.Opts.Util.ExtendDecoratorRegister[]} local function_properties = setmetatable({}, { __mode = "k" }) +--- The default extend function implementation. +--- +---@param arg any[] +---@param extend any[] +---@return any[] local function default_extend(arg, extend) return vim.tbl_extend("keep", arg or {}, extend or {}) end ----Create a new decorated version of `fn`. ----@param fn The function to create a decorator for. ----@vararg The values to extend with. These should match the descriptions passed ----in `register`: ----```lua ----local function somefn(arg1, arg2, opts1, opts2) ----... ----end ----register(somefn, {arg_indx=4}, {arg_indx=3}) ----apply(somefn, ---- {key = "opts2 is extended with this"}, ---- {key = "and opts1 with this"}) ----``` ----@return function: The decorated function. +--- Create a new decorated version of `fn`. +--- +---@generic T: fun(...: any): any +---@param fn T The function to create a decorator for. +---@vararg any The values to extend with. +--- These should match the descriptions passed in `register`. +--- +--- Example: +--- ```lua +--- local function somefn(arg1, arg2, opts1, opts2) +--- ... +--- end +--- register(somefn, {arg_indx=4}, {arg_indx=3}) +--- apply(somefn, +--- {key = "opts2 is extended with this"}, +--- {key = "and opts1 with this"} +--- ) +--- ``` +---@return T The decorated function. function M.apply(fn, ...) local extend_properties = function_properties[fn] assert( @@ -54,20 +74,16 @@ function M.apply(fn, ...) return decorated_fn end ----Prepare a function for usage with extend_decorator. ----To create a decorated function which extends `opts`-style tables passed to it, we need to know ---- 1. which parameter-position the opts are in and ---- 2. how to extend them. ----@param fn function: the function that should be registered. ----@vararg tables. Each describes how to extend one parameter to `fn`. ----The tables accept the following keys: ---- - arg_indx, number (required): the position of the parameter to override. ---- - extend, fn(arg, extend_value) -> effective_arg (optional): this function ---- is used to extend the args passed to the decorated function. ---- It defaults to a function which just extends the the arg-table with the ---- extend-table. ---- This extend-behaviour is adaptable to accomodate `s`, where the first ---- argument may be string or table. +--- Prepare a function for usage with extend_decorator. +--- +--- To create a decorated function which extends `opts`-style tables passed to +--- it, we need to know: +--- 1. which parameter-position the opts are in and +--- 2. how to extend them. +--- +---@param fn function The function that should be registered. +---@vararg LuaSnip.Opts.Util.ExtendDecoratorRegister Each describes how to +--- extend one parameter to `fn`. function M.register(fn, ...) local fn_eps = { ... } From b455852d9d12ff10c9bae1a65f0de1e2b8f94b4f Mon Sep 17 00:00:00 2001 From: Benoit de Chezelles Date: Sat, 10 Jan 2026 13:50:44 +0100 Subject: [PATCH 3/6] extras: Add types for fmt helper functions --- lua/luasnip/extras/fmt.lua | 83 +++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/lua/luasnip/extras/fmt.lua b/lua/luasnip/extras/fmt.lua index 5ca461d5c..39172c7f5 100644 --- a/lua/luasnip/extras/fmt.lua +++ b/lua/luasnip/extras/fmt.lua @@ -4,30 +4,37 @@ local extend_decorator = require("luasnip.util.extend_decorator") local Str = require("luasnip.util.str") local rp = require("luasnip.extras").rep --- Interpolate elements from `args` into format string with placeholders. --- --- The placeholder syntax for selecting from `args` is similar to fmtlib and --- Python's .format(), with some notable differences: --- * no format options (like `{:.2f}`) --- * 1-based indexing --- * numbered/auto-numbered placeholders can be mixed; numbered ones set the --- current index to new value, so following auto-numbered placeholders start --- counting from the new value (e.g. `{} {3} {}` is `{1} {3} {4}`) --- --- Arguments: --- fmt: string with placeholders --- args: table with list-like and/or map-like keys --- opts: --- delimiters: string, 2 distinct characters (left, right), default "{}" --- strict: boolean, set to false to allow for unused `args`, default true --- repeat_duplicates: boolean, repeat nodes which have jump_index instead of copying them, default false --- Returns: a list of strings and elements of `args` inserted into placeholders +---@class LuaSnip.Opts.Extra.FmtInterpolate +---@field delimiters? string String of 2 distinct characters (left, right). +--- Defaults to "{}". +---@field strict? boolean Whether to allow error out on unused `args`. +--- Defaults to true. +---@field repeat_duplicates? boolean Repeat nodes which have the same jump_index +--- instead of copying them. Default to false. + +--- Interpolate elements from `args` into format string with placeholders. +--- +--- The placeholder syntax for selecting from `args` is similar to fmtlib and +--- Python's .format(), with some notable differences: +--- * no format options (like `{:.2f}`) +--- * 1-based indexing +--- * numbered/auto-numbered placeholders can be mixed; numbered ones set the +--- current index to new value, so following auto-numbered placeholders start +--- counting from the new value (e.g. `{} {3} {}` is `{1} {3} {4}`) +--- +---@param fmt string String with placeholders +---@param args LuaSnip.Node[]|{[string]: LuaSnip.Node} Table with list-like +--- and/or map-like keys +---@param opts? LuaSnip.Opts.Extra.FmtInterpolate +---@return (string|LuaSnip.Node)[] _ A list of strings & elements of `args` +--- inserted into placeholders. local function interpolate(fmt, args, opts) local defaults = { delimiters = "{}", strict = true, repeat_duplicates = false, } + ---@type LuaSnip.Opts.Extra.FmtInterpolate opts = vim.tbl_extend("force", defaults, opts or {}) -- sanitize delimiters @@ -47,9 +54,9 @@ local function interpolate(fmt, args, opts) } -- manage insertion of text/args - local elements = {} + local elements = {} ---@type (string|LuaSnip.Node)[] local last_index = 0 - local used_keys = {} + local used_keys = {} ---@type {[any]: boolean} local add_text = function(text) if #text > 0 then @@ -155,26 +162,30 @@ local function interpolate(fmt, args, opts) return elements end --- Use a format string with placeholders to interpolate nodes. --- --- See `interpolate` documentation for details on the format. --- --- Arguments: --- str: format string --- nodes: snippet node or list of nodes --- opts: optional table --- trim_empty: boolean, remove whitespace-only first/last lines, default true --- dedent: boolean, remove all common indent in `str`, default true --- indent_string: string, convert `indent_string` at beginning of each line to unit indent ('\t') --- after applying `dedent`, default empty string (disabled) --- ... the rest is passed to `interpolate` --- Returns: list of snippet nodes +---@class LuaSnip.Opts.Extra.Fmt: LuaSnip.Opts.Extra.FmtInterpolate +---@field trim_empty? boolean Whether to remove whitespace-only first/last lines +--- Defaults to true. +---@field dedent? boolean Whether to remove all common indent in `str`. +--- Defaults to true. +---@field indent_string? string When set, will convert `indent_string` at +--- beginning of each line to unit indent ('\t') after applying `dedent`. +--- Defaults to empty string (disabled). + +--- Use a format string with placeholders to interpolate nodes. +--- +--- See `interpolate` documentation for details on the format. +--- +---@param str string The format string +---@param nodes LuaSnip.Node|LuaSnip.Node[]|{[string]: LuaSnip.Node} +---@param opts? LuaSnip.Opts.Extra.Fmt +---@return LuaSnip.Node[] local function format_nodes(str, nodes, opts) local defaults = { trim_empty = true, dedent = true, indent_string = "", } + ---@type LuaSnip.Opts.Extra.Fmt opts = vim.tbl_extend("force", defaults, opts or {}) -- allow to pass a single node @@ -183,7 +194,7 @@ local function format_nodes(str, nodes, opts) -- optimization: avoid splitting multiple times local lines = nil - lines = vim.split(str, "\n", true) + lines = vim.split(str, "\n", {plain=true}) Str.process_multiline(lines, opts) str = table.concat(lines, "\n") @@ -196,7 +207,7 @@ local function format_nodes(str, nodes, opts) return vim.tbl_map(function(part) -- wrap strings in text nodes if type(part) == "string" then - return text_node(vim.split(part, "\n", true)) + return text_node(vim.split(part, "\n", {plain=true})) else return part end From 30a5ea8c50921ca8771c572d7698eb8f0dab3d16 Mon Sep 17 00:00:00 2001 From: bew Date: Sat, 10 Jan 2026 12:57:29 +0000 Subject: [PATCH 4/6] Auto generate docs --- doc/luasnip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/luasnip.txt b/doc/luasnip.txt index be67aed3e..fcc47c9c6 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -1,4 +1,4 @@ -*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2025 November 03 +*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2026 January 10 ============================================================================== Table of Contents *luasnip-table-of-contents* From 4db74b9e9325dd9db752c6f601c9e5736651dc53 Mon Sep 17 00:00:00 2001 From: bew Date: Sat, 10 Jan 2026 12:57:32 +0000 Subject: [PATCH 5/6] Format with stylua --- lua/luasnip/extras/fmt.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/luasnip/extras/fmt.lua b/lua/luasnip/extras/fmt.lua index 39172c7f5..c199f41c2 100644 --- a/lua/luasnip/extras/fmt.lua +++ b/lua/luasnip/extras/fmt.lua @@ -194,7 +194,7 @@ local function format_nodes(str, nodes, opts) -- optimization: avoid splitting multiple times local lines = nil - lines = vim.split(str, "\n", {plain=true}) + lines = vim.split(str, "\n", { plain = true }) Str.process_multiline(lines, opts) str = table.concat(lines, "\n") @@ -207,7 +207,7 @@ local function format_nodes(str, nodes, opts) return vim.tbl_map(function(part) -- wrap strings in text nodes if type(part) == "string" then - return text_node(vim.split(part, "\n", {plain=true})) + return text_node(vim.split(part, "\n", { plain = true })) else return part end From 21179dd9e796a53c9c4e9106ace75d1577d0c0ea Mon Sep 17 00:00:00 2001 From: bew Date: Fri, 23 Jan 2026 07:59:09 +0000 Subject: [PATCH 6/6] Auto generate docs --- doc/luasnip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/luasnip.txt b/doc/luasnip.txt index b9a8376e8..ab6ec9b4f 100644 --- a/doc/luasnip.txt +++ b/doc/luasnip.txt @@ -1,4 +1,4 @@ -*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2026 January 19 +*luasnip.txt* For NeoVim 0.7-0.11 Last change: 2026 January 23 ============================================================================== Table of Contents *luasnip-table-of-contents*