diff --git a/DOC.md b/DOC.md index 1fc4e0fc0..d8f324db9 100644 --- a/DOC.md +++ b/DOC.md @@ -752,21 +752,21 @@ ChoiceNodes allow choosing between multiple nodes. -`c(pos, choices, opts?): LuaSnip.ChoiceNode`: Create a new choiceNode from a list of choices. The -first item in this list is the initial choice, and it can be changed while any node of a choice is -active. So, if all choices should be reachable, every choice has to have a place for the cursor to -stop at. +`c(pos?, choices, node_opts?): LuaSnip.ChoiceNode`: Create a new choiceNode from a list of choices. +The first item in this list is the initial choice, and it can be changed while any node of a choice +is active. So, if all choices should be reachable, every choice has to have a place for the cursor +to stop at. If the choice is a snippetNode like `sn(nil, {...nodes...})` the given `nodes` have to contain an `insertNode` (e.g. `i(1)`). Using an `insertNode` or `textNode` directly as a choice is also fine, the latter is special-cased to have a jump-point at the beginning of its text. -* `pos: integer` Jump-index of the node. (See [Basics-Jump-Index](#jump-index)) +* `pos?: integer?` Jump-index of the node. (See [Basics-Jump-Index](#jump-index)) * `choices: (LuaSnip.Node|LuaSnip.Node[])[]` A list of nodes that can be switched between interactively. If a list of nodes is passed as a choice, it will be turned into a snippetNode. Jumpable nodes that generally need a jump-index don't need one when used as a choice since they inherit the choiceNode's jump-index anyway. -* `opts?: LuaSnip.Opts.ChoiceNode?` Additional optional arguments. +* `node_opts?: LuaSnip.Opts.ChoiceNode?` Additional optional arguments. Valid keys are: * `restore_cursor?: boolean?` If set, the currently active node is looked up in the switched-to @@ -785,7 +785,7 @@ the latter is special-cased to have a jump-point at the beginning of its text. end ``` Consider passing this override into `snip_env`. - * `node_callbacks?: { [("change_choice"|"enter"...)]: fun(...) -> ... }?` + * `node_callbacks?: { [LuaSnip.EventType]: fun(...) -> ... }?` * `node_ext_opts?: LuaSnip.NodeExtOpts?` Pass these opts through to the underlying extmarks representing the node. Notably, this enables highlighting the nodes, and allows the highlight to be different based on the state of the node/snippet. See [ext_opts](#ext_opts) diff --git a/doc/luasnip.txt b/doc/luasnip.txt index be67aed3e..be921342d 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: 2025 November 18 ============================================================================== Table of Contents *luasnip-table-of-contents* @@ -772,23 +772,24 @@ ChoiceNodes allow choosing between multiple nodes. })) < -`c(pos, choices, opts?): LuaSnip.ChoiceNode`: Create a new choiceNode from a -list of choices. The first item in this list is the initial choice, and it can -be changed while any node of a choice is active. So, if all choices should be -reachable, every choice has to have a place for the cursor to stop at. +`c(pos?, choices, node_opts?): LuaSnip.ChoiceNode`: Create a new choiceNode +from a list of choices. The first item in this list is the initial choice, and +it can be changed while any node of a choice is active. So, if all choices +should be reachable, every choice has to have a place for the cursor to stop +at. If the choice is a snippetNode like `sn(nil, {...nodes...})` the given `nodes` have to contain an `insertNode` (e.g. `i(1)`). Using an `insertNode` or `textNode` directly as a choice is also fine, the latter is special-cased to have a jump-point at the beginning of its text. -- `pos: integer` Jump-index of the node. (See |luasnip-basics-jump-index|) +- `pos?: integer?` Jump-index of the node. (See |luasnip-basics-jump-index|) - `choices: (LuaSnip.Node|LuaSnip.Node[])[]` A list of nodes that can be switched between interactively. If a list of nodes is passed as a choice, it will be turned into a snippetNode. Jumpable nodes that generally need a jump-index don’t need one when used as a choice since they inherit the choiceNode’s jump-index anyway. -- `opts?: LuaSnip.Opts.ChoiceNode?` Additional optional arguments. +- `node_opts?: LuaSnip.Opts.ChoiceNode?` Additional optional arguments. Valid keys are: - `restore_cursor?: boolean?` If set, the currently active node is looked up in the switched-to choice, and the cursor restored to preserve the current @@ -806,7 +807,7 @@ have a jump-point at the beginning of its text. end < Consider passing this override into `snip_env`. - - `node_callbacks?: { [("change_choice"|"enter"...)]: fun(...) -> ... }?` + - `node_callbacks?: { [LuaSnip.EventType]: fun(...) -> ... }?` - `node_ext_opts?: LuaSnip.NodeExtOpts?` Pass these opts through to the underlying extmarks representing the node. Notably, this enables highlighting the nodes, and allows the highlight to be different based on the state of the diff --git a/lua/luasnip/_types.lua b/lua/luasnip/_types.lua index e179c8257..18f272292 100644 --- a/lua/luasnip/_types.lua +++ b/lua/luasnip/_types.lua @@ -16,3 +16,6 @@ ---@class LuaSnip.BufferRegion ---@field from LuaSnip.BytecolBufferPosition Starting position, included. ---@field to LuaSnip.BytecolBufferPosition Ending position, excluded. + +---@alias LuaSnip.NormalizedNodeRef LuaSnip.KeyIndexer|LuaSnip.AbsoluteIndexer|LuaSnip.OptionalNodeRef +---@alias LuaSnip.NodeRef LuaSnip.KeyIndexer|LuaSnip.AbsoluteIndexer|LuaSnip.OptionalNodeRef|number diff --git a/lua/luasnip/extras/conditions/expand.lua b/lua/luasnip/extras/conditions/expand.lua index c6e17fc39..5e18e99b4 100644 --- a/lua/luasnip/extras/conditions/expand.lua +++ b/lua/luasnip/extras/conditions/expand.lua @@ -9,6 +9,7 @@ local function line_begin(line_to_cursor, matched_trigger) -- +1 because `string.sub("abcd", 1, -2)` -> abc return line_to_cursor:sub(1, -(#matched_trigger + 1)):match("^%s*$") end +--- A condition obj that is true when the trigger is at start of line (maybe after indent). M.line_begin = cond_obj.make_condition(line_begin) --- The wordTrig flag will only expand the snippet if @@ -41,16 +42,17 @@ M.line_begin = cond_obj.make_condition(line_begin) --- I think the character wordTrig=true uses should be customized --- A condtion seems like the best way to do it --- ---- @param pattern string should be a character class eg `[%w]` +---@param pattern string should be a character class eg `[%w]` +---@return LuaSnip.SnipContext.ConditionObj function M.trigger_not_preceded_by(pattern) local condition = function(line_to_cursor, matched_trigger) local line_to_trigger_len = #line_to_cursor - #matched_trigger if line_to_trigger_len == 0 then return true end - return not string - .sub(line_to_cursor, line_to_trigger_len, line_to_trigger_len) - :match(pattern) + local char_before_trigger = + line_to_cursor:sub(line_to_trigger_len, line_to_trigger_len) + return not char_before_trigger:match(pattern) end return cond_obj.make_condition(condition) end diff --git a/lua/luasnip/extras/conditions/init.lua b/lua/luasnip/extras/conditions/init.lua index db9ff39ff..cdfd5cbb0 100644 --- a/lua/luasnip/extras/conditions/init.lua +++ b/lua/luasnip/extras/conditions/init.lua @@ -1,55 +1,137 @@ -local M = {} - ------------------------ --- CONDITION OBJECTS -- ------------------------ -local condition_mt = { - -- logic operators - -- not '-' - __unm = function(o1) - return M.make_condition(function(...) - return not o1(...) - end) - end, - -- or '+' - __add = function(o1, o2) - return M.make_condition(function(...) - return o1(...) or o2(...) - end) - end, - __sub = function(o1, o2) - return M.make_condition(function(...) - return o1(...) and not o2(...) - end) - end, - -- and '*' - __mul = function(o1, o2) - return M.make_condition(function(...) - return o1(...) and o2(...) - end) - end, - -- xor '^' - __pow = function(o1, o2) - return M.make_condition(function(...) - return o1(...) ~= o2(...) - end) - end, - -- xnor '%' - -- might be counter intuitive, but as we can't use '==' (must return bool) - -- it's best to use something weird (doesn't have to be used) - __mod = function(o1, o2) - return function(...) - return o1(...) == o2(...) - end - end, +--- A composable condition object. It can be used for `condition` in a snippet +--- context but can also be logically combined with other condition +--- function/object to build complex conditions. +--- +--- This makes logical combinations of conditions very readable. +--- +--- Compare +--- ```lua +--- local conds = require"luasnip.extras.conditions.expand" +--- ... +--- -- using combinator functions: +--- condition = conds.line_end:or_(conds.line_begin) +--- -- using operators: +--- condition = conds.line_end + conds.line_begin +--- ``` +--- +--- with the more verbose +--- +--- ```lua +--- local conds = require"luasnip.extras.conditions.expand" +--- ... +--- condition = function(...) return conds.line_end(...) or conds.line_begin(...) end +--- ``` +--- +--- The conditions provided in `show` and `expand` are already condition objects. +--- To create new ones, use: +--- ```lua +--- require("luasnip.extras.conditions").make_condition(condition_fn) +--- ``` +--- +---@class LuaSnip.SnipContext.ConditionObj +---@field func LuaSnip.SnipContext.ConditionFn|LuaSnip.SnipContext.ShowConditionFn +--- +---@overload fun(line_to_cursor: string, matched_trigger: string, captures: string[]): boolean +--- (note: same signature as `func` field) +--- +---@operator unm: LuaSnip.SnipContext.ConditionObj +---@operator add(LuaSnip.SnipContext.Condition): LuaSnip.SnipContext.ConditionObj +---@operator sub(LuaSnip.SnipContext.Condition): LuaSnip.SnipContext.ConditionObj +---@operator mul(LuaSnip.SnipContext.Condition): LuaSnip.SnipContext.ConditionObj +---@operator pow(LuaSnip.SnipContext.Condition): LuaSnip.SnipContext.ConditionObj +---@operator mod(LuaSnip.SnipContext.Condition): LuaSnip.SnipContext.ConditionObj +local ConditionObj = {} +local ConditionObj_mt = { + __index = ConditionObj, -- use table like a function by overloading __call - __call = function(tab, line_to_cursor, matched_trigger, captures) - return tab.func(line_to_cursor, matched_trigger, captures) + __call = function(self, line_to_cursor, matched_trigger, captures) + return self.func(line_to_cursor, matched_trigger, captures) end, } -function M.make_condition(func) - return setmetatable({ func = func }, condition_mt) +--- Wrap the given `condition` function into a composable condition object. +---@param func LuaSnip.SnipContext.ConditionFn +---@return LuaSnip.SnipContext.ConditionObj +function ConditionObj.make_condition(func) + return setmetatable({ func = func }, ConditionObj_mt) +end + +--- Returns a condition object equivalent to `not self(...)` +---@return LuaSnip.SnipContext.ConditionObj +function ConditionObj:inverted() + return ConditionObj.make_condition(function(...) + return not self(...) + end) +end +--- (e.g. `-cond`, implemented as `cond:inverted()`) +ConditionObj_mt.__unm = ConditionObj.inverted + +--- Returns a condition object equivalent to `self(...) or other(...)` +---@param other LuaSnip.SnipContext.Condition +---@return LuaSnip.SnipContext.ConditionObj +function ConditionObj:or_(other) + return ConditionObj.make_condition(function(...) + return self(...) or other(...) + end) +end +--- (e.g. `cond1 + cond2`, implemented as `cond1:or_(cond2)`) +ConditionObj_mt.__add = ConditionObj.or_ + +--- Returns a condition object equivalent to `self(...) and not other(...)` +--- +--- This is similar to set differences: `A \ B = {a in A | a not in B}`. +--- This makes `-(a + b) = -a - b` an identity representing de Morgan's law: +--- `not (a or b) = not a and not b`. +--- However, since boolean algebra lacks an additive inverse, `a + (-b) = a - b` +--- does not hold. Thus, this is NOT the same as `c1 + (-c2)`. +--- +---@param other LuaSnip.SnipContext.Condition +---@return LuaSnip.SnipContext.ConditionObj +function ConditionObj:and_not(other) + return ConditionObj.make_condition(function(...) + return self(...) and not other(...) + end) +end +--- (e.g. `cond1 - cond2`, implemented as `cond1:and_not(cond2)`) +ConditionObj_mt.__sub = ConditionObj.and_not + +--- Returns a condition object equivalent to `self(...) and other(...)` +---@param other LuaSnip.SnipContext.Condition +---@return LuaSnip.SnipContext.ConditionObj +function ConditionObj:and_(other) + return ConditionObj.make_condition(function(...) + return self(...) and other(...) + end) +end +--- (e.g. `cond1 * cond2`, implemented as `cond1:and_(cond2)`) +ConditionObj_mt.__mul = ConditionObj.and_ + +--- Returns a condition object equivalent to `self(...) ~= other(...)` (xor) +---@param other LuaSnip.SnipContext.Condition +---@return LuaSnip.SnipContext.ConditionObj +function ConditionObj:not_same_as(other) + return ConditionObj.make_condition(function(...) + return self(...) ~= other(...) + end) +end +--- (e.g. `cond1 ^ cond2`, implemented as `cond1:not_same_as(cond2)`) +ConditionObj_mt.__pow = ConditionObj.not_same_as + +--- Returns a condition object equivalent to `self(...) == other(...)` (xnor) +---@param other LuaSnip.SnipContext.Condition +---@return LuaSnip.SnipContext.ConditionObj +function ConditionObj:same_as(other) + return ConditionObj.make_condition(function(...) + return self(...) == other(...) + end) end +--- (e.g. `cond1 % cond2`, implemented as `cond1:same_as(cond2)`) +--- +--- Using `%` might be counter intuitive, considering the `==`-operator exists, +--- unfortunately, it's not possible to use this for our purposes (some info +--- [here](https://github.com/L3MON4D3/LuaSnip/pull/612#issuecomment-1264487743)). +--- We decided instead to make use of a more obscure symbol (which will +--- hopefully avoid false assumptions about its meaning). +ConditionObj_mt.__mod = ConditionObj.same_as -return M +return ConditionObj diff --git a/lua/luasnip/extras/fmt.lua b/lua/luasnip/extras/fmt.lua index 62cf9aff7..bde5015c3 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 @@ -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,30 +162,34 @@ 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 diff --git a/lua/luasnip/extras/init.lua b/lua/luasnip/extras/init.lua index c4be557d5..a5fdaa5e1 100644 --- a/lua/luasnip/extras/init.lua +++ b/lua/luasnip/extras/init.lua @@ -116,11 +116,18 @@ end return { lambda = lambda, match = match, - -- repeat a node. - rep = function(node_indx) + --- Repeat a node, by inserting the text of the passed node. + --- + --- ```lua + --- s("extras4", { i(1), t { "", "" }, extras.rep(1) }) + --- ``` + --- + ---@param node_ref LuaSnip.NodeRef a single [Node Reference](../../../DOC.md#node-reference). + ---@return LuaSnip.FunctionNode + rep = function(node_ref) return F(function(args) return args[1] - end, node_indx) + end, node_ref) end, -- Insert the output of a function. partial = function(func, ...) diff --git a/lua/luasnip/init.lua b/lua/luasnip/init.lua index 734c84b51..adfa9c0dc 100644 --- a/lua/luasnip/init.lua +++ b/lua/luasnip/init.lua @@ -61,6 +61,10 @@ local function api_leave() session.luasnip_changedtick = nil end +---@generic T: any? +---@param fn (fun(...))|(fun(...): T) +---@param ... any +---@return T local function api_do(fn, ...) api_enter() @@ -523,6 +527,7 @@ end ---@field jump_into_func? (fun(snip: LuaSnip.Snippet): LuaSnip.Node) --- Callback responsible for jumping into the snippet. The returned node is --- set as the new active node, i.e. it is the origin of the next jump. +--- --- The default is basically this: --- ```lua --- function(snip) @@ -531,15 +536,14 @@ end --- return snip:jump_into(1) --- end --- ``` ---- while this can be used to insert the snippet and immediately move the cursor ---- at the `i(0)`: +--- while this can be used to insert the snippet and immediately move the +--- cursor at the `i(0)`: --- ```lua --- function(snip) --- return snip.insert_nodes[0] --- end --- ``` ---This can also use `jump_into_func`. ---@class LuaSnip.Opts.SnipExpand: LuaSnip.Opts.Expand --- ---@field clear_region? LuaSnip.BufferRegion A region of text to clear after @@ -590,8 +594,12 @@ end --- } --- ``` +---@param snippet LuaSnip.Snippet +---@param opts? LuaSnip.Opts.SnipExpand +---@return LuaSnip.ExpandedSnippet local function _snip_expand(snippet, opts) local snip = snippet:copy() + ---@cast snip LuaSnip.ExpandedSnippet opts = opts or {} opts.expand_params = opts.expand_params or {} @@ -681,9 +689,8 @@ function API.snip_expand(snippet, opts) end ---Find a snippet matching the current cursor-position. ----@param opts table: may contain: ---- - `jump_into_func`: passed through to `snip_expand`. ----@return boolean: whether a snippet was expanded. +---@param opts? LuaSnip.Opts.Expand +---@return boolean _ Whether a snippet was expanded. local function _expand(opts) local expand_params local snip @@ -740,6 +747,7 @@ function API.expand_auto() local snip, expand_params = match_snippet(util.get_current_line_to_cursor(), "autosnippets") if snip then + ---@cast expand_params -nil local cursor = util.get_cursor_0ind() local clear_region = expand_params.clear_region or { diff --git a/lua/luasnip/loaders/from_lua.lua b/lua/luasnip/loaders/from_lua.lua index 8900921ed..925e4b8bc 100644 --- a/lua/luasnip/loaders/from_lua.lua +++ b/lua/luasnip/loaders/from_lua.lua @@ -102,6 +102,7 @@ local function _luasnip_load_file(file) log.error("Failed to load %s\n: %s", file, error_msg) error(string.format("Failed to load %s\n: %s", file, error_msg)) end + ---@cast func -nil -- the loaded file may add snippets to these tables, they'll be -- combined with the snippets returned regularly. diff --git a/lua/luasnip/loaders/util.lua b/lua/luasnip/loaders/util.lua index 4c0156071..91f8d1736 100644 --- a/lua/luasnip/loaders/util.lua +++ b/lua/luasnip/loaders/util.lua @@ -87,7 +87,7 @@ end ---Get paths of .snippets files ---@param root string @snippet directory path ----@return table @keys are file types, values are paths +---@return table _ @keys are file types, values are paths local function get_ft_paths(root, extension) local ft_path = {} local files, dirs = Path.scandir(root) @@ -155,7 +155,7 @@ end --- directory named `rtp_dirname` in the runtimepath. ---@param extension string: extension of valid snippet-files for the given --- collection (eg `.lua` or `.snippets`) ----@return table: a list of tables, each of the inner tables contains two +---@return table _ a list of tables, each of the inner tables contains two --- entries: --- - collection_paths: ft->files for the entire collection and --- - load_paths: ft->files for only the files that should be loaded. diff --git a/lua/luasnip/nodes/absolute_indexer.lua b/lua/luasnip/nodes/absolute_indexer.lua index 18546bdb0..ce964ae0b 100644 --- a/lua/luasnip/nodes/absolute_indexer.lua +++ b/lua/luasnip/nodes/absolute_indexer.lua @@ -1,5 +1,9 @@ -- absolute_indexer[0][1][2][3] -> { absolute_insert_position = {0,1,2,3} } +---@class LuaSnip.AbsoluteIndexer: {[integer]: LuaSnip.AbsoluteIndexer} +---@field absolute_insert_position integer[] + +---@return LuaSnip.AbsoluteIndexer local function new() return setmetatable({ absolute_insert_position = {}, diff --git a/lua/luasnip/nodes/choiceNode.lua b/lua/luasnip/nodes/choiceNode.lua index 1331e644c..7a8b0f84e 100644 --- a/lua/luasnip/nodes/choiceNode.lua +++ b/lua/luasnip/nodes/choiceNode.lua @@ -11,9 +11,17 @@ local feedkeys = require("luasnip.util.feedkeys") local log = require("luasnip.util.log").new("choice") ---@class LuaSnip.ChoiceNode.ItemNode: LuaSnip.Node +---@field choice LuaSnip.ChoiceNode +---@field next_choice LuaSnip.ChoiceNode.ItemNode +---@field prev_choice LuaSnip.ChoiceNode.ItemNode ---@class LuaSnip.ChoiceNode: LuaSnip.Node ---@field choices LuaSnip.ChoiceNode.ItemNode[] +---@field active_choice LuaSnip.ChoiceNode.ItemNode The active choice item node +---@field restore_cursor boolean Whether the cursor should be restored when +--- changing the current choice. +---@field absolute_position integer[] (note: init when a choice is set) +---@field absolute_insert_position integer[] (note: init when a choice is set) local ChoiceNode = Node:new() function ChoiceNode:init_nodes() @@ -53,7 +61,7 @@ end --- ``` --- Consider passing this override into `snip_env`. --- ----@field node_callbacks? {["change_choice"|"enter"|"leave"]: fun(node:LuaSnip.Node)} +---@field node_callbacks? {[LuaSnip.EventType]: fun(node:LuaSnip.Node)} --- Specify functions to call after changing the choice, or entering or leaving --- the node. The callback receives the `node` the callback was called on. @@ -66,7 +74,8 @@ end --- `nodes` have to contain an `insertNode` (e.g. `i(1)`). Using an `insertNode` --- or `textNode` directly as a choice is also fine, the latter is special-cased --- to have a jump-point at the beginning of its text. ----@param pos integer Jump-index of the node. +--- +---@param pos integer? Jump-index of the node. --- (See [Basics-Jump-Index](../../../DOC.md#jump-index)) --- ---@param choices (LuaSnip.Node|LuaSnip.Node[])[] A list of nodes that can be @@ -75,13 +84,13 @@ end --- Jumpable nodes that generally need a jump-index don't need one when used as --- a choice since they inherit the choiceNode's jump-index anyway. --- ----@param opts? LuaSnip.Opts.ChoiceNode Additional optional arguments. +---@param node_opts? LuaSnip.Opts.ChoiceNode Additional optional arguments. ---@return LuaSnip.ChoiceNode -function ChoiceNode.C(pos, choices, opts) - opts = opts or {} - if opts.restore_cursor == nil then +function ChoiceNode.C(pos, choices, node_opts) + node_opts = node_opts or {} + if node_opts.restore_cursor == nil then -- disable by default, can affect performance. - opts.restore_cursor = false + node_opts.restore_cursor = false end -- allow passing table of nodes in choices, will be turned into a @@ -101,8 +110,9 @@ function ChoiceNode.C(pos, choices, opts) mark = nil, dependents = {}, -- default to true. - restore_cursor = opts.restore_cursor, - }, opts) + restore_cursor = node_opts.restore_cursor, + }, node_opts) + ---@cast c LuaSnip.ChoiceNode c:init_nodes() return c end @@ -111,8 +121,13 @@ extend_decorator.register(ChoiceNode.C, { arg_indx = 3 }) function ChoiceNode:subsnip_init() for _, choice in ipairs(self.choices) do choice.parent = self.parent - -- only insertNode needs this. - if choice.type == 2 or choice.type == 1 or choice.type == 3 then + -- only insertNode needs this. (?) + if + vim.tbl_contains( + { types.textNode, types.insertNode, types.functionNode }, + choice.type + ) + then choice.pos = self.pos end end @@ -139,6 +154,8 @@ function ChoiceNode:make_args_absolute() for _, choice in ipairs(self.choices) do -- relative to choiceNode!! + ---(allowed: this arg only exists for some node types) + ---@diagnostic disable-next-line: redundant-parameter choice:make_args_absolute(self.absolute_insert_position) end @@ -258,6 +275,7 @@ end function ChoiceNode:setup_choice_jumps() end +---@return LuaSnip.Node? function ChoiceNode:find_node(predicate, opts) if self.active_choice then if predicate(self.active_choice) then @@ -354,7 +372,12 @@ function ChoiceNode:copy() local o = vim.deepcopy(self) for i, node in ipairs(self.choices) do if node.type == types.snippetNode or node.type == types.choiceNode then - o.choices[i] = node:copy() + ---@diagnostic disable-next-line: cast-type-mismatch (not smart enough..) + ---@cast node LuaSnip.SnippetNode|LuaSnip.ChoiceNode + local nodecopy = node:copy() + ---@diagnostic disable-next-line: cast-type-mismatch (not smart enough..) + ---@cast nodecopy LuaSnip.ChoiceNode.ItemNode + o.choices[i] = nodecopy else setmetatable(o.choices[i], getmetatable(node)) end diff --git a/lua/luasnip/nodes/dynamicNode.lua b/lua/luasnip/nodes/dynamicNode.lua index 23c77f4be..e2fd700b1 100644 --- a/lua/luasnip/nodes/dynamicNode.lua +++ b/lua/luasnip/nodes/dynamicNode.lua @@ -1,4 +1,3 @@ -local DynamicNode = require("luasnip.nodes.node").Node:new() local util = require("luasnip.util.util") local node_util = require("luasnip.nodes.util") local Node = require("luasnip.nodes.node").Node @@ -12,20 +11,90 @@ local log = require("luasnip.util.log").new("dynamicNode") local describe = require("luasnip.util.log").describe local session = require("luasnip.session") -local function D(pos, fn, args, opts) - opts = opts or {} - - return DynamicNode:new({ +---@class LuaSnip.SnippetNodeForDynNode: LuaSnip.SnippetNode +---@field old_state? any +---@field dynamicNode? LuaSnip.DynamicNode + +---@alias LuaSnip.DynamicNode.Fn fun(args: (string[])[], parent: LuaSnip.Snippet | LuaSnip.SnippetNode, old_state?: table, ...: any): LuaSnip.SnippetNode + +---@class LuaSnip.DynamicNode: LuaSnip.Node +---@field fn LuaSnip.DynamicNode.Fn +---@field user_args any[] Additional args that will be passed to `fn` +---@field args LuaSnip.NodeRef[] +---@field snip? LuaSnip.SnippetNodeForDynNode +---@field static_snip? LuaSnip.SnippetNodeForDynNode +---@field last_static_args? (string[])[] +---@field snippetstring_args boolean +local DynamicNode = Node:new() + +---@class LuaSnip.Opts.DynamicNode: LuaSnip.Opts.FunctionNode +---@field snippetstring_args? boolean (FIXME: not documented?) + +--- Very similar to functionNode, but returns a snippetNode instead of just text, +--- which makes them very powerful as parts of the snippet can be changed based on +--- user input. +--- +---@param pos integer? Just like all jumpable nodes, its' position in the +--- jump-list ([Basics-Jump-Index](#jump-index)). +--- +---@param fn LuaSnip.DynamicNode.Fn +--- This function is called when the argnodes' text changes. +--- It should generate and return (wrapped inside a `snippetNode`) nodes, which +--- will be inserted at the dynamicNode's place. +--- +--- note: `args`, `parent` and `user_args` are also explained in +--- [FunctionNode](#functionnode) +--- +--- - `argnode_text`: The text currently contained in the argnodes +--- (e.g. `{{line1}, {line1, line2}}`). +--- +--- - `parent`: The immediate parent of the `functionNode`. It is included here +--- as it allows easy access to some information that could be useful in +--- functionNodes (see [Snippets-Data](#data) for some examples). +--- +--- Many snippets access the surrounding snippet just as `parent`, but if the +--- `functionNode` is nested within a `snippetNode`, the immediate parent is +--- a `snippetNode`, not the surrounding snippet (only the surrounding +--- snippet contains data like `env` or `captures`). +--- +--- - `old_state`: a user-defined table. +--- This table may contain anything; its intended usage is to preserve +--- information from the previously generated `snippetNode`. +--- If the `dynamicNode` depends on other nodes, it may be +--- reconstructed, which means all user input (text inserted in +--- `insertNodes`, changed choices) to the previous `dynamicNode` is lost. +--- +--- The `old_state` table must be stored in `snippetNode` returned by +--- the function (`snippetNode.old_state`). +--- The second example below illustrates the usage of `old_state`. +--- +--- - `user_args`: The `user_args` passed in `opts`. +--- Note that there may be multiple `user_args`. +--- (e.g. `user_args1, ..., user_argsn`) +--- +---@param argsnode_refs? LuaSnip.NodeRef[]|LuaSnip.NodeRef +--- [Node References](#node-reference) to the nodes the dynamicNode depends on. +--- Changing any of these will trigger a re-evaluation of `fn`, and the result will be inserted at the `dynamicNode`'s place. +--- (`dynamicNode` behaves exactly the same as `functionNode` in this regard) +--- +---@param node_opts? LuaSnip.Opts.DynamicNode +---@return LuaSnip.DynamicNode +local function D(pos, fn, argsnode_refs, node_opts) + node_opts = node_opts or {} + + local node = DynamicNode:new({ pos = pos, fn = fn, - args = node_util.wrap_args(args), + args = node_util.wrap_args(argsnode_refs or {}), type = types.dynamicNode, mark = nil, - user_args = opts.user_args or {}, - snippetstring_args = opts.snippetstring_args or false, + user_args = node_opts.user_args or {}, + snippetstring_args = node_opts.snippetstring_args or false, dependents = {}, active = false, - }, opts) + }, node_opts) + ---@cast node LuaSnip.DynamicNode + return node end extend_decorator.register(D, { arg_indx = 4 }) @@ -148,6 +217,8 @@ function DynamicNode:jump_into_snippet(no_move) return self:jump_into(1, no_move, false) end +-- FIXME(@bew): This should be on a `ExpandedDynamicNode` class? +-- To have access to `next`/`prev` (that would go on a `ExpandedNode` 🤔) function DynamicNode:update() local args = self:get_args() local str_args = node_util.str_args(args) @@ -194,8 +265,13 @@ function DynamicNode:update() -- build new snippet before exiting, markers may be needed for -- construncting. - tmp = - self.fn(effective_args, self.parent, old_state, unpack(self.user_args)) + tmp = self.fn( + effective_args or {}, + self.parent, + old_state, + unpack(self.user_args) + ) + ---@cast tmp LuaSnip.SnippetNodeForDynNode if self.snip then self.snip:exit() @@ -332,6 +408,8 @@ function DynamicNode:update_static() -- set empty snippet on failure tmp = SnippetNode(nil, {}) end + ---@cast tmp LuaSnip.SnippetNodeForDynNode + self.last_static_args = str_args -- act as if snip is directly inside parent. @@ -419,6 +497,7 @@ function DynamicNode:update_restore() and vim.deep_equal(str_args, self.last_args) then local tmp = self.snip + ---@cast tmp -nil -- position might (will probably!!) still have changed, so update it -- here too (as opposed to only in update). @@ -498,7 +577,7 @@ end DynamicNode.make_args_absolute = FunctionNode.make_args_absolute DynamicNode.set_dependents = FunctionNode.set_dependents -function DynamicNode:resolve_position(position, static) +function DynamicNode:resolve_position(_position, static) -- position must be 0, there are no other options. if static then return self.static_snip diff --git a/lua/luasnip/nodes/functionNode.lua b/lua/luasnip/nodes/functionNode.lua index f9224712c..1e0a6f74d 100644 --- a/lua/luasnip/nodes/functionNode.lua +++ b/lua/luasnip/nodes/functionNode.lua @@ -1,5 +1,4 @@ local Node = require("luasnip.nodes.node").Node -local FunctionNode = Node:new() local util = require("luasnip.util.util") local node_util = require("luasnip.nodes.util") local types = require("luasnip.util.types") @@ -9,16 +8,104 @@ local key_indexer = require("luasnip.nodes.key_indexer") local opt_args = require("luasnip.nodes.optional_arg") local snippet_string = require("luasnip.nodes.util.snippet_string") -local function F(fn, args, opts) - opts = opts or {} +---@alias LuaSnip.FunctionNode.Fn fun(args: (string[])[], parent: LuaSnip.Snippet | LuaSnip.SnippetNode, ...: table): string|string[] + +---@class LuaSnip.FunctionNode: LuaSnip.Node +---@field fn LuaSnip.FunctionNode.Fn +---@field user_args any[] Additional args that will be passed to `fn` +---@field args LuaSnip.NodeRef[] +---@field args_absolute LuaSnip.NormalizedNodeRef[] +---@field last_args ((string[])[])? +local FunctionNode = Node:new() - return FunctionNode:new({ +---@class LuaSnip.Opts.FunctionNode: LuaSnip.Opts.Node +---@field user_args? any[] Additional args that will be passed to `fn` as +--- `user_arg1`-`user_argn`. +--- +--- These make it easier to reuse similar functions, for example a functionNode +--- that wraps some text in different delimiters (`()`, `[]`, ...). +--- ```lua +--- local function reused_func(_,_, user_arg1) +--- return user_arg1 +--- end +--- +--- s("trig", { +--- f(reused_func, {}, { +--- user_args = {"text"} +--- }), +--- f(reused_func, {}, { +--- user_args = {"different text"} +--- }), +--- }) +--- ``` + +--- Function Nodes insert text based on the content of other nodes using a +--- user-defined function: +--- +--- ```lua +--- local function fn( +--- args, -- text from i(2) in this example i.e. { { "456" } } +--- parent, -- parent snippet or parent node +--- user_args -- user_args from opts.user_args +--- ) +--- return '[' .. args[1][1] .. user_args .. ']' +--- end +--- +--- s("trig", { +--- i(1), t '<-i(1) ', +--- f(fn, -- callback (args, parent, user_args) -> string +--- {2}, -- node indice(s) whose text is passed to fn, i.e. i(2) +--- { user_args = { "user_args_value" }} -- opts +--- ), +--- t ' i(2)->', i(2), t '<-i(2) i(0)->', i(0) +--- }) +--- ``` +--- +---@param fn LuaSnip.FunctionNode.Fn +--- +--- - `argnode_text`: The text currently contained in the argnodes +--- (e.g. `{{line1}, {line1, line2}}`). +--- The snippet indent will be removed from all lines following the first. +--- +--- - `parent`: The immediate parent of the `functionNode`. It is included here +--- as it allows easy access to some information that could be useful in +--- functionNodes (see [Snippets-Data](#data) for some examples). +--- +--- Many snippets access the surrounding snippet just as `parent`, but if the +--- `functionNode` is nested within a `snippetNode`, the immediate parent is +--- a `snippetNode`, not the surrounding snippet (only the surrounding +--- snippet contains data like `env` or `captures`). +--- +--- - `user_args`: The `user_args` passed in `opts`. Note that there may be +--- multiple `user_args` (e.g. `user_args1, ..., user_argsn`). +--- +--- The function shall return a string, which will be inserted as is, or a +--- table of strings for multiline strings, where all lines following the first +--- will be prefixed with the snippets' indentation. +--- +---@param argsnode_refs? LuaSnip.NodeRef[]|LuaSnip.NodeRef +--- [Node References](#node-reference) to the nodes the functionNode depends +--- on. +--- Changing any of these will trigger a re-evaluation of `fn`, and insertion of +--- the updated text. +--- If no node reference is passed, the `functionNode` is evaluated once upon +--- expansion. +--- +---@param node_opts? LuaSnip.Opts.FunctionNode +---@return LuaSnip.FunctionNode +local function F(fn, argsnode_refs, node_opts) + node_opts = node_opts or {} + + local node = FunctionNode:new({ fn = fn, - args = node_util.wrap_args(args), + args = node_util.wrap_args(argsnode_refs or {}), + args_absolute = {}, type = types.functionNode, mark = nil, - user_args = opts.user_args or {}, - }, opts) + user_args = node_opts.user_args or {}, + }, node_opts) + ---@cast node LuaSnip.FunctionNode + return node end extend_decorator.register(F, { arg_indx = 3 }) @@ -119,8 +206,8 @@ function FunctionNode:indent(_) end function FunctionNode:expand_tabs(_) end function FunctionNode:make_args_absolute(position_so_far) - self.args_absolute = {} - node_util.make_args_absolute(self.args, position_so_far, self.args_absolute) + self.args_absolute = + node_util.make_args_absolute(self.args, position_so_far) end function FunctionNode:set_dependents() diff --git a/lua/luasnip/nodes/insertNode.lua b/lua/luasnip/nodes/insertNode.lua index a379e4ba3..fcabd0482 100644 --- a/lua/luasnip/nodes/insertNode.lua +++ b/lua/luasnip/nodes/insertNode.lua @@ -1,6 +1,5 @@ -local Node = require("luasnip.nodes.node") -local InsertNode = Node.Node:new() -local ExitNode = InsertNode:new() +local node_mod = require("luasnip.nodes.node") +local Node = node_mod.Node local util = require("luasnip.util.util") local node_util = require("luasnip.nodes.util") local types = require("luasnip.util.types") @@ -12,10 +11,88 @@ local str_util = require("luasnip.util.str") local log = require("luasnip.util.log").new("insertNode") local session = require("luasnip.session") -local function I(pos, static_text, opts) +---@class LuaSnip.InsertNode: LuaSnip.Node +---@field static_text LuaSnip.SnippetString +---@field inner_active boolean +---@field input_active boolean +local InsertNode = Node:new() + +---@class LuaSnip.ExitNode: LuaSnip.InsertNode +local ExitNode = InsertNode:new() + +--- These Nodes contain editable text and can be jumped to- and from (e.g. +--- traditional placeholders and tabstops, like `$1` in TextMate-snippets). +--- +--- The functionality is best demonstrated with an example: +--- +--- ```lua +--- s("trigger", { +--- t({"After expanding, the cursor is here ->"}), i(1), +--- t({"", "After jumping forward once, cursor is here ->"}), i(2), +--- t({"", "After jumping once more, the snippet is exited there ->"}), i(0), +--- }) +--- ``` +--- +--- +--- +--- The Insert Nodes are visited in order `1,2,3,..,n,0`. +--- (The jump-index 0 also _has_ to belong to an `insertNode`!) +--- So the order of InsertNode-jumps is as follows: +--- +--- 1. After expansion, the cursor is at InsertNode 1, +--- 2. after jumping forward once at InsertNode 2, +--- 3. and after jumping forward again at InsertNode 0. +--- +--- If no 0-th InsertNode is found in a snippet, one is automatically inserted +--- after all other nodes. +--- +--- The jump-order doesn't have to follow the "textual" order of the nodes: +--- ```lua +--- s("trigger", { +--- t({"After jumping forward once, cursor is here ->"}), i(2), +--- t({"", "After expanding, the cursor is here ->"}), i(1), +--- t({"", "After jumping once more, the snippet is exited there ->"}), i(0), +--- }) +--- ``` +--- The above snippet will behave as follows: +--- +--- 1. After expansion, we will be at InsertNode 1. +--- 2. After jumping forward, we will be at InsertNode 2. +--- 3. After jumping forward again, we will be at InsertNode 0. +--- +--- An **important** (because here Luasnip differs from other snippet engines) detail +--- is that the jump-indices restart at 1 in nested snippets: +--- ```lua +--- s("trigger", { +--- i(1, "First jump"), +--- t(" :: "), +--- sn(2, { +--- i(1, "Second jump"), +--- t" : ", +--- i(2, "Third jump") +--- }) +--- }) +--- ``` +--- +--- +--- +--- as opposed to e.g. the TextMate syntax, where tabstops are snippet-global: +--- ```snippet +--- ${1:First jump} :: ${2: ${3:Third jump} : ${4:Fourth jump}} +--- ``` +--- (this is not exactly the same snippet of course, but as close as possible) +--- (the restart-rule only applies when defining snippets in Lua, the above +--- TextMate-snippet will expand correctly when parsed). +--- +---@param pos integer? Jump-index of the node. +---@param static_text? string|LuaSnip.SnippetString +---@param node_opts? LuaSnip.Opts.Node +---@return LuaSnip.InsertNode|LuaSnip.ExitNode +local function I(pos, static_text, node_opts) if not snippet_string.isinstance(static_text) then static_text = snippet_string.new(util.to_string_table(static_text)) end + ---@cast static_text LuaSnip.SnippetString local node if pos == 0 then @@ -28,7 +105,7 @@ local function I(pos, static_text, opts) ext_gravities_active = { false, false }, inner_active = false, input_active = false, - }, opts) + }, node_opts) else node = InsertNode:new({ pos = pos, @@ -37,8 +114,9 @@ local function I(pos, static_text, opts) type = types.insertNode, inner_active = false, input_active = false, - }, opts) + }, node_opts) end + ---@cast node LuaSnip.InsertNode|LuaSnip.ExitNode -- make static text owned by this insertNode. -- This includes copying it so that it is separate from the snippets that @@ -86,7 +164,7 @@ function ExitNode:focus() rrgrav = true end - Node.focus_node(self, lrgrav, rrgrav) + node_mod.focus_node(self, lrgrav, rrgrav) end function ExitNode:input_leave(no_move, dry_run) @@ -387,14 +465,7 @@ function InsertNode:get_snippetstring() snippetstring:append_text( str_util.multiline_substr(text, current, snip_from_base_rel) ) - snippetstring:append_snip( - snip, - str_util.multiline_substr( - text, - snip_from_base_rel, - snip_to_base_rel - ) - ) + snippetstring:append_snip(snip) current = snip_to_base_rel end end diff --git a/lua/luasnip/nodes/key_indexer.lua b/lua/luasnip/nodes/key_indexer.lua index 04e6f7f38..868c90f8a 100644 --- a/lua/luasnip/nodes/key_indexer.lua +++ b/lua/luasnip/nodes/key_indexer.lua @@ -1,10 +1,18 @@ local M = {} +---@class LuaSnip.KeyIndexer +---@field key string + local key_mt = {} + +--- Create a key indexer +---@param key string +---@return LuaSnip.KeyIndexer function M.new_key(key) return setmetatable({ key = key }, key_mt) end +---@return boolean function M.is_key(t) return getmetatable(t) == key_mt end diff --git a/lua/luasnip/nodes/multiSnippet.lua b/lua/luasnip/nodes/multiSnippet.lua index 108206ea5..565a042de 100644 --- a/lua/luasnip/nodes/multiSnippet.lua +++ b/lua/luasnip/nodes/multiSnippet.lua @@ -2,6 +2,10 @@ local snip_mod = require("luasnip.nodes.snippet") local node_util = require("luasnip.nodes.util") local extend_decorator = require("luasnip.util.extend_decorator") +---@class LuaSnip.VirtualSnippet: LuaSnip.NormalizedSnippetContext +---@field id? integer Internal ID of this snippet (used for source mapping) +--- (note: this is part of LuaSnip.Addable, which is present on LuaSnip.MultiSnippet) +---@field snippet LuaSnip.BareInternalSnippet local VirtualSnippet = {} local VirtualSnippet_mt = { __index = VirtualSnippet } @@ -10,6 +14,8 @@ function VirtualSnippet:get_docstring() end function VirtualSnippet:copy() local copy = self.snippet:copy() + ---@diagnostic disable-next-line: cast-type-mismatch + ---@cast copy LuaSnip.VirtualSnippet copy.id = self.id return copy @@ -22,12 +28,14 @@ VirtualSnippet.invalidate = snip_mod.Snippet.invalidate ---Create new virtual snippet, ie. an object which is capable of performning ---all the functions expected from a snippet which is yet to be expanded ---(`matches`,`get_docstring`,`invalidate`,`retrieve_all`,`copy`) ----@param context context as defined for snippet-constructor. Table, not nil. ----@param snippet The snippet this virtual snippet will return on `copy`, also not nil. ----@param opts opts as defined for snippet-constructor. Has to be a table, may be empty. +---@param context LuaSnip.SnipContext The context, as defined for snippet-constructor. +---@param snippet LuaSnip.BareInternalSnippet The snippet this virtual snippet will return on `copy`. +---@param opts LuaSnip.Opts.Snippet|table Snippet options as defined for snippet-constructor. Has to be a table, may be empty. +---@return LuaSnip.VirtualSnippet local function new_virtual_snippet(context, snippet, opts) -- init fields necessary for matches, invalidate, adding the snippet. local o = snip_mod.init_snippet_context(context, opts) + ---@cast o LuaSnip.VirtualSnippet o.snippet = snippet setmetatable(o, VirtualSnippet_mt) @@ -35,6 +43,8 @@ local function new_virtual_snippet(context, snippet, opts) return o end +---@class LuaSnip.MultiSnippet: LuaSnip.Addable +---@field v_snips LuaSnip.VirtualSnippet[] local MultiSnippet = {} local MultiSnippet_mt = { __index = MultiSnippet } @@ -42,6 +52,15 @@ function MultiSnippet:retrieve_all() return self.v_snips end +---@class LuaSnip.Opts.MultiSnippetContexts: LuaSnip.SnipContext[] +---@field common? LuaSnip.SnipContext + +---@param contexts LuaSnip.Opts.MultiSnippetContexts +---@param snippet LuaSnip.BareInternalSnippet +--- (FIXME: this will never be a full LuaSnip.Snippet, so we can't really +--- annotate it as such 🤔) +---@param snippet_opts LuaSnip.Opts.Snippet +---@return LuaSnip.MultiSnippet local function multisnippet_from_snippet_obj(contexts, snippet, snippet_opts) assert( type(contexts) == "table", @@ -49,6 +68,7 @@ local function multisnippet_from_snippet_obj(contexts, snippet, snippet_opts) ) local common_context = node_util.wrap_context(contexts.common) or {} + ---@type LuaSnip.VirtualSnippet[] local v_snips = {} for _, context in ipairs(contexts) do local complete_context = vim.tbl_extend( @@ -65,12 +85,15 @@ local function multisnippet_from_snippet_obj(contexts, snippet, snippet_opts) local o = { v_snips = v_snips, } - setmetatable(o, MultiSnippet_mt) return o end +---@param contexts LuaSnip.Opts.MultiSnippetContexts +---@param nodes LuaSnip.Node|LuaSnip.Node[] +---@param opts? {common_opts: LuaSnip.Opts.Snippet} +---@return LuaSnip.MultiSnippet local function multisnippet_from_nodes(contexts, nodes, opts) opts = opts or {} local common_snip_opts = opts.common_opts or {} diff --git a/lua/luasnip/nodes/node.lua b/lua/luasnip/nodes/node.lua index 40010ba50..8e6232c61 100644 --- a/lua/luasnip/nodes/node.lua +++ b/lua/luasnip/nodes/node.lua @@ -10,37 +10,65 @@ local snippet_string = require("luasnip.nodes.util.snippet_string") local log = require("luasnip.util.log").new("node") local describe = require("luasnip.util.log").describe ----@class LuaSnip.Node +---@class LuaSnip.NormalizedNodeOpts ---@field key? any Key to identify the node with. ----@field store_id? number May be set when the node is used to store/restore. ----A generic node. +---@field node_ext_opts LuaSnip.NodeExtOpts +---@field merge_node_ext_opts boolean +---@field node_callbacks {[LuaSnip.EventType]: fun(node:LuaSnip.Node)} + +---@class LuaSnip.Node: LuaSnip.NormalizedNodeOpts +---@field pos? integer Jump-index of the node +---@field store_id? number May be set when the node is used to store/restore a +--- generic node. ---@field mark? LuaSnip.Mark The mark associated with this node. ----@field type number Identifies the type of the snippet. +---@field active? boolean +---@field type LuaSnip.NodeType Identifies the type of the snippet. ---@field next LuaSnip.Node Link to the next node in jump-order. ---@field prev LuaSnip.Node Link to the previous node in jump-order. +---@field parent LuaSnip.Snippet|LuaSnip.SnippetNode The parent snippet or +--- snippet node. +---@field indx integer Index of the node in the snippet or snippet node. +--- +---(FIXME(@L3MON4D3): Document these) +---@field visible boolean +---@field static_text string[] +---@field static_visible boolean +---@field visited boolean +---@field old_text ... (?) local Node = {} ----@alias LuaSnip.NodeExtOpts {["active"|"passive"|"visited"|"unvisited"|"snippet_passive"]: vim.api.keyset.set_extmark} - ----@class LuaSnip.Opts.Node ----@field node_ext_opts LuaSnip.NodeExtOpts? Pass these opts through to the ----underlying extmarks representing the node. Notably, this enables highlighting ----the nodes, and allows the highlight to be different based on the state of the ----node/snippet. See [ext_opts](../../../DOC.md#ext_opts) ----@field merge_node_ext_opts boolean? Whether to use the parents' `ext_opts` to ----compute this nodes' `ext_opts`. ----@field key any? Some unique value (strings seem useful) to identify this ----node. ----This is useful for [Key Indexer](../../../DOC.md#key-indexer) or for finding the node at ----runtime (See [Snippets-API](../../../DOC.md#snippets-api) ----These keys don't have to be unique across the entire lifetime of the snippet, ----but every key should occur only once at the same time. This means it is fine ----to return a keyed node from a dynamicNode, because even if it will be ----generated multiple times, the same key not occur twice at the same time. ----@field node_callbacks {["enter"|"leave"]: fun(node:LuaSnip.Node)} ----Specify functions to call after changing the choice, or entering or leaving ----the node. The callback receives the `node` the callback was called on. - +---@class LuaSnip.NodeExtOpts: {["active"|"passive"|"visited"|"unvisited"|"snippet_passive"]: vim.api.keyset.set_extmark} +--- Extmark options by node state. + +---@class LuaSnip.ChildExtOpts: {[LuaSnip.NodeType]: LuaSnip.NodeExtOpts} +--- Extmark options by node state, by node type. + +---@class LuaSnip.Opts.Node Common options for nodes +--- +---@field node_ext_opts? LuaSnip.NodeExtOpts Pass these opts through to the +--- underlying extmarks representing the node. Notably, this enables highlighting +--- the nodes, and allows the highlight to be different based on the state of the +--- node/snippet. See [ext_opts](../../../DOC.md#ext_opts) +--- +---@field merge_node_ext_opts? boolean Whether to use the parents' `ext_opts` to +--- compute this nodes' `ext_opts`. +--- +---@field key? any Some unique value (strings seem useful) to identify this +--- node. +--- This is useful for [Key Indexer](../../../DOC.md#key-indexer) or for finding the node at +--- runtime (See [Snippets-API](../../../DOC.md#snippets-api) +--- These keys don't have to be unique across the entire lifetime of the snippet, +--- but every key should occur only once at the same time. This means it is fine +--- to return a keyed node from a dynamicNode, because even if it will be +--- generated multiple times, the same key not occur twice at the same time. +--- +---@field node_callbacks? {[LuaSnip.EventType]: fun(node:LuaSnip.Node)} +--- Specify functions to call after changing the choice, or entering or leaving +--- the node. The callback receives the `node` the callback was called on. + +---@param o? table +---@param opts? LuaSnip.Opts.Node +---@return LuaSnip.Node function Node:new(o, opts) o = o or {} @@ -138,6 +166,7 @@ function Node:jumpable(dir) end end +---@return string[]? function Node:get_text() if not self.visible then return nil @@ -163,11 +192,13 @@ function Node:get_text() return ok and text or { "" } end +---@return LuaSnip.SnippetString function Node:get_snippetstring() -- if this is not overridden, get_text returns a multiline string. return snippet_string.new(self:get_text()) end +---@return LuaSnip.SnippetString function Node:get_static_snippetstring() -- if this is not overridden, get_static_text() is a multiline string. return snippet_string.new(self:get_static_text()) @@ -223,6 +254,12 @@ function Node:init_insert_positions(position_so_far) self.absolute_insert_position = vim.deepcopy(position_so_far) end +--- Run event handlers for the given event type. +--- - runs node callback if defined +--- - runs parent callback for the node if defined +--- - fires `User LuaSnip` autocmd +--- +---@param event LuaSnip.EventType function Node:event(event) local node_callback = self.node_callbacks[event] if node_callback then @@ -231,11 +268,9 @@ function Node:event(event) -- try to get the callback from the parent. if self.pos then - -- node needs position to get callback (nodes may not have position if - -- defined in a choiceNode, ie. c(1, { - -- i(nil, {"works!"}) - -- })) - -- works just fine. + -- The node needs position to get callback + -- (nodes may not have position if defined in a choiceNode) + -- ie. `c(1, { i(nil, {"works!"}) }))` works just fine. local parent_callback = self.parent.callbacks[self.pos][event] if parent_callback then parent_callback(self) @@ -249,6 +284,7 @@ function Node:event(event) }) end +---@return string[]? local function get_args(node, get_text_func_name, static) local argnodes_text = {} for key, arg in ipairs(node.args_absolute) do @@ -313,9 +349,11 @@ local function get_args(node, get_text_func_name, static) return argnodes_text end +---@return string[]? function Node:get_args() return get_args(self, "argnode_text", false) end +---@return string[]? function Node:get_static_args() return get_args(self, "get_static_snippetstring", true) end @@ -339,13 +377,14 @@ function Node:store() end function Node:update_restore() end -- find_node only needs to check children, self is checked by the parent. -function Node:find_node() +---@return LuaSnip.Node? +function Node:find_node(_predicate, _opts) return nil end Node.ext_gravities_active = { false, true } -function Node:insert_to_node_absolute(position) +function Node:insert_to_node_absolute(_position) -- this node is a leaf, just return its position return self.absolute_position end @@ -401,7 +440,8 @@ function Node:resolve_node_ext_opts(base_prio, parent_ext_opts) ) end -function Node:is_interactive() +---@param info any (note: this is used in ast_parser) +function Node:is_interactive(info) -- safe default. return true end @@ -604,6 +644,8 @@ end function Node:interactive() -- interactive if immediately inside choiceNode. return vim.tbl_contains({ types.insertNode, types.exitNode }, self.type) + ---(allowed: exists only in ChoiceNode) + ---@diagnostic disable-next-line: undefined-field or self.choice ~= nil end function Node:leaf() @@ -660,6 +702,7 @@ function Node:subtree_do(opts) opts.post(self) end +---@return LuaSnip.Snippet function Node:get_snippet() return self.parent.snippet end @@ -668,6 +711,7 @@ end -- those that don't. function Node:subtree_leave_entered() end +---@return LuaSnip.SnippetString function Node:argnode_text() return self:get_snippetstring() end diff --git a/lua/luasnip/nodes/optional_arg.lua b/lua/luasnip/nodes/optional_arg.lua index fd54fa195..d31c8178d 100644 --- a/lua/luasnip/nodes/optional_arg.lua +++ b/lua/luasnip/nodes/optional_arg.lua @@ -1,10 +1,18 @@ local M = {} +---@class LuaSnip.OptionalNodeRef +---@field ref LuaSnip.NodeRef + local opt_mt = {} + +--- Create an optional node ref +---@param ref LuaSnip.NodeRef +---@return LuaSnip.OptionalNodeRef function M.new_opt(ref) return setmetatable({ ref = ref }, opt_mt) end +---@return boolean function M.is_opt(t) return getmetatable(t) == opt_mt end diff --git a/lua/luasnip/nodes/snippet.lua b/lua/luasnip/nodes/snippet.lua index 933556dd7..38b414e17 100644 --- a/lua/luasnip/nodes/snippet.lua +++ b/lua/luasnip/nodes/snippet.lua @@ -21,6 +21,9 @@ local true_func = function() return true end +---@param condition LuaSnip.SnipContext.Condition +---@param user_resolve LuaSnip.ResolveExpandParamsFn +---@return LuaSnip.ResolveExpandParamsFn local generate_resolve_expand_params_func = function(condition, user_resolve) return function(self, line_to_cursor, match, captures) if condition then @@ -29,6 +32,7 @@ local generate_resolve_expand_params_func = function(condition, user_resolve) end end + ---@type LuaSnip.ExpandParams local default_expand_params = { trigger = match, captures = captures, @@ -56,31 +60,76 @@ local callbacks_mt = { -- declare SN here, is needed in metatable. local SN -local stored_mt = { - __index = function(table, key) - -- default-node is just empty text. - local val = SN(nil, { iNode.I(1) }) - val.is_default = true - rawset(table, key, val) - return val - end, -} - ----@class LuaSnip.Snippet: LuaSnip.Addable, LuaSnip.ExpandedSnippet +---@class LuaSnip.BareInternalSnippet: LuaSnip.Node +--- To be used as a base for all snippet-like nodes (Snippet, SnippetProxy, ..) +--- ---@field nodes LuaSnip.Node[] +---@field insert_nodes LuaSnip.InsertNode[] +--- +---(FIXME(@bew): these fields are only for ExpandedSnippet?) +---@field snippet LuaSnip.Snippet +---@field dependents table (FIXME(@L3MON4D3): type/doc!) +---@field dependents_dict table (FIXME(@L3MON4D3): type/doc!) +---@field child_snippets table[] (FIXME(@L3MON4D3): type/doc!) +---@field static_text string[]? +---@field indentstr string local Snippet = node_mod.Node:new() +---@class LuaSnip.Snippet: LuaSnip.BareInternalSnippet, LuaSnip.NormalizedSnippetContext, LuaSnip.NormalizedSnippetOpts, LuaSnip.Addable +---@field _source? LuaSnip.Source +---@field node_store_id integer + -- very approximate classes, for now. ---@alias LuaSnip.SnippetID integer ---Anything that can be passed to ls.add_snippets(). ---@class LuaSnip.Addable +---@field id? integer Internal ID of this snippet (used for source mapping) +---@field retrieve_all (fun(self: LuaSnip.Addable): LuaSnip.Snippet[]) ---Represents an expanded snippet. ----@class LuaSnip.ExpandedSnippet: LuaSnip.Node - ----@class LuaSnip.SnippetNode: LuaSnip.Node - +---@class LuaSnip.ExpandedSnippet: LuaSnip.Snippet +---@field env table Variables used in the LSP-protocol +--- (e.g. `TM_CURRENT_LINE` or `TM_FILENAME`). +---@field trigger string The string that triggered this snipper. +--- Only interesting when the snippet was triggered with a non-"plain" +--- `trigEngine` for getting the full match. +---@field captures string[] The capture-groups when the snippet was triggered +--- with a non-"plain" `trigEngine`. +--- +---@field prev LuaSnip.Node +---@field next LuaSnip.Node + +---@class LuaSnip.NormalizedSnippetContext +---@field trigger string The trigger of the snippet +---@field name string +---@field description string[] +---@field dscr string[] Same as `description`, kept to avoid breaking downstream +--- usages. +---@field docstring? string[] +---@field priority? integer +---@field snippetType? "snippets"|"autosnippets" +---@field filetype? string +---@field wordTrig boolean +---@field hidden boolean +---@field regTrig boolean +---@field docTrig? string +---@field trig_matcher LuaSnip.SnipContext.TrigMatcher +---@field resolveExpandParams LuaSnip.ResolveExpandParamsFn +---@field show_condition LuaSnip.SnipContext.ShowCondition +---@field condition LuaSnip.SnipContext.Condition +---@field invalidated boolean + +---@class LuaSnip.NormalizedSnippetNodeOpts +---@field callbacks {[integer]: {[LuaSnip.EventType]: fun(node: LuaSnip.Node, event_args?: table)}} +---@field child_ext_opts LuaSnip.ChildExtOpts +---@field merge_child_ext_opts boolean + +---@class LuaSnip.NormalizedSnippetOpts: LuaSnip.NormalizedSnippetNodeOpts +---@field stored {[string]: LuaSnip.SnippetNode} + +-- FIXME(@bew): What is this for? (not documented..) +-- FIXME(@bew): Should be moved to its own file? (like the other indexers) local Parent_indexer = {} function Parent_indexer:new(o) @@ -103,9 +152,17 @@ local function P(indx) return Parent_indexer:new({ indx = indx }) end +-- TODO(@bew): Categorize each Snippet method, between: +-- - InitializedSnippet (created, not yet added) +-- - RegisteredSnippet (added in collection) +-- - ExpandedSnippet +-- - ..(?) + function Snippet:init_nodes() local insert_nodes = {} for i, node in ipairs(self.nodes) do + ---(allowed: A BareInternalSnippet will later be a full snippet) + ---@diagnostic disable-next-line: assign-type-mismatch node.parent = self node.indx = i if @@ -140,6 +197,8 @@ function Snippet:init_nodes() self.insert_nodes = insert_nodes end +---@param nodes LuaSnip.Node|LuaSnip.Node[] +---@return LuaSnip.SnippetNode local function wrap_nodes_in_snippetNode(nodes) if getmetatable(nodes) then -- is a node, not a table. @@ -151,7 +210,7 @@ local function wrap_nodes_in_snippetNode(nodes) return SN(nil, { nodes }) else -- is a snippetNode, wrapping it twice is unnecessary. - return nodes + return nodes ---@type LuaSnip.SnippetNode end else -- is a table of nodes. @@ -159,8 +218,11 @@ local function wrap_nodes_in_snippetNode(nodes) end end +---@param opts LuaSnip.Opts.SnippetNode +---@return LuaSnip.NormalizedSnippetNodeOpts local function init_snippetNode_opts(opts) - local in_node = {} + ---@type LuaSnip.NormalizedSnippetNodeOpts + local in_node = {} ---@diagnostic disable-line: missing-fields in_node.child_ext_opts = ext_util.child_complete(vim.deepcopy(opts.child_ext_opts or {})) @@ -178,34 +240,56 @@ local function init_snippetNode_opts(opts) return in_node end +local stored_mt = { + __index = function(table, key) + -- default-node is just empty text. + local val = SN(nil, { iNode.I(1) }) + val.is_default = true + rawset(table, key, val) + return val + end, +} + +---@param opts LuaSnip.Opts.Snippet +---@return LuaSnip.NormalizedSnippetOpts local function init_snippet_opts(opts) - local in_node = {} + ---@type LuaSnip.NormalizedSnippetOpts + local in_node = {} ---@diagnostic disable-line: missing-fields - -- return sn(t("")) for so-far-undefined keys. - in_node.stored = setmetatable(opts.stored or {}, stored_mt) + -- The metatable will return `sn(t(""))` for so-far-undefined keys. + in_node.stored = setmetatable({}, stored_mt) -- wrap non-snippetNode in snippetNode. - for key, nodes in pairs(in_node.stored) do + for key, nodes in pairs(opts.stored or {}) do in_node.stored[key] = wrap_nodes_in_snippetNode(nodes) end return vim.tbl_extend("error", in_node, init_snippetNode_opts(opts)) end --- context, opts non-nil tables. +---@param context LuaSnip.SnipContext +---@param opts LuaSnip.Opts.Snippet +---@return LuaSnip.NormalizedSnippetContext local function init_snippet_context(context, opts) - local effective_context = {} + ---@type LuaSnip.NormalizedSnippetContext + local effective_context = {} ---@diagnostic disable-line: missing-fields + + local given_trigger = context.trig + if not given_trigger then + error("Snippet trigger is not set!") + end + -- note: at this point `given_trigger` is guaranteed to be a string -- trig is set by user, trigger is used internally. -- not worth a breaking change, we just make it compatible here. - effective_context.trigger = context.trig + effective_context.trigger = given_trigger - effective_context.name = context.name or context.trig + effective_context.name = context.name or given_trigger -- context.{desc,dscr} could be nil, string or table. -- (defaults to trigger) effective_context.description = - util.to_line_table(context.desc or context.dscr or context.trig) + util.to_line_table(context.desc or context.dscr or given_trigger) -- (keep dscr to avoid breaking downstream usages) effective_context.dscr = effective_context.description @@ -263,7 +347,12 @@ local function init_snippet_context(context, opts) util.ternary(context.regTrig ~= nil, "pattern", "plain") ) engine = trig_engines[engine_name] + if not engine then + error("Unknown trigEngine '" .. engine_name .. "'") + end end + ---@cast engine -nil (We know it's valid here) + -- make sure to pass through nil-trigEngineOpts, they will be recognized and -- we will get a default-version of that function instead of generating a -- curried (?) version of it (which would waste space I think). @@ -303,6 +392,10 @@ end -- Create snippet without initializing opts+context. -- this might be called from snippetProxy. +---@param snip table +---@param nodes LuaSnip.Node|LuaSnip.Node[] +---@param opts? LuaSnip.Opts.Node +---@return LuaSnip.BareInternalSnippet local function _S(snip, nodes, opts) nodes = util.wrap_nodes(nodes) -- tbl_extend creates a new table! Important with Proxy, metatable of snip @@ -311,7 +404,6 @@ local function _S(snip, nodes, opts) vim.tbl_extend("error", snip, { nodes = nodes, insert_nodes = {}, - current_insert = 0, mark = nil, dependents = {}, active = false, @@ -378,9 +470,13 @@ local function _S(snip, nodes, opts) }), opts ) + ---@cast snip LuaSnip.BareInternalSnippet -- is propagated to all subsnippets, used to quickly find the outer snippet snip.snippet = snip + -- FIXME(@bew): typing is annoying here, because at this stage we only have + -- the guarentee that snip is a BareInternalSnippet. + -- (and we know this function's return might never be a full snippet..) verify_nodes(nodes) snip:init_nodes() @@ -389,6 +485,7 @@ local function _S(snip, nodes, opts) -- Generate implied i(0) local i0 = iNode.I(0) local i0_indx = #nodes + 1 + -- FIXME(@bew): same comment as for `snip.snippet`'s typing.. i0.parent = snip i0.indx = i0_indx snip.insert_nodes[0] = i0 @@ -398,13 +495,179 @@ local function _S(snip, nodes, opts) return snip end +---@alias LuaSnip.SnipContext.BuiltinTrigEngine +---| '"plain"' # The default behavior, the trigger has to match the text before the +--- cursor exactly. +--- +---| '"pattern"' # The trigger is interpreted as a Lua pattern, and is a match +--- if `trig .. "$"` matches the line up to the cursor. +--- Capture-groups will be accessible as `snippet.captures`. +--- +---| '"ecma"' # The trigger is interpreted as an ECMAscript-regex, and is a +--- match if `trig .. "$"` matches the line up to the cursor. +--- Capture-groups will be accessible as `snippet.captures`. +--- This `trigEngine` requires `jsregexp` (see +--- [LSP-snippets-transformations](#transformations)) to be installed, if it +--- is not, this engine will behave like `"plain"`. +--- +---| '"vim"' # The trigger is interpreted as a vim-regex, and is a match if +--- `trig .. "$"` matches the line up to the cursor. +--- Capture-groups will be accessible as `snippet.captures`, but there is one +--- caveat: the matching is done using `matchlist`, so for now empty-string +--- submatches will be interpreted as unmatched, and the corresponding +--- `snippet.captures[i]` will be `nil` (this will most likely change, don't +--- rely on this behavior). + +---@class LuaSnip.SnipContext.TrigEngineFn.Opts +---@field max_len integer Upper bound on the length of the trigger. +-- If set, the `line_to_cursor` will be truncated (from the cursor of +-- course) to `max_len` characters before performing the match. +-- This is implemented because feeding long `line_to_cursor` into e.g. the +-- pattern-`trigEngine` will hurt performance quite a bit. +-- (see issue Luasnip#1103) +-- This option is implemented for all `trigEngines`. + +---@alias LuaSnip.SnipContext.TrigMatcher fun(line_to_cursor: string, trigger: string): [string, string[]] +---@alias LuaSnip.SnipContext.TrigEngineFn fun(trigger: string, opts: LuaSnip.SnipContext.TrigEngineFn.Opts): LuaSnip.SnipContext.TrigMatcher + +---@alias LuaSnip.ResolveExpandParamsFn fun(snippet: LuaSnip.Snippet, line_to_cursor: string, matched_trigger: string, captures: string[]): LuaSnip.ExpandParams? + +---@class LuaSnip.ExpandParams +--- +---@field trigger? string The fully matched trigger. +---@field captures? string[] Updated capture-groups from parameter in snippet +--- expansion. +--- NOTE: Both `trigger` and `captures` can override the values returned via +--- `trigEngine`. +---@field clear_region? {from: [integer, integer], to: [integer, integer]} +--- Both (0, 0)-indexed {, }, the region where text has to be +--- cleared before inserting the snippet. +---@field env_override? {[string]: string[]|string} Override or extend +--- the snippet's environment (`snip.env`) + +---@alias LuaSnip.SnipContext.ShowConditionFn fun(line_to_cursor: string): boolean +---@alias LuaSnip.SnipContext.ShowCondition LuaSnip.SnipContext.ShowConditionFn|LuaSnip.SnipContext.ConditionObj +---@alias LuaSnip.SnipContext.ConditionFn fun(line_to_cursor: string, matched_trigger: string, captures: string[]): boolean +---@alias LuaSnip.SnipContext.Condition LuaSnip.SnipContext.ConditionFn|LuaSnip.SnipContext.ConditionObj + +---@class LuaSnip.SnipContext +--- +---@field trig? string The trigger of the snippet. +--- If the text in front of (to the left of) the cursor when `ls.expand()` is +--- called matches it, the snippet will be expanded. +--- By default, "matches" means the text in front of the cursor matches the +--- trigger exactly, this behavior can be modified through `trigEngine`. +--- +---@field name? string Can be used to identify the snippet. +--- +---@field desc? string|string[] Description of the snippet. +--- +---@field dscr? string|string[] Same as `desc`. +--- +---@field wordTrig? boolean If true, the snippet is only expanded if the word +--- (`[%w_]+`) before the cursor matches the trigger entirely. +--- Defaults to true. +--- +---@field regTrig? boolean whether the trigger should be interpreted as a +--- Lua pattern. Defaults to false. +--- Consider setting `trigEngine` to `"pattern"` instead, it is more expressive, +--- and in line with other settings. +--- +---@field trigEngine? LuaSnip.SnipContext.BuiltinTrigEngine|LuaSnip.SnipContext.TrigEngineFn +--- Determines how `trig` is interpreted, and what it means for it to "match" +--- the text in front of the cursor. +--- This behavior can be completely customized by passing a function, but the +--- predefined ones should suffice in most cases. +--- +---@field trigEngineOpts? LuaSnip.SnipContext.TrigEngineFn.Opts Options for the +--- used `trigEngine`. +--- +---@field docstring? string|string[] Textual representation of the snippet, specified like +--- `desc`. Overrides docstrings loaded from `json`. +--- +---@field docTrig? string used as `line_to_cursor` during docstring-generation. +--- This might be relevant if the snippet relies on specific values in the +--- capture-groups (for example, numbers, which won't work with the default +--- `$CAPTURESN` used during docstring-generation) +--- +---@field hidden? boolean Hint for completion-engines. +--- If set, the snippet should not show up when querying snippets. +--- +---@field priority? number Priority of the snippet. Defaults to 1000. +--- Snippets with high priority will be matched to a trigger before those with +--- a lower one. +--- The priority for multiple snippets can also be set in `add_snippets`. +--- +---@field snippetType? "snippet"|"autosnippet" Decides whether this snippet has +--- to be triggered by `ls.expand()` or whether is triggered automatically. +--- (don't forget to set `ls.config.setup({ enable_autosnippets = true })` if +--- you want to use this feature). +--- If unset, the snippet type will be determined by how the snippet is added. +--- +---@field resolveExpandParams? LuaSnip.ResolveExpandParamsFn +--- - `snippet`: The expanding snippet object +--- - `line_to_cursor`: The line up to the cursor. +--- - `matched_trigger`: The fully matched trigger (can be retrieved +--- from `line_to_cursor`, but we already have that info here :D) +--- - `captures`: Captures as returned by `trigEngine`. +--- +--- This function will be evaluated in `Snippet:matches()` to decide whether +--- the snippet can be expanded or not. +--- Returns a table if the snippet can be expanded, `nil` if can not. +--- +--- If any field in the returned table is `nil`, the default is used (`trigger` and `captures` as +--- returned by `trigEngine`, `clear_region` such that exactly the trigger is +--- deleted, no overridden environment-variables). +--- +--- A good example for the usage of `resolveExpandParams` can be found in the +--- implementation of [`postfix`](https://github.com/L3MON4D3/LuaSnip/blob/master/lua/luasnip/extras/postfix.lua). +--- +---@field condition? LuaSnip.SnipContext.Condition +--- - `line_to_cursor`: the line up to the cursor. +--- - `matched_trigger`: the fully matched trigger (can be retrieved +--- from `line_to_cursor`, but we already have that info here :D). +--- - `captures`: if the trigger is pattern, contains the capture-groups. +--- Again, could be computed from `line_to_cursor`, but we already did so. +--- +--- This function can prevent manual snippet expansion via `ls.expand()`. +--- Return `true` to allow expansion, and `false` to prevent it. +--- +---@field show_condition? LuaSnip.SnipContext.ShowCondition +--- This function is (should be) evaluated by completion engines, indicating +--- whether the snippet should be included in current completion candidates. +--- Defaults to a function returning `true`. +--- +--- This is different from `condition` because `condition` is evaluated by +--- LuaSnip on snippet expansion (and thus has access to the matched trigger and +--- captures), while `show_condition` is (should be) evaluated by the +--- completion engines when scanning for available snippet candidates. +--- +---@field filetype? string The filetype of the snippet. +--- This overrides the filetype the snippet is added (via `add_snippet`) as. + +---@class LuaSnip.Opts.Snippet: LuaSnip.Opts.SnippetNode +---@field stored? {[string]: LuaSnip.Node} Snippet-level state for restore node. +--- +---@field show_condition? LuaSnip.SnipContext.ShowCondition Same as +--- `show_condition` in snippet context. (here for backward compat) +---@field condition? LuaSnip.SnipContext.Condition Same as `condition` in +--- snippet context. (here for backward compat) + +---@param context string|LuaSnip.SnipContext The snippet context. +--- Passing a string is equivalent to passing `{ trig = }`. +---@param nodes LuaSnip.Node|LuaSnip.Node[] The nodes that make up the snippet. +---@param opts? LuaSnip.Opts.Snippet +---@return LuaSnip.Snippet local function S(context, nodes, opts) opts = opts or {} - local snip = init_snippet_context(node_util.wrap_context(context), opts) - snip = vim.tbl_extend("error", snip, init_snippet_opts(opts)) + local snip_with_ctx = + init_snippet_context(node_util.wrap_context(context), opts) + local snip_with_opts = init_snippet_opts(opts) - snip = _S(snip, nodes, opts) + local base_snip = vim.tbl_extend("error", snip_with_ctx, snip_with_opts) + local snip = _S(base_snip, nodes, opts) + ---@cast snip LuaSnip.Snippet if __luasnip_get_loaded_file_frame_debuginfo ~= nil then -- this snippet is being lua-loaded, and the source should be recorded. @@ -420,6 +683,36 @@ extend_decorator.register( { arg_indx = 3 } ) +---@class LuaSnip.SnippetNode: LuaSnip.BareInternalSnippet, LuaSnip.NormalizedSnippetNodeOpts +---@field is_default boolean + +---@class LuaSnip.Opts.SnippetNode: LuaSnip.Opts.Node +---@field callbacks? {[integer]: {[LuaSnip.EventType]: fun(node: LuaSnip.Node, event_args?: table)}} +--- Contains functions by node position, that are called upon entering/leaving +--- a node of this snippet. +--- To register a callback for the snippets' own events, the key `[-1]` may +--- be used. +--- +--- For example: to print text upon entering the _second_ node of a snippet, +--- `callbacks` should be set as follows: +--- ```lua +--- { +--- -- position of the node, not the jump-index!! +--- -- s("trig", {t"first node", t"second node", i(1, "third node")}). +--- [2] = { +--- [events.enter] = function(node, _event_args) print("2!") end +--- } +--- } +--- ``` +--- More info on events in [events](#events). +--- +---@field child_ext_opts? `false`|LuaSnip.ChildExtOpts (TODO: doc!) +---@field merge_child_ext_opts? boolean (TODO: doc!) + +---@param pos integer? +---@param nodes LuaSnip.Node|LuaSnip.Node[] The nodes that make up the snippet. +---@param opts? LuaSnip.Opts.SnippetNode +---@return LuaSnip.SnippetNode function SN(pos, nodes, opts) opts = opts or {} @@ -428,7 +721,6 @@ function SN(pos, nodes, opts) pos = pos, nodes = util.wrap_nodes(nodes), insert_nodes = {}, - current_insert = 0, mark = nil, dependents = {}, active = false, @@ -436,6 +728,8 @@ function SN(pos, nodes, opts) }, init_snippetNode_opts(opts)), opts ) + ---@cast snip LuaSnip.SnippetNode + verify_nodes(nodes) snip:init_nodes() @@ -443,6 +737,13 @@ function SN(pos, nodes, opts) end extend_decorator.register(SN, { arg_indx = 3 }) +---@param pos integer? +---@param nodes LuaSnip.Node|LuaSnip.Node[] The nodes that make up the `snippetNode`. +---@param indent_text string Used to indent the nodes inside this `snippetNode`. +--- All occurrences of `"$PARENT_INDENT"` are replaced with the actual indent +--- of the parent. +---@param opts? LuaSnip.Opts.SnippetNode +---@return LuaSnip.SnippetNode local function ISN(pos, nodes, indent_text, opts) local snip = SN(pos, nodes, opts) @@ -487,6 +788,7 @@ local function ISN(pos, nodes, indent_text, opts) end extend_decorator.register(ISN, { arg_indx = 4 }) +-- FIXME(@bew): should only be on ExpandedSnippet 🤔 function Snippet:remove_from_jumplist() if not self.visible then -- snippet not visible => already removed. @@ -545,6 +847,7 @@ function Snippet:remove_from_jumplist() end end +-- FIXME(@bew): should only be on ExpandedSnippet 🤔 function Snippet:insert_into_jumplist( current_node, parent_node, @@ -650,6 +953,12 @@ function Snippet:insert_into_jumplist( table.insert(sibling_snippets, own_indx, self) end +-- IDEA(THINKING, @bew): Most methods in Snippet should really be on a BareInternalSnippet class +-- But this method should be on an actual Snippet class +-- (so it can be called on a full Snippet, but not a BareInternalSnippet) +-- This came to my mind because this function uses `self.stored` & +-- `self.merge_child_ext_opts` that _only_ exist on full Snippet but not on +-- BareInternalSnippet. function Snippet:trigger_expand(current_node, pos_id, env, indent_nodes) local pos = vim.api.nvim_buf_get_extmark_by_id(0, session.ns_id, pos_id, {}) @@ -817,6 +1126,12 @@ end -- the text does usually not match, but resolveExpandParams may still give -- useful data (e.g. when the snippet is a treesitter_postfix, see -- https://github.com/L3MON4D3/LuaSnip/issues/1374) +-- +-- IDEA(THINKING, @bew): Similar to `trigger_expand`, this uses fields from +-- Snippet (from its context & opts) not BareInternalSnippet, should be moved. +---@param line_to_cursor string +---@param opts? {fallback_match?: string} +---@return LuaSnip.ExpandParams? function Snippet:matches(line_to_cursor, opts) local fallback_match = util.default_tbl_get(nil, opts, "fallback_match") @@ -859,28 +1174,11 @@ 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 - +---@generic T +---@param self T +---@return T function Snippet:copy() - return copy3(self) + return util.copy3(self) end function Snippet:del_marks() @@ -929,6 +1227,9 @@ end -- populate env,inden,captures,trigger(regex),... but don't put any text. -- the env may be passed in opts via opts.env, if none is passed a new one is -- generated. +-- +-- IDEA(THINKING, @bew): Similar to `trigger_expand`, this uses fields from +-- Snippet (from its context & opts) not BareInternalSnippet, should be moved. function Snippet:fake_expand(opts) if not opts then opts = {} @@ -983,6 +1284,7 @@ end -- to work correctly, this may require that the snippets' env,indent,captures? are -- set. +---@return string[]? function Snippet:get_static_text() if self.static_text then return self.static_text @@ -1015,6 +1317,7 @@ function Snippet:get_static_text() return text end +---@return string[] function Snippet:get_docstring() if self.docstring then return self.docstring @@ -1099,6 +1402,8 @@ Snippet.init_insert_positions = node_util.init_child_positions_func( function Snippet:make_args_absolute() for _, node in ipairs(self.nodes) do + ---(allowed: this arg only exists for some node types) + ---@diagnostic disable-next-line: redundant-parameter node:make_args_absolute(self.absolute_insert_position) end end @@ -1198,6 +1503,12 @@ function Snippet:text_only() return true end +--- Trigger event with args +---@param event LuaSnip.EventType +---@param event_args? table +---@return any +-- FIXME(@bew): This should only be on SnippetNode & Snippet 🤔 +-- (to access callbacks) function Snippet:event(event, event_args) -- there are 3 sources of a callback, for a snippetNode: -- self.callbacks[-1], self.node_callbacks, and parent.callbacks[self.pos]. @@ -1347,6 +1658,7 @@ function Snippet:static_init() end -- called only for snippetNodes! +-- => FIXME(@bew): should then be in a SnippetNode class? function Snippet:resolve_child_ext_opts() if self.merge_child_ext_opts then self.effective_child_ext_opts = ext_util.child_extend( diff --git a/lua/luasnip/nodes/util.lua b/lua/luasnip/nodes/util.lua index cb27d9ffd..3ce545f15 100644 --- a/lua/luasnip/nodes/util.lua +++ b/lua/luasnip/nodes/util.lua @@ -19,6 +19,10 @@ local function subsnip_init_children(parent, children) end end +---@param key string +---@param node_children_key string +---@param child_func_name string +---@return fun(node: LuaSnip.Node, position_so_far: integer[]) local function init_child_positions_func( key, node_children_key, @@ -38,23 +42,32 @@ local function init_child_positions_func( end end -local function make_args_absolute(args, parent_insert_position, target) +---@param args LuaSnip.NodeRef[] +---@param parent_insert_position integer[] +---@return LuaSnip.NormalizedNodeRef[] _ The normalized node refs +local function make_args_absolute(args, parent_insert_position) + ---@type LuaSnip.NormalizedNodeRef[] + local normalized_args = {} for i, arg in ipairs(args) do if type(arg) == "number" then -- the arg is a number, should be interpreted relative to direct -- parent. local t = vim.deepcopy(parent_insert_position) table.insert(t, arg) - target[i] = { absolute_insert_position = t } + normalized_args[i] = { absolute_insert_position = t } else -- insert node, absolute_indexer, or key itself, node's -- absolute_insert_position may be nil, check for that during -- usage. - target[i] = arg + normalized_args[i] = arg end end + return normalized_args end +--- Normarlizes node references +---@param args LuaSnip.NodeRef[]|LuaSnip.NodeRef +---@return LuaSnip.NodeRef[] local function wrap_args(args) -- stylua: ignore if type(args) ~= "table" or @@ -127,6 +140,7 @@ local function enter_nodes_between(parent, child, no_move) nodes[#nodes]:input_enter(no_move) end +---@param node LuaSnip.Node local function select_node(node) local node_begin, node_end = node.mark:pos_begin_end_raw() feedkeys.select_range(node_begin, node_end) @@ -144,6 +158,8 @@ local function print_dict(dict) })) end +---@param opts LuaSnip.Opts.Node +---@return LuaSnip.NormalizedNodeOpts local function init_node_opts(opts) local in_node = {} if not opts then @@ -176,6 +192,8 @@ local function snippet_extend_context(arg, extend) return vim.tbl_extend("keep", arg or {}, extend or {}) end +---@param context LuaSnip.SnipContext|string +---@return LuaSnip.SnipContext local function wrap_context(context) if type(context) == "string" then return { trig = context } @@ -851,6 +869,8 @@ local function collect_dependents(node, which, static) return tbl_util.set_to_list(dependents_set) end +---@param args (string|LuaSnip.SnippetString)[]? +---@return string[][]? local function str_args(args) return args and vim.tbl_map(function(arg) @@ -860,38 +880,43 @@ local function str_args(args) end ---@class LuaSnip.SnippetCursorRestoreData ----This class holds data about the current position of the cursor in a snippet. +--- This class holds data about the current position of the cursor in a snippet. +--- ---@field key string key of the current node. ---@field store_id number uniquely identifies the data associated with this ----store-restore cycle. ----This is necessary because eg. the snippetStrings may contain cursor-positions ----of more than one restore data, and the correct ones can be identified via ----store_id. +--- store-restore cycle. +--- This is necessary because eg. the snippetStrings may contain +--- cursor-positions of more than one restore data, and the correct ones can be +--- identified via store_id. +--- ---@field node LuaSnip.Node The node the cursor will be stored relative to. +--- ---@field cursor_start_relative LuaSnip.BytecolBufferPosition The position of ----the cursor, or beginning of selected area, relative to the beginning of ----`node`. +--- the cursor, or beginning of selected area, relative to the beginning of +--- `node`. +--- ---@field selection_end_start_relative LuaSnip.BytecolBufferPosition The ----position of the cursor, or end of selected area, relative to the beginning of ----`node`. The column is one beyond the byte where the selection ends. +--- position of the cursor, or end of selected area, relative to the beginning of +--- `node`. The column is one beyond the byte where the selection ends. +--- ---@field mode string The first character (see `vim.fn.mode()`) of the mode at ----the time of `store`. +--- the time of `store`. ---@alias LuaSnip.CursorRestoreData table ----Represents the position of the cursor relative to all snippets the cursor was ----inside. ----Maps a `store_id` to the data needed to restore the cursor relative to the ----stored node of that snippet. ----We need the data relative to all parent-snippets of some node because the ----first 1,2,... snippets may disappear when a choice is changed. +--- Represents the position of the cursor relative to all snippets the cursor +--- was inside. +--- Maps a `store_id` to the data needed to restore the cursor relative to the +--- stored node of that snippet. +--- We need the data relative to all parent-snippets of some node because the +--- first 1,2,... snippets may disappear when a choice is changed. ----@class LuaSnip.StoreCursorNodeRelativeOpts ----@field place_cursor_mark boolean? Whether to, if possible, place a mark in ----snippetText. +---@class LuaSnip.Opts.StoreCursorNodeRelative +---@field place_cursor_mark? boolean Whether to, if possible, place a mark in +--- snippetText. local store_id = 0 ---@param node LuaSnip.Node The node to store the cursor relative to. ----@param opts LuaSnip.StoreCursorNodeRelativeOpts +---@param opts LuaSnip.Opts.StoreCursorNodeRelative local function store_cursor_node_relative(node, opts) local data = {} @@ -937,6 +962,7 @@ local function store_cursor_node_relative(node, opts) snippet_current_node.type == types.insertNode and opts.place_cursor_mark then + ---@cast snippet_current_node LuaSnip.InsertNode -- if the snippet_current_node is not an insertNode, the cursor -- should always be exactly at the beginning if the node is entered -- (which, btw, can only happen if a text or functionNode is diff --git a/lua/luasnip/nodes/util/snippet_string.lua b/lua/luasnip/nodes/util/snippet_string.lua index 57ad9aba6..3e3f22484 100644 --- a/lua/luasnip/nodes/util/snippet_string.lua +++ b/lua/luasnip/nodes/util/snippet_string.lua @@ -1,7 +1,28 @@ local str_util = require("luasnip.util.str") local util = require("luasnip.util.util") ----@class SnippetString +---@class LuaSnip.SnippetString.Mark +--- A kind of extmark, but for a string not a Neovim buffer. +--- +--- It moves with inserted text, and has a gravity to control into which +--- direction it shifts. pos is 1-based and refers to one character in the +--- string +--- +--- If the edge is in the middle of multiple characters (for example rgrav=true, +--- and chars at pos and pos+1 are replaced), the mark is removed. +--- +---@field id string ID of the mark +---@field pos integer 1-based, refers to one character in the string +---@field rgrav boolean The gravity of the mark. +--- - When rgrav=true, the mark being incident with the characters right edge. +--- (replace character at pos with multiple characters => mark will move to +--- the right of the newly inserted chars) +--- - When rgrav=false, the mark follows the the left edge. +--- (replace char with multiple chars => mark stays at char) + +---@class LuaSnip.SnippetString +---@field marks LuaSnip.SnippetString.Mark[] +---@field metadata? table local SnippetString = {} local SnippetString_mt = { __index = SnippetString, @@ -9,12 +30,11 @@ local SnippetString_mt = { -- __concat and __tostring will be set later on. } -local M = {} - ---Create new SnippetString. ---@param initial_str string[]?, optional initial multiline string. ----@return SnippetString -function M.new(initial_str, metadata) +---@param metadata? table +---@return LuaSnip.SnippetString +function SnippetString.new(initial_str, metadata) local o = { initial_str and table.concat(initial_str, "\n"), marks = {}, @@ -23,7 +43,7 @@ function M.new(initial_str, metadata) return setmetatable(o, SnippetString_mt) end -function M.isinstance(o) +function SnippetString.isinstance(o) return getmetatable(o) == SnippetString_mt end @@ -50,7 +70,7 @@ local function gen_snipstr_map(self, map, from_offset) v.snip:subtree_do({ pre = function(node) if node.static_text then - if M.isinstance(node.static_text) then + if SnippetString.isinstance(node.static_text) then local nested_str = gen_snipstr_map( node.static_text, map, @@ -130,6 +150,7 @@ function SnippetString:put(pos) end end +---@return LuaSnip.SnippetString function SnippetString:copy() -- on 0.7 vim.deepcopy does not behave correctly on snippets => have to manually copy. return setmetatable( @@ -178,6 +199,7 @@ function SnippetString:copy() end -- copy without copying snippets. +---@return LuaSnip.SnippetString function SnippetString:flatcopy() local res = {} for i, v in ipairs(self) do @@ -189,17 +211,21 @@ function SnippetString:flatcopy() return setmetatable(res, SnippetString_mt) end --- where o is string, string[] or SnippetString. +---@param o string|string[]|LuaSnip.SnippetString +---@return LuaSnip.SnippetString local function to_snippetstring(o) if type(o) == "string" then - return M.new({ o }) + return SnippetString.new({ o }) elseif getmetatable(o) == SnippetString_mt then return o else - return M.new(o) + return SnippetString.new(o) end end +---@param a string|string[]|LuaSnip.SnippetString +---@param b string|string[]|LuaSnip.SnippetString +---@return LuaSnip.SnippetString function SnippetString.concat(a, b) a = to_snippetstring(a):flatcopy() b = to_snippetstring(b):flatcopy() @@ -272,7 +298,7 @@ local function nodetext_len(node, snipstr_map) return 0 end - if M.isinstance(node.static_text) then + if SnippetString.isinstance(node.static_text) then return #snipstr_map[node.static_text].str else -- +1 for each newline. @@ -324,7 +350,9 @@ local function _replace(self, replacements, snipstr_map) and node_relative_repl_from <= node_len then if node_relative_repl_to <= node_len then - if M.isinstance(node.static_text) then + if + SnippetString.isinstance(node.static_text) + then -- node contains a snippetString, recurse! -- since we only check string-positions via -- snipstr_map, we don't even have to @@ -432,7 +460,7 @@ local function upper(self) v.snip:subtree_do({ pre = function(node) if node.static_text then - if M.isinstance(node.static_text) then + if SnippetString.isinstance(node.static_text) then node.static_text:_upper() else str_util.multiline_upper(node.static_text) @@ -453,7 +481,7 @@ local function lower(self) v.snip:subtree_do({ pre = function(node) if node.static_text then - if M.isinstance(node.static_text) then + if SnippetString.isinstance(node.static_text) then node.static_text:_lower() else str_util.multiline_lower(node.static_text) @@ -532,7 +560,7 @@ function SnippetString:sub(from, to) -- empty range => return empty snippetString. if from > #str or to < from or to < 1 then - return M.new({ "" }) + return SnippetString.new({ "" }) end from = math.max(from, 1) @@ -552,15 +580,10 @@ function SnippetString:sub(from, to) return self end --- add a kind-of extmark to the text in this buffer. It moves with inserted --- text, and has a gravity to control into which direction it shifts. --- pos is 1-based and refers to one character in the string, rgrav = true can be --- understood as the mark being incident with the characters right edge (replace --- character at pos with multiple characters => mark will move to the right of --- the newly inserted chars), and rgrav = false with the left edge (replace char --- with multiple chars => mark stays at char). --- If the edge is in the middle of multiple characters (for example rgrav=true, --- and chars at pos and pos+1 are replaced), the mark is removed. +--- Add a kind-of extmark to the text in this buffer. +---@param id string ID of the mark +---@param pos integer 1-based, refers to one character in the string. +---@param rgrav boolean The gravity of the mark, true=right, false=left. function SnippetString:add_mark(id, pos, rgrav) -- I'd expect there to be at most 0-2 marks in any given static_text, which -- are those set to track the cursor-position. @@ -578,6 +601,8 @@ function SnippetString:add_mark(id, pos, rgrav) }) end +---@param id string ID of the mark +---@return integer? function SnippetString:get_mark_pos(id) for _, mark in ipairs(self.marks) do if mark.id == id then @@ -590,4 +615,4 @@ function SnippetString:clear_marks() self.marks = {} end -return M +return SnippetString diff --git a/lua/luasnip/session/snippet_collection/init.lua b/lua/luasnip/session/snippet_collection/init.lua index 4490b606d..aaef32024 100644 --- a/lua/luasnip/session/snippet_collection/init.lua +++ b/lua/luasnip/session/snippet_collection/init.lua @@ -1,5 +1,5 @@ local source = require("luasnip.session.snippet_collection.source") -local u_table = require("luasnip.util.table") +local table_util = require("luasnip.util.table") local auto_creating_tables = require("luasnip.util.auto_table").warn_depth_autotable local session = require("luasnip.session") @@ -328,7 +328,7 @@ local function get_all_snippet_fts() ft_set[ft] = true end - return u_table.set_to_list(ft_set) + return table_util.set_to_list(ft_set) end -- modules that want to call refresh_notify probably also want to notify others diff --git a/lua/luasnip/session/snippet_collection/source.lua b/lua/luasnip/session/snippet_collection/source.lua index 9f3319ef2..dcfea9ac0 100644 --- a/lua/luasnip/session/snippet_collection/source.lua +++ b/lua/luasnip/session/snippet_collection/source.lua @@ -1,7 +1,14 @@ +---@class LuaSnip.Source +---@field file string +---@field line? integer +---@field line_end? integer + +---@type {[integer]: LuaSnip.Source} local id_to_source = {} local M = {} +---@return LuaSnip.Source function M.from_debuginfo(debuginfo) assert(debuginfo.source, "debuginfo contains source") assert( @@ -16,6 +23,9 @@ function M.from_debuginfo(debuginfo) } end +---@param file string +---@param opts? {line: integer, line_end: integer} +---@return LuaSnip.Source function M.from_location(file, opts) assert(file, "source needs file") opts = opts or {} @@ -23,6 +33,8 @@ function M.from_location(file, opts) return { file = file, line = opts.line, line_end = opts.line_end } end +---@param snippet LuaSnip.Snippet +---@param source LuaSnip.Source function M.set(snippet, source) -- snippets only get their id after being added, make sure this is the -- case. @@ -31,6 +43,8 @@ function M.set(snippet, source) id_to_source[snippet.id] = source end +---@param snippet LuaSnip.Snippet +---@return LuaSnip.Source function M.get(snippet) return id_to_source[snippet.id] end diff --git a/lua/luasnip/util/directed_graph.lua b/lua/luasnip/util/directed_graph.lua index 4c9063289..9de72a412 100644 --- a/lua/luasnip/util/directed_graph.lua +++ b/lua/luasnip/util/directed_graph.lua @@ -1,6 +1,8 @@ local tbl_util = require("luasnip.util.table") local autotable = require("luasnip.util.auto_table").autotable +---@class LuaSnip.Util.DirectedGraph +---@field vertices LuaSnip.Util.Vertex[] local DirectedGraph = {} -- set __index directly in DirectedGraph, otherwise each DirectedGraph-object would have its' @@ -8,15 +10,22 @@ local DirectedGraph = {} -- unnecessary nonetheless. DirectedGraph.__index = DirectedGraph +---@alias LuaSnip.Util.VertexSet {[LuaSnip.Util.Vertex]: boolean} + +---@class LuaSnip.Util.Vertex +---@field incoming_edge_verts LuaSnip.Util.VertexSet +---@field outgoing_edge_verts LuaSnip.Util.VertexSet local Vertex = {} Vertex.__index = Vertex +---@return LuaSnip.Util.DirectedGraph local function new_graph() return setmetatable({ -- all vertices of this graph. vertices = {}, }, DirectedGraph) end +---@return LuaSnip.Util.Vertex local function new_vertex() return setmetatable({ -- vertices this vertex has an edge from/to. @@ -27,7 +36,7 @@ local function new_vertex() end ---Add new vertex to the DirectedGraph ----@return table: the generated vertex, to be used in `set_edge`, for example. +---@return LuaSnip.Util.Vertex _ the generated vertex, to be used in `set_edge`, for example. function DirectedGraph:add_vertex() local vert = new_vertex() table.insert(self.vertices, vert) @@ -35,7 +44,7 @@ function DirectedGraph:add_vertex() end ---Remove vertex and its edges from DirectedGraph. ----@param v table: the vertex. +---@param v LuaSnip.Util.Vertex The vertex. function DirectedGraph:clear_vertex(v) if not vim.tbl_contains(self.vertices, v) then -- vertex does not belong to this graph. Maybe throw error/make @@ -53,8 +62,8 @@ function DirectedGraph:clear_vertex(v) end ---Add edge from v1 to v2 ----@param v1 table: vertex in the graph. ----@param v2 table: vertex in the graph. +---@param v1 LuaSnip.Util.Vertex Vertex in the graph. +---@param v2 LuaSnip.Util.Vertex Vertex in the graph. function DirectedGraph:set_edge(v1, v2) if v1.outgoing_edge_verts[v2] then -- the edge already exists. Don't return an error, for now. @@ -66,8 +75,8 @@ function DirectedGraph:set_edge(v1, v2) end ---Remove edge from v1 to v2 ----@param v1 table: vertex in the graph. ----@param v2 table: vertex in the graph. +---@param v1 LuaSnip.Util.Vertex Vertex in the graph. +---@param v2 LuaSnip.Util.Vertex Vertex in the graph. function DirectedGraph:clear_edge(v1, v2) assert(v1.outgoing_edge_verts[v2], "nonexistent edge cannot be removed.") -- unlink vertices. @@ -76,8 +85,8 @@ function DirectedGraph:clear_edge(v1, v2) end ---Find and return verts with indegree 0. ----@param graph table: graph. ----@return table of vertices. +---@param graph LuaSnip.Util.DirectedGraph +---@return LuaSnip.Util.Vertex[] local function source_verts(graph) local indegree_0_verts = {} for _, vert in ipairs(graph.vertices) do @@ -89,10 +98,10 @@ local function source_verts(graph) end ---Copy graph. ----@param graph table: graph. ----@return table,table: copied graph and table for mapping copied node to ----original node(original_vert[some_vert_from_copy] -> corresponding original ----vert). +---@param graph LuaSnip.Util.DirectedGraph +---@return LuaSnip.Util.DirectedGraph, table _ +--- Copied graph and table mapping for copied node to original node. +--- (original_vert[some_vert_from_copy] -> corresponding original vert) local function graph_copy(graph) local copy = vim.deepcopy(graph) local original_vert = {} @@ -105,8 +114,8 @@ end ---Generate a (it's not necessarily unique) topological sorting of this graphs ---vertices. ---https://en.wikipedia.org/wiki/Topological_sorting, this uses Kahn's Algorithm. ----@return table|nil: sorted vertices of this graph, nil if there is no ----topological sorting (eg. if the graph has a cycle). +---@return LuaSnip.Util.Vertex[]? _ Sorted vertices of this graph, nil if there +--- is no topological sorting (eg. if the graph has a cycle). function DirectedGraph:topological_sort() local sorting = {} @@ -148,13 +157,18 @@ function DirectedGraph:topological_sort() return sorting end --- return all vertices reachable from this one. +--- Return all vertices reachable from this one. +---@param vert LuaSnip.Util.Vertex +---@param edge_direction "Forward"|"Backward" +---@return LuaSnip.Util.Vertex[] function DirectedGraph:connected_component(vert, edge_direction) local outgoing_vertices_field = edge_direction == "Backward" and "incoming_edge_verts" or "outgoing_edge_verts" + ---@type LuaSnip.Util.VertexSet local visited = {} + ---@type LuaSnip.Util.VertexSet local to_visit = { [vert] = true } -- get any value in table. @@ -164,6 +178,7 @@ function DirectedGraph:connected_component(vert, edge_direction) visited[next_vert] = true for neighbor, _ in pairs(next_vert[outgoing_vertices_field]) do + ---@cast neighbor LuaSnip.Util.Vertex if not visited[neighbor] then to_visit[neighbor] = true end diff --git a/lua/luasnip/util/events.lua b/lua/luasnip/util/events.lua index a7ba52d46..416d98a9e 100644 --- a/lua/luasnip/util/events.lua +++ b/lua/luasnip/util/events.lua @@ -1,18 +1,30 @@ local node_names = require("luasnip.util.types").names_pascal_case -return { +---@enum LuaSnip.EventType +local EventType = { enter = 1, leave = 2, change_choice = 3, pre_expand = 4, - to_string = function(node_type, event_id) - if event_id == 3 then - return "ChangeChoice" - elseif event_id == 4 then - return "PreExpand" - else - return node_names[node_type] - .. (event_id == 1 and "Enter" or "Leave") - end - end, } + +local M = setmetatable({}, { __index = EventType }) +-- NOTE: The metatable is set so that callers of this `events` module can do +-- `events.change_choice` to get a named value from the enum, while leaving the +-- enum definition standalone to avoid adding unnecessary enum fields. + +---@param node_type LuaSnip.NodeType +---@param event_id LuaSnip.EventType +---@return string +function M.to_string(node_type, event_id) + if event_id == EventType.change_choice then + return "ChangeChoice" + elseif event_id == EventType.pre_expand then + return "PreExpand" + else + return node_names[node_type] + .. (event_id == EventType.enter and "Enter" or "Leave") + end +end + +return M diff --git a/lua/luasnip/util/ext_opts.lua b/lua/luasnip/util/ext_opts.lua index 109bd6fb7..1e4a5da96 100644 --- a/lua/luasnip/util/ext_opts.lua +++ b/lua/luasnip/util/ext_opts.lua @@ -35,6 +35,8 @@ local function clear_invalid(opts) --stylua: ignore end end +---@param ext_opts? LuaSnip.NodeExtOpts +---@return LuaSnip.NodeExtOpts local function _complete_ext_opts(ext_opts) if not ext_opts then ext_opts = {} @@ -78,18 +80,22 @@ end -- active inherits unset values from passive, which in turn inherits from -- snippet_passive. -- Also make sure that all keys have a table, and are not nil! +---@param ext_opts LuaSnip.ChildExtOpts +---@return LuaSnip.ChildExtOpts local function child_complete(ext_opts) for _, node_type in pairs(types.node_types) do ext_opts[node_type] = _complete_ext_opts(ext_opts[node_type]) end - ext_opts.base_prio = 0 + ext_opts.base_prio = 0 ---@diagnostic disable-line: inject-field return ext_opts end +---@param ext_opts? LuaSnip.NodeExtOpts +---@return LuaSnip.NodeExtOpts local function complete(ext_opts) - _complete_ext_opts(ext_opts) - ext_opts.base_prio = 0 + local ext_opts = _complete_ext_opts(ext_opts) + ext_opts.base_prio = 0 ---@diagnostic disable-line: inject-field return ext_opts end 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 = { ... } diff --git a/lua/luasnip/util/feedkeys.lua b/lua/luasnip/util/feedkeys.lua index 0b5a362d0..930867356 100644 --- a/lua/luasnip/util/feedkeys.lua +++ b/lua/luasnip/util/feedkeys.lua @@ -75,7 +75,13 @@ function M.feedkeys_insert(keys) end, next_id()) end --- pos: (0,0)-indexed. +--- Returns keybind-expr actions to place the cursor at the given position, +--- optionally on the previous char on the left or previous line. +--- +---@param pos LuaSnip.RawPos0 Position, 0-indexed +---@param before? boolean Place the cursor on the previous char, +--- on the left or previous line. +---@return string local function cursor_set_keys(pos, before) if before then if pos[2] == 0 then @@ -115,10 +121,12 @@ local function cursor_set_keys(pos, before) .. "," -- -1 works for multibyte because of rounding, apparently. .. pos[2] - .. "})" - .. ":silent! foldopen!" + .. "})" + .. ":silent! foldopen!" end +---@param b LuaSnip.RawPos0 +---@param e LuaSnip.RawPos0 function M.select_range(b, e) local id = next_id() enqueued_cursor_state = diff --git a/lua/luasnip/util/mark.lua b/lua/luasnip/util/mark.lua index ab4e539f7..0162bb00f 100644 --- a/lua/luasnip/util/mark.lua +++ b/lua/luasnip/util/mark.lua @@ -1,17 +1,30 @@ local session = require("luasnip.session") local util = require("luasnip.util.util") ----@class LuaSnip.Mark +---@class LuaSnip.Pos0 A [row, col] position in a buffer, 0-indexed. +---@field [1] integer +---@field [2] integer + +---@class LuaSnip.RawPos0: LuaSnip.Pos0 A raw (byte-level) [raw, col] position +--- in a buffer, 0-indexed. + +---@class LuaSnip.Mark Represents an extmark in a buffer. +---@field id? integer Extmark ID +---@field opts vim.api.keyset.set_extmark Extmark options local Mark = {} +---@return LuaSnip.Mark function Mark:new(o) - o = o or {} + o = o or { id = nil, opts = {} } setmetatable(o, self) self.__index = self return o end --- opts just like in nvim_buf_set_extmark. +---@param pos_begin LuaSnip.RawPos0 +---@param pos_end LuaSnip.RawPos0 +---@param opts vim.api.keyset.set_extmark +---@return LuaSnip.Mark local function mark(pos_begin, pos_end, opts) return Mark:new({ id = vim.api.nvim_buf_set_extmark( @@ -31,12 +44,17 @@ local function mark(pos_begin, pos_end, opts) }) end +---@param pos LuaSnip.RawPos0 +---@return LuaSnip.Pos0 local function bytecol_to_utfcol(pos) local line = vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false) -- line[1]: get_lines returns table. return { pos[1], util.str_utf32index(line[1] or "", pos[2]) } end +--- Returns the (utf) begin/end positions of the mark. +---@return LuaSnip.Pos0 _ begin position, 0-indexed +---@return LuaSnip.Pos0 _ end position, 0-indexed function Mark:pos_begin_end() local mark_info = vim.api.nvim_buf_get_extmark_by_id( 0, @@ -49,6 +67,8 @@ function Mark:pos_begin_end() bytecol_to_utfcol({ mark_info[3].end_row, mark_info[3].end_col }) end +--- Returns the (utf) begin position of the mark +---@return LuaSnip.Pos0 function Mark:pos_begin() local mark_info = vim.api.nvim_buf_get_extmark_by_id( 0, @@ -60,6 +80,8 @@ function Mark:pos_begin() return bytecol_to_utfcol({ mark_info[1], mark_info[2] }) end +--- Returns the (utf) end position of the mark +---@return LuaSnip.Pos0 function Mark:pos_end() local mark_info = vim.api.nvim_buf_get_extmark_by_id( 0, @@ -71,6 +93,9 @@ function Mark:pos_end() return bytecol_to_utfcol({ mark_info[3].end_row, mark_info[3].end_col }) end +--- Returns the raw (byte) begin/end positions of the mark. +---@return LuaSnip.RawPos0 _ begin position, 0-indexed +---@return LuaSnip.RawPos0 _ end position, 0-indexed function Mark:pos_begin_end_raw() local mark_info = vim.api.nvim_buf_get_extmark_by_id( 0, @@ -84,6 +109,8 @@ function Mark:pos_begin_end_raw() } end +--- Returns the raw (byte) begin position of the mark +---@return LuaSnip.RawPos0 function Mark:pos_begin_raw() local mark_info = vim.api.nvim_buf_get_extmark_by_id( 0, @@ -94,6 +121,8 @@ function Mark:pos_begin_raw() return { mark_info[1], mark_info[2] } end +---@param opts vim.api.keyset.set_extmark +---@return LuaSnip.Mark function Mark:copy_pos_gravs(opts) local pos_beg, pos_end = self:pos_begin_end_raw() opts.right_gravity = self.opts.right_gravity @@ -101,11 +130,17 @@ function Mark:copy_pos_gravs(opts) return mark(pos_beg, pos_end, opts) end --- opts just like in nvim_buf_set_extmark. --- opts as first arg bcs. pos are pretty likely to stay the same. +--- Update the extmark with the given opts & positions. +--- +--- note: opts as first arg because positions are pretty likely to stay the same. +--- +---@param opts vim.api.keyset.set_extmark +---@param pos_begin LuaSnip.RawPos0 +---@param pos_end LuaSnip.RawPos0 function Mark:update(opts, pos_begin, pos_end) -- if one is changed, the other is likely as well. if not pos_begin then + -- FIXME(@bew): old_pos_begin & old_pos_end don't exist?? pos_begin = old_pos_begin if not pos_end then pos_end = old_pos_end @@ -126,6 +161,7 @@ function Mark:update(opts, pos_begin, pos_end) ) end +---@param opts vim.api.keyset.set_extmark function Mark:set_opts(opts) local pos_begin, pos_end = self:pos_begin_end_raw() vim.api.nvim_buf_del_extmark(0, session.ns_id, self.id) @@ -146,6 +182,9 @@ function Mark:set_opts(opts) ) end +--- Set right-gravity for left & right sides +---@param rgrav_left boolean +---@param rgrav_right boolean function Mark:set_rgravs(rgrav_left, rgrav_right) -- don't update if nothing would change. if @@ -158,6 +197,9 @@ function Mark:set_rgravs(rgrav_left, rgrav_right) end end +--- Returns right-gravity for left or right side +---@param which -1|1 +---@return boolean? function Mark:get_rgrav(which) if which == -1 then return self.opts.right_gravity @@ -166,6 +208,9 @@ function Mark:get_rgrav(which) end end +--- Set right-gravity for left or right side +---@param which -1|1 +---@param rgrav boolean function Mark:set_rgrav(which, rgrav) if which == -1 then if self.opts.right_gravity == rgrav then @@ -181,6 +226,9 @@ function Mark:set_rgrav(which, rgrav) self:set_opts(self.opts) end +--- Returns the raw (byte) position of the wanted side +---@param which -1|1 +---@return LuaSnip.RawPos0 function Mark:get_endpoint(which) -- simpler for now, look into perf here later. local l, r = self:pos_begin_end_raw() @@ -191,7 +239,8 @@ function Mark:get_endpoint(which) end end --- change all opts except rgravs. +--- Update all opts except right-gravities +---@param opts vim.api.keyset.set_extmark function Mark:update_opts(opts) local opts_cp = vim.deepcopy(opts) opts_cp.right_gravity = self.opts.right_gravity @@ -199,14 +248,16 @@ function Mark:update_opts(opts) self:set_opts(opts_cp) end --- invalidate this mark object only, leave the underlying extmark alone. +--- Invalidate this mark object only, leave the underlying extmark alone. function Mark:invalidate() self.id = nil end +--- Delete the underlying extmark if any. function Mark:clear() if self.id then vim.api.nvim_buf_del_extmark(0, session.ns_id, self.id) + -- FIXME(@bew): Should also invalidate the Mark obj? (self.id = nil) end end diff --git a/lua/luasnip/util/str.lua b/lua/luasnip/util/str.lua index 8f8a29ec8..0806fef1e 100644 --- a/lua/luasnip/util/str.lua +++ b/lua/luasnip/util/str.lua @@ -8,7 +8,7 @@ local function dedent(lines) local ind_size = math.huge for i, _ in ipairs(lines) do local i1, i2 = lines[i]:find("^%s*[^%s]") - if i1 and i2 < ind_size then + if i1 and i2 and i2 < ind_size then ind_size = i2 end end @@ -77,12 +77,17 @@ function M.process_multiline(lines, options) end end +---@param s string +---@return string function M.dedent(s) local lst = vim.split(s, "\n") dedent(lst) return table.concat(lst, "\n") end +---@param s string +---@param indent_string string +---@return string function M.convert_indent(s, indent_string) local lst = vim.split(s, "\n") convert_indent(lst, indent_string, "\t") @@ -101,11 +106,12 @@ local function is_escaped(s, indx) return count % 2 == 1 end ---- return position of next (relative to `start`) unescaped occurence of +--- Return position of next (relative to `start`) unescaped occurence of --- `target` in `s`. ---@param s string ---@param target string ----@param start number +---@param start integer +---@return integer? local function find_next_unescaped(s, target, start) while true do local from = s:find(target, start, true) @@ -125,7 +131,7 @@ end ---@param s string ---@param left string ---@param right string ----@return function: iterator, returns pairs from,to. +---@return fun(): (integer?, integer?) An iterator returning pairs from,to. function M.unescaped_pairs(s, left, right) local search_from = 1 @@ -198,12 +204,13 @@ function M.multiline_append(strmod, strappend) end end --- turn a row+col-offset for a multiline-string (string[]) (where the column is --- given in bytes and 0-based) into an offset (in bytes, 1-based) for --- the \n-concatenated version of that string. +--- Turns a row+col-offset for a multiline-string (string[]) (where the column is +--- given in bytes and 0-based) into an offset (in bytes, 1-based) for +--- the \n-concatenated version of that string. --- ---@param str string[], a multiline string ---@param pos LuaSnip.ApiPosition, an api-position relative to the start of str. +---@return integer? function M.multiline_to_byte_offset(str, pos) if pos[1] < 0 or pos[1] + 1 > #str or pos[2] < 0 then -- pos is trivially (row negative or beyond str, or col negative) @@ -236,6 +243,7 @@ end -- inverse of multiline_to_byte_offset, 1-based byte to 0,0-based row,column. ---@param str string[], the multiline string ---@param byte_pos number, a 1-based index into the \n-concatenated `str`. +---@return [integer, integer]? function M.byte_to_multiline_offset(str, byte_pos) if byte_pos < 0 then return nil @@ -256,10 +264,15 @@ end -- string-operations implemented according to -- https://github.com/microsoft/vscode/blob/71c221c532996c9976405f62bb888283c0cf6545/src/vs/editor/contrib/snippet/browser/snippetParser.ts#L372-L415 -- such that they can be used for snippet-transformations in vscode-snippets. +---@param str string +---@return string local function capitalize(str) -- uppercase first character. - return str:gsub("^.", string.upper) + local ret = str:gsub("^.", string.upper) + return ret -- note: local var required for correct typing end +---@param str string +---@return string local function pascalcase(str) local pascalcased = "" for match in str:gmatch("[a-zA-Z0-9]+") do @@ -267,16 +280,20 @@ local function pascalcase(str) end return pascalcased end +---@param str string +---@return string +local function camelcase(str) + -- same as pascalcase, but first character lowercased. + local ret = pascalcase(str):gsub("^.", string.lower) + return ret -- note: local var required for correct typing +end M.vscode_string_modifiers = { upcase = string.upper, downcase = string.lower, capitalize = capitalize, pascalcase = pascalcase, - camelcase = function(str) - -- same as pascalcase, but first character lowercased. - return pascalcase(str):gsub("^.", string.lower) - end, + camelcase = camelcase, } return M diff --git a/lua/luasnip/util/table.lua b/lua/luasnip/util/table.lua index 78e1aeeac..720fdadf8 100644 --- a/lua/luasnip/util/table.lua +++ b/lua/luasnip/util/table.lua @@ -1,7 +1,7 @@ ---Convert set of values to a list of those values. ---@generic T ----@param tbl T|T[]|{[T]: boolean} ----@return {[T]: boolean} +---@param tbl {[T]: boolean} +---@return T[] local function set_to_list(tbl) local ls = {} @@ -14,7 +14,7 @@ end ---Convert value or list of values to a table of booleans for fast lookup. ---@generic T ----@param values T|T[]|{[T]: boolean} +---@param values T|T[]? ---@return {[T]: boolean} local function list_to_set(values) if values == nil then diff --git a/lua/luasnip/util/types.lua b/lua/luasnip/util/types.lua index a9ded11d1..87bb8acd3 100644 --- a/lua/luasnip/util/types.lua +++ b/lua/luasnip/util/types.lua @@ -1,4 +1,5 @@ -return { +---@enum LuaSnip.NodeType +local NodeType = { textNode = 1, insertNode = 2, functionNode = 3, @@ -8,27 +9,60 @@ return { snippet = 7, exitNode = 8, restoreNode = 9, - node_types = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, - names = { - "textNode", - "insertNode", - "functionNode", - "snippetNode", - "choiceNode", - "dynamicNode", - "snippet", - "exitNode", - "restoreNode", +} + +local M = setmetatable({}, { __index = NodeType }) +-- NOTE: The metatable is set so that callers of this `types` module can do +-- `types.insertNode` to get a named value from the enum, while leaving the +-- enum definition standalone to avoid adding unnecessary enum fields. + +local refs = { + { value = NodeType.textNode, name = "textNode", pascal_name = "TextNode" }, + { + value = NodeType.insertNode, + name = "insertNode", + pascal_name = "InsertNode", + }, + { + value = NodeType.functionNode, + name = "functionNode", + pascal_name = "FunctionNode", + }, + { + value = NodeType.snippetNode, + name = "snippetNode", + pascal_name = "SnippetNode", + }, + { + value = NodeType.choiceNode, + name = "choiceNode", + pascal_name = "ChoiceNode", + }, + { + value = NodeType.dynamicNode, + name = "dynamicNode", + pascal_name = "DynamicNode", }, - names_pascal_case = { - "TextNode", - "InsertNode", - "FunctionNode", - "SnippetNode", - "ChoiceNode", - "DynamicNode", - "Snippet", - "ExitNode", - "RestoreNode", + { value = NodeType.snippet, name = "snippet", pascal_name = "Snippet" }, + { value = NodeType.exitNode, name = "exitNode", pascal_name = "ExitNode" }, + { + value = NodeType.restoreNode, + name = "restoreNode", + pascal_name = "RestoreNode", }, } + +---@type LuaSnip.NodeType[] +M.node_types = {} +---@type string[] +M.names = {} +---@type string[] +M.names_pascal_case = {} + +for _, ref in ipairs(refs) do + table.insert(M.node_types, ref.value) + table.insert(M.names, ref.name) + table.insert(M.names_pascal_case, ref.pascal_name) +end + +return M diff --git a/lua/luasnip/util/util.lua b/lua/luasnip/util/util.lua index 144dc71d2..68f50cea6 100644 --- a/lua/luasnip/util/util.lua +++ b/lua/luasnip/util/util.lua @@ -59,16 +59,18 @@ local function indent(text, indentstring) end --- In-place expands tabs in `text`. ---- Difficulties: ---- we cannot simply replace tabs with a given number of spaces, the tabs align ---- text at multiples of `tabwidth`. This is also the reason we need the number ---- of columns the text is already indented by (otherwise we can only start a 0). ----@param text string[], multiline string. ----@param tabwidth number, displaycolumns one tab should shift following text ---- by. ----@param parent_indent_displaycolumns number, displaycolumn this text is ---- already at. ----@return string[], `text` (only for simple nesting). +--- +--- _Difficulties_: +--- We cannot simply replace tabs with a given number of spaces, the tabs align +--- text at multiples of `tabwidth`. +--- This is also the reason we need the number of columns the text is already +--- indented by (otherwise we can only start a 0). +--- +---@param text string[] multiline string. +---@param tabwidth number displaycolumns one tab should shift following text by. +---@param parent_indent_displaycolumns number displaycolumn this text is +--- already at. +---@return string[] _ `text` (only for simple nesting). local function expand_tabs(text, tabwidth, parent_indent_displaycolumns) for i, line in ipairs(text) do local new_line = "" @@ -171,8 +173,9 @@ local function put(text, pos) pos[2] = (#text > 1 and 0 or pos[2]) + #text[#text] end ---[[ Wraps the value in a table if it's not one, makes - the first element an empty str if the table is empty]] +--- Wraps the value in a table if it's not one, makes +--- the first element an empty str if the table is empty +---@return string[] local function to_string_table(value) if not value then return { "" } @@ -188,13 +191,17 @@ local function to_string_table(value) return value end --- Wrap node in a table if it is not one +--- Wrap node in a table if it is not one +---@param nodes LuaSnip.Node[]|LuaSnip.Node +---@return LuaSnip.Node[] local function wrap_nodes(nodes) -- safe to assume, if nodes has a metatable, it is a single node, not a -- table. if getmetatable(nodes) and nodes.type then + ---@cast nodes LuaSnip.Node return { nodes } else + ---@cast nodes LuaSnip.Node[] return nodes end end @@ -240,6 +247,8 @@ local function buffer_comment_chars() return comments end +---@param table_or_string string|string[] +---@return string[] local function to_line_table(table_or_string) local tbl = to_string_table(table_or_string) @@ -444,6 +453,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 +524,5 @@ return { pos_offset = pos_offset, pos_from_offset = pos_from_offset, shallow_copy = shallow_copy, + copy3 = copy3, }