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.
This commit is contained in:
Jongwook Choi 2023-12-10 23:50:54 -05:00 committed by Mathias Fußenegger
parent 3767468b96
commit 2f43af6423

View File

@ -24,7 +24,17 @@ local function tofile(fname, text)
end end
end end
---@param opt gen_lsp._opt --- 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 function read_json(opt)
local uri = 'https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/' local uri = 'https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/'
.. opt.version .. opt.version
@ -46,6 +56,7 @@ local function name(s)
return s:gsub('^%$', 'dollar'):gsub('/', '_') return s:gsub('^%$', 'dollar'):gsub('/', '_')
end end
---@param protocol vim._gen_lsp.Protocol
local function gen_methods(protocol) local function gen_methods(protocol)
local output = { local output = {
'-- Generated by gen_lsp.lua, keep at end of file.', '-- Generated by gen_lsp.lua, keep at end of file.',
@ -56,6 +67,32 @@ local function gen_methods(protocol)
} }
local indent = (' '):rep(2) 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) local all = vim.list_extend(protocol.requests, protocol.notifications)
table.sort(all, function(a, b) table.sort(all, function(a, b)
return name(a.method) < name(b.method) return name(a.method) < name(b.method)
@ -106,14 +143,15 @@ return protocol
vim.cmd.write() vim.cmd.write()
end end
---@class gen_lsp._opt ---@class vim._gen_lsp.opt
---@field output_file string ---@field output_file string
---@field version string ---@field version string
---@field methods boolean ---@field methods boolean
---@param opt gen_lsp._opt ---@param opt vim._gen_lsp.opt
function M.gen(opt) function M.gen(opt)
local protocol = read_json(opt) --- @type table --- @type vim._gen_lsp.Protocol
local protocol = read_json(opt)
if opt.methods then if opt.methods then
gen_methods(protocol) gen_methods(protocol)
@ -144,6 +182,7 @@ function M.gen(opt)
local anonymous_num = 0 local anonymous_num = 0
---@type string[]
local anonym_classes = {} local anonym_classes = {}
local simple_types = { local simple_types = {
@ -154,32 +193,65 @@ function M.gen(opt)
'decimal', '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) local function parse_type(type)
-- ReferenceType | BaseType
if type.kind == 'reference' or type.kind == 'base' then if type.kind == 'reference' or type.kind == 'base' then
if vim.tbl_contains(simple_types, type.name) then if vim.tbl_contains(simple_types, type.name) then
return type.name return type.name
end end
return 'lsp.' .. type.name return 'lsp.' .. type.name
-- ArrayType
elseif type.kind == 'array' then elseif type.kind == 'array' then
return parse_type(type.element) .. '[]' return parse_type(type.element) .. '[]'
-- OrType
elseif type.kind == 'or' then elseif type.kind == 'or' then
local val = '' local val = ''
for _, item in ipairs(type.items) do for _, item in ipairs(type.items) do
val = val .. parse_type(item) .. '|' val = val .. parse_type(item) .. '|' --[[ @as string ]]
end end
val = val:sub(0, -2) val = val:sub(0, -2)
return val return val
-- StringLiteralType
elseif type.kind == 'stringLiteral' then elseif type.kind == 'stringLiteral' then
return '"' .. type.value .. '"' return '"' .. type.value .. '"'
-- MapType
elseif type.kind == 'map' then 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) .. ', ' .. parse_type(value) .. '>'
-- StructureLiteralType
elseif type.kind == 'literal' then elseif type.kind == 'literal' then
-- can I use ---@param disabled? {reason: string} -- can I use ---@param disabled? {reason: string}
-- use | to continue the inline class to be able to add docs -- use | to continue the inline class to be able to add docs
-- https://github.com/LuaLS/lua-language-server/issues/2128 -- https://github.com/LuaLS/lua-language-server/issues/2128
anonymous_num = anonymous_num + 1 anonymous_num = anonymous_num + 1
local anonym = { '---@class anonym' .. anonymous_num } local anonym = { '---@class anonym' .. anonymous_num }
for _, field in ipairs(type.value.properties) do
--- @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 if field.documentation then
field.documentation = field.documentation:gsub('\n', '\n---') field.documentation = field.documentation:gsub('\n', '\n---')
anonym[#anonym + 1] = '---' .. field.documentation anonym[#anonym + 1] = '---' .. field.documentation
@ -195,6 +267,8 @@ function M.gen(opt)
anonym_classes[#anonym_classes + 1] = line anonym_classes[#anonym_classes + 1] = line
end end
return 'anonym' .. anonymous_num return 'anonym' .. anonymous_num
-- TupleType
elseif type.kind == 'tuple' then elseif type.kind == 'tuple' then
local tuple = '{ ' local tuple = '{ '
for i, value in ipairs(type.items) do for i, value in ipairs(type.items) do
@ -204,10 +278,20 @@ function M.gen(opt)
tuple = tuple:sub(0, -3) tuple = tuple:sub(0, -3)
return tuple .. ' }' return tuple .. ' }'
end end
vim.print(type)
vim.print('WARNING: Unknown type ', type)
return '' return ''
end 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 for _, structure in ipairs(protocol.structures) do
if structure.documentation then if structure.documentation then
structure.documentation = structure.documentation:gsub('\n', '\n---') structure.documentation = structure.documentation:gsub('\n', '\n---')
@ -225,6 +309,15 @@ function M.gen(opt)
else else
output[#output + 1] = '---@class lsp.' .. structure.name output[#output + 1] = '---@class lsp.' .. structure.name
end 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 for _, field in ipairs(structure.properties or {}) do
if field.documentation then if field.documentation then
field.documentation = field.documentation:gsub('\n', '\n---') field.documentation = field.documentation:gsub('\n', '\n---')
@ -239,6 +332,14 @@ function M.gen(opt)
output[#output + 1] = '' output[#output + 1] = ''
end 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 for _, enum in ipairs(protocol.enumerations) do
if enum.documentation then if enum.documentation then
enum.documentation = enum.documentation:gsub('\n', '\n---') enum.documentation = enum.documentation:gsub('\n', '\n---')
@ -256,6 +357,13 @@ function M.gen(opt)
output[#output + 1] = '' output[#output + 1] = ''
end 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 for _, alias in ipairs(protocol.typeAliases) do
if alias.documentation then if alias.documentation then
alias.documentation = alias.documentation:gsub('\n', '\n---') alias.documentation = alias.documentation:gsub('\n', '\n---')
@ -274,6 +382,7 @@ function M.gen(opt)
output[#output + 1] = '' output[#output + 1] = ''
end end
-- anonymous classes
for _, line in ipairs(anonym_classes) do for _, line in ipairs(anonym_classes) do
output[#output + 1] = line output[#output + 1] = line
end end
@ -281,6 +390,7 @@ function M.gen(opt)
tofile(opt.output_file, table.concat(output, '\n')) tofile(opt.output_file, table.concat(output, '\n'))
end end
---@type vim._gen_lsp.opt
local opt = { local opt = {
output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua', output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua',
version = DEFAULT_LSP_VERSION, version = DEFAULT_LSP_VERSION,