123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- -- 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 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)
- local output = {
- '-- Generated by gen_lsp.lua, keep at end of file.',
- '--- @alias vim.lsp.protocol.Method.ClientToServer',
- }
- for _, item in ipairs(all) do
- if item.method and item.messageDirection == 'clientToServer' then
- output[#output + 1] = ("--- | '%s',"):format(item.method)
- end
- end
- vim.list_extend(output, {
- '',
- '--- @alias vim.lsp.protocol.Method.ServerToClient',
- })
- for _, item in ipairs(all) do
- if item.method and item.messageDirection == 'serverToClient' then
- output[#output + 1] = ("--- | '%s',"):format(item.method)
- end
- end
- vim.list_extend(output, {
- '',
- '--- @alias vim.lsp.protocol.Method',
- '--- | vim.lsp.protocol.Method.ClientToServer',
- '--- | vim.lsp.protocol.Method.ServerToClient',
- '',
- '-- Generated by gen_lsp.lua, keep at end of file.',
- '---',
- '--- @enum vim.lsp.protocol.Methods',
- '--- @see https://microsoft.github.io/language-server-protocol/specification/#metaModel',
- '--- LSP method names.',
- 'protocol.Methods = {',
- })
- 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[#output + 1] = ''
- output[#output + 1] = 'return protocol'
- 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 _, 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)
- 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 <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 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
|