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()