Files
neovim/scripts/gen_lsp.lua
Jongwook Choi 2f43af6423 refactor(gen_lsp.lua): add typing for the LSP protocol JSON data model
Enhance readability and intellisense by incorporating type annotations.
Types are not very strict and may not encompass th entire LSP Protocol
metamodel; the scope is up to what's relevant for generating type
annotations for LSP (`_meta/protocol.lua`).

Based on the model schema:
https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/3.18/metaModel/metaModel.schema.json

No behavioral changes (and hence no diff on _meta/protocol.lua) should
exist in this commit.
2023-12-27 10:48:06 +01:00

432 lines
13 KiB
Lua

-- 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 name(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 name(a.method) < name(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, name(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 autogenerated from scripts/gen_lsp.lua',
'Regenerate:',
([=[nvim -l scripts/gen_lsp.lua gen --version %s --out runtime/lua/vim/lsp/_meta/protocol.lua]=]):format(
DEFAULT_LSP_VERSION
),
'--' .. ']]',
'',
'---@meta',
"error('Cannot require a meta file')",
'',
'---@alias lsp.null nil',
'---@alias uinteger integer',
'---@alias lsp.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 = {
'string',
'boolean',
'integer',
'uinteger',
'decimal',
}
--- @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
---@return string
local function parse_type(type)
-- 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) .. '[]'
-- OrType
elseif type.kind == 'or' then
local val = ''
for _, item in ipairs(type.items) do
val = val .. parse_type(item) .. '|' --[[ @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) .. ', ' .. parse_type(value) .. '>'
-- 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 }
--- @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
if field.documentation then
field.documentation = field.documentation:gsub('\n', '\n---')
anonym[#anonym + 1] = '---' .. field.documentation
end
anonym[#anonym + 1] = '---@field '
.. field.name
.. (field.optional and '?' or '')
.. ' '
.. parse_type(field.type)
end
anonym[#anonym + 1] = ''
for _, line in ipairs(anonym) do
anonym_classes[#anonym_classes + 1] = line
end
return 'anonym' .. anonymous_num
-- TupleType
elseif type.kind == 'tuple' then
local tuple = '{ '
for i, value in ipairs(type.items) do
tuple = tuple .. '[' .. i .. ']: ' .. parse_type(value) .. ', '
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
if structure.documentation then
structure.documentation = structure.documentation:gsub('\n', '\n---')
output[#output + 1] = '---' .. 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
end
--- @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
if field.documentation then
field.documentation = field.documentation:gsub('\n', '\n---')
output[#output + 1] = '---' .. field.documentation
end
output[#output + 1] = '---@field '
.. field.name
.. (field.optional and '?' or '')
.. ' '
.. parse_type(field.type)
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
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
alias.documentation = alias.documentation:gsub('\n', '\n---')
output[#output + 1] = '---' .. 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) .. '|'
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)
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'))
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