gen_lsp.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. -- Generates lua-ls annotations for lsp.
  2. local USAGE = [[
  3. Generates lua-ls annotations for lsp.
  4. USAGE:
  5. nvim -l scripts/gen_lsp.lua gen # by default, this will overwrite runtime/lua/vim/lsp/_meta/protocol.lua
  6. nvim -l scripts/gen_lsp.lua gen --version 3.18 --out runtime/lua/vim/lsp/_meta/protocol.lua
  7. nvim -l scripts/gen_lsp.lua gen --version 3.18 --methods --capabilities
  8. ]]
  9. local DEFAULT_LSP_VERSION = '3.18'
  10. local M = {}
  11. local function tofile(fname, text)
  12. local f = io.open(fname, 'w')
  13. if not f then
  14. error(('failed to write: %s'):format(f))
  15. else
  16. print(('Written to: %s'):format(fname))
  17. f:write(text)
  18. f:close()
  19. end
  20. end
  21. --- The LSP protocol JSON data (it's partial, non-exhaustive).
  22. --- https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/3.18/metaModel/metaModel.schema.json
  23. --- @class vim._gen_lsp.Protocol
  24. --- @field requests vim._gen_lsp.Request[]
  25. --- @field notifications vim._gen_lsp.Notification[]
  26. --- @field structures vim._gen_lsp.Structure[]
  27. --- @field enumerations vim._gen_lsp.Enumeration[]
  28. --- @field typeAliases vim._gen_lsp.TypeAlias[]
  29. ---@param opt vim._gen_lsp.opt
  30. ---@return vim._gen_lsp.Protocol
  31. local function read_json(opt)
  32. local uri = 'https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/'
  33. .. opt.version
  34. .. '/metaModel/metaModel.json'
  35. print('Reading ' .. uri)
  36. local res = vim.system({ 'curl', '--no-progress-meter', uri, '-o', '-' }):wait()
  37. if res.code ~= 0 or (res.stdout or ''):len() < 999 then
  38. print(('URL failed: %s'):format(uri))
  39. vim.print(res)
  40. error(res.stdout)
  41. end
  42. return vim.json.decode(res.stdout)
  43. end
  44. -- Gets the Lua symbol for a given fully-qualified LSP method name.
  45. local function to_luaname(s)
  46. -- "$/" prefix is special: https://microsoft.github.io/language-server-protocol/specification/#dollarRequests
  47. return s:gsub('^%$', 'dollar'):gsub('/', '_')
  48. end
  49. ---@param protocol vim._gen_lsp.Protocol
  50. ---@param gen_methods boolean
  51. ---@param gen_capabilities boolean
  52. local function write_to_protocol(protocol, gen_methods, gen_capabilities)
  53. if not gen_methods and not gen_capabilities then
  54. return
  55. end
  56. local indent = (' '):rep(2)
  57. --- @class vim._gen_lsp.Request
  58. --- @field deprecated? string
  59. --- @field documentation? string
  60. --- @field messageDirection string
  61. --- @field clientCapability? string
  62. --- @field serverCapability? string
  63. --- @field method string
  64. --- @field params? any
  65. --- @field proposed? boolean
  66. --- @field registrationMethod? string
  67. --- @field registrationOptions? any
  68. --- @field since? string
  69. --- @class vim._gen_lsp.Notification
  70. --- @field deprecated? string
  71. --- @field documentation? string
  72. --- @field errorData? any
  73. --- @field messageDirection string
  74. --- @field clientCapability? string
  75. --- @field serverCapability? string
  76. --- @field method string
  77. --- @field params? any[]
  78. --- @field partialResult? any
  79. --- @field proposed? boolean
  80. --- @field registrationMethod? string
  81. --- @field registrationOptions? any
  82. --- @field result any
  83. --- @field since? string
  84. ---@type (vim._gen_lsp.Request|vim._gen_lsp.Notification)[]
  85. local all = vim.list_extend(protocol.requests, protocol.notifications)
  86. table.sort(all, function(a, b)
  87. return to_luaname(a.method) < to_luaname(b.method)
  88. end)
  89. local output = { '-- Generated by gen_lsp.lua, keep at end of file.' }
  90. if gen_methods then
  91. output[#output + 1] = '--- @alias vim.lsp.protocol.Method.ClientToServer'
  92. for _, item in ipairs(all) do
  93. if item.method and item.messageDirection == 'clientToServer' then
  94. output[#output + 1] = ("--- | '%s',"):format(item.method)
  95. end
  96. end
  97. vim.list_extend(output, {
  98. '',
  99. '--- @alias vim.lsp.protocol.Method.ServerToClient',
  100. })
  101. for _, item in ipairs(all) do
  102. if item.method and item.messageDirection == 'serverToClient' then
  103. output[#output + 1] = ("--- | '%s',"):format(item.method)
  104. end
  105. end
  106. vim.list_extend(output, {
  107. '',
  108. '--- @alias vim.lsp.protocol.Method',
  109. '--- | vim.lsp.protocol.Method.ClientToServer',
  110. '--- | vim.lsp.protocol.Method.ServerToClient',
  111. '',
  112. '-- Generated by gen_lsp.lua, keep at end of file.',
  113. '--- @enum vim.lsp.protocol.Methods',
  114. '--- @see https://microsoft.github.io/language-server-protocol/specification/#metaModel',
  115. '--- LSP method names.',
  116. 'protocol.Methods = {',
  117. })
  118. for _, item in ipairs(all) do
  119. if item.method then
  120. if item.documentation then
  121. local document = vim.split(item.documentation, '\n?\n', { trimempty = true })
  122. for _, docstring in ipairs(document) do
  123. output[#output + 1] = indent .. '--- ' .. docstring
  124. end
  125. end
  126. output[#output + 1] = ("%s%s = '%s',"):format(indent, to_luaname(item.method), item.method)
  127. end
  128. end
  129. output[#output + 1] = '}'
  130. end
  131. if gen_capabilities then
  132. vim.list_extend(output, {
  133. '',
  134. '-- stylua: ignore start',
  135. '-- Generated by gen_lsp.lua, keep at end of file.',
  136. '--- Maps method names to the required server capability',
  137. 'protocol._request_name_to_capability = {',
  138. })
  139. for _, item in ipairs(all) do
  140. if item.serverCapability then
  141. output[#output + 1] = ("%s['%s'] = { %s },"):format(
  142. indent,
  143. item.method,
  144. table.concat(
  145. vim
  146. .iter(vim.split(item.serverCapability, '.', { plain = true }))
  147. :map(function(segment)
  148. return "'" .. segment .. "'"
  149. end)
  150. :totable(),
  151. ', '
  152. )
  153. )
  154. end
  155. end
  156. output[#output + 1] = '}'
  157. output[#output + 1] = '-- stylua: ignore end'
  158. end
  159. output[#output + 1] = ''
  160. output[#output + 1] = 'return protocol'
  161. local fname = './runtime/lua/vim/lsp/protocol.lua'
  162. local bufnr = vim.fn.bufadd(fname)
  163. vim.fn.bufload(bufnr)
  164. vim.api.nvim_set_current_buf(bufnr)
  165. local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
  166. local index = vim.iter(ipairs(lines)):find(function(key, item)
  167. return vim.startswith(item, '-- Generated by') and key or nil
  168. end)
  169. index = index and index - 1 or vim.api.nvim_buf_line_count(bufnr) - 1
  170. vim.api.nvim_buf_set_lines(bufnr, index, -1, true, output)
  171. vim.cmd.write()
  172. end
  173. ---@class vim._gen_lsp.opt
  174. ---@field output_file string
  175. ---@field version string
  176. ---@field methods boolean
  177. ---@field capabilities boolean
  178. ---@param opt vim._gen_lsp.opt
  179. function M.gen(opt)
  180. --- @type vim._gen_lsp.Protocol
  181. local protocol = read_json(opt)
  182. write_to_protocol(protocol, opt.methods, opt.capabilities)
  183. local output = {
  184. '--' .. '[[',
  185. 'THIS FILE IS GENERATED by scripts/gen_lsp.lua',
  186. 'DO NOT EDIT MANUALLY',
  187. '',
  188. 'Based on LSP protocol ' .. opt.version,
  189. '',
  190. 'Regenerate:',
  191. ([=[nvim -l scripts/gen_lsp.lua gen --version %s]=]):format(DEFAULT_LSP_VERSION),
  192. '--' .. ']]',
  193. '',
  194. '---@meta',
  195. "error('Cannot require a meta file')",
  196. '',
  197. '---@alias lsp.null nil',
  198. '---@alias uinteger integer',
  199. '---@alias decimal number',
  200. '---@alias lsp.DocumentUri string',
  201. '---@alias lsp.URI string',
  202. '',
  203. }
  204. local anonymous_num = 0
  205. ---@type string[]
  206. local anonym_classes = {}
  207. local simple_types = {
  208. 'string',
  209. 'boolean',
  210. 'integer',
  211. 'uinteger',
  212. 'decimal',
  213. }
  214. ---@param documentation string
  215. local _process_documentation = function(documentation)
  216. documentation = documentation:gsub('\n', '\n---')
  217. -- Remove <200b> (zero-width space) unicode characters: e.g., `**/<200b>*`
  218. documentation = documentation:gsub('\226\128\139', '')
  219. -- Escape annotations that are not recognized by lua-ls
  220. documentation = documentation:gsub('%^---@sample', '---\\@sample')
  221. return '---' .. documentation
  222. end
  223. --- @class vim._gen_lsp.Type
  224. --- @field kind string a common field for all Types.
  225. --- @field name? string for ReferenceType, BaseType
  226. --- @field element? any for ArrayType
  227. --- @field items? vim._gen_lsp.Type[] for OrType, AndType
  228. --- @field key? vim._gen_lsp.Type for MapType
  229. --- @field value? string|vim._gen_lsp.Type for StringLiteralType, MapType, StructureLiteralType
  230. ---@param type vim._gen_lsp.Type
  231. ---@param prefix? string Optional prefix associated with the this type, made of (nested) field name.
  232. --- Used to generate class name for structure literal types.
  233. ---@return string
  234. local function parse_type(type, prefix)
  235. -- ReferenceType | BaseType
  236. if type.kind == 'reference' or type.kind == 'base' then
  237. if vim.tbl_contains(simple_types, type.name) then
  238. return type.name
  239. end
  240. return 'lsp.' .. type.name
  241. -- ArrayType
  242. elseif type.kind == 'array' then
  243. local parsed_items = parse_type(type.element, prefix)
  244. if type.element.items and #type.element.items > 1 then
  245. parsed_items = '(' .. parsed_items .. ')'
  246. end
  247. return parsed_items .. '[]'
  248. -- OrType
  249. elseif type.kind == 'or' then
  250. local val = ''
  251. for _, item in ipairs(type.items) do
  252. val = val .. parse_type(item, prefix) .. '|' --[[ @as string ]]
  253. end
  254. val = val:sub(0, -2)
  255. return val
  256. -- StringLiteralType
  257. elseif type.kind == 'stringLiteral' then
  258. return '"' .. type.value .. '"'
  259. -- MapType
  260. elseif type.kind == 'map' then
  261. local key = assert(type.key)
  262. local value = type.value --[[ @as vim._gen_lsp.Type ]]
  263. return 'table<' .. parse_type(key, prefix) .. ', ' .. parse_type(value, prefix) .. '>'
  264. -- StructureLiteralType
  265. elseif type.kind == 'literal' then
  266. -- can I use ---@param disabled? {reason: string}
  267. -- use | to continue the inline class to be able to add docs
  268. -- https://github.com/LuaLS/lua-language-server/issues/2128
  269. anonymous_num = anonymous_num + 1
  270. local anonymous_classname = 'lsp._anonym' .. anonymous_num
  271. if prefix then
  272. anonymous_classname = anonymous_classname .. '.' .. prefix
  273. end
  274. local anonym = vim
  275. .iter({
  276. (anonymous_num > 1 and { '' } or {}),
  277. { '---@class ' .. anonymous_classname },
  278. })
  279. :flatten()
  280. :totable()
  281. --- @class vim._gen_lsp.StructureLiteral translated to anonymous @class.
  282. --- @field deprecated? string
  283. --- @field description? string
  284. --- @field properties vim._gen_lsp.Property[]
  285. --- @field proposed? boolean
  286. --- @field since? string
  287. ---@type vim._gen_lsp.StructureLiteral
  288. local structural_literal = assert(type.value) --[[ @as vim._gen_lsp.StructureLiteral ]]
  289. for _, field in ipairs(structural_literal.properties) do
  290. anonym[#anonym + 1] = '---'
  291. if field.documentation then
  292. anonym[#anonym + 1] = _process_documentation(field.documentation)
  293. end
  294. anonym[#anonym + 1] = '---@field '
  295. .. field.name
  296. .. (field.optional and '?' or '')
  297. .. ' '
  298. .. parse_type(field.type, prefix .. '.' .. field.name)
  299. end
  300. -- anonym[#anonym + 1] = ''
  301. for _, line in ipairs(anonym) do
  302. if line then
  303. anonym_classes[#anonym_classes + 1] = line
  304. end
  305. end
  306. return anonymous_classname
  307. -- TupleType
  308. elseif type.kind == 'tuple' then
  309. local tuple = '['
  310. for _, value in ipairs(type.items) do
  311. tuple = tuple .. parse_type(value, prefix) .. ', '
  312. end
  313. -- remove , at the end
  314. tuple = tuple:sub(0, -3)
  315. return tuple .. ']'
  316. end
  317. vim.print('WARNING: Unknown type ', type)
  318. return ''
  319. end
  320. --- @class vim._gen_lsp.Structure translated to @class
  321. --- @field deprecated? string
  322. --- @field documentation? string
  323. --- @field extends? { kind: string, name: string }[]
  324. --- @field mixins? { kind: string, name: string }[]
  325. --- @field name string
  326. --- @field properties? vim._gen_lsp.Property[] members, translated to @field
  327. --- @field proposed? boolean
  328. --- @field since? string
  329. for _, structure in ipairs(protocol.structures) do
  330. -- output[#output + 1] = ''
  331. if structure.documentation then
  332. output[#output + 1] = _process_documentation(structure.documentation)
  333. end
  334. local class_string = ('---@class lsp.%s'):format(structure.name)
  335. if structure.extends or structure.mixins then
  336. local inherits_from = table.concat(
  337. vim.list_extend(
  338. vim.tbl_map(parse_type, structure.extends or {}),
  339. vim.tbl_map(parse_type, structure.mixins or {})
  340. ),
  341. ', '
  342. )
  343. class_string = class_string .. ': ' .. inherits_from
  344. end
  345. output[#output + 1] = class_string
  346. --- @class vim._gen_lsp.Property translated to @field
  347. --- @field deprecated? string
  348. --- @field documentation? string
  349. --- @field name string
  350. --- @field optional? boolean
  351. --- @field proposed? boolean
  352. --- @field since? string
  353. --- @field type { kind: string, name: string }
  354. for _, field in ipairs(structure.properties or {}) do
  355. output[#output + 1] = '---' -- Insert a single newline between @fields (and after @class)
  356. if field.documentation then
  357. output[#output + 1] = _process_documentation(field.documentation)
  358. end
  359. output[#output + 1] = '---@field '
  360. .. field.name
  361. .. (field.optional and '?' or '')
  362. .. ' '
  363. .. parse_type(field.type, field.name)
  364. end
  365. output[#output + 1] = ''
  366. end
  367. --- @class vim._gen_lsp.Enumeration translated to @enum
  368. --- @field deprecated string?
  369. --- @field documentation string?
  370. --- @field name string?
  371. --- @field proposed boolean?
  372. --- @field since string?
  373. --- @field suportsCustomValues boolean?
  374. --- @field values { name: string, value: string, documentation?: string, since?: string }[]
  375. for _, enum in ipairs(protocol.enumerations) do
  376. if enum.documentation then
  377. output[#output + 1] = _process_documentation(enum.documentation)
  378. end
  379. local enum_type = '---@alias lsp.' .. enum.name
  380. for _, value in ipairs(enum.values) do
  381. enum_type = enum_type
  382. .. '\n---| '
  383. .. (type(value.value) == 'string' and '"' .. value.value .. '"' or value.value)
  384. .. ' # '
  385. .. value.name
  386. end
  387. output[#output + 1] = enum_type
  388. output[#output + 1] = ''
  389. end
  390. --- @class vim._gen_lsp.TypeAlias translated to @alias
  391. --- @field deprecated? string?
  392. --- @field documentation? string
  393. --- @field name string
  394. --- @field proposed? boolean
  395. --- @field since? string
  396. --- @field type vim._gen_lsp.Type
  397. for _, alias in ipairs(protocol.typeAliases) do
  398. if alias.documentation then
  399. output[#output + 1] = _process_documentation(alias.documentation)
  400. end
  401. if alias.type.kind == 'or' then
  402. local alias_type = '---@alias lsp.' .. alias.name .. ' '
  403. for _, item in ipairs(alias.type.items) do
  404. alias_type = alias_type .. parse_type(item, alias.name) .. '|'
  405. end
  406. alias_type = alias_type:sub(0, -2)
  407. output[#output + 1] = alias_type
  408. else
  409. output[#output + 1] = '---@alias lsp.'
  410. .. alias.name
  411. .. ' '
  412. .. parse_type(alias.type, alias.name)
  413. end
  414. output[#output + 1] = ''
  415. end
  416. -- anonymous classes
  417. for _, line in ipairs(anonym_classes) do
  418. output[#output + 1] = line
  419. end
  420. tofile(opt.output_file, table.concat(output, '\n') .. '\n')
  421. end
  422. ---@type vim._gen_lsp.opt
  423. local opt = {
  424. output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua',
  425. version = DEFAULT_LSP_VERSION,
  426. methods = false,
  427. capabilities = false,
  428. }
  429. local command = nil
  430. local i = 1
  431. while i <= #_G.arg do
  432. if _G.arg[i] == '--out' then
  433. opt.output_file = assert(_G.arg[i + 1], '--out <outfile> needed')
  434. i = i + 1
  435. elseif _G.arg[i] == '--version' then
  436. opt.version = assert(_G.arg[i + 1], '--version <version> needed')
  437. i = i + 1
  438. elseif _G.arg[i] == '--methods' then
  439. opt.methods = true
  440. elseif _G.arg[i] == '--capabilities' then
  441. opt.capabilities = true
  442. elseif vim.startswith(_G.arg[i], '-') then
  443. error('Unrecognized args: ' .. _G.arg[i])
  444. else
  445. if command then
  446. error('More than one command was given: ' .. _G.arg[i])
  447. else
  448. command = _G.arg[i]
  449. end
  450. end
  451. i = i + 1
  452. end
  453. if not command then
  454. print(USAGE)
  455. elseif M[command] then
  456. M[command](opt) -- see M.gen()
  457. else
  458. error('Unknown command: ' .. command)
  459. end
  460. return M