gen_lsp.lua (20622B)
1 #!/usr/bin/env -S nvim -l 2 -- Generates lua-ls annotations for lsp. 3 4 local USAGE = [[ 5 Generates lua-ls annotations for lsp. 6 7 Also updates types in runtime/lua/vim/lsp/protocol.lua 8 9 Usage: 10 src/gen/gen_lsp.lua [options] 11 12 Options: 13 --version <version> LSP version to use (default: 3.18) 14 --out <out> Output file (default: runtime/lua/vim/lsp/_meta/protocol.lua) 15 --help Print this help message 16 ]] 17 18 --- The LSP protocol JSON data (it's partial, non-exhaustive). 19 --- https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/3.18/metaModel/metaModel.schema.json 20 --- @class vim._gen_lsp.Protocol 21 --- @field requests vim._gen_lsp.Request[] 22 --- @field notifications vim._gen_lsp.Notification[] 23 --- @field structures vim._gen_lsp.Structure[] 24 --- @field enumerations vim._gen_lsp.Enumeration[] 25 --- @field typeAliases vim._gen_lsp.TypeAlias[] 26 27 --- @class vim._gen_lsp.Notification 28 --- @field deprecated? string 29 --- @field documentation? string 30 --- @field messageDirection string 31 --- @field clientCapability? string 32 --- @field serverCapability? string 33 --- @field method vim.lsp.protocol.Method 34 --- @field params? any 35 --- @field proposed? boolean 36 --- @field registrationMethod? string 37 --- @field registrationOptions? any 38 --- @field since? string 39 40 --- @class vim._gen_lsp.Request : vim._gen_lsp.Notification 41 --- @field errorData? any 42 --- @field partialResult? any 43 --- @field result any 44 45 --- @class vim._gen_lsp.Structure translated to @class 46 --- @field deprecated? string 47 --- @field documentation? string 48 --- @field extends? { kind: string, name: string }[] 49 --- @field mixins? { kind: string, name: string }[] 50 --- @field name string 51 --- @field properties? vim._gen_lsp.Property[] members, translated to @field 52 --- @field proposed? boolean 53 --- @field since? string 54 55 --- @class vim._gen_lsp.StructureLiteral translated to anonymous @class. 56 --- @field deprecated? string 57 --- @field description? string 58 --- @field properties vim._gen_lsp.Property[] 59 --- @field proposed? boolean 60 --- @field since? string 61 62 --- @class vim._gen_lsp.Property translated to @field 63 --- @field deprecated? string 64 --- @field documentation? string 65 --- @field name string 66 --- @field optional? boolean 67 --- @field proposed? boolean 68 --- @field since? string 69 --- @field type { kind: string, name: string } 70 71 --- @class vim._gen_lsp.Enumeration translated to @enum 72 --- @field deprecated string? 73 --- @field documentation string? 74 --- @field name string? 75 --- @field proposed boolean? 76 --- @field since string? 77 --- @field suportsCustomValues boolean? 78 --- @field values { name: string, value: string, documentation?: string, since?: string }[] 79 80 --- @class vim._gen_lsp.TypeAlias translated to @alias 81 --- @field deprecated? string? 82 --- @field documentation? string 83 --- @field name string 84 --- @field proposed? boolean 85 --- @field since? string 86 --- @field type vim._gen_lsp.Type 87 88 --- @class vim._gen_lsp.Type 89 --- @field kind string a common field for all Types. 90 --- @field name? string for ReferenceType, BaseType 91 --- @field element? any for ArrayType 92 --- @field items? vim._gen_lsp.Type[] for OrType, AndType 93 --- @field key? vim._gen_lsp.Type for MapType 94 --- @field value? string|vim._gen_lsp.Type for StringLiteralType, MapType, StructureLiteralType 95 96 --- @param fname string 97 --- @param text string 98 local function tofile(fname, text) 99 local f = assert(io.open(fname, 'w'), ('failed to open: %s'):format(fname)) 100 f:write(text) 101 f:close() 102 print('Written to:', fname) 103 end 104 105 ---@param opt vim._gen_lsp.opt 106 ---@return vim._gen_lsp.Protocol 107 local function read_json(opt) 108 local uri = 'https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/' 109 .. opt.version 110 .. '/metaModel/metaModel.json' 111 print('Reading ' .. uri) 112 113 local res = vim.system({ 'curl', '--no-progress-meter', uri, '-o', '-' }):wait() 114 if res.code ~= 0 or (res.stdout or ''):len() < 999 then 115 print(('URL failed: %s'):format(uri)) 116 vim.print(res) 117 error(res.stdout) 118 end 119 return vim.json.decode(res.stdout) 120 end 121 122 --- Gets the Lua symbol for a given fully-qualified LSP method name. 123 --- @param s string 124 --- @return string 125 local function to_luaname(s) 126 -- "$/" prefix is special: https://microsoft.github.io/language-server-protocol/specification/#dollarRequests 127 return (s:gsub('^%$', 'dollar'):gsub('/', '_')) 128 end 129 130 --- @param a vim._gen_lsp.Notification 131 --- @param b vim._gen_lsp.Notification 132 --- @return boolean 133 local function compare_method(a, b) 134 return to_luaname(a.method) < to_luaname(b.method) 135 end 136 137 ---@param protocol vim._gen_lsp.Protocol 138 local function write_to_vim_protocol(protocol) 139 local all = {} --- @type (vim._gen_lsp.Request|vim._gen_lsp.Notification)[] 140 vim.list_extend(all, protocol.notifications) 141 vim.list_extend(all, protocol.requests) 142 143 table.sort(all, compare_method) 144 table.sort(protocol.requests, compare_method) 145 table.sort(protocol.notifications, compare_method) 146 147 local output = { '-- Generated by gen_lsp.lua, keep at end of file.' } 148 149 do -- methods 150 for _, dir in ipairs({ 'clientToServer', 'serverToClient' }) do 151 local dir1 = dir:sub(1, 1):upper() .. dir:sub(2) 152 local alias = ('vim.lsp.protocol.Method.%s'):format(dir1) 153 for _, b in ipairs({ 154 { title = 'Request', methods = protocol.requests }, 155 { title = 'Notification', methods = protocol.notifications }, 156 }) do 157 output[#output + 1] = ('--- LSP %s (direction: %s)'):format(b.title, dir) 158 output[#output + 1] = ('--- @alias %s.%s'):format(alias, b.title) 159 for _, item in ipairs(b.methods) do 160 if item.messageDirection == dir or item.messageDirection == 'both' then 161 output[#output + 1] = ("--- | '%s',"):format(item.method) 162 end 163 end 164 output[#output + 1] = '' 165 end 166 167 vim.list_extend(output, { 168 ('--- LSP Message (direction: %s).'):format(dir), 169 ('--- @alias %s'):format(alias), 170 ('--- | %s.Request'):format(alias), 171 ('--- | %s.Notification'):format(alias), 172 '', 173 }) 174 end 175 176 vim.list_extend(output, { 177 '--- @alias vim.lsp.protocol.Method', 178 '--- | vim.lsp.protocol.Method.ClientToServer', 179 '--- | vim.lsp.protocol.Method.ServerToClient', 180 '', 181 '-- Generated by gen_lsp.lua, keep at end of file.', 182 '--- @deprecated Use `vim.lsp.protocol.Method` instead.', 183 '--- @enum vim.lsp.protocol.Methods', 184 '--- @see https://microsoft.github.io/language-server-protocol/specification/#metaModel', 185 '--- LSP method names.', 186 'protocol.Methods = {', 187 }) 188 189 for _, item in ipairs(all) do 190 if item.method then 191 if item.documentation then 192 local document = vim.split(item.documentation, '\n?\n', { trimempty = true }) 193 for _, docstring in ipairs(document) do 194 output[#output + 1] = ' --- ' .. docstring 195 end 196 end 197 output[#output + 1] = (" %s = '%s',"):format(to_luaname(item.method), item.method) 198 end 199 end 200 output[#output + 1] = '}' 201 end 202 203 do -- registrationMethods 204 local found = {} --- @type table<string, boolean> 205 vim.list_extend(output, { 206 '', 207 '-- Generated by gen_lsp.lua, keep at end of file.', 208 '--- LSP registration methods', 209 '---@alias vim.lsp.protocol.Method.Registration', 210 }) 211 for _, item in ipairs(all) do 212 if item.registrationMethod and not found[item.registrationMethod] then 213 vim.list_extend(output, { 214 ("--- | '%s'"):format(item.registrationMethod or item.method), 215 }) 216 found[item.registrationMethod or item.method] = true 217 end 218 end 219 end 220 221 do -- capabilities 222 vim.list_extend(output, { 223 '', 224 '-- stylua: ignore start', 225 '-- Generated by gen_lsp.lua, keep at end of file.', 226 '--- Maps method names to the required client capability', 227 '---TODO: also has workspace/* items because spec lacks a top-level "workspaceProvider"', 228 'protocol._provider_to_client_registration = {', 229 }) 230 231 local providers = {} --- @type table<string, string> 232 for _, item in ipairs(all) do 233 local base_provider = item.serverCapability and item.serverCapability:match('^[^%.]+') 234 if item.registrationOptions and not providers[base_provider] and item.clientCapability then 235 if item.clientCapability == item.serverCapability then 236 base_provider = nil 237 end 238 local key = base_provider or item.method 239 providers[key] = item.clientCapability 240 end 241 end 242 243 ---@type { provider: string, path : string }[] 244 local found_entries = {} 245 for key, value in pairs(providers) do 246 found_entries[#found_entries + 1] = { provider = key, path = value } 247 end 248 table.sort(found_entries, function(a, b) 249 return a.provider < b.provider 250 end) 251 for _, entry in ipairs(found_entries) do 252 output[#output + 1] = (" ['%s'] = { %s },"):format( 253 entry.provider, 254 "'" .. entry.path:gsub('%.', "', '") .. "'" 255 ) 256 end 257 258 output[#output + 1] = '}' 259 output[#output + 1] = '-- stylua: ignore end' 260 261 vim.list_extend(output, { 262 '', 263 '-- stylua: ignore start', 264 '-- Generated by gen_lsp.lua, keep at end of file.', 265 '--- Maps method names to the required server capability', 266 '-- A server capability equal to the method means there is no related server capability', 267 'protocol._request_name_to_server_capability = {', 268 }) 269 270 for _, item in ipairs(all) do 271 output[#output + 1] = (" ['%s'] = { %s },"):format( 272 item.method, 273 "'" .. (item.serverCapability or item.method):gsub('%.', "', '") .. "'" 274 ) 275 end 276 277 ---@type table<string, string[]> 278 local registration_capability = {} 279 for _, item in ipairs(all) do 280 if item.serverCapability then 281 if item.registrationMethod and item.registrationMethod ~= item.method then 282 local registrationMethod = item.registrationMethod 283 assert(registrationMethod, 'registrationMethod is nil') 284 if not registration_capability[item.registrationMethod] then 285 registration_capability[registrationMethod] = {} 286 end 287 table.insert(registration_capability[registrationMethod], item.serverCapability) 288 end 289 end 290 end 291 292 for registrationMethod, capabilities in pairs(registration_capability) do 293 output[#output + 1] = (" ['%s'] = { '%s' },"):format( 294 registrationMethod, 295 vim.iter(capabilities):fold(capabilities[1], function(acc, v) 296 return #v < #acc and v or acc 297 end) 298 ) 299 end 300 301 output[#output + 1] = '}' 302 output[#output + 1] = '-- stylua: ignore end' 303 304 vim.list_extend(output, { 305 '', 306 '-- stylua: ignore start', 307 '-- Generated by gen_lsp.lua, keep at end of file.', 308 'protocol._method_supports_dynamic_registration = {', 309 }) 310 311 --- These methods have no registrationOptions but can still be registered 312 --- TODO: remove if resolved upstream: https://github.com/microsoft/language-server-protocol/issues/2218 313 local methods_with_no_registration_options = { 314 ['workspace/didChangeWorkspaceFolders'] = true, 315 } 316 317 for _, item in ipairs(all) do 318 if 319 item.registrationMethod 320 or item.registrationOptions 321 or methods_with_no_registration_options[item.method] 322 then 323 output[#output + 1] = (" ['%s'] = %s,"):format(item.method, true) 324 end 325 end 326 327 output[#output + 1] = '}' 328 output[#output + 1] = '-- stylua: ignore end' 329 330 vim.list_extend(output, { 331 '', 332 '-- stylua: ignore start', 333 '-- Generated by gen_lsp.lua, keep at end of file.', 334 'protocol._method_supports_static_registration = {', 335 }) 336 337 for _, item in ipairs(all) do 338 if 339 item.registrationOptions 340 and (item.serverCapability and not item.serverCapability:find('%.')) 341 then 342 output[#output + 1] = (" ['%s'] = %s,"):format(item.method, true) 343 end 344 end 345 346 output[#output + 1] = '}' 347 output[#output + 1] = '-- stylua: ignore end' 348 349 vim.list_extend(output, { 350 '', 351 '-- stylua: ignore start', 352 '-- Generated by gen_lsp.lua, keep at end of file.', 353 '-- These methods have no registration options but can still be registered dynamically.', 354 'protocol._methods_with_no_registration_options = {', 355 }) 356 for key, v in pairs(methods_with_no_registration_options) do 357 output[#output + 1] = (" ['%s'] = %s ,"):format(key, v) 358 end 359 output[#output + 1] = '}' 360 output[#output + 1] = '-- stylua: ignore end' 361 end 362 363 output[#output + 1] = '' 364 output[#output + 1] = 'return protocol' 365 366 local fname = './runtime/lua/vim/lsp/protocol.lua' 367 local bufnr = vim.fn.bufadd(fname) 368 vim.fn.bufload(bufnr) 369 vim.api.nvim_set_current_buf(bufnr) 370 local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 371 local index = vim.iter(ipairs(lines)):find(function(key, item) 372 return vim.startswith(item, '-- Generated by') and key or nil 373 end) 374 index = index and index - 1 or vim.api.nvim_buf_line_count(bufnr) - 1 375 vim.api.nvim_buf_set_lines(bufnr, index, -1, true, output) 376 vim.cmd.write() 377 end 378 379 --- @param doc string 380 local function process_documentation(doc) 381 doc = doc:gsub('\n', '\n---') 382 -- Remove <200b> (zero-width space) unicode characters: e.g., `**/<200b>*` 383 doc = doc:gsub('\226\128\139', '') 384 -- Escape annotations that are not recognized by lua-ls 385 doc = doc:gsub('%^---@sample', '---\\@sample') 386 return '---' .. doc 387 end 388 389 local simple_types = { 390 string = true, 391 boolean = true, 392 integer = true, 393 uinteger = true, 394 decimal = true, 395 } 396 397 local anonymous_num = 0 398 399 --- @type string[] 400 local anonym_classes = {} 401 402 --- @param type vim._gen_lsp.Type 403 --- @param prefix? string Optional prefix associated with the this type, made of (nested) field name. 404 --- Used to generate class name for structure literal types. 405 --- @return string 406 local function parse_type(type, prefix) 407 if type.kind == 'reference' or type.kind == 'base' then 408 if type.kind == 'base' and type.name == 'string' and prefix == 'method' then 409 return 'vim.lsp.protocol.Method' 410 end 411 if simple_types[type.name] then 412 return type.name 413 end 414 return 'lsp.' .. type.name 415 elseif type.kind == 'array' then 416 local parsed_items = parse_type(type.element, prefix) 417 if type.element.items and #type.element.items > 1 then 418 parsed_items = '(' .. parsed_items .. ')' 419 end 420 return parsed_items .. '[]' 421 elseif type.kind == 'or' then 422 local types = {} --- @type string[] 423 for _, item in ipairs(type.items) do 424 types[#types + 1] = parse_type(item, prefix) 425 end 426 return table.concat(types, '|') 427 elseif type.kind == 'stringLiteral' then 428 return '"' .. type.value .. '"' 429 elseif type.kind == 'map' then 430 local key = assert(type.key) 431 local value = type.value --[[ @as vim._gen_lsp.Type ]] 432 return ('table<%s, %s>'):format(parse_type(key, prefix), parse_type(value, prefix)) 433 elseif type.kind == 'literal' then 434 -- can I use ---@param disabled? {reason: string} 435 -- use | to continue the inline class to be able to add docs 436 -- https://github.com/LuaLS/lua-language-server/issues/2128 437 anonymous_num = anonymous_num + 1 438 local anonymous_classname = 'lsp._anonym' .. anonymous_num 439 if prefix then 440 anonymous_classname = anonymous_classname .. '.' .. prefix 441 end 442 443 local anonym = { '---@class ' .. anonymous_classname } 444 if anonymous_num > 1 then 445 table.insert(anonym, 1, '') 446 end 447 448 ---@type vim._gen_lsp.StructureLiteral 449 local structural_literal = assert(type.value) --[[ @as vim._gen_lsp.StructureLiteral ]] 450 for _, field in ipairs(structural_literal.properties) do 451 anonym[#anonym + 1] = '---' 452 if field.documentation then 453 anonym[#anonym + 1] = process_documentation(field.documentation) 454 end 455 anonym[#anonym + 1] = ('---@field %s%s %s'):format( 456 field.name, 457 (field.optional and '?' or ''), 458 parse_type(field.type, prefix .. '.' .. field.name) 459 ) 460 end 461 for _, line in ipairs(anonym) do 462 if line then 463 anonym_classes[#anonym_classes + 1] = line 464 end 465 end 466 return anonymous_classname 467 elseif type.kind == 'tuple' then 468 local types = {} --- @type string[] 469 for _, value in ipairs(type.items) do 470 types[#types + 1] = parse_type(value, prefix) 471 end 472 return '[' .. table.concat(types, ', ') .. ']' 473 end 474 475 vim.print('WARNING: Unknown type ', type) 476 return '' 477 end 478 479 --- @param protocol vim._gen_lsp.Protocol 480 --- @param version string 481 --- @param output_file string 482 local function write_to_meta_protocol(protocol, version, output_file) 483 local output = { 484 '--' .. '[[', 485 'THIS FILE IS GENERATED by src/gen/gen_lsp.lua', 486 'DO NOT EDIT MANUALLY', 487 '', 488 'Based on LSP protocol ' .. version, 489 '', 490 'Regenerate:', 491 ([=[nvim -l src/gen/gen_lsp.lua --version %s]=]):format(version), 492 '--' .. ']]', 493 '', 494 '---@meta', 495 "error('Cannot require a meta file')", 496 '', 497 '---@alias lsp.null vim.NIL', 498 '---@alias uinteger integer', 499 '---@alias decimal number', 500 '---@alias lsp.DocumentUri string', 501 '---@alias lsp.URI string', 502 '', 503 } 504 505 for _, structure in ipairs(protocol.structures) do 506 if structure.documentation then 507 output[#output + 1] = process_documentation(structure.documentation) 508 end 509 local class_string = ('---@class lsp.%s'):format(structure.name) 510 if structure.extends or structure.mixins then 511 local inherits_from = table.concat( 512 vim.list_extend( 513 vim.tbl_map(parse_type, structure.extends or {}), 514 vim.tbl_map(parse_type, structure.mixins or {}) 515 ), 516 ', ' 517 ) 518 class_string = class_string .. ': ' .. inherits_from 519 end 520 output[#output + 1] = class_string 521 522 for _, field in ipairs(structure.properties or {}) do 523 output[#output + 1] = '---' -- Insert a single newline between @fields (and after @class) 524 if field.documentation then 525 output[#output + 1] = process_documentation(field.documentation) 526 end 527 output[#output + 1] = ('---@field %s%s %s'):format( 528 field.name, 529 (field.optional and '?' or ''), 530 parse_type(field.type, field.name) 531 ) 532 end 533 output[#output + 1] = '' 534 end 535 536 for _, enum in ipairs(protocol.enumerations) do 537 if enum.documentation then 538 output[#output + 1] = process_documentation(enum.documentation) 539 end 540 output[#output + 1] = '---@alias lsp.' .. enum.name 541 for _, value in ipairs(enum.values) do 542 local value1 = (type(value.value) == 'string' and ('"%s"'):format(value.value) or value.value) 543 output[#output + 1] = ('---| %s # %s'):format(value1, value.name) 544 end 545 output[#output + 1] = '' 546 end 547 548 for _, alias in ipairs(protocol.typeAliases) do 549 if alias.documentation then 550 output[#output + 1] = process_documentation(alias.documentation) 551 end 552 553 local alias_type --- @type string 554 555 if alias.type.kind == 'or' then 556 local alias_types = {} --- @type string[] 557 for _, item in ipairs(alias.type.items) do 558 alias_types[#alias_types + 1] = parse_type(item, alias.name) 559 end 560 alias_type = table.concat(alias_types, '|') 561 else 562 alias_type = parse_type(alias.type, alias.name) 563 end 564 output[#output + 1] = ('---@alias lsp.%s %s'):format(alias.name, alias_type) 565 output[#output + 1] = '' 566 end 567 568 -- anonymous classes 569 vim.list_extend(output, anonym_classes) 570 571 tofile(output_file, table.concat(output, '\n') .. '\n') 572 end 573 574 ---@class vim._gen_lsp.opt 575 ---@field output_file string 576 ---@field version string 577 578 --- @return vim._gen_lsp.opt 579 local function parse_args() 580 ---@type vim._gen_lsp.opt 581 local opt = { 582 output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua', 583 version = '3.18', 584 } 585 586 local i = 1 587 while i <= #_G.arg do 588 local cur_arg = _G.arg[i] 589 if cur_arg == '--out' then 590 opt.output_file = assert(_G.arg[i + 1], '--out <outfile> needed') 591 i = i + 1 592 elseif cur_arg == '--version' then 593 opt.version = assert(_G.arg[i + 1], '--version <version> needed') 594 i = i + 1 595 elseif cur_arg == '--help' or cur_arg == '-h' then 596 print(USAGE) 597 os.exit(0) 598 elseif vim.startswith(cur_arg, '-') then 599 print('Unrecognized option:', cur_arg, '\n') 600 os.exit(1) 601 end 602 i = i + 1 603 end 604 605 return opt 606 end 607 608 local function main() 609 local opt = parse_args() 610 local protocol = read_json(opt) 611 write_to_vim_protocol(protocol) 612 write_to_meta_protocol(protocol, opt.version, opt.output_file) 613 end 614 615 main()