diff options
Diffstat (limited to 'scripts/gen_lsp.lua')
-rw-r--r-- | scripts/gen_lsp.lua | 294 |
1 files changed, 230 insertions, 64 deletions
diff --git a/scripts/gen_lsp.lua b/scripts/gen_lsp.lua index 6ff8dcb3f4..19fad7bab4 100644 --- a/scripts/gen_lsp.lua +++ b/scripts/gen_lsp.lua @@ -1,11 +1,15 @@ ---[[ -Generates lua-ls annotations for lsp +-- Generates lua-ls annotations for lsp. + +local USAGE = [[ +Generates lua-ls annotations for lsp. + USAGE: -nvim -l scripts/gen_lsp.lua gen # this will overwrite runtime/lua/vim/lsp/_meta/protocol.lua -nvim -l scripts/gen_lsp.lua gen --version 3.18 --build/new_lsp_types.lua +nvim -l scripts/gen_lsp.lua gen # by default, this will overwrite runtime/lua/vim/lsp/_meta/protocol.lua nvim -l scripts/gen_lsp.lua gen --version 3.18 --out runtime/lua/vim/lsp/_meta/protocol.lua nvim -l scripts/gen_lsp.lua gen --version 3.18 --methods ---]] +]] + +local DEFAULT_LSP_VERSION = '3.18' local M = {} @@ -14,15 +18,28 @@ local function tofile(fname, text) 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 +--- 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 +--- @class vim._gen_lsp.Protocol +--- @field requests vim._gen_lsp.Request[] +--- @field notifications vim._gen_lsp.Notification[] +--- @field structures vim._gen_lsp.Structure[] +--- @field enumerations vim._gen_lsp.Enumeration[] +--- @field typeAliases vim._gen_lsp.TypeAlias[] + +---@param opt vim._gen_lsp.opt +---@return vim._gen_lsp.Protocol local function read_json(opt) local uri = 'https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/' .. opt.version .. '/metaModel/metaModel.json' + print('Reading ' .. uri) local res = vim.system({ 'curl', '--no-progress-meter', uri, '-o', '-' }):wait() if res.code ~= 0 or (res.stdout or ''):len() < 999 then @@ -34,11 +51,12 @@ local function read_json(opt) end -- Gets the Lua symbol for a given fully-qualified LSP method name. -local function name(s) +local function to_luaname(s) -- "$/" prefix is special: https://microsoft.github.io/language-server-protocol/specification/#dollarRequests return s:gsub('^%$', 'dollar'):gsub('/', '_') end +---@param protocol vim._gen_lsp.Protocol local function gen_methods(protocol) local output = { '-- Generated by gen_lsp.lua, keep at end of file.', @@ -49,9 +67,35 @@ local function gen_methods(protocol) } local indent = (' '):rep(2) + --- @class vim._gen_lsp.Request + --- @field deprecated? string + --- @field documentation? string + --- @field messageDirection string + --- @field method string + --- @field params? any + --- @field proposed? boolean + --- @field registrationMethod? string + --- @field registrationOptions? any + --- @field since? string + + --- @class vim._gen_lsp.Notification + --- @field deprecated? string + --- @field documentation? string + --- @field errorData? any + --- @field messageDirection string + --- @field method string + --- @field params? any[] + --- @field partialResult? any + --- @field proposed? boolean + --- @field registrationMethod? string + --- @field registrationOptions? any + --- @field result any + --- @field since? string + + ---@type (vim._gen_lsp.Request|vim._gen_lsp.Notification)[] local all = vim.list_extend(protocol.requests, protocol.notifications) table.sort(all, function(a, b) - return name(a.method) < name(b.method) + return to_luaname(a.method) < to_luaname(b.method) end) for _, item in ipairs(all) do if item.method then @@ -61,7 +105,7 @@ local function gen_methods(protocol) output[#output + 1] = indent .. '--- ' .. docstring end end - output[#output + 1] = ("%s%s = '%s',"):format(indent, name(item.method), item.method) + output[#output + 1] = ("%s%s = '%s',"):format(indent, to_luaname(item.method), item.method) end end output[#output + 1] = '}' @@ -99,7 +143,14 @@ return protocol vim.cmd.write() end +---@class vim._gen_lsp.opt +---@field output_file string +---@field version string +---@field methods boolean + +---@param opt vim._gen_lsp.opt function M.gen(opt) + --- @type vim._gen_lsp.Protocol local protocol = read_json(opt) if opt.methods then @@ -107,25 +158,30 @@ function M.gen(opt) end local output = { - '--[[', - 'This file is autogenerated from scripts/gen_lsp.lua', + '--' .. '[[', + 'THIS FILE IS GENERATED by scripts/gen_lsp.lua', + 'DO NOT EDIT MANUALLY', + '', + 'Based on LSP protocol ' .. opt.version, + '', 'Regenerate:', - [=[nvim -l scripts/gen_lsp.lua gen --version 3.18 --runtime/lua/vim/lsp/_meta/protocol.lua]=], - '--]]', + ([=[nvim -l scripts/gen_lsp.lua gen --version %s]=]):format(DEFAULT_LSP_VERSION), + '--' .. ']]', + '', + '---@meta', + "error('Cannot require a meta file')", '', '---@alias lsp.null nil', '---@alias uinteger integer', - '---@alias lsp.decimal number', + '---@alias decimal number', '---@alias lsp.DocumentUri string', '---@alias lsp.URI string', - '---@alias lsp.LSPObject table<string, lsp.LSPAny>', - '---@alias lsp.LSPArray lsp.LSPAny[]', - '---@alias lsp.LSPAny lsp.LSPObject|lsp.LSPArray|string|number|boolean|nil', '', } local anonymous_num = 0 + ---@type string[] local anonym_classes = {} local simple_types = { @@ -136,95 +192,181 @@ function M.gen(opt) 'decimal', } - local function parse_type(type) + ---@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 - return parse_type(type.element) .. '[]' + 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) .. '|' + 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 - return 'table<' .. parse_type(type.key) .. ', ' .. parse_type(type.value) .. '>' + 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 anonym = { '---@class anonym' .. anonymous_num } - for _, field in ipairs(type.value.properties) do + local anonymous_classname = 'lsp._anonym' .. anonymous_num + if prefix then + anonymous_classname = anonymous_classname .. '.' .. prefix + end + local anonym = vim.tbl_flatten { -- remove nil + anonymous_num > 1 and '' or nil, + '---@class ' .. anonymous_classname, + } + + --- @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 - field.documentation = field.documentation:gsub('\n', '\n---') - anonym[#anonym + 1] = '---' .. field.documentation + anonym[#anonym + 1] = _process_documentation(field.documentation) end anonym[#anonym + 1] = '---@field ' .. field.name .. (field.optional and '?' or '') .. ' ' - .. parse_type(field.type) + .. parse_type(field.type, prefix .. '.' .. field.name) end - anonym[#anonym + 1] = '' + -- anonym[#anonym + 1] = '' for _, line in ipairs(anonym) do - anonym_classes[#anonym_classes + 1] = line + if line then + anonym_classes[#anonym_classes + 1] = line + end end - return 'anonym' .. anonymous_num + return anonymous_classname + + -- TupleType elseif type.kind == 'tuple' then local tuple = '{ ' for i, value in ipairs(type.items) do - tuple = tuple .. '[' .. i .. ']: ' .. parse_type(value) .. ', ' + tuple = tuple .. '[' .. i .. ']: ' .. parse_type(value, prefix) .. ', ' end -- remove , at the end tuple = tuple:sub(0, -3) return tuple .. ' }' end - vim.print(type) + + 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 - structure.documentation = structure.documentation:gsub('\n', '\n---') - output[#output + 1] = '---' .. structure.documentation + output[#output + 1] = _process_documentation(structure.documentation) end - if structure.extends then - local class_string = '---@class lsp.' - .. structure.name - .. ': ' - .. parse_type(structure.extends[1]) - for _, mixin in ipairs(structure.mixins or {}) do - class_string = class_string .. ', ' .. parse_type(mixin) - end - output[#output + 1] = class_string - else - output[#output + 1] = '---@class lsp.' .. structure.name + local class_string = ('---@class lsp.%s'):format(structure.name) + if structure.extends or structure.mixins then + local inherits_from = table.concat( + vim.list_extend( + vim.tbl_map(parse_type, structure.extends or {}), + vim.tbl_map(parse_type, structure.mixins or {}) + ), + ', ' + ) + class_string = class_string .. ': ' .. inherits_from 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 - field.documentation = field.documentation:gsub('\n', '\n---') - output[#output + 1] = '---' .. field.documentation + output[#output + 1] = _process_documentation(field.documentation) end output[#output + 1] = '---@field ' .. field.name .. (field.optional and '?' or '') .. ' ' - .. parse_type(field.type) + .. 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 - enum.documentation = enum.documentation:gsub('\n', '\n---') - output[#output + 1] = '---' .. enum.documentation + output[#output + 1] = _process_documentation(enum.documentation) end local enum_type = '---@alias lsp.' .. enum.name for _, value in ipairs(enum.values) do @@ -238,53 +380,77 @@ function M.gen(opt) 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 - alias.documentation = alias.documentation:gsub('\n', '\n---') - output[#output + 1] = '---' .. alias.documentation + output[#output + 1] = _process_documentation(alias.documentation) end if alias.type.kind == 'or' then local alias_type = '---@alias lsp.' .. alias.name .. ' ' for _, item in ipairs(alias.type.items) do - alias_type = alias_type .. parse_type(item) .. '|' + alias_type = alias_type .. parse_type(item, alias.name) .. '|' end alias_type = alias_type:sub(0, -2) output[#output + 1] = alias_type else - output[#output + 1] = '---@alias lsp.' .. alias.name .. ' ' .. parse_type(alias.type) + output[#output + 1] = '---@alias lsp.' + .. alias.name + .. ' ' + .. parse_type(alias.type, alias.name) end output[#output + 1] = '' end + -- anonymous classes for _, line in ipairs(anonym_classes) do output[#output + 1] = line end - tofile(opt.output_file, table.concat(output, '\n')) + tofile(opt.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 = nil, - methods = nil, + version = DEFAULT_LSP_VERSION, + methods = false, } -for i = 1, #_G.arg do +local command = nil +local i = 1 +while i <= #_G.arg do if _G.arg[i] == '--out' then - opt.output_file = _G.arg[i + 1] + opt.output_file = assert(_G.arg[i + 1], '--out <outfile> needed') + i = i + 1 elseif _G.arg[i] == '--version' then - opt.version = _G.arg[i + 1] + opt.version = assert(_G.arg[i + 1], '--version <version> needed') + i = i + 1 elseif _G.arg[i] == '--methods' then opt.methods = true - elseif vim.startswith(_G.arg[i], '--') then - opt.output_file = _G.arg[i]:sub(3) + 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] + end end + i = i + 1 end -for _, a in ipairs(arg) do - if M[a] then - M[a](opt) - end +if not command then + print(USAGE) +elseif M[command] then + M[command](opt) -- see M.gen() +else + error('Unknown command: ' .. command) end return M |