gen_eval_files.lua 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078
  1. #!/usr/bin/env -S nvim -l
  2. -- Generator for various vimdoc and Lua type files
  3. local util = require('scripts.util')
  4. local fmt = string.format
  5. local DEP_API_METADATA = 'build/funcs_metadata.mpack'
  6. local TEXT_WIDTH = 78
  7. --- @class vim.api.metadata
  8. --- @field name string
  9. --- @field parameters [string,string][]
  10. --- @field return_type string
  11. --- @field deprecated_since integer
  12. --- @field eval boolean
  13. --- @field fast boolean
  14. --- @field handler_id integer
  15. --- @field impl_name string
  16. --- @field lua boolean
  17. --- @field method boolean
  18. --- @field remote boolean
  19. --- @field since integer
  20. local LUA_API_RETURN_OVERRIDES = {
  21. nvim_buf_get_command = 'table<string,vim.api.keyset.command_info>',
  22. nvim_buf_get_extmark_by_id = 'vim.api.keyset.get_extmark_item_by_id',
  23. nvim_buf_get_extmarks = 'vim.api.keyset.get_extmark_item[]',
  24. nvim_buf_get_keymap = 'vim.api.keyset.get_keymap[]',
  25. nvim_get_autocmds = 'vim.api.keyset.get_autocmds.ret[]',
  26. nvim_get_color_map = 'table<string,integer>',
  27. nvim_get_command = 'table<string,vim.api.keyset.command_info>',
  28. nvim_get_keymap = 'vim.api.keyset.get_keymap[]',
  29. nvim_get_mark = 'vim.api.keyset.get_mark',
  30. -- Can also return table<string,vim.api.keyset.get_hl_info>, however we need to
  31. -- pick one to get some benefit.
  32. -- REVISIT lewrus01 (26/01/24): we can maybe add
  33. -- @overload fun(ns: integer, {}): table<string,vim.api.keyset.get_hl_info>
  34. nvim_get_hl = 'vim.api.keyset.get_hl_info',
  35. nvim_get_mode = 'vim.api.keyset.get_mode',
  36. nvim_get_namespaces = 'table<string,integer>',
  37. nvim_get_option_info = 'vim.api.keyset.get_option_info',
  38. nvim_get_option_info2 = 'vim.api.keyset.get_option_info',
  39. nvim_parse_cmd = 'vim.api.keyset.parse_cmd',
  40. nvim_win_get_config = 'vim.api.keyset.win_config',
  41. }
  42. local LUA_API_KEYSET_OVERRIDES = {
  43. create_autocmd = {
  44. callback = 'string|(fun(args: vim.api.keyset.create_autocmd.callback_args): boolean?)',
  45. },
  46. }
  47. local LUA_API_PARAM_OVERRIDES = {
  48. nvim_create_user_command = {
  49. command = 'string|fun(args: vim.api.keyset.create_user_command.command_args)',
  50. },
  51. }
  52. local LUA_META_HEADER = {
  53. '--- @meta _',
  54. '-- THIS FILE IS GENERATED',
  55. '-- DO NOT EDIT',
  56. "error('Cannot require a meta file')",
  57. }
  58. local LUA_API_META_HEADER = {
  59. '--- @meta _',
  60. '-- THIS FILE IS GENERATED',
  61. '-- DO NOT EDIT',
  62. "error('Cannot require a meta file')",
  63. '',
  64. 'vim.api = {}',
  65. }
  66. local LUA_OPTION_META_HEADER = {
  67. '--- @meta _',
  68. '-- THIS FILE IS GENERATED',
  69. '-- DO NOT EDIT',
  70. "error('Cannot require a meta file')",
  71. '',
  72. '---@class vim.bo',
  73. '---@field [integer] vim.bo',
  74. 'vim.bo = vim.bo',
  75. '',
  76. '---@class vim.wo',
  77. '---@field [integer] vim.wo',
  78. 'vim.wo = vim.wo',
  79. }
  80. local LUA_VVAR_META_HEADER = {
  81. '--- @meta _',
  82. '-- THIS FILE IS GENERATED',
  83. '-- DO NOT EDIT',
  84. "error('Cannot require a meta file')",
  85. '',
  86. '--- @class vim.v',
  87. 'vim.v = ...',
  88. }
  89. local LUA_KEYWORDS = {
  90. ['and'] = true,
  91. ['end'] = true,
  92. ['function'] = true,
  93. ['or'] = true,
  94. ['if'] = true,
  95. ['while'] = true,
  96. ['repeat'] = true,
  97. ['true'] = true,
  98. ['false'] = true,
  99. }
  100. local OPTION_TYPES = {
  101. boolean = 'boolean',
  102. number = 'integer',
  103. string = 'string',
  104. }
  105. local API_TYPES = {
  106. Window = 'integer',
  107. Tabpage = 'integer',
  108. Buffer = 'integer',
  109. Boolean = 'boolean',
  110. Object = 'any',
  111. Integer = 'integer',
  112. String = 'string',
  113. Array = 'any[]',
  114. LuaRef = 'function',
  115. Dict = 'table<string,any>',
  116. Float = 'number',
  117. HLGroupID = 'integer|string',
  118. void = '',
  119. }
  120. --- @param s string
  121. --- @return string
  122. local function luaescape(s)
  123. if LUA_KEYWORDS[s] then
  124. return s .. '_'
  125. end
  126. return s
  127. end
  128. --- @param x string
  129. --- @param sep? string
  130. --- @return string[]
  131. local function split(x, sep)
  132. return vim.split(x, sep or '\n', { plain = true })
  133. end
  134. --- Convert an API type to Lua
  135. --- @param t string
  136. --- @return string
  137. local function api_type(t)
  138. if vim.startswith(t, '*') then
  139. return api_type(t:sub(2)) .. '?'
  140. end
  141. local as0 = t:match('^ArrayOf%((.*)%)')
  142. if as0 then
  143. local as = split(as0, ', ')
  144. return api_type(as[1]) .. '[]'
  145. end
  146. local d = t:match('^Dict%((.*)%)')
  147. if d then
  148. return 'vim.api.keyset.' .. d
  149. end
  150. local d0 = t:match('^DictOf%((.*)%)')
  151. if d0 then
  152. return 'table<string,' .. api_type(d0) .. '>'
  153. end
  154. local u = t:match('^Union%((.*)%)')
  155. if u then
  156. local us = vim.split(u, ',%s*')
  157. return table.concat(vim.tbl_map(api_type, us), '|')
  158. end
  159. local l = t:match('^LuaRefOf%((.*)%)')
  160. if l then
  161. --- @type string
  162. l = l:gsub('%s+', ' ')
  163. --- @type string?, string?
  164. local as, r = l:match('%((.*)%),%s*(.*)')
  165. if not as then
  166. --- @type string
  167. as = assert(l:match('%((.*)%)'))
  168. end
  169. local as1 = {} --- @type string[]
  170. for a in vim.gsplit(as, ',%s') do
  171. local a1 = vim.split(a, '%s+', { trimempty = true })
  172. local nm = a1[2]:gsub('%*(.*)$', '%1?')
  173. as1[#as1 + 1] = nm .. ': ' .. api_type(a1[1])
  174. end
  175. return ('fun(%s)%s'):format(table.concat(as1, ', '), r and ': ' .. api_type(r) or '')
  176. end
  177. return API_TYPES[t] or t
  178. end
  179. --- @param f string
  180. --- @param params [string,string][]|true
  181. --- @return string
  182. local function render_fun_sig(f, params)
  183. local param_str --- @type string
  184. if params == true then
  185. param_str = '...'
  186. else
  187. param_str = table.concat(
  188. vim.tbl_map(
  189. --- @param v [string,string]
  190. --- @return string
  191. function(v)
  192. return luaescape(v[1])
  193. end,
  194. params
  195. ),
  196. ', '
  197. )
  198. end
  199. if LUA_KEYWORDS[f] then
  200. return fmt("vim.fn['%s'] = function(%s) end", f, param_str)
  201. else
  202. return fmt('function vim.fn.%s(%s) end', f, param_str)
  203. end
  204. end
  205. --- Uniquify names
  206. --- @param params [string,string,string][]
  207. --- @return [string,string,string][]
  208. local function process_params(params)
  209. local seen = {} --- @type table<string,true>
  210. local sfx = 1
  211. for _, p in ipairs(params) do
  212. if seen[p[1]] then
  213. p[1] = p[1] .. sfx
  214. sfx = sfx + 1
  215. else
  216. seen[p[1]] = true
  217. end
  218. end
  219. return params
  220. end
  221. --- @return table<string, vim.EvalFn>
  222. local function get_api_meta()
  223. local ret = {} --- @type table<string, vim.EvalFn>
  224. local cdoc_parser = require('scripts.cdoc_parser')
  225. local f = 'src/nvim/api'
  226. local function include(fun)
  227. if not vim.startswith(fun.name, 'nvim_') then
  228. return false
  229. end
  230. if vim.tbl_contains(fun.attrs or {}, 'lua_only') then
  231. return true
  232. end
  233. if vim.tbl_contains(fun.attrs or {}, 'remote_only') then
  234. return false
  235. end
  236. return true
  237. end
  238. --- @type table<string,nvim.cdoc.parser.fun>
  239. local functions = {}
  240. for path, ty in vim.fs.dir(f) do
  241. if ty == 'file' then
  242. local filename = vim.fs.joinpath(f, path)
  243. local _, funs = cdoc_parser.parse(filename)
  244. for _, fn in ipairs(funs) do
  245. if include(fn) then
  246. functions[fn.name] = fn
  247. end
  248. end
  249. end
  250. end
  251. for _, fun in pairs(functions) do
  252. local deprecated = fun.deprecated_since ~= nil
  253. local notes = {} --- @type string[]
  254. for _, note in ipairs(fun.notes or {}) do
  255. notes[#notes + 1] = note.desc
  256. end
  257. local sees = {} --- @type string[]
  258. for _, see in ipairs(fun.see or {}) do
  259. sees[#sees + 1] = see.desc
  260. end
  261. local pty_overrides = LUA_API_PARAM_OVERRIDES[fun.name] or {}
  262. local params = {} --- @type [string,string][]
  263. for _, p in ipairs(fun.params) do
  264. params[#params + 1] = {
  265. p.name,
  266. api_type(pty_overrides[p.name] or p.type),
  267. not deprecated and p.desc or nil,
  268. }
  269. end
  270. local r = {
  271. signature = 'NA',
  272. name = fun.name,
  273. params = params,
  274. notes = notes,
  275. see = sees,
  276. returns = api_type(fun.returns[1].type),
  277. deprecated = deprecated,
  278. }
  279. if not deprecated then
  280. r.desc = fun.desc
  281. r.returns_desc = fun.returns[1].desc
  282. end
  283. ret[fun.name] = r
  284. end
  285. return ret
  286. end
  287. --- Convert vimdoc references to markdown literals
  288. --- Convert vimdoc codeblocks to markdown codeblocks
  289. ---
  290. --- Ensure code blocks have one empty line before the start fence and after the closing fence.
  291. ---
  292. --- @param x string
  293. --- @param special string?
  294. --- | 'see-api-meta' Normalize `@see` for API meta docstrings.
  295. --- @return string
  296. local function norm_text(x, special)
  297. if special == 'see-api-meta' then
  298. -- Try to guess a symbol that actually works in @see.
  299. -- "nvim_xx()" => "vim.api.nvim_xx"
  300. x = x:gsub([=[%|?(nvim_[^.()| ]+)%(?%)?%|?]=], 'vim.api.%1')
  301. -- TODO: Remove backticks when LuaLS resolves: https://github.com/LuaLS/lua-language-server/issues/2889
  302. -- "|foo|" => "`:help foo`"
  303. x = x:gsub([=[|([^ ]+)|]=], '`:help %1`')
  304. end
  305. return (
  306. x:gsub('|([^ ]+)|', '`%1`')
  307. :gsub('\n*>lua', '\n\n```lua')
  308. :gsub('\n*>vim', '\n\n```vim')
  309. :gsub('\n+<$', '\n```')
  310. :gsub('\n+<\n+', '\n```\n\n')
  311. :gsub('%s+>\n+', '\n```\n')
  312. :gsub('\n+<%s+\n?', '\n```\n')
  313. )
  314. end
  315. --- Generates LuaLS docstring for an API function.
  316. --- @param _f string
  317. --- @param fun vim.EvalFn
  318. --- @param write fun(line: string)
  319. local function render_api_meta(_f, fun, write)
  320. write('')
  321. if vim.startswith(fun.name, 'nvim__') then
  322. write('--- @private')
  323. end
  324. if fun.deprecated then
  325. write('--- @deprecated')
  326. end
  327. local desc = fun.desc
  328. if desc then
  329. write(util.prefix_lines('--- ', norm_text(desc)))
  330. end
  331. -- LuaLS doesn't support @note. Render @note items as a markdown list.
  332. if fun.notes and #fun.notes > 0 then
  333. write('--- Note:')
  334. write(util.prefix_lines('--- ', table.concat(fun.notes, '\n')))
  335. write('---')
  336. end
  337. for _, see in ipairs(fun.see or {}) do
  338. write(util.prefix_lines('--- @see ', norm_text(see, 'see-api-meta')))
  339. end
  340. local param_names = {} --- @type string[]
  341. local params = process_params(fun.params)
  342. for _, p in ipairs(params) do
  343. local pname, ptype, pdesc = luaescape(p[1]), p[2], p[3]
  344. param_names[#param_names + 1] = pname
  345. if pdesc then
  346. local s = '--- @param ' .. pname .. ' ' .. ptype .. ' '
  347. local pdesc_a = split(vim.trim(norm_text(pdesc)))
  348. write(s .. pdesc_a[1])
  349. for i = 2, #pdesc_a do
  350. if not pdesc_a[i] then
  351. break
  352. end
  353. write('--- ' .. pdesc_a[i])
  354. end
  355. else
  356. write('--- @param ' .. pname .. ' ' .. ptype)
  357. end
  358. end
  359. if fun.returns ~= '' then
  360. local ret_desc = fun.returns_desc and ' # ' .. fun.returns_desc or ''
  361. local ret = LUA_API_RETURN_OVERRIDES[fun.name] or fun.returns
  362. write(util.prefix_lines('--- ', '@return ' .. ret .. ret_desc))
  363. end
  364. local param_str = table.concat(param_names, ', ')
  365. write(fmt('function vim.api.%s(%s) end', fun.name, param_str))
  366. end
  367. --- @return table<string, vim.EvalFn>
  368. local function get_api_keysets_meta()
  369. local mpack_f = assert(io.open(DEP_API_METADATA, 'rb'))
  370. local metadata = assert(vim.mpack.decode(mpack_f:read('*all')))
  371. local ret = {} --- @type table<string, vim.EvalFn>
  372. --- @type {name: string, keys: string[], types: table<string,string>}[]
  373. local keysets = metadata.keysets
  374. for _, k in ipairs(keysets) do
  375. local pty_overrides = LUA_API_KEYSET_OVERRIDES[k.name] or {}
  376. local params = {}
  377. for _, key in ipairs(k.keys) do
  378. local pty = pty_overrides[key] or k.types[key] or 'any'
  379. table.insert(params, { key .. '?', api_type(pty) })
  380. end
  381. ret[k.name] = {
  382. signature = 'NA',
  383. name = k.name,
  384. params = params,
  385. }
  386. end
  387. return ret
  388. end
  389. --- Generates LuaLS docstring for an API keyset.
  390. --- @param _f string
  391. --- @param fun vim.EvalFn
  392. --- @param write fun(line: string)
  393. local function render_api_keyset_meta(_f, fun, write)
  394. if string.sub(fun.name, 1, 1) == '_' then
  395. return -- not exported
  396. end
  397. write('')
  398. write('--- @class vim.api.keyset.' .. fun.name)
  399. for _, p in ipairs(fun.params) do
  400. write('--- @field ' .. p[1] .. ' ' .. p[2])
  401. end
  402. end
  403. --- @return table<string, vim.EvalFn>
  404. local function get_eval_meta()
  405. return require('src/nvim/eval').funcs
  406. end
  407. --- Generates LuaLS docstring for a Vimscript "eval" function.
  408. --- @param f string
  409. --- @param fun vim.EvalFn
  410. --- @param write fun(line: string)
  411. local function render_eval_meta(f, fun, write)
  412. if fun.lua == false then
  413. return
  414. end
  415. local funname = fun.name or f
  416. local params = process_params(fun.params)
  417. write('')
  418. if fun.deprecated then
  419. write('--- @deprecated')
  420. end
  421. local desc = fun.desc
  422. if desc then
  423. --- @type string
  424. desc = desc:gsub('\n%s*\n%s*$', '\n')
  425. for _, l in ipairs(split(desc)) do
  426. l = l:gsub('^ ', ''):gsub('\t', ' '):gsub('@', '\\@')
  427. write('--- ' .. l)
  428. end
  429. end
  430. for _, text in ipairs(vim.fn.reverse(fun.generics or {})) do
  431. write(fmt('--- @generic %s', text))
  432. end
  433. local req_args = type(fun.args) == 'table' and fun.args[1] or fun.args or 0
  434. for i, param in ipairs(params) do
  435. local pname, ptype = luaescape(param[1]), param[2]
  436. local optional = (pname ~= '...' and i > req_args) and '?' or ''
  437. write(fmt('--- @param %s%s %s', pname, optional, ptype))
  438. end
  439. if fun.returns ~= false then
  440. local ret_desc = fun.returns_desc and ' # ' .. fun.returns_desc or ''
  441. write('--- @return ' .. (fun.returns or 'any') .. ret_desc)
  442. end
  443. write(render_fun_sig(funname, params))
  444. end
  445. --- Generates vimdoc heading for a Vimscript "eval" function signature.
  446. --- @param name string
  447. --- @param name_tag boolean
  448. --- @param fun vim.EvalFn
  449. --- @param write fun(line: string)
  450. local function render_sig_and_tag(name, name_tag, fun, write)
  451. if not fun.signature then
  452. return
  453. end
  454. local tags = name_tag and { '*' .. name .. '()*' } or {}
  455. if fun.tags then
  456. for _, t in ipairs(fun.tags) do
  457. tags[#tags + 1] = '*' .. t .. '*'
  458. end
  459. end
  460. if #tags == 0 then
  461. write(fun.signature)
  462. return
  463. end
  464. local tag = table.concat(tags, ' ')
  465. local siglen = #fun.signature
  466. local conceal_offset = 2 * (#tags - 1)
  467. local tag_pad_len = math.max(1, 80 - #tag + conceal_offset)
  468. if siglen + #tag > 80 then
  469. write(string.rep(' ', tag_pad_len) .. tag)
  470. write(fun.signature)
  471. else
  472. write(fmt('%s%s%s', fun.signature, string.rep(' ', tag_pad_len - siglen), tag))
  473. end
  474. end
  475. --- Generates vimdoc for a Vimscript "eval" function.
  476. --- @param f string
  477. --- @param fun vim.EvalFn
  478. --- @param write fun(line: string)
  479. local function render_eval_doc(f, fun, write)
  480. if fun.deprecated or not fun.signature then
  481. return
  482. end
  483. render_sig_and_tag(fun.name or f, not f:find('__%d+$'), fun, write)
  484. if not fun.desc then
  485. return
  486. end
  487. local params = process_params(fun.params)
  488. local req_args = type(fun.args) == 'table' and fun.args[1] or fun.args or 0
  489. local desc_l = split(vim.trim(fun.desc))
  490. for _, l in ipairs(desc_l) do
  491. l = l:gsub('^ ', '')
  492. if vim.startswith(l, '<') and not l:match('^<[^ \t]+>') then
  493. write('<\t\t' .. l:sub(2))
  494. elseif l:match('^>[a-z0-9]*$') then
  495. write(l)
  496. else
  497. write('\t\t' .. l)
  498. end
  499. end
  500. if #desc_l > 0 and not desc_l[#desc_l]:match('^<?$') then
  501. write('')
  502. end
  503. if #params > 0 then
  504. write(util.md_to_vimdoc('Parameters: ~', 16, 16, TEXT_WIDTH))
  505. for i, param in ipairs(params) do
  506. local pname, ptype = param[1], param[2]
  507. local optional = (pname ~= '...' and i > req_args) and '?' or ''
  508. local s = fmt('- %-14s (`%s%s`)', fmt('{%s}', pname), ptype, optional)
  509. write(util.md_to_vimdoc(s, 16, 18, TEXT_WIDTH))
  510. end
  511. write('')
  512. end
  513. if fun.returns ~= false then
  514. write(util.md_to_vimdoc('Return: ~', 16, 16, TEXT_WIDTH))
  515. local ret = ('(`%s`)'):format((fun.returns or 'any'))
  516. ret = ret .. (fun.returns_desc and ' ' .. fun.returns_desc or '')
  517. ret = util.md_to_vimdoc(ret, 18, 18, TEXT_WIDTH)
  518. write(ret)
  519. write('')
  520. end
  521. end
  522. --- @param d vim.option_defaults
  523. --- @param vimdoc? boolean
  524. --- @return string
  525. local function render_option_default(d, vimdoc)
  526. local dt --- @type integer|boolean|string|fun(): string
  527. if d.if_false ~= nil then
  528. dt = d.if_false
  529. else
  530. dt = d.if_true
  531. end
  532. if vimdoc then
  533. if d.doc then
  534. return d.doc
  535. end
  536. if type(dt) == 'boolean' then
  537. return dt and 'on' or 'off'
  538. end
  539. end
  540. if dt == '' or dt == nil or type(dt) == 'function' then
  541. dt = d.meta
  542. end
  543. local v --- @type string
  544. if not vimdoc then
  545. v = vim.inspect(dt) --[[@as string]]
  546. else
  547. v = type(dt) == 'string' and '"' .. dt .. '"' or tostring(dt)
  548. end
  549. --- @type table<string, string|false>
  550. local envvars = {
  551. TMPDIR = false,
  552. VIMRUNTIME = false,
  553. XDG_CONFIG_HOME = vim.env.HOME .. '/.local/config',
  554. XDG_DATA_HOME = vim.env.HOME .. '/.local/share',
  555. XDG_STATE_HOME = vim.env.HOME .. '/.local/state',
  556. }
  557. for name, default in pairs(envvars) do
  558. local value = vim.env[name] or default
  559. if value then
  560. v = v:gsub(vim.pesc(value), '$' .. name)
  561. end
  562. end
  563. return v
  564. end
  565. --- @param _f string
  566. --- @param opt vim.option_meta
  567. --- @param write fun(line: string)
  568. local function render_option_meta(_f, opt, write)
  569. write('')
  570. for _, l in ipairs(split(norm_text(opt.desc))) do
  571. write('--- ' .. l)
  572. end
  573. write('--- @type ' .. OPTION_TYPES[opt.type])
  574. write('vim.o.' .. opt.full_name .. ' = ' .. render_option_default(opt.defaults))
  575. if opt.abbreviation then
  576. write('vim.o.' .. opt.abbreviation .. ' = vim.o.' .. opt.full_name)
  577. end
  578. for _, s in pairs {
  579. { 'wo', 'win' },
  580. { 'bo', 'buf' },
  581. { 'go', 'global' },
  582. } do
  583. local id, scope = s[1], s[2]
  584. if vim.list_contains(opt.scope, scope) or (id == 'go' and #opt.scope > 1) then
  585. local pfx = 'vim.' .. id .. '.'
  586. write(pfx .. opt.full_name .. ' = vim.o.' .. opt.full_name)
  587. if opt.abbreviation then
  588. write(pfx .. opt.abbreviation .. ' = ' .. pfx .. opt.full_name)
  589. end
  590. end
  591. end
  592. end
  593. --- @param _f string
  594. --- @param opt vim.option_meta
  595. --- @param write fun(line: string)
  596. local function render_vvar_meta(_f, opt, write)
  597. write('')
  598. local desc = split(norm_text(opt.desc))
  599. while desc[#desc]:match('^%s*$') do
  600. desc[#desc] = nil
  601. end
  602. for _, l in ipairs(desc) do
  603. write('--- ' .. l)
  604. end
  605. write('--- @type ' .. (opt.type or 'any'))
  606. if LUA_KEYWORDS[opt.full_name] then
  607. write("vim.v['" .. opt.full_name .. "'] = ...")
  608. else
  609. write('vim.v.' .. opt.full_name .. ' = ...')
  610. end
  611. end
  612. --- @param s string[]
  613. --- @return string
  614. local function scope_to_doc(s)
  615. local m = {
  616. global = 'global',
  617. buf = 'local to buffer',
  618. win = 'local to window',
  619. tab = 'local to tab page',
  620. }
  621. if #s == 1 then
  622. return m[s[1]]
  623. end
  624. assert(s[1] == 'global')
  625. return 'global or ' .. m[s[2]] .. (s[2] ~= 'tab' and ' |global-local|' or '')
  626. end
  627. -- @param o vim.option_meta
  628. -- @return string
  629. local function scope_more_doc(o)
  630. if
  631. vim.list_contains({
  632. 'bufhidden',
  633. 'buftype',
  634. 'filetype',
  635. 'modified',
  636. 'previewwindow',
  637. 'readonly',
  638. 'scroll',
  639. 'syntax',
  640. 'winfixheight',
  641. 'winfixwidth',
  642. }, o.full_name)
  643. then
  644. return ' |local-noglobal|'
  645. end
  646. return ''
  647. end
  648. --- @param x string
  649. --- @return string
  650. local function dedent(x)
  651. local xs = split(x)
  652. local leading_ws = xs[1]:match('^%s*') --[[@as string]]
  653. local leading_ws_pat = '^' .. leading_ws
  654. for i in ipairs(xs) do
  655. local strip_pat = xs[i]:match(leading_ws_pat) and leading_ws_pat or '^%s*'
  656. xs[i] = xs[i]:gsub(strip_pat, '')
  657. end
  658. return table.concat(xs, '\n')
  659. end
  660. --- @return table<string,vim.option_meta>
  661. local function get_option_meta()
  662. local opts = require('src/nvim/options').options
  663. local optinfo = vim.api.nvim_get_all_options_info()
  664. local ret = {} --- @type table<string,vim.option_meta>
  665. for _, o in ipairs(opts) do
  666. local is_window_option = #o.scope == 1 and o.scope[1] == 'win'
  667. local is_option_hidden = o.immutable and not o.varname and not is_window_option
  668. if not is_option_hidden and o.desc then
  669. if o.full_name == 'cmdheight' then
  670. table.insert(o.scope, 'tab')
  671. end
  672. local r = vim.deepcopy(o) --[[@as vim.option_meta]]
  673. r.desc = o.desc:gsub('^ ', ''):gsub('\n ', '\n')
  674. r.defaults = r.defaults or {}
  675. if r.defaults.meta == nil then
  676. r.defaults.meta = optinfo[o.full_name].default
  677. end
  678. ret[o.full_name] = r
  679. end
  680. end
  681. return ret
  682. end
  683. --- @return table<string,vim.option_meta>
  684. local function get_vvar_meta()
  685. local info = require('src/nvim/vvars').vars
  686. local ret = {} --- @type table<string,vim.option_meta>
  687. for name, o in pairs(info) do
  688. o.desc = dedent(o.desc)
  689. o.full_name = name
  690. ret[name] = o
  691. end
  692. return ret
  693. end
  694. --- @param opt vim.option_meta
  695. --- @return string[]
  696. local function build_option_tags(opt)
  697. --- @type string[]
  698. local tags = { opt.full_name }
  699. tags[#tags + 1] = opt.abbreviation
  700. if opt.type == 'boolean' then
  701. for i = 1, #tags do
  702. tags[#tags + 1] = 'no' .. tags[i]
  703. end
  704. end
  705. for i, t in ipairs(tags) do
  706. tags[i] = "'" .. t .. "'"
  707. end
  708. for _, t in ipairs(opt.tags or {}) do
  709. tags[#tags + 1] = t
  710. end
  711. for i, t in ipairs(tags) do
  712. tags[i] = '*' .. t .. '*'
  713. end
  714. return tags
  715. end
  716. --- @param _f string
  717. --- @param opt vim.option_meta
  718. --- @param write fun(line: string)
  719. local function render_option_doc(_f, opt, write)
  720. local tags = build_option_tags(opt)
  721. local tag_str = table.concat(tags, ' ')
  722. local conceal_offset = 2 * (#tags - 1)
  723. local tag_pad = string.rep('\t', math.ceil((64 - #tag_str + conceal_offset) / 8))
  724. -- local pad = string.rep(' ', 80 - #tag_str + conceal_offset)
  725. write(tag_pad .. tag_str)
  726. local name_str --- @type string
  727. if opt.abbreviation then
  728. name_str = fmt("'%s' '%s'", opt.full_name, opt.abbreviation)
  729. else
  730. name_str = fmt("'%s'", opt.full_name)
  731. end
  732. local otype = opt.type == 'boolean' and 'boolean' or opt.type
  733. if opt.defaults.doc or opt.defaults.if_true ~= nil or opt.defaults.meta ~= nil then
  734. local v = render_option_default(opt.defaults, true)
  735. local pad = string.rep('\t', math.max(1, math.ceil((24 - #name_str) / 8)))
  736. if opt.defaults.doc then
  737. local deflen = #fmt('%s%s%s (', name_str, pad, otype)
  738. --- @type string
  739. v = v:gsub('\n', '\n' .. string.rep(' ', deflen - 2))
  740. end
  741. write(fmt('%s%s%s\t(default %s)', name_str, pad, otype, v))
  742. else
  743. write(fmt('%s\t%s', name_str, otype))
  744. end
  745. write('\t\t\t' .. scope_to_doc(opt.scope) .. scope_more_doc(opt))
  746. for _, l in ipairs(split(opt.desc)) do
  747. if l == '<' or l:match('^<%s') then
  748. write(l)
  749. else
  750. write('\t' .. l:gsub('\\<', '<'))
  751. end
  752. end
  753. end
  754. --- @param _f string
  755. --- @param vvar vim.option_meta
  756. --- @param write fun(line: string)
  757. local function render_vvar_doc(_f, vvar, write)
  758. local name = vvar.full_name
  759. local tags = { 'v:' .. name, name .. '-variable' }
  760. if vvar.tags then
  761. vim.list_extend(tags, vvar.tags)
  762. end
  763. for i, t in ipairs(tags) do
  764. tags[i] = '*' .. t .. '*'
  765. end
  766. local tag_str = table.concat(tags, ' ')
  767. local conceal_offset = 2 * (#tags - 1)
  768. local tag_pad = string.rep('\t', math.ceil((64 - #tag_str + conceal_offset) / 8))
  769. write(tag_pad .. tag_str)
  770. local desc = split(vvar.desc)
  771. if (#desc == 1 or #desc == 2 and desc[2]:match('^%s*$')) and #name < 10 then
  772. -- single line
  773. write('v:' .. name .. '\t' .. desc[1]:gsub('^%s*', ''))
  774. write('')
  775. else
  776. write('v:' .. name)
  777. for _, l in ipairs(desc) do
  778. if l == '<' or l:match('^<%s') then
  779. write(l)
  780. else
  781. write('\t\t' .. l:gsub('\\<', '<'))
  782. end
  783. end
  784. end
  785. end
  786. --- @class nvim.gen_eval_files.elem
  787. --- @field path string
  788. --- @field from? string Skip lines in path until this pattern is reached.
  789. --- @field funcs fun(): table<string, table>
  790. --- @field render fun(f:string,obj:table,write:fun(line:string))
  791. --- @field header? string[]
  792. --- @field footer? string[]
  793. --- @type nvim.gen_eval_files.elem[]
  794. local CONFIG = {
  795. {
  796. path = 'runtime/lua/vim/_meta/vimfn.lua',
  797. header = LUA_META_HEADER,
  798. funcs = get_eval_meta,
  799. render = render_eval_meta,
  800. },
  801. {
  802. path = 'runtime/lua/vim/_meta/api.lua',
  803. header = LUA_API_META_HEADER,
  804. funcs = get_api_meta,
  805. render = render_api_meta,
  806. },
  807. {
  808. path = 'runtime/lua/vim/_meta/api_keysets.lua',
  809. header = LUA_META_HEADER,
  810. funcs = get_api_keysets_meta,
  811. render = render_api_keyset_meta,
  812. },
  813. {
  814. path = 'runtime/doc/builtin.txt',
  815. funcs = get_eval_meta,
  816. render = render_eval_doc,
  817. header = {
  818. '*builtin.txt* Nvim',
  819. '',
  820. '',
  821. '\t\t NVIM REFERENCE MANUAL',
  822. '',
  823. '',
  824. 'Builtin functions\t\t*vimscript-functions* *builtin-functions*',
  825. '',
  826. 'For functions grouped by what they are used for see |function-list|.',
  827. '',
  828. '\t\t\t\t Type |gO| to see the table of contents.',
  829. '==============================================================================',
  830. '1. Details *builtin-function-details*',
  831. '',
  832. },
  833. footer = {
  834. '==============================================================================',
  835. '2. Matching a pattern in a String *string-match*',
  836. '',
  837. 'This is common between several functions. A regexp pattern as explained at',
  838. '|pattern| is normally used to find a match in the buffer lines. When a',
  839. 'pattern is used to find a match in a String, almost everything works in the',
  840. 'same way. The difference is that a String is handled like it is one line.',
  841. 'When it contains a "\\n" character, this is not seen as a line break for the',
  842. 'pattern. It can be matched with a "\\n" in the pattern, or with ".". Example:',
  843. '>vim',
  844. '\tlet a = "aaaa\\nxxxx"',
  845. '\techo matchstr(a, "..\\n..")',
  846. '\t" aa',
  847. '\t" xx',
  848. '\techo matchstr(a, "a.x")',
  849. '\t" a',
  850. '\t" x',
  851. '',
  852. 'Don\'t forget that "^" will only match at the first character of the String and',
  853. '"$" at the last character of the string. They don\'t match after or before a',
  854. '"\\n".',
  855. '',
  856. ' vim:tw=78:ts=8:noet:ft=help:norl:',
  857. },
  858. },
  859. {
  860. path = 'runtime/lua/vim/_meta/options.lua',
  861. header = LUA_OPTION_META_HEADER,
  862. funcs = get_option_meta,
  863. render = render_option_meta,
  864. },
  865. {
  866. path = 'runtime/doc/options.txt',
  867. header = { '' },
  868. from = 'A jump table for the options with a short description can be found at |Q_op|.',
  869. footer = {
  870. ' vim:tw=78:ts=8:noet:ft=help:norl:',
  871. },
  872. funcs = get_option_meta,
  873. render = render_option_doc,
  874. },
  875. {
  876. path = 'runtime/lua/vim/_meta/vvars.lua',
  877. header = LUA_VVAR_META_HEADER,
  878. funcs = get_vvar_meta,
  879. render = render_vvar_meta,
  880. },
  881. {
  882. path = 'runtime/doc/vvars.txt',
  883. header = { '' },
  884. from = 'Type |gO| to see the table of contents.',
  885. footer = {
  886. ' vim:tw=78:ts=8:noet:ft=help:norl:',
  887. },
  888. funcs = get_vvar_meta,
  889. render = render_vvar_doc,
  890. },
  891. }
  892. --- @param elem nvim.gen_eval_files.elem
  893. local function render(elem)
  894. print('Rendering ' .. elem.path)
  895. local from_lines = {} --- @type string[]
  896. local from = elem.from
  897. if from then
  898. for line in io.lines(elem.path) do
  899. from_lines[#from_lines + 1] = line
  900. if line:match(from) then
  901. break
  902. end
  903. end
  904. end
  905. local o = assert(io.open(elem.path, 'w'))
  906. --- @param l string
  907. local function write(l)
  908. local l1 = l:gsub('%s+$', '')
  909. o:write(l1)
  910. o:write('\n')
  911. end
  912. for _, l in ipairs(from_lines) do
  913. write(l)
  914. end
  915. for _, l in ipairs(elem.header or {}) do
  916. write(l)
  917. end
  918. local funcs = elem.funcs()
  919. --- @type string[]
  920. local fnames = vim.tbl_keys(funcs)
  921. table.sort(fnames)
  922. for _, f in ipairs(fnames) do
  923. elem.render(f, funcs[f], write)
  924. end
  925. for _, l in ipairs(elem.footer or {}) do
  926. write(l)
  927. end
  928. o:close()
  929. end
  930. local function main()
  931. for _, c in ipairs(CONFIG) do
  932. render(c)
  933. end
  934. end
  935. main()