gen_lsp.lua 15 KB

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