gen_vimdoc.lua 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  1. #!/usr/bin/env -S nvim -l
  2. --- Generates Nvim :help docs from Lua/C docstrings
  3. ---
  4. --- The generated :help text for each function is formatted as follows:
  5. --- - Max width of 78 columns (`TEXT_WIDTH`).
  6. --- - Indent with spaces (not tabs).
  7. --- - Indent of 4 columns for body text (`INDENTATION`).
  8. --- - Function signature and helptag (right-aligned) on the same line.
  9. --- - Signature and helptag must have a minimum of 8 spaces between them.
  10. --- - If the signature is too long, it is placed on the line after the helptag.
  11. --- Signature wraps with subsequent lines indented to the open parenthesis.
  12. --- - Subsection bodies are indented an additional 4 spaces.
  13. --- - Body consists of function description, parameters, return description, and
  14. --- C declaration (`INCLUDE_C_DECL`).
  15. --- - Parameters are omitted for the `void` and `Error *` types, or if the
  16. --- parameter is marked as [out].
  17. --- - Each function documentation is separated by a single line.
  18. local luacats_parser = require('scripts.luacats_parser')
  19. local cdoc_parser = require('scripts.cdoc_parser')
  20. local util = require('scripts.util')
  21. local fmt = string.format
  22. local wrap = util.wrap
  23. local md_to_vimdoc = util.md_to_vimdoc
  24. local TEXT_WIDTH = 78
  25. local INDENTATION = 4
  26. --- @class (exact) nvim.gen_vimdoc.Config
  27. ---
  28. --- Generated documentation target, e.g. api.txt
  29. --- @field filename string
  30. ---
  31. --- @field section_order string[]
  32. ---
  33. --- List of files/directories for doxygen to read, relative to `base_dir`.
  34. --- @field files string[]
  35. ---
  36. --- @field exclude_types? true
  37. ---
  38. --- Section name overrides. Key: filename (e.g., vim.c)
  39. --- @field section_name? table<string,string>
  40. ---
  41. --- @field fn_name_pat? string
  42. ---
  43. --- @field fn_xform? fun(fun: nvim.luacats.parser.fun)
  44. ---
  45. --- For generated section names.
  46. --- @field section_fmt fun(name: string): string
  47. ---
  48. --- @field helptag_fmt fun(name: string): string|string[]
  49. ---
  50. --- Per-function helptag.
  51. --- @field fn_helptag_fmt? fun(fun: nvim.luacats.parser.fun): string
  52. ---
  53. --- @field append_only? string[]
  54. local function contains(t, xs)
  55. return vim.tbl_contains(xs, t)
  56. end
  57. --- @type {level:integer, prerelease:boolean}?
  58. local nvim_api_info_
  59. --- @return {level: integer, prerelease:boolean}
  60. local function nvim_api_info()
  61. if not nvim_api_info_ then
  62. --- @type integer?, boolean?
  63. local level, prerelease
  64. for l in io.lines('CMakeLists.txt') do
  65. --- @cast l string
  66. if level and prerelease then
  67. break
  68. end
  69. local m1 = l:match('^set%(NVIM_API_LEVEL%s+(%d+)%)')
  70. if m1 then
  71. level = tonumber(m1) --[[@as integer]]
  72. end
  73. local m2 = l:match('^set%(NVIM_API_PRERELEASE%s+(%w+)%)')
  74. if m2 then
  75. prerelease = m2 == 'true'
  76. end
  77. end
  78. nvim_api_info_ = { level = level, prerelease = prerelease }
  79. end
  80. return nvim_api_info_
  81. end
  82. --- @param fun nvim.luacats.parser.fun
  83. --- @return string
  84. local function fn_helptag_fmt_common(fun)
  85. local fn_sfx = fun.table and '' or '()'
  86. if fun.classvar then
  87. return fmt('%s:%s%s', fun.classvar, fun.name, fn_sfx)
  88. end
  89. if fun.module then
  90. return fmt('%s.%s%s', fun.module, fun.name, fn_sfx)
  91. end
  92. return fun.name .. fn_sfx
  93. end
  94. --- @type table<string,nvim.gen_vimdoc.Config>
  95. local config = {
  96. api = {
  97. filename = 'api.txt',
  98. section_order = {
  99. 'vim.c',
  100. 'vimscript.c',
  101. 'command.c',
  102. 'options.c',
  103. 'buffer.c',
  104. 'extmark.c',
  105. 'window.c',
  106. 'win_config.c',
  107. 'tabpage.c',
  108. 'autocmd.c',
  109. 'ui.c',
  110. },
  111. exclude_types = true,
  112. fn_name_pat = 'nvim_.*',
  113. files = { 'src/nvim/api' },
  114. section_name = {
  115. ['vim.c'] = 'Global',
  116. },
  117. section_fmt = function(name)
  118. return name .. ' Functions'
  119. end,
  120. helptag_fmt = function(name)
  121. return fmt('api-%s', name:lower())
  122. end,
  123. },
  124. lua = {
  125. filename = 'lua.txt',
  126. section_order = {
  127. 'hl.lua',
  128. 'diff.lua',
  129. 'mpack.lua',
  130. 'json.lua',
  131. 'base64.lua',
  132. 'spell.lua',
  133. 'builtin.lua',
  134. '_options.lua',
  135. '_editor.lua',
  136. '_inspector.lua',
  137. 'shared.lua',
  138. 'loader.lua',
  139. 'uri.lua',
  140. 'ui.lua',
  141. 'filetype.lua',
  142. 'keymap.lua',
  143. 'fs.lua',
  144. 'glob.lua',
  145. 'lpeg.lua',
  146. 're.lua',
  147. 'regex.lua',
  148. 'secure.lua',
  149. 'version.lua',
  150. 'iter.lua',
  151. 'snippet.lua',
  152. 'text.lua',
  153. 'tohtml.lua',
  154. },
  155. files = {
  156. 'runtime/lua/vim/iter.lua',
  157. 'runtime/lua/vim/_editor.lua',
  158. 'runtime/lua/vim/_options.lua',
  159. 'runtime/lua/vim/shared.lua',
  160. 'runtime/lua/vim/loader.lua',
  161. 'runtime/lua/vim/uri.lua',
  162. 'runtime/lua/vim/ui.lua',
  163. 'runtime/lua/vim/filetype.lua',
  164. 'runtime/lua/vim/keymap.lua',
  165. 'runtime/lua/vim/fs.lua',
  166. 'runtime/lua/vim/hl.lua',
  167. 'runtime/lua/vim/secure.lua',
  168. 'runtime/lua/vim/version.lua',
  169. 'runtime/lua/vim/_inspector.lua',
  170. 'runtime/lua/vim/snippet.lua',
  171. 'runtime/lua/vim/text.lua',
  172. 'runtime/lua/vim/glob.lua',
  173. 'runtime/lua/vim/_meta/builtin.lua',
  174. 'runtime/lua/vim/_meta/diff.lua',
  175. 'runtime/lua/vim/_meta/mpack.lua',
  176. 'runtime/lua/vim/_meta/json.lua',
  177. 'runtime/lua/vim/_meta/base64.lua',
  178. 'runtime/lua/vim/_meta/regex.lua',
  179. 'runtime/lua/vim/_meta/lpeg.lua',
  180. 'runtime/lua/vim/_meta/re.lua',
  181. 'runtime/lua/vim/_meta/spell.lua',
  182. 'runtime/lua/tohtml.lua',
  183. },
  184. fn_xform = function(fun)
  185. if contains(fun.module, { 'vim.uri', 'vim.shared', 'vim._editor' }) then
  186. fun.module = 'vim'
  187. end
  188. if fun.module == 'vim' and contains(fun.name, { 'cmd', 'inspect' }) then
  189. fun.table = nil
  190. end
  191. if fun.classvar or vim.startswith(fun.name, 'vim.') or fun.module == 'vim.iter' then
  192. return
  193. end
  194. fun.name = fmt('%s.%s', fun.module, fun.name)
  195. end,
  196. section_name = {
  197. ['_inspector.lua'] = 'inspector',
  198. },
  199. section_fmt = function(name)
  200. name = name:lower()
  201. if name == '_editor' then
  202. return 'Lua module: vim'
  203. elseif name == '_options' then
  204. return 'LUA-VIMSCRIPT BRIDGE'
  205. elseif name == 'builtin' then
  206. return 'VIM'
  207. end
  208. if
  209. contains(name, {
  210. 'hl',
  211. 'mpack',
  212. 'json',
  213. 'base64',
  214. 'diff',
  215. 'spell',
  216. 'regex',
  217. 'lpeg',
  218. 're',
  219. })
  220. then
  221. return 'VIM.' .. name:upper()
  222. end
  223. if name == 'tohtml' then
  224. return 'Lua module: tohtml'
  225. end
  226. return 'Lua module: vim.' .. name
  227. end,
  228. helptag_fmt = function(name)
  229. if name == '_editor' then
  230. return 'lua-vim'
  231. elseif name == '_options' then
  232. return 'lua-vimscript'
  233. elseif name == 'tohtml' then
  234. return 'tohtml'
  235. end
  236. return 'vim.' .. name:lower()
  237. end,
  238. fn_helptag_fmt = function(fun)
  239. local name = fun.name
  240. if vim.startswith(name, 'vim.') then
  241. local fn_sfx = fun.table and '' or '()'
  242. return name .. fn_sfx
  243. elseif fun.classvar == 'Option' then
  244. return fmt('vim.opt:%s()', name)
  245. end
  246. return fn_helptag_fmt_common(fun)
  247. end,
  248. append_only = {
  249. 'shared.lua',
  250. },
  251. },
  252. lsp = {
  253. filename = 'lsp.txt',
  254. section_order = {
  255. 'lsp.lua',
  256. 'client.lua',
  257. 'buf.lua',
  258. 'diagnostic.lua',
  259. 'codelens.lua',
  260. 'completion.lua',
  261. 'folding_range.lua',
  262. 'inlay_hint.lua',
  263. 'tagfunc.lua',
  264. 'semantic_tokens.lua',
  265. 'handlers.lua',
  266. 'util.lua',
  267. 'log.lua',
  268. 'rpc.lua',
  269. 'protocol.lua',
  270. },
  271. files = {
  272. 'runtime/lua/vim/lsp',
  273. 'runtime/lua/vim/lsp.lua',
  274. },
  275. fn_xform = function(fun)
  276. fun.name = fun.name:gsub('result%.', '')
  277. if fun.module == 'vim.lsp.protocol' then
  278. fun.classvar = nil
  279. end
  280. end,
  281. section_fmt = function(name)
  282. if name:lower() == 'lsp' then
  283. return 'Lua module: vim.lsp'
  284. end
  285. return 'Lua module: vim.lsp.' .. name:lower()
  286. end,
  287. helptag_fmt = function(name)
  288. if name:lower() == 'lsp' then
  289. return 'lsp-core'
  290. end
  291. return fmt('lsp-%s', name:lower())
  292. end,
  293. },
  294. diagnostic = {
  295. filename = 'diagnostic.txt',
  296. section_order = {
  297. 'diagnostic.lua',
  298. },
  299. files = { 'runtime/lua/vim/diagnostic.lua' },
  300. section_fmt = function()
  301. return 'Lua module: vim.diagnostic'
  302. end,
  303. helptag_fmt = function()
  304. return 'diagnostic-api'
  305. end,
  306. },
  307. treesitter = {
  308. filename = 'treesitter.txt',
  309. section_order = {
  310. 'tstree.lua',
  311. 'tsnode.lua',
  312. 'treesitter.lua',
  313. 'language.lua',
  314. 'query.lua',
  315. 'highlighter.lua',
  316. 'languagetree.lua',
  317. 'dev.lua',
  318. },
  319. files = {
  320. 'runtime/lua/vim/treesitter/_meta/',
  321. 'runtime/lua/vim/treesitter.lua',
  322. 'runtime/lua/vim/treesitter/',
  323. },
  324. section_fmt = function(name)
  325. if name:lower() == 'treesitter' then
  326. return 'Lua module: vim.treesitter'
  327. elseif name:lower() == 'tstree' then
  328. return 'TREESITTER TREES'
  329. elseif name:lower() == 'tsnode' then
  330. return 'TREESITTER NODES'
  331. end
  332. return 'Lua module: vim.treesitter.' .. name:lower()
  333. end,
  334. helptag_fmt = function(name)
  335. if name:lower() == 'treesitter' then
  336. return 'lua-treesitter-core'
  337. elseif name:lower() == 'query' then
  338. return 'lua-treesitter-query'
  339. elseif name:lower() == 'tstree' then
  340. return { 'treesitter-tree', 'TSTree' }
  341. elseif name:lower() == 'tsnode' then
  342. return { 'treesitter-node', 'TSNode' }
  343. end
  344. return 'treesitter-' .. name:lower()
  345. end,
  346. },
  347. editorconfig = {
  348. filename = 'editorconfig.txt',
  349. files = {
  350. 'runtime/lua/editorconfig.lua',
  351. },
  352. section_order = {
  353. 'editorconfig.lua',
  354. },
  355. section_fmt = function(_name)
  356. return 'EditorConfig integration'
  357. end,
  358. helptag_fmt = function(name)
  359. return name:lower()
  360. end,
  361. fn_xform = function(fun)
  362. fun.table = true
  363. fun.name = vim.split(fun.name, '.', { plain = true })[2]
  364. end,
  365. },
  366. health = {
  367. filename = 'health.txt',
  368. files = {
  369. 'runtime/lua/vim/health.lua',
  370. },
  371. section_order = {
  372. 'health.lua',
  373. },
  374. section_fmt = function(_name)
  375. return 'Checkhealth'
  376. end,
  377. helptag_fmt = function()
  378. return { 'vim.health', 'health' }
  379. end,
  380. },
  381. }
  382. --- @param ty string
  383. --- @param generics table<string,string>
  384. --- @return string
  385. local function replace_generics(ty, generics)
  386. if ty:sub(-2) == '[]' then
  387. local ty0 = ty:sub(1, -3)
  388. if generics[ty0] then
  389. return generics[ty0] .. '[]'
  390. end
  391. elseif ty:sub(-1) == '?' then
  392. local ty0 = ty:sub(1, -2)
  393. if generics[ty0] then
  394. return generics[ty0] .. '?'
  395. end
  396. end
  397. return generics[ty] or ty
  398. end
  399. --- @param name string
  400. local function fmt_field_name(name)
  401. local name0, opt = name:match('^([^?]*)(%??)$')
  402. return fmt('{%s}%s', name0, opt)
  403. end
  404. --- @param ty string
  405. --- @param generics? table<string,string>
  406. --- @param default? string
  407. local function render_type(ty, generics, default)
  408. if generics then
  409. ty = replace_generics(ty, generics)
  410. end
  411. ty = ty:gsub('%s*|%s*nil', '?')
  412. ty = ty:gsub('nil%s*|%s*(.*)', '%1?')
  413. ty = ty:gsub('%s*|%s*', '|')
  414. if default then
  415. return fmt('(`%s`, default: %s)', ty, default)
  416. end
  417. return fmt('(`%s`)', ty)
  418. end
  419. --- @param p nvim.luacats.parser.param|nvim.luacats.parser.field
  420. local function should_render_field_or_param(p)
  421. return not p.nodoc
  422. and not p.access
  423. and not contains(p.name, { '_', 'self' })
  424. and not vim.startswith(p.name, '_')
  425. end
  426. --- @param desc? string
  427. --- @return string?, string?
  428. local function get_default(desc)
  429. if not desc then
  430. return
  431. end
  432. local default = desc:match('\n%s*%([dD]efault: ([^)]+)%)')
  433. if default then
  434. desc = desc:gsub('\n%s*%([dD]efault: [^)]+%)', '')
  435. end
  436. return desc, default
  437. end
  438. --- @param ty string
  439. --- @param classes? table<string,nvim.luacats.parser.class>
  440. --- @return nvim.luacats.parser.class?
  441. local function get_class(ty, classes)
  442. if not classes then
  443. return
  444. end
  445. local cty = ty:gsub('%s*|%s*nil', '?'):gsub('?$', ''):gsub('%[%]$', '')
  446. return classes[cty]
  447. end
  448. --- @param obj nvim.luacats.parser.param|nvim.luacats.parser.return|nvim.luacats.parser.field
  449. --- @param classes? table<string,nvim.luacats.parser.class>
  450. local function inline_type(obj, classes)
  451. local ty = obj.type
  452. if not ty then
  453. return
  454. end
  455. local cls = get_class(ty, classes)
  456. if not cls or cls.nodoc then
  457. return
  458. end
  459. if not cls.inlinedoc then
  460. -- Not inlining so just add a: "See |tag|."
  461. local tag = fmt('|%s|', cls.name)
  462. if obj.desc and obj.desc:find(tag) then
  463. -- Tag already there
  464. return
  465. end
  466. -- TODO(lewis6991): Aim to remove this. Need this to prevent dead
  467. -- references to types defined in runtime/lua/vim/lsp/_meta/protocol.lua
  468. if not vim.startswith(cls.name, 'vim.') then
  469. return
  470. end
  471. obj.desc = obj.desc or ''
  472. local period = (obj.desc == '' or vim.endswith(obj.desc, '.')) and '' or '.'
  473. obj.desc = obj.desc .. fmt('%s See %s.', period, tag)
  474. return
  475. end
  476. local ty_isopt = (ty:match('%?$') or ty:match('%s*|%s*nil')) ~= nil
  477. local ty_islist = (ty:match('%[%]$')) ~= nil
  478. ty = ty_isopt and 'table?' or ty_islist and 'table[]' or 'table'
  479. local desc = obj.desc or ''
  480. if cls.desc then
  481. desc = desc .. cls.desc
  482. elseif desc == '' then
  483. if ty_islist then
  484. desc = desc .. 'A list of objects with the following fields:'
  485. elseif cls.parent then
  486. desc = desc .. fmt('Extends |%s| with the additional fields:', cls.parent)
  487. else
  488. desc = desc .. 'A table with the following fields:'
  489. end
  490. end
  491. local desc_append = {}
  492. for _, f in ipairs(cls.fields) do
  493. if not f.access then
  494. local fdesc, default = get_default(f.desc)
  495. local fty = render_type(f.type, nil, default)
  496. local fnm = fmt_field_name(f.name)
  497. table.insert(desc_append, table.concat({ '-', fnm, fty, fdesc }, ' '))
  498. end
  499. end
  500. desc = desc .. '\n' .. table.concat(desc_append, '\n')
  501. obj.type = ty
  502. obj.desc = desc
  503. end
  504. --- @param xs (nvim.luacats.parser.param|nvim.luacats.parser.field)[]
  505. --- @param generics? table<string,string>
  506. --- @param classes? table<string,nvim.luacats.parser.class>
  507. --- @param exclude_types? true
  508. --- @param cfg nvim.gen_vimdoc.Config
  509. local function render_fields_or_params(xs, generics, classes, exclude_types, cfg)
  510. local ret = {} --- @type string[]
  511. xs = vim.tbl_filter(should_render_field_or_param, xs)
  512. local indent = 0
  513. for _, p in ipairs(xs) do
  514. if p.type or p.desc then
  515. indent = math.max(indent, #p.name + 3)
  516. end
  517. if exclude_types then
  518. p.type = nil
  519. end
  520. end
  521. for _, p in ipairs(xs) do
  522. local pdesc, default = get_default(p.desc)
  523. p.desc = pdesc
  524. inline_type(p, classes)
  525. local nm, ty = p.name, p.type
  526. local desc = p.classvar and string.format('See |%s|.', cfg.fn_helptag_fmt(p)) or p.desc
  527. local fnm = p.kind == 'operator' and fmt('op(%s)', nm) or fmt_field_name(nm)
  528. local pnm = fmt(' • %-' .. indent .. 's', fnm)
  529. if ty then
  530. local pty = render_type(ty, generics, default)
  531. if desc then
  532. table.insert(ret, pnm)
  533. if #pty > TEXT_WIDTH - indent then
  534. vim.list_extend(ret, { ' ', pty, '\n' })
  535. table.insert(ret, md_to_vimdoc(desc, 9 + indent, 9 + indent, TEXT_WIDTH, true))
  536. else
  537. desc = fmt('%s %s', pty, desc)
  538. table.insert(ret, md_to_vimdoc(desc, 1, 9 + indent, TEXT_WIDTH, true))
  539. end
  540. else
  541. table.insert(ret, fmt('%s %s\n', pnm, pty))
  542. end
  543. else
  544. if desc then
  545. table.insert(ret, pnm)
  546. table.insert(ret, md_to_vimdoc(desc, 1, 9 + indent, TEXT_WIDTH, true))
  547. end
  548. end
  549. end
  550. return table.concat(ret)
  551. end
  552. --- @param class nvim.luacats.parser.class
  553. --- @param classes table<string,nvim.luacats.parser.class>
  554. --- @param cfg nvim.gen_vimdoc.Config
  555. local function render_class(class, classes, cfg)
  556. if class.access or class.nodoc or class.inlinedoc then
  557. return
  558. end
  559. local ret = {} --- @type string[]
  560. table.insert(ret, fmt('*%s*\n', class.name))
  561. if class.parent then
  562. local txt = fmt('Extends: |%s|', class.parent)
  563. table.insert(ret, md_to_vimdoc(txt, INDENTATION, INDENTATION, TEXT_WIDTH))
  564. table.insert(ret, '\n')
  565. end
  566. if class.desc then
  567. table.insert(ret, md_to_vimdoc(class.desc, INDENTATION, INDENTATION, TEXT_WIDTH))
  568. end
  569. local fields_txt = render_fields_or_params(class.fields, nil, classes, nil, cfg)
  570. if not fields_txt:match('^%s*$') then
  571. table.insert(ret, '\n Fields: ~\n')
  572. table.insert(ret, fields_txt)
  573. end
  574. table.insert(ret, '\n')
  575. return table.concat(ret)
  576. end
  577. --- @param classes table<string,nvim.luacats.parser.class>
  578. --- @param cfg nvim.gen_vimdoc.Config
  579. local function render_classes(classes, cfg)
  580. local ret = {} --- @type string[]
  581. for _, class in vim.spairs(classes) do
  582. ret[#ret + 1] = render_class(class, classes, cfg)
  583. end
  584. return table.concat(ret)
  585. end
  586. --- @param fun nvim.luacats.parser.fun
  587. --- @param cfg nvim.gen_vimdoc.Config
  588. local function render_fun_header(fun, cfg)
  589. local ret = {} --- @type string[]
  590. local args = {} --- @type string[]
  591. for _, p in ipairs(fun.params or {}) do
  592. if p.name ~= 'self' then
  593. args[#args + 1] = fmt_field_name(p.name)
  594. end
  595. end
  596. local nm = fun.name
  597. if fun.classvar then
  598. nm = fmt('%s:%s', fun.classvar, nm)
  599. end
  600. if nm == 'vim.bo' then
  601. nm = 'vim.bo[{bufnr}]'
  602. end
  603. if nm == 'vim.wo' then
  604. nm = 'vim.wo[{winid}][{bufnr}]'
  605. end
  606. local proto = fun.table and nm or nm .. '(' .. table.concat(args, ', ') .. ')'
  607. local tag = '*' .. cfg.fn_helptag_fmt(fun) .. '*'
  608. if #proto + #tag > TEXT_WIDTH - 8 then
  609. table.insert(ret, fmt('%78s\n', tag))
  610. local name, pargs = proto:match('([^(]+%()(.*)')
  611. table.insert(ret, name)
  612. table.insert(ret, wrap(pargs, 0, #name, TEXT_WIDTH))
  613. else
  614. local pad = TEXT_WIDTH - #proto - #tag
  615. table.insert(ret, proto .. string.rep(' ', pad) .. tag)
  616. end
  617. return table.concat(ret)
  618. end
  619. --- @param returns nvim.luacats.parser.return[]
  620. --- @param generics? table<string,string>
  621. --- @param classes? table<string,nvim.luacats.parser.class>
  622. --- @param exclude_types boolean
  623. local function render_returns(returns, generics, classes, exclude_types)
  624. local ret = {} --- @type string[]
  625. returns = vim.deepcopy(returns)
  626. if exclude_types then
  627. for _, r in ipairs(returns) do
  628. r.type = nil
  629. end
  630. end
  631. if #returns > 1 then
  632. table.insert(ret, ' Return (multiple): ~\n')
  633. elseif #returns == 1 and next(returns[1]) then
  634. table.insert(ret, ' Return: ~\n')
  635. end
  636. for _, p in ipairs(returns) do
  637. inline_type(p, classes)
  638. local rnm, ty, desc = p.name, p.type, p.desc
  639. local blk = {} --- @type string[]
  640. if ty then
  641. blk[#blk + 1] = render_type(ty, generics)
  642. end
  643. blk[#blk + 1] = rnm
  644. blk[#blk + 1] = desc
  645. table.insert(ret, md_to_vimdoc(table.concat(blk, ' '), 8, 8, TEXT_WIDTH, true))
  646. end
  647. return table.concat(ret)
  648. end
  649. --- @param fun nvim.luacats.parser.fun
  650. --- @param classes table<string,nvim.luacats.parser.class>
  651. --- @param cfg nvim.gen_vimdoc.Config
  652. local function render_fun(fun, classes, cfg)
  653. if fun.access or fun.deprecated or fun.nodoc then
  654. return
  655. end
  656. if cfg.fn_name_pat and not fun.name:match(cfg.fn_name_pat) then
  657. return
  658. end
  659. if vim.startswith(fun.name, '_') or fun.name:find('[:.]_') then
  660. return
  661. end
  662. local ret = {} --- @type string[]
  663. table.insert(ret, render_fun_header(fun, cfg))
  664. table.insert(ret, '\n')
  665. if fun.since then
  666. local since = assert(tonumber(fun.since), 'invalid @since on ' .. fun.name)
  667. local info = nvim_api_info()
  668. if since == 0 or (info.prerelease and since == info.level) then
  669. -- Experimental = (since==0 or current prerelease)
  670. local s = 'WARNING: This feature is experimental/unstable.'
  671. table.insert(ret, md_to_vimdoc(s, INDENTATION, INDENTATION, TEXT_WIDTH))
  672. table.insert(ret, '\n')
  673. else
  674. local v = assert(util.version_level[since], 'invalid @since on ' .. fun.name)
  675. fun.attrs = fun.attrs or {}
  676. table.insert(fun.attrs, ('Since: %s'):format(v))
  677. end
  678. end
  679. if fun.desc then
  680. table.insert(ret, md_to_vimdoc(fun.desc, INDENTATION, INDENTATION, TEXT_WIDTH))
  681. end
  682. if fun.notes then
  683. table.insert(ret, '\n Note: ~\n')
  684. for _, p in ipairs(fun.notes) do
  685. table.insert(ret, ' • ' .. md_to_vimdoc(p.desc, 0, 8, TEXT_WIDTH, true))
  686. end
  687. end
  688. if fun.attrs then
  689. table.insert(ret, '\n Attributes: ~\n')
  690. for _, attr in ipairs(fun.attrs) do
  691. local attr_str = ({
  692. textlock = 'not allowed when |textlock| is active or in the |cmdwin|',
  693. textlock_allow_cmdwin = 'not allowed when |textlock| is active',
  694. fast = '|api-fast|',
  695. remote_only = '|RPC| only',
  696. lua_only = 'Lua |vim.api| only',
  697. })[attr] or attr
  698. table.insert(ret, fmt(' %s\n', attr_str))
  699. end
  700. end
  701. if fun.params and #fun.params > 0 then
  702. local param_txt =
  703. render_fields_or_params(fun.params, fun.generics, classes, cfg.exclude_types, cfg)
  704. if not param_txt:match('^%s*$') then
  705. table.insert(ret, '\n Parameters: ~\n')
  706. ret[#ret + 1] = param_txt
  707. end
  708. end
  709. if fun.returns then
  710. local txt = render_returns(fun.returns, fun.generics, classes, cfg.exclude_types)
  711. if not txt:match('^%s*$') then
  712. table.insert(ret, '\n')
  713. ret[#ret + 1] = txt
  714. end
  715. end
  716. if fun.see then
  717. table.insert(ret, '\n See also: ~\n')
  718. for _, p in ipairs(fun.see) do
  719. table.insert(ret, ' • ' .. md_to_vimdoc(p.desc, 0, 8, TEXT_WIDTH, true))
  720. end
  721. end
  722. table.insert(ret, '\n')
  723. return table.concat(ret)
  724. end
  725. --- @param funs nvim.luacats.parser.fun[]
  726. --- @param classes table<string,nvim.luacats.parser.class>
  727. --- @param cfg nvim.gen_vimdoc.Config
  728. local function render_funs(funs, classes, cfg)
  729. local ret = {} --- @type string[]
  730. for _, f in ipairs(funs) do
  731. if cfg.fn_xform then
  732. cfg.fn_xform(f)
  733. end
  734. ret[#ret + 1] = render_fun(f, classes, cfg)
  735. end
  736. -- Sort via prototype. Experimental API functions ("nvim__") sort last.
  737. table.sort(ret, function(a, b)
  738. local a1 = ('\n' .. a):match('\n[a-zA-Z_][^\n]+\n')
  739. local b1 = ('\n' .. b):match('\n[a-zA-Z_][^\n]+\n')
  740. local a1__ = a1:find('^%s*nvim__') and 1 or 0
  741. local b1__ = b1:find('^%s*nvim__') and 1 or 0
  742. if a1__ ~= b1__ then
  743. return a1__ < b1__
  744. end
  745. return a1:lower() < b1:lower()
  746. end)
  747. return table.concat(ret)
  748. end
  749. --- @return string
  750. local function get_script_path()
  751. local str = debug.getinfo(2, 'S').source:sub(2)
  752. return str:match('(.*[/\\])') or './'
  753. end
  754. local script_path = get_script_path()
  755. local base_dir = vim.fs.dirname(vim.fs.dirname(script_path))
  756. local function delete_lines_below(doc_file, tokenstr)
  757. local lines = {} --- @type string[]
  758. local found = false
  759. for line in io.lines(doc_file) do
  760. if line:find(vim.pesc(tokenstr)) then
  761. found = true
  762. break
  763. end
  764. lines[#lines + 1] = line
  765. end
  766. if not found then
  767. error(fmt('not found: %s in %s', tokenstr, doc_file))
  768. end
  769. lines[#lines] = nil
  770. local fp = assert(io.open(doc_file, 'w'))
  771. fp:write(table.concat(lines, '\n'))
  772. fp:write('\n')
  773. fp:close()
  774. end
  775. --- @param x string
  776. local function mktitle(x)
  777. if x == 'ui' then
  778. return 'UI'
  779. end
  780. return x:sub(1, 1):upper() .. x:sub(2)
  781. end
  782. --- @class nvim.gen_vimdoc.Section
  783. --- @field name string
  784. --- @field title string
  785. --- @field help_tag string
  786. --- @field funs_txt string
  787. --- @field doc? string[]
  788. --- @param filename string
  789. --- @param cfg nvim.gen_vimdoc.Config
  790. --- @param section_docs table<string,nvim.gen_vimdoc.Section>
  791. --- @param funs_txt string
  792. --- @return nvim.gen_vimdoc.Section?
  793. local function make_section(filename, cfg, section_docs, funs_txt)
  794. -- filename: e.g., 'autocmd.c'
  795. -- name: e.g. 'autocmd'
  796. local name = filename:match('(.*)%.[a-z]+')
  797. -- Formatted (this is what's going to be written in the vimdoc)
  798. -- e.g., "Autocmd Functions"
  799. local sectname = cfg.section_name and cfg.section_name[filename] or mktitle(name)
  800. -- section tag: e.g., "*api-autocmd*"
  801. local help_labels = cfg.helptag_fmt(sectname)
  802. if type(help_labels) == 'table' then
  803. help_labels = table.concat(help_labels, '* *')
  804. end
  805. local help_tags = '*' .. help_labels .. '*'
  806. if funs_txt == '' and #section_docs == 0 then
  807. return
  808. end
  809. return {
  810. name = sectname,
  811. title = cfg.section_fmt(sectname),
  812. help_tag = help_tags,
  813. funs_txt = funs_txt,
  814. doc = section_docs,
  815. }
  816. end
  817. --- @param section nvim.gen_vimdoc.Section
  818. --- @param add_header? boolean
  819. local function render_section(section, add_header)
  820. local doc = {} --- @type string[]
  821. if add_header ~= false then
  822. vim.list_extend(doc, {
  823. string.rep('=', TEXT_WIDTH),
  824. '\n',
  825. section.title,
  826. fmt('%' .. (TEXT_WIDTH - section.title:len()) .. 's', section.help_tag),
  827. })
  828. end
  829. local sdoc = '\n\n' .. table.concat(section.doc or {}, '\n')
  830. if sdoc:find('[^%s]') then
  831. doc[#doc + 1] = sdoc
  832. end
  833. if section.funs_txt then
  834. table.insert(doc, '\n\n')
  835. table.insert(doc, section.funs_txt)
  836. end
  837. return table.concat(doc)
  838. end
  839. local parsers = {
  840. lua = luacats_parser.parse,
  841. c = cdoc_parser.parse,
  842. h = cdoc_parser.parse,
  843. }
  844. --- @param files string[]
  845. local function expand_files(files)
  846. for k, f in pairs(files) do
  847. if vim.fn.isdirectory(f) == 1 then
  848. table.remove(files, k)
  849. for path, ty in vim.fs.dir(f) do
  850. if ty == 'file' then
  851. table.insert(files, vim.fs.joinpath(f, path))
  852. end
  853. end
  854. end
  855. end
  856. end
  857. --- @param cfg nvim.gen_vimdoc.Config
  858. local function gen_target(cfg)
  859. cfg.fn_helptag_fmt = cfg.fn_helptag_fmt or fn_helptag_fmt_common
  860. print('Target:', cfg.filename)
  861. local sections = {} --- @type table<string,nvim.gen_vimdoc.Section>
  862. expand_files(cfg.files)
  863. --- @type table<string,[table<string,nvim.luacats.parser.class>, nvim.luacats.parser.fun[], string[]]>
  864. local file_results = {}
  865. --- @type table<string,nvim.luacats.parser.class>
  866. local all_classes = {}
  867. --- First pass so we can collect all classes
  868. for _, f in vim.spairs(cfg.files) do
  869. local ext = assert(f:match('%.([^.]+)$')) --[[@as 'h'|'c'|'lua']]
  870. local parser = assert(parsers[ext])
  871. local classes, funs, briefs = parser(f)
  872. file_results[f] = { classes, funs, briefs }
  873. all_classes = vim.tbl_extend('error', all_classes, classes)
  874. end
  875. for f, r in vim.spairs(file_results) do
  876. local classes, funs, briefs = r[1], r[2], r[3]
  877. local briefs_txt = {} --- @type string[]
  878. for _, b in ipairs(briefs) do
  879. briefs_txt[#briefs_txt + 1] = md_to_vimdoc(b, 0, 0, TEXT_WIDTH)
  880. end
  881. print(' Processing file:', f)
  882. local funs_txt = render_funs(funs, all_classes, cfg)
  883. if next(classes) then
  884. local classes_txt = render_classes(classes, cfg)
  885. if vim.trim(classes_txt) ~= '' then
  886. funs_txt = classes_txt .. '\n' .. funs_txt
  887. end
  888. end
  889. -- FIXME: Using f_base will confuse `_meta/protocol.lua` with `protocol.lua`
  890. local f_base = vim.fs.basename(f)
  891. sections[f_base] = make_section(f_base, cfg, briefs_txt, funs_txt)
  892. end
  893. local first_section_tag = sections[cfg.section_order[1]].help_tag
  894. local docs = {} --- @type string[]
  895. for _, f in ipairs(cfg.section_order) do
  896. local section = sections[f]
  897. if section then
  898. print(string.format(" Rendering section: '%s'", section.title))
  899. local add_sep_and_header = not vim.tbl_contains(cfg.append_only or {}, f)
  900. docs[#docs + 1] = render_section(section, add_sep_and_header)
  901. end
  902. end
  903. table.insert(
  904. docs,
  905. fmt(' vim:tw=78:ts=8:sw=%d:sts=%d:et:ft=help:norl:\n', INDENTATION, INDENTATION)
  906. )
  907. local doc_file = vim.fs.joinpath(base_dir, 'runtime', 'doc', cfg.filename)
  908. if vim.uv.fs_stat(doc_file) then
  909. delete_lines_below(doc_file, first_section_tag)
  910. end
  911. local fp = assert(io.open(doc_file, 'a'))
  912. fp:write(table.concat(docs, '\n'))
  913. fp:close()
  914. end
  915. local function run()
  916. for _, cfg in vim.spairs(config) do
  917. gen_target(cfg)
  918. end
  919. end
  920. run()