-- Generates lua-ls annotations for lsp. local USAGE = [[ Generates lua-ls annotations for lsp. USAGE: 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 = {} 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 --- 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 print(('URL failed: %s'):format(uri)) vim.print(res) error(res.stdout) end return vim.json.decode(res.stdout) end -- Gets the Lua symbol for a given fully-qualified LSP method name. 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.', '--- LSP method names.', '---', '---@see https://microsoft.github.io/language-server-protocol/specification/#metaModel', 'protocol.Methods = {', } 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 to_luaname(a.method) < to_luaname(b.method) end) for _, item in ipairs(all) do if item.method then 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 end end output[#output + 1] = ("%s%s = '%s',"):format(indent, to_luaname(item.method), item.method) end end output[#output + 1] = '}' output = vim.list_extend( output, vim.split( [[ local function freeze(t) return setmetatable({}, { __index = t, __newindex = function() error('cannot modify immutable table') end, }) end protocol.Methods = freeze(protocol.Methods) return protocol ]], '\n', { trimempty = true } ) ) local fname = './runtime/lua/vim/lsp/protocol.lua' local bufnr = vim.fn.bufadd(fname) vim.fn.bufload(bufnr) vim.api.nvim_set_current_buf(bufnr) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) local index = vim.iter(ipairs(lines)):find(function(key, item) return vim.startswith(item, '-- Generated by') and key or nil end) index = index and index - 1 or vim.api.nvim_buf_line_count(bufnr) - 1 vim.api.nvim_buf_set_lines(bufnr, index, -1, true, output) 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 gen_methods(protocol) end local output = { '--' .. '[[', '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 %s]=]):format(DEFAULT_LSP_VERSION), '--' .. ']]', '', '---@meta', "error('Cannot require a meta file')", '', '---@alias lsp.null nil', '---@alias uinteger integer', '---@alias decimal number', '---@alias lsp.DocumentUri string', '---@alias lsp.URI string', '', } 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 i, value in ipairs(type.items) do tuple = tuple .. '[' .. i .. ']: ' .. 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) end 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 output[#output + 1] = _process_documentation(field.documentation) end output[#output + 1] = '---@field ' .. 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) end local enum_type = '---@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 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) 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.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, 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') .. '\n') end ---@type vim._gen_lsp.opt local opt = { output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua', version = DEFAULT_LSP_VERSION, methods = false, } 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 needed') i = i + 1 elseif _G.arg[i] == '--version' then opt.version = assert(_G.arg[i + 1], '--version needed') i = i + 1 elseif _G.arg[i] == '--methods' then opt.methods = 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] end end i = i + 1 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