Skip to content
Draft
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,21 +752,21 @@ ChoiceNodes allow choosing between multiple nodes.

<!-- panvimdoc-ignore-end -->

`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
Expand Down
15 changes: 8 additions & 7 deletions doc/luasnip.txt
Original file line number Diff line number Diff line change
@@ -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 08

==============================================================================
Table of Contents *luasnip-table-of-contents*
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lua/luasnip/_types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 5 additions & 4 deletions lua/luasnip/extras/conditions/expand.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -41,16 +42,16 @@ 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
Expand Down
142 changes: 93 additions & 49 deletions lua/luasnip/extras/conditions/init.lua
Original file line number Diff line number Diff line change
@@ -1,55 +1,99 @@
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, can be used for `condition` in a snippet
--- context.
---
---@class LuaSnip.SnipContext.ConditionObj
---@field func LuaSnip.SnipContext.ConditionFn
---
---@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 in a composable condition object.
---@param func LuaSnip.SnipContext.Condition
---@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`)
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`)
ConditionObj_mt.__add = ConditionObj.or_

--- Returns a condition object equivalent to `self(...) and not other(...)`
---@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`)
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`)
ConditionObj_mt.__mul = ConditionObj.and_

--- Returns a condition object equivalent to `self(...) ~= other(...)`
---@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`)
ConditionObj_mt.__pow = ConditionObj.not_same_as

--- Returns a condition object equivalent to `self(...) == other(...)`
---@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`)
-- This operator might be counter intuitive, but '==' can't be used as it must
-- return a boolean. It's best to use something weird (doesn't have to be used)
ConditionObj_mt.__mod = ConditionObj.same_as

return M
return ConditionObj
100 changes: 45 additions & 55 deletions lua/luasnip/extras/fmt.lua
Original file line number Diff line number Diff line change
@@ -1,53 +1,39 @@
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 {[integer|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
Expand Down Expand Up @@ -102,7 +88,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
Expand Down Expand Up @@ -175,30 +161,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[]
---@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
Expand Down
Loading