mirror of
https://github.com/neovim/neovim.git
synced 2026-09-09 15:35:51 +00:00
Problem: LuaLS struggles with the generics used in Nvim's runtime, requiring broad diagnostic suppressions. Indexing is also slow. Solution: Use EmmyLua for type checks in the build and CI. It offers more sophisticated type checking, substantially better support for generics, and much better flow analysis. Correct the affected annotations. Use `@internal`, supported directly by EmmyLua, instead of `@nodoc` for shared internal declarations, and support it in the help parser. AI-assisted
181 lines
6.1 KiB
Lua
181 lines
6.1 KiB
Lua
--- Grammar for LSP snippets, based on https://microsoft.github.io/language-server-protocol/specification/#snippet_syntax
|
|
|
|
local lpeg = vim.lpeg
|
|
local P, S, R, V = lpeg.P, lpeg.S, lpeg.R, lpeg.V
|
|
local C, Cg, Ct = lpeg.C, lpeg.Cg, lpeg.Ct
|
|
|
|
local M = {}
|
|
|
|
local alpha = R('az', 'AZ')
|
|
local backslash = P('\\')
|
|
local colon = P(':')
|
|
local dollar = P('$')
|
|
local int = R('09') ^ 1
|
|
local l_brace, r_brace = P('{'), P('}')
|
|
local pipe = P('|')
|
|
local slash = P('/')
|
|
local underscore = P('_')
|
|
local var = Cg((underscore + alpha) * ((underscore + alpha + int) ^ 0), 'name')
|
|
local format_capture = Cg(int / tonumber, 'capture')
|
|
local format_modifier = Cg(P('upcase') + P('downcase') + P('capitalize'), 'modifier')
|
|
local tabstop = Cg(int / tonumber, 'tabstop')
|
|
|
|
-- These characters are always escapable in text nodes no matter the context.
|
|
local escapable = '$}\\'
|
|
|
|
--- Returns a function that unescapes occurrences of "special" characters.
|
|
---
|
|
--- @param special? string
|
|
--- @return fun(match: string): string
|
|
local function escape_text(special)
|
|
special = special or escapable
|
|
return function(match)
|
|
local escaped = match:gsub('\\(.)', function(c)
|
|
return special:find(c) and c or '\\' .. c
|
|
end)
|
|
return escaped
|
|
end
|
|
end
|
|
|
|
--- Returns a pattern for text nodes. Will match characters in `escape` when preceded by a backslash,
|
|
--- and will stop with characters in `stop_with`.
|
|
---
|
|
--- @param escape string
|
|
--- @param stop_with? string
|
|
--- @return vim.lpeg.Pattern
|
|
local function text(escape, stop_with)
|
|
stop_with = stop_with or escape
|
|
return (backslash * S(escape)) + (P(1) - S(stop_with))
|
|
end
|
|
|
|
-- For text nodes inside curly braces. It stops parsing when reaching an escapable character.
|
|
local braced_text = (text(escapable) ^ 0) / escape_text()
|
|
|
|
-- Within choice nodes, \ also escapes comma and pipe characters.
|
|
local choice_text = C(text(escapable .. ',|') ^ 1) / escape_text(escapable .. ',|')
|
|
|
|
-- Within format nodes, make sure we stop at /
|
|
local format_text = C(text(escapable, escapable .. '/') ^ 1) / escape_text()
|
|
|
|
local if_text, else_text = Cg(braced_text, 'if_text'), Cg(braced_text, 'else_text')
|
|
|
|
-- Within ternary condition format nodes, make sure we stop at :
|
|
local if_till_colon_text = Cg(C(text(escapable, escapable .. ':') ^ 1) / escape_text(), 'if_text')
|
|
|
|
-- Matches the string inside //, allowing escaping of the closing slash.
|
|
local regex = Cg(text('/') ^ 1, 'regex')
|
|
|
|
-- Regex constructor flags (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp#parameters).
|
|
local options = Cg(S('dgimsuvy') ^ 0, 'options')
|
|
|
|
--- @enum vim.snippet.Type
|
|
local Type = {
|
|
Tabstop = 1,
|
|
Placeholder = 2,
|
|
Choice = 3,
|
|
Variable = 4,
|
|
Format = 5,
|
|
Text = 6,
|
|
Snippet = 7,
|
|
}
|
|
M.NodeType = Type
|
|
|
|
--- @class vim.snippet.Node<T>: { type: vim.snippet.Type, data: T }
|
|
--- @class vim.snippet.TabstopData: { tabstop: integer }
|
|
--- @class vim.snippet.TextData: { text: string }
|
|
--- @class vim.snippet.PlaceholderData: { tabstop: integer, value: vim.snippet.Node<any> }
|
|
--- @class vim.snippet.ChoiceData: { tabstop: integer, values: string[] }
|
|
--- @class vim.snippet.VariableData: { name: string, default?: vim.snippet.Node<any>, regex?: string, format?: vim.snippet.Node<vim.snippet.FormatData|vim.snippet.TextData>[], options?: string }
|
|
--- @class vim.snippet.FormatData: { capture: number, modifier?: string, if_text?: string, else_text?: string }
|
|
--- @class vim.snippet.SnippetData: { children: vim.snippet.Node<any>[] }
|
|
|
|
--- @type vim.snippet.Node<any>
|
|
local Node = {}
|
|
|
|
--- @return string
|
|
--- @diagnostic disable-next-line: inject-field
|
|
function Node:__tostring()
|
|
local node_text = {}
|
|
local type, data = self.type, self.data
|
|
if type == Type.Snippet then
|
|
--- @cast data vim.snippet.SnippetData
|
|
for _, child in ipairs(data.children) do
|
|
table.insert(node_text, tostring(child))
|
|
end
|
|
elseif type == Type.Choice then
|
|
--- @cast data vim.snippet.ChoiceData
|
|
table.insert(node_text, data.values[1])
|
|
elseif type == Type.Placeholder then
|
|
--- @cast data vim.snippet.PlaceholderData
|
|
table.insert(node_text, tostring(data.value))
|
|
elseif type == Type.Text then
|
|
--- @cast data vim.snippet.TextData
|
|
table.insert(node_text, data.text)
|
|
end
|
|
return table.concat(node_text)
|
|
end
|
|
|
|
--- Returns a function that constructs a snippet node of the given type.
|
|
---
|
|
--- @generic T
|
|
--- @param type vim.snippet.Type
|
|
--- @return fun(data: T): vim.snippet.Node<T>
|
|
local function node(type)
|
|
return function(data)
|
|
return setmetatable({ type = type, data = data }, Node)
|
|
end
|
|
end
|
|
|
|
-- stylua: ignore
|
|
--- @diagnostic disable-next-line: missing-fields
|
|
local G = P({
|
|
'snippet';
|
|
snippet = Ct(Cg(
|
|
Ct((
|
|
V('any') +
|
|
(Ct(Cg((text(escapable, '$') ^ 1) / escape_text(), 'text')) / node(Type.Text))
|
|
) ^ 1), 'children'
|
|
) * -P(1)) / node(Type.Snippet),
|
|
any = V('placeholder') + V('tabstop') + V('choice') + V('variable'),
|
|
any_or_text = V('any') + (Ct(Cg(braced_text, 'text')) / node(Type.Text)),
|
|
tabstop = Ct(dollar * (tabstop + (l_brace * tabstop * r_brace))) / node(Type.Tabstop),
|
|
placeholder = Ct(dollar * l_brace * tabstop * colon * Cg(V('any_or_text'), 'value') * r_brace) / node(Type.Placeholder),
|
|
choice = Ct(dollar *
|
|
l_brace *
|
|
tabstop *
|
|
pipe *
|
|
Cg(Ct(choice_text * (P(',') * choice_text) ^ 0), 'values') *
|
|
pipe *
|
|
r_brace) / node(Type.Choice),
|
|
variable = Ct(dollar * (
|
|
var + (
|
|
l_brace * var * (
|
|
r_brace +
|
|
(colon * Cg(V('any_or_text'), 'default') * r_brace) +
|
|
(slash * regex * slash * Cg(Ct((V('format') + (C(format_text) / node(Type.Text))) ^ 1), 'format') * slash * options * r_brace)
|
|
))
|
|
)) / node(Type.Variable),
|
|
format = Ct(dollar * (
|
|
format_capture + (
|
|
l_brace * format_capture * (
|
|
r_brace +
|
|
(colon * (
|
|
(slash * format_modifier * r_brace) +
|
|
(P('+') * if_text * r_brace) +
|
|
(P('?') * if_till_colon_text * colon * else_text * r_brace) +
|
|
(P('-') * else_text * r_brace) +
|
|
(else_text * r_brace)
|
|
))
|
|
))
|
|
)) / node(Type.Format),
|
|
})
|
|
|
|
--- Parses the given input into a snippet tree.
|
|
--- @param input string
|
|
--- @return vim.snippet.Node<vim.snippet.SnippetData>
|
|
function M.parse(input)
|
|
return (assert(G:match(input), 'snippet parsing failed'))
|
|
end
|
|
|
|
return M
|