neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit 2fcdeb0128933ac28baeb645eb691c559560920e
parent d7e0d46ffa8f9f1eaf27f8b7ee32ef9a21cb9b84
Author: Lewis Russell <lewis6991@gmail.com>
Date:   Thu, 17 Apr 2025 11:41:38 +0100

refactor(lsp): gen_lsp

- Simplify usage:
  - Instead of `nvim -l src/gen/gen_lsp.lua gen` now just
    run `./src/gen/gen_lsp.lua`

- Removed `--methods` and `--capabilities` options.

- Improved rendering code in various areas.

Diffstat:
MMAINTAIN.md | 2+-
Mruntime/lua/vim/lsp/_meta/protocol.lua | 4++--
Msrc/gen/gen_lsp.lua | 536++++++++++++++++++++++++++++++++++++-------------------------------------------
3 files changed, 249 insertions(+), 293 deletions(-)

diff --git a/MAINTAIN.md b/MAINTAIN.md @@ -152,7 +152,7 @@ These dependencies are "vendored" (inlined), we must update the sources manually * `src/nvim/tui/terminfo_defs.h`: terminfo definitions * Run `scripts/update_terminfo.sh` to update these definitions. * `runtime/lua/vim/lsp/_meta/protocol.lua`: LSP specification - * Run `scripts/gen_lsp.lua` to update. + * Run `src/gen/gen_lsp.lua` to update. * `runtime/lua/vim/_meta/lpeg.lua`: LPeg definitions. * Refer to [`LuaCATS/lpeg`](https://github.com/LuaCATS/lpeg) for updates. * Update the git SHA revision from which the documentation was taken. diff --git a/runtime/lua/vim/lsp/_meta/protocol.lua b/runtime/lua/vim/lsp/_meta/protocol.lua @@ -1,11 +1,11 @@ --[[ -THIS FILE IS GENERATED by scr/gen/gen_lsp.lua +THIS FILE IS GENERATED by src/gen/gen_lsp.lua DO NOT EDIT MANUALLY Based on LSP protocol 3.18 Regenerate: -nvim -l scr/gen/gen_lsp.lua gen --version 3.18 +nvim -l src/gen/gen_lsp.lua --version 3.18 --]] ---@meta diff --git a/src/gen/gen_lsp.lua b/src/gen/gen_lsp.lua @@ -1,28 +1,19 @@ +#!/usr/bin/env -S nvim -l -- Generates lua-ls annotations for lsp. local USAGE = [[ Generates lua-ls annotations for lsp. -USAGE: -nvim -l src/gen/gen_lsp.lua gen # by default, this will overwrite runtime/lua/vim/lsp/_meta/protocol.lua -nvim -l src/gen/gen_lsp.lua gen --version 3.18 --out runtime/lua/vim/lsp/_meta/protocol.lua -nvim -l src/gen/gen_lsp.lua gen --version 3.18 --methods --capabilities -]] - -local DEFAULT_LSP_VERSION = '3.18' +Also updates types in runtime/lua/vim/lsp/protocol.lua -local M = {} +Usage: + src/gen/gen_lsp.lua [options] -local function tofile(fname, text) - local f = io.open(fname, 'w') - if not f then - error(('failed to write: %s'):format(f)) - else - print(('Written to: %s'):format(fname)) - f:write(text) - f:close() - end -end +Options: + --version <version> LSP version to use (default: 3.18) + --out <out> Output file (default: runtime/lua/vim/lsp/_meta/protocol.lua) + --help Print this help message +]] --- The LSP protocol JSON data (it's partial, non-exhaustive). --- https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/3.18/metaModel/metaModel.schema.json @@ -51,6 +42,66 @@ end --- @field partialResult? any --- @field result any +--- @class vim._gen_lsp.Structure translated to @class +--- @field deprecated? string +--- @field documentation? string +--- @field extends? { kind: string, name: string }[] +--- @field mixins? { kind: string, name: string }[] +--- @field name string +--- @field properties? vim._gen_lsp.Property[] members, translated to @field +--- @field proposed? boolean +--- @field since? string + +--- @class vim._gen_lsp.StructureLiteral translated to anonymous @class. +--- @field deprecated? string +--- @field description? string +--- @field properties vim._gen_lsp.Property[] +--- @field proposed? boolean +--- @field since? string + +--- @class vim._gen_lsp.Property translated to @field +--- @field deprecated? string +--- @field documentation? string +--- @field name string +--- @field optional? boolean +--- @field proposed? boolean +--- @field since? string +--- @field type { kind: string, name: string } + +--- @class vim._gen_lsp.Enumeration translated to @enum +--- @field deprecated string? +--- @field documentation string? +--- @field name string? +--- @field proposed boolean? +--- @field since string? +--- @field suportsCustomValues boolean? +--- @field values { name: string, value: string, documentation?: string, since?: string }[] + +--- @class vim._gen_lsp.TypeAlias translated to @alias +--- @field deprecated? string? +--- @field documentation? string +--- @field name string +--- @field proposed? boolean +--- @field since? string +--- @field type vim._gen_lsp.Type + +--- @class vim._gen_lsp.Type +--- @field kind string a common field for all Types. +--- @field name? string for ReferenceType, BaseType +--- @field element? any for ArrayType +--- @field items? vim._gen_lsp.Type[] for OrType, AndType +--- @field key? vim._gen_lsp.Type for MapType +--- @field value? string|vim._gen_lsp.Type for StringLiteralType, MapType, StructureLiteralType + +--- @param fname string +--- @param text string +local function tofile(fname, text) + local f = assert(io.open(fname, 'w'), ('failed to open: %s'):format(fname)) + f:write(text) + f:close() + print('Written to:', fname) +end + ---@param opt vim._gen_lsp.opt ---@return vim._gen_lsp.Protocol local function read_json(opt) @@ -68,28 +119,24 @@ local function read_json(opt) return vim.json.decode(res.stdout) end --- Gets the Lua symbol for a given fully-qualified LSP method name. +--- Gets the Lua symbol for a given fully-qualified LSP method name. +--- @param s string +--- @return string local function to_luaname(s) -- "$/" prefix is special: https://microsoft.github.io/language-server-protocol/specification/#dollarRequests - return s:gsub('^%$', 'dollar'):gsub('/', '_') + return (s:gsub('^%$', 'dollar'):gsub('/', '_')) end ----@param protocol vim._gen_lsp.Protocol ----@param gen_methods boolean ----@param gen_capabilities boolean -local function write_to_protocol(protocol, gen_methods, gen_capabilities) - if not gen_methods and not gen_capabilities then - return - end - - local indent = (' '):rep(2) - - local function compare_method(a, b) - return to_luaname(a.method) < to_luaname(b.method) - end +--- @param a vim._gen_lsp.Notification +--- @param b vim._gen_lsp.Notification +--- @return boolean +local function compare_method(a, b) + return to_luaname(a.method) < to_luaname(b.method) +end - ---@type (vim._gen_lsp.Request|vim._gen_lsp.Notification)[] - local all = {} +---@param protocol vim._gen_lsp.Protocol +local function write_to_vim_protocol(protocol) + local all = {} --- @type (vim._gen_lsp.Request|vim._gen_lsp.Notification)[] vim.list_extend(all, protocol.notifications) vim.list_extend(all, protocol.requests) @@ -99,7 +146,7 @@ local function write_to_protocol(protocol, gen_methods, gen_capabilities) local output = { '-- Generated by gen_lsp.lua, keep at end of file.' } - if gen_methods then + do -- methods for _, dir in ipairs({ 'clientToServer', 'serverToClient' }) do local dir1 = dir:sub(1, 1):upper() .. dir:sub(2) local alias = ('vim.lsp.protocol.Method.%s'):format(dir1) @@ -141,16 +188,16 @@ local function write_to_protocol(protocol, gen_methods, gen_capabilities) if item.documentation then local document = vim.split(item.documentation, '\n?\n', { trimempty = true }) for _, docstring in ipairs(document) do - output[#output + 1] = indent .. '--- ' .. docstring + output[#output + 1] = ' --- ' .. docstring end end - output[#output + 1] = ("%s%s = '%s',"):format(indent, to_luaname(item.method), item.method) + output[#output + 1] = (" %s = '%s',"):format(to_luaname(item.method), item.method) end end output[#output + 1] = '}' end - if gen_capabilities then + do -- capabilities vim.list_extend(output, { '', '-- stylua: ignore start', @@ -161,18 +208,9 @@ local function write_to_protocol(protocol, gen_methods, gen_capabilities) for _, item in ipairs(all) do if item.serverCapability then - output[#output + 1] = ("%s['%s'] = { %s },"):format( - indent, + output[#output + 1] = (" ['%s'] = { %s },"):format( item.method, - table.concat( - vim - .iter(vim.split(item.serverCapability, '.', { plain = true })) - :map(function(segment) - return "'" .. segment .. "'" - end) - :totable(), - ', ' - ) + "'" .. item.serverCapability:gsub('%.', "', '") .. "'" ) end end @@ -197,28 +235,116 @@ local function write_to_protocol(protocol, gen_methods, gen_capabilities) vim.cmd.write() end ----@class vim._gen_lsp.opt ----@field output_file string ----@field version string ----@field methods boolean ----@field capabilities boolean +--- @param doc string +local function process_documentation(doc) + doc = doc:gsub('\n', '\n---') + -- Remove <200b> (zero-width space) unicode characters: e.g., `**/<200b>*` + doc = doc:gsub('\226\128\139', '') + -- Escape annotations that are not recognized by lua-ls + doc = doc:gsub('%^---@sample', '---\\@sample') + return '---' .. doc +end ----@param opt vim._gen_lsp.opt -function M.gen(opt) - --- @type vim._gen_lsp.Protocol - local protocol = read_json(opt) +local simple_types = { + string = true, + boolean = true, + integer = true, + uinteger = true, + decimal = true, +} + +local anonymous_num = 0 + +--- @type string[] +local anonym_classes = {} + +--- @param type vim._gen_lsp.Type +--- @param prefix? string Optional prefix associated with the this type, made of (nested) field name. +--- Used to generate class name for structure literal types. +--- @return string +local function parse_type(type, prefix) + if type.kind == 'reference' or type.kind == 'base' then + if simple_types[type.name] then + return type.name + end + return 'lsp.' .. type.name + elseif type.kind == 'array' then + local parsed_items = parse_type(type.element, prefix) + if type.element.items and #type.element.items > 1 then + parsed_items = '(' .. parsed_items .. ')' + end + return parsed_items .. '[]' + elseif type.kind == 'or' then + local types = {} --- @type string[] + for _, item in ipairs(type.items) do + types[#types + 1] = parse_type(item, prefix) + end + return table.concat(types, '|') + elseif type.kind == 'stringLiteral' then + return '"' .. type.value .. '"' + elseif type.kind == 'map' then + local key = assert(type.key) + local value = type.value --[[ @as vim._gen_lsp.Type ]] + return ('table<%s, %s>'):format(parse_type(key, prefix), parse_type(value, prefix)) + elseif type.kind == 'literal' then + -- can I use ---@param disabled? {reason: string} + -- use | to continue the inline class to be able to add docs + -- https://github.com/LuaLS/lua-language-server/issues/2128 + anonymous_num = anonymous_num + 1 + local anonymous_classname = 'lsp._anonym' .. anonymous_num + if prefix then + anonymous_classname = anonymous_classname .. '.' .. prefix + end + + local anonym = { '---@class ' .. anonymous_classname } + if anonymous_num > 1 then + table.insert(anonym, 1, '') + end - write_to_protocol(protocol, opt.methods, opt.capabilities) + ---@type vim._gen_lsp.StructureLiteral + local structural_literal = assert(type.value) --[[ @as vim._gen_lsp.StructureLiteral ]] + for _, field in ipairs(structural_literal.properties) do + anonym[#anonym + 1] = '---' + if field.documentation then + anonym[#anonym + 1] = process_documentation(field.documentation) + end + anonym[#anonym + 1] = ('---@field %s%s %s'):format( + field.name, + (field.optional and '?' or ''), + parse_type(field.type, prefix .. '.' .. field.name) + ) + end + for _, line in ipairs(anonym) do + if line then + anonym_classes[#anonym_classes + 1] = line + end + end + return anonymous_classname + elseif type.kind == 'tuple' then + local types = {} --- @type string[] + for _, value in ipairs(type.items) do + types[#types + 1] = parse_type(value, prefix) + end + return '[' .. table.concat(types, ', ') .. ']' + end + + vim.print('WARNING: Unknown type ', type) + return '' +end +--- @param protocol vim._gen_lsp.Protocol +--- @param version string +--- @param output_file string +local function write_to_meta_protocol(protocol, version, output_file) local output = { '--' .. '[[', - 'THIS FILE IS GENERATED by scr/gen/gen_lsp.lua', + 'THIS FILE IS GENERATED by src/gen/gen_lsp.lua', 'DO NOT EDIT MANUALLY', '', - 'Based on LSP protocol ' .. opt.version, + 'Based on LSP protocol ' .. version, '', 'Regenerate:', - ([=[nvim -l scr/gen/gen_lsp.lua gen --version %s]=]):format(DEFAULT_LSP_VERSION), + ([=[nvim -l src/gen/gen_lsp.lua --version %s]=]):format(version), '--' .. ']]', '', '---@meta', @@ -232,150 +358,9 @@ function M.gen(opt) '', } - local anonymous_num = 0 - - ---@type string[] - local anonym_classes = {} - - local simple_types = { - 'string', - 'boolean', - 'integer', - 'uinteger', - 'decimal', - } - - ---@param documentation string - local _process_documentation = function(documentation) - documentation = documentation:gsub('\n', '\n---') - -- Remove <200b> (zero-width space) unicode characters: e.g., `**/<200b>*` - documentation = documentation:gsub('\226\128\139', '') - -- Escape annotations that are not recognized by lua-ls - documentation = documentation:gsub('%^---@sample', '---\\@sample') - return '---' .. documentation - end - - --- @class vim._gen_lsp.Type - --- @field kind string a common field for all Types. - --- @field name? string for ReferenceType, BaseType - --- @field element? any for ArrayType - --- @field items? vim._gen_lsp.Type[] for OrType, AndType - --- @field key? vim._gen_lsp.Type for MapType - --- @field value? string|vim._gen_lsp.Type for StringLiteralType, MapType, StructureLiteralType - - ---@param type vim._gen_lsp.Type - ---@param prefix? string Optional prefix associated with the this type, made of (nested) field name. - --- Used to generate class name for structure literal types. - ---@return string - local function parse_type(type, prefix) - -- ReferenceType | BaseType - if type.kind == 'reference' or type.kind == 'base' then - if vim.tbl_contains(simple_types, type.name) then - return type.name - end - return 'lsp.' .. type.name - - -- ArrayType - elseif type.kind == 'array' then - local parsed_items = parse_type(type.element, prefix) - if type.element.items and #type.element.items > 1 then - parsed_items = '(' .. parsed_items .. ')' - end - return parsed_items .. '[]' - - -- OrType - elseif type.kind == 'or' then - local val = '' - for _, item in ipairs(type.items) do - val = val .. parse_type(item, prefix) .. '|' --[[ @as string ]] - end - val = val:sub(0, -2) - return val - - -- StringLiteralType - elseif type.kind == 'stringLiteral' then - return '"' .. type.value .. '"' - - -- MapType - elseif type.kind == 'map' then - local key = assert(type.key) - local value = type.value --[[ @as vim._gen_lsp.Type ]] - return 'table<' .. parse_type(key, prefix) .. ', ' .. parse_type(value, prefix) .. '>' - - -- StructureLiteralType - elseif type.kind == 'literal' then - -- can I use ---@param disabled? {reason: string} - -- use | to continue the inline class to be able to add docs - -- https://github.com/LuaLS/lua-language-server/issues/2128 - anonymous_num = anonymous_num + 1 - local anonymous_classname = 'lsp._anonym' .. anonymous_num - if prefix then - anonymous_classname = anonymous_classname .. '.' .. prefix - end - local anonym = vim - .iter({ - (anonymous_num > 1 and { '' } or {}), - { '---@class ' .. anonymous_classname }, - }) - :flatten() - :totable() - - --- @class vim._gen_lsp.StructureLiteral translated to anonymous @class. - --- @field deprecated? string - --- @field description? string - --- @field properties vim._gen_lsp.Property[] - --- @field proposed? boolean - --- @field since? string - - ---@type vim._gen_lsp.StructureLiteral - local structural_literal = assert(type.value) --[[ @as vim._gen_lsp.StructureLiteral ]] - for _, field in ipairs(structural_literal.properties) do - anonym[#anonym + 1] = '---' - if field.documentation then - anonym[#anonym + 1] = _process_documentation(field.documentation) - end - anonym[#anonym + 1] = '---@field ' - .. field.name - .. (field.optional and '?' or '') - .. ' ' - .. parse_type(field.type, prefix .. '.' .. field.name) - end - -- anonym[#anonym + 1] = '' - for _, line in ipairs(anonym) do - if line then - anonym_classes[#anonym_classes + 1] = line - end - end - return anonymous_classname - - -- TupleType - elseif type.kind == 'tuple' then - local tuple = '[' - for _, value in ipairs(type.items) do - tuple = tuple .. parse_type(value, prefix) .. ', ' - end - -- remove , at the end - tuple = tuple:sub(0, -3) - return tuple .. ']' - end - - vim.print('WARNING: Unknown type ', type) - return '' - end - - --- @class vim._gen_lsp.Structure translated to @class - --- @field deprecated? string - --- @field documentation? string - --- @field extends? { kind: string, name: string }[] - --- @field mixins? { kind: string, name: string }[] - --- @field name string - --- @field properties? vim._gen_lsp.Property[] members, translated to @field - --- @field proposed? boolean - --- @field since? string for _, structure in ipairs(protocol.structures) do - -- output[#output + 1] = '' if structure.documentation then - output[#output + 1] = _process_documentation(structure.documentation) + output[#output + 1] = process_documentation(structure.documentation) end local class_string = ('---@class lsp.%s'):format(structure.name) if structure.extends or structure.mixins then @@ -390,126 +375,97 @@ function M.gen(opt) end output[#output + 1] = class_string - --- @class vim._gen_lsp.Property translated to @field - --- @field deprecated? string - --- @field documentation? string - --- @field name string - --- @field optional? boolean - --- @field proposed? boolean - --- @field since? string - --- @field type { kind: string, name: string } for _, field in ipairs(structure.properties or {}) do output[#output + 1] = '---' -- Insert a single newline between @fields (and after @class) if field.documentation then - output[#output + 1] = _process_documentation(field.documentation) + output[#output + 1] = process_documentation(field.documentation) end - output[#output + 1] = '---@field ' - .. field.name - .. (field.optional and '?' or '') - .. ' ' - .. parse_type(field.type, field.name) + output[#output + 1] = ('---@field %s%s %s'):format( + field.name, + (field.optional and '?' or ''), + parse_type(field.type, field.name) + ) end output[#output + 1] = '' end - --- @class vim._gen_lsp.Enumeration translated to @enum - --- @field deprecated string? - --- @field documentation string? - --- @field name string? - --- @field proposed boolean? - --- @field since string? - --- @field suportsCustomValues boolean? - --- @field values { name: string, value: string, documentation?: string, since?: string }[] for _, enum in ipairs(protocol.enumerations) do if enum.documentation then - output[#output + 1] = _process_documentation(enum.documentation) + output[#output + 1] = process_documentation(enum.documentation) end - local enum_type = '---@alias lsp.' .. enum.name + output[#output + 1] = '---@alias lsp.' .. enum.name for _, value in ipairs(enum.values) do - enum_type = enum_type - .. '\n---| ' - .. (type(value.value) == 'string' and '"' .. value.value .. '"' or value.value) - .. ' # ' - .. value.name + local value1 = (type(value.value) == 'string' and ('"%s"'):format(value.value) or value.value) + output[#output + 1] = ('---| %s # %s'):format(value1, value.name) end - output[#output + 1] = enum_type output[#output + 1] = '' end - --- @class vim._gen_lsp.TypeAlias translated to @alias - --- @field deprecated? string? - --- @field documentation? string - --- @field name string - --- @field proposed? boolean - --- @field since? string - --- @field type vim._gen_lsp.Type for _, alias in ipairs(protocol.typeAliases) do if alias.documentation then - output[#output + 1] = _process_documentation(alias.documentation) + output[#output + 1] = process_documentation(alias.documentation) end + + local alias_type --- @type string + if alias.type.kind == 'or' then - local alias_type = '---@alias lsp.' .. alias.name .. ' ' + local alias_types = {} --- @type string[] for _, item in ipairs(alias.type.items) do - alias_type = alias_type .. parse_type(item, alias.name) .. '|' + alias_types[#alias_types + 1] = parse_type(item, alias.name) end - alias_type = alias_type:sub(0, -2) - output[#output + 1] = alias_type + alias_type = table.concat(alias_types, '|') else - output[#output + 1] = '---@alias lsp.' - .. alias.name - .. ' ' - .. parse_type(alias.type, alias.name) + alias_type = parse_type(alias.type, alias.name) end + output[#output + 1] = ('---@alias lsp.%s %s'):format(alias.name, alias_type) output[#output + 1] = '' end -- anonymous classes - for _, line in ipairs(anonym_classes) do - output[#output + 1] = line - end + vim.list_extend(output, anonym_classes) - tofile(opt.output_file, table.concat(output, '\n') .. '\n') + tofile(output_file, table.concat(output, '\n') .. '\n') end ----@type vim._gen_lsp.opt -local opt = { - output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua', - version = DEFAULT_LSP_VERSION, - methods = false, - capabilities = false, -} +---@class vim._gen_lsp.opt +---@field output_file string +---@field version string -local command = nil -local i = 1 -while i <= #_G.arg do - if _G.arg[i] == '--out' then - opt.output_file = assert(_G.arg[i + 1], '--out <outfile> needed') - i = i + 1 - elseif _G.arg[i] == '--version' then - opt.version = assert(_G.arg[i + 1], '--version <version> needed') - i = i + 1 - elseif _G.arg[i] == '--methods' then - opt.methods = true - elseif _G.arg[i] == '--capabilities' then - opt.capabilities = true - elseif vim.startswith(_G.arg[i], '-') then - error('Unrecognized args: ' .. _G.arg[i]) - else - if command then - error('More than one command was given: ' .. _G.arg[i]) - else - command = _G.arg[i] +--- @return vim._gen_lsp.opt +local function parse_args() + ---@type vim._gen_lsp.opt + local opt = { + output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua', + version = '3.18', + } + + local i = 1 + while i <= #_G.arg do + local cur_arg = _G.arg[i] + if cur_arg == '--out' then + opt.output_file = assert(_G.arg[i + 1], '--out <outfile> needed') + i = i + 1 + elseif cur_arg == '--version' then + opt.version = assert(_G.arg[i + 1], '--version <version> needed') + i = i + 1 + elseif cur_arg == '--help' or cur_arg == '-h' then + print(USAGE) + os.exit(0) + elseif vim.startswith(cur_arg, '-') then + print('Unrecognized option:', cur_arg, '\n') + os.exit(1) end + i = i + 1 end - i = i + 1 + + return opt end -if not command then - print(USAGE) -elseif M[command] then - M[command](opt) -- see M.gen() -else - error('Unknown command: ' .. command) +local function main() + local opt = parse_args() + local protocol = read_json(opt) + write_to_vim_protocol(protocol) + write_to_meta_protocol(protocol, opt.version, opt.output_file) end -return M +main()