neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

gen_eval_files.lua (26192B)


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