1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042 |
- #!/usr/bin/env -S nvim -l
- --- Generates Nvim :help docs from Lua/C docstrings
- ---
- --- The generated :help text for each function is formatted as follows:
- --- - Max width of 78 columns (`TEXT_WIDTH`).
- --- - Indent with spaces (not tabs).
- --- - Indent of 4 columns for body text (`INDENTATION`).
- --- - Function signature and helptag (right-aligned) on the same line.
- --- - Signature and helptag must have a minimum of 8 spaces between them.
- --- - If the signature is too long, it is placed on the line after the helptag.
- --- Signature wraps with subsequent lines indented to the open parenthesis.
- --- - Subsection bodies are indented an additional 4 spaces.
- --- - Body consists of function description, parameters, return description, and
- --- C declaration (`INCLUDE_C_DECL`).
- --- - Parameters are omitted for the `void` and `Error *` types, or if the
- --- parameter is marked as [out].
- --- - Each function documentation is separated by a single line.
- local luacats_parser = require('scripts.luacats_parser')
- local cdoc_parser = require('scripts.cdoc_parser')
- local util = require('scripts.util')
- local fmt = string.format
- local wrap = util.wrap
- local md_to_vimdoc = util.md_to_vimdoc
- local TEXT_WIDTH = 78
- local INDENTATION = 4
- --- @class (exact) nvim.gen_vimdoc.Config
- ---
- --- Generated documentation target, e.g. api.txt
- --- @field filename string
- ---
- --- @field section_order string[]
- ---
- --- List of files/directories for doxygen to read, relative to `base_dir`.
- --- @field files string[]
- ---
- --- @field exclude_types? true
- ---
- --- Section name overrides. Key: filename (e.g., vim.c)
- --- @field section_name? table<string,string>
- ---
- --- @field fn_name_pat? string
- ---
- --- @field fn_xform? fun(fun: nvim.luacats.parser.fun)
- ---
- --- For generated section names.
- --- @field section_fmt fun(name: string): string
- ---
- --- @field helptag_fmt fun(name: string): string|string[]
- ---
- --- Per-function helptag.
- --- @field fn_helptag_fmt? fun(fun: nvim.luacats.parser.fun): string
- ---
- --- @field append_only? string[]
- local function contains(t, xs)
- return vim.tbl_contains(xs, t)
- end
- --- @type {level:integer, prerelease:boolean}?
- local nvim_api_info_
- --- @return {level: integer, prerelease:boolean}
- local function nvim_api_info()
- if not nvim_api_info_ then
- --- @type integer?, boolean?
- local level, prerelease
- for l in io.lines('CMakeLists.txt') do
- --- @cast l string
- if level and prerelease then
- break
- end
- local m1 = l:match('^set%(NVIM_API_LEVEL%s+(%d+)%)')
- if m1 then
- level = tonumber(m1) --[[@as integer]]
- end
- local m2 = l:match('^set%(NVIM_API_PRERELEASE%s+(%w+)%)')
- if m2 then
- prerelease = m2 == 'true'
- end
- end
- nvim_api_info_ = { level = level, prerelease = prerelease }
- end
- return nvim_api_info_
- end
- --- @param fun nvim.luacats.parser.fun
- --- @return string
- local function fn_helptag_fmt_common(fun)
- local fn_sfx = fun.table and '' or '()'
- if fun.classvar then
- return fmt('%s:%s%s', fun.classvar, fun.name, fn_sfx)
- end
- if fun.module then
- return fmt('%s.%s%s', fun.module, fun.name, fn_sfx)
- end
- return fun.name .. fn_sfx
- end
- --- @type table<string,nvim.gen_vimdoc.Config>
- local config = {
- api = {
- filename = 'api.txt',
- section_order = {
- 'vim.c',
- 'vimscript.c',
- 'command.c',
- 'options.c',
- 'buffer.c',
- 'extmark.c',
- 'window.c',
- 'win_config.c',
- 'tabpage.c',
- 'autocmd.c',
- 'ui.c',
- },
- exclude_types = true,
- fn_name_pat = 'nvim_.*',
- files = { 'src/nvim/api' },
- section_name = {
- ['vim.c'] = 'Global',
- },
- section_fmt = function(name)
- return name .. ' Functions'
- end,
- helptag_fmt = function(name)
- return fmt('api-%s', name:lower())
- end,
- },
- lua = {
- filename = 'lua.txt',
- section_order = {
- 'hl.lua',
- 'diff.lua',
- 'mpack.lua',
- 'json.lua',
- 'base64.lua',
- 'spell.lua',
- 'builtin.lua',
- '_options.lua',
- '_editor.lua',
- '_inspector.lua',
- 'shared.lua',
- 'loader.lua',
- 'uri.lua',
- 'ui.lua',
- 'filetype.lua',
- 'keymap.lua',
- 'fs.lua',
- 'glob.lua',
- 'lpeg.lua',
- 're.lua',
- 'regex.lua',
- 'secure.lua',
- 'version.lua',
- 'iter.lua',
- 'snippet.lua',
- 'text.lua',
- 'tohtml.lua',
- },
- files = {
- 'runtime/lua/vim/iter.lua',
- 'runtime/lua/vim/_editor.lua',
- 'runtime/lua/vim/_options.lua',
- 'runtime/lua/vim/shared.lua',
- 'runtime/lua/vim/loader.lua',
- 'runtime/lua/vim/uri.lua',
- 'runtime/lua/vim/ui.lua',
- 'runtime/lua/vim/filetype.lua',
- 'runtime/lua/vim/keymap.lua',
- 'runtime/lua/vim/fs.lua',
- 'runtime/lua/vim/hl.lua',
- 'runtime/lua/vim/secure.lua',
- 'runtime/lua/vim/version.lua',
- 'runtime/lua/vim/_inspector.lua',
- 'runtime/lua/vim/snippet.lua',
- 'runtime/lua/vim/text.lua',
- 'runtime/lua/vim/glob.lua',
- 'runtime/lua/vim/_meta/builtin.lua',
- 'runtime/lua/vim/_meta/diff.lua',
- 'runtime/lua/vim/_meta/mpack.lua',
- 'runtime/lua/vim/_meta/json.lua',
- 'runtime/lua/vim/_meta/base64.lua',
- 'runtime/lua/vim/_meta/regex.lua',
- 'runtime/lua/vim/_meta/lpeg.lua',
- 'runtime/lua/vim/_meta/re.lua',
- 'runtime/lua/vim/_meta/spell.lua',
- 'runtime/lua/tohtml.lua',
- },
- fn_xform = function(fun)
- if contains(fun.module, { 'vim.uri', 'vim.shared', 'vim._editor' }) then
- fun.module = 'vim'
- end
- if fun.module == 'vim' and contains(fun.name, { 'cmd', 'inspect' }) then
- fun.table = nil
- end
- if fun.classvar or vim.startswith(fun.name, 'vim.') or fun.module == 'vim.iter' then
- return
- end
- fun.name = fmt('%s.%s', fun.module, fun.name)
- end,
- section_name = {
- ['_inspector.lua'] = 'inspector',
- },
- section_fmt = function(name)
- name = name:lower()
- if name == '_editor' then
- return 'Lua module: vim'
- elseif name == '_options' then
- return 'LUA-VIMSCRIPT BRIDGE'
- elseif name == 'builtin' then
- return 'VIM'
- end
- if
- contains(name, {
- 'hl',
- 'mpack',
- 'json',
- 'base64',
- 'diff',
- 'spell',
- 'regex',
- 'lpeg',
- 're',
- })
- then
- return 'VIM.' .. name:upper()
- end
- if name == 'tohtml' then
- return 'Lua module: tohtml'
- end
- return 'Lua module: vim.' .. name
- end,
- helptag_fmt = function(name)
- if name == '_editor' then
- return 'lua-vim'
- elseif name == '_options' then
- return 'lua-vimscript'
- elseif name == 'tohtml' then
- return 'tohtml'
- end
- return 'vim.' .. name:lower()
- end,
- fn_helptag_fmt = function(fun)
- local name = fun.name
- if vim.startswith(name, 'vim.') then
- local fn_sfx = fun.table and '' or '()'
- return name .. fn_sfx
- elseif fun.classvar == 'Option' then
- return fmt('vim.opt:%s()', name)
- end
- return fn_helptag_fmt_common(fun)
- end,
- append_only = {
- 'shared.lua',
- },
- },
- lsp = {
- filename = 'lsp.txt',
- section_order = {
- 'lsp.lua',
- 'client.lua',
- 'buf.lua',
- 'diagnostic.lua',
- 'codelens.lua',
- 'completion.lua',
- 'folding_range.lua',
- 'inlay_hint.lua',
- 'tagfunc.lua',
- 'semantic_tokens.lua',
- 'handlers.lua',
- 'util.lua',
- 'log.lua',
- 'rpc.lua',
- 'protocol.lua',
- },
- files = {
- 'runtime/lua/vim/lsp',
- 'runtime/lua/vim/lsp.lua',
- },
- fn_xform = function(fun)
- fun.name = fun.name:gsub('result%.', '')
- if fun.module == 'vim.lsp.protocol' then
- fun.classvar = nil
- end
- end,
- section_fmt = function(name)
- if name:lower() == 'lsp' then
- return 'Lua module: vim.lsp'
- end
- return 'Lua module: vim.lsp.' .. name:lower()
- end,
- helptag_fmt = function(name)
- if name:lower() == 'lsp' then
- return 'lsp-core'
- end
- return fmt('lsp-%s', name:lower())
- end,
- },
- diagnostic = {
- filename = 'diagnostic.txt',
- section_order = {
- 'diagnostic.lua',
- },
- files = { 'runtime/lua/vim/diagnostic.lua' },
- section_fmt = function()
- return 'Lua module: vim.diagnostic'
- end,
- helptag_fmt = function()
- return 'diagnostic-api'
- end,
- },
- treesitter = {
- filename = 'treesitter.txt',
- section_order = {
- 'tstree.lua',
- 'tsnode.lua',
- 'treesitter.lua',
- 'language.lua',
- 'query.lua',
- 'highlighter.lua',
- 'languagetree.lua',
- 'dev.lua',
- },
- files = {
- 'runtime/lua/vim/treesitter/_meta/',
- 'runtime/lua/vim/treesitter.lua',
- 'runtime/lua/vim/treesitter/',
- },
- section_fmt = function(name)
- if name:lower() == 'treesitter' then
- return 'Lua module: vim.treesitter'
- elseif name:lower() == 'tstree' then
- return 'TREESITTER TREES'
- elseif name:lower() == 'tsnode' then
- return 'TREESITTER NODES'
- end
- return 'Lua module: vim.treesitter.' .. name:lower()
- end,
- helptag_fmt = function(name)
- if name:lower() == 'treesitter' then
- return 'lua-treesitter-core'
- elseif name:lower() == 'query' then
- return 'lua-treesitter-query'
- elseif name:lower() == 'tstree' then
- return { 'treesitter-tree', 'TSTree' }
- elseif name:lower() == 'tsnode' then
- return { 'treesitter-node', 'TSNode' }
- end
- return 'treesitter-' .. name:lower()
- end,
- },
- editorconfig = {
- filename = 'editorconfig.txt',
- files = {
- 'runtime/lua/editorconfig.lua',
- },
- section_order = {
- 'editorconfig.lua',
- },
- section_fmt = function(_name)
- return 'EditorConfig integration'
- end,
- helptag_fmt = function(name)
- return name:lower()
- end,
- fn_xform = function(fun)
- fun.table = true
- fun.name = vim.split(fun.name, '.', { plain = true })[2]
- end,
- },
- health = {
- filename = 'health.txt',
- files = {
- 'runtime/lua/vim/health.lua',
- },
- section_order = {
- 'health.lua',
- },
- section_fmt = function(_name)
- return 'Checkhealth'
- end,
- helptag_fmt = function()
- return { 'vim.health', 'health' }
- end,
- },
- }
- --- @param ty string
- --- @param generics table<string,string>
- --- @return string
- local function replace_generics(ty, generics)
- if ty:sub(-2) == '[]' then
- local ty0 = ty:sub(1, -3)
- if generics[ty0] then
- return generics[ty0] .. '[]'
- end
- elseif ty:sub(-1) == '?' then
- local ty0 = ty:sub(1, -2)
- if generics[ty0] then
- return generics[ty0] .. '?'
- end
- end
- return generics[ty] or ty
- end
- --- @param name string
- local function fmt_field_name(name)
- local name0, opt = name:match('^([^?]*)(%??)$')
- return fmt('{%s}%s', name0, opt)
- end
- --- @param ty string
- --- @param generics? table<string,string>
- --- @param default? string
- local function render_type(ty, generics, default)
- if generics then
- ty = replace_generics(ty, generics)
- end
- ty = ty:gsub('%s*|%s*nil', '?')
- ty = ty:gsub('nil%s*|%s*(.*)', '%1?')
- ty = ty:gsub('%s*|%s*', '|')
- if default then
- return fmt('(`%s`, default: %s)', ty, default)
- end
- return fmt('(`%s`)', ty)
- end
- --- @param p nvim.luacats.parser.param|nvim.luacats.parser.field
- local function should_render_field_or_param(p)
- return not p.nodoc
- and not p.access
- and not contains(p.name, { '_', 'self' })
- and not vim.startswith(p.name, '_')
- end
- --- @param desc? string
- --- @return string?, string?
- local function get_default(desc)
- if not desc then
- return
- end
- local default = desc:match('\n%s*%([dD]efault: ([^)]+)%)')
- if default then
- desc = desc:gsub('\n%s*%([dD]efault: [^)]+%)', '')
- end
- return desc, default
- end
- --- @param ty string
- --- @param classes? table<string,nvim.luacats.parser.class>
- --- @return nvim.luacats.parser.class?
- local function get_class(ty, classes)
- if not classes then
- return
- end
- local cty = ty:gsub('%s*|%s*nil', '?'):gsub('?$', ''):gsub('%[%]$', '')
- return classes[cty]
- end
- --- @param obj nvim.luacats.parser.param|nvim.luacats.parser.return|nvim.luacats.parser.field
- --- @param classes? table<string,nvim.luacats.parser.class>
- local function inline_type(obj, classes)
- local ty = obj.type
- if not ty then
- return
- end
- local cls = get_class(ty, classes)
- if not cls or cls.nodoc then
- return
- end
- if not cls.inlinedoc then
- -- Not inlining so just add a: "See |tag|."
- local tag = fmt('|%s|', cls.name)
- if obj.desc and obj.desc:find(tag) then
- -- Tag already there
- return
- end
- -- TODO(lewis6991): Aim to remove this. Need this to prevent dead
- -- references to types defined in runtime/lua/vim/lsp/_meta/protocol.lua
- if not vim.startswith(cls.name, 'vim.') then
- return
- end
- obj.desc = obj.desc or ''
- local period = (obj.desc == '' or vim.endswith(obj.desc, '.')) and '' or '.'
- obj.desc = obj.desc .. fmt('%s See %s.', period, tag)
- return
- end
- local ty_isopt = (ty:match('%?$') or ty:match('%s*|%s*nil')) ~= nil
- local ty_islist = (ty:match('%[%]$')) ~= nil
- ty = ty_isopt and 'table?' or ty_islist and 'table[]' or 'table'
- local desc = obj.desc or ''
- if cls.desc then
- desc = desc .. cls.desc
- elseif desc == '' then
- if ty_islist then
- desc = desc .. 'A list of objects with the following fields:'
- elseif cls.parent then
- desc = desc .. fmt('Extends |%s| with the additional fields:', cls.parent)
- else
- desc = desc .. 'A table with the following fields:'
- end
- end
- local desc_append = {}
- for _, f in ipairs(cls.fields) do
- if not f.access then
- local fdesc, default = get_default(f.desc)
- local fty = render_type(f.type, nil, default)
- local fnm = fmt_field_name(f.name)
- table.insert(desc_append, table.concat({ '-', fnm, fty, fdesc }, ' '))
- end
- end
- desc = desc .. '\n' .. table.concat(desc_append, '\n')
- obj.type = ty
- obj.desc = desc
- end
- --- @param xs (nvim.luacats.parser.param|nvim.luacats.parser.field)[]
- --- @param generics? table<string,string>
- --- @param classes? table<string,nvim.luacats.parser.class>
- --- @param exclude_types? true
- --- @param cfg nvim.gen_vimdoc.Config
- local function render_fields_or_params(xs, generics, classes, exclude_types, cfg)
- local ret = {} --- @type string[]
- xs = vim.tbl_filter(should_render_field_or_param, xs)
- local indent = 0
- for _, p in ipairs(xs) do
- if p.type or p.desc then
- indent = math.max(indent, #p.name + 3)
- end
- if exclude_types then
- p.type = nil
- end
- end
- for _, p in ipairs(xs) do
- local pdesc, default = get_default(p.desc)
- p.desc = pdesc
- inline_type(p, classes)
- local nm, ty = p.name, p.type
- local desc = p.classvar and string.format('See |%s|.', cfg.fn_helptag_fmt(p)) or p.desc
- local fnm = p.kind == 'operator' and fmt('op(%s)', nm) or fmt_field_name(nm)
- local pnm = fmt(' • %-' .. indent .. 's', fnm)
- if ty then
- local pty = render_type(ty, generics, default)
- if desc then
- table.insert(ret, pnm)
- if #pty > TEXT_WIDTH - indent then
- vim.list_extend(ret, { ' ', pty, '\n' })
- table.insert(ret, md_to_vimdoc(desc, 9 + indent, 9 + indent, TEXT_WIDTH, true))
- else
- desc = fmt('%s %s', pty, desc)
- table.insert(ret, md_to_vimdoc(desc, 1, 9 + indent, TEXT_WIDTH, true))
- end
- else
- table.insert(ret, fmt('%s %s\n', pnm, pty))
- end
- else
- if desc then
- table.insert(ret, pnm)
- table.insert(ret, md_to_vimdoc(desc, 1, 9 + indent, TEXT_WIDTH, true))
- end
- end
- end
- return table.concat(ret)
- end
- --- @param class nvim.luacats.parser.class
- --- @param classes table<string,nvim.luacats.parser.class>
- --- @param cfg nvim.gen_vimdoc.Config
- local function render_class(class, classes, cfg)
- if class.access or class.nodoc or class.inlinedoc then
- return
- end
- local ret = {} --- @type string[]
- table.insert(ret, fmt('*%s*\n', class.name))
- if class.parent then
- local txt = fmt('Extends: |%s|', class.parent)
- table.insert(ret, md_to_vimdoc(txt, INDENTATION, INDENTATION, TEXT_WIDTH))
- table.insert(ret, '\n')
- end
- if class.desc then
- table.insert(ret, md_to_vimdoc(class.desc, INDENTATION, INDENTATION, TEXT_WIDTH))
- end
- local fields_txt = render_fields_or_params(class.fields, nil, classes, nil, cfg)
- if not fields_txt:match('^%s*$') then
- table.insert(ret, '\n Fields: ~\n')
- table.insert(ret, fields_txt)
- end
- table.insert(ret, '\n')
- return table.concat(ret)
- end
- --- @param classes table<string,nvim.luacats.parser.class>
- --- @param cfg nvim.gen_vimdoc.Config
- local function render_classes(classes, cfg)
- local ret = {} --- @type string[]
- for _, class in vim.spairs(classes) do
- ret[#ret + 1] = render_class(class, classes, cfg)
- end
- return table.concat(ret)
- end
- --- @param fun nvim.luacats.parser.fun
- --- @param cfg nvim.gen_vimdoc.Config
- local function render_fun_header(fun, cfg)
- local ret = {} --- @type string[]
- local args = {} --- @type string[]
- for _, p in ipairs(fun.params or {}) do
- if p.name ~= 'self' then
- args[#args + 1] = fmt_field_name(p.name)
- end
- end
- local nm = fun.name
- if fun.classvar then
- nm = fmt('%s:%s', fun.classvar, nm)
- end
- if nm == 'vim.bo' then
- nm = 'vim.bo[{bufnr}]'
- end
- if nm == 'vim.wo' then
- nm = 'vim.wo[{winid}][{bufnr}]'
- end
- local proto = fun.table and nm or nm .. '(' .. table.concat(args, ', ') .. ')'
- local tag = '*' .. cfg.fn_helptag_fmt(fun) .. '*'
- if #proto + #tag > TEXT_WIDTH - 8 then
- table.insert(ret, fmt('%78s\n', tag))
- local name, pargs = proto:match('([^(]+%()(.*)')
- table.insert(ret, name)
- table.insert(ret, wrap(pargs, 0, #name, TEXT_WIDTH))
- else
- local pad = TEXT_WIDTH - #proto - #tag
- table.insert(ret, proto .. string.rep(' ', pad) .. tag)
- end
- return table.concat(ret)
- end
- --- @param returns nvim.luacats.parser.return[]
- --- @param generics? table<string,string>
- --- @param classes? table<string,nvim.luacats.parser.class>
- --- @param exclude_types boolean
- local function render_returns(returns, generics, classes, exclude_types)
- local ret = {} --- @type string[]
- returns = vim.deepcopy(returns)
- if exclude_types then
- for _, r in ipairs(returns) do
- r.type = nil
- end
- end
- if #returns > 1 then
- table.insert(ret, ' Return (multiple): ~\n')
- elseif #returns == 1 and next(returns[1]) then
- table.insert(ret, ' Return: ~\n')
- end
- for _, p in ipairs(returns) do
- inline_type(p, classes)
- local rnm, ty, desc = p.name, p.type, p.desc
- local blk = {} --- @type string[]
- if ty then
- blk[#blk + 1] = render_type(ty, generics)
- end
- blk[#blk + 1] = rnm
- blk[#blk + 1] = desc
- table.insert(ret, md_to_vimdoc(table.concat(blk, ' '), 8, 8, TEXT_WIDTH, true))
- end
- return table.concat(ret)
- end
- --- @param fun nvim.luacats.parser.fun
- --- @param classes table<string,nvim.luacats.parser.class>
- --- @param cfg nvim.gen_vimdoc.Config
- local function render_fun(fun, classes, cfg)
- if fun.access or fun.deprecated or fun.nodoc then
- return
- end
- if cfg.fn_name_pat and not fun.name:match(cfg.fn_name_pat) then
- return
- end
- if vim.startswith(fun.name, '_') or fun.name:find('[:.]_') then
- return
- end
- local ret = {} --- @type string[]
- table.insert(ret, render_fun_header(fun, cfg))
- table.insert(ret, '\n')
- if fun.since then
- local since = assert(tonumber(fun.since), 'invalid @since on ' .. fun.name)
- local info = nvim_api_info()
- if since == 0 or (info.prerelease and since == info.level) then
- -- Experimental = (since==0 or current prerelease)
- local s = 'WARNING: This feature is experimental/unstable.'
- table.insert(ret, md_to_vimdoc(s, INDENTATION, INDENTATION, TEXT_WIDTH))
- table.insert(ret, '\n')
- else
- local v = assert(util.version_level[since], 'invalid @since on ' .. fun.name)
- fun.attrs = fun.attrs or {}
- table.insert(fun.attrs, ('Since: %s'):format(v))
- end
- end
- if fun.desc then
- table.insert(ret, md_to_vimdoc(fun.desc, INDENTATION, INDENTATION, TEXT_WIDTH))
- end
- if fun.notes then
- table.insert(ret, '\n Note: ~\n')
- for _, p in ipairs(fun.notes) do
- table.insert(ret, ' • ' .. md_to_vimdoc(p.desc, 0, 8, TEXT_WIDTH, true))
- end
- end
- if fun.attrs then
- table.insert(ret, '\n Attributes: ~\n')
- for _, attr in ipairs(fun.attrs) do
- local attr_str = ({
- textlock = 'not allowed when |textlock| is active or in the |cmdwin|',
- textlock_allow_cmdwin = 'not allowed when |textlock| is active',
- fast = '|api-fast|',
- remote_only = '|RPC| only',
- lua_only = 'Lua |vim.api| only',
- })[attr] or attr
- table.insert(ret, fmt(' %s\n', attr_str))
- end
- end
- if fun.params and #fun.params > 0 then
- local param_txt =
- render_fields_or_params(fun.params, fun.generics, classes, cfg.exclude_types, cfg)
- if not param_txt:match('^%s*$') then
- table.insert(ret, '\n Parameters: ~\n')
- ret[#ret + 1] = param_txt
- end
- end
- if fun.returns then
- local txt = render_returns(fun.returns, fun.generics, classes, cfg.exclude_types)
- if not txt:match('^%s*$') then
- table.insert(ret, '\n')
- ret[#ret + 1] = txt
- end
- end
- if fun.see then
- table.insert(ret, '\n See also: ~\n')
- for _, p in ipairs(fun.see) do
- table.insert(ret, ' • ' .. md_to_vimdoc(p.desc, 0, 8, TEXT_WIDTH, true))
- end
- end
- table.insert(ret, '\n')
- return table.concat(ret)
- end
- --- @param funs nvim.luacats.parser.fun[]
- --- @param classes table<string,nvim.luacats.parser.class>
- --- @param cfg nvim.gen_vimdoc.Config
- local function render_funs(funs, classes, cfg)
- local ret = {} --- @type string[]
- for _, f in ipairs(funs) do
- if cfg.fn_xform then
- cfg.fn_xform(f)
- end
- ret[#ret + 1] = render_fun(f, classes, cfg)
- end
- -- Sort via prototype. Experimental API functions ("nvim__") sort last.
- table.sort(ret, function(a, b)
- local a1 = ('\n' .. a):match('\n[a-zA-Z_][^\n]+\n')
- local b1 = ('\n' .. b):match('\n[a-zA-Z_][^\n]+\n')
- local a1__ = a1:find('^%s*nvim__') and 1 or 0
- local b1__ = b1:find('^%s*nvim__') and 1 or 0
- if a1__ ~= b1__ then
- return a1__ < b1__
- end
- return a1:lower() < b1:lower()
- end)
- return table.concat(ret)
- end
- --- @return string
- local function get_script_path()
- local str = debug.getinfo(2, 'S').source:sub(2)
- return str:match('(.*[/\\])') or './'
- end
- local script_path = get_script_path()
- local base_dir = vim.fs.dirname(vim.fs.dirname(script_path))
- local function delete_lines_below(doc_file, tokenstr)
- local lines = {} --- @type string[]
- local found = false
- for line in io.lines(doc_file) do
- if line:find(vim.pesc(tokenstr)) then
- found = true
- break
- end
- lines[#lines + 1] = line
- end
- if not found then
- error(fmt('not found: %s in %s', tokenstr, doc_file))
- end
- lines[#lines] = nil
- local fp = assert(io.open(doc_file, 'w'))
- fp:write(table.concat(lines, '\n'))
- fp:write('\n')
- fp:close()
- end
- --- @param x string
- local function mktitle(x)
- if x == 'ui' then
- return 'UI'
- end
- return x:sub(1, 1):upper() .. x:sub(2)
- end
- --- @class nvim.gen_vimdoc.Section
- --- @field name string
- --- @field title string
- --- @field help_tag string
- --- @field funs_txt string
- --- @field doc? string[]
- --- @param filename string
- --- @param cfg nvim.gen_vimdoc.Config
- --- @param section_docs table<string,nvim.gen_vimdoc.Section>
- --- @param funs_txt string
- --- @return nvim.gen_vimdoc.Section?
- local function make_section(filename, cfg, section_docs, funs_txt)
- -- filename: e.g., 'autocmd.c'
- -- name: e.g. 'autocmd'
- local name = filename:match('(.*)%.[a-z]+')
- -- Formatted (this is what's going to be written in the vimdoc)
- -- e.g., "Autocmd Functions"
- local sectname = cfg.section_name and cfg.section_name[filename] or mktitle(name)
- -- section tag: e.g., "*api-autocmd*"
- local help_labels = cfg.helptag_fmt(sectname)
- if type(help_labels) == 'table' then
- help_labels = table.concat(help_labels, '* *')
- end
- local help_tags = '*' .. help_labels .. '*'
- if funs_txt == '' and #section_docs == 0 then
- return
- end
- return {
- name = sectname,
- title = cfg.section_fmt(sectname),
- help_tag = help_tags,
- funs_txt = funs_txt,
- doc = section_docs,
- }
- end
- --- @param section nvim.gen_vimdoc.Section
- --- @param add_header? boolean
- local function render_section(section, add_header)
- local doc = {} --- @type string[]
- if add_header ~= false then
- vim.list_extend(doc, {
- string.rep('=', TEXT_WIDTH),
- '\n',
- section.title,
- fmt('%' .. (TEXT_WIDTH - section.title:len()) .. 's', section.help_tag),
- })
- end
- local sdoc = '\n\n' .. table.concat(section.doc or {}, '\n')
- if sdoc:find('[^%s]') then
- doc[#doc + 1] = sdoc
- end
- if section.funs_txt then
- table.insert(doc, '\n\n')
- table.insert(doc, section.funs_txt)
- end
- return table.concat(doc)
- end
- local parsers = {
- lua = luacats_parser.parse,
- c = cdoc_parser.parse,
- h = cdoc_parser.parse,
- }
- --- @param files string[]
- local function expand_files(files)
- for k, f in pairs(files) do
- if vim.fn.isdirectory(f) == 1 then
- table.remove(files, k)
- for path, ty in vim.fs.dir(f) do
- if ty == 'file' then
- table.insert(files, vim.fs.joinpath(f, path))
- end
- end
- end
- end
- end
- --- @param cfg nvim.gen_vimdoc.Config
- local function gen_target(cfg)
- cfg.fn_helptag_fmt = cfg.fn_helptag_fmt or fn_helptag_fmt_common
- print('Target:', cfg.filename)
- local sections = {} --- @type table<string,nvim.gen_vimdoc.Section>
- expand_files(cfg.files)
- --- @type table<string,[table<string,nvim.luacats.parser.class>, nvim.luacats.parser.fun[], string[]]>
- local file_results = {}
- --- @type table<string,nvim.luacats.parser.class>
- local all_classes = {}
- --- First pass so we can collect all classes
- for _, f in vim.spairs(cfg.files) do
- local ext = assert(f:match('%.([^.]+)$')) --[[@as 'h'|'c'|'lua']]
- local parser = assert(parsers[ext])
- local classes, funs, briefs = parser(f)
- file_results[f] = { classes, funs, briefs }
- all_classes = vim.tbl_extend('error', all_classes, classes)
- end
- for f, r in vim.spairs(file_results) do
- local classes, funs, briefs = r[1], r[2], r[3]
- local briefs_txt = {} --- @type string[]
- for _, b in ipairs(briefs) do
- briefs_txt[#briefs_txt + 1] = md_to_vimdoc(b, 0, 0, TEXT_WIDTH)
- end
- print(' Processing file:', f)
- local funs_txt = render_funs(funs, all_classes, cfg)
- if next(classes) then
- local classes_txt = render_classes(classes, cfg)
- if vim.trim(classes_txt) ~= '' then
- funs_txt = classes_txt .. '\n' .. funs_txt
- end
- end
- -- FIXME: Using f_base will confuse `_meta/protocol.lua` with `protocol.lua`
- local f_base = vim.fs.basename(f)
- sections[f_base] = make_section(f_base, cfg, briefs_txt, funs_txt)
- end
- local first_section_tag = sections[cfg.section_order[1]].help_tag
- local docs = {} --- @type string[]
- for _, f in ipairs(cfg.section_order) do
- local section = sections[f]
- if section then
- print(string.format(" Rendering section: '%s'", section.title))
- local add_sep_and_header = not vim.tbl_contains(cfg.append_only or {}, f)
- docs[#docs + 1] = render_section(section, add_sep_and_header)
- end
- end
- table.insert(
- docs,
- fmt(' vim:tw=78:ts=8:sw=%d:sts=%d:et:ft=help:norl:\n', INDENTATION, INDENTATION)
- )
- local doc_file = vim.fs.joinpath(base_dir, 'runtime', 'doc', cfg.filename)
- if vim.uv.fs_stat(doc_file) then
- delete_lines_below(doc_file, first_section_tag)
- end
- local fp = assert(io.open(doc_file, 'a'))
- fp:write(table.concat(docs, '\n'))
- fp:close()
- end
- local function run()
- for _, cfg in vim.spairs(config) do
- gen_target(cfg)
- end
- end
- run()
|