gen_eval_files.lua 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  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.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.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. local req_args = type(fun.args) == 'table' and fun.args[1] or fun.args or 0
  431. for i, param in ipairs(params) do
  432. local pname, ptype = luaescape(param[1]), param[2]
  433. local optional = (pname ~= '...' and i > req_args) and '?' or ''
  434. write(fmt('--- @param %s%s %s', pname, optional, ptype))
  435. end
  436. if fun.returns ~= false then
  437. local ret_desc = fun.returns_desc and ' # ' .. fun.returns_desc or ''
  438. write('--- @return ' .. (fun.returns or 'any') .. ret_desc)
  439. end
  440. write(render_fun_sig(funname, params))
  441. end
  442. --- Generates vimdoc heading for a Vimscript "eval" function signature.
  443. --- @param name string
  444. --- @param name_tag boolean
  445. --- @param fun vim.EvalFn
  446. --- @param write fun(line: string)
  447. local function render_sig_and_tag(name, name_tag, fun, write)
  448. if not fun.signature then
  449. return
  450. end
  451. local tags = name_tag and { '*' .. name .. '()*' } or {}
  452. if fun.tags then
  453. for _, t in ipairs(fun.tags) do
  454. tags[#tags + 1] = '*' .. t .. '*'
  455. end
  456. end
  457. if #tags == 0 then
  458. write(fun.signature)
  459. return
  460. end
  461. local tag = table.concat(tags, ' ')
  462. local siglen = #fun.signature
  463. local conceal_offset = 2 * (#tags - 1)
  464. local tag_pad_len = math.max(1, 80 - #tag + conceal_offset)
  465. if siglen + #tag > 80 then
  466. write(string.rep(' ', tag_pad_len) .. tag)
  467. write(fun.signature)
  468. else
  469. write(fmt('%s%s%s', fun.signature, string.rep(' ', tag_pad_len - siglen), tag))
  470. end
  471. end
  472. --- Generates vimdoc for a Vimscript "eval" function.
  473. --- @param f string
  474. --- @param fun vim.EvalFn
  475. --- @param write fun(line: string)
  476. local function render_eval_doc(f, fun, write)
  477. if fun.deprecated or not fun.signature then
  478. return
  479. end
  480. render_sig_and_tag(fun.name or f, not f:find('__%d+$'), fun, write)
  481. if not fun.desc then
  482. return
  483. end
  484. local params = process_params(fun.params)
  485. local req_args = type(fun.args) == 'table' and fun.args[1] or fun.args or 0
  486. local desc_l = split(vim.trim(fun.desc))
  487. for _, l in ipairs(desc_l) do
  488. l = l:gsub('^ ', '')
  489. if vim.startswith(l, '<') and not l:match('^<[^ \t]+>') then
  490. write('<\t\t' .. l:sub(2))
  491. elseif l:match('^>[a-z0-9]*$') then
  492. write(l)
  493. else
  494. write('\t\t' .. l)
  495. end
  496. end
  497. if #desc_l > 0 and not desc_l[#desc_l]:match('^<?$') then
  498. write('')
  499. end
  500. if #params > 0 then
  501. write(util.md_to_vimdoc('Parameters: ~', 16, 16, TEXT_WIDTH))
  502. for i, param in ipairs(params) do
  503. local pname, ptype = param[1], param[2]
  504. local optional = (pname ~= '...' and i > req_args) and '?' or ''
  505. local s = fmt('- %-14s (`%s%s`)', fmt('{%s}', pname), ptype, optional)
  506. write(util.md_to_vimdoc(s, 16, 18, TEXT_WIDTH))
  507. end
  508. write('')
  509. end
  510. if fun.returns ~= false then
  511. write(util.md_to_vimdoc('Return: ~', 16, 16, TEXT_WIDTH))
  512. local ret = ('(`%s`)'):format((fun.returns or 'any'))
  513. ret = ret .. (fun.returns_desc and ' ' .. fun.returns_desc or '')
  514. ret = util.md_to_vimdoc(ret, 18, 18, TEXT_WIDTH)
  515. write(ret)
  516. write('')
  517. end
  518. end
  519. --- @param d vim.option_defaults
  520. --- @param vimdoc? boolean
  521. --- @return string
  522. local function render_option_default(d, vimdoc)
  523. local dt --- @type integer|boolean|string|fun(): string
  524. if d.if_false ~= nil then
  525. dt = d.if_false
  526. else
  527. dt = d.if_true
  528. end
  529. if vimdoc then
  530. if d.doc then
  531. return d.doc
  532. end
  533. if type(dt) == 'boolean' then
  534. return dt and 'on' or 'off'
  535. end
  536. end
  537. if dt == '' or dt == nil or type(dt) == 'function' then
  538. dt = d.meta
  539. end
  540. local v --- @type string
  541. if not vimdoc then
  542. v = vim.inspect(dt) --[[@as string]]
  543. else
  544. v = type(dt) == 'string' and '"' .. dt .. '"' or tostring(dt)
  545. end
  546. --- @type table<string, string|false>
  547. local envvars = {
  548. TMPDIR = false,
  549. VIMRUNTIME = false,
  550. XDG_CONFIG_HOME = vim.env.HOME .. '/.local/config',
  551. XDG_DATA_HOME = vim.env.HOME .. '/.local/share',
  552. XDG_STATE_HOME = vim.env.HOME .. '/.local/state',
  553. }
  554. for name, default in pairs(envvars) do
  555. local value = vim.env[name] or default
  556. if value then
  557. v = v:gsub(vim.pesc(value), '$' .. name)
  558. end
  559. end
  560. return v
  561. end
  562. --- @param _f string
  563. --- @param opt vim.option_meta
  564. --- @param write fun(line: string)
  565. local function render_option_meta(_f, opt, write)
  566. write('')
  567. for _, l in ipairs(split(norm_text(opt.desc))) do
  568. write('--- ' .. l)
  569. end
  570. write('--- @type ' .. OPTION_TYPES[opt.type])
  571. write('vim.o.' .. opt.full_name .. ' = ' .. render_option_default(opt.defaults))
  572. if opt.abbreviation then
  573. write('vim.o.' .. opt.abbreviation .. ' = vim.o.' .. opt.full_name)
  574. end
  575. for _, s in pairs {
  576. { 'wo', 'win' },
  577. { 'bo', 'buf' },
  578. { 'go', 'global' },
  579. } do
  580. local id, scope = s[1], s[2]
  581. if vim.list_contains(opt.scope, scope) or (id == 'go' and #opt.scope > 1) then
  582. local pfx = 'vim.' .. id .. '.'
  583. write(pfx .. opt.full_name .. ' = vim.o.' .. opt.full_name)
  584. if opt.abbreviation then
  585. write(pfx .. opt.abbreviation .. ' = ' .. pfx .. opt.full_name)
  586. end
  587. end
  588. end
  589. end
  590. --- @param _f string
  591. --- @param opt vim.option_meta
  592. --- @param write fun(line: string)
  593. local function render_vvar_meta(_f, opt, write)
  594. write('')
  595. local desc = split(norm_text(opt.desc))
  596. while desc[#desc]:match('^%s*$') do
  597. desc[#desc] = nil
  598. end
  599. for _, l in ipairs(desc) do
  600. write('--- ' .. l)
  601. end
  602. write('--- @type ' .. (opt.type or 'any'))
  603. if LUA_KEYWORDS[opt.full_name] then
  604. write("vim.v['" .. opt.full_name .. "'] = ...")
  605. else
  606. write('vim.v.' .. opt.full_name .. ' = ...')
  607. end
  608. end
  609. --- @param s string[]
  610. --- @return string
  611. local function scope_to_doc(s)
  612. local m = {
  613. global = 'global',
  614. buf = 'local to buffer',
  615. win = 'local to window',
  616. tab = 'local to tab page',
  617. }
  618. if #s == 1 then
  619. return m[s[1]]
  620. end
  621. assert(s[1] == 'global')
  622. return 'global or ' .. m[s[2]] .. (s[2] ~= 'tab' and ' |global-local|' or '')
  623. end
  624. -- @param o vim.option_meta
  625. -- @return string
  626. local function scope_more_doc(o)
  627. if
  628. vim.list_contains({
  629. 'bufhidden',
  630. 'buftype',
  631. 'filetype',
  632. 'modified',
  633. 'previewwindow',
  634. 'readonly',
  635. 'scroll',
  636. 'syntax',
  637. 'winfixheight',
  638. 'winfixwidth',
  639. }, o.full_name)
  640. then
  641. return ' |local-noglobal|'
  642. end
  643. return ''
  644. end
  645. --- @param x string
  646. --- @return string
  647. local function dedent(x)
  648. local xs = split(x)
  649. local leading_ws = xs[1]:match('^%s*') --[[@as string]]
  650. local leading_ws_pat = '^' .. leading_ws
  651. for i in ipairs(xs) do
  652. local strip_pat = xs[i]:match(leading_ws_pat) and leading_ws_pat or '^%s*'
  653. xs[i] = xs[i]:gsub(strip_pat, '')
  654. end
  655. return table.concat(xs, '\n')
  656. end
  657. --- @return table<string,vim.option_meta>
  658. local function get_option_meta()
  659. local opts = require('src/nvim/options').options
  660. local optinfo = vim.api.nvim_get_all_options_info()
  661. local ret = {} --- @type table<string,vim.option_meta>
  662. for _, o in ipairs(opts) do
  663. local is_window_option = #o.scope == 1 and o.scope[1] == 'win'
  664. local is_option_hidden = o.immutable and not o.varname and not is_window_option
  665. if not is_option_hidden and o.desc then
  666. if o.full_name == 'cmdheight' then
  667. table.insert(o.scope, 'tab')
  668. end
  669. local r = vim.deepcopy(o) --[[@as vim.option_meta]]
  670. r.desc = o.desc:gsub('^ ', ''):gsub('\n ', '\n')
  671. r.defaults = r.defaults or {}
  672. if r.defaults.meta == nil then
  673. r.defaults.meta = optinfo[o.full_name].default
  674. end
  675. ret[o.full_name] = r
  676. end
  677. end
  678. return ret
  679. end
  680. --- @return table<string,vim.option_meta>
  681. local function get_vvar_meta()
  682. local info = require('src/nvim/vvars').vars
  683. local ret = {} --- @type table<string,vim.option_meta>
  684. for name, o in pairs(info) do
  685. o.desc = dedent(o.desc)
  686. o.full_name = name
  687. ret[name] = o
  688. end
  689. return ret
  690. end
  691. --- @param opt vim.option_meta
  692. --- @return string[]
  693. local function build_option_tags(opt)
  694. --- @type string[]
  695. local tags = { opt.full_name }
  696. tags[#tags + 1] = opt.abbreviation
  697. if opt.type == 'boolean' then
  698. for i = 1, #tags do
  699. tags[#tags + 1] = 'no' .. tags[i]
  700. end
  701. end
  702. for i, t in ipairs(tags) do
  703. tags[i] = "'" .. t .. "'"
  704. end
  705. for _, t in ipairs(opt.tags or {}) do
  706. tags[#tags + 1] = t
  707. end
  708. for i, t in ipairs(tags) do
  709. tags[i] = '*' .. t .. '*'
  710. end
  711. return tags
  712. end
  713. --- @param _f string
  714. --- @param opt vim.option_meta
  715. --- @param write fun(line: string)
  716. local function render_option_doc(_f, opt, write)
  717. local tags = build_option_tags(opt)
  718. local tag_str = table.concat(tags, ' ')
  719. local conceal_offset = 2 * (#tags - 1)
  720. local tag_pad = string.rep('\t', math.ceil((64 - #tag_str + conceal_offset) / 8))
  721. -- local pad = string.rep(' ', 80 - #tag_str + conceal_offset)
  722. write(tag_pad .. tag_str)
  723. local name_str --- @type string
  724. if opt.abbreviation then
  725. name_str = fmt("'%s' '%s'", opt.full_name, opt.abbreviation)
  726. else
  727. name_str = fmt("'%s'", opt.full_name)
  728. end
  729. local otype = opt.type == 'boolean' and 'boolean' or opt.type
  730. if opt.defaults.doc or opt.defaults.if_true ~= nil or opt.defaults.meta ~= nil then
  731. local v = render_option_default(opt.defaults, true)
  732. local pad = string.rep('\t', math.max(1, math.ceil((24 - #name_str) / 8)))
  733. if opt.defaults.doc then
  734. local deflen = #fmt('%s%s%s (', name_str, pad, otype)
  735. --- @type string
  736. v = v:gsub('\n', '\n' .. string.rep(' ', deflen - 2))
  737. end
  738. write(fmt('%s%s%s\t(default %s)', name_str, pad, otype, v))
  739. else
  740. write(fmt('%s\t%s', name_str, otype))
  741. end
  742. write('\t\t\t' .. scope_to_doc(opt.scope) .. scope_more_doc(opt))
  743. for _, l in ipairs(split(opt.desc)) do
  744. if l == '<' or l:match('^<%s') then
  745. write(l)
  746. else
  747. write('\t' .. l:gsub('\\<', '<'))
  748. end
  749. end
  750. end
  751. --- @param _f string
  752. --- @param vvar vim.option_meta
  753. --- @param write fun(line: string)
  754. local function render_vvar_doc(_f, vvar, write)
  755. local name = vvar.full_name
  756. local tags = { 'v:' .. name, name .. '-variable' }
  757. if vvar.tags then
  758. vim.list_extend(tags, vvar.tags)
  759. end
  760. for i, t in ipairs(tags) do
  761. tags[i] = '*' .. t .. '*'
  762. end
  763. local tag_str = table.concat(tags, ' ')
  764. local conceal_offset = 2 * (#tags - 1)
  765. local tag_pad = string.rep('\t', math.ceil((64 - #tag_str + conceal_offset) / 8))
  766. write(tag_pad .. tag_str)
  767. local desc = split(vvar.desc)
  768. if (#desc == 1 or #desc == 2 and desc[2]:match('^%s*$')) and #name < 10 then
  769. -- single line
  770. write('v:' .. name .. '\t' .. desc[1]:gsub('^%s*', ''))
  771. write('')
  772. else
  773. write('v:' .. name)
  774. for _, l in ipairs(desc) do
  775. if l == '<' or l:match('^<%s') then
  776. write(l)
  777. else
  778. write('\t\t' .. l:gsub('\\<', '<'))
  779. end
  780. end
  781. end
  782. end
  783. --- @class nvim.gen_eval_files.elem
  784. --- @field path string
  785. --- @field from? string Skip lines in path until this pattern is reached.
  786. --- @field funcs fun(): table<string, table>
  787. --- @field render fun(f:string,obj:table,write:fun(line:string))
  788. --- @field header? string[]
  789. --- @field footer? string[]
  790. --- @type nvim.gen_eval_files.elem[]
  791. local CONFIG = {
  792. {
  793. path = 'runtime/lua/vim/_meta/vimfn.lua',
  794. header = LUA_META_HEADER,
  795. funcs = get_eval_meta,
  796. render = render_eval_meta,
  797. },
  798. {
  799. path = 'runtime/lua/vim/_meta/api.lua',
  800. header = LUA_API_META_HEADER,
  801. funcs = get_api_meta,
  802. render = render_api_meta,
  803. },
  804. {
  805. path = 'runtime/lua/vim/_meta/api_keysets.lua',
  806. header = LUA_META_HEADER,
  807. funcs = get_api_keysets_meta,
  808. render = render_api_keyset_meta,
  809. },
  810. {
  811. path = 'runtime/doc/builtin.txt',
  812. funcs = get_eval_meta,
  813. render = render_eval_doc,
  814. header = {
  815. '*builtin.txt* Nvim',
  816. '',
  817. '',
  818. '\t\t NVIM REFERENCE MANUAL',
  819. '',
  820. '',
  821. 'Builtin functions\t\t*vimscript-functions* *builtin-functions*',
  822. '',
  823. 'For functions grouped by what they are used for see |function-list|.',
  824. '',
  825. '\t\t\t\t Type |gO| to see the table of contents.',
  826. '==============================================================================',
  827. '1. Details *builtin-function-details*',
  828. '',
  829. },
  830. footer = {
  831. '==============================================================================',
  832. '2. Matching a pattern in a String *string-match*',
  833. '',
  834. 'This is common between several functions. A regexp pattern as explained at',
  835. '|pattern| is normally used to find a match in the buffer lines. When a',
  836. 'pattern is used to find a match in a String, almost everything works in the',
  837. 'same way. The difference is that a String is handled like it is one line.',
  838. 'When it contains a "\\n" character, this is not seen as a line break for the',
  839. 'pattern. It can be matched with a "\\n" in the pattern, or with ".". Example:',
  840. '>vim',
  841. '\tlet a = "aaaa\\nxxxx"',
  842. '\techo matchstr(a, "..\\n..")',
  843. '\t" aa',
  844. '\t" xx',
  845. '\techo matchstr(a, "a.x")',
  846. '\t" a',
  847. '\t" x',
  848. '',
  849. 'Don\'t forget that "^" will only match at the first character of the String and',
  850. '"$" at the last character of the string. They don\'t match after or before a',
  851. '"\\n".',
  852. '',
  853. ' vim:tw=78:ts=8:noet:ft=help:norl:',
  854. },
  855. },
  856. {
  857. path = 'runtime/lua/vim/_meta/options.lua',
  858. header = LUA_OPTION_META_HEADER,
  859. funcs = get_option_meta,
  860. render = render_option_meta,
  861. },
  862. {
  863. path = 'runtime/doc/options.txt',
  864. header = { '' },
  865. from = 'A jump table for the options with a short description can be found at |Q_op|.',
  866. footer = {
  867. ' vim:tw=78:ts=8:noet:ft=help:norl:',
  868. },
  869. funcs = get_option_meta,
  870. render = render_option_doc,
  871. },
  872. {
  873. path = 'runtime/lua/vim/_meta/vvars.lua',
  874. header = LUA_VVAR_META_HEADER,
  875. funcs = get_vvar_meta,
  876. render = render_vvar_meta,
  877. },
  878. {
  879. path = 'runtime/doc/vvars.txt',
  880. header = { '' },
  881. from = 'Type |gO| to see the table of contents.',
  882. footer = {
  883. ' vim:tw=78:ts=8:noet:ft=help:norl:',
  884. },
  885. funcs = get_vvar_meta,
  886. render = render_vvar_doc,
  887. },
  888. }
  889. --- @param elem nvim.gen_eval_files.elem
  890. local function render(elem)
  891. print('Rendering ' .. elem.path)
  892. local from_lines = {} --- @type string[]
  893. local from = elem.from
  894. if from then
  895. for line in io.lines(elem.path) do
  896. from_lines[#from_lines + 1] = line
  897. if line:match(from) then
  898. break
  899. end
  900. end
  901. end
  902. local o = assert(io.open(elem.path, 'w'))
  903. --- @param l string
  904. local function write(l)
  905. local l1 = l:gsub('%s+$', '')
  906. o:write(l1)
  907. o:write('\n')
  908. end
  909. for _, l in ipairs(from_lines) do
  910. write(l)
  911. end
  912. for _, l in ipairs(elem.header or {}) do
  913. write(l)
  914. end
  915. local funcs = elem.funcs()
  916. --- @type string[]
  917. local fnames = vim.tbl_keys(funcs)
  918. table.sort(fnames)
  919. for _, f in ipairs(fnames) do
  920. elem.render(f, funcs[f], write)
  921. end
  922. for _, l in ipairs(elem.footer or {}) do
  923. write(l)
  924. end
  925. o:close()
  926. end
  927. local function main()
  928. for _, c in ipairs(CONFIG) do
  929. render(c)
  930. end
  931. end
  932. main()