diff options
author | Lewis Russell <lewis6991@gmail.com> | 2025-02-26 11:38:07 +0000 |
---|---|---|
committer | Lewis Russell <me@lewisr.dev> | 2025-02-26 16:54:37 +0000 |
commit | 0f24b0826a27b7868a3aacc25199787e7453d4cc (patch) | |
tree | 49585aac252581a735577f2e5711201a85ab8a7e /src/gen/gen_eval_files.lua | |
parent | 85caaa70d44b7b18c633aa0b140de5f3f6d3eee7 (diff) | |
download | rneovim-0f24b0826a27b7868a3aacc25199787e7453d4cc.tar.gz rneovim-0f24b0826a27b7868a3aacc25199787e7453d4cc.tar.bz2 rneovim-0f24b0826a27b7868a3aacc25199787e7453d4cc.zip |
build: move all generator scripts to `src/gen/`
- Move all generator Lua scripts to the `src/gen/`
- Add a `.luarc.json` to `src/gen/`
- Add a `preload.lua` to `src/gen/`
- Add `src` to `package.path` so it aligns with `.luarc.json'
- Fix all `require` statements in `src/gen/` so they are consistent:
- `require('scripts.foo')` -> `require('gen.foo')`
- `require('src.nvim.options')` -> `require('nvim.options')`
- `require('api.dispatch_deprecated')` -> `require('nvim.api.dispatch_deprecated')`
Diffstat (limited to 'src/gen/gen_eval_files.lua')
-rwxr-xr-x | src/gen/gen_eval_files.lua | 1090 |
1 files changed, 1090 insertions, 0 deletions
diff --git a/src/gen/gen_eval_files.lua b/src/gen/gen_eval_files.lua new file mode 100755 index 0000000000..74e45507e5 --- /dev/null +++ b/src/gen/gen_eval_files.lua @@ -0,0 +1,1090 @@ +#!/usr/bin/env -S nvim -l + +-- Generator for various vimdoc and Lua type files + +local util = require('gen.util') +local fmt = string.format + +local DEP_API_METADATA = 'build/funcs_metadata.mpack' +local TEXT_WIDTH = 78 + +--- @class vim.api.metadata +--- @field name string +--- @field parameters [string,string][] +--- @field return_type string +--- @field deprecated_since integer +--- @field eval boolean +--- @field fast boolean +--- @field handler_id integer +--- @field impl_name string +--- @field lua boolean +--- @field method boolean +--- @field remote boolean +--- @field since integer + +local LUA_API_RETURN_OVERRIDES = { + nvim_buf_get_command = 'table<string,vim.api.keyset.command_info>', + nvim_buf_get_extmark_by_id = 'vim.api.keyset.get_extmark_item_by_id', + nvim_buf_get_extmarks = 'vim.api.keyset.get_extmark_item[]', + nvim_buf_get_keymap = 'vim.api.keyset.get_keymap[]', + nvim_get_autocmds = 'vim.api.keyset.get_autocmds.ret[]', + nvim_get_color_map = 'table<string,integer>', + nvim_get_command = 'table<string,vim.api.keyset.command_info>', + nvim_get_keymap = 'vim.api.keyset.get_keymap[]', + nvim_get_mark = 'vim.api.keyset.get_mark', + + -- Can also return table<string,vim.api.keyset.get_hl_info>, however we need to + -- pick one to get some benefit. + -- REVISIT lewrus01 (26/01/24): we can maybe add + -- @overload fun(ns: integer, {}): table<string,vim.api.keyset.get_hl_info> + nvim_get_hl = 'vim.api.keyset.get_hl_info', + + nvim_get_mode = 'vim.api.keyset.get_mode', + nvim_get_namespaces = 'table<string,integer>', + nvim_get_option_info = 'vim.api.keyset.get_option_info', + nvim_get_option_info2 = 'vim.api.keyset.get_option_info', + nvim_parse_cmd = 'vim.api.keyset.parse_cmd', + nvim_win_get_config = 'vim.api.keyset.win_config', +} + +local LUA_API_KEYSET_OVERRIDES = { + create_autocmd = { + callback = 'string|(fun(args: vim.api.keyset.create_autocmd.callback_args): boolean?)', + }, +} + +local LUA_API_PARAM_OVERRIDES = { + nvim_create_user_command = { + command = 'string|fun(args: vim.api.keyset.create_user_command.command_args)', + }, +} + +local LUA_META_HEADER = { + '--- @meta _', + '-- THIS FILE IS GENERATED', + '-- DO NOT EDIT', + "error('Cannot require a meta file')", +} + +local LUA_API_META_HEADER = { + '--- @meta _', + '-- THIS FILE IS GENERATED', + '-- DO NOT EDIT', + "error('Cannot require a meta file')", + '', + '--- This file embeds vimdoc as the function descriptions', + '--- so ignore any doc related errors.', + '--- @diagnostic disable: undefined-doc-name,luadoc-miss-symbol', + '', + 'vim.api = {}', +} + +local LUA_OPTION_META_HEADER = { + '--- @meta _', + '-- THIS FILE IS GENERATED', + '-- DO NOT EDIT', + "error('Cannot require a meta file')", + '', + '---@class vim.bo', + '---@field [integer] vim.bo', + 'vim.bo = vim.bo', + '', + '---@class vim.wo', + '---@field [integer] vim.wo', + 'vim.wo = vim.wo', +} + +local LUA_VVAR_META_HEADER = { + '--- @meta _', + '-- THIS FILE IS GENERATED', + '-- DO NOT EDIT', + "error('Cannot require a meta file')", + '', + '--- @class vim.v', + 'vim.v = ...', +} + +local LUA_KEYWORDS = { + ['and'] = true, + ['end'] = true, + ['function'] = true, + ['or'] = true, + ['if'] = true, + ['while'] = true, + ['repeat'] = true, + ['true'] = true, + ['false'] = true, +} + +local OPTION_TYPES = { + boolean = 'boolean', + number = 'integer', + string = 'string', +} + +local API_TYPES = { + Window = 'integer', + Tabpage = 'integer', + Buffer = 'integer', + Boolean = 'boolean', + Object = 'any', + Integer = 'integer', + String = 'string', + Array = 'any[]', + LuaRef = 'function', + Dict = 'table<string,any>', + Float = 'number', + HLGroupID = 'integer|string', + void = '', +} + +--- @param s string +--- @return string +local function luaescape(s) + if LUA_KEYWORDS[s] then + return s .. '_' + end + return s +end + +--- @param x string +--- @param sep? string +--- @return string[] +local function split(x, sep) + return vim.split(x, sep or '\n', { plain = true }) +end + +--- Convert an API type to Lua +--- @param t string +--- @return string +local function api_type(t) + if vim.startswith(t, '*') then + return api_type(t:sub(2)) .. '?' + end + + local as0 = t:match('^ArrayOf%((.*)%)') + if as0 then + local as = split(as0, ', ') + return api_type(as[1]) .. '[]' + end + + local d = t:match('^Dict%((.*)%)') + if d then + return 'vim.api.keyset.' .. d + end + + local d0 = t:match('^DictOf%((.*)%)') + if d0 then + return 'table<string,' .. api_type(d0) .. '>' + end + + local u = t:match('^Union%((.*)%)') + if u then + local us = vim.split(u, ',%s*') + return table.concat(vim.tbl_map(api_type, us), '|') + end + + local l = t:match('^LuaRefOf%((.*)%)') + if l then + --- @type string + l = l:gsub('%s+', ' ') + --- @type string?, string? + local as, r = l:match('%((.*)%),%s*(.*)') + if not as then + --- @type string + as = assert(l:match('%((.*)%)')) + end + + local as1 = {} --- @type string[] + for a in vim.gsplit(as, ',%s') do + local a1 = vim.split(a, '%s+', { trimempty = true }) + local nm = a1[2]:gsub('%*(.*)$', '%1?') + as1[#as1 + 1] = nm .. ': ' .. api_type(a1[1]) + end + + return ('fun(%s)%s'):format(table.concat(as1, ', '), r and ': ' .. api_type(r) or '') + end + + return API_TYPES[t] or t +end + +--- @param f string +--- @param params [string,string][]|true +--- @return string +local function render_fun_sig(f, params) + local param_str --- @type string + if params == true then + param_str = '...' + else + param_str = table.concat( + vim.tbl_map( + --- @param v [string,string] + --- @return string + function(v) + return luaescape(v[1]) + end, + params + ), + ', ' + ) + end + + if LUA_KEYWORDS[f] then + return fmt("vim.fn['%s'] = function(%s) end", f, param_str) + else + return fmt('function vim.fn.%s(%s) end', f, param_str) + end +end + +--- Uniquify names +--- @param params [string,string,string][] +--- @return [string,string,string][] +local function process_params(params) + local seen = {} --- @type table<string,true> + local sfx = 1 + + for _, p in ipairs(params) do + if seen[p[1]] then + p[1] = p[1] .. sfx + sfx = sfx + 1 + else + seen[p[1]] = true + end + end + + return params +end + +--- @return table<string, vim.EvalFn> +local function get_api_meta() + local ret = {} --- @type table<string, vim.EvalFn> + + local cdoc_parser = require('gen.cdoc_parser') + + local f = 'src/nvim/api' + + local function include(fun) + if not vim.startswith(fun.name, 'nvim_') then + return false + end + if vim.tbl_contains(fun.attrs or {}, 'lua_only') then + return true + end + if vim.tbl_contains(fun.attrs or {}, 'remote_only') then + return false + end + return true + end + + --- @type table<string,nvim.cdoc.parser.fun> + local functions = {} + for path, ty in vim.fs.dir(f) do + if ty == 'file' then + local filename = vim.fs.joinpath(f, path) + local _, funs = cdoc_parser.parse(filename) + for _, fn in ipairs(funs) do + if include(fn) then + functions[fn.name] = fn + end + end + end + end + + for _, fun in pairs(functions) do + local deprecated = fun.deprecated_since ~= nil + + local notes = {} --- @type string[] + for _, note in ipairs(fun.notes or {}) do + notes[#notes + 1] = note.desc + end + + local sees = {} --- @type string[] + for _, see in ipairs(fun.see or {}) do + sees[#sees + 1] = see.desc + end + + local pty_overrides = LUA_API_PARAM_OVERRIDES[fun.name] or {} + + local params = {} --- @type [string,string][] + for _, p in ipairs(fun.params) do + params[#params + 1] = { + p.name, + api_type(pty_overrides[p.name] or p.type), + not deprecated and p.desc or nil, + } + end + + local r = { + signature = 'NA', + name = fun.name, + params = params, + notes = notes, + see = sees, + returns = api_type(fun.returns[1].type), + deprecated = deprecated, + } + + if not deprecated then + r.desc = fun.desc + r.returns_desc = fun.returns[1].desc + end + + ret[fun.name] = r + end + return ret +end + +--- Convert vimdoc references to markdown literals +--- Convert vimdoc codeblocks to markdown codeblocks +--- +--- Ensure code blocks have one empty line before the start fence and after the closing fence. +--- +--- @param x string +--- @param special string? +--- | 'see-api-meta' Normalize `@see` for API meta docstrings. +--- @return string +local function norm_text(x, special) + if special == 'see-api-meta' then + -- Try to guess a symbol that actually works in @see. + -- "nvim_xx()" => "vim.api.nvim_xx" + x = x:gsub([=[%|?(nvim_[^.()| ]+)%(?%)?%|?]=], 'vim.api.%1') + -- TODO: Remove backticks when LuaLS resolves: https://github.com/LuaLS/lua-language-server/issues/2889 + -- "|foo|" => "`:help foo`" + x = x:gsub([=[|([^ ]+)|]=], '`:help %1`') + end + + return ( + x:gsub('|([^ ]+)|', '`%1`') + :gsub('\n*>lua', '\n\n```lua') + :gsub('\n*>vim', '\n\n```vim') + :gsub('\n+<$', '\n```') + :gsub('\n+<\n+', '\n```\n\n') + :gsub('%s+>\n+', '\n```\n') + :gsub('\n+<%s+\n?', '\n```\n') + ) +end + +--- Generates LuaLS docstring for an API function. +--- @param _f string +--- @param fun vim.EvalFn +--- @param write fun(line: string) +local function render_api_meta(_f, fun, write) + write('') + + if vim.startswith(fun.name, 'nvim__') then + write('--- @private') + end + + if fun.deprecated then + write('--- @deprecated') + end + + local desc = fun.desc + if desc then + write(util.prefix_lines('--- ', norm_text(desc))) + end + + -- LuaLS doesn't support @note. Render @note items as a markdown list. + if fun.notes and #fun.notes > 0 then + write('--- Note:') + write(util.prefix_lines('--- ', table.concat(fun.notes, '\n'))) + write('---') + end + + for _, see in ipairs(fun.see or {}) do + write(util.prefix_lines('--- @see ', norm_text(see, 'see-api-meta'))) + end + + local param_names = {} --- @type string[] + local params = process_params(fun.params) + for _, p in ipairs(params) do + local pname, ptype, pdesc = luaescape(p[1]), p[2], p[3] + param_names[#param_names + 1] = pname + if pdesc then + local s = '--- @param ' .. pname .. ' ' .. ptype .. ' ' + local pdesc_a = split(vim.trim(norm_text(pdesc))) + write(s .. pdesc_a[1]) + for i = 2, #pdesc_a do + if not pdesc_a[i] then + break + end + write('--- ' .. pdesc_a[i]) + end + else + write('--- @param ' .. pname .. ' ' .. ptype) + end + end + + if fun.returns ~= '' then + local ret_desc = fun.returns_desc and ' # ' .. fun.returns_desc or '' + local ret = LUA_API_RETURN_OVERRIDES[fun.name] or fun.returns + write(util.prefix_lines('--- ', '@return ' .. ret .. ret_desc)) + end + local param_str = table.concat(param_names, ', ') + + write(fmt('function vim.api.%s(%s) end', fun.name, param_str)) +end + +--- @return table<string, vim.EvalFn> +local function get_api_keysets_meta() + local mpack_f = assert(io.open(DEP_API_METADATA, 'rb')) + local metadata = assert(vim.mpack.decode(mpack_f:read('*all'))) + + local ret = {} --- @type table<string, vim.EvalFn> + + --- @type {name: string, keys: string[], types: table<string,string>}[] + local keysets = metadata.keysets + + for _, k in ipairs(keysets) do + local pty_overrides = LUA_API_KEYSET_OVERRIDES[k.name] or {} + local params = {} + for _, key in ipairs(k.keys) do + local pty = pty_overrides[key] or k.types[key] or 'any' + table.insert(params, { key .. '?', api_type(pty) }) + end + ret[k.name] = { + signature = 'NA', + name = k.name, + params = params, + } + end + + return ret +end + +--- Generates LuaLS docstring for an API keyset. +--- @param _f string +--- @param fun vim.EvalFn +--- @param write fun(line: string) +local function render_api_keyset_meta(_f, fun, write) + if string.sub(fun.name, 1, 1) == '_' then + return -- not exported + end + write('') + write('--- @class vim.api.keyset.' .. fun.name) + for _, p in ipairs(fun.params) do + write('--- @field ' .. p[1] .. ' ' .. p[2]) + end +end + +--- @return table<string, vim.EvalFn> +local function get_eval_meta() + return require('nvim.eval').funcs +end + +--- Generates LuaLS docstring for a Vimscript "eval" function. +--- @param f string +--- @param fun vim.EvalFn +--- @param write fun(line: string) +local function render_eval_meta(f, fun, write) + if fun.lua == false then + return + end + + local funname = fun.name or f + local params = process_params(fun.params) + + write('') + if fun.deprecated then + write('--- @deprecated') + end + + local desc = fun.desc + + if desc then + --- @type string + desc = desc:gsub('\n%s*\n%s*$', '\n') + for _, l in ipairs(split(desc)) do + l = l:gsub('^ ', ''):gsub('\t', ' '):gsub('@', '\\@') + write('--- ' .. l) + end + end + + for _, text in ipairs(vim.fn.reverse(fun.generics or {})) do + write(fmt('--- @generic %s', text)) + end + + local req_args = type(fun.args) == 'table' and fun.args[1] or fun.args or 0 + + for i, param in ipairs(params) do + local pname, ptype = luaescape(param[1]), param[2] + local optional = (pname ~= '...' and i > req_args) and '?' or '' + write(fmt('--- @param %s%s %s', pname, optional, ptype)) + end + + if fun.returns ~= false then + local ret_desc = fun.returns_desc and ' # ' .. fun.returns_desc or '' + write('--- @return ' .. (fun.returns or 'any') .. ret_desc) + end + + write(render_fun_sig(funname, params)) +end + +--- Generates vimdoc heading for a Vimscript "eval" function signature. +--- @param name string +--- @param name_tag boolean +--- @param fun vim.EvalFn +--- @param write fun(line: string) +local function render_sig_and_tag(name, name_tag, fun, write) + if not fun.signature then + return + end + + local tags = name_tag and { '*' .. name .. '()*' } or {} + + if fun.tags then + for _, t in ipairs(fun.tags) do + tags[#tags + 1] = '*' .. t .. '*' + end + end + + if #tags == 0 then + write(fun.signature) + return + end + + local tag = table.concat(tags, ' ') + local siglen = #fun.signature + local conceal_offset = 2 * (#tags - 1) + local tag_pad_len = math.max(1, 80 - #tag + conceal_offset) + + if siglen + #tag > 80 then + write(string.rep(' ', tag_pad_len) .. tag) + write(fun.signature) + else + write(fmt('%s%s%s', fun.signature, string.rep(' ', tag_pad_len - siglen), tag)) + end +end + +--- Generates vimdoc for a Vimscript "eval" function. +--- @param f string +--- @param fun vim.EvalFn +--- @param write fun(line: string) +local function render_eval_doc(f, fun, write) + if fun.deprecated or not fun.signature then + return + end + + render_sig_and_tag(fun.name or f, not f:find('__%d+$'), fun, write) + + if not fun.desc then + return + end + + local params = process_params(fun.params) + local req_args = type(fun.args) == 'table' and fun.args[1] or fun.args or 0 + + local desc_l = split(vim.trim(fun.desc)) + for _, l in ipairs(desc_l) do + l = l:gsub('^ ', '') + if vim.startswith(l, '<') and not l:match('^<[^ \t]+>') then + write('<\t\t' .. l:sub(2)) + elseif l:match('^>[a-z0-9]*$') then + write(l) + else + write('\t\t' .. l) + end + end + + if #desc_l > 0 and not desc_l[#desc_l]:match('^<?$') then + write('') + end + + if #params > 0 then + write(util.md_to_vimdoc('Parameters: ~', 16, 16, TEXT_WIDTH)) + for i, param in ipairs(params) do + local pname, ptype = param[1], param[2] + local optional = (pname ~= '...' and i > req_args) and '?' or '' + local s = fmt('- %-14s (`%s%s`)', fmt('{%s}', pname), ptype, optional) + write(util.md_to_vimdoc(s, 16, 18, TEXT_WIDTH)) + end + write('') + end + + if fun.returns ~= false then + write(util.md_to_vimdoc('Return: ~', 16, 16, TEXT_WIDTH)) + local ret = ('(`%s`)'):format((fun.returns or 'any')) + ret = ret .. (fun.returns_desc and ' ' .. fun.returns_desc or '') + ret = util.md_to_vimdoc(ret, 18, 18, TEXT_WIDTH) + write(ret) + write('') + end +end + +--- @param d vim.option_defaults +--- @param vimdoc? boolean +--- @return string +local function render_option_default(d, vimdoc) + local dt --- @type integer|boolean|string|fun(): string + if d.if_false ~= nil then + dt = d.if_false + else + dt = d.if_true + end + + if vimdoc then + if d.doc then + return d.doc + end + if type(dt) == 'boolean' then + return dt and 'on' or 'off' + end + end + + if dt == '' or dt == nil or type(dt) == 'function' then + dt = d.meta + end + + local v --- @type string + if not vimdoc then + v = vim.inspect(dt) --[[@as string]] + else + v = type(dt) == 'string' and '"' .. dt .. '"' or tostring(dt) + end + + --- @type table<string, string|false> + local envvars = { + TMPDIR = false, + VIMRUNTIME = false, + XDG_CONFIG_HOME = vim.env.HOME .. '/.local/config', + XDG_DATA_HOME = vim.env.HOME .. '/.local/share', + XDG_STATE_HOME = vim.env.HOME .. '/.local/state', + } + + for name, default in pairs(envvars) do + local value = vim.env[name] or default + if value then + v = v:gsub(vim.pesc(value), '$' .. name) + end + end + + return v +end + +--- @param _f string +--- @param opt vim.option_meta +--- @param write fun(line: string) +local function render_option_meta(_f, opt, write) + write('') + for _, l in ipairs(split(norm_text(opt.desc))) do + write('--- ' .. l) + end + + if opt.type == 'string' and not opt.list and opt.values then + local values = {} --- @type string[] + for _, e in ipairs(opt.values) do + values[#values + 1] = fmt("'%s'", e) + end + write('--- @type ' .. table.concat(values, '|')) + else + write('--- @type ' .. OPTION_TYPES[opt.type]) + end + + write('vim.o.' .. opt.full_name .. ' = ' .. render_option_default(opt.defaults)) + if opt.abbreviation then + write('vim.o.' .. opt.abbreviation .. ' = vim.o.' .. opt.full_name) + end + + for _, s in pairs { + { 'wo', 'win' }, + { 'bo', 'buf' }, + { 'go', 'global' }, + } do + local id, scope = s[1], s[2] + if vim.list_contains(opt.scope, scope) or (id == 'go' and #opt.scope > 1) then + local pfx = 'vim.' .. id .. '.' + write(pfx .. opt.full_name .. ' = vim.o.' .. opt.full_name) + if opt.abbreviation then + write(pfx .. opt.abbreviation .. ' = ' .. pfx .. opt.full_name) + end + end + end +end + +--- @param _f string +--- @param opt vim.option_meta +--- @param write fun(line: string) +local function render_vvar_meta(_f, opt, write) + write('') + + local desc = split(norm_text(opt.desc)) + while desc[#desc]:match('^%s*$') do + desc[#desc] = nil + end + + for _, l in ipairs(desc) do + write('--- ' .. l) + end + + write('--- @type ' .. (opt.type or 'any')) + + if LUA_KEYWORDS[opt.full_name] then + write("vim.v['" .. opt.full_name .. "'] = ...") + else + write('vim.v.' .. opt.full_name .. ' = ...') + end +end + +--- @param s string[] +--- @return string +local function scope_to_doc(s) + local m = { + global = 'global', + buf = 'local to buffer', + win = 'local to window', + tab = 'local to tab page', + } + + if #s == 1 then + return m[s[1]] + end + assert(s[1] == 'global') + return 'global or ' .. m[s[2]] .. (s[2] ~= 'tab' and ' |global-local|' or '') +end + +-- @param o vim.option_meta +-- @return string +local function scope_more_doc(o) + if + vim.list_contains({ + 'bufhidden', + 'buftype', + 'filetype', + 'modified', + 'previewwindow', + 'readonly', + 'scroll', + 'syntax', + 'winfixheight', + 'winfixwidth', + }, o.full_name) + then + return ' |local-noglobal|' + end + + return '' +end + +--- @param x string +--- @return string +local function dedent(x) + local xs = split(x) + local leading_ws = xs[1]:match('^%s*') --[[@as string]] + local leading_ws_pat = '^' .. leading_ws + + for i in ipairs(xs) do + local strip_pat = xs[i]:match(leading_ws_pat) and leading_ws_pat or '^%s*' + xs[i] = xs[i]:gsub(strip_pat, '') + end + + return table.concat(xs, '\n') +end + +--- @return table<string,vim.option_meta> +local function get_option_meta() + local opts = require('nvim.options').options + local optinfo = vim.api.nvim_get_all_options_info() + local ret = {} --- @type table<string,vim.option_meta> + for _, o in ipairs(opts) do + local is_window_option = #o.scope == 1 and o.scope[1] == 'win' + local is_option_hidden = o.immutable and not o.varname and not is_window_option + if not is_option_hidden and o.desc then + if o.full_name == 'cmdheight' then + table.insert(o.scope, 'tab') + end + local r = vim.deepcopy(o) --[[@as vim.option_meta]] + r.desc = o.desc:gsub('^ ', ''):gsub('\n ', '\n') + r.defaults = r.defaults or {} + if r.defaults.meta == nil then + r.defaults.meta = optinfo[o.full_name].default + end + ret[o.full_name] = r + end + end + return ret +end + +--- @return table<string,vim.option_meta> +local function get_vvar_meta() + local info = require('nvim.vvars').vars + local ret = {} --- @type table<string,vim.option_meta> + for name, o in pairs(info) do + o.desc = dedent(o.desc) + o.full_name = name + ret[name] = o + end + return ret +end + +--- @param opt vim.option_meta +--- @return string[] +local function build_option_tags(opt) + --- @type string[] + local tags = { opt.full_name } + + tags[#tags + 1] = opt.abbreviation + if opt.type == 'boolean' then + for i = 1, #tags do + tags[#tags + 1] = 'no' .. tags[i] + end + end + + for i, t in ipairs(tags) do + tags[i] = "'" .. t .. "'" + end + + for _, t in ipairs(opt.tags or {}) do + tags[#tags + 1] = t + end + + for i, t in ipairs(tags) do + tags[i] = '*' .. t .. '*' + end + + return tags +end + +--- @param _f string +--- @param opt vim.option_meta +--- @param write fun(line: string) +local function render_option_doc(_f, opt, write) + local tags = build_option_tags(opt) + local tag_str = table.concat(tags, ' ') + local conceal_offset = 2 * (#tags - 1) + local tag_pad = string.rep('\t', math.ceil((64 - #tag_str + conceal_offset) / 8)) + -- local pad = string.rep(' ', 80 - #tag_str + conceal_offset) + write(tag_pad .. tag_str) + + local name_str --- @type string + if opt.abbreviation then + name_str = fmt("'%s' '%s'", opt.full_name, opt.abbreviation) + else + name_str = fmt("'%s'", opt.full_name) + end + + local otype = opt.type == 'boolean' and 'boolean' or opt.type + if opt.defaults.doc or opt.defaults.if_true ~= nil or opt.defaults.meta ~= nil then + local v = render_option_default(opt.defaults, true) + local pad = string.rep('\t', math.max(1, math.ceil((24 - #name_str) / 8))) + if opt.defaults.doc then + local deflen = #fmt('%s%s%s (', name_str, pad, otype) + --- @type string + v = v:gsub('\n', '\n' .. string.rep(' ', deflen - 2)) + end + write(fmt('%s%s%s\t(default %s)', name_str, pad, otype, v)) + else + write(fmt('%s\t%s', name_str, otype)) + end + + write('\t\t\t' .. scope_to_doc(opt.scope) .. scope_more_doc(opt)) + for _, l in ipairs(split(opt.desc)) do + if l == '<' or l:match('^<%s') then + write(l) + else + write('\t' .. l:gsub('\\<', '<')) + end + end +end + +--- @param _f string +--- @param vvar vim.option_meta +--- @param write fun(line: string) +local function render_vvar_doc(_f, vvar, write) + local name = vvar.full_name + + local tags = { 'v:' .. name, name .. '-variable' } + if vvar.tags then + vim.list_extend(tags, vvar.tags) + end + + for i, t in ipairs(tags) do + tags[i] = '*' .. t .. '*' + end + + local tag_str = table.concat(tags, ' ') + local conceal_offset = 2 * (#tags - 1) + + local tag_pad = string.rep('\t', math.ceil((64 - #tag_str + conceal_offset) / 8)) + write(tag_pad .. tag_str) + + local desc = split(vvar.desc) + + if (#desc == 1 or #desc == 2 and desc[2]:match('^%s*$')) and #name < 10 then + -- single line + write('v:' .. name .. '\t' .. desc[1]:gsub('^%s*', '')) + write('') + else + write('v:' .. name) + for _, l in ipairs(desc) do + if l == '<' or l:match('^<%s') then + write(l) + else + write('\t\t' .. l:gsub('\\<', '<')) + end + end + end +end + +--- @class nvim.gen_eval_files.elem +--- @field path string +--- @field from? string Skip lines in path until this pattern is reached. +--- @field funcs fun(): table<string, table> +--- @field render fun(f:string,obj:table,write:fun(line:string)) +--- @field header? string[] +--- @field footer? string[] + +--- @type nvim.gen_eval_files.elem[] +local CONFIG = { + { + path = 'runtime/lua/vim/_meta/vimfn.lua', + header = LUA_META_HEADER, + funcs = get_eval_meta, + render = render_eval_meta, + }, + { + path = 'runtime/lua/vim/_meta/api.lua', + header = LUA_API_META_HEADER, + funcs = get_api_meta, + render = render_api_meta, + }, + { + path = 'runtime/lua/vim/_meta/api_keysets.lua', + header = LUA_META_HEADER, + funcs = get_api_keysets_meta, + render = render_api_keyset_meta, + }, + { + path = 'runtime/doc/builtin.txt', + funcs = get_eval_meta, + render = render_eval_doc, + header = { + '*builtin.txt* Nvim', + '', + '', + '\t\t NVIM REFERENCE MANUAL', + '', + '', + 'Builtin functions\t\t*vimscript-functions* *builtin-functions*', + '', + 'For functions grouped by what they are used for see |function-list|.', + '', + '\t\t\t\t Type |gO| to see the table of contents.', + '==============================================================================', + '1. Details *builtin-function-details*', + '', + }, + footer = { + '==============================================================================', + '2. Matching a pattern in a String *string-match*', + '', + 'This is common between several functions. A regexp pattern as explained at', + '|pattern| is normally used to find a match in the buffer lines. When a', + 'pattern is used to find a match in a String, almost everything works in the', + 'same way. The difference is that a String is handled like it is one line.', + 'When it contains a "\\n" character, this is not seen as a line break for the', + 'pattern. It can be matched with a "\\n" in the pattern, or with ".". Example:', + '>vim', + '\tlet a = "aaaa\\nxxxx"', + '\techo matchstr(a, "..\\n..")', + '\t" aa', + '\t" xx', + '\techo matchstr(a, "a.x")', + '\t" a', + '\t" x', + '', + 'Don\'t forget that "^" will only match at the first character of the String and', + '"$" at the last character of the string. They don\'t match after or before a', + '"\\n".', + '', + ' vim:tw=78:ts=8:noet:ft=help:norl:', + }, + }, + { + path = 'runtime/lua/vim/_meta/options.lua', + header = LUA_OPTION_META_HEADER, + funcs = get_option_meta, + render = render_option_meta, + }, + { + path = 'runtime/doc/options.txt', + header = { '' }, + from = 'A jump table for the options with a short description can be found at |Q_op|.', + footer = { + ' vim:tw=78:ts=8:noet:ft=help:norl:', + }, + funcs = get_option_meta, + render = render_option_doc, + }, + { + path = 'runtime/lua/vim/_meta/vvars.lua', + header = LUA_VVAR_META_HEADER, + funcs = get_vvar_meta, + render = render_vvar_meta, + }, + { + path = 'runtime/doc/vvars.txt', + header = { '' }, + from = 'Type |gO| to see the table of contents.', + footer = { + ' vim:tw=78:ts=8:noet:ft=help:norl:', + }, + funcs = get_vvar_meta, + render = render_vvar_doc, + }, +} + +--- @param elem nvim.gen_eval_files.elem +local function render(elem) + print('Rendering ' .. elem.path) + local from_lines = {} --- @type string[] + local from = elem.from + if from then + for line in io.lines(elem.path) do + from_lines[#from_lines + 1] = line + if line:match(from) then + break + end + end + end + + local o = assert(io.open(elem.path, 'w')) + + --- @param l string + local function write(l) + local l1 = l:gsub('%s+$', '') + o:write(l1) + o:write('\n') + end + + for _, l in ipairs(from_lines) do + write(l) + end + + for _, l in ipairs(elem.header or {}) do + write(l) + end + + local funcs = elem.funcs() + + --- @type string[] + local fnames = vim.tbl_keys(funcs) + table.sort(fnames) + + for _, f in ipairs(fnames) do + elem.render(f, funcs[f], write) + end + + for _, l in ipairs(elem.footer or {}) do + write(l) + end + + o:close() +end + +local function main() + for _, c in ipairs(CONFIG) do + render(c) + end +end + +main() |