aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorLewis Russell <lewis6991@gmail.com>2025-02-26 11:38:07 +0000
committerLewis Russell <me@lewisr.dev>2025-02-26 16:54:37 +0000
commit0f24b0826a27b7868a3aacc25199787e7453d4cc (patch)
tree49585aac252581a735577f2e5711201a85ab8a7e /scripts
parent85caaa70d44b7b18c633aa0b140de5f3f6d3eee7 (diff)
downloadrneovim-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 'scripts')
-rw-r--r--scripts/cdoc_grammar.lua87
-rw-r--r--scripts/cdoc_parser.lua223
-rwxr-xr-xscripts/gen_eval_files.lua1090
-rw-r--r--scripts/gen_filetype.lua209
-rw-r--r--scripts/gen_help_html.lua1491
-rw-r--r--scripts/gen_lsp.lua514
-rwxr-xr-xscripts/gen_vimdoc.lua1041
-rwxr-xr-xscripts/lintdoc.lua4
-rw-r--r--scripts/luacats_grammar.lua207
-rw-r--r--scripts/luacats_parser.lua535
-rwxr-xr-xscripts/release.sh2
-rw-r--r--scripts/util.lua399
12 files changed, 3 insertions, 5799 deletions
diff --git a/scripts/cdoc_grammar.lua b/scripts/cdoc_grammar.lua
deleted file mode 100644
index 6a7610883b..0000000000
--- a/scripts/cdoc_grammar.lua
+++ /dev/null
@@ -1,87 +0,0 @@
---[[!
-LPEG grammar for C doc comments
-]]
-
---- @class nvim.cdoc.Param
---- @field kind 'param'
---- @field name string
---- @field desc? string
-
---- @class nvim.cdoc.Return
---- @field kind 'return'
---- @field desc string
-
---- @class nvim.cdoc.Note
---- @field desc? string
-
---- @alias nvim.cdoc.grammar.result
---- | nvim.cdoc.Param
---- | nvim.cdoc.Return
---- | nvim.cdoc.Note
-
---- @class nvim.cdoc.grammar
---- @field match fun(self, input: string): nvim.cdoc.grammar.result?
-
-local lpeg = vim.lpeg
-local P, R, S = lpeg.P, lpeg.R, lpeg.S
-local Ct, Cg = lpeg.Ct, lpeg.Cg
-
---- @param x vim.lpeg.Pattern
-local function rep(x)
- return x ^ 0
-end
-
---- @param x vim.lpeg.Pattern
-local function rep1(x)
- return x ^ 1
-end
-
---- @param x vim.lpeg.Pattern
-local function opt(x)
- return x ^ -1
-end
-
-local nl = P('\r\n') + P('\n')
-local ws = rep1(S(' \t') + nl)
-
-local any = P(1) -- (consume one character)
-local letter = R('az', 'AZ') + S('_$')
-local ident = letter * rep(letter + R('09'))
-
-local io = P('[') * (P('in') + P('out') + P('inout')) * P(']')
-
---- @param x string
-local function Pf(x)
- return opt(ws) * P(x) * opt(ws)
-end
-
---- @type table<string,vim.lpeg.Pattern>
-local v = setmetatable({}, {
- __index = function(_, k)
- return lpeg.V(k)
- end,
-})
-
-local grammar = P {
- rep1(P('@') * v.ats),
-
- ats = v.at_param + v.at_return + v.at_deprecated + v.at_see + v.at_brief + v.at_note + v.at_nodoc,
-
- at_param = Ct(
- Cg(P('param'), 'kind') * opt(io) * ws * Cg(ident, 'name') * opt(ws * Cg(rep(any), 'desc'))
- ),
-
- at_return = Ct(Cg(P('return'), 'kind') * opt(S('s')) * opt(ws * Cg(rep(any), 'desc'))),
-
- at_deprecated = Ct(Cg(P('deprecated'), 'kind')),
-
- at_see = Ct(Cg(P('see'), 'kind') * ws * opt(Pf('#')) * Cg(rep(any), 'desc')),
-
- at_brief = Ct(Cg(P('brief'), 'kind') * ws * Cg(rep(any), 'desc')),
-
- at_note = Ct(Cg(P('note'), 'kind') * ws * Cg(rep(any), 'desc')),
-
- at_nodoc = Ct(Cg(P('nodoc'), 'kind')),
-}
-
-return grammar --[[@as nvim.cdoc.grammar]]
diff --git a/scripts/cdoc_parser.lua b/scripts/cdoc_parser.lua
deleted file mode 100644
index 5f0dc7be2c..0000000000
--- a/scripts/cdoc_parser.lua
+++ /dev/null
@@ -1,223 +0,0 @@
-local cdoc_grammar = require('scripts.cdoc_grammar')
-local c_grammar = require('src.nvim.generators.c_grammar')
-
---- @class nvim.cdoc.parser.param
---- @field name string
---- @field type string
---- @field desc string
-
---- @class nvim.cdoc.parser.return
---- @field name string
---- @field type string
---- @field desc string
-
---- @class nvim.cdoc.parser.note
---- @field desc string
-
---- @class nvim.cdoc.parser.brief
---- @field kind 'brief'
---- @field desc string
-
---- @class nvim.cdoc.parser.fun
---- @field name string
---- @field params nvim.cdoc.parser.param[]
---- @field returns nvim.cdoc.parser.return[]
---- @field desc string
---- @field deprecated? true
---- @field since? string
---- @field attrs? string[]
---- @field nodoc? true
---- @field notes? nvim.cdoc.parser.note[]
---- @field see? nvim.cdoc.parser.note[]
-
---- @class nvim.cdoc.parser.State
---- @field doc_lines? string[]
---- @field cur_obj? nvim.cdoc.parser.obj
---- @field last_doc_item? nvim.cdoc.parser.param|nvim.cdoc.parser.return|nvim.cdoc.parser.note
---- @field last_doc_item_indent? integer
-
---- @alias nvim.cdoc.parser.obj
---- | nvim.cdoc.parser.fun
---- | nvim.cdoc.parser.brief
-
---- If we collected any `---` lines. Add them to the existing (or new) object
---- Used for function/class descriptions and multiline param descriptions.
---- @param state nvim.cdoc.parser.State
-local function add_doc_lines_to_obj(state)
- if state.doc_lines then
- state.cur_obj = state.cur_obj or {}
- local cur_obj = assert(state.cur_obj)
- local txt = table.concat(state.doc_lines, '\n')
- if cur_obj.desc then
- cur_obj.desc = cur_obj.desc .. '\n' .. txt
- else
- cur_obj.desc = txt
- end
- state.doc_lines = nil
- end
-end
-
---- @param line string
---- @param state nvim.cdoc.parser.State
-local function process_doc_line(line, state)
- line = line:gsub('^%s+@', '@')
-
- local parsed = cdoc_grammar:match(line)
-
- if not parsed then
- if line:match('^ ') then
- line = line:sub(2)
- end
-
- if state.last_doc_item then
- if not state.last_doc_item_indent then
- state.last_doc_item_indent = #line:match('^%s*') + 1
- end
- state.last_doc_item.desc = (state.last_doc_item.desc or '')
- .. '\n'
- .. line:sub(state.last_doc_item_indent or 1)
- else
- state.doc_lines = state.doc_lines or {}
- table.insert(state.doc_lines, line)
- end
- return
- end
-
- state.last_doc_item_indent = nil
- state.last_doc_item = nil
-
- local kind = parsed.kind
-
- state.cur_obj = state.cur_obj or {}
- local cur_obj = assert(state.cur_obj)
-
- if kind == 'brief' then
- state.cur_obj = {
- kind = 'brief',
- desc = parsed.desc,
- }
- elseif kind == 'param' then
- state.last_doc_item_indent = nil
- cur_obj.params = cur_obj.params or {}
- state.last_doc_item = {
- name = parsed.name,
- desc = parsed.desc,
- }
- table.insert(cur_obj.params, state.last_doc_item)
- elseif kind == 'return' then
- cur_obj.returns = { {
- desc = parsed.desc,
- } }
- state.last_doc_item_indent = nil
- state.last_doc_item = cur_obj.returns[1]
- elseif kind == 'deprecated' then
- cur_obj.deprecated = true
- elseif kind == 'nodoc' then
- cur_obj.nodoc = true
- elseif kind == 'since' then
- cur_obj.since = parsed.desc
- elseif kind == 'see' then
- cur_obj.see = cur_obj.see or {}
- table.insert(cur_obj.see, { desc = parsed.desc })
- elseif kind == 'note' then
- state.last_doc_item_indent = nil
- state.last_doc_item = {
- desc = parsed.desc,
- }
- cur_obj.notes = cur_obj.notes or {}
- table.insert(cur_obj.notes, state.last_doc_item)
- else
- error('Unhandled' .. vim.inspect(parsed))
- end
-end
-
---- @param item table
---- @param state nvim.cdoc.parser.State
-local function process_proto(item, state)
- state.cur_obj = state.cur_obj or {}
- local cur_obj = assert(state.cur_obj)
- cur_obj.name = item.name
- cur_obj.params = cur_obj.params or {}
-
- for _, p in ipairs(item.parameters) do
- local param = { name = p[2], type = p[1] }
- local added = false
- for _, cp in ipairs(cur_obj.params) do
- if cp.name == param.name then
- cp.type = param.type
- added = true
- break
- end
- end
-
- if not added then
- table.insert(cur_obj.params, param)
- end
- end
-
- cur_obj.returns = cur_obj.returns or { {} }
- cur_obj.returns[1].type = item.return_type
-
- for _, a in ipairs({
- 'fast',
- 'remote_only',
- 'lua_only',
- 'textlock',
- 'textlock_allow_cmdwin',
- }) do
- if item[a] then
- cur_obj.attrs = cur_obj.attrs or {}
- table.insert(cur_obj.attrs, a)
- end
- end
-
- cur_obj.deprecated_since = item.deprecated_since
-
- -- Remove some arguments
- for i = #cur_obj.params, 1, -1 do
- local p = cur_obj.params[i]
- if p.name == 'channel_id' or vim.tbl_contains({ 'lstate', 'arena', 'error' }, p.type) then
- table.remove(cur_obj.params, i)
- end
- end
-end
-
-local M = {}
-
---- @param filename string
---- @return {} classes
---- @return nvim.cdoc.parser.fun[] funs
---- @return string[] briefs
-function M.parse(filename)
- local funs = {} --- @type nvim.cdoc.parser.fun[]
- local briefs = {} --- @type string[]
- local state = {} --- @type nvim.cdoc.parser.State
-
- local txt = assert(io.open(filename, 'r')):read('*all')
-
- local parsed = c_grammar.grammar:match(txt)
- for _, item in ipairs(parsed) do
- if item.comment then
- process_doc_line(item.comment, state)
- else
- add_doc_lines_to_obj(state)
- if item[1] == 'proto' then
- process_proto(item, state)
- table.insert(funs, state.cur_obj)
- end
- local cur_obj = state.cur_obj
- if cur_obj and not item.static then
- if cur_obj.kind == 'brief' then
- table.insert(briefs, cur_obj.desc)
- end
- end
- state = {}
- end
- end
-
- return {}, funs, briefs
-end
-
--- M.parse('src/nvim/api/vim.c')
-
-return M
diff --git a/scripts/gen_eval_files.lua b/scripts/gen_eval_files.lua
deleted file mode 100755
index aaf76a0411..0000000000
--- a/scripts/gen_eval_files.lua
+++ /dev/null
@@ -1,1090 +0,0 @@
-#!/usr/bin/env -S nvim -l
-
--- Generator for various vimdoc and Lua type files
-
-local util = require('scripts.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('scripts.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('src/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('src/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('src/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()
diff --git a/scripts/gen_filetype.lua b/scripts/gen_filetype.lua
deleted file mode 100644
index 18b53f1ea4..0000000000
--- a/scripts/gen_filetype.lua
+++ /dev/null
@@ -1,209 +0,0 @@
-local do_not_run = true
-if do_not_run then
- print([[
- This script was used to bootstrap the filetype patterns in runtime/lua/vim/filetype.lua. It
- should no longer be used except for testing purposes. New filetypes, or changes to existing
- filetypes, should be ported manually as part of the vim-patch process.
- ]])
- return
-end
-
-local filetype_vim = 'runtime/filetype.vim'
-local filetype_lua = 'runtime/lua/vim/filetype.lua'
-
-local keywords = {
- ['for'] = true,
- ['or'] = true,
- ['and'] = true,
- ['end'] = true,
- ['do'] = true,
- ['if'] = true,
- ['while'] = true,
- ['repeat'] = true,
-}
-
-local sections = {
- extension = { str = {}, func = {} },
- filename = { str = {}, func = {} },
- pattern = { str = {}, func = {} },
-}
-
-local specialchars = '%*%?\\%$%[%]%{%}'
-
-local function add_pattern(pat, ft)
- local ok = true
-
- -- Patterns that start or end with { or } confuse splitting on commas and make parsing harder, so just skip those
- if not string.find(pat, '^%{') and not string.find(pat, '%}$') then
- for part in string.gmatch(pat, '[^,]+') do
- if not string.find(part, '[' .. specialchars .. ']') then
- if type(ft) == 'string' then
- sections.filename.str[part] = ft
- else
- sections.filename.func[part] = ft
- end
- elseif string.match(part, '^%*%.[^%./' .. specialchars .. ']+$') then
- if type(ft) == 'string' then
- sections.extension.str[part:sub(3)] = ft
- else
- sections.extension.func[part:sub(3)] = ft
- end
- else
- if string.match(part, '^%*/[^' .. specialchars .. ']+$') then
- -- For patterns matching */some/pattern we want to easily match files
- -- with path /some/pattern, so include those in filename detection
- if type(ft) == 'string' then
- sections.filename.str[part:sub(2)] = ft
- else
- sections.filename.func[part:sub(2)] = ft
- end
- end
-
- if string.find(part, '^[%w-_.*?%[%]/]+$') then
- local p = part:gsub('%.', '%%.'):gsub('%*', '.*'):gsub('%?', '.')
- -- Insert into array to maintain order rather than setting
- -- key-value directly
- if type(ft) == 'string' then
- sections.pattern.str[p] = ft
- else
- sections.pattern.func[p] = ft
- end
- else
- ok = false
- end
- end
- end
- end
-
- return ok
-end
-
-local function parse_line(line)
- local pat, ft
- pat, ft = line:match('^%s*au%a* Buf[%a,]+%s+(%S+)%s+setf%s+(%S+)')
- if pat then
- return add_pattern(pat, ft)
- else
- local func
- pat, func = line:match('^%s*au%a* Buf[%a,]+%s+(%S+)%s+call%s+(%S+)')
- if pat then
- return add_pattern(pat, function()
- return func
- end)
- end
- end
-end
-
-local unparsed = {}
-local full_line
-for line in io.lines(filetype_vim) do
- local cont = string.match(line, '^%s*\\%s*(.*)$')
- if cont then
- full_line = full_line .. ' ' .. cont
- else
- if full_line then
- if not parse_line(full_line) and string.find(full_line, '^%s*au%a* Buf') then
- table.insert(unparsed, full_line)
- end
- end
- full_line = line
- end
-end
-
-if #unparsed > 0 then
- print('Failed to parse the following patterns:')
- for _, v in ipairs(unparsed) do
- print(v)
- end
-end
-
-local function add_item(indent, key, ft)
- if type(ft) == 'string' then
- if string.find(key, '%A') or keywords[key] then
- key = string.format('["%s"]', key)
- end
- return string.format([[%s%s = "%s",]], indent, key, ft)
- elseif type(ft) == 'function' then
- local func = ft()
- if string.find(key, '%A') or keywords[key] then
- key = string.format('["%s"]', key)
- end
- -- Right now only a single argument is supported, which covers
- -- everything in filetype.vim as of this writing
- local arg = string.match(func, '%((.*)%)$')
- func = string.gsub(func, '%(.*$', '')
- if arg == '' then
- -- Function with no arguments, call the function directly
- return string.format([[%s%s = function() vim.fn["%s"]() end,]], indent, key, func)
- elseif string.match(arg, [[^(["']).*%1$]]) then
- -- String argument
- if func == 's:StarSetf' then
- return string.format([[%s%s = starsetf(%s),]], indent, key, arg)
- else
- return string.format([[%s%s = function() vim.fn["%s"](%s) end,]], indent, key, func, arg)
- end
- elseif string.find(arg, '%(') then
- -- Function argument
- return string.format(
- [[%s%s = function() vim.fn["%s"](vim.fn.%s) end,]],
- indent,
- key,
- func,
- arg
- )
- else
- assert(false, arg)
- end
- end
-end
-
-do
- local lines = {}
- local start = false
- for line in io.lines(filetype_lua) do
- if line:match('^%s+-- END [A-Z]+$') then
- start = false
- end
-
- if not start then
- table.insert(lines, line)
- end
-
- local indent, section = line:match('^(%s+)-- BEGIN ([A-Z]+)$')
- if section then
- start = true
- local t = sections[string.lower(section)]
-
- local sorted = {}
- for k, v in pairs(t.str) do
- table.insert(sorted, { [k] = v })
- end
-
- table.sort(sorted, function(a, b)
- return a[next(a)] < b[next(b)]
- end)
-
- for _, v in ipairs(sorted) do
- local k = next(v)
- table.insert(lines, add_item(indent, k, v[k]))
- end
-
- sorted = {}
- for k, v in pairs(t.func) do
- table.insert(sorted, { [k] = v })
- end
-
- table.sort(sorted, function(a, b)
- return next(a) < next(b)
- end)
-
- for _, v in ipairs(sorted) do
- local k = next(v)
- table.insert(lines, add_item(indent, k, v[k]))
- end
- end
- end
- local f = io.open(filetype_lua, 'w')
- f:write(table.concat(lines, '\n') .. '\n')
- f:close()
-end
diff --git a/scripts/gen_help_html.lua b/scripts/gen_help_html.lua
deleted file mode 100644
index 53a65fd65f..0000000000
--- a/scripts/gen_help_html.lua
+++ /dev/null
@@ -1,1491 +0,0 @@
---- Converts Nvim :help files to HTML. Validates |tag| links and document syntax (parser errors).
---
--- USAGE (For CI/local testing purposes): Simply `make lintdoc` or `scripts/lintdoc.lua`, which
--- basically does the following:
--- 1. :helptags ALL
--- 2. nvim -V1 -es +"lua require('scripts.gen_help_html').run_validate()" +q
--- 3. nvim -V1 -es +"lua require('scripts.gen_help_html').test_gen()" +q
---
--- USAGE (GENERATE HTML):
--- 1. `:helptags ALL` first; this script depends on vim.fn.taglist().
--- 2. nvim -V1 -es --clean +"lua require('scripts.gen_help_html').gen('./runtime/doc', 'target/dir/')" +q
--- - Read the docstring at gen().
--- 3. cd target/dir/ && jekyll serve --host 0.0.0.0
--- 4. Visit http://localhost:4000/…/help.txt.html
---
--- USAGE (VALIDATE):
--- 1. nvim -V1 -es +"lua require('scripts.gen_help_html').validate('./runtime/doc')" +q
--- - validate() is 10x faster than gen(), so it is used in CI.
---
--- SELF-TEST MODE:
--- 1. nvim -V1 -es +"lua require('scripts.gen_help_html')._test()" +q
---
--- NOTES:
--- * This script is used by the automation repo: https://github.com/neovim/doc
--- * :helptags checks for duplicate tags, whereas this script checks _links_ (to tags).
--- * gen() and validate() are the primary (programmatic) entrypoints. validate() only exists
--- because gen() is too slow (~1 min) to run in per-commit CI.
--- * visit_node() is the core function used by gen() to traverse the document tree and produce HTML.
--- * visit_validate() is the core function used by validate().
--- * Files in `new_layout` will be generated with a "flow" layout instead of preformatted/fixed-width layout.
-
-local tagmap = nil ---@type table<string, string>
-local helpfiles = nil ---@type string[]
-local invalid_links = {} ---@type table<string, any>
-local invalid_urls = {} ---@type table<string, any>
-local invalid_spelling = {} ---@type table<string, table<string, string>>
-local spell_dict = {
- Neovim = 'Nvim',
- NeoVim = 'Nvim',
- neovim = 'Nvim',
- lua = 'Lua',
- VimL = 'Vimscript',
- vimL = 'Vimscript',
- viml = 'Vimscript',
- ['tree-sitter'] = 'treesitter',
- ['Tree-sitter'] = 'Treesitter',
-}
---- specify the list of keywords to ignore (i.e. allow), or true to disable spell check completely.
---- @type table<string, true|string[]>
-local spell_ignore_files = {
- ['credits.txt'] = { 'Neovim' },
- ['news.txt'] = { 'tree-sitter' }, -- in news, may refer to the upstream "tree-sitter" library
- ['news-0.10.txt'] = { 'tree-sitter' },
-}
-local language = nil
-
-local M = {}
-
--- These files are generated with "flow" layout (non fixed-width, wrapped text paragraphs).
--- All other files are "legacy" files which require fixed-width layout.
-local new_layout = {
- ['api.txt'] = true,
- ['lsp.txt'] = true,
- ['channel.txt'] = true,
- ['deprecated.txt'] = true,
- ['develop.txt'] = true,
- ['dev_style.txt'] = true,
- ['dev_theme.txt'] = true,
- ['dev_tools.txt'] = true,
- ['dev_vimpatch.txt'] = true,
- ['editorconfig.txt'] = true,
- ['faq.txt'] = true,
- ['gui.txt'] = true,
- ['intro.txt'] = true,
- ['lua.txt'] = true,
- ['luaref.txt'] = true,
- ['news.txt'] = true,
- ['news-0.9.txt'] = true,
- ['news-0.10.txt'] = true,
- ['nvim.txt'] = true,
- ['provider.txt'] = true,
- ['tui.txt'] = true,
- ['ui.txt'] = true,
- ['vim_diff.txt'] = true,
-}
-
--- Map of new:old pages, to redirect renamed pages.
-local redirects = {
- ['credits'] = 'backers',
- ['tui'] = 'term',
- ['terminal'] = 'nvim_terminal_emulator',
-}
-
--- TODO: These known invalid |links| require an update to the relevant docs.
-local exclude_invalid = {
- ["'string'"] = 'eval.txt',
- Query = 'treesitter.txt',
- matchit = 'vim_diff.txt',
- ['set!'] = 'treesitter.txt',
-}
-
--- False-positive "invalid URLs".
-local exclude_invalid_urls = {
- ['http://'] = 'usr_23.txt',
- ['http://.'] = 'usr_23.txt',
- ['http://aspell.net/man-html/Affix-Compression.html'] = 'spell.txt',
- ['http://aspell.net/man-html/Phonetic-Code.html'] = 'spell.txt',
- ['http://canna.sourceforge.jp/'] = 'mbyte.txt',
- ['http://gnuada.sourceforge.net'] = 'ft_ada.txt',
- ['http://lua-users.org/wiki/StringLibraryTutorial'] = 'lua.txt',
- ['http://michael.toren.net/code/'] = 'pi_tar.txt',
- ['http://papp.plan9.de'] = 'syntax.txt',
- ['http://wiki.services.openoffice.org/wiki/Dictionaries'] = 'spell.txt',
- ['http://www.adapower.com'] = 'ft_ada.txt',
- ['http://www.jclark.com/'] = 'quickfix.txt',
- ['http://oldblog.antirez.com/post/redis-and-scripting.html'] = 'faq.txt',
-}
-
--- Deprecated, brain-damaged files that I don't care about.
-local ignore_errors = {
- ['pi_netrw.txt'] = true,
- ['credits.txt'] = true,
-}
-
-local function tofile(fname, text)
- local f = io.open(fname, 'w')
- if not f then
- error(('failed to write: %s'):format(f))
- else
- f:write(text)
- f:close()
- end
-end
-
----@type fun(s: string): string
-local function html_esc(s)
- return (s:gsub('&', '&amp;'):gsub('<', '&lt;'):gsub('>', '&gt;'))
-end
-
-local function url_encode(s)
- -- Credit: tpope / vim-unimpaired
- -- NOTE: these chars intentionally *not* escaped: ' ( )
- return vim.fn.substitute(
- vim.fn.iconv(s, 'latin1', 'utf-8'),
- [=[[^A-Za-z0-9()'_.~-]]=],
- [=[\="%".printf("%02X",char2nr(submatch(0)))]=],
- 'g'
- )
-end
-
-local function expandtabs(s)
- return s:gsub('\t', (' '):rep(8)) --[[ @as string ]]
-end
-
-local function to_titlecase(s)
- local text = ''
- for w in vim.gsplit(s, '[ \t]+') do
- text = ('%s %s%s'):format(text, vim.fn.toupper(w:sub(1, 1)), w:sub(2))
- end
- return text
-end
-
-local function to_heading_tag(text)
- -- Prepend "_" to avoid conflicts with actual :help tags.
- return text and string.format('_%s', vim.fn.tolower((text:gsub('%s+', '-')))) or 'unknown'
-end
-
-local function basename_noext(f)
- return vim.fs.basename(f:gsub('%.txt', ''))
-end
-
-local function is_blank(s)
- return not not s:find([[^[\t ]*$]])
-end
-
----@type fun(s: string, dir?:0|1|2): string
-local function trim(s, dir)
- return vim.fn.trim(s, '\r\t\n ', dir or 0)
-end
-
---- Removes common punctuation from URLs.
----
---- TODO: fix this in the parser instead... https://github.com/neovim/tree-sitter-vimdoc
----
---- @param url string
---- @return string, string (fixed_url, removed_chars) where `removed_chars` is in the order found in the input.
-local function fix_url(url)
- local removed_chars = ''
- local fixed_url = url
- -- Remove up to one of each char from end of the URL, in this order.
- for _, c in ipairs({ '.', ')' }) do
- if fixed_url:sub(-1) == c then
- removed_chars = c .. removed_chars
- fixed_url = fixed_url:sub(1, -2)
- end
- end
- return fixed_url, removed_chars
-end
-
---- Checks if a given line is a "noise" line that doesn't look good in HTML form.
-local function is_noise(line, noise_lines)
- if
- -- First line is always noise.
- (noise_lines ~= nil and vim.tbl_count(noise_lines) == 0)
- or line:find('Type .*gO.* to see the table of contents')
- -- Title line of traditional :help pages.
- -- Example: "NVIM REFERENCE MANUAL by ..."
- or line:find([[^%s*N?VIM[ \t]*REFERENCE[ \t]*MANUAL]])
- -- First line of traditional :help pages.
- -- Example: "*api.txt* Nvim"
- or line:find('%s*%*?[a-zA-Z]+%.txt%*?%s+N?[vV]im%s*$')
- -- modeline
- -- Example: "vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:"
- or line:find('^%s*vim?%:.*ft=help')
- or line:find('^%s*vim?%:.*filetype=help')
- or line:find('[*>]local%-additions[*<]')
- then
- -- table.insert(stats.noise_lines, getbuflinestr(root, opt.buf, 0))
- table.insert(noise_lines or {}, line)
- return true
- end
- return false
-end
-
---- Creates a github issue URL at neovim/tree-sitter-vimdoc with prefilled content.
---- @return string
-local function get_bug_url_vimdoc(fname, to_fname, sample_text)
- local this_url = string.format('https://neovim.io/doc/user/%s', vim.fs.basename(to_fname))
- local bug_url = (
- 'https://github.com/neovim/tree-sitter-vimdoc/issues/new?labels=bug&title=parse+error%3A+'
- .. vim.fs.basename(fname)
- .. '+&body=Found+%60tree-sitter-vimdoc%60+parse+error+at%3A+'
- .. this_url
- .. '%0D%0DContext%3A%0D%0D%60%60%60%0D'
- .. url_encode(sample_text)
- .. '%0D%60%60%60'
- )
- return bug_url
-end
-
---- Creates a github issue URL at neovim/neovim with prefilled content.
---- @return string
-local function get_bug_url_nvim(fname, to_fname, sample_text, token_name)
- local this_url = string.format('https://neovim.io/doc/user/%s', vim.fs.basename(to_fname))
- local bug_url = (
- 'https://github.com/neovim/neovim/issues/new?labels=bug&title=user+docs+HTML%3A+'
- .. vim.fs.basename(fname)
- .. '+&body=%60gen_help_html.lua%60+problem+at%3A+'
- .. this_url
- .. '%0D'
- .. (token_name and '+unhandled+token%3A+%60' .. token_name .. '%60' or '')
- .. '%0DContext%3A%0D%0D%60%60%60%0D'
- .. url_encode(sample_text)
- .. '%0D%60%60%60'
- )
- return bug_url
-end
-
---- Gets a "foo.html" name from a "foo.txt" helpfile name.
-local function get_helppage(f)
- if not f then
- return nil
- end
- -- Special case: help.txt is the "main landing page" of :help files, not index.txt.
- if f == 'index.txt' then
- return 'vimindex.html'
- elseif f == 'help.txt' then
- return 'index.html'
- end
-
- return (f:gsub('%.txt$', '')) .. '.html'
-end
-
---- Counts leading spaces (tab=8) to decide the indent size of multiline text.
----
---- Blank lines (empty or whitespace-only) are ignored.
-local function get_indent(s)
- local min_indent = nil
- for line in vim.gsplit(s, '\n') do
- if line and not is_blank(line) then
- local ws = expandtabs(line:match('^%s+') or '')
- min_indent = (not min_indent or ws:len() < min_indent) and ws:len() or min_indent
- end
- end
- return min_indent or 0
-end
-
---- Removes the common indent level, after expanding tabs to 8 spaces.
-local function trim_indent(s)
- local indent_size = get_indent(s)
- local trimmed = ''
- for line in vim.gsplit(s, '\n') do
- line = expandtabs(line)
- trimmed = ('%s%s\n'):format(trimmed, line:sub(indent_size + 1))
- end
- return trimmed:sub(1, -2)
-end
-
---- Gets raw buffer text in the node's range (+/- an offset), as a newline-delimited string.
----@param node TSNode
----@param bufnr integer
----@param offset integer
-local function getbuflinestr(node, bufnr, offset)
- local line1, _, line2, _ = node:range()
- line1 = line1 - offset
- line2 = line2 + offset
- local lines = vim.fn.getbufline(bufnr, line1 + 1, line2 + 1)
- return table.concat(lines, '\n')
-end
-
---- Gets the whitespace just before `node` from the raw buffer text.
---- Needed for preformatted `old` lines.
----@param node TSNode
----@param bufnr integer
----@return string
-local function getws(node, bufnr)
- local line1, c1, line2, _ = node:range()
- ---@type string
- local raw = vim.fn.getbufline(bufnr, line1 + 1, line2 + 1)[1]
- local text_before = raw:sub(1, c1)
- local leading_ws = text_before:match('%s+$') or ''
- return leading_ws
-end
-
-local function get_tagname(node, bufnr)
- local text = vim.treesitter.get_node_text(node, bufnr)
- local tag = (node:type() == 'optionlink' or node:parent():type() == 'optionlink')
- and ("'%s'"):format(text)
- or text
- local helpfile = vim.fs.basename(tagmap[tag]) or nil -- "api.txt"
- local helppage = get_helppage(helpfile) -- "api.html"
- return helppage, tag
-end
-
---- Returns true if the given invalid tagname is a false positive.
-local function ignore_invalid(s)
- return not not (
- exclude_invalid[s]
- -- Strings like |~/====| appear in various places and the parser thinks they are links, but they
- -- are just table borders.
- or s:find('===')
- or s:find('%-%-%-')
- )
-end
-
-local function ignore_parse_error(fname, s)
- if ignore_errors[vim.fs.basename(fname)] then
- return true
- end
- -- Ignore parse errors for unclosed tag.
- -- This is common in vimdocs and is treated as plaintext by :help.
- return s:find("^[`'|*]")
-end
-
----@param node TSNode
-local function has_ancestor(node, ancestor_name)
- local p = node ---@type TSNode?
- while p do
- p = p:parent()
- if not p or p:type() == 'help_file' then
- break
- elseif p:type() == ancestor_name then
- return true
- end
- end
- return false
-end
-
---- Gets the first matching child node matching `name`.
----@param node TSNode
-local function first(node, name)
- for c, _ in node:iter_children() do
- if c:named() and c:type() == name then
- return c
- end
- end
- return nil
-end
-
-local function validate_link(node, bufnr, fname)
- local helppage, tagname = get_tagname(node:child(1), bufnr)
- local ignored = false
- if not tagmap[tagname] then
- ignored = has_ancestor(node, 'column_heading') or node:has_error() or ignore_invalid(tagname)
- if not ignored then
- invalid_links[tagname] = vim.fs.basename(fname)
- end
- end
- return helppage, tagname, ignored
-end
-
---- TODO: port the logic from scripts/check_urls.vim
-local function validate_url(text, fname)
- local ignored = false
- if ignore_errors[vim.fs.basename(fname)] then
- ignored = true
- elseif text:find('http%:') and not exclude_invalid_urls[text] then
- invalid_urls[text] = vim.fs.basename(fname)
- end
- return ignored
-end
-
---- Traverses the tree at `root` and checks that |tag| links point to valid helptags.
----@param root TSNode
----@param level integer
----@param lang_tree TSTree
----@param opt table
----@param stats table
-local function visit_validate(root, level, lang_tree, opt, stats)
- level = level or 0
- local node_name = (root.named and root:named()) and root:type() or nil
- -- Parent kind (string).
- local parent = root:parent() and root:parent():type() or nil
- local toplevel = level < 1
- local function node_text(node)
- return vim.treesitter.get_node_text(node or root, opt.buf)
- end
- local text = trim(node_text())
-
- if root:child_count() > 0 then
- for node, _ in root:iter_children() do
- if node:named() then
- visit_validate(node, level + 1, lang_tree, opt, stats)
- end
- end
- end
-
- if node_name == 'ERROR' then
- if ignore_parse_error(opt.fname, text) then
- return
- end
- -- Store the raw text to give context to the error report.
- local sample_text = not toplevel and getbuflinestr(root, opt.buf, 0) or '[top level!]'
- -- Flatten the sample text to a single, truncated line.
- sample_text = vim.trim(sample_text):gsub('[\t\n]', ' '):sub(1, 80)
- table.insert(stats.parse_errors, sample_text)
- elseif
- (node_name == 'word' or node_name == 'uppercase_name')
- and (not vim.tbl_contains({ 'codespan', 'taglink', 'tag' }, parent))
- then
- local text_nopunct = vim.fn.trim(text, '.,', 0) -- Ignore some punctuation.
- local fname_basename = assert(vim.fs.basename(opt.fname))
- if spell_dict[text_nopunct] then
- local should_ignore = (
- spell_ignore_files[fname_basename] == true
- or vim.tbl_contains(
- (spell_ignore_files[fname_basename] or {}) --[[ @as string[] ]],
- text_nopunct
- )
- )
- if not should_ignore then
- invalid_spelling[text_nopunct] = invalid_spelling[text_nopunct] or {}
- invalid_spelling[text_nopunct][fname_basename] = node_text(root:parent())
- end
- end
- elseif node_name == 'url' then
- local fixed_url, _ = fix_url(trim(text))
- validate_url(fixed_url, opt.fname)
- elseif node_name == 'taglink' or node_name == 'optionlink' then
- local _, _, _ = validate_link(root, opt.buf, opt.fname)
- end
-end
-
--- Fix tab alignment issues caused by concealed characters like |, `, * in tags
--- and code blocks.
----@param text string
----@param next_node_text string
-local function fix_tab_after_conceal(text, next_node_text)
- -- Vim tabs take into account the two concealed characters even though they
- -- are invisible, so we need to add back in the two spaces if this is
- -- followed by a tab to make the tab alignment to match Vim's behavior.
- if string.sub(next_node_text, 1, 1) == '\t' then
- text = text .. ' '
- end
- return text
-end
-
----@class (exact) nvim.gen_help_html.heading
----@field name string
----@field subheadings nvim.gen_help_html.heading[]
----@field tag string
-
--- Generates HTML from node `root` recursively.
----@param root TSNode
----@param level integer
----@param lang_tree TSTree
----@param headings nvim.gen_help_html.heading[]
----@param opt table
----@param stats table
-local function visit_node(root, level, lang_tree, headings, opt, stats)
- level = level or 0
-
- local node_name = (root.named and root:named()) and root:type() or nil
- -- Previous sibling kind (string).
- local prev = root:prev_sibling()
- and (root:prev_sibling().named and root:prev_sibling():named())
- and root:prev_sibling():type()
- or nil
- -- Next sibling kind (string).
- local next_ = root:next_sibling()
- and (root:next_sibling().named and root:next_sibling():named())
- and root:next_sibling():type()
- or nil
- -- Parent kind (string).
- local parent = root:parent() and root:parent():type() or nil
- -- Gets leading whitespace of `node`.
- local function ws(node)
- node = node or root
- local ws_ = getws(node, opt.buf)
- -- XXX: first node of a (line) includes whitespace, even after
- -- https://github.com/neovim/tree-sitter-vimdoc/pull/31 ?
- if ws_ == '' then
- ws_ = vim.treesitter.get_node_text(node, opt.buf):match('^%s+') or ''
- end
- return ws_
- end
- local function node_text(node, ws_)
- node = node or root
- ws_ = (ws_ == nil or ws_ == true) and getws(node, opt.buf) or ''
- return string.format('%s%s', ws_, vim.treesitter.get_node_text(node, opt.buf))
- end
-
- local text = ''
- local trimmed ---@type string
- if root:named_child_count() == 0 or node_name == 'ERROR' then
- text = node_text()
- trimmed = html_esc(trim(text))
- text = html_esc(text)
- else
- -- Process children and join them with whitespace.
- for node, _ in root:iter_children() do
- if node:named() then
- local r = visit_node(node, level + 1, lang_tree, headings, opt, stats)
- text = string.format('%s%s', text, r)
- end
- end
- trimmed = trim(text)
- end
-
- if node_name == 'help_file' then -- root node
- return text
- elseif node_name == 'url' then
- local fixed_url, removed_chars = fix_url(trimmed)
- return ('%s<a href="%s">%s</a>%s'):format(ws(), fixed_url, fixed_url, removed_chars)
- elseif node_name == 'word' or node_name == 'uppercase_name' then
- return text
- elseif node_name == 'note' then
- return ('<b>%s</b>'):format(text)
- elseif node_name == 'h1' or node_name == 'h2' or node_name == 'h3' then
- if is_noise(text, stats.noise_lines) then
- return '' -- Discard common "noise" lines.
- end
- -- Remove tags from ToC text.
- local heading_node = first(root, 'heading')
- local hname = trim(node_text(heading_node):gsub('%*.*%*', ''))
- if not heading_node or hname == '' then
- return '' -- Spurious "===" or "---" in the help doc.
- end
-
- -- Generate an anchor id from the heading text.
- local tagname = to_heading_tag(hname)
- if node_name == 'h1' or #headings == 0 then
- ---@type nvim.gen_help_html.heading
- local heading = { name = hname, subheadings = {}, tag = tagname }
- headings[#headings + 1] = heading
- else
- table.insert(
- headings[#headings].subheadings,
- { name = hname, subheadings = {}, tag = tagname }
- )
- end
- local el = node_name == 'h1' and 'h2' or 'h3'
- return ('<%s id="%s" class="help-heading">%s</%s>\n'):format(el, tagname, trimmed, el)
- elseif node_name == 'heading' then
- return trimmed
- elseif node_name == 'column_heading' or node_name == 'column_name' then
- if root:has_error() then
- return text
- end
- return ('<div class="help-column_heading">%s</div>'):format(text)
- elseif node_name == 'block' then
- if is_blank(text) then
- return ''
- end
- if opt.old then
- -- XXX: Treat "old" docs as preformatted: they use indentation for layout.
- -- Trim trailing newlines to avoid too much whitespace between divs.
- return ('<div class="old-help-para">%s</div>\n'):format(trim(text, 2))
- end
- return string.format('<div class="help-para">\n%s\n</div>\n', text)
- elseif node_name == 'line' then
- if
- (parent ~= 'codeblock' or parent ~= 'code')
- and (is_blank(text) or is_noise(text, stats.noise_lines))
- then
- return '' -- Discard common "noise" lines.
- end
- -- XXX: Avoid newlines (too much whitespace) after block elements in old (preformatted) layout.
- local div = opt.old
- and root:child(0)
- and vim.list_contains({ 'column_heading', 'h1', 'h2', 'h3' }, root:child(0):type())
- return string.format('%s%s', div and trim(text) or text, div and '' or '\n')
- elseif node_name == 'line_li' then
- local sib = root:prev_sibling()
- local prev_li = sib and sib:type() == 'line_li'
-
- if not prev_li then
- opt.indent = 1
- else
- -- The previous listitem _sibling_ is _logically_ the _parent_ if it is indented less.
- local parent_indent = get_indent(node_text(sib))
- local this_indent = get_indent(node_text())
- if this_indent > parent_indent then
- opt.indent = opt.indent + 1
- elseif this_indent < parent_indent then
- opt.indent = math.max(1, opt.indent - 1)
- end
- end
- local margin = opt.indent == 1 and '' or ('margin-left: %drem;'):format((1.5 * opt.indent))
-
- return string.format('<div class="help-li" style="%s">%s</div>', margin, text)
- elseif node_name == 'taglink' or node_name == 'optionlink' then
- local helppage, tagname, ignored = validate_link(root, opt.buf, opt.fname)
- if ignored then
- return text
- end
- local s = ('%s<a href="%s#%s">%s</a>'):format(
- ws(),
- helppage,
- url_encode(tagname),
- html_esc(tagname)
- )
- if opt.old and node_name == 'taglink' then
- s = fix_tab_after_conceal(s, node_text(root:next_sibling()))
- end
- return s
- elseif vim.list_contains({ 'codespan', 'keycode' }, node_name) then
- if root:has_error() then
- return text
- end
- local s = ('%s<code>%s</code>'):format(ws(), trimmed)
- if opt.old and node_name == 'codespan' then
- s = fix_tab_after_conceal(s, node_text(root:next_sibling()))
- end
- return s
- elseif node_name == 'argument' then
- return ('%s<code>{%s}</code>'):format(ws(), text)
- elseif node_name == 'codeblock' then
- return text
- elseif node_name == 'language' then
- language = node_text(root)
- return ''
- elseif node_name == 'code' then -- Highlighted codeblock (child).
- if is_blank(text) then
- return ''
- end
- local code ---@type string
- if language then
- code = ('<pre><code class="language-%s">%s</code></pre>'):format(
- language,
- trim(trim_indent(text), 2)
- )
- language = nil
- else
- code = ('<pre>%s</pre>'):format(trim(trim_indent(text), 2))
- end
- return code
- elseif node_name == 'tag' then -- anchor, h4 pseudo-heading
- if root:has_error() then
- return text
- end
- local in_heading = vim.list_contains({ 'h1', 'h2', 'h3' }, parent)
- local h4 = not in_heading and not next_ and get_indent(node_text()) > 8 -- h4 pseudo-heading
- local cssclass = h4 and 'help-tag-right' or 'help-tag'
- local tagname = node_text(root:child(1), false)
- if vim.tbl_count(stats.first_tags) < 2 then
- -- Force the first 2 tags in the doc to be anchored at the main heading.
- table.insert(stats.first_tags, tagname)
- return ''
- end
- local el = 'span'
- local encoded_tagname = url_encode(tagname)
- local s = ('%s<%s id="%s" class="%s"><a href="#%s">%s</a></%s>'):format(
- ws(),
- el,
- encoded_tagname,
- cssclass,
- encoded_tagname,
- trimmed,
- el
- )
- if opt.old then
- s = fix_tab_after_conceal(s, node_text(root:next_sibling()))
- end
-
- if in_heading and prev ~= 'tag' then
- -- Start the <span> container for tags in a heading.
- -- This makes "justify-content:space-between" right-align the tags.
- -- <h2>foo bar<span>tag1 tag2</span></h2>
- return string.format('<span class="help-heading-tags">%s', s)
- elseif in_heading and next_ == nil then
- -- End the <span> container for tags in a heading.
- return string.format('%s</span>', s)
- end
- return s .. (h4 and '<br>' or '') -- HACK: <br> avoids h4 pseudo-heading mushing with text.
- elseif node_name == 'delimiter' or node_name == 'modeline' then
- return ''
- elseif node_name == 'ERROR' then
- if ignore_parse_error(opt.fname, trimmed) then
- return text
- end
-
- -- Store the raw text to give context to the bug report.
- local sample_text = level > 0 and getbuflinestr(root, opt.buf, 3) or '[top level!]'
- table.insert(stats.parse_errors, sample_text)
- return ('<a class="parse-error" target="_blank" title="Report bug... (parse error)" href="%s">%s</a>'):format(
- get_bug_url_vimdoc(opt.fname, opt.to_fname, sample_text),
- trimmed
- )
- else -- Unknown token.
- local sample_text = level > 0 and getbuflinestr(root, opt.buf, 3) or '[top level!]'
- return ('<a class="unknown-token" target="_blank" title="Report bug... (unhandled token "%s")" href="%s">%s</a>'):format(
- node_name,
- get_bug_url_nvim(opt.fname, opt.to_fname, sample_text, node_name),
- trimmed
- ),
- ('unknown-token:"%s"'):format(node_name)
- end
-end
-
---- @param dir string e.g. '$VIMRUNTIME/doc'
---- @param include string[]|nil
---- @return string[]
-local function get_helpfiles(dir, include)
- local rv = {}
- for f, type in vim.fs.dir(dir) do
- if
- vim.endswith(f, '.txt')
- and type == 'file'
- and (not include or vim.list_contains(include, f))
- then
- local fullpath = vim.fn.fnamemodify(('%s/%s'):format(dir, f), ':p')
- table.insert(rv, fullpath)
- end
- end
- return rv
-end
-
---- Populates the helptags map.
-local function get_helptags(help_dir)
- local m = {}
- -- Load a random help file to convince taglist() to do its job.
- vim.cmd(string.format('split %s/api.txt', help_dir))
- vim.cmd('lcd %:p:h')
- for _, item in ipairs(vim.fn.taglist('.*')) do
- if vim.endswith(item.filename, '.txt') then
- m[item.name] = item.filename
- end
- end
- vim.cmd('q!')
- return m
-end
-
---- Use the vimdoc parser defined in the build, not whatever happens to be installed on the system.
-local function ensure_runtimepath()
- if not vim.o.runtimepath:find('build/lib/nvim/') then
- vim.cmd [[set runtimepath^=./build/lib/nvim/]]
- end
-end
-
---- Opens `fname` (or `text`, if given) in a buffer and gets a treesitter parser for the buffer contents.
----
---- @param fname string :help file to parse
---- @param text string? :help file contents
---- @param parser_path string? path to non-default vimdoc.so
---- @return vim.treesitter.LanguageTree, integer (lang_tree, bufnr)
-local function parse_buf(fname, text, parser_path)
- local buf ---@type integer
- if text then
- vim.cmd('split new') -- Text contents.
- vim.api.nvim_put(vim.split(text, '\n'), '', false, false)
- vim.cmd('setfiletype help')
- -- vim.treesitter.language.add('vimdoc')
- buf = vim.api.nvim_get_current_buf()
- elseif type(fname) == 'string' then
- vim.cmd('split ' .. vim.fn.fnameescape(fname)) -- Filename.
- buf = vim.api.nvim_get_current_buf()
- else
- -- Left for debugging
- ---@diagnostic disable-next-line: no-unknown
- buf = fname
- vim.cmd('sbuffer ' .. tostring(fname)) -- Buffer number.
- end
- if parser_path then
- vim.treesitter.language.add('vimdoc', { path = parser_path })
- end
- local lang_tree = assert(vim.treesitter.get_parser(buf, nil, { error = false }))
- return lang_tree, buf
-end
-
---- Validates one :help file `fname`:
---- - checks that |tag| links point to valid helptags.
---- - recursively counts parse errors ("ERROR" nodes)
----
---- @param fname string help file to validate
---- @param parser_path string? path to non-default vimdoc.so
---- @return { invalid_links: number, parse_errors: string[] }
-local function validate_one(fname, parser_path)
- local stats = {
- parse_errors = {},
- }
- local lang_tree, buf = parse_buf(fname, nil, parser_path)
- for _, tree in ipairs(lang_tree:trees()) do
- visit_validate(tree:root(), 0, tree, { buf = buf, fname = fname }, stats)
- end
- lang_tree:destroy()
- vim.cmd.close()
- return stats
-end
-
---- Generates HTML from one :help file `fname` and writes the result to `to_fname`.
----
---- @param fname string Source :help file.
---- @param text string|nil Source :help file contents, or nil to read `fname`.
---- @param to_fname string Destination .html file
---- @param old boolean Preformat paragraphs (for old :help files which are full of arbitrary whitespace)
---- @param parser_path string? path to non-default vimdoc.so
----
---- @return string html
---- @return table stats
-local function gen_one(fname, text, to_fname, old, commit, parser_path)
- local stats = {
- noise_lines = {},
- parse_errors = {},
- first_tags = {}, -- Track the first few tags in doc.
- }
- local lang_tree, buf = parse_buf(fname, text, parser_path)
- ---@type nvim.gen_help_html.heading[]
- local headings = {} -- Headings (for ToC). 2-dimensional: h1 contains h2/h3.
- local title = to_titlecase(basename_noext(fname))
-
- local html = ([[
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta name="description" content="Neovim user documentation">
-
- <!-- algolia docsearch https://docsearch.algolia.com/docs/docsearch-v3/ -->
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@docsearch/css@3" />
- <link rel="preconnect" href="https://X185E15FPG-dsn.algolia.net" crossorigin />
-
- <link href="/css/bootstrap.min.css" rel="stylesheet">
- <link href="/css/main.css" rel="stylesheet">
- <link href="help.css" rel="stylesheet">
- <link href="/highlight/styles/neovim.min.css" rel="stylesheet">
-
- <script src="/highlight/highlight.min.js"></script>
- <script>hljs.highlightAll();</script>
- <title>%s - Neovim docs</title>
- </head>
- <body>
- ]]):format(title)
-
- local logo_svg = [[
- <svg xmlns="http://www.w3.org/2000/svg" role="img" width="173" height="50" viewBox="0 0 742 214" aria-label="Neovim">
- <title>Neovim</title>
- <defs>
- <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a">
- <stop stop-color="#16B0ED" stop-opacity=".8" offset="0%" />
- <stop stop-color="#0F59B2" stop-opacity=".837" offset="100%" />
- </linearGradient>
- <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="b">
- <stop stop-color="#7DB643" offset="0%" />
- <stop stop-color="#367533" offset="100%" />
- </linearGradient>
- <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="c">
- <stop stop-color="#88C649" stop-opacity=".8" offset="0%" />
- <stop stop-color="#439240" stop-opacity=".84" offset="100%" />
- </linearGradient>
- </defs>
- <g fill="none" fill-rule="evenodd">
- <path
- d="M.027 45.459L45.224-.173v212.171L.027 166.894V45.459z"
- fill="url(#a)"
- transform="translate(1 1)"
- />
- <path
- d="M129.337 45.89L175.152-.149l-.928 212.146-45.197-45.104.31-121.005z"
- fill="url(#b)"
- transform="matrix(-1 0 0 1 305 1)"
- />
- <path
- d="M45.194-.137L162.7 179.173l-32.882 32.881L12.25 33.141 45.194-.137z"
- fill="url(#c)"
- transform="translate(1 1)"
- />
- <path
- d="M46.234 84.032l-.063 7.063-36.28-53.563 3.36-3.422 32.983 49.922z"
- fill-opacity=".13"
- fill="#000"
- />
- <g fill="#444">
- <path
- d="M227 154V64.44h4.655c1.55 0 2.445.75 2.685 2.25l.806 13.502c4.058-5.16 8.786-9.316 14.188-12.466 5.4-3.15 11.413-4.726 18.037-4.726 4.893 0 9.205.781 12.935 2.34 3.729 1.561 6.817 3.811 9.264 6.751 2.448 2.942 4.297 6.48 5.55 10.621 1.253 4.14 1.88 8.821 1.88 14.042V154h-8.504V96.754c0-8.402-1.91-14.987-5.729-19.757-3.82-4.771-9.667-7.156-17.544-7.156-5.851 0-11.28 1.516-16.292 4.545-5.013 3.032-9.489 7.187-13.427 12.467V154H227zM350.624 63c5.066 0 9.755.868 14.069 2.605 4.312 1.738 8.052 4.268 11.219 7.592s5.638 7.412 7.419 12.264C385.11 90.313 386 95.883 386 102.17c0 1.318-.195 2.216-.588 2.696-.393.48-1.01.719-1.851.719h-64.966v1.70c0 6.708.784 12.609 2.353 17.7 1.567 5.09 3.8 9.357 6.695 12.802 2.895 3.445 6.393 6.034 10.495 7.771 4.1 1.738 8.686 2.606 13.752 2.606 4.524 0 8.446-.494 11.762-1.483 3.317-.988 6.108-2.097 8.37-3.324 2.261-1.227 4.056-2.336 5.383-3.324 1.326-.988 2.292-1.482 2.895-1.482.784 0 1.388.3 1.81.898l2.352 2.875c-1.448 1.797-3.362 3.475-5.745 5.031-2.383 1.558-5.038 2.891-7.962 3.998-2.926 1.109-6.062 1.991-9.41 2.65a52.21 52.21 0 01-10.088.989c-6.152 0-11.762-1.064-16.828-3.19-5.067-2.125-9.415-5.225-13.043-9.298-3.63-4.074-6.435-9.06-8.415-14.96C310.99 121.655 310 114.9 310 107.294c0-6.408.92-12.323 2.76-17.744 1.84-5.421 4.493-10.093 7.961-14.016 3.467-3.922 7.72-6.991 12.758-9.209C338.513 64.11 344.229 63 350.624 63zm.573 6c-4.696 0-8.904.702-12.623 2.105-3.721 1.404-6.936 3.421-9.65 6.053-2.713 2.631-4.908 5.79-6.586 9.474S319.55 94.439 319 99h60c0-4.679-.672-8.874-2.013-12.588-1.343-3.712-3.232-6.856-5.67-9.43-2.44-2.571-5.367-4.545-8.782-5.92-3.413-1.374-7.192-2.062-11.338-2.062zM435.546 63c6.526 0 12.368 1.093 17.524 3.28 5.154 2.186 9.5 5.286 13.04 9.298 3.538 4.013 6.238 8.85 8.099 14.51 1.861 5.66 2.791 11.994 2.791 19.002 0 7.008-.932 13.327-2.791 18.957-1.861 5.631-4.561 10.452-8.099 14.465-3.54 4.012-7.886 7.097-13.04 9.254-5.156 2.156-10.998 3.234-17.524 3.234-6.529 0-12.369-1.078-17.525-3.234-5.155-2.157-9.517-5.242-13.085-9.254-3.57-4.013-6.285-8.836-8.145-14.465-1.861-5.63-2.791-11.95-2.791-18.957 0-7.008.93-13.342 2.791-19.002 1.861-5.66 4.576-10.496 8.145-14.51 3.568-4.012 7.93-7.112 13.085-9.299C423.177 64.094 429.017 63 435.546 63zm-.501 86c5.341 0 10.006-.918 13.997-2.757 3.99-1.838 7.32-4.474 9.992-7.909 2.67-3.435 4.664-7.576 5.986-12.428 1.317-4.85 1.98-10.288 1.98-16.316 0-5.965-.66-11.389-1.98-16.27-1.322-4.88-3.316-9.053-5.986-12.519-2.67-3.463-6-6.13-9.992-7.999-3.991-1.867-8.657-2.802-13.997-2.802s-10.008.935-13.997 2.802c-3.991 1.87-7.322 4.536-9.992 8-2.671 3.465-4.68 7.637-6.03 12.518-1.35 4.881-2.026 10.305-2.026 16.27 0 6.026.675 11.465 2.025 16.316 1.35 4.852 3.36 8.993 6.031 12.428 2.67 3.435 6 6.07 9.992 7.91 3.99 1.838 8.656 2.756 13.997 2.756z"
- fill="currentColor"
- />
- <path
- d="M530.57 152h-20.05L474 60h18.35c1.61 0 2.967.39 4.072 1.166 1.103.778 1.865 1.763 2.283 2.959l17.722 49.138a92.762 92.762 0 012.551 8.429c.686 2.751 1.298 5.5 1.835 8.25.537-2.75 1.148-5.499 1.835-8.25a77.713 77.713 0 012.64-8.429l18.171-49.138c.417-1.196 1.164-2.181 2.238-2.96 1.074-.776 2.356-1.165 3.849-1.165H567l-36.43 92zM572 61h23v92h-23zM610 153V60.443h13.624c2.887 0 4.78 1.354 5.682 4.06l1.443 6.856a52.7 52.7 0 015.097-4.962 32.732 32.732 0 015.683-3.879 30.731 30.731 0 016.496-2.57c2.314-.632 4.855-.948 7.624-.948 5.832 0 10.63 1.579 14.39 4.736 3.758 3.157 6.57 7.352 8.434 12.585 1.444-3.068 3.248-5.698 5.413-7.894 2.165-2.194 4.541-3.984 7.127-5.367a32.848 32.848 0 018.254-3.068 39.597 39.597 0 018.796-.992c5.111 0 9.653.783 13.622 2.345 3.97 1.565 7.307 3.849 10.014 6.857 2.706 3.007 4.766 6.675 6.18 11.005C739.29 83.537 740 88.5 740 94.092V153h-22.284V94.092c0-5.894-1.294-10.329-3.878-13.306-2.587-2.977-6.376-4.465-11.368-4.465-2.286 0-4.404.391-6.358 1.172a15.189 15.189 0 00-5.144 3.383c-1.473 1.474-2.631 3.324-3.474 5.548-.842 2.225-1.263 4.781-1.263 7.668V153h-22.37V94.092c0-6.194-1.249-10.704-3.744-13.532-2.497-2.825-6.18-4.24-11.051-4.24-3.19 0-6.18.798-8.976 2.391-2.799 1.593-5.399 3.775-7.804 6.54V153H610zM572 30h23v19h-23z"
- fill="currentColor"
- fill-opacity=".8"
- />
- </g>
- </g>
- </svg>
- ]]
-
- local main = ''
- for _, tree in ipairs(lang_tree:trees()) do
- main = main
- .. (
- visit_node(
- tree:root(),
- 0,
- tree,
- headings,
- { buf = buf, old = old, fname = fname, to_fname = to_fname, indent = 1 },
- stats
- )
- )
- end
-
- main = ([[
- <header class="container">
- <nav class="navbar navbar-expand-lg">
- <div class="container-fluid">
- <a href="/" class="navbar-brand" aria-label="logo">
- <!--TODO: use <img src="….svg"> here instead. Need one that has green lettering instead of gray. -->
- %s
- <!--<img src="https://neovim.io/logos/neovim-logo.svg" width="173" height="50" alt="Neovim" />-->
- </a>
- <div id="docsearch"></div> <!-- algolia docsearch https://docsearch.algolia.com/docs/docsearch-v3/ -->
- </div>
- </nav>
- </header>
-
- <div class="container golden-grid help-body">
- <div class="col-wide">
- <a name="%s" href="#%s"><h1 id="%s">%s</h1></a>
- <p>
- <i>
- Nvim <code>:help</code> pages, <a href="https://github.com/neovim/neovim/blob/master/scripts/gen_help_html.lua">generated</a>
- from <a href="https://github.com/neovim/neovim/blob/master/runtime/doc/%s">source</a>
- using the <a href="https://github.com/neovim/tree-sitter-vimdoc">tree-sitter-vimdoc</a> parser.
- </i>
- </p>
- <hr/>
- %s
- </div>
- ]]):format(
- logo_svg,
- stats.first_tags[1] or '',
- stats.first_tags[2] or '',
- stats.first_tags[2] or '',
- title,
- vim.fs.basename(fname),
- main
- )
-
- ---@type string
- local toc = [[
- <div class="col-narrow toc">
- <div><a href="index.html">Main</a></div>
- <div><a href="vimindex.html">Commands index</a></div>
- <div><a href="quickref.html">Quick reference</a></div>
- <hr/>
- ]]
-
- local n = 0 -- Count of all headings + subheadings.
- for _, h1 in ipairs(headings) do
- n = n + 1 + #h1.subheadings
- end
- for _, h1 in ipairs(headings) do
- ---@type string
- toc = toc .. ('<div class="help-toc-h1"><a href="#%s">%s</a>\n'):format(h1.tag, h1.name)
- if n < 30 or #headings < 10 then -- Show subheadings only if there aren't too many.
- for _, h2 in ipairs(h1.subheadings) do
- toc = toc
- .. ('<div class="help-toc-h2"><a href="#%s">%s</a></div>\n'):format(h2.tag, h2.name)
- end
- end
- toc = toc .. '</div>'
- end
- toc = toc .. '</div>\n'
-
- local bug_url = get_bug_url_nvim(fname, to_fname, 'TODO', nil)
- local bug_link = string.format('(<a href="%s" target="_blank">report docs bug...</a>)', bug_url)
-
- local footer = ([[
- <footer>
- <div class="container flex">
- <div class="generator-stats">
- Generated at %s from <code><a href="https://github.com/neovim/neovim/commit/%s">%s</a></code>
- </div>
- <div class="generator-stats">
- parse_errors: %d %s | <span title="%s">noise_lines: %d</span>
- </div>
- <div>
-
- <!-- algolia docsearch https://docsearch.algolia.com/docs/docsearch-v3/ -->
- <script src="https://cdn.jsdelivr.net/npm/@docsearch/js@3"></script>
- <script type="module">
- docsearch({
- container: '#docsearch',
- appId: 'X185E15FPG',
- apiKey: 'b5e6b2f9c636b2b471303205e59832ed',
- indexName: 'nvim',
- });
- </script>
-
- </footer>
- ]]):format(
- os.date('%Y-%m-%d %H:%M'),
- commit,
- commit:sub(1, 7),
- #stats.parse_errors,
- bug_link,
- html_esc(table.concat(stats.noise_lines, '\n')),
- #stats.noise_lines
- )
-
- html = ('%s%s%s</div>\n%s</body>\n</html>\n'):format(html, main, toc, footer)
- vim.cmd('q!')
- lang_tree:destroy()
- return html, stats
-end
-
-local function gen_css(fname)
- local css = [[
- :root {
- --code-color: #004b4b;
- --tag-color: #095943;
- }
- @media (prefers-color-scheme: dark) {
- :root {
- --code-color: #00c243;
- --tag-color: #00b7b7;
- }
- }
- @media (min-width: 40em) {
- .toc {
- position: fixed;
- left: 67%;
- }
- .golden-grid {
- display: grid;
- grid-template-columns: 65% auto;
- grid-gap: 1em;
- }
- }
- @media (max-width: 40em) {
- .golden-grid {
- /* Disable grid for narrow viewport (mobile phone). */
- display: block;
- }
- }
- .toc {
- /* max-width: 12rem; */
- height: 85%; /* Scroll if there are too many items. https://github.com/neovim/neovim.github.io/issues/297 */
- overflow: auto; /* Scroll if there are too many items. https://github.com/neovim/neovim.github.io/issues/297 */
- }
- .toc > div {
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- }
- html {
- scroll-behavior: auto;
- }
- body {
- font-size: 18px;
- line-height: 1.5;
- }
- h1, h2, h3, h4, h5 {
- font-family: sans-serif;
- border-bottom: 1px solid var(--tag-color); /*rgba(0, 0, 0, .9);*/
- }
- h3, h4, h5 {
- border-bottom-style: dashed;
- }
- .help-column_heading {
- color: var(--code-color);
- }
- .help-body {
- padding-bottom: 2em;
- }
- .help-line {
- /* font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; */
- }
- .help-li {
- white-space: normal;
- display: list-item;
- margin-left: 1.5rem; /* padding-left: 1rem; */
- }
- .help-para {
- padding-top: 10px;
- padding-bottom: 10px;
- }
-
- .old-help-para {
- padding-top: 10px;
- padding-bottom: 10px;
- /* Tabs are used for alignment in old docs, so we must match Vim's 8-char expectation. */
- tab-size: 8;
- white-space: pre-wrap;
- font-size: 16px;
- font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
- word-wrap: break-word;
- }
- .old-help-para pre, .old-help-para pre:hover {
- /* Text following <pre> is already visually separated by the linebreak. */
- margin-bottom: 0;
- /* Long lines that exceed the textwidth should not be wrapped (no "pre-wrap").
- Since text may overflow horizontally, we make the contents to be scrollable
- (only if necessary) to prevent overlapping with the navigation bar at the right. */
- white-space: pre;
- overflow-x: auto;
- }
-
- /* TODO: should this rule be deleted? help tags are rendered as <code> or <span>, not <a> */
- a.help-tag, a.help-tag:focus, a.help-tag:hover {
- color: inherit;
- text-decoration: none;
- }
- .help-tag {
- color: var(--tag-color);
- }
- /* Tag pseudo-header common in :help docs. */
- .help-tag-right {
- color: var(--tag-color);
- margin-left: auto;
- margin-right: 0;
- float: right;
- display: block;
- }
- .help-tag a,
- .help-tag-right a {
- color: inherit;
- }
- .help-tag a:not(:hover),
- .help-tag-right a:not(:hover) {
- text-decoration: none;
- }
- h1 .help-tag, h2 .help-tag, h3 .help-tag {
- font-size: smaller;
- }
- .help-heading {
- white-space: normal;
- display: flex;
- flex-flow: row wrap;
- justify-content: space-between;
- gap: 0 15px;
- }
- /* The (right-aligned) "tags" part of a section heading. */
- .help-heading-tags {
- margin-right: 10px;
- }
- .help-toc-h1 {
- }
- .help-toc-h2 {
- margin-left: 1em;
- }
- .parse-error {
- background-color: red;
- }
- .unknown-token {
- color: black;
- background-color: yellow;
- }
- code {
- color: var(--code-color);
- font-size: 16px;
- }
- pre {
- /* Tabs are used in codeblocks only for indentation, not alignment, so we can aggressively shrink them. */
- tab-size: 2;
- white-space: pre-wrap;
- line-height: 1.3; /* Important for ascii art. */
- overflow: visible;
- /* font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; */
- font-size: 16px;
- margin-top: 10px;
- }
- pre:last-child {
- margin-bottom: 0;
- }
- pre:hover {
- overflow: visible;
- }
- .generator-stats {
- color: gray;
- font-size: smaller;
- }
- ]]
- tofile(fname, css)
-end
-
--- Testing
-
-local function ok(cond, expected, actual, message)
- assert(
- (not expected and not actual) or (expected and actual),
- 'if "expected" is given, "actual" is also required'
- )
- if expected then
- assert(
- cond,
- ('%sexpected %s, got: %s'):format(
- message and (message .. '\n') or '',
- vim.inspect(expected),
- vim.inspect(actual)
- )
- )
- return cond
- else
- return assert(cond)
- end
-end
-local function eq(expected, actual, message)
- return ok(vim.deep_equal(expected, actual), expected, actual, message)
-end
-
-function M._test()
- tagmap = get_helptags('$VIMRUNTIME/doc')
- helpfiles = get_helpfiles(vim.fs.normalize('$VIMRUNTIME/doc'))
-
- ok(vim.tbl_count(tagmap) > 3000, '>3000', vim.tbl_count(tagmap))
- ok(
- vim.endswith(tagmap['vim.diagnostic.set()'], 'diagnostic.txt'),
- tagmap['vim.diagnostic.set()'],
- 'diagnostic.txt'
- )
- ok(vim.endswith(tagmap['%:s'], 'cmdline.txt'), tagmap['%:s'], 'cmdline.txt')
- ok(is_noise([[vim:tw=78:isk=!-~,^*,^\|,^\":ts=8:noet:ft=help:norl:]]))
- ok(is_noise([[ NVIM REFERENCE MANUAL by Thiago de Arruda ]]))
- ok(not is_noise([[vim:tw=78]]))
-
- eq(0, get_indent('a'))
- eq(1, get_indent(' a'))
- eq(2, get_indent(' a\n b\n c\n'))
- eq(5, get_indent(' a\n \n b\n c\n d\n e\n'))
- eq(
- 'a\n \n b\n c\n d\n e\n',
- trim_indent(' a\n \n b\n c\n d\n e\n')
- )
-
- local fixed_url, removed_chars = fix_url('https://example.com).')
- eq('https://example.com', fixed_url)
- eq(').', removed_chars)
- fixed_url, removed_chars = fix_url('https://example.com.)')
- eq('https://example.com.', fixed_url)
- eq(')', removed_chars)
- fixed_url, removed_chars = fix_url('https://example.com.')
- eq('https://example.com', fixed_url)
- eq('.', removed_chars)
- fixed_url, removed_chars = fix_url('https://example.com)')
- eq('https://example.com', fixed_url)
- eq(')', removed_chars)
- fixed_url, removed_chars = fix_url('https://example.com')
- eq('https://example.com', fixed_url)
- eq('', removed_chars)
-
- print('all tests passed.\n')
-end
-
---- @class nvim.gen_help_html.gen_result
---- @field helpfiles string[] list of generated HTML files, from the source docs {include}
---- @field err_count integer number of parse errors in :help docs
---- @field invalid_links table<string, any>
-
---- Generates HTML from :help docs located in `help_dir` and writes the result in `to_dir`.
----
---- Example:
----
---- gen('$VIMRUNTIME/doc', '/path/to/neovim.github.io/_site/doc/', {'api.txt', 'autocmd.txt', 'channel.txt'}, nil)
----
---- @param help_dir string Source directory containing the :help files. Must run `make helptags` first.
---- @param to_dir string Target directory where the .html files will be written.
---- @param include string[]|nil Process only these filenames. Example: {'api.txt', 'autocmd.txt', 'channel.txt'}
----
---- @return nvim.gen_help_html.gen_result result
-function M.gen(help_dir, to_dir, include, commit, parser_path)
- vim.validate('help_dir', help_dir, function(d)
- return vim.fn.isdirectory(vim.fs.normalize(d)) == 1
- end, 'valid directory')
- vim.validate('to_dir', to_dir, 'string')
- vim.validate('include', include, 'table', true)
- vim.validate('commit', commit, 'string', true)
- vim.validate('parser_path', parser_path, function(f)
- return vim.fn.filereadable(vim.fs.normalize(f)) == 1
- end, true, 'valid vimdoc.{so,dll} filepath')
-
- local err_count = 0
- local redirects_count = 0
- ensure_runtimepath()
- tagmap = get_helptags(vim.fs.normalize(help_dir))
- helpfiles = get_helpfiles(help_dir, include)
- to_dir = vim.fs.normalize(to_dir)
- parser_path = parser_path and vim.fs.normalize(parser_path) or nil
-
- print(('output dir: %s\n\n'):format(to_dir))
- vim.fn.mkdir(to_dir, 'p')
- gen_css(('%s/help.css'):format(to_dir))
-
- for _, f in ipairs(helpfiles) do
- -- "foo.txt"
- local helpfile = vim.fs.basename(f)
- -- "to/dir/foo.html"
- local to_fname = ('%s/%s'):format(to_dir, get_helppage(helpfile))
- local html, stats =
- gen_one(f, nil, to_fname, not new_layout[helpfile], commit or '?', parser_path)
- tofile(to_fname, html)
- print(
- ('generated (%-2s errors): %-15s => %s'):format(
- #stats.parse_errors,
- helpfile,
- vim.fs.basename(to_fname)
- )
- )
-
- -- Generate redirect pages for renamed help files.
- local helpfile_tag = (helpfile:gsub('%.txt$', ''))
- local redirect_from = redirects[helpfile_tag]
- if redirect_from then
- local redirect_text = ([[
-*%s* Nvim
-
-This document moved to: |%s|
-
-==============================================================================
-This document moved to: |%s|
-
-This document moved to: |%s|
-
-==============================================================================
- vim:tw=78:ts=8:ft=help:norl:
- ]]):format(
- redirect_from,
- helpfile_tag,
- helpfile_tag,
- helpfile_tag,
- helpfile_tag,
- helpfile_tag
- )
- local redirect_to = ('%s/%s'):format(to_dir, get_helppage(redirect_from))
- local redirect_html, _ =
- gen_one(redirect_from, redirect_text, redirect_to, false, commit or '?', parser_path)
- assert(redirect_html:find(helpfile_tag))
- tofile(redirect_to, redirect_html)
-
- print(
- ('generated (redirect) : %-15s => %s'):format(
- redirect_from .. '.txt',
- vim.fs.basename(to_fname)
- )
- )
- redirects_count = redirects_count + 1
- end
-
- err_count = err_count + #stats.parse_errors
- end
-
- print(('\ngenerated %d html pages'):format(#helpfiles + redirects_count))
- print(('total errors: %d'):format(err_count))
- print(('invalid tags: %s'):format(vim.inspect(invalid_links)))
- assert(#(include or {}) > 0 or redirects_count == vim.tbl_count(redirects)) -- sanity check
- print(('redirects: %d'):format(redirects_count))
- print('\n')
-
- --- @type nvim.gen_help_html.gen_result
- return {
- helpfiles = helpfiles,
- err_count = err_count,
- invalid_links = invalid_links,
- }
-end
-
---- @class nvim.gen_help_html.validate_result
---- @field helpfiles integer number of generated helpfiles
---- @field err_count integer number of parse errors
---- @field parse_errors table<string, string[]>
---- @field invalid_links table<string, any> invalid tags in :help docs
---- @field invalid_urls table<string, any> invalid URLs in :help docs
---- @field invalid_spelling table<string, table<string, string>> invalid spelling in :help docs
-
---- Validates all :help files found in `help_dir`:
---- - checks that |tag| links point to valid helptags.
---- - recursively counts parse errors ("ERROR" nodes)
----
---- This is 10x faster than gen(), for use in CI.
----
---- @return nvim.gen_help_html.validate_result result
-function M.validate(help_dir, include, parser_path)
- vim.validate('help_dir', help_dir, function(d)
- return vim.fn.isdirectory(vim.fs.normalize(d)) == 1
- end, 'valid directory')
- vim.validate('include', include, 'table', true)
- vim.validate('parser_path', parser_path, function(f)
- return vim.fn.filereadable(vim.fs.normalize(f)) == 1
- end, true, 'valid vimdoc.{so,dll} filepath')
- local err_count = 0 ---@type integer
- local files_to_errors = {} ---@type table<string, string[]>
- ensure_runtimepath()
- tagmap = get_helptags(vim.fs.normalize(help_dir))
- helpfiles = get_helpfiles(help_dir, include)
- parser_path = parser_path and vim.fs.normalize(parser_path) or nil
-
- for _, f in ipairs(helpfiles) do
- local helpfile = vim.fs.basename(f)
- local rv = validate_one(f, parser_path)
- print(('validated (%-4s errors): %s'):format(#rv.parse_errors, helpfile))
- if #rv.parse_errors > 0 then
- files_to_errors[helpfile] = rv.parse_errors
- vim.print(('%s'):format(vim.iter(rv.parse_errors):fold('', function(s, v)
- return s .. '\n ' .. v
- end)))
- end
- err_count = err_count + #rv.parse_errors
- end
-
- ---@type nvim.gen_help_html.validate_result
- return {
- helpfiles = #helpfiles,
- err_count = err_count,
- parse_errors = files_to_errors,
- invalid_links = invalid_links,
- invalid_urls = invalid_urls,
- invalid_spelling = invalid_spelling,
- }
-end
-
---- Validates vimdoc files on $VIMRUNTIME. and print human-readable error messages if fails.
----
---- If this fails, try these steps (in order):
---- 1. Fix/cleanup the :help docs.
---- 2. Fix the parser: https://github.com/neovim/tree-sitter-vimdoc
---- 3. File a parser bug, and adjust the tolerance of this test in the meantime.
----
---- @param help_dir? string e.g. '$VIMRUNTIME/doc' or './runtime/doc'
-function M.run_validate(help_dir)
- help_dir = vim.fs.normalize(help_dir or '$VIMRUNTIME/doc')
- print('doc path = ' .. vim.uv.fs_realpath(help_dir))
-
- local rv = M.validate(help_dir)
-
- -- Check that we actually found helpfiles.
- ok(rv.helpfiles > 100, '>100 :help files', rv.helpfiles)
-
- eq({}, rv.parse_errors, 'no parse errors')
- eq(0, rv.err_count, 'no parse errors')
- eq({}, rv.invalid_links, 'invalid tags in :help docs')
- eq({}, rv.invalid_urls, 'invalid URLs in :help docs')
- eq(
- {},
- rv.invalid_spelling,
- 'invalid spelling in :help docs (see spell_dict in scripts/gen_help_html.lua)'
- )
-end
-
---- Test-generates HTML from docs.
----
---- 1. Test that gen_help_html.lua actually works.
---- 2. Test that parse errors did not increase wildly. Because we explicitly test only a few
---- :help files, we can be precise about the tolerances here.
---- @param help_dir? string e.g. '$VIMRUNTIME/doc' or './runtime/doc'
-function M.test_gen(help_dir)
- local tmpdir = vim.fs.dirname(vim.fn.tempname())
- help_dir = vim.fs.normalize(help_dir or '$VIMRUNTIME/doc')
- print('doc path = ' .. vim.uv.fs_realpath(help_dir))
-
- -- Because gen() is slow (~30s), this test is limited to a few files.
- local input = { 'help.txt', 'index.txt', 'nvim.txt' }
- local rv = M.gen(help_dir, tmpdir, input)
- eq(#input, #rv.helpfiles)
- eq(0, rv.err_count, 'parse errors in :help docs')
- eq({}, rv.invalid_links, 'invalid tags in :help docs')
-end
-
-return M
diff --git a/scripts/gen_lsp.lua b/scripts/gen_lsp.lua
deleted file mode 100644
index 3e419c7d59..0000000000
--- a/scripts/gen_lsp.lua
+++ /dev/null
@@ -1,514 +0,0 @@
--- Generates lua-ls annotations for lsp.
-
-local USAGE = [[
-Generates lua-ls annotations for lsp.
-
-USAGE:
-nvim -l scripts/gen_lsp.lua gen # by default, this will overwrite runtime/lua/vim/lsp/_meta/protocol.lua
-nvim -l scripts/gen_lsp.lua gen --version 3.18 --out runtime/lua/vim/lsp/_meta/protocol.lua
-nvim -l scripts/gen_lsp.lua gen --version 3.18 --methods --capabilities
-]]
-
-local DEFAULT_LSP_VERSION = '3.18'
-
-local M = {}
-
-local function tofile(fname, text)
- local f = io.open(fname, 'w')
- if not f then
- error(('failed to write: %s'):format(f))
- else
- print(('Written to: %s'):format(fname))
- f:write(text)
- f:close()
- end
-end
-
---- The LSP protocol JSON data (it's partial, non-exhaustive).
---- https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/3.18/metaModel/metaModel.schema.json
---- @class vim._gen_lsp.Protocol
---- @field requests vim._gen_lsp.Request[]
---- @field notifications vim._gen_lsp.Notification[]
---- @field structures vim._gen_lsp.Structure[]
---- @field enumerations vim._gen_lsp.Enumeration[]
---- @field typeAliases vim._gen_lsp.TypeAlias[]
-
----@param opt vim._gen_lsp.opt
----@return vim._gen_lsp.Protocol
-local function read_json(opt)
- local uri = 'https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/'
- .. opt.version
- .. '/metaModel/metaModel.json'
- print('Reading ' .. uri)
-
- local res = vim.system({ 'curl', '--no-progress-meter', uri, '-o', '-' }):wait()
- if res.code ~= 0 or (res.stdout or ''):len() < 999 then
- print(('URL failed: %s'):format(uri))
- vim.print(res)
- error(res.stdout)
- end
- return vim.json.decode(res.stdout)
-end
-
--- Gets the Lua symbol for a given fully-qualified LSP method name.
-local function to_luaname(s)
- -- "$/" prefix is special: https://microsoft.github.io/language-server-protocol/specification/#dollarRequests
- return s:gsub('^%$', 'dollar'):gsub('/', '_')
-end
-
----@param protocol vim._gen_lsp.Protocol
----@param gen_methods boolean
----@param gen_capabilities boolean
-local function write_to_protocol(protocol, gen_methods, gen_capabilities)
- if not gen_methods and not gen_capabilities then
- return
- end
-
- local indent = (' '):rep(2)
-
- --- @class vim._gen_lsp.Request
- --- @field deprecated? string
- --- @field documentation? string
- --- @field messageDirection string
- --- @field clientCapability? string
- --- @field serverCapability? string
- --- @field method string
- --- @field params? any
- --- @field proposed? boolean
- --- @field registrationMethod? string
- --- @field registrationOptions? any
- --- @field since? string
-
- --- @class vim._gen_lsp.Notification
- --- @field deprecated? string
- --- @field documentation? string
- --- @field errorData? any
- --- @field messageDirection string
- --- @field clientCapability? string
- --- @field serverCapability? string
- --- @field method string
- --- @field params? any[]
- --- @field partialResult? any
- --- @field proposed? boolean
- --- @field registrationMethod? string
- --- @field registrationOptions? any
- --- @field result any
- --- @field since? string
-
- ---@type (vim._gen_lsp.Request|vim._gen_lsp.Notification)[]
- local all = vim.list_extend(protocol.requests, protocol.notifications)
- table.sort(all, function(a, b)
- return to_luaname(a.method) < to_luaname(b.method)
- end)
-
- local output = { '-- Generated by gen_lsp.lua, keep at end of file.' }
-
- if gen_methods then
- output[#output + 1] = '--- @alias vim.lsp.protocol.Method.ClientToServer'
-
- for _, item in ipairs(all) do
- if item.method and item.messageDirection == 'clientToServer' then
- output[#output + 1] = ("--- | '%s',"):format(item.method)
- end
- end
-
- vim.list_extend(output, {
- '',
- '--- @alias vim.lsp.protocol.Method.ServerToClient',
- })
- for _, item in ipairs(all) do
- if item.method and item.messageDirection == 'serverToClient' then
- output[#output + 1] = ("--- | '%s',"):format(item.method)
- end
- end
-
- vim.list_extend(output, {
- '',
- '--- @alias vim.lsp.protocol.Method',
- '--- | vim.lsp.protocol.Method.ClientToServer',
- '--- | vim.lsp.protocol.Method.ServerToClient',
- '',
- '-- Generated by gen_lsp.lua, keep at end of file.',
- '--- @enum vim.lsp.protocol.Methods',
- '--- @see https://microsoft.github.io/language-server-protocol/specification/#metaModel',
- '--- LSP method names.',
- 'protocol.Methods = {',
- })
-
- for _, item in ipairs(all) do
- if item.method then
- if item.documentation then
- local document = vim.split(item.documentation, '\n?\n', { trimempty = true })
- for _, docstring in ipairs(document) do
- output[#output + 1] = indent .. '--- ' .. docstring
- end
- end
- output[#output + 1] = ("%s%s = '%s',"):format(indent, to_luaname(item.method), item.method)
- end
- end
- output[#output + 1] = '}'
- end
-
- if gen_capabilities then
- vim.list_extend(output, {
- '',
- '-- stylua: ignore start',
- '-- Generated by gen_lsp.lua, keep at end of file.',
- '--- Maps method names to the required server capability',
- 'protocol._request_name_to_capability = {',
- })
-
- for _, item in ipairs(all) do
- if item.serverCapability then
- output[#output + 1] = ("%s['%s'] = { %s },"):format(
- indent,
- item.method,
- table.concat(
- vim
- .iter(vim.split(item.serverCapability, '.', { plain = true }))
- :map(function(segment)
- return "'" .. segment .. "'"
- end)
- :totable(),
- ', '
- )
- )
- end
- end
-
- output[#output + 1] = '}'
- output[#output + 1] = '-- stylua: ignore end'
- end
-
- output[#output + 1] = ''
- output[#output + 1] = 'return protocol'
-
- local fname = './runtime/lua/vim/lsp/protocol.lua'
- local bufnr = vim.fn.bufadd(fname)
- vim.fn.bufload(bufnr)
- vim.api.nvim_set_current_buf(bufnr)
- local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
- local index = vim.iter(ipairs(lines)):find(function(key, item)
- return vim.startswith(item, '-- Generated by') and key or nil
- end)
- index = index and index - 1 or vim.api.nvim_buf_line_count(bufnr) - 1
- vim.api.nvim_buf_set_lines(bufnr, index, -1, true, output)
- vim.cmd.write()
-end
-
----@class vim._gen_lsp.opt
----@field output_file string
----@field version string
----@field methods boolean
----@field capabilities boolean
-
----@param opt vim._gen_lsp.opt
-function M.gen(opt)
- --- @type vim._gen_lsp.Protocol
- local protocol = read_json(opt)
-
- write_to_protocol(protocol, opt.methods, opt.capabilities)
-
- local output = {
- '--' .. '[[',
- 'THIS FILE IS GENERATED by scripts/gen_lsp.lua',
- 'DO NOT EDIT MANUALLY',
- '',
- 'Based on LSP protocol ' .. opt.version,
- '',
- 'Regenerate:',
- ([=[nvim -l scripts/gen_lsp.lua gen --version %s]=]):format(DEFAULT_LSP_VERSION),
- '--' .. ']]',
- '',
- '---@meta',
- "error('Cannot require a meta file')",
- '',
- '---@alias lsp.null nil',
- '---@alias uinteger integer',
- '---@alias decimal number',
- '---@alias lsp.DocumentUri string',
- '---@alias lsp.URI string',
- '',
- }
-
- local anonymous_num = 0
-
- ---@type string[]
- local anonym_classes = {}
-
- local simple_types = {
- 'string',
- 'boolean',
- 'integer',
- 'uinteger',
- 'decimal',
- }
-
- ---@param documentation string
- local _process_documentation = function(documentation)
- documentation = documentation:gsub('\n', '\n---')
- -- Remove <200b> (zero-width space) unicode characters: e.g., `**/<200b>*`
- documentation = documentation:gsub('\226\128\139', '')
- -- Escape annotations that are not recognized by lua-ls
- documentation = documentation:gsub('%^---@sample', '---\\@sample')
- return '---' .. documentation
- end
-
- --- @class vim._gen_lsp.Type
- --- @field kind string a common field for all Types.
- --- @field name? string for ReferenceType, BaseType
- --- @field element? any for ArrayType
- --- @field items? vim._gen_lsp.Type[] for OrType, AndType
- --- @field key? vim._gen_lsp.Type for MapType
- --- @field value? string|vim._gen_lsp.Type for StringLiteralType, MapType, StructureLiteralType
-
- ---@param type vim._gen_lsp.Type
- ---@param prefix? string Optional prefix associated with the this type, made of (nested) field name.
- --- Used to generate class name for structure literal types.
- ---@return string
- local function parse_type(type, prefix)
- -- ReferenceType | BaseType
- if type.kind == 'reference' or type.kind == 'base' then
- if vim.tbl_contains(simple_types, type.name) then
- return type.name
- end
- return 'lsp.' .. type.name
-
- -- ArrayType
- elseif type.kind == 'array' then
- local parsed_items = parse_type(type.element, prefix)
- if type.element.items and #type.element.items > 1 then
- parsed_items = '(' .. parsed_items .. ')'
- end
- return parsed_items .. '[]'
-
- -- OrType
- elseif type.kind == 'or' then
- local val = ''
- for _, item in ipairs(type.items) do
- val = val .. parse_type(item, prefix) .. '|' --[[ @as string ]]
- end
- val = val:sub(0, -2)
- return val
-
- -- StringLiteralType
- elseif type.kind == 'stringLiteral' then
- return '"' .. type.value .. '"'
-
- -- MapType
- elseif type.kind == 'map' then
- local key = assert(type.key)
- local value = type.value --[[ @as vim._gen_lsp.Type ]]
- return 'table<' .. parse_type(key, prefix) .. ', ' .. parse_type(value, prefix) .. '>'
-
- -- StructureLiteralType
- elseif type.kind == 'literal' then
- -- can I use ---@param disabled? {reason: string}
- -- use | to continue the inline class to be able to add docs
- -- https://github.com/LuaLS/lua-language-server/issues/2128
- anonymous_num = anonymous_num + 1
- local anonymous_classname = 'lsp._anonym' .. anonymous_num
- if prefix then
- anonymous_classname = anonymous_classname .. '.' .. prefix
- end
- local anonym = vim
- .iter({
- (anonymous_num > 1 and { '' } or {}),
- { '---@class ' .. anonymous_classname },
- })
- :flatten()
- :totable()
-
- --- @class vim._gen_lsp.StructureLiteral translated to anonymous @class.
- --- @field deprecated? string
- --- @field description? string
- --- @field properties vim._gen_lsp.Property[]
- --- @field proposed? boolean
- --- @field since? string
-
- ---@type vim._gen_lsp.StructureLiteral
- local structural_literal = assert(type.value) --[[ @as vim._gen_lsp.StructureLiteral ]]
- for _, field in ipairs(structural_literal.properties) do
- anonym[#anonym + 1] = '---'
- if field.documentation then
- anonym[#anonym + 1] = _process_documentation(field.documentation)
- end
- anonym[#anonym + 1] = '---@field '
- .. field.name
- .. (field.optional and '?' or '')
- .. ' '
- .. parse_type(field.type, prefix .. '.' .. field.name)
- end
- -- anonym[#anonym + 1] = ''
- for _, line in ipairs(anonym) do
- if line then
- anonym_classes[#anonym_classes + 1] = line
- end
- end
- return anonymous_classname
-
- -- TupleType
- elseif type.kind == 'tuple' then
- local tuple = '['
- for _, value in ipairs(type.items) do
- tuple = tuple .. parse_type(value, prefix) .. ', '
- end
- -- remove , at the end
- tuple = tuple:sub(0, -3)
- return tuple .. ']'
- end
-
- vim.print('WARNING: Unknown type ', type)
- return ''
- end
-
- --- @class vim._gen_lsp.Structure translated to @class
- --- @field deprecated? string
- --- @field documentation? string
- --- @field extends? { kind: string, name: string }[]
- --- @field mixins? { kind: string, name: string }[]
- --- @field name string
- --- @field properties? vim._gen_lsp.Property[] members, translated to @field
- --- @field proposed? boolean
- --- @field since? string
- for _, structure in ipairs(protocol.structures) do
- -- output[#output + 1] = ''
- if structure.documentation then
- output[#output + 1] = _process_documentation(structure.documentation)
- end
- local class_string = ('---@class lsp.%s'):format(structure.name)
- if structure.extends or structure.mixins then
- local inherits_from = table.concat(
- vim.list_extend(
- vim.tbl_map(parse_type, structure.extends or {}),
- vim.tbl_map(parse_type, structure.mixins or {})
- ),
- ', '
- )
- class_string = class_string .. ': ' .. inherits_from
- end
- output[#output + 1] = class_string
-
- --- @class vim._gen_lsp.Property translated to @field
- --- @field deprecated? string
- --- @field documentation? string
- --- @field name string
- --- @field optional? boolean
- --- @field proposed? boolean
- --- @field since? string
- --- @field type { kind: string, name: string }
- for _, field in ipairs(structure.properties or {}) do
- output[#output + 1] = '---' -- Insert a single newline between @fields (and after @class)
- if field.documentation then
- output[#output + 1] = _process_documentation(field.documentation)
- end
- output[#output + 1] = '---@field '
- .. field.name
- .. (field.optional and '?' or '')
- .. ' '
- .. parse_type(field.type, field.name)
- end
- output[#output + 1] = ''
- end
-
- --- @class vim._gen_lsp.Enumeration translated to @enum
- --- @field deprecated string?
- --- @field documentation string?
- --- @field name string?
- --- @field proposed boolean?
- --- @field since string?
- --- @field suportsCustomValues boolean?
- --- @field values { name: string, value: string, documentation?: string, since?: string }[]
- for _, enum in ipairs(protocol.enumerations) do
- if enum.documentation then
- output[#output + 1] = _process_documentation(enum.documentation)
- end
- local enum_type = '---@alias lsp.' .. enum.name
- for _, value in ipairs(enum.values) do
- enum_type = enum_type
- .. '\n---| '
- .. (type(value.value) == 'string' and '"' .. value.value .. '"' or value.value)
- .. ' # '
- .. value.name
- end
- output[#output + 1] = enum_type
- output[#output + 1] = ''
- end
-
- --- @class vim._gen_lsp.TypeAlias translated to @alias
- --- @field deprecated? string?
- --- @field documentation? string
- --- @field name string
- --- @field proposed? boolean
- --- @field since? string
- --- @field type vim._gen_lsp.Type
- for _, alias in ipairs(protocol.typeAliases) do
- if alias.documentation then
- output[#output + 1] = _process_documentation(alias.documentation)
- end
- if alias.type.kind == 'or' then
- local alias_type = '---@alias lsp.' .. alias.name .. ' '
- for _, item in ipairs(alias.type.items) do
- alias_type = alias_type .. parse_type(item, alias.name) .. '|'
- end
- alias_type = alias_type:sub(0, -2)
- output[#output + 1] = alias_type
- else
- output[#output + 1] = '---@alias lsp.'
- .. alias.name
- .. ' '
- .. parse_type(alias.type, alias.name)
- end
- output[#output + 1] = ''
- end
-
- -- anonymous classes
- for _, line in ipairs(anonym_classes) do
- output[#output + 1] = line
- end
-
- tofile(opt.output_file, table.concat(output, '\n') .. '\n')
-end
-
----@type vim._gen_lsp.opt
-local opt = {
- output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua',
- version = DEFAULT_LSP_VERSION,
- methods = false,
- capabilities = false,
-}
-
-local command = nil
-local i = 1
-while i <= #_G.arg do
- if _G.arg[i] == '--out' then
- opt.output_file = assert(_G.arg[i + 1], '--out <outfile> needed')
- i = i + 1
- elseif _G.arg[i] == '--version' then
- opt.version = assert(_G.arg[i + 1], '--version <version> needed')
- i = i + 1
- elseif _G.arg[i] == '--methods' then
- opt.methods = true
- elseif _G.arg[i] == '--capabilities' then
- opt.capabilities = true
- elseif vim.startswith(_G.arg[i], '-') then
- error('Unrecognized args: ' .. _G.arg[i])
- else
- if command then
- error('More than one command was given: ' .. _G.arg[i])
- else
- command = _G.arg[i]
- end
- end
- i = i + 1
-end
-
-if not command then
- print(USAGE)
-elseif M[command] then
- M[command](opt) -- see M.gen()
-else
- error('Unknown command: ' .. command)
-end
-
-return M
diff --git a/scripts/gen_vimdoc.lua b/scripts/gen_vimdoc.lua
deleted file mode 100755
index d200050fe1..0000000000
--- a/scripts/gen_vimdoc.lua
+++ /dev/null
@@ -1,1041 +0,0 @@
-#!/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()
diff --git a/scripts/lintdoc.lua b/scripts/lintdoc.lua
index 5e78b4cdcb..78cf9fed72 100755
--- a/scripts/lintdoc.lua
+++ b/scripts/lintdoc.lua
@@ -14,7 +14,7 @@ print('Running lintdoc ...')
-- :helptags checks for duplicate tags.
vim.cmd [[ helptags ALL ]]
-require('scripts.gen_help_html').run_validate()
-require('scripts.gen_help_html').test_gen()
+require('src.gen.gen_help_html').run_validate()
+require('src.gen.gen_help_html').test_gen()
print('lintdoc PASSED.')
diff --git a/scripts/luacats_grammar.lua b/scripts/luacats_grammar.lua
deleted file mode 100644
index b700bcf58f..0000000000
--- a/scripts/luacats_grammar.lua
+++ /dev/null
@@ -1,207 +0,0 @@
---[[!
-LPEG grammar for LuaCATS
-]]
-
-local lpeg = vim.lpeg
-local P, R, S = lpeg.P, lpeg.R, lpeg.S
-local C, Ct, Cg = lpeg.C, lpeg.Ct, lpeg.Cg
-
---- @param x vim.lpeg.Pattern
-local function rep(x)
- return x ^ 0
-end
-
---- @param x vim.lpeg.Pattern
-local function rep1(x)
- return x ^ 1
-end
-
---- @param x vim.lpeg.Pattern
-local function opt(x)
- return x ^ -1
-end
-
-local ws = rep1(S(' \t'))
-local fill = opt(ws)
-local any = P(1) -- (consume one character)
-local letter = R('az', 'AZ')
-local num = R('09')
-
---- @param x string | vim.lpeg.Pattern
-local function Pf(x)
- return fill * P(x) * fill
-end
-
---- @param x string | vim.lpeg.Pattern
-local function Plf(x)
- return fill * P(x)
-end
-
---- @param x string
-local function Sf(x)
- return fill * S(x) * fill
-end
-
---- @param x vim.lpeg.Pattern
-local function paren(x)
- return Pf('(') * x * fill * P(')')
-end
-
---- @param x vim.lpeg.Pattern
-local function parenOpt(x)
- return paren(x) + x
-end
-
---- @param x vim.lpeg.Pattern
-local function comma1(x)
- return parenOpt(x * rep(Pf(',') * x))
-end
-
---- @param x vim.lpeg.Pattern
-local function comma(x)
- return opt(comma1(x))
-end
-
---- @type table<string,vim.lpeg.Pattern>
-local v = setmetatable({}, {
- __index = function(_, k)
- return lpeg.V(k)
- end,
-})
-
---- @class nvim.luacats.Param
---- @field kind 'param'
---- @field name string
---- @field type string
---- @field desc? string
-
---- @class nvim.luacats.Return
---- @field kind 'return'
---- @field [integer] { type: string, name?: string}
---- @field desc? string
-
---- @class nvim.luacats.Generic
---- @field kind 'generic'
---- @field name string
---- @field type? string
-
---- @class nvim.luacats.Class
---- @field kind 'class'
---- @field name string
---- @field parent? string
---- @field access? 'private'|'protected'|'package'
-
---- @class nvim.luacats.Field
---- @field kind 'field'
---- @field name string
---- @field type string
---- @field desc? string
---- @field access? 'private'|'protected'|'package'
-
---- @class nvim.luacats.Note
---- @field desc? string
-
---- @alias nvim.luacats.grammar.result
---- | nvim.luacats.Param
---- | nvim.luacats.Return
---- | nvim.luacats.Generic
---- | nvim.luacats.Class
---- | nvim.luacats.Field
---- | nvim.luacats.Note
-
---- @class nvim.luacats.grammar
---- @field match fun(self, input: string): nvim.luacats.grammar.result?
-
-local function annot(nm, pat)
- if type(nm) == 'string' then
- nm = P(nm)
- end
- if pat then
- return Ct(Cg(P(nm), 'kind') * fill * pat)
- end
- return Ct(Cg(P(nm), 'kind'))
-end
-
-local colon = Pf(':')
-local ellipsis = P('...')
-local ident_first = P('_') + letter
-local ident = ident_first * rep(ident_first + num)
-local opt_ident = ident * opt(P('?'))
-local ty_ident_sep = S('-._')
-local ty_ident = ident * rep(ty_ident_sep * ident)
-local string_single = P "'" * rep(any - P "'") * P "'"
-local string_double = P('"') * rep(any - P('"')) * P('"')
-local generic = P('`') * ty_ident * P('`')
-local literal = string_single + string_double + (opt(P('-')) * rep1(num)) + P('false') + P('true')
-local ty_prims = ty_ident + literal + generic
-
-local array_postfix = rep1(Plf('[]'))
-local opt_postfix = rep1(Plf('?'))
-local rep_array_opt_postfix = rep(array_postfix + opt_postfix)
-
-local typedef = P({
- 'typedef',
- typedef = C(v.type),
-
- type = v.ty * rep_array_opt_postfix * rep(Pf('|') * v.ty * rep_array_opt_postfix),
- ty = v.composite + paren(v.typedef),
- composite = (v.types * array_postfix) + (v.types * opt_postfix) + v.types,
- types = v.generics + v.kv_table + v.tuple + v.dict + v.table_literal + v.fun + ty_prims,
-
- tuple = Pf('[') * comma1(v.type) * Plf(']'),
- dict = Pf('{') * comma1(Pf('[') * v.type * Pf(']') * colon * v.type) * Plf('}'),
- kv_table = Pf('table') * Pf('<') * v.type * Pf(',') * v.type * Plf('>'),
- table_literal = Pf('{') * comma1(opt_ident * Pf(':') * v.type) * Plf('}'),
- fun_param = (opt_ident + ellipsis) * opt(colon * v.type),
- fun_ret = v.type + (ellipsis * opt(colon * v.type)),
- fun = Pf('fun') * paren(comma(v.fun_param)) * opt(Pf(':') * comma1(v.fun_ret)),
- generics = P(ty_ident) * Pf('<') * comma1(v.type) * Plf('>'),
-}) / function(match)
- return vim.trim(match):gsub('^%((.*)%)$', '%1'):gsub('%?+', '?')
-end
-
-local access = P('private') + P('protected') + P('package')
-local caccess = Cg(access, 'access')
-local cattr = Cg(comma(access + P('exact')), 'access')
-local desc_delim = Sf '#:' + ws
-local desc = Cg(rep(any), 'desc')
-local opt_desc = opt(desc_delim * desc)
-local ty_name = Cg(ty_ident, 'name')
-local opt_parent = opt(colon * Cg(ty_ident, 'parent'))
-local lname = (ident + ellipsis) * opt(P('?'))
-
-local grammar = P {
- rep1(P('@') * (v.ats + v.ext_ats)),
-
- ats = annot('param', Cg(lname, 'name') * ws * v.ctype * opt_desc)
- + annot('return', comma1(Ct(v.ctype * opt(ws * (ty_name + Cg(ellipsis, 'name'))))) * opt_desc)
- + annot('type', comma1(Ct(v.ctype)) * opt_desc)
- + annot('cast', ty_name * ws * opt(Sf('+-')) * v.ctype)
- + annot('generic', ty_name * opt(colon * v.ctype))
- + annot('class', opt(paren(cattr)) * fill * ty_name * opt_parent)
- + annot('field', opt(caccess * ws) * v.field_name * ws * v.ctype * opt_desc)
- + annot('operator', ty_name * opt(paren(Cg(v.ctype, 'argtype'))) * colon * v.ctype)
- + annot(access)
- + annot('deprecated')
- + annot('alias', ty_name * opt(ws * v.ctype))
- + annot('enum', ty_name)
- + annot('overload', v.ctype)
- + annot('see', opt(desc_delim) * desc)
- + annot('diagnostic', opt(desc_delim) * desc)
- + annot('meta'),
-
- --- Custom extensions
- ext_ats = (
- annot('note', desc)
- + annot('since', desc)
- + annot('nodoc')
- + annot('inlinedoc')
- + annot('brief', desc)
- ),
-
- field_name = Cg(lname + (v.ty_index * opt(P('?'))), 'name'),
- ty_index = C(Pf('[') * typedef * fill * P(']')),
- ctype = Cg(typedef, 'type'),
-}
-
-return grammar --[[@as nvim.luacats.grammar]]
diff --git a/scripts/luacats_parser.lua b/scripts/luacats_parser.lua
deleted file mode 100644
index 8a50077aa8..0000000000
--- a/scripts/luacats_parser.lua
+++ /dev/null
@@ -1,535 +0,0 @@
-local luacats_grammar = require('scripts.luacats_grammar')
-
---- @class nvim.luacats.parser.param : nvim.luacats.Param
-
---- @class nvim.luacats.parser.return
---- @field name string
---- @field type string
---- @field desc string
-
---- @class nvim.luacats.parser.note
---- @field desc string
-
---- @class nvim.luacats.parser.brief
---- @field kind 'brief'
---- @field desc string
-
---- @class nvim.luacats.parser.alias
---- @field kind 'alias'
---- @field type string[]
---- @field desc string
-
---- @class nvim.luacats.parser.fun
---- @field name string
---- @field params nvim.luacats.parser.param[]
---- @field returns nvim.luacats.parser.return[]
---- @field desc string
---- @field access? 'private'|'package'|'protected'
---- @field class? string
---- @field module? string
---- @field modvar? string
---- @field classvar? string
---- @field deprecated? true
---- @field since? string
---- @field attrs? string[]
---- @field nodoc? true
---- @field generics? table<string,string>
---- @field table? true
---- @field notes? nvim.luacats.parser.note[]
---- @field see? nvim.luacats.parser.note[]
-
---- @class nvim.luacats.parser.field : nvim.luacats.Field
---- @field classvar? string
---- @field nodoc? true
-
---- @class nvim.luacats.parser.class : nvim.luacats.Class
---- @field desc? string
---- @field nodoc? true
---- @field inlinedoc? true
---- @field fields nvim.luacats.parser.field[]
---- @field notes? string[]
-
---- @class nvim.luacats.parser.State
---- @field doc_lines? string[]
---- @field cur_obj? nvim.luacats.parser.obj
---- @field last_doc_item? nvim.luacats.parser.param|nvim.luacats.parser.return|nvim.luacats.parser.note
---- @field last_doc_item_indent? integer
-
---- @alias nvim.luacats.parser.obj
---- | nvim.luacats.parser.class
---- | nvim.luacats.parser.fun
---- | nvim.luacats.parser.brief
---- | nvim.luacats.parser.alias
-
--- Remove this when we document classes properly
---- Some doc lines have the form:
---- param name some.complex.type (table) description
---- if so then transform the line to remove the complex type:
---- param name (table) description
---- @param line string
-local function use_type_alt(line)
- for _, type in ipairs({ 'table', 'function' }) do
- line = line:gsub('@param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. ')%)', '@param %1 %2')
- line = line:gsub('@param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. '|nil)%)', '@param %1 %2')
- line = line:gsub('@param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. '%?)%)', '@param %1 %2')
-
- line = line:gsub('@return%s+.*%((' .. type .. ')%)', '@return %1')
- line = line:gsub('@return%s+.*%((' .. type .. '|nil)%)', '@return %1')
- line = line:gsub('@return%s+.*%((' .. type .. '%?)%)', '@return %1')
- end
- return line
-end
-
---- If we collected any `---` lines. Add them to the existing (or new) object
---- Used for function/class descriptions and multiline param descriptions.
---- @param state nvim.luacats.parser.State
-local function add_doc_lines_to_obj(state)
- if state.doc_lines then
- state.cur_obj = state.cur_obj or {}
- local cur_obj = assert(state.cur_obj)
- local txt = table.concat(state.doc_lines, '\n')
- if cur_obj.desc then
- cur_obj.desc = cur_obj.desc .. '\n' .. txt
- else
- cur_obj.desc = txt
- end
- state.doc_lines = nil
- end
-end
-
---- @param line string
---- @param state nvim.luacats.parser.State
-local function process_doc_line(line, state)
- line = line:sub(4):gsub('^%s+@', '@')
- line = use_type_alt(line)
-
- local parsed = luacats_grammar:match(line)
-
- if not parsed then
- if line:match('^ ') then
- line = line:sub(2)
- end
-
- if state.last_doc_item then
- if not state.last_doc_item_indent then
- state.last_doc_item_indent = #line:match('^%s*') + 1
- end
- state.last_doc_item.desc = (state.last_doc_item.desc or '')
- .. '\n'
- .. line:sub(state.last_doc_item_indent or 1)
- else
- state.doc_lines = state.doc_lines or {}
- table.insert(state.doc_lines, line)
- end
- return
- end
-
- state.last_doc_item_indent = nil
- state.last_doc_item = nil
- state.cur_obj = state.cur_obj or {}
- local cur_obj = assert(state.cur_obj)
-
- local kind = parsed.kind
-
- if kind == 'brief' then
- state.cur_obj = {
- kind = 'brief',
- desc = parsed.desc,
- }
- elseif kind == 'class' then
- --- @cast parsed nvim.luacats.Class
- cur_obj.kind = 'class'
- cur_obj.name = parsed.name
- cur_obj.parent = parsed.parent
- cur_obj.access = parsed.access
- cur_obj.desc = state.doc_lines and table.concat(state.doc_lines, '\n') or nil
- state.doc_lines = nil
- cur_obj.fields = {}
- elseif kind == 'field' then
- --- @cast parsed nvim.luacats.Field
- parsed.desc = parsed.desc or state.doc_lines and table.concat(state.doc_lines, '\n') or nil
- if parsed.desc then
- parsed.desc = vim.trim(parsed.desc)
- end
- table.insert(cur_obj.fields, parsed)
- state.doc_lines = nil
- elseif kind == 'operator' then
- parsed.desc = parsed.desc or state.doc_lines and table.concat(state.doc_lines, '\n') or nil
- if parsed.desc then
- parsed.desc = vim.trim(parsed.desc)
- end
- table.insert(cur_obj.fields, parsed)
- state.doc_lines = nil
- elseif kind == 'param' then
- state.last_doc_item_indent = nil
- cur_obj.params = cur_obj.params or {}
- if vim.endswith(parsed.name, '?') then
- parsed.name = parsed.name:sub(1, -2)
- parsed.type = parsed.type .. '?'
- end
- state.last_doc_item = {
- name = parsed.name,
- type = parsed.type,
- desc = parsed.desc,
- }
- table.insert(cur_obj.params, state.last_doc_item)
- elseif kind == 'return' then
- cur_obj.returns = cur_obj.returns or {}
- for _, t in ipairs(parsed) do
- table.insert(cur_obj.returns, {
- name = t.name,
- type = t.type,
- desc = parsed.desc,
- })
- end
- state.last_doc_item_indent = nil
- state.last_doc_item = cur_obj.returns[#cur_obj.returns]
- elseif kind == 'private' then
- cur_obj.access = 'private'
- elseif kind == 'package' then
- cur_obj.access = 'package'
- elseif kind == 'protected' then
- cur_obj.access = 'protected'
- elseif kind == 'deprecated' then
- cur_obj.deprecated = true
- elseif kind == 'inlinedoc' then
- cur_obj.inlinedoc = true
- elseif kind == 'nodoc' then
- cur_obj.nodoc = true
- elseif kind == 'since' then
- cur_obj.since = parsed.desc
- elseif kind == 'see' then
- cur_obj.see = cur_obj.see or {}
- table.insert(cur_obj.see, { desc = parsed.desc })
- elseif kind == 'note' then
- state.last_doc_item_indent = nil
- state.last_doc_item = {
- desc = parsed.desc,
- }
- cur_obj.notes = cur_obj.notes or {}
- table.insert(cur_obj.notes, state.last_doc_item)
- elseif kind == 'type' then
- cur_obj.desc = parsed.desc
- parsed.desc = nil
- parsed.kind = nil
- cur_obj.type = parsed
- elseif kind == 'alias' then
- state.cur_obj = {
- kind = 'alias',
- desc = parsed.desc,
- }
- elseif kind == 'enum' then
- -- TODO
- state.doc_lines = nil
- elseif
- vim.tbl_contains({
- 'diagnostic',
- 'cast',
- 'overload',
- 'meta',
- }, kind)
- then
- -- Ignore
- return
- elseif kind == 'generic' then
- cur_obj.generics = cur_obj.generics or {}
- cur_obj.generics[parsed.name] = parsed.type or 'any'
- else
- error('Unhandled' .. vim.inspect(parsed))
- end
-end
-
---- @param fun nvim.luacats.parser.fun
---- @return nvim.luacats.parser.field
-local function fun2field(fun)
- local parts = { 'fun(' }
-
- local params = {} ---@type string[]
- for _, p in ipairs(fun.params or {}) do
- params[#params + 1] = string.format('%s: %s', p.name, p.type)
- end
- parts[#parts + 1] = table.concat(params, ', ')
- parts[#parts + 1] = ')'
- if fun.returns then
- parts[#parts + 1] = ': '
- local tys = {} --- @type string[]
- for _, p in ipairs(fun.returns) do
- tys[#tys + 1] = p.type
- end
- parts[#parts + 1] = table.concat(tys, ', ')
- end
-
- return {
- name = fun.name,
- type = table.concat(parts, ''),
- access = fun.access,
- desc = fun.desc,
- nodoc = fun.nodoc,
- }
-end
-
---- Function to normalize known form for declaring functions and normalize into a more standard
---- form.
---- @param line string
---- @return string
-local function filter_decl(line)
- -- M.fun = vim._memoize(function(...)
- -- ->
- -- function M.fun(...)
- line = line:gsub('^local (.+) = memoize%([^,]+, function%((.*)%)$', 'local function %1(%2)')
- line = line:gsub('^(.+) = memoize%([^,]+, function%((.*)%)$', 'function %1(%2)')
- return line
-end
-
---- @param line string
---- @param state nvim.luacats.parser.State
---- @param classes table<string,nvim.luacats.parser.class>
---- @param classvars table<string,string>
---- @param has_indent boolean
-local function process_lua_line(line, state, classes, classvars, has_indent)
- line = filter_decl(line)
-
- if state.cur_obj and state.cur_obj.kind == 'class' then
- local nm = line:match('^local%s+([a-zA-Z0-9_]+)%s*=')
- if nm then
- classvars[nm] = state.cur_obj.name
- end
- return
- end
-
- do
- local parent_tbl, sep, fun_or_meth_nm =
- line:match('^function%s+([a-zA-Z0-9_]+)([.:])([a-zA-Z0-9_]+)%s*%(')
- if parent_tbl then
- -- Have a decl. Ensure cur_obj
- state.cur_obj = state.cur_obj or {}
- local cur_obj = assert(state.cur_obj)
-
- -- Match `Class:foo` methods for defined classes
- local class = classvars[parent_tbl]
- if class then
- --- @cast cur_obj nvim.luacats.parser.fun
- cur_obj.name = fun_or_meth_nm
- cur_obj.class = class
- cur_obj.classvar = parent_tbl
- -- Add self param to methods
- if sep == ':' then
- cur_obj.params = cur_obj.params or {}
- table.insert(cur_obj.params, 1, {
- name = 'self',
- type = class,
- })
- end
-
- -- Add method as the field to the class
- local cls = classes[class]
- local field = fun2field(cur_obj)
- field.classvar = cur_obj.classvar
- table.insert(cls.fields, field)
- return
- end
-
- -- Match `M.foo`
- if cur_obj and parent_tbl == cur_obj.modvar then
- cur_obj.name = fun_or_meth_nm
- return
- end
- end
- end
-
- do
- -- Handle: `function A.B.C.foo(...)`
- local fn_nm = line:match('^function%s+([.a-zA-Z0-9_]+)%s*%(')
- if fn_nm then
- state.cur_obj = state.cur_obj or {}
- state.cur_obj.name = fn_nm
- return
- end
- end
-
- do
- -- Handle: `M.foo = {...}` where `M` is the modvar
- local parent_tbl, tbl_nm = line:match('([a-zA-Z_]+)%.([a-zA-Z0-9_]+)%s*=')
- if state.cur_obj and parent_tbl and parent_tbl == state.cur_obj.modvar then
- state.cur_obj.name = tbl_nm
- state.cur_obj.table = true
- return
- end
- end
-
- do
- -- Handle: `foo = {...}`
- local tbl_nm = line:match('^([a-zA-Z0-9_]+)%s*=')
- if tbl_nm and not has_indent then
- state.cur_obj = state.cur_obj or {}
- state.cur_obj.name = tbl_nm
- state.cur_obj.table = true
- return
- end
- end
-
- do
- -- Handle: `vim.foo = {...}`
- local tbl_nm = line:match('^(vim%.[a-zA-Z0-9_]+)%s*=')
- if state.cur_obj and tbl_nm and not has_indent then
- state.cur_obj.name = tbl_nm
- state.cur_obj.table = true
- return
- end
- end
-
- if state.cur_obj then
- if line:find('^%s*%-%- luacheck:') then
- state.cur_obj = nil
- elseif line:find('^%s*local%s+') then
- state.cur_obj = nil
- elseif line:find('^%s*return%s+') then
- state.cur_obj = nil
- elseif line:find('^%s*[a-zA-Z_.]+%(%s+') then
- state.cur_obj = nil
- end
- end
-end
-
---- Determine the table name used to export functions of a module
---- Usually this is `M`.
---- @param str string
---- @return string?
-local function determine_modvar(str)
- local modvar --- @type string?
- for line in vim.gsplit(str, '\n') do
- do
- --- @type string?
- local m = line:match('^return%s+([a-zA-Z_]+)')
- if m then
- modvar = m
- end
- end
- do
- --- @type string?
- local m = line:match('^return%s+setmetatable%(([a-zA-Z_]+),')
- if m then
- modvar = m
- end
- end
- end
- return modvar
-end
-
---- @param obj nvim.luacats.parser.obj
---- @param funs nvim.luacats.parser.fun[]
---- @param classes table<string,nvim.luacats.parser.class>
---- @param briefs string[]
---- @param uncommitted nvim.luacats.parser.obj[]
-local function commit_obj(obj, classes, funs, briefs, uncommitted)
- local commit = false
- if obj.kind == 'class' then
- --- @cast obj nvim.luacats.parser.class
- if not classes[obj.name] then
- classes[obj.name] = obj
- commit = true
- end
- elseif obj.kind == 'alias' then
- -- Just pretend
- commit = true
- elseif obj.kind == 'brief' then
- --- @cast obj nvim.luacats.parser.brief`
- briefs[#briefs + 1] = obj.desc
- commit = true
- else
- --- @cast obj nvim.luacats.parser.fun`
- if obj.name then
- funs[#funs + 1] = obj
- commit = true
- end
- end
- if not commit then
- table.insert(uncommitted, obj)
- end
- return commit
-end
-
---- @param filename string
---- @param uncommitted nvim.luacats.parser.obj[]
--- luacheck: no unused
-local function dump_uncommitted(filename, uncommitted)
- local out_path = 'luacats-uncommited/' .. filename:gsub('/', '%%') .. '.txt'
- if #uncommitted > 0 then
- print(string.format('Could not commit %d objects in %s', #uncommitted, filename))
- vim.fn.mkdir(vim.fs.dirname(out_path), 'p')
- local f = assert(io.open(out_path, 'w'))
- for i, x in ipairs(uncommitted) do
- f:write(i)
- f:write(': ')
- f:write(vim.inspect(x))
- f:write('\n')
- end
- f:close()
- else
- vim.fn.delete(out_path)
- end
-end
-
-local M = {}
-
-function M.parse_str(str, filename)
- local funs = {} --- @type nvim.luacats.parser.fun[]
- local classes = {} --- @type table<string,nvim.luacats.parser.class>
- local briefs = {} --- @type string[]
-
- local mod_return = determine_modvar(str)
-
- --- @type string
- local module = filename:match('.*/lua/([a-z_][a-z0-9_/]+)%.lua') or filename
- module = module:gsub('/', '.')
-
- local classvars = {} --- @type table<string,string>
-
- local state = {} --- @type nvim.luacats.parser.State
-
- -- Keep track of any partial objects we don't commit
- local uncommitted = {} --- @type nvim.luacats.parser.obj[]
-
- for line in vim.gsplit(str, '\n') do
- local has_indent = line:match('^%s+') ~= nil
- line = vim.trim(line)
- if vim.startswith(line, '---') then
- process_doc_line(line, state)
- else
- add_doc_lines_to_obj(state)
-
- if state.cur_obj then
- state.cur_obj.modvar = mod_return
- state.cur_obj.module = module
- end
-
- process_lua_line(line, state, classes, classvars, has_indent)
-
- -- Commit the object
- local cur_obj = state.cur_obj
- if cur_obj then
- if not commit_obj(cur_obj, classes, funs, briefs, uncommitted) then
- --- @diagnostic disable-next-line:inject-field
- cur_obj.line = line
- end
- end
-
- state = {}
- end
- end
-
- -- dump_uncommitted(filename, uncommitted)
-
- return classes, funs, briefs, uncommitted
-end
-
---- @param filename string
-function M.parse(filename)
- local f = assert(io.open(filename, 'r'))
- local txt = f:read('*all')
- f:close()
-
- return M.parse_str(txt, filename)
-end
-
-return M
diff --git a/scripts/release.sh b/scripts/release.sh
index 257fa127c4..58acbf85a0 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -59,7 +59,7 @@ _do_release_commit() {
$__sed -i.bk 's/(NVIM_API_PRERELEASE) true/\1 false/' CMakeLists.txt
build/bin/nvim --api-info > "test/functional/fixtures/api_level_$__API_LEVEL.mpack"
git add "test/functional/fixtures/api_level_${__API_LEVEL}.mpack"
- VIMRUNTIME=./runtime build/bin/nvim -u NONE -l scripts/gen_vimdoc.lua
+ VIMRUNTIME=./runtime build/bin/nvim -u NONE -l src/gen/preload.lua src/gen/gen_vimdoc.lua
git add -u -- runtime/doc/
fi
diff --git a/scripts/util.lua b/scripts/util.lua
deleted file mode 100644
index 5940221abe..0000000000
--- a/scripts/util.lua
+++ /dev/null
@@ -1,399 +0,0 @@
--- TODO(justinmk): move most of this to `vim.text`.
-
-local fmt = string.format
-
---- @class nvim.util.MDNode
---- @field [integer] nvim.util.MDNode
---- @field type string
---- @field text? string
-
-local INDENTATION = 4
-
-local NBSP = string.char(160)
-
-local M = {}
-
-local function contains(t, xs)
- return vim.tbl_contains(xs, t)
-end
-
--- Map of api_level:version, by inspection of:
--- :lua= vim.mpack.decode(vim.fn.readfile('test/functional/fixtures/api_level_9.mpack','B')).version
-M.version_level = {
- [13] = '0.11.0',
- [12] = '0.10.0',
- [11] = '0.9.0',
- [10] = '0.8.0',
- [9] = '0.7.0',
- [8] = '0.6.0',
- [7] = '0.5.0',
- [6] = '0.4.0',
- [5] = '0.3.2',
- [4] = '0.3.0',
- [3] = '0.2.1',
- [2] = '0.2.0',
- [1] = '0.1.0',
-}
-
---- @param txt string
---- @param srow integer
---- @param scol integer
---- @param erow? integer
---- @param ecol? integer
---- @return string
-local function slice_text(txt, srow, scol, erow, ecol)
- local lines = vim.split(txt, '\n')
-
- if srow == erow then
- return lines[srow + 1]:sub(scol + 1, ecol)
- end
-
- if erow then
- -- Trim the end
- for _ = erow + 2, #lines do
- table.remove(lines, #lines)
- end
- end
-
- -- Trim the start
- for _ = 1, srow do
- table.remove(lines, 1)
- end
-
- lines[1] = lines[1]:sub(scol + 1)
- lines[#lines] = lines[#lines]:sub(1, ecol)
-
- return table.concat(lines, '\n')
-end
-
---- @param text string
---- @return nvim.util.MDNode
-local function parse_md_inline(text)
- local parser = vim.treesitter.languagetree.new(text, 'markdown_inline')
- local root = parser:parse(true)[1]:root()
-
- --- @param node TSNode
- --- @return nvim.util.MDNode?
- local function extract(node)
- local ntype = node:type()
-
- if ntype:match('^%p$') then
- return
- end
-
- --- @type table<any,any>
- local ret = { type = ntype }
- ret.text = vim.treesitter.get_node_text(node, text)
-
- local row, col = 0, 0
-
- for child, child_field in node:iter_children() do
- local e = extract(child)
- if e and ntype == 'inline' then
- local srow, scol = child:start()
- if (srow == row and scol > col) or srow > row then
- local t = slice_text(ret.text, row, col, srow, scol)
- if t and t ~= '' then
- table.insert(ret, { type = 'text', j = true, text = t })
- end
- end
- row, col = child:end_()
- end
-
- if child_field then
- ret[child_field] = e
- else
- table.insert(ret, e)
- end
- end
-
- if ntype == 'inline' and (row > 0 or col > 0) then
- local t = slice_text(ret.text, row, col)
- if t and t ~= '' then
- table.insert(ret, { type = 'text', text = t })
- end
- end
-
- return ret
- end
-
- return extract(root) or {}
-end
-
---- @param text string
---- @return nvim.util.MDNode
-local function parse_md(text)
- local parser = vim.treesitter.languagetree.new(text, 'markdown', {
- injections = { markdown = '' },
- })
-
- local root = parser:parse(true)[1]:root()
-
- local EXCLUDE_TEXT_TYPE = {
- list = true,
- list_item = true,
- section = true,
- document = true,
- fenced_code_block = true,
- fenced_code_block_delimiter = true,
- }
-
- --- @param node TSNode
- --- @return nvim.util.MDNode?
- local function extract(node)
- local ntype = node:type()
-
- if ntype:match('^%p$') or contains(ntype, { 'block_continuation' }) then
- return
- end
-
- --- @type table<any,any>
- local ret = { type = ntype }
-
- if not EXCLUDE_TEXT_TYPE[ntype] then
- ret.text = vim.treesitter.get_node_text(node, text)
- end
-
- if ntype == 'inline' then
- ret = parse_md_inline(ret.text)
- end
-
- for child, child_field in node:iter_children() do
- local e = extract(child)
- if child_field then
- ret[child_field] = e
- else
- table.insert(ret, e)
- end
- end
-
- return ret
- end
-
- return extract(root) or {}
-end
-
---- Prefixes each line in `text`.
----
---- Does not wrap, not important for "meta" files? (You probably want md_to_vimdoc instead.)
----
---- @param text string
---- @param prefix_ string
-function M.prefix_lines(prefix_, text)
- local r = ''
- for _, l in ipairs(vim.split(text, '\n', { plain = true })) do
- r = r .. vim.trim(prefix_ .. l) .. '\n'
- end
- return r
-end
-
---- @param x string
---- @param start_indent integer
---- @param indent integer
---- @param text_width integer
---- @return string
-function M.wrap(x, start_indent, indent, text_width)
- local words = vim.split(vim.trim(x), '%s+')
- local parts = { string.rep(' ', start_indent) } --- @type string[]
- local count = indent
-
- for i, w in ipairs(words) do
- if count > indent and count + #w > text_width - 1 then
- parts[#parts + 1] = '\n'
- parts[#parts + 1] = string.rep(' ', indent)
- count = indent
- elseif i ~= 1 then
- parts[#parts + 1] = ' '
- count = count + 1
- end
- count = count + #w
- parts[#parts + 1] = w
- end
-
- return (table.concat(parts):gsub('%s+\n', '\n'):gsub('\n+$', ''))
-end
-
---- @param node nvim.util.MDNode
---- @param start_indent integer
---- @param indent integer
---- @param text_width integer
---- @param level integer
---- @return string[]
-local function render_md(node, start_indent, indent, text_width, level, is_list)
- local parts = {} --- @type string[]
-
- -- For debugging
- local add_tag = false
- -- local add_tag = true
-
- local ntype = node.type
-
- if add_tag then
- parts[#parts + 1] = '<' .. ntype .. '>'
- end
-
- if ntype == 'text' then
- parts[#parts + 1] = node.text
- elseif ntype == 'html_tag' then
- error('html_tag: ' .. node.text)
- elseif ntype == 'inline_link' then
- vim.list_extend(parts, { '*', node[1].text, '*' })
- elseif ntype == 'shortcut_link' then
- if node[1].text:find('^<.*>$') then
- parts[#parts + 1] = node[1].text
- elseif node[1].text:find('^%d+$') then
- vim.list_extend(parts, { '[', node[1].text, ']' })
- else
- vim.list_extend(parts, { '|', node[1].text, '|' })
- end
- elseif ntype == 'backslash_escape' then
- parts[#parts + 1] = node.text
- elseif ntype == 'emphasis' then
- parts[#parts + 1] = node.text:sub(2, -2)
- elseif ntype == 'code_span' then
- vim.list_extend(parts, { '`', node.text:sub(2, -2):gsub(' ', NBSP), '`' })
- elseif ntype == 'inline' then
- if #node == 0 then
- local text = assert(node.text)
- parts[#parts + 1] = M.wrap(text, start_indent, indent, text_width)
- else
- for _, child in ipairs(node) do
- vim.list_extend(parts, render_md(child, start_indent, indent, text_width, level + 1))
- end
- end
- elseif ntype == 'paragraph' then
- local pparts = {}
- for _, child in ipairs(node) do
- vim.list_extend(pparts, render_md(child, start_indent, indent, text_width, level + 1))
- end
- parts[#parts + 1] = M.wrap(table.concat(pparts), start_indent, indent, text_width)
- parts[#parts + 1] = '\n'
- elseif ntype == 'code_fence_content' then
- local lines = vim.split(node.text:gsub('\n%s*$', ''), '\n')
-
- local cindent = indent + INDENTATION
- if level > 3 then
- -- The tree-sitter markdown parser doesn't parse the code blocks indents
- -- correctly in lists. Fudge it!
- lines[1] = ' ' .. lines[1] -- ¯\_(ツ)_/¯
- cindent = indent - level
- local _, initial_indent = lines[1]:find('^%s*')
- initial_indent = initial_indent + cindent
- if initial_indent < indent then
- cindent = indent - INDENTATION
- end
- end
-
- for _, l in ipairs(lines) do
- if #l > 0 then
- parts[#parts + 1] = string.rep(' ', cindent)
- parts[#parts + 1] = l
- end
- parts[#parts + 1] = '\n'
- end
- elseif ntype == 'fenced_code_block' then
- parts[#parts + 1] = '>'
- for _, child in ipairs(node) do
- if child.type == 'info_string' then
- parts[#parts + 1] = child.text
- break
- end
- end
- parts[#parts + 1] = '\n'
- for _, child in ipairs(node) do
- if child.type ~= 'info_string' then
- vim.list_extend(parts, render_md(child, start_indent, indent, text_width, level + 1))
- end
- end
- parts[#parts + 1] = '<\n'
- elseif ntype == 'html_block' then
- local text = node.text:gsub('^<pre>help', '')
- text = text:gsub('</pre>%s*$', '')
- parts[#parts + 1] = text
- elseif ntype == 'list_marker_dot' then
- parts[#parts + 1] = node.text
- elseif contains(ntype, { 'list_marker_minus', 'list_marker_star' }) then
- parts[#parts + 1] = '• '
- elseif ntype == 'list_item' then
- parts[#parts + 1] = string.rep(' ', indent)
- local offset = node[1].type == 'list_marker_dot' and 3 or 2
- for i, child in ipairs(node) do
- local sindent = i <= 2 and 0 or (indent + offset)
- vim.list_extend(
- parts,
- render_md(child, sindent, indent + offset, text_width, level + 1, true)
- )
- end
- else
- if node.text then
- error(fmt('cannot render:\n%s', vim.inspect(node)))
- end
- for i, child in ipairs(node) do
- local start_indent0 = i == 1 and start_indent or indent
- vim.list_extend(
- parts,
- render_md(child, start_indent0, indent, text_width, level + 1, is_list)
- )
- if ntype ~= 'list' and i ~= #node then
- if (node[i + 1] or {}).type ~= 'list' then
- parts[#parts + 1] = '\n'
- end
- end
- end
- end
-
- if add_tag then
- parts[#parts + 1] = '</' .. ntype .. '>'
- end
-
- return parts
-end
-
---- @param text_width integer
-local function align_tags(text_width)
- --- @param line string
- --- @return string
- return function(line)
- local tag_pat = '%s*(%*.+%*)%s*$'
- local tags = {}
- for m in line:gmatch(tag_pat) do
- table.insert(tags, m)
- end
-
- if #tags > 0 then
- line = line:gsub(tag_pat, '')
- local tags_str = ' ' .. table.concat(tags, ' ')
- --- @type integer
- local conceal_offset = select(2, tags_str:gsub('%*', '')) - 2
- local pad = string.rep(' ', text_width - #line - #tags_str + conceal_offset)
- return line .. pad .. tags_str
- end
-
- return line
- end
-end
-
---- @param text string
---- @param start_indent integer
---- @param indent integer
---- @param is_list? boolean
---- @return string
-function M.md_to_vimdoc(text, start_indent, indent, text_width, is_list)
- -- Add an extra newline so the parser can properly capture ending ```
- local parsed = parse_md(text .. '\n')
- local ret = render_md(parsed, start_indent, indent, text_width, 0, is_list)
-
- local lines = vim.split(table.concat(ret):gsub(NBSP, ' '), '\n')
-
- lines = vim.tbl_map(align_tags(text_width), lines)
-
- local s = table.concat(lines, '\n')
-
- -- Reduce whitespace in code-blocks
- s = s:gsub('\n+%s*>([a-z]+)\n', ' >%1\n')
- s = s:gsub('\n+%s*>\n?\n', ' >\n')
-
- return s
-end
-
-return M