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* diff --git a/lua/luasnip/extras/fmt.lua b/lua/luasnip/extras/fmt.lua index 62cf9aff7..c199f41c2 100644 --- a/lua/luasnip/extras/fmt.lua +++ b/lua/luasnip/extras/fmt.lua @@ -1,53 +1,40 @@ 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 --- 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 @@ -67,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 @@ -102,7 +89,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 @@ -175,35 +162,39 @@ 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 - nodes = wrap_nodes(nodes) + nodes = util.wrap_nodes(nodes) -- 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") @@ -216,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 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, }