aboutsummaryrefslogtreecommitdiff
path: root/runtime
diff options
context:
space:
mode:
Diffstat (limited to 'runtime')
-rw-r--r--runtime/doc/eval.txt47
-rw-r--r--runtime/doc/lua.txt227
-rw-r--r--runtime/doc/repeat.txt11
-rw-r--r--runtime/doc/vim_diff.txt1
-rw-r--r--runtime/lua/vim/lsp/_snippet.lua399
-rw-r--r--runtime/lua/vim/lsp/util.lua75
6 files changed, 549 insertions, 211 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index f27d1e01a0..9d15bd52a5 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -9139,11 +9139,23 @@ synstack({lnum}, {col}) *synstack()*
valid positions.
system({cmd} [, {input}]) *system()* *E677*
- Get the output of {cmd} as a |string| (use |systemlist()| to
- get a |List|). {cmd} is treated exactly as in |jobstart()|.
- Not to be used for interactive commands.
+ Gets the output of {cmd} as a |string| (|systemlist()| returns
+ a |List|) and sets |v:shell_error| to the error code.
+ {cmd} is treated as in |jobstart()|:
+ If {cmd} is a List it runs directly (no 'shell').
+ If {cmd} is a String it runs in the 'shell', like this: >
+ :call jobstart(split(&shell) + split(&shellcmdflag) + ['{cmd}'])
- If {input} is a string it is written to a pipe and passed as
+< Not to be used for interactive commands.
+
+ Result is a String, filtered to avoid platform-specific quirks:
+ - <CR><NL> is replaced with <NL>
+ - NUL characters are replaced with SOH (0x01)
+
+ Example: >
+ :echo system(['ls', expand('%:h')])
+
+< If {input} is a string it is written to a pipe and passed as
stdin to the command. The string is written as-is, line
separators are not changed.
If {input} is a |List| it is written to the pipe as
@@ -9165,29 +9177,12 @@ system({cmd} [, {input}]) *system()* *E677*
Note: Use |shellescape()| or |::S| with |expand()| or
|fnamemodify()| to escape special characters in a command
- argument. Newlines in {cmd} may cause the command to fail.
- The characters in 'shellquote' and 'shellxquote' may also
- cause trouble.
-
- Result is a String. Example: >
- :let files = system("ls " . shellescape(expand('%:h')))
- :let files = system('ls ' . expand('%:h:S'))
-
-< To make the result more system-independent, the shell output
- is filtered to replace <CR> with <NL> for Macintosh, and
- <CR><NL> with <NL> for DOS-like systems.
- To avoid the string being truncated at a NUL, all NUL
- characters are replaced with SOH (0x01).
-
- The command executed is constructed using several options when
- {cmd} is a string: 'shell' 'shellcmdflag' {cmd}
-
- The resulting error code can be found in |v:shell_error|.
+ argument. 'shellquote' and 'shellxquote' must be properly
+ configured. Example: >
+ :echo system('ls '..shellescape(expand('%:h')))
+ :echo system('ls '..expand('%:h:S'))
- Note that any wrong value in the options mentioned above may
- make the function fail. It has also been reported to fail
- when using a security agent application.
- Unlike ":!cmd" there is no automatic check for changed files.
+< Unlike ":!cmd" there is no automatic check for changed files.
Use |:checktime| to force a check.
Can also be used as a |method|: >
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
index 5731569947..032c28c659 100644
--- a/runtime/doc/lua.txt
+++ b/runtime/doc/lua.txt
@@ -1,15 +1,15 @@
*lua.txt* Nvim
- NVIM REFERENCE MANUAL
+ NVIM REFERENCE MANUAL
-Lua engine *lua* *Lua*
+Lua engine *lua* *Lua*
Type |gO| to see the table of contents.
==============================================================================
-INTRODUCTION *lua-intro*
+INTRODUCTION *lua-intro*
The Lua 5.1 language is builtin and always available. Try this command to get
an idea of what lurks beneath: >
@@ -27,11 +27,12 @@ are on 'runtimepath':
~/.config/nvim/lua/foo.lua
then `require('foo')` loads "~/.config/nvim/lua/foo.lua", and
"runtime/lua/foo.lua" is not used. See |lua-require| to understand how Nvim
-finds and loads Lua modules. The conventions are similar to VimL plugins,
-with some extra features. See |lua-require-example| for a walkthrough.
+finds and loads Lua modules. The conventions are similar to those of
+Vimscript |plugin|s, with some extra features. See |lua-require-example| for
+a walkthrough.
==============================================================================
-IMPORTING LUA MODULES *lua-require*
+IMPORTING LUA MODULES *lua-require*
*lua-package-path*
Nvim automatically adjusts `package.path` and `package.cpath` according to
@@ -157,7 +158,7 @@ function without any parentheses. This is most often used to approximate
------------------------------------------------------------------------------
-LUA PLUGIN EXAMPLE *lua-require-example*
+LUA PLUGIN EXAMPLE *lua-require-example*
The following example plugin adds a command `:MakeCharBlob` which transforms
current buffer into a long `unsigned char` array. Lua contains transformation
@@ -234,7 +235,7 @@ lua/charblob.lua: >
}
==============================================================================
-COMMANDS *lua-commands*
+COMMANDS *lua-commands*
These commands execute a Lua chunk from either the command line (:lua, :luado)
or a file (:luafile) on the given line [range]. As always in Lua, each chunk
@@ -298,19 +299,20 @@ arguments separated by " " (space) instead of "\t" (tab).
:luado if bp:match(line) then return "-->\t" .. line end
<
- *:luafile*
+ *:luafile*
:[range]luafile {file}
- Execute Lua script in {file}.
- The whole argument is used as a single file name.
+ Execute Lua script in {file}.
+ The whole argument is used as the filename (like
+ |:edit|), spaces do not need to be escaped.
+ Alternatively you can |:source| Lua files.
- Examples:
- >
+ Examples: >
:luafile script.lua
:luafile %
<
==============================================================================
-luaeval() *lua-eval* *luaeval()*
+luaeval() *lua-eval* *luaeval()*
The (dual) equivalent of "vim.eval" for passing Lua values to Nvim is
"luaeval". "luaeval" takes an expression string and an optional argument used
@@ -324,8 +326,7 @@ semantically equivalent in Lua to:
end
Lua nils, numbers, strings, tables and booleans are converted to their
-respective VimL types. An error is thrown if conversion of any other Lua types
-is attempted.
+respective Vimscript types. Conversion of other Lua types is an error.
The magic global "_A" contains the second argument to luaeval().
@@ -348,21 +349,21 @@ cases there is the following agreement:
3. Table with string keys, at least one of which contains NUL byte, is also
considered to be a dictionary, but this time it is converted to
a |msgpack-special-map|.
- *lua-special-tbl*
+ *lua-special-tbl*
4. Table with `vim.type_idx` key may be a dictionary, a list or floating-point
value:
- - `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to
- a floating-point 1.0. Note that by default integral Lua numbers are
- converted to |Number|s, non-integral are converted to |Float|s. This
+ - `{[vim.type_idx]=vim.types.float, [vim.val_idx]=1}` is converted to
+ a floating-point 1.0. Note that by default integral Lua numbers are
+ converted to |Number|s, non-integral are converted to |Float|s. This
variant allows integral |Float|s.
- - `{[vim.type_idx]=vim.types.dictionary}` is converted to an empty
- dictionary, `{[vim.type_idx]=vim.types.dictionary, [42]=1, a=2}` is
- converted to a dictionary `{'a': 42}`: non-string keys are ignored.
- Without `vim.type_idx` key tables with keys not fitting in 1., 2. or 3.
+ - `{[vim.type_idx]=vim.types.dictionary}` is converted to an empty
+ dictionary, `{[vim.type_idx]=vim.types.dictionary, [42]=1, a=2}` is
+ converted to a dictionary `{'a': 42}`: non-string keys are ignored.
+ Without `vim.type_idx` key tables with keys not fitting in 1., 2. or 3.
are errors.
- - `{[vim.type_idx]=vim.types.list}` is converted to an empty list. As well
- as `{[vim.type_idx]=vim.types.list, [42]=1}`: integral keys that do not
- form a 1-step sequence from 1 to N are ignored, as well as all
+ - `{[vim.type_idx]=vim.types.list}` is converted to an empty list. As well
+ as `{[vim.type_idx]=vim.types.list, [42]=1}`: integral keys that do not
+ form a 1-step sequence from 1 to N are ignored, as well as all
non-integral keys.
Examples: >
@@ -373,13 +374,13 @@ Examples: >
: endfunction
:echo Rand(1,10)
-Note: second argument to `luaeval` undergoes VimL to Lua conversion
-("marshalled"), so changes to Lua containers do not affect values in VimL.
-Return value is also always converted. When converting,
-|msgpack-special-dict|s are treated specially.
+Note: second argument to `luaeval` is converted ("marshalled") from Vimscript
+to Lua, so changes to Lua containers do not affect values in Vimscript. Return
+value is also always converted. When converting, |msgpack-special-dict|s are
+treated specially.
==============================================================================
-Vimscript v:lua interface *v:lua-call*
+Vimscript v:lua interface *v:lua-call*
From Vimscript the special `v:lua` prefix can be used to call Lua functions
which are global or accessible from global tables. The expression >
@@ -419,7 +420,7 @@ Note: `v:lua` without a call is not allowed in a Vimscript expression:
==============================================================================
-Lua standard modules *lua-stdlib*
+Lua standard modules *lua-stdlib*
The Nvim Lua "standard library" (stdlib) is the `vim` module, which exposes
various functions and sub-modules. It is always loaded, thus require("vim")
@@ -453,7 +454,7 @@ Note that underscore-prefixed functions (e.g. "_os_proc_children") are
internal/private and must not be used by plugins.
------------------------------------------------------------------------------
-VIM.LOOP *lua-loop* *vim.loop*
+VIM.LOOP *lua-loop* *vim.loop*
`vim.loop` exposes all features of the Nvim event-loop. This is a low-level
API that provides functionality for networking, filesystem, and process
@@ -464,7 +465,7 @@ management. Try this command to see available functions: >
Reference: https://github.com/luvit/luv/blob/master/docs.md
Examples: https://github.com/luvit/luv/tree/master/examples
- *E5560* *lua-loop-callbacks*
+ *E5560* *lua-loop-callbacks*
It is an error to directly invoke `vim.api` functions (except |api-fast|) in
`vim.loop` callbacks. For example, this is an error: >
@@ -500,7 +501,7 @@ Example: repeating timer
print('sleeping');
-Example: File-change detection *watch-file*
+Example: File-change detection *watch-file*
1. Save this code to a file.
2. Execute it with ":luafile %".
3. Use ":Watch %" to watch any file.
@@ -526,7 +527,7 @@ Example: File-change detection *watch-file*
"command! -nargs=1 Watch call luaeval('watch_file(_A)', expand('<args>'))")
-Example: TCP echo-server *tcp-server*
+Example: TCP echo-server *tcp-server*
1. Save this code to a file.
2. Execute it with ":luafile %".
3. Note the port number.
@@ -556,7 +557,7 @@ Example: TCP echo-server *tcp-server*
print('TCP echo-server listening on port: '..server:getsockname().port)
------------------------------------------------------------------------------
-VIM.HIGHLIGHT *lua-highlight*
+VIM.HIGHLIGHT *lua-highlight*
Nvim includes a function for highlighting a selection on yank (see for example
https://github.com/machakann/vim-highlightedyank). To enable it, add
@@ -591,21 +592,19 @@ vim.highlight.range({bufnr}, {ns}, {higroup}, {start}, {finish}, {rtype}, {inclu
range is inclusive (default false).
------------------------------------------------------------------------------
-VIM.REGEX *lua-regex*
+VIM.REGEX *lua-regex*
Vim regexes can be used directly from lua. Currently they only allow
matching within a single line.
-vim.regex({re}) *vim.regex()*
+vim.regex({re}) *vim.regex()*
+ Parse the Vim regex {re} and return a regex object. Regexes are
+ "magic" and case-insensitive by default, regardless of 'magic' and
+ 'ignorecase'. The can be controlled with flags, see |/magic|.
- Parse the regex {re} and return a regex object. 'magic' and
- 'ignorecase' options are ignored, lua regexes always defaults to magic
- and ignoring case. The behavior can be changed with flags in
- the beginning of the string |/magic|.
+Methods on the regex object:
-Regex objects support the following methods:
-
-regex:match_str({str}) *regex:match_str()*
+regex:match_str({str}) *regex:match_str()*
Match the string against the regex. If the string should match the
regex precisely, surround the regex with `^` and `$`.
If the was a match, the byte indices for the beginning and end of
@@ -613,7 +612,7 @@ regex:match_str({str}) *regex:match_str()*
As any integer is truth-y, `regex:match()` can be directly used
as a condition in an if-statement.
-regex:match_line({bufnr}, {line_idx}[, {start}, {end}]) *regex:match_line()*
+regex:match_line({bufnr}, {line_idx}[, {start}, {end}]) *regex:match_line()*
Match line {line_idx} (zero-based) in buffer {bufnr}. If {start} and
{end} are supplied, match only this byte index range. Otherwise see
|regex:match_str()|. If {start} is used, then the returned byte
@@ -692,67 +691,65 @@ VIM.MPACK *lua-mpack*
The *vim.mpack* module provides packing and unpacking of lua objects to
msgpack encoded strings. |vim.NIL| and |vim.empty_dict()| are supported.
-vim.mpack.pack({obj}) *vim.mpack.pack*
+vim.mpack.pack({obj}) *vim.mpack.pack*
Packs a lua object {obj} and returns the msgpack representation as
a string
-vim.mpack.unpack({str}) *vim.mpack.unpack*
+vim.mpack.unpack({str}) *vim.mpack.unpack*
Unpacks the msgpack encoded {str} and returns a lua object
------------------------------------------------------------------------------
-VIM *lua-builtin*
+VIM *lua-builtin*
-vim.api.{func}({...}) *vim.api*
+vim.api.{func}({...}) *vim.api*
Invokes Nvim |API| function {func} with arguments {...}.
Example: call the "nvim_get_current_line()" API function: >
print(tostring(vim.api.nvim_get_current_line()))
-vim.version() *vim.version*
- Returns the version of the current neovim build.
+vim.version() *vim.version*
+ Gets the version of the current Nvim build.
-vim.in_fast_event() *vim.in_fast_event()*
+vim.in_fast_event() *vim.in_fast_event()*
Returns true if the code is executing as part of a "fast" event
handler, where most of the API is disabled. These are low-level events
(e.g. |lua-loop-callbacks|) which can be invoked whenever Nvim polls
for input. When this is `false` most API functions are callable (but
may be subject to other restrictions such as |textlock|).
-vim.NIL *vim.NIL*
- Special value used to represent NIL in msgpack-rpc and |v:null| in
- vimL interaction, and similar cases. Lua `nil` cannot be used as
- part of a lua table representing a Dictionary or Array, as it
- is equivalent to a missing value: `{"foo", nil}` is the same as
- `{"foo"}`
+vim.NIL *vim.NIL*
+ Special value representing NIL in |RPC| and |v:null| in Vimscript
+ conversion, and similar cases. Lua `nil` cannot be used as part of
+ a Lua table representing a Dictionary or Array, because it is
+ treated as missing: `{"foo", nil}` is the same as `{"foo"}`.
-vim.empty_dict() *vim.empty_dict()*
- Creates a special table which will be converted to an empty
- dictionary when converting lua values to vimL or API types. The
- table is empty, and this property is marked using a metatable. An
- empty table `{}` without this metatable will default to convert to
- an array/list.
+vim.empty_dict() *vim.empty_dict()*
+ Creates a special empty table (marked with a metatable), which Nvim
+ converts to an empty dictionary when translating Lua values to
+ Vimscript or API types. Nvim by default converts an empty table `{}`
+ without this metatable to an list/array.
- Note: if numeric keys are added to the table, the metatable will be
- ignored and the dict converted to a list/array anyway.
+ Note: if numeric keys are present in the table, Nvim ignores the
+ metatable marker and converts the dict to a list/array anyway.
-vim.rpcnotify({channel}, {method}[, {args}...]) *vim.rpcnotify()*
- Sends {event} to {channel} via |RPC| and returns immediately.
- If {channel} is 0, the event is broadcast to all channels.
+vim.rpcnotify({channel}, {method}[, {args}...]) *vim.rpcnotify()*
+ Sends {event} to {channel} via |RPC| and returns immediately. If
+ {channel} is 0, the event is broadcast to all channels.
- This function also works in a fast callback |lua-loop-callbacks|.
+ This function also works in a fast callback |lua-loop-callbacks|.
-vim.rpcrequest({channel}, {method}[, {args}...]) *vim.rpcrequest()*
- Sends a request to {channel} to invoke {method} via
- |RPC| and blocks until a response is received.
+vim.rpcrequest({channel}, {method}[, {args}...]) *vim.rpcrequest()*
+ Sends a request to {channel} to invoke {method} via |RPC| and blocks
+ until a response is received.
- Note: NIL values as part of the return value is represented as
- |vim.NIL| special value
+ Note: NIL values as part of the return value is represented as
+ |vim.NIL| special value
-vim.stricmp({a}, {b}) *vim.stricmp()*
+vim.stricmp({a}, {b}) *vim.stricmp()*
Compares strings case-insensitively. Returns 0, 1 or -1 if strings
are equal, {a} is greater than {b} or {a} is lesser than {b},
respectively.
-vim.str_utfindex({str}[, {index}]) *vim.str_utfindex()*
+vim.str_utfindex({str}[, {index}]) *vim.str_utfindex()*
Convert byte index to UTF-32 and UTF-16 indicies. If {index} is not
supplied, the length of the string is used. All indicies are zero-based.
Returns two values: the UTF-32 and UTF-16 indicies respectively.
@@ -840,40 +837,40 @@ vim.wait({time} [, {callback}, {interval}, {fast_only}]) *vim.wait()*
end
<
-vim.type_idx *vim.type_idx*
- Type index for use in |lua-special-tbl|. Specifying one of the
- values from |vim.types| allows typing the empty table (it is
- unclear whether empty Lua table represents empty list or empty array)
- and forcing integral numbers to be |Float|. See |lua-special-tbl| for
- more details.
+vim.type_idx *vim.type_idx*
+ Type index for use in |lua-special-tbl|. Specifying one of the values
+ from |vim.types| allows typing the empty table (it is unclear whether
+ empty Lua table represents empty list or empty array) and forcing
+ integral numbers to be |Float|. See |lua-special-tbl| for more
+ details.
-vim.val_idx *vim.val_idx*
- Value index for tables representing |Float|s. A table representing
- floating-point value 1.0 looks like this: >
+vim.val_idx *vim.val_idx*
+ Value index for tables representing |Float|s. A table representing
+ floating-point value 1.0 looks like this: >
{
[vim.type_idx] = vim.types.float,
[vim.val_idx] = 1.0,
}
-< See also |vim.type_idx| and |lua-special-tbl|.
-
-vim.types *vim.types*
- Table with possible values for |vim.type_idx|. Contains two sets
- of key-value pairs: first maps possible values for |vim.type_idx|
- to human-readable strings, second maps human-readable type names to
- values for |vim.type_idx|. Currently contains pairs for `float`,
- `array` and `dictionary` types.
-
- Note: one must expect that values corresponding to `vim.types.float`,
- `vim.types.array` and `vim.types.dictionary` fall under only two
- following assumptions:
- 1. Value may serve both as a key and as a value in a table. Given the
- properties of Lua tables this basically means “value is not `nil`”.
- 2. For each value in `vim.types` table `vim.types[vim.types[value]]`
- is the same as `value`.
- No other restrictions are put on types, and it is not guaranteed that
- values corresponding to `vim.types.float`, `vim.types.array` and
- `vim.types.dictionary` will not change or that `vim.types` table will
- only contain values for these three types.
+< See also |vim.type_idx| and |lua-special-tbl|.
+
+vim.types *vim.types*
+ Table with possible values for |vim.type_idx|. Contains two sets of
+ key-value pairs: first maps possible values for |vim.type_idx| to
+ human-readable strings, second maps human-readable type names to
+ values for |vim.type_idx|. Currently contains pairs for `float`,
+ `array` and `dictionary` types.
+
+ Note: one must expect that values corresponding to `vim.types.float`,
+ `vim.types.array` and `vim.types.dictionary` fall under only two
+ following assumptions:
+ 1. Value may serve both as a key and as a value in a table. Given the
+ properties of Lua tables this basically means “value is not `nil`”.
+ 2. For each value in `vim.types` table `vim.types[vim.types[value]]`
+ is the same as `value`.
+ No other restrictions are put on types, and it is not guaranteed that
+ values corresponding to `vim.types.float`, `vim.types.array` and
+ `vim.types.dictionary` will not change or that `vim.types` table will
+ only contain values for these three types.
------------------------------------------------------------------------------
LUA-VIMSCRIPT BRIDGE *lua-vimscript*
@@ -966,8 +963,8 @@ vim.env *vim.env*
*lua-vim-optlocal*
*lua-vim-setlocal*
-In vimL, there is a succint and simple way to set options. For more
-information, see |set-option|. In Lua, the corresponding method is `vim.opt`.
+In Vimscript, there is an way to set options |set-option|. In Lua, the
+corresponding method is `vim.opt`.
`vim.opt` provides several conveniences for setting and controlling options
from within Lua.
@@ -975,18 +972,18 @@ from within Lua.
Examples: ~
To set a boolean toggle:
- In vimL:
+ In Vimscript:
`set number`
In Lua:
`vim.opt.number = true`
To set an array of values:
- In vimL:
+ In Vimscript:
`set wildignore=*.o,*.a,__pycache__`
In Lua, there are two ways you can do this now. One is very similar to
- the vimL way:
+ the Vimscript form:
`vim.opt.wildignore = '*.o,*.a,__pycache__'`
However, vim.opt also supports a more elegent way of setting
@@ -1019,7 +1016,7 @@ from within Lua.
vim.opt.wildignore:remove { "node_modules" }
<
To set a map of values:
- In vimL:
+ In Vimscript:
`set listchars=space:_,tab:>~`
In Lua:
diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt
index 6851cd1511..5fdd5fc3c0 100644
--- a/runtime/doc/repeat.txt
+++ b/runtime/doc/repeat.txt
@@ -172,15 +172,16 @@ Using Vim scripts *using-scripts*
For writing a Vim script, see chapter 41 of the user manual |usr_41.txt|.
*:so* *:source* *load-vim-script*
-:so[urce] {file} Runs |Ex| commands or Lua code (".lua" files) read
- from {file}.
+:[range]so[urce] [file] Runs |Ex| commands or Lua code (".lua" files) from
+ [file], or from the current buffer if no [file] is
+ given.
Triggers the |SourcePre| autocommand.
*:source!*
-:so[urce]! {file} Runs |Normal-mode| commands read from {file}. When
- used after |:global|, |:argdo|, |:windo|, |:bufdo|, in
+:[range]so[urce]! {file}
+ Runs |Normal-mode| commands from {file}. When used
+ after |:global|, |:argdo|, |:windo|, |:bufdo|, in
a loop or when another command follows the display
won't be updated while executing the commands.
- Cannot be used in the |sandbox|.
*:ru* *:runtime*
:ru[ntime][!] [where] {file} ..
diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt
index 4dea053bc7..64824b2e3f 100644
--- a/runtime/doc/vim_diff.txt
+++ b/runtime/doc/vim_diff.txt
@@ -472,6 +472,7 @@ Compile-time features:
X11 integration (see |x11-selection|)
Eval:
+ Vim9script
*js_encode()*
*js_decode()*
*v:none* (used by Vim to represent JavaScript "undefined"); use |v:null| instead.
diff --git a/runtime/lua/vim/lsp/_snippet.lua b/runtime/lua/vim/lsp/_snippet.lua
new file mode 100644
index 0000000000..0140b0aee3
--- /dev/null
+++ b/runtime/lua/vim/lsp/_snippet.lua
@@ -0,0 +1,399 @@
+local P = {}
+
+---Take characters until the target characters (The escape sequence is '\' + char)
+---@param targets string[] The character list for stop consuming text.
+---@param specials string[] If the character isn't contained in targets/specials, '\' will be left.
+P.take_until = function(targets, specials)
+ targets = targets or {}
+ specials = specials or {}
+
+ return function(input, pos)
+ local new_pos = pos
+ local raw = {}
+ local esc = {}
+ while new_pos <= #input do
+ local c = string.sub(input, new_pos, new_pos)
+ if c == '\\' then
+ table.insert(raw, '\\')
+ new_pos = new_pos + 1
+ c = string.sub(input, new_pos, new_pos)
+ if not vim.tbl_contains(targets, c) and not vim.tbl_contains(specials, c) then
+ table.insert(esc, '\\')
+ end
+ table.insert(raw, c)
+ table.insert(esc, c)
+ new_pos = new_pos + 1
+ else
+ if vim.tbl_contains(targets, c) then
+ break
+ end
+ table.insert(raw, c)
+ table.insert(esc, c)
+ new_pos = new_pos + 1
+ end
+ end
+
+ if new_pos == pos then
+ return P.unmatch(pos)
+ end
+
+ return {
+ parsed = true,
+ value = {
+ raw = table.concat(raw, ''),
+ esc = table.concat(esc, '')
+ },
+ pos = new_pos,
+ }
+ end
+end
+
+P.unmatch = function(pos)
+ return {
+ parsed = false,
+ value = nil,
+ pos = pos,
+ }
+end
+
+P.map = function(parser, map)
+ return function(input, pos)
+ local result = parser(input, pos)
+ if result.parsed then
+ return {
+ parsed = true,
+ value = map(result.value),
+ pos = result.pos,
+ }
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.lazy = function(factory)
+ return function(input, pos)
+ return factory()(input, pos)
+ end
+end
+
+P.token = function(token)
+ return function(input, pos)
+ local maybe_token = string.sub(input, pos, pos + #token - 1)
+ if token == maybe_token then
+ return {
+ parsed = true,
+ value = maybe_token,
+ pos = pos + #token,
+ }
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.pattern = function(p)
+ return function(input, pos)
+ local maybe_match = string.match(string.sub(input, pos), '^' .. p)
+ if maybe_match then
+ return {
+ parsed = true,
+ value = maybe_match,
+ pos = pos + #maybe_match,
+ }
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.many = function(parser)
+ return function(input, pos)
+ local values = {}
+ local new_pos = pos
+ while new_pos <= #input do
+ local result = parser(input, new_pos)
+ if not result.parsed then
+ break
+ end
+ table.insert(values, result.value)
+ new_pos = result.pos
+ end
+ if #values > 0 then
+ return {
+ parsed = true,
+ value = values,
+ pos = new_pos,
+ }
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.any = function(...)
+ local parsers = { ... }
+ return function(input, pos)
+ for _, parser in ipairs(parsers) do
+ local result = parser(input, pos)
+ if result.parsed then
+ return result
+ end
+ end
+ return P.unmatch(pos)
+ end
+end
+
+P.opt = function(parser)
+ return function(input, pos)
+ local result = parser(input, pos)
+ return {
+ parsed = true,
+ value = result.value,
+ pos = result.pos,
+ }
+ end
+end
+
+P.seq = function(...)
+ local parsers = { ... }
+ return function(input, pos)
+ local values = {}
+ local new_pos = pos
+ for _, parser in ipairs(parsers) do
+ local result = parser(input, new_pos)
+ if result.parsed then
+ table.insert(values, result.value)
+ new_pos = result.pos
+ else
+ return P.unmatch(pos)
+ end
+ end
+ return {
+ parsed = true,
+ value = values,
+ pos = new_pos,
+ }
+ end
+end
+
+local Node = {}
+
+Node.Type = {
+ SNIPPET = 0,
+ TABSTOP = 1,
+ PLACEHOLDER = 2,
+ VARIABLE = 3,
+ CHOICE = 4,
+ TRANSFORM = 5,
+ FORMAT = 6,
+ TEXT = 7,
+}
+
+function Node:__tostring()
+ local insert_text = {}
+ if self.type == Node.Type.SNIPPET then
+ for _, c in ipairs(self.children) do
+ table.insert(insert_text, tostring(c))
+ end
+ elseif self.type == Node.Type.CHOICE then
+ table.insert(insert_text, self.items[1])
+ elseif self.type == Node.Type.PLACEHOLDER then
+ for _, c in ipairs(self.children or {}) do
+ table.insert(insert_text, tostring(c))
+ end
+ elseif self.type == Node.Type.TEXT then
+ table.insert(insert_text, self.esc)
+ end
+ return table.concat(insert_text, '')
+end
+
+--@see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
+
+local S = {}
+S.dollar = P.token('$')
+S.open = P.token('{')
+S.close = P.token('}')
+S.colon = P.token(':')
+S.slash = P.token('/')
+S.comma = P.token(',')
+S.pipe = P.token('|')
+S.plus = P.token('+')
+S.minus = P.token('-')
+S.question = P.token('?')
+S.int = P.map(P.pattern('[0-9]+'), function(value)
+ return tonumber(value, 10)
+end)
+S.var = P.pattern('[%a_][%w_]+')
+S.text = function(targets, specials)
+ return P.map(P.take_until(targets, specials), function(value)
+ return setmetatable({
+ type = Node.Type.TEXT,
+ raw = value.raw,
+ esc = value.esc,
+ }, Node)
+ end)
+end
+
+S.toplevel = P.lazy(function()
+ return P.any(S.placeholder, S.tabstop, S.variable, S.choice)
+end)
+
+S.format = P.any(
+ P.map(P.seq(S.dollar, S.int), function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[2],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.slash, P.any(
+ P.token('upcase'),
+ P.token('downcase'),
+ P.token('capitalize'),
+ P.token('camelcase'),
+ P.token('pascalcase')
+ ), S.close), function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ modifier = values[6],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.any(
+ P.seq(S.question, P.take_until({ ':' }, { '\\' }), S.colon, P.take_until({ '}' }, { '\\' })),
+ P.seq(S.plus, P.take_until({ '}' }, { '\\' })),
+ P.seq(S.minus, P.take_until({ '}' }, { '\\' }))
+ ), S.close), function(values)
+ return setmetatable({
+ type = Node.Type.FORMAT,
+ capture_index = values[3],
+ if_text = values[5][2].esc,
+ else_text = (values[5][4] or {}).esc,
+ }, Node)
+ end)
+)
+
+S.transform = P.map(P.seq(
+ S.slash,
+ P.take_until({ '/' }, { '\\' }),
+ S.slash,
+ P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))),
+ S.slash,
+ P.opt(P.pattern('[ig]+'))
+), function(values)
+ return setmetatable({
+ type = Node.Type.TRANSFORM,
+ pattern = values[2].raw,
+ format = values[4],
+ option = values[6],
+ }, Node)
+end)
+
+S.tabstop = P.any(
+ P.map(P.seq(S.dollar, S.int), function(values)
+ return setmetatable({
+ type = Node.Type.TABSTOP,
+ tabstop = values[2],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.TABSTOP,
+ tabstop = values[3],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.int, S.transform, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.TABSTOP,
+ tabstop = values[3],
+ transform = values[4],
+ }, Node)
+ end)
+)
+
+S.placeholder = P.any(
+ P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
+ return setmetatable({
+ type = Node.Type.PLACEHOLDER,
+ tabstop = values[3],
+ children = values[5],
+ }, Node)
+ end)
+)
+
+S.choice = P.map(P.seq(
+ S.dollar,
+ S.open,
+ S.int,
+ S.pipe,
+ P.many(
+ P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values)
+ return values[1].esc
+ end)
+ ),
+ S.pipe,
+ S.close
+), function(values)
+ return setmetatable({
+ type = Node.Type.CHOICE,
+ tabstop = values[3],
+ items = values[5],
+ }, Node)
+end)
+
+S.variable = P.any(
+ P.map(P.seq(S.dollar, S.var), function(values)
+ return setmetatable({
+ type = Node.Type.VARIABLE,
+ name = values[2],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.var, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.VARIABLE,
+ name = values[3],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.var, S.transform, S.close), function(values)
+ return setmetatable({
+ type = Node.Type.VARIABLE,
+ name = values[3],
+ transform = values[4],
+ }, Node)
+ end),
+ P.map(P.seq(S.dollar, S.open, S.var, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
+ return setmetatable({
+ type = Node.Type.VARIABLE,
+ name = values[3],
+ children = values[5],
+ }, Node)
+ end)
+)
+
+S.snippet = P.map(P.many(P.any(S.toplevel, S.text({ '$' }, { '}', '\\' }))), function(values)
+ return setmetatable({
+ type = Node.Type.SNIPPET,
+ children = values,
+ }, Node)
+end)
+
+local M = {}
+
+---The snippet node type enum
+---@types table<string, number>
+M.NodeType = Node.Type
+
+---Parse snippet string and returns the AST
+---@param input string
+---@return table
+function M.parse(input)
+ local result = S.snippet(input, 1)
+ if not result.parsed then
+ error('snippet parsing failed.')
+ end
+ return result.value
+end
+
+return M
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index a4c8b69f6c..5a22a311e0 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -1,4 +1,5 @@
local protocol = require 'vim.lsp.protocol'
+local snippet = require 'vim.lsp._snippet'
local vim = vim
local validate = vim.validate
local api = vim.api
@@ -523,74 +524,18 @@ function M.apply_text_document_edit(text_document_edit, index)
M.apply_text_edits(text_document_edit.edits, bufnr)
end
----@private
---- Recursively parses snippets in a completion entry.
----
----@param input (string) Snippet text to parse for snippets
----@param inner (bool) Whether this function is being called recursively
----@returns 2-tuple of strings: The first is the parsed result, the second is the
----unparsed rest of the input
-local function parse_snippet_rec(input, inner)
- local res = ""
-
- local close, closeend = nil, nil
- if inner then
- close, closeend = input:find("}", 1, true)
- while close ~= nil and input:sub(close-1,close-1) == "\\" do
- close, closeend = input:find("}", closeend+1, true)
- end
- end
-
- local didx = input:find('$', 1, true)
- if didx == nil and close == nil then
- return input, ""
- elseif close ~=nil and (didx == nil or close < didx) then
- -- No inner placeholders
- return input:sub(0, close-1), input:sub(closeend+1)
- end
-
- res = res .. input:sub(0, didx-1)
- input = input:sub(didx+1)
-
- local tabstop, tabstopend = input:find('^%d+')
- local placeholder, placeholderend = input:find('^{%d+:')
- local choice, choiceend = input:find('^{%d+|')
-
- if tabstop then
- input = input:sub(tabstopend+1)
- elseif choice then
- input = input:sub(choiceend+1)
- close, closeend = input:find("|}", 1, true)
-
- res = res .. input:sub(0, close-1)
- input = input:sub(closeend+1)
- elseif placeholder then
- -- TODO: add support for variables
- input = input:sub(placeholderend+1)
-
- -- placeholders and variables are recursive
- while input ~= "" do
- local r, tail = parse_snippet_rec(input, true)
- r = r:gsub("\\}", "}")
-
- res = res .. r
- input = tail
- end
- else
- res = res .. "$"
- end
-
- return res, input
-end
-
--- Parses snippets in a completion entry.
---
----@param input (string) unparsed snippet
----@returns (string) parsed snippet
+---@param input string unparsed snippet
+---@returns string parsed snippet
function M.parse_snippet(input)
- local res, _ = parse_snippet_rec(input, false)
-
- return res
+ local ok, parsed = pcall(function()
+ return tostring(snippet.parse(input))
+ end)
+ if not ok then
+ return input
+ end
+ return parsed
end
---@private