aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/bump_deps.lua53
-rw-r--r--scripts/cdoc_grammar.lua87
-rw-r--r--scripts/cdoc_parser.lua223
-rwxr-xr-xscripts/gen_eval_files.lua406
-rw-r--r--scripts/gen_filetype.lua102
-rw-r--r--scripts/gen_help_html.lua587
-rw-r--r--scripts/gen_lsp.lua294
-rwxr-xr-xscripts/gen_vimdoc.lua953
-rwxr-xr-xscripts/gen_vimdoc.py1447
-rwxr-xr-xscripts/genappimage.sh6
-rw-r--r--scripts/lintcommit.lua76
-rwxr-xr-xscripts/lintdoc.lua20
-rw-r--r--scripts/lua2dox.lua475
-rw-r--r--scripts/luacats_grammar.lua184
-rw-r--r--scripts/luacats_parser.lua537
-rw-r--r--scripts/text_utils.lua361
-rwxr-xr-xscripts/vim-patch.sh6
-rwxr-xr-xscripts/vimpatch.lua10
-rw-r--r--scripts/windows.ti6
19 files changed, 3429 insertions, 2404 deletions
diff --git a/scripts/bump_deps.lua b/scripts/bump_deps.lua
index 076ad374cf..c5294893e0 100755
--- a/scripts/bump_deps.lua
+++ b/scripts/bump_deps.lua
@@ -138,9 +138,10 @@ local function get_archive_info(repo, ref)
'Failed to download archive from GitHub'
)
- local shacmd = (vim.fn.executable('sha256sum') == 1
- and{ 'sha256sum', archive_path }
- or { 'shasum', '-a', '256', archive_path })
+ local shacmd = (
+ vim.fn.executable('sha256sum') == 1 and { 'sha256sum', archive_path }
+ or { 'shasum', '-a', '256', archive_path }
+ )
local archive_sha = run(shacmd):gmatch('%w+')()
return { url = archive_url, sha = archive_sha }
end
@@ -152,18 +153,7 @@ local function write_cmakelists_line(symbol, kind, value)
'sed',
'-i',
'-e',
- 's/'
- .. symbol
- .. '_'
- .. kind
- .. '.*$'
- .. '/'
- .. symbol
- .. '_'
- .. kind
- .. ' '
- .. value
- .. '/',
+ 's/' .. symbol .. '_' .. kind .. '.*$' .. '/' .. symbol .. '_' .. kind .. ' ' .. value .. '/',
deps_file,
}, 'Failed to write ' .. deps_file)
end
@@ -203,16 +193,13 @@ local function update_cmakelists(dependency, archive, comment)
p('Updating ' .. dependency.name .. ' to ' .. archive.url .. '\n')
write_cmakelists_line(dependency.symbol, 'URL', archive.url:gsub('/', '\\/'))
write_cmakelists_line(dependency.symbol, 'SHA256', archive.sha)
- run_die(
- {
- 'git',
- 'commit',
- deps_file,
- '-m',
- commit_prefix .. 'bump ' .. dependency.name .. ' to ' .. comment,
- },
- 'git failed to commit'
- )
+ run_die({
+ 'git',
+ 'commit',
+ deps_file,
+ '-m',
+ commit_prefix .. 'bump ' .. dependency.name .. ' to ' .. comment,
+ }, 'git failed to commit')
end
local function verify_cmakelists_committed()
@@ -318,9 +305,9 @@ function M.commit(dependency_name, commit)
end
function M.version(dependency_name, version)
- vim.validate{
- dependency_name={dependency_name,'s'},
- version={version,'s'},
+ vim.validate {
+ dependency_name = { dependency_name, 's' },
+ version = { version, 's' },
}
local dependency = assert(get_dependency(dependency_name))
verify_cmakelists_committed()
@@ -384,7 +371,7 @@ function M.submit_pr()
end
local function usage()
- local this_script = _G.arg[0]:match("[^/]*.lua$")
+ local this_script = _G.arg[0]:match('[^/]*.lua$')
print(([=[
Bump Nvim dependencies
@@ -421,13 +408,13 @@ local function parseargs()
elseif _G.arg[i] == '--pr' then
args.pr = true
elseif _G.arg[i] == '--branch' then
- args.branch = _G.arg[i+1]
+ args.branch = _G.arg[i + 1]
elseif _G.arg[i] == '--dep' then
- args.dep = _G.arg[i+1]
+ args.dep = _G.arg[i + 1]
elseif _G.arg[i] == '--version' then
- args.version = _G.arg[i+1]
+ args.version = _G.arg[i + 1]
elseif _G.arg[i] == '--commit' then
- args.commit = _G.arg[i+1]
+ args.commit = _G.arg[i + 1]
elseif _G.arg[i] == '--head' then
args.head = true
end
diff --git a/scripts/cdoc_grammar.lua b/scripts/cdoc_grammar.lua
new file mode 100644
index 0000000000..6a7610883b
--- /dev/null
+++ b/scripts/cdoc_grammar.lua
@@ -0,0 +1,87 @@
+--[[!
+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
new file mode 100644
index 0000000000..5f0dc7be2c
--- /dev/null
+++ b/scripts/cdoc_parser.lua
@@ -0,0 +1,223 @@
+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
index e331dd996e..f1bba5c0a2 100755
--- a/scripts/gen_eval_files.lua
+++ b/scripts/gen_eval_files.lua
@@ -1,7 +1,8 @@
+#!/usr/bin/env -S nvim -l
+
-- Generator for various vimdoc and Lua type files
-local DEP_API_METADATA = 'build/api_metadata.mpack'
-local DEP_API_DOC = 'runtime/doc/api.mpack'
+local DEP_API_METADATA = 'build/funcs_metadata.mpack'
--- @class vim.api.metadata
--- @field name string
@@ -17,6 +18,31 @@ local DEP_API_DOC = 'runtime/doc/api.mpack'
--- @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',
+ nvim_buf_get_extmarks = 'vim.api.keyset.get_extmark_item[]',
+ nvim_buf_get_keymap = 'vim.api.keyset.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.keymap[]',
+ nvim_get_mark = 'vim.api.keyset.get_mark',
+
+ -- Can also return table<string,vim.api.keyset.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.hl_info>
+ nvim_get_hl = 'vim.api.keyset.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_META_HEADER = {
'--- @meta _',
'-- THIS FILE IS GENERATED',
@@ -48,6 +74,16 @@ local LUA_OPTION_META_HEADER = {
'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,
@@ -56,10 +92,12 @@ local LUA_KEYWORDS = {
['if'] = true,
['while'] = true,
['repeat'] = true,
+ ['true'] = true,
+ ['false'] = true,
}
local OPTION_TYPES = {
- bool = 'boolean',
+ boolean = 'boolean',
number = 'integer',
string = 'string',
}
@@ -76,6 +114,7 @@ local API_TYPES = {
LuaRef = 'function',
Dictionary = 'table<string,any>',
Float = 'number',
+ HLGroupID = 'number|string',
void = '',
}
@@ -170,44 +209,65 @@ end
--- @return table<string, vim.EvalFn>
local function get_api_meta()
- local mpack_f = assert(io.open(DEP_API_METADATA, 'rb'))
- local metadata = vim.mpack.decode(mpack_f:read('*all')) --[[@as vim.api.metadata[] ]]
local ret = {} --- @type table<string, vim.EvalFn>
- local doc_mpack_f = assert(io.open(DEP_API_DOC, 'rb'))
- local doc_metadata = vim.mpack.decode(doc_mpack_f:read('*all')) --[[@as table<string,vim.gen_vim_doc_fun>]]
-
- for _, fun in ipairs(metadata) do
- if fun.lua then
- local fdoc = doc_metadata[fun.name]
-
- local params = {} --- @type {[1]:string,[2]:string}[]
- for _, p in ipairs(fun.parameters) do
- local ptype, pname = p[1], p[2]
- params[#params + 1] = {
- pname,
- api_type(ptype),
- fdoc and fdoc.parameters_doc[pname] or nil,
- }
- end
+ local cdoc_parser = require('scripts.cdoc_parser')
- local r = {
- signature = 'NA',
- name = fun.name,
- params = params,
- returns = api_type(fun.return_type),
- deprecated = fun.deprecated_since ~= nil,
- }
+ local f = 'src/nvim/api'
- if fdoc then
- if #fdoc.doc > 0 then
- r.desc = table.concat(fdoc.doc, '\n')
+ 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
- r.return_desc = (fdoc['return'] or {})[1]
end
+ end
+ end
- ret[fun.name] = r
+ for _, fun in pairs(functions) do
+ local deprecated = fun.deprecated_since ~= nil
+
+ local params = {} --- @type {[1]:string,[2]:string}[]
+ for _, p in ipairs(fun.params) do
+ params[#params + 1] = {
+ p.name,
+ api_type(p.type),
+ not deprecated and p.desc or nil,
+ }
end
+
+ local r = {
+ signature = 'NA',
+ name = fun.name,
+ params = params,
+ returns = api_type(fun.returns[1].type),
+ deprecated = deprecated,
+ }
+
+ if not deprecated then
+ r.desc = fun.desc
+ r.return_desc = fun.returns[1].desc
+ end
+
+ ret[fun.name] = r
end
return ret
end
@@ -235,12 +295,10 @@ end
--- @param fun vim.EvalFn
--- @param write fun(line: string)
local function render_api_meta(_f, fun, write)
- if not vim.startswith(fun.name, 'nvim_') then
- return
- end
-
write('')
+ local text_utils = require('scripts.text_utils')
+
if vim.startswith(fun.name, 'nvim__') then
write('--- @private')
end
@@ -251,10 +309,10 @@ local function render_api_meta(_f, fun, write)
local desc = fun.desc
if desc then
+ desc = text_utils.md_to_vimdoc(desc, 0, 0, 74)
for _, l in ipairs(split(norm_text(desc))) do
write('--- ' .. l)
end
- write('---')
end
local param_names = {} --- @type string[]
@@ -263,8 +321,11 @@ local function render_api_meta(_f, fun, write)
param_names[#param_names + 1] = p[1]
local pdesc = p[3]
if pdesc then
- local pdesc_a = split(norm_text(pdesc))
- write('--- @param ' .. p[1] .. ' ' .. p[2] .. ' ' .. pdesc_a[1])
+ local s = '--- @param ' .. p[1] .. ' ' .. p[2] .. ' '
+ local indent = #('@param ' .. p[1] .. ' ')
+ pdesc = text_utils.md_to_vimdoc(pdesc, #s, indent, 74, true)
+ 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
@@ -276,11 +337,10 @@ local function render_api_meta(_f, fun, write)
end
end
if fun.returns ~= '' then
- if fun.returns_desc then
- write('--- @return ' .. fun.returns .. ' : ' .. fun.returns_desc)
- else
- write('--- @return ' .. fun.returns)
- end
+ local ret_desc = fun.returns_desc and ' : ' .. fun.returns_desc or ''
+ ret_desc = text_utils.md_to_vimdoc(ret_desc, 0, 0, 74)
+ local ret = LUA_API_RETURN_OVERRIDES[fun.name] or fun.returns
+ write('--- @return ' .. ret .. ret_desc)
end
local param_str = table.concat(param_names, ', ')
@@ -290,8 +350,6 @@ end
--- @return table<string, vim.EvalFn>
local function get_api_keysets_meta()
local mpack_f = assert(io.open(DEP_API_METADATA, 'rb'))
-
- --- @diagnostic disable-next-line:no-unknown
local metadata = assert(vim.mpack.decode(mpack_f:read('*all')))
local ret = {} --- @type table<string, vim.EvalFn>
@@ -302,7 +360,7 @@ local function get_api_keysets_meta()
for _, k in ipairs(keysets) do
local params = {}
for _, key in ipairs(k.keys) do
- table.insert(params, {key..'?', api_type(k.types[key] or 'any')})
+ table.insert(params, { key .. '?', api_type(k.types[key] or 'any') })
end
ret[k.name] = {
signature = 'NA',
@@ -342,50 +400,45 @@ local function render_eval_meta(f, fun, write)
local params = process_params(fun.params)
- if fun.signature then
- 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
+ write('')
+ if fun.deprecated then
+ write('--- @deprecated')
+ end
- local req_args = type(fun.args) == 'table' and fun.args[1] or fun.args or 0
+ local desc = fun.desc
- for i, param in ipairs(params) do
- local pname, ptype = param[1], param[2]
- local optional = (pname ~= '...' and i > req_args) and '?' or ''
- write(string.format('--- @param %s%s %s', pname, optional, ptype))
+ 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
- if fun.returns ~= false then
- write('--- @return ' .. (fun.returns or 'any'))
- end
+ local req_args = type(fun.args) == 'table' and fun.args[1] or fun.args or 0
- write(render_fun_sig(funname, params))
+ for i, param in ipairs(params) do
+ local pname, ptype = param[1], param[2]
+ local optional = (pname ~= '...' and i > req_args) and '?' or ''
+ write(string.format('--- @param %s%s %s', pname, optional, ptype))
+ end
- return
+ if fun.returns ~= false then
+ write('--- @return ' .. (fun.returns or 'any'))
end
- print('no doc for', funname)
+ write(render_fun_sig(funname, params))
end
---- @type table<string,true>
-local rendered_tags = {}
-
--- @param name string
--- @param fun vim.EvalFn
--- @param write fun(line: string)
local function render_sig_and_tag(name, fun, write)
+ if not fun.signature then
+ return
+ end
+
local tags = { '*' .. name .. '()*' }
if fun.tags then
@@ -396,7 +449,7 @@ local function render_sig_and_tag(name, fun, write)
local tag = table.concat(tags, ' ')
local siglen = #fun.signature
- local conceal_offset = 2*(#tags - 1)
+ local conceal_offset = 2 * (#tags - 1)
local tag_pad_len = math.max(1, 80 - #tag + conceal_offset)
if siglen + #tag > 80 then
@@ -419,24 +472,17 @@ local function render_eval_doc(f, fun, write)
return
end
- local desc = fun.desc
-
- if not desc then
+ if f:find('__%d+$') then
write(fun.signature)
- return
+ else
+ render_sig_and_tag(fun.name or f, fun, write)
end
- local name = fun.name or f
-
- if rendered_tags[name] then
- write(fun.signature)
- else
- render_sig_and_tag(name, fun, write)
- rendered_tags[name] = true
+ if not fun.desc then
+ return
end
- desc = vim.trim(desc)
- local desc_l = split(desc)
+ 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
@@ -473,7 +519,7 @@ local function render_option_default(d, vimdoc)
end
end
- if dt == "" or dt == nil or type(dt) == 'function' then
+ if dt == '' or dt == nil or type(dt) == 'function' then
dt = d.meta
end
@@ -481,22 +527,22 @@ local function render_option_default(d, vimdoc)
if not vimdoc then
v = vim.inspect(dt) --[[@as string]]
else
- v = type(dt) == 'string' and '"'..dt..'"' or tostring(dt)
+ 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',
+ 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)
+ v = v:gsub(vim.pesc(value), '$' .. name)
end
end
@@ -509,31 +555,55 @@ end
local function render_option_meta(_f, opt, write)
write('')
for _, l in ipairs(split(norm_text(opt.desc))) do
- write('--- '..l)
+ write('--- ' .. l)
end
- write('--- @type '..OPTION_TYPES[opt.type])
- write('vim.o.'..opt.full_name..' = '..render_option_default(opt.defaults))
+ write('--- @type ' .. OPTION_TYPES[opt.type])
+ 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)
+ write('vim.o.' .. opt.abbreviation .. ' = vim.o.' .. opt.full_name)
end
for _, s in pairs {
- {'wo', 'window'},
- {'bo', 'buffer'},
- {'go', 'global'},
+ { 'wo', 'window' },
+ { 'bo', 'buffer' },
+ { '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)
+ 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)
+ 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)
@@ -541,14 +611,14 @@ local function scope_to_doc(s)
global = 'global',
buffer = 'local to buffer',
window = 'local to window',
- tab = 'local to tab page'
+ tab = 'local to tab page',
}
if #s == 1 then
return m[s[1]]
end
assert(s[1] == 'global')
- return 'global or '..m[s[2]]..' |global-local|'
+ return 'global or ' .. m[s[2]] .. ' |global-local|'
end
-- @param o vim.option_meta
@@ -574,6 +644,21 @@ local function scope_more_doc(o)
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
@@ -596,29 +681,41 @@ local function get_option_meta()
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 == 'bool' then
+ tags[#tags + 1] = opt.abbreviation
+ if opt.type == 'boolean' then
for i = 1, #tags do
- tags[#tags+1] = 'no'..tags[i]
+ tags[#tags + 1] = 'no' .. tags[i]
end
end
for i, t in ipairs(tags) do
- tags[i] = "'"..t.."'"
+ tags[i] = "'" .. t .. "'"
end
for _, t in ipairs(opt.tags or {}) do
- tags[#tags+1] = t
+ tags[#tags + 1] = t
end
for i, t in ipairs(tags) do
- tags[i] = "*"..t.."*"
+ tags[i] = '*' .. t .. '*'
end
return tags
@@ -630,10 +727,10 @@ end
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 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)
+ write(tag_pad .. tag_str)
local name_str --- @type string
if opt.abbreviation then
@@ -642,26 +739,65 @@ local function render_option_doc(_f, opt, write)
name_str = string.format("'%s'", opt.full_name)
end
- local otype = opt.type == 'bool' and 'boolean' or opt.type
+ 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 = #string.format('%s%s%s (', name_str, pad, otype)
--- @type string
- v = v:gsub('\n', '\n'..string.rep(' ', deflen - 2))
+ v = v:gsub('\n', '\n' .. string.rep(' ', deflen - 2))
end
write(string.format('%s%s%s\t(default %s)', name_str, pad, otype, v))
else
write(string.format('%s\t%s', name_str, otype))
end
- write('\t\t\t'..scope_to_doc(opt.scope)..scope_more_doc(opt))
+ 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('\\<', '<'))
+ 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
@@ -751,21 +887,37 @@ local CONFIG = {
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:'
+ ' 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[]
+ 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
+ from_lines[#from_lines + 1] = line
if line:match(from) then
break
end
diff --git a/scripts/gen_filetype.lua b/scripts/gen_filetype.lua
index 42478a1082..18b53f1ea4 100644
--- a/scripts/gen_filetype.lua
+++ b/scripts/gen_filetype.lua
@@ -8,18 +8,18 @@ if do_not_run then
return
end
-local filetype_vim = "runtime/filetype.vim"
-local filetype_lua = "runtime/lua/vim/filetype.lua"
+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,
+ ['for'] = true,
+ ['or'] = true,
+ ['and'] = true,
+ ['end'] = true,
+ ['do'] = true,
+ ['if'] = true,
+ ['while'] = true,
+ ['repeat'] = true,
}
local sections = {
@@ -28,42 +28,42 @@ local sections = {
pattern = { str = {}, func = {} },
}
-local specialchars = "%*%?\\%$%[%]%{%}"
+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
+ 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
+ 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
+ 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
+ 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("%?", ".")
+ 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
+ if type(ft) == 'string' then
sections.pattern.str[p] = ft
else
sections.pattern.func[p] = ft
@@ -80,14 +80,16 @@ end
local function parse_line(line)
local pat, ft
- pat, ft = line:match("^%s*au%a* Buf[%a,]+%s+(%S+)%s+setf%s+(%S+)")
+ 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+)")
+ 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)
+ return add_pattern(pat, function()
+ return func
+ end)
end
end
end
@@ -95,12 +97,12 @@ end
local unparsed = {}
local full_line
for line in io.lines(filetype_vim) do
- local cont = string.match(line, "^%s*\\%s*(.*)$")
+ local cont = string.match(line, '^%s*\\%s*(.*)$')
if cont then
- full_line = full_line .. " " .. cont
+ 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
+ if not parse_line(full_line) and string.find(full_line, '^%s*au%a* Buf') then
table.insert(unparsed, full_line)
end
end
@@ -109,40 +111,46 @@ for line in io.lines(filetype_vim) do
end
if #unparsed > 0 then
- print("Failed to parse the following patterns:")
+ 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)
+ 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
+ elseif type(ft) == 'function' then
local func = ft()
- if string.find(key, "%A") or keywords[key] then
- key = string.format("[\"%s\"]", key)
+ 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
+ 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
+ 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
+ elseif string.find(arg, '%(') then
-- Function argument
- return string.format([[%s%s = function() vim.fn["%s"](vim.fn.%s) end,]], indent, key, func, arg)
+ return string.format(
+ [[%s%s = function() vim.fn["%s"](vim.fn.%s) end,]],
+ indent,
+ key,
+ func,
+ arg
+ )
else
assert(false, arg)
end
@@ -153,7 +161,7 @@ do
local lines = {}
local start = false
for line in io.lines(filetype_lua) do
- if line:match("^%s+-- END [A-Z]+$") then
+ if line:match('^%s+-- END [A-Z]+$') then
start = false
end
@@ -161,14 +169,14 @@ do
table.insert(lines, line)
end
- local indent, section = line:match("^(%s+)-- BEGIN ([A-Z]+)$")
+ 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})
+ table.insert(sorted, { [k] = v })
end
table.sort(sorted, function(a, b)
@@ -182,7 +190,7 @@ do
sorted = {}
for k, v in pairs(t.func) do
- table.insert(sorted, {[k] = v})
+ table.insert(sorted, { [k] = v })
end
table.sort(sorted, function(a, b)
@@ -195,7 +203,7 @@ do
end
end
end
- local f = io.open(filetype_lua, "w")
- f:write(table.concat(lines, "\n") .. "\n")
+ 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
index 633207e018..43040151eb 100644
--- a/scripts/gen_help_html.lua
+++ b/scripts/gen_help_html.lua
@@ -2,32 +2,38 @@
--
-- NOTE: :helptags checks for duplicate tags, whereas this script checks _links_ (to tags).
--
+-- 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. Run `make helptags` first; this script depends on vim.fn.taglist().
--- 2. nvim -V1 -es --clean +"lua require('scripts.gen_help_html').gen('./build/runtime/doc/', 'target/dir/')"
+-- 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()"
+-- 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()"
+-- 1. nvim -V1 -es +"lua require('scripts.gen_help_html')._test()" +q
--
-- NOTES:
--- * gen() and validate() are the primary entrypoints. validate() only exists because gen() is too
--- slow (~1 min) to run in per-commit CI.
+-- * 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
-local helpfiles = nil
-local invalid_links = {}
-local invalid_urls = {}
-local invalid_spelling = {}
+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',
@@ -36,6 +42,14 @@ local spell_dict = {
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 = {
+ ['backers.txt'] = true,
+ ['news.txt'] = { 'tree-sitter' }, -- in news, may refer to the upstream "tree-sitter" library
}
local language = nil
@@ -60,31 +74,33 @@ local new_layout = {
-- TODO: These known invalid |links| require an update to the relevant docs.
local exclude_invalid = {
- ["'string'"] = "eval.txt",
+ ["'string'"] = 'eval.txt',
Query = 'treesitter.txt',
matchit = 'vim_diff.txt',
- ["set!"] = "treesitter.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://'] = '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,
+ ['backers.txt'] = true,
}
local function tofile(fname, text)
@@ -97,24 +113,24 @@ local function tofile(fname, text)
end
end
+---@type fun(s: string): string
local function html_esc(s)
- return s:gsub(
- '&', '&amp;'):gsub(
- '<', '&lt;'):gsub(
- '>', '&gt;')
+ 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'),
+ return vim.fn.substitute(
+ vim.fn.iconv(s, 'latin1', 'utf-8'),
[=[[^A-Za-z0-9()'_.~-]]=],
[=[\="%".printf("%02X",char2nr(submatch(0)))]=],
- 'g')
+ 'g'
+ )
end
local function expandtabs(s)
- return s:gsub('\t', (' '):rep(8))
+ return s:gsub('\t', (' '):rep(8)) --[[ @as string ]]
end
local function to_titlecase(s)
@@ -131,13 +147,14 @@ local function to_heading_tag(text)
end
local function basename_noext(f)
- return vim.fs.basename(f:gsub('%.txt', ''))
+ 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
@@ -146,12 +163,13 @@ end
---
--- TODO: fix this in the parser instead... https://github.com/neovim/tree-sitter-vimdoc
---
---- @returns (fixed_url, removed_chars) where `removed_chars` is in the order found in the input.
+--- @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
+ for _, c in ipairs({ '.', ')' }) do
if fixed_url:sub(-1) == c then
removed_chars = c .. removed_chars
fixed_url = fixed_url:sub(1, -2)
@@ -162,7 +180,7 @@ 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 (
+ 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')
@@ -177,7 +195,7 @@ local function is_noise(line, noise_lines)
or line:find('^%s*vim?%:.*ft=help')
or line:find('^%s*vim?%:.*filetype=help')
or line:find('[*>]local%-additions[*<]')
- ) then
+ then
-- table.insert(stats.noise_lines, getbuflinestr(root, opt.buf, 0))
table.insert(noise_lines or {}, line)
return true
@@ -188,28 +206,32 @@ end
--- Creates a github issue URL at neovim/tree-sitter-vimdoc with prefilled content.
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')
+ 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.
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')
+ 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
@@ -254,6 +276,9 @@ local function trim_indent(s)
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
@@ -264,8 +289,12 @@ 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 ''
@@ -274,9 +303,11 @@ 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"
+ 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
@@ -295,16 +326,15 @@ local function ignore_parse_error(fname, s)
if ignore_errors[vim.fs.basename(fname)] then
return true
end
- return (
- -- Ignore parse errors for unclosed tag.
- -- This is common in vimdocs and is treated as plaintext by :help.
- s:find("^[`'|*]")
- )
+ -- 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
- while true do
+ local p = node ---@type TSNode?
+ while p do
p = p:parent()
if not p or p:type() == 'help_file' then
break
@@ -316,6 +346,7 @@ local function has_ancestor(node, ancestor_name)
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
@@ -349,6 +380,11 @@ local function validate_url(text, fname)
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
@@ -377,13 +413,24 @@ local function visit_validate(root, level, lang_tree, opt, stats)
-- 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))
+ 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 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
- invalid_spelling[text_nopunct] = invalid_spelling[text_nopunct] or {}
- invalid_spelling[text_nopunct][vim.fs.basename(opt.fname)] = node_text(root:parent())
+ 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))
@@ -395,29 +442,47 @@ 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
+ 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
+ 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
+ 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
local text = ''
- local trimmed
-- Gets leading whitespace of `node`.
local function ws(node)
node = node or root
@@ -435,6 +500,7 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
return string.format('%s%s', ws_, vim.treesitter.get_node_text(node, opt.buf))
end
+ local trimmed ---@type string
if root:named_child_count() == 0 or node_name == 'ERROR' then
text = node_text()
trimmed = html_esc(trim(text))
@@ -450,7 +516,7 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
trimmed = trim(text)
end
- if node_name == 'help_file' then -- root node
+ if node_name == 'help_file' then -- root node
return text
elseif node_name == 'url' then
local fixed_url, removed_chars = fix_url(trimmed)
@@ -459,18 +525,24 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
return 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.
+ return '' -- Discard common "noise" lines.
end
-- Remove "===" and tags from ToC text.
local hname = (node_text():gsub('%-%-%-%-+', ''):gsub('%=%=%=%=+', ''):gsub('%*.*%*', ''))
-- Use the first *tag* node as the heading anchor, if any.
local tagnode = first(root, 'tag')
-- Use the *tag* as the heading anchor id, if possible.
- local tagname = tagnode and url_encode(node_text(tagnode:child(1), false)) or to_heading_tag(hname)
+ local tagname = tagnode and url_encode(node_text(tagnode:child(1), false))
+ or to_heading_tag(hname)
if node_name == 'h1' or #headings == 0 then
- table.insert(headings, { name = hname, subheadings = {}, tag = tagname })
+ ---@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 })
+ 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, text, el)
@@ -490,11 +562,16 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
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.
+ 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())
+ 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()
@@ -520,12 +597,17 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
if ignored then
return text
end
- local s = ('%s<a href="%s#%s">%s</a>'):format(ws(), helppage, url_encode(tagname), html_esc(tagname))
+ 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
+ elseif vim.list_contains({ 'codespan', 'keycode' }, node_name) then
if root:has_error() then
return text
end
@@ -541,24 +623,28 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
elseif node_name == 'language' then
language = node_text(root)
return ''
- elseif node_name == 'code' then -- Highlighted codeblock (child).
+ elseif node_name == 'code' then -- Highlighted codeblock (child).
if is_blank(text) then
return ''
end
- local code
+ local code ---@type string
if language then
- code = ('<pre><code class="language-%s">%s</code></pre>'):format(language,trim(trim_indent(text), 2))
+ 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
+ elseif node_name == 'tag' then -- anchor
if root:has_error() then
return text
end
- local in_heading = vim.list_contains({'h1', 'h2', 'h3'}, parent)
- local cssclass = (not in_heading and get_indent(node_text()) > 8) and 'help-tag-right' or 'help-tag'
+ local in_heading = vim.list_contains({ 'h1', 'h2', 'h3' }, parent)
+ local cssclass = (not in_heading and get_indent(node_text()) > 8) 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.
@@ -567,14 +653,29 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
end
local el = in_heading and 'span' or 'code'
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)
+ 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()))
+ s = fix_tab_after_conceal(s, node_text(root:next_sibling()))
end
if in_heading and prev ~= 'tag' then
-- Don't set "id", let the heading use the tag as its "id" (used by search engines).
- s = ('%s<%s class="%s"><a href="#%s">%s</a></%s>'):format(ws(), el, cssclass, encoded_tagname, trimmed, el)
+ s = ('%s<%s class="%s"><a href="#%s">%s</a></%s>'):format(
+ ws(),
+ el,
+ cssclass,
+ encoded_tagname,
+ trimmed,
+ el
+ )
-- 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>
@@ -593,21 +694,31 @@ local function visit_node(root, level, lang_tree, headings, opt, stats)
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.
+ 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)
+ node_name,
+ get_bug_url_nvim(opt.fname, opt.to_fname, sample_text, node_name),
+ trimmed
+ ),
+ ('unknown-token:"%s"'):format(node_name)
end
end
-local function get_helpfiles(include)
- local dir = './build/runtime/doc'
+--- @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
+ 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
@@ -633,7 +744,7 @@ 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/]]
+ vim.cmd [[set runtimepath^=./build/lib/nvim/]]
end
end
@@ -641,15 +752,17 @@ end
---
--- @param fname string help file to parse
--- @param parser_path string? path to non-default vimdoc.so
---- @returns lang_tree, bufnr
+--- @return vim.treesitter.LanguageTree, integer (lang_tree, bufnr)
local function parse_buf(fname, parser_path)
- local buf
+ local buf ---@type integer
if type(fname) == 'string' then
- vim.cmd('split '..vim.fn.fnameescape(fname)) -- Filename.
+ 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.
+ vim.cmd('sbuffer ' .. tostring(fname)) -- Buffer number.
end
if parser_path then
vim.treesitter.language.add('vimdoc', { path = parser_path })
@@ -664,14 +777,14 @@ end
---
--- @param fname string help file to validate
--- @param parser_path string? path to non-default vimdoc.so
---- @returns { invalid_links: number, parse_errors: string[] }
+--- @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, parser_path)
for _, tree in ipairs(lang_tree:trees()) do
- visit_validate(tree:root(), 0, tree, { buf = buf, fname = fname, }, stats)
+ visit_validate(tree:root(), 0, tree, { buf = buf, fname = fname }, stats)
end
lang_tree:destroy()
vim.cmd.close()
@@ -685,15 +798,17 @@ end
--- @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
---
---- @returns html, stats
+--- @return string html
+--- @return table stats
local function gen_one(fname, to_fname, old, commit, parser_path)
local stats = {
noise_lines = {},
parse_errors = {},
- first_tags = {}, -- Track the first few tags in doc.
+ first_tags = {}, -- Track the first few tags in doc.
}
local lang_tree, buf = parse_buf(fname, parser_path)
- local headings = {} -- Headings (for ToC). 2-dimensional: h1 contains h2/h3.
+ ---@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 = ([[
@@ -777,9 +892,17 @@ local function gen_one(fname, to_fname, old, commit, parser_path)
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))
+ main = main
+ .. (
+ visit_node(
+ tree:root(),
+ 0,
+ tree,
+ headings,
+ { buf = buf, old = old, fname = fname, to_fname = to_fname, indent = 1 },
+ stats
+ )
+ )
end
main = ([[
@@ -809,8 +932,16 @@ local function gen_one(fname, to_fname, old, commit, parser_path)
<hr/>
%s
</div>
- ]]):format(logo_svg, stats.first_tags[2] or '', stats.first_tags[1] or '', title, vim.fs.basename(fname), main)
+ ]]):format(
+ logo_svg,
+ stats.first_tags[2] or '',
+ stats.first_tags[1] or '',
+ title,
+ vim.fs.basename(fname),
+ main
+ )
+ ---@type string
local toc = [[
<div class="col-narrow toc">
<div><a href="index.html">Main</a></div>
@@ -819,13 +950,17 @@ local function gen_one(fname, to_fname, old, commit, parser_path)
<hr/>
]]
- local n = 0 -- Count of all headings + subheadings.
- for _, h1 in ipairs(headings) do n = n + 1 + #h1.subheadings end
+ 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.
+ 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)
+ toc = toc
+ .. ('<div class="help-toc-h2"><a href="#%s">%s</a></div>\n'):format(h2.tag, h2.name)
end
end
toc = toc .. '</div>'
@@ -859,11 +994,16 @@ local function gen_one(fname, to_fname, old, commit, parser_path)
</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)
+ 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)
+ 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
@@ -1033,24 +1173,41 @@ local function gen_css(fname)
tofile(fname, css)
end
-function M._test()
- tagmap = get_helptags('./build/runtime/doc')
- helpfiles = get_helpfiles()
+-- Testing
- local function ok(cond, expected, actual)
- assert((not expected and not actual) or (expected and actual), 'if "expected" is given, "actual" is also required')
- if expected then
- return assert(cond, ('expected %s, got: %s'):format(vim.inspect(expected), vim.inspect(actual)))
- else
- return assert(cond)
- end
- end
- local function eq(expected, actual)
- return ok(expected == actual, expected, actual)
+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.fn.expand('$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['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 ]]))
@@ -1060,7 +1217,10 @@ function M._test()
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'))
+ 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)
@@ -1078,33 +1238,50 @@ function M._test()
eq('https://example.com', fixed_url)
eq('', removed_chars)
- print('all tests passed')
+ 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('./build/runtime/doc', '/path/to/neovim.github.io/_site/doc/', {'api.txt', 'autocmd.txt', 'channel.txt'}, nil)
+--- 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 table|nil Process only these filenames. Example: {'api.txt', 'autocmd.txt', 'channel.txt'}
+--- @param include string[]|nil Process only these filenames. Example: {'api.txt', 'autocmd.txt', 'channel.txt'}
---
---- @returns info dict
+--- @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.fn.expand(d)) == 1 end, 'valid directory'},
- to_dir={to_dir, 's'},
- include={include, 't', true},
- commit={commit, 's', true},
- parser_path={parser_path, function(f) return f == nil or vim.fn.filereadable(vim.fn.expand(f)) == 1 end, 'valid vimdoc.{so,dll} filepath'},
+ vim.validate {
+ help_dir = {
+ help_dir,
+ function(d)
+ return vim.fn.isdirectory(vim.fn.expand(d)) == 1
+ end,
+ 'valid directory',
+ },
+ to_dir = { to_dir, 's' },
+ include = { include, 't', true },
+ commit = { commit, 's', true },
+ parser_path = {
+ parser_path,
+ function(f)
+ return f == nil or vim.fn.filereadable(vim.fn.expand(f)) == 1
+ end,
+ 'valid vimdoc.{so,dll} filepath',
+ },
}
local err_count = 0
ensure_runtimepath()
tagmap = get_helptags(vim.fn.expand(help_dir))
- helpfiles = get_helpfiles(include)
+ helpfiles = get_helpfiles(help_dir, include)
to_dir = vim.fn.expand(to_dir)
parser_path = parser_path and vim.fn.expand(parser_path) or nil
@@ -1117,13 +1294,20 @@ function M.gen(help_dir, to_dir, include, commit, parser_path)
local to_fname = ('%s/%s'):format(to_dir, get_helppage(helpfile))
local html, stats = gen_one(f, to_fname, not new_layout[helpfile], commit or '?', parser_path)
tofile(to_fname, html)
- print(('generated (%-4s errors): %-15s => %s'):format(#stats.parse_errors, helpfile, vim.fs.basename(to_fname)))
+ print(
+ ('generated (%-4s errors): %-15s => %s'):format(
+ #stats.parse_errors,
+ helpfile,
+ vim.fs.basename(to_fname)
+ )
+ )
err_count = err_count + #stats.parse_errors
end
print(('generated %d html pages'):format(#helpfiles))
print(('total errors: %d'):format(err_count))
print(('invalid tags:\n%s'):format(vim.inspect(invalid_links)))
+ --- @type nvim.gen_help_html.gen_result
return {
helpfiles = helpfiles,
err_count = err_count,
@@ -1131,45 +1315,118 @@ function M.gen(help_dir, to_dir, include, commit, parser_path)
}
end
--- 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.
---
--- @returns results dict
+--- @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.fn.expand(d)) == 1 end, 'valid directory'},
- include={include, 't', true},
- parser_path={parser_path, function(f) return f == nil or vim.fn.filereadable(vim.fn.expand(f)) == 1 end, 'valid vimdoc.{so,dll} filepath'},
+ vim.validate {
+ help_dir = {
+ help_dir,
+ function(d)
+ return vim.fn.isdirectory(vim.fn.expand(d)) == 1
+ end,
+ 'valid directory',
+ },
+ include = { include, 't', true },
+ parser_path = {
+ parser_path,
+ function(f)
+ return f == nil or vim.fn.filereadable(vim.fn.expand(f)) == 1
+ end,
+ 'valid vimdoc.{so,dll} filepath',
+ },
}
- local err_count = 0
- local files_to_errors = {}
+ local err_count = 0 ---@type integer
+ local files_to_errors = {} ---@type table<string, string[]>
ensure_runtimepath()
tagmap = get_helptags(vim.fn.expand(help_dir))
- helpfiles = get_helpfiles(include)
+ helpfiles = get_helpfiles(help_dir, include)
parser_path = parser_path and vim.fn.expand(parser_path) or nil
for _, f in ipairs(helpfiles) do
- local helpfile = vim.fs.basename(f)
+ local helpfile = assert(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)))
+ 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,
- parse_errors = files_to_errors,
}
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.fn.expand(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 = assert(vim.fs.dirname(vim.fn.tempname()))
+ help_dir = vim.fn.expand(help_dir or '$VIMRUNTIME/doc')
+ print('doc path = ' .. vim.uv.fs_realpath(help_dir))
+
+ local rv = M.gen(
+ help_dir,
+ tmpdir,
+ -- Because gen() is slow (~30s), this test is limited to a few files.
+ { 'pi_health.txt', 'help.txt', 'index.txt', 'nvim.txt' }
+ )
+ eq(4, #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
index 6ff8dcb3f4..19fad7bab4 100644
--- a/scripts/gen_lsp.lua
+++ b/scripts/gen_lsp.lua
@@ -1,11 +1,15 @@
---[[
-Generates lua-ls annotations for lsp
+-- Generates lua-ls annotations for lsp.
+
+local USAGE = [[
+Generates lua-ls annotations for lsp.
+
USAGE:
-nvim -l scripts/gen_lsp.lua gen # this will overwrite runtime/lua/vim/lsp/_meta/protocol.lua
-nvim -l scripts/gen_lsp.lua gen --version 3.18 --build/new_lsp_types.lua
+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
---]]
+]]
+
+local DEFAULT_LSP_VERSION = '3.18'
local M = {}
@@ -14,15 +18,28 @@ local function tofile(fname, text)
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
@@ -34,11 +51,12 @@ local function read_json(opt)
end
-- Gets the Lua symbol for a given fully-qualified LSP method name.
-local function name(s)
+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
local function gen_methods(protocol)
local output = {
'-- Generated by gen_lsp.lua, keep at end of file.',
@@ -49,9 +67,35 @@ local function gen_methods(protocol)
}
local indent = (' '):rep(2)
+ --- @class vim._gen_lsp.Request
+ --- @field deprecated? string
+ --- @field documentation? string
+ --- @field messageDirection 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 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 name(a.method) < name(b.method)
+ return to_luaname(a.method) < to_luaname(b.method)
end)
for _, item in ipairs(all) do
if item.method then
@@ -61,7 +105,7 @@ local function gen_methods(protocol)
output[#output + 1] = indent .. '--- ' .. docstring
end
end
- output[#output + 1] = ("%s%s = '%s',"):format(indent, name(item.method), item.method)
+ output[#output + 1] = ("%s%s = '%s',"):format(indent, to_luaname(item.method), item.method)
end
end
output[#output + 1] = '}'
@@ -99,7 +143,14 @@ return protocol
vim.cmd.write()
end
+---@class vim._gen_lsp.opt
+---@field output_file string
+---@field version string
+---@field methods boolean
+
+---@param opt vim._gen_lsp.opt
function M.gen(opt)
+ --- @type vim._gen_lsp.Protocol
local protocol = read_json(opt)
if opt.methods then
@@ -107,25 +158,30 @@ function M.gen(opt)
end
local output = {
- '--[[',
- 'This file is autogenerated from scripts/gen_lsp.lua',
+ '--' .. '[[',
+ '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 3.18 --runtime/lua/vim/lsp/_meta/protocol.lua]=],
- '--]]',
+ ([=[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 lsp.decimal number',
+ '---@alias decimal number',
'---@alias lsp.DocumentUri string',
'---@alias lsp.URI string',
- '---@alias lsp.LSPObject table<string, lsp.LSPAny>',
- '---@alias lsp.LSPArray lsp.LSPAny[]',
- '---@alias lsp.LSPAny lsp.LSPObject|lsp.LSPArray|string|number|boolean|nil',
'',
}
local anonymous_num = 0
+ ---@type string[]
local anonym_classes = {}
local simple_types = {
@@ -136,95 +192,181 @@ function M.gen(opt)
'decimal',
}
- local function parse_type(type)
+ ---@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
- return parse_type(type.element) .. '[]'
+ 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) .. '|'
+ 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
- return 'table<' .. parse_type(type.key) .. ', ' .. parse_type(type.value) .. '>'
+ 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 anonym = { '---@class anonym' .. anonymous_num }
- for _, field in ipairs(type.value.properties) do
+ local anonymous_classname = 'lsp._anonym' .. anonymous_num
+ if prefix then
+ anonymous_classname = anonymous_classname .. '.' .. prefix
+ end
+ local anonym = vim.tbl_flatten { -- remove nil
+ anonymous_num > 1 and '' or nil,
+ '---@class ' .. anonymous_classname,
+ }
+
+ --- @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
- field.documentation = field.documentation:gsub('\n', '\n---')
- anonym[#anonym + 1] = '---' .. field.documentation
+ anonym[#anonym + 1] = _process_documentation(field.documentation)
end
anonym[#anonym + 1] = '---@field '
.. field.name
.. (field.optional and '?' or '')
.. ' '
- .. parse_type(field.type)
+ .. parse_type(field.type, prefix .. '.' .. field.name)
end
- anonym[#anonym + 1] = ''
+ -- anonym[#anonym + 1] = ''
for _, line in ipairs(anonym) do
- anonym_classes[#anonym_classes + 1] = line
+ if line then
+ anonym_classes[#anonym_classes + 1] = line
+ end
end
- return 'anonym' .. anonymous_num
+ return anonymous_classname
+
+ -- TupleType
elseif type.kind == 'tuple' then
local tuple = '{ '
for i, value in ipairs(type.items) do
- tuple = tuple .. '[' .. i .. ']: ' .. parse_type(value) .. ', '
+ tuple = tuple .. '[' .. i .. ']: ' .. parse_type(value, prefix) .. ', '
end
-- remove , at the end
tuple = tuple:sub(0, -3)
return tuple .. ' }'
end
- vim.print(type)
+
+ 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
- structure.documentation = structure.documentation:gsub('\n', '\n---')
- output[#output + 1] = '---' .. structure.documentation
+ output[#output + 1] = _process_documentation(structure.documentation)
end
- if structure.extends then
- local class_string = '---@class lsp.'
- .. structure.name
- .. ': '
- .. parse_type(structure.extends[1])
- for _, mixin in ipairs(structure.mixins or {}) do
- class_string = class_string .. ', ' .. parse_type(mixin)
- end
- output[#output + 1] = class_string
- else
- output[#output + 1] = '---@class lsp.' .. structure.name
+ 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
- field.documentation = field.documentation:gsub('\n', '\n---')
- output[#output + 1] = '---' .. field.documentation
+ output[#output + 1] = _process_documentation(field.documentation)
end
output[#output + 1] = '---@field '
.. field.name
.. (field.optional and '?' or '')
.. ' '
- .. parse_type(field.type)
+ .. 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
- enum.documentation = enum.documentation:gsub('\n', '\n---')
- output[#output + 1] = '---' .. enum.documentation
+ output[#output + 1] = _process_documentation(enum.documentation)
end
local enum_type = '---@alias lsp.' .. enum.name
for _, value in ipairs(enum.values) do
@@ -238,53 +380,77 @@ function M.gen(opt)
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
- alias.documentation = alias.documentation:gsub('\n', '\n---')
- output[#output + 1] = '---' .. alias.documentation
+ 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_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)
+ 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'))
+ 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 = nil,
- methods = nil,
+ version = DEFAULT_LSP_VERSION,
+ methods = false,
}
-for i = 1, #_G.arg do
+local command = nil
+local i = 1
+while i <= #_G.arg do
if _G.arg[i] == '--out' then
- opt.output_file = _G.arg[i + 1]
+ opt.output_file = assert(_G.arg[i + 1], '--out <outfile> needed')
+ i = i + 1
elseif _G.arg[i] == '--version' then
- opt.version = _G.arg[i + 1]
+ opt.version = assert(_G.arg[i + 1], '--version <version> needed')
+ i = i + 1
elseif _G.arg[i] == '--methods' then
opt.methods = true
- elseif vim.startswith(_G.arg[i], '--') then
- opt.output_file = _G.arg[i]:sub(3)
+ 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
-for _, a in ipairs(arg) do
- if M[a] then
- M[a](opt)
- 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
new file mode 100755
index 0000000000..22df411a35
--- /dev/null
+++ b/scripts/gen_vimdoc.lua
@@ -0,0 +1,953 @@
+#!/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 text_utils = require('scripts.text_utils')
+
+local fmt = string.format
+
+local wrap = text_utils.wrap
+local md_to_vimdoc = text_utils.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
+---
+--- 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 fmt('*%s%s*', 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 = {
+ 'highlight.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/highlight.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, {
+ 'highlight',
+ '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 fmt('*%s%s*', 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',
+ '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%.', '')
+ 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 = {
+ 'treesitter.lua',
+ 'language.lua',
+ 'query.lua',
+ 'highlighter.lua',
+ 'languagetree.lua',
+ 'dev.lua',
+ },
+ files = {
+ 'runtime/lua/vim/treesitter.lua',
+ 'runtime/lua/vim/treesitter/',
+ },
+ section_fmt = function(name)
+ if name:lower() == 'treesitter' then
+ return 'Lua module: vim.treesitter'
+ end
+ return 'Lua module: vim.treesitter.' .. name:lower()
+ end,
+ helptag_fmt = function(name)
+ if name:lower() == 'treesitter' then
+ return '*lua-treesitter-core*'
+ end
+ return '*lua-treesitter-' .. name:lower() .. '*'
+ 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_param(p)
+ return not p.access and not contains(p.name, { '_', 'self' })
+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:'
+ else
+ desc = desc .. 'A table with the following fields:'
+ end
+ end
+
+ local desc_append = {}
+ for _, f in ipairs(cls.fields) do
+ 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
+
+ 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
+local function render_fields_or_params(xs, generics, classes, exclude_types)
+ local ret = {} --- @type string[]
+
+ xs = vim.tbl_filter(should_render_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, desc = p.name, p.type, 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>
+local function render_class(class, classes)
+ 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)
+ 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>
+local function render_classes(classes)
+ local ret = {} --- @type string[]
+
+ for _, class in vim.spairs(classes) do
+ ret[#ret + 1] = render_class(class, classes)
+ 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
+
+ local proto = fun.table and nm or nm .. '(' .. table.concat(args, ', ') .. ')'
+
+ if not cfg.fn_helptag_fmt then
+ cfg.fn_helptag_fmt = fn_helptag_fmt_common
+ end
+
+ 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.desc then
+ table.insert(ret, md_to_vimdoc(fun.desc, INDENTATION, INDENTATION, TEXT_WIDTH))
+ end
+
+ if fun.since then
+ local since = tonumber(fun.since)
+ local info = nvim_api_info()
+ if since and (since > info.level or since == info.level and info.prerelease) then
+ fun.notes = fun.notes or {}
+ table.insert(fun.notes, { desc = 'This API is pre-release (unstable).' })
+ end
+ 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)
+ 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
+ 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')
+ 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(assert(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_tag = cfg.helptag_fmt(sectname)
+
+ if funs_txt == '' and #section_docs == 0 then
+ return
+ end
+
+ return {
+ name = sectname,
+ title = cfg.section_fmt(sectname),
+ help_tag = help_tag,
+ 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
+
+ if section.doc and #section.doc > 0 then
+ table.insert(doc, '\n\n')
+ vim.list_extend(doc, section.doc)
+ 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)
+ local sections = {} --- @type table<string,nvim.gen_vimdoc.Section>
+
+ expand_files(cfg.files)
+
+ --- @type table<string,{[1]:table<string,nvim.luacats.parser.class>, [2]: nvim.luacats.parser.fun[], [3]: 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 pairs(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 pairs(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
+ local funs_txt = render_funs(funs, all_classes, cfg)
+ if next(classes) then
+ local classes_txt = render_classes(classes)
+ 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 = assert(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
+ local add_sep_and_header = not vim.tbl_contains(cfg.append_only or {}, f)
+ table.insert(docs, 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 pairs(config) do
+ gen_target(cfg)
+ end
+end
+
+run()
diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py
deleted file mode 100755
index 8ed88cb8f5..0000000000
--- a/scripts/gen_vimdoc.py
+++ /dev/null
@@ -1,1447 +0,0 @@
-#!/usr/bin/env python3
-"""Generates Nvim :help docs from C/Lua docstrings, using Doxygen.
-
-Also generates *.mpack files. To inspect the *.mpack structure:
- :new | put=v:lua.vim.inspect(v:lua.vim.mpack.decode(readfile('runtime/doc/api.mpack','B')))
-
-Flow:
- main
- extract_from_xml
- fmt_node_as_vimhelp \
- para_as_map } recursive
- update_params_map /
- render_node
-
-TODO: eliminate this script and use Lua+treesitter (requires parsers for C and
-Lua markdown-style 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 at `text_width - 8` characters 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.
-"""
-import argparse
-import os
-import re
-import sys
-import shutil
-import textwrap
-import subprocess
-import collections
-import msgpack
-import logging
-from typing import Tuple
-from pathlib import Path
-
-from xml.dom import minidom
-Element = minidom.Element
-Document = minidom.Document
-
-MIN_PYTHON_VERSION = (3, 6)
-MIN_DOXYGEN_VERSION = (1, 9, 0)
-
-if sys.version_info < MIN_PYTHON_VERSION:
- print("requires Python {}.{}+".format(*MIN_PYTHON_VERSION))
- sys.exit(1)
-
-doxygen_version = tuple((int(i) for i in subprocess.check_output(["doxygen", "-v"],
- universal_newlines=True).split()[0].split('.')))
-
-if doxygen_version < MIN_DOXYGEN_VERSION:
- print("\nRequires doxygen {}.{}.{}+".format(*MIN_DOXYGEN_VERSION))
- print("Your doxygen version is {}.{}.{}\n".format(*doxygen_version))
- sys.exit(1)
-
-
-# Need a `nvim` that supports `-l`, try the local build
-nvim_path = Path(__file__).parent / "../build/bin/nvim"
-if nvim_path.exists():
- nvim = str(nvim_path)
-else:
- # Until 0.9 is released, use this hacky way to check that "nvim -l foo.lua" works.
- nvim_out = subprocess.check_output(['nvim', '-h'], universal_newlines=True)
- nvim_version = [line for line in nvim_out.split('\n')
- if '-l ' in line]
- if len(nvim_version) == 0:
- print((
- "\nYou need to have a local Neovim build or a `nvim` version 0.9 for `-l` "
- "support to build the documentation."))
- sys.exit(1)
- nvim = 'nvim'
-
-
-# DEBUG = ('DEBUG' in os.environ)
-INCLUDE_C_DECL = ('INCLUDE_C_DECL' in os.environ)
-INCLUDE_DEPRECATED = ('INCLUDE_DEPRECATED' in os.environ)
-
-log = logging.getLogger(__name__)
-
-LOG_LEVELS = {
- logging.getLevelName(level): level for level in [
- logging.DEBUG, logging.INFO, logging.ERROR
- ]
-}
-
-text_width = 78
-indentation = 4
-script_path = os.path.abspath(__file__)
-base_dir = os.path.dirname(os.path.dirname(script_path))
-out_dir = os.path.join(base_dir, 'tmp-{target}-doc')
-filter_cmd = '%s %s' % (sys.executable, script_path)
-msgs = [] # Messages to show on exit.
-lua2dox = os.path.join(base_dir, 'scripts', 'lua2dox.lua')
-
-CONFIG = {
- 'api': {
- 'mode': 'c',
- 'filename': 'api.txt',
- # Section ordering.
- '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',
- ],
- # List of files/directories for doxygen to read, relative to `base_dir`
- 'files': ['src/nvim/api'],
- # file patterns used by doxygen
- 'file_patterns': '*.h *.c',
- # Only function with this prefix are considered
- 'fn_name_prefix': 'nvim_',
- # Section name overrides.
- 'section_name': {
- 'vim.c': 'Global',
- },
- # For generated section names.
- 'section_fmt': lambda name: f'{name} Functions',
- # Section helptag.
- 'helptag_fmt': lambda name: f'*api-{name.lower()}*',
- # Per-function helptag.
- 'fn_helptag_fmt': lambda fstem, name, istbl: f'*{name}()*',
- # Module name overrides (for Lua).
- 'module_override': {},
- # Append the docs for these modules, do not start a new section.
- 'append_only': [],
- },
- 'lua': {
- 'mode': 'lua',
- 'filename': 'lua.txt',
- 'section_order': [
- 'highlight.lua',
- 'regex.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',
- 'secure.lua',
- 'version.lua',
- 'iter.lua',
- 'snippet.lua',
- 'text.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/highlight.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/_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/spell.lua',
- ],
- 'file_patterns': '*.lua',
- 'fn_name_prefix': '',
- 'fn_name_fmt': lambda fstem, name: (
- name if fstem in [ 'vim.iter' ] else
- f'vim.{name}' if fstem in [ '_editor', 'vim.regex'] else
- f'vim.{name}' if fstem == '_options' and not name[0].isupper() else
- f'{fstem}.{name}' if fstem.startswith('vim') else
- name
- ),
- 'section_name': {
- 'lsp.lua': 'core',
- '_inspector.lua': 'inspector',
- },
- 'section_fmt': lambda name: (
- 'Lua module: vim' if name.lower() == '_editor' else
- 'LUA-VIMSCRIPT BRIDGE' if name.lower() == '_options' else
- f'VIM.{name.upper()}' if name.lower() in [ 'highlight', 'mpack', 'json', 'base64', 'diff', 'spell', 'regex' ] else
- 'VIM' if name.lower() == 'builtin' else
- f'Lua module: vim.{name.lower()}'),
- 'helptag_fmt': lambda name: (
- '*lua-vim*' if name.lower() == '_editor' else
- '*lua-vimscript*' if name.lower() == '_options' else
- f'*vim.{name.lower()}*'),
- 'fn_helptag_fmt': lambda fstem, name, istbl: (
- f'*vim.opt:{name.split(":")[-1]}()*' if ':' in name and name.startswith('Option') else
- # Exclude fstem for methods
- f'*{name}()*' if ':' in name else
- f'*vim.{name}()*' if fstem.lower() == '_editor' else
- f'*vim.{name}*' if fstem.lower() == '_options' and istbl else
- # Prevents vim.regex.regex
- f'*{fstem}()*' if fstem.endswith('.' + name) else
- f'*{fstem}.{name}{"" if istbl else "()"}*'
- ),
- 'module_override': {
- # `shared` functions are exposed on the `vim` module.
- 'shared': 'vim',
- '_inspector': 'vim',
- 'uri': 'vim',
- 'ui': 'vim.ui',
- 'loader': 'vim.loader',
- 'filetype': 'vim.filetype',
- 'keymap': 'vim.keymap',
- 'fs': 'vim.fs',
- 'highlight': 'vim.highlight',
- 'secure': 'vim.secure',
- 'version': 'vim.version',
- 'iter': 'vim.iter',
- 'diff': 'vim',
- 'builtin': 'vim',
- 'mpack': 'vim.mpack',
- 'json': 'vim.json',
- 'base64': 'vim.base64',
- 'regex': 'vim.regex',
- 'spell': 'vim.spell',
- 'snippet': 'vim.snippet',
- 'text': 'vim.text',
- },
- 'append_only': [
- 'shared.lua',
- ],
- },
- 'lsp': {
- 'mode': 'lua',
- 'filename': 'lsp.txt',
- 'section_order': [
- 'lsp.lua',
- 'buf.lua',
- 'diagnostic.lua',
- 'codelens.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',
- ],
- 'file_patterns': '*.lua',
- 'fn_name_prefix': '',
- 'section_name': {'lsp.lua': 'lsp'},
- 'section_fmt': lambda name: (
- 'Lua module: vim.lsp'
- if name.lower() == 'lsp'
- else f'Lua module: vim.lsp.{name.lower()}'),
- 'helptag_fmt': lambda name: (
- '*lsp-core*'
- if name.lower() == 'lsp'
- else f'*lsp-{name.lower()}*'),
- 'fn_helptag_fmt': lambda fstem, name, istbl: (
- f'*vim.lsp.{name}{"" if istbl else "()"}*' if fstem == 'lsp' and name != 'client' else
- # HACK. TODO(justinmk): class/structure support in lua2dox
- '*vim.lsp.client*' if 'lsp.client' == f'{fstem}.{name}' else
- f'*vim.lsp.{fstem}.{name}{"" if istbl else "()"}*'),
- 'module_override': {},
- 'append_only': [],
- },
- 'diagnostic': {
- 'mode': 'lua',
- 'filename': 'diagnostic.txt',
- 'section_order': [
- 'diagnostic.lua',
- ],
- 'files': ['runtime/lua/vim/diagnostic.lua'],
- 'file_patterns': '*.lua',
- 'fn_name_prefix': '',
- 'include_tables': False,
- 'section_name': {'diagnostic.lua': 'diagnostic'},
- 'section_fmt': lambda _: 'Lua module: vim.diagnostic',
- 'helptag_fmt': lambda _: '*diagnostic-api*',
- 'fn_helptag_fmt': lambda fstem, name, istbl: f'*vim.{fstem}.{name}{"" if istbl else "()"}*',
- 'module_override': {},
- 'append_only': [],
- },
- 'treesitter': {
- 'mode': 'lua',
- 'filename': 'treesitter.txt',
- 'section_order': [
- 'treesitter.lua',
- 'language.lua',
- 'query.lua',
- 'highlighter.lua',
- 'languagetree.lua',
- 'dev.lua',
- ],
- 'files': [
- 'runtime/lua/vim/treesitter.lua',
- 'runtime/lua/vim/treesitter/',
- ],
- 'file_patterns': '*.lua',
- 'fn_name_prefix': '',
- 'section_name': {},
- 'section_fmt': lambda name: (
- 'Lua module: vim.treesitter'
- if name.lower() == 'treesitter'
- else f'Lua module: vim.treesitter.{name.lower()}'),
- 'helptag_fmt': lambda name: (
- '*lua-treesitter-core*'
- if name.lower() == 'treesitter'
- else f'*lua-treesitter-{name.lower()}*'),
- 'fn_helptag_fmt': lambda fstem, name, istbl: (
- f'*vim.{fstem}.{name}()*'
- if fstem == 'treesitter'
- else f'*{name}()*'
- if name[0].isupper()
- else f'*vim.treesitter.{fstem}.{name}()*'),
- 'module_override': {},
- 'append_only': [],
- }
-}
-
-param_exclude = (
- 'channel_id',
-)
-
-# Annotations are displayed as line items after API function descriptions.
-annotation_map = {
- 'FUNC_API_FAST': '|api-fast|',
- 'FUNC_API_TEXTLOCK': 'not allowed when |textlock| is active or in the |cmdwin|',
- 'FUNC_API_TEXTLOCK_ALLOW_CMDWIN': 'not allowed when |textlock| is active',
- 'FUNC_API_REMOTE_ONLY': '|RPC| only',
- 'FUNC_API_LUA_ONLY': 'Lua |vim.api| only',
-}
-
-
-def nvim_api_info() -> Tuple[int, bool]:
- """Returns NVIM_API_LEVEL, NVIM_API_PRERELEASE from CMakeLists.txt"""
- if not hasattr(nvim_api_info, 'LEVEL'):
- script_dir = os.path.dirname(os.path.abspath(__file__))
- cmake_file_path = os.path.join(script_dir, '..', 'CMakeLists.txt')
- with open(cmake_file_path, 'r') as cmake_file:
- cmake_content = cmake_file.read()
-
- api_level_match = re.search(r'set\(NVIM_API_LEVEL (\d+)\)', cmake_content)
- api_prerelease_match = re.search(
- r'set\(NVIM_API_PRERELEASE (\w+)\)', cmake_content
- )
-
- if not api_level_match or not api_prerelease_match:
- raise RuntimeError(
- 'Could not find NVIM_API_LEVEL or NVIM_API_PRERELEASE in CMakeLists.txt'
- )
-
- nvim_api_info.LEVEL = int(api_level_match.group(1))
- nvim_api_info.PRERELEASE = api_prerelease_match.group(1).lower() == 'true'
-
- return nvim_api_info.LEVEL, nvim_api_info.PRERELEASE
-
-
-# Raises an error with details about `o`, if `cond` is in object `o`,
-# or if `cond()` is callable and returns True.
-def debug_this(o, cond=True):
- name = ''
- if cond is False:
- return
- if not isinstance(o, str):
- try:
- name = o.nodeName
- o = o.toprettyxml(indent=' ', newl='\n')
- except Exception:
- pass
- if (cond is True
- or (callable(cond) and cond())
- or (not callable(cond) and cond in o)):
- raise RuntimeError('xxx: {}\n{}'.format(name, o))
-
-
-# Appends a message to a list which will be printed on exit.
-def msg(s):
- msgs.append(s)
-
-
-# Print all collected messages.
-def msg_report():
- for m in msgs:
- print(f' {m}')
-
-
-# Print collected messages, then throw an exception.
-def fail(s):
- msg_report()
- raise RuntimeError(s)
-
-
-def find_first(parent, name):
- """Finds the first matching node within parent."""
- sub = parent.getElementsByTagName(name)
- if not sub:
- return None
- return sub[0]
-
-
-def iter_children(parent, name):
- """Yields matching child nodes within parent."""
- for child in parent.childNodes:
- if child.nodeType == child.ELEMENT_NODE and child.nodeName == name:
- yield child
-
-
-def get_child(parent, name):
- """Gets the first matching child node."""
- for child in iter_children(parent, name):
- return child
- return None
-
-
-def self_or_child(n):
- """Gets the first child node, or self."""
- if len(n.childNodes) == 0:
- return n
- return n.childNodes[0]
-
-
-def align_tags(line):
- tag_regex = r"\s(\*.+?\*)(?:\s|$)"
- tags = re.findall(tag_regex, line)
-
- if len(tags) > 0:
- line = re.sub(tag_regex, "", line)
- tags = " " + " ".join(tags)
- line = line + (" " * (78 - len(line) - len(tags))) + tags
- return line
-
-
-def clean_lines(text):
- """Removes superfluous lines.
-
- The beginning and end of the string is trimmed. Empty lines are collapsed.
- """
- return re.sub(r'\A\n\s*\n*|\n\s*\n*\Z', '', re.sub(r'(\n\s*\n+)+', '\n\n', text))
-
-
-def is_blank(text):
- return '' == clean_lines(text)
-
-
-def get_text(n):
- """Recursively concatenates all text in a node tree."""
- text = ''
- if n.nodeType == n.TEXT_NODE:
- return n.data
- if n.nodeName == 'computeroutput':
- for node in n.childNodes:
- text += get_text(node)
- return '`{}`'.format(text)
- if n.nodeName == 'sp': # space, used in "programlisting" nodes
- return ' '
- for node in n.childNodes:
- if node.nodeType == node.TEXT_NODE:
- text += node.data
- elif node.nodeType == node.ELEMENT_NODE:
- text += get_text(node)
- return text
-
-
-# Gets the length of the last line in `text`, excluding newline ("\n") char.
-def len_lastline(text):
- lastnl = text.rfind('\n')
- if -1 == lastnl:
- return len(text)
- if '\n' == text[-1]:
- return lastnl - (1 + text.rfind('\n', 0, lastnl))
- return len(text) - (1 + lastnl)
-
-
-def len_lastline_withoutindent(text, indent):
- n = len_lastline(text)
- return (n - len(indent)) if n > len(indent) else 0
-
-
-# Returns True if node `n` contains only inline (not block-level) elements.
-def is_inline(n):
- # if len(n.childNodes) == 0:
- # return n.nodeType == n.TEXT_NODE or n.nodeName == 'computeroutput'
- for c in n.childNodes:
- if c.nodeType != c.TEXT_NODE and c.nodeName != 'computeroutput':
- return False
- if not is_inline(c):
- return False
- return True
-
-
-def doc_wrap(text, prefix='', width=70, func=False, indent=None):
- """Wraps text to `width`.
-
- First line is prefixed with `prefix`, subsequent lines are aligned.
- If `func` is True, only wrap at commas.
- """
- if not width:
- # return prefix + text
- return text
-
- # Whitespace used to indent all lines except the first line.
- indent = ' ' * len(prefix) if indent is None else indent
- indent_only = (prefix == '' and indent is not None)
-
- if func:
- lines = [prefix]
- for part in text.split(', '):
- if part[-1] not in ');':
- part += ', '
- if len(lines[-1]) + len(part) > width:
- lines.append(indent)
- lines[-1] += part
- return '\n'.join(x.rstrip() for x in lines).rstrip()
-
- # XXX: Dummy prefix to force TextWrapper() to wrap the first line.
- if indent_only:
- prefix = indent
-
- tw = textwrap.TextWrapper(break_long_words=False,
- break_on_hyphens=False,
- width=width,
- initial_indent=prefix,
- subsequent_indent=indent)
- result = '\n'.join(tw.wrap(text.strip()))
-
- # XXX: Remove the dummy prefix.
- if indent_only:
- result = result[len(indent):]
-
- return result
-
-
-def max_name(names):
- if len(names) == 0:
- return 0
- return max(len(name) for name in names)
-
-
-def update_params_map(parent, ret_map, width=text_width - indentation):
- """Updates `ret_map` with name:desc key-value pairs extracted
- from Doxygen XML node `parent`.
- """
- params = collections.OrderedDict()
- for node in parent.childNodes:
- if node.nodeType == node.TEXT_NODE:
- continue
- name_node = find_first(node, 'parametername')
- if name_node.getAttribute('direction') == 'out':
- continue
- name = get_text(name_node)
- if name in param_exclude:
- continue
- params[name.strip()] = node
- max_name_len = max_name(params.keys()) + 8
- # `ret_map` is a name:desc map.
- for name, node in params.items():
- desc = ''
- desc_node = get_child(node, 'parameterdescription')
- if desc_node:
- desc = fmt_node_as_vimhelp(
- desc_node, width=width, indent=(' ' * max_name_len))
- ret_map[name] = desc
- return ret_map
-
-
-def render_node(n, text, prefix='', indent='', width=text_width - indentation,
- fmt_vimhelp=False):
- """Renders a node as Vim help text, recursively traversing all descendants."""
-
- def ind(s):
- return s if fmt_vimhelp else ''
-
- text = ''
- # space_preceding = (len(text) > 0 and ' ' == text[-1][-1])
- # text += (int(not space_preceding) * ' ')
-
- if n.nodeName == 'preformatted':
- o = get_text(n)
- ensure_nl = '' if o[-1] == '\n' else '\n'
- if o[0:4] == 'lua\n':
- text += '>lua{}{}\n<'.format(ensure_nl, o[3:-1])
- elif o[0:4] == 'vim\n':
- text += '>vim{}{}\n<'.format(ensure_nl, o[3:-1])
- elif o[0:5] == 'help\n':
- text += o[4:-1]
- else:
- text += '>{}{}\n<'.format(ensure_nl, o)
- elif n.nodeName == 'programlisting': # codeblock (```)
- o = get_text(n)
- text += '>'
- if 'filename' in n.attributes:
- filename = n.attributes['filename'].value
- text += filename.lstrip('.')
-
- text += '\n{}\n<'.format(textwrap.indent(o, ' ' * 4))
- elif is_inline(n):
- text = doc_wrap(get_text(n), prefix=prefix, indent=indent, width=width)
- elif n.nodeName == 'verbatim':
- # TODO: currently we don't use this. The "[verbatim]" hint is there as
- # a reminder that we must decide how to format this if we do use it.
- text += ' [verbatim] {}'.format(get_text(n))
- elif n.nodeName == 'listitem':
- for c in n.childNodes:
- result = render_node(
- c,
- text,
- indent=indent + (' ' * len(prefix)),
- width=width
- )
- if is_blank(result):
- continue
- text += indent + prefix + result
- elif n.nodeName in ('para', 'heading'):
- did_prefix = False
- for c in n.childNodes:
- if (is_inline(c)
- and '' != get_text(c).strip()
- and text
- and ' ' != text[-1]):
- text += ' '
- text += render_node(c, text, prefix=(prefix if not did_prefix else ''), indent=indent, width=width)
- did_prefix = True
- elif n.nodeName == 'itemizedlist':
- for c in n.childNodes:
- text += '{}\n'.format(render_node(c, text, prefix='• ',
- indent=indent, width=width))
- elif n.nodeName == 'orderedlist':
- i = 1
- for c in n.childNodes:
- if is_blank(get_text(c)):
- text += '\n'
- continue
- text += '{}\n'.format(render_node(c, text, prefix='{}. '.format(i),
- indent=indent, width=width))
- i = i + 1
- elif n.nodeName == 'simplesect' and 'note' == n.getAttribute('kind'):
- text += ind(' ')
- for c in n.childNodes:
- if is_blank(render_node(c, text, prefix='• ', indent=' ', width=width)):
- continue
- text += render_node(c, text, prefix='• ', indent=' ', width=width)
- # text += '\n'
- elif n.nodeName == 'simplesect' and 'warning' == n.getAttribute('kind'):
- text += 'Warning:\n '
- for c in n.childNodes:
- text += render_node(c, text, indent=' ', width=width)
- text += '\n'
- elif n.nodeName == 'simplesect' and 'see' == n.getAttribute('kind'):
- text += ind(' ')
- # Example:
- # <simplesect kind="see">
- # <para>|autocommand|</para>
- # </simplesect>
- for c in n.childNodes:
- text += render_node(c, text, prefix='• ', indent=' ', width=width)
- elif n.nodeName == 'simplesect' and 'return' == n.getAttribute('kind'):
- text += ind(' ')
- for c in n.childNodes:
- text += render_node(c, text, indent=' ', width=width)
- elif n.nodeName == 'computeroutput':
- return get_text(n)
- else:
- raise RuntimeError('unhandled node type: {}\n{}'.format(
- n.nodeName, n.toprettyxml(indent=' ', newl='\n')))
-
- return text
-
-
-def para_as_map(parent, indent='', width=text_width - indentation, fmt_vimhelp=False):
- """Extracts a Doxygen XML <para> node to a map.
-
- Keys:
- 'text': Text from this <para> element
- 'note': List of @note strings
- 'params': <parameterlist> map
- 'return': List of @return strings
- 'seealso': List of @see strings
- 'xrefs': ?
- """
- chunks = {
- 'text': '',
- 'note': [],
- 'params': collections.OrderedDict(),
- 'return': [],
- 'seealso': [],
- 'prerelease': False,
- 'xrefs': []
- }
-
- # Ordered dict of ordered lists.
- groups = collections.OrderedDict([
- ('note', []),
- ('params', []),
- ('return', []),
- ('seealso', []),
- ('xrefs', []),
- ])
-
- # Gather nodes into groups. Mostly this is because we want "parameterlist"
- # nodes to appear together.
- text = ''
- kind = ''
- if is_inline(parent):
- # Flatten inline text from a tree of non-block nodes.
- text = doc_wrap(render_node(parent, "", fmt_vimhelp=fmt_vimhelp),
- indent=indent, width=width)
- else:
- prev = None # Previous node
- for child in parent.childNodes:
- if child.nodeName == 'parameterlist':
- groups['params'].append(child)
- elif child.nodeName == 'xrefsect':
- groups['xrefs'].append(child)
- elif child.nodeName == 'simplesect':
- kind = child.getAttribute('kind')
- if kind == 'note':
- groups['note'].append(child)
- elif kind == 'return':
- groups['return'].append(child)
- elif kind == 'see':
- groups['seealso'].append(child)
- elif kind == 'warning':
- text += render_node(child, text, indent=indent,
- width=width, fmt_vimhelp=fmt_vimhelp)
- elif kind == 'since':
- since_match = re.match(r'^(\d+)', get_text(child))
- since = int(since_match.group(1)) if since_match else 0
- NVIM_API_LEVEL, NVIM_API_PRERELEASE = nvim_api_info()
- if since > NVIM_API_LEVEL or (
- since == NVIM_API_LEVEL and NVIM_API_PRERELEASE
- ):
- chunks['prerelease'] = True
- else:
- raise RuntimeError('unhandled simplesect: {}\n{}'.format(
- child.nodeName, child.toprettyxml(indent=' ', newl='\n')))
- else:
- if (prev is not None
- and is_inline(self_or_child(prev))
- and is_inline(self_or_child(child))
- and '' != get_text(self_or_child(child)).strip()
- and text
- and ' ' != text[-1]):
- text += ' '
-
- text += render_node(child, text, indent=indent, width=width,
- fmt_vimhelp=fmt_vimhelp)
- prev = child
-
- chunks['text'] += text
-
- # Generate map from the gathered items.
- if len(groups['params']) > 0:
- for child in groups['params']:
- update_params_map(child, ret_map=chunks['params'], width=width)
- for child in groups['note']:
- chunks['note'].append(render_node(
- child, '', indent=indent, width=width, fmt_vimhelp=fmt_vimhelp).rstrip())
- for child in groups['return']:
- chunks['return'].append(render_node(
- child, '', indent=indent, width=width, fmt_vimhelp=fmt_vimhelp))
- for child in groups['seealso']:
- # Example:
- # <simplesect kind="see">
- # <para>|autocommand|</para>
- # </simplesect>
- chunks['seealso'].append(render_node(
- child, '', indent=indent, width=width, fmt_vimhelp=fmt_vimhelp))
-
- xrefs = set()
- for child in groups['xrefs']:
- # XXX: Add a space (or any char) to `title` here, otherwise xrefs
- # ("Deprecated" section) acts very weird...
- title = get_text(get_child(child, 'xreftitle')) + ' '
- xrefs.add(title)
- xrefdesc = get_text(get_child(child, 'xrefdescription'))
- chunks['xrefs'].append(doc_wrap(xrefdesc, prefix='{}: '.format(title),
- width=width) + '\n')
-
- return chunks, xrefs
-
-def is_program_listing(para):
- """
- Return True if `para` contains a "programlisting" (i.e. a Markdown code
- block ```).
-
- Sometimes a <para> element will have only a single "programlisting" child
- node, but othertimes it will have extra whitespace around the
- "programlisting" node.
-
- @param para XML <para> node
- @return True if <para> is a programlisting
- """
-
- # Remove any child text nodes that are only whitespace
- children = [
- n for n in para.childNodes
- if n.nodeType != n.TEXT_NODE or n.data.strip() != ''
- ]
-
- return len(children) == 1 and children[0].nodeName == 'programlisting'
-
-def fmt_node_as_vimhelp(parent: Element, width=text_width - indentation, indent='',
- fmt_vimhelp=False):
- """Renders (nested) Doxygen <para> nodes as Vim :help text.
-
- NB: Blank lines in a docstring manifest as <para> tags.
- """
- rendered_blocks = []
-
- def fmt_param_doc(m):
- """Renders a params map as Vim :help text."""
- max_name_len = max_name(m.keys()) + 4
- out = ''
- for name, desc in m.items():
- if name == 'self':
- continue
- name = ' • {}'.format('{{{}}}'.format(name).ljust(max_name_len))
- out += '{}{}\n'.format(name, desc)
- return out.rstrip()
-
- def has_nonexcluded_params(m):
- """Returns true if any of the given params has at least
- one non-excluded item."""
- if fmt_param_doc(m) != '':
- return True
-
- for child in parent.childNodes:
- para, _ = para_as_map(child, indent, width, fmt_vimhelp)
-
- # 'programlisting' blocks are Markdown code blocks. Do not include
- # these as a separate paragraph, but append to the last non-empty line
- # in the text
- if is_program_listing(child):
- while rendered_blocks and rendered_blocks[-1] == '':
- rendered_blocks.pop()
- rendered_blocks[-1] += ' ' + para['text']
- continue
-
- # Generate text from the gathered items.
- chunks = [para['text']]
- notes = [" This API is pre-release (unstable)."] if para['prerelease'] else []
- notes += para['note']
- if len(notes) > 0:
- chunks.append('\nNote: ~')
- for s in notes:
- chunks.append(s)
- if len(para['params']) > 0 and has_nonexcluded_params(para['params']):
- chunks.append('\nParameters: ~')
- chunks.append(fmt_param_doc(para['params']))
- if len(para['return']) > 0:
- chunks.append('\nReturn (multiple): ~' if len(para['return']) > 1 else '\nReturn: ~')
- for s in para['return']:
- chunks.append(s)
- if len(para['seealso']) > 0:
- chunks.append('\nSee also: ~')
- for s in para['seealso']:
- chunks.append(s)
- for s in para['xrefs']:
- chunks.append(s)
-
- rendered_blocks.append(clean_lines('\n'.join(chunks).strip()))
- rendered_blocks.append('')
-
- return clean_lines('\n'.join(rendered_blocks).strip())
-
-
-def extract_from_xml(filename, target, width, fmt_vimhelp):
- """Extracts Doxygen info as maps without formatting the text.
-
- Returns two maps:
- 1. Functions
- 2. Deprecated functions
-
- The `fmt_vimhelp` variable controls some special cases for use by
- fmt_doxygen_xml_as_vimhelp(). (TODO: ugly :)
- """
- fns = {} # Map of func_name:docstring.
- deprecated_fns = {} # Map of func_name:docstring.
-
- dom = minidom.parse(filename)
- compoundname = get_text(dom.getElementsByTagName('compoundname')[0])
- for member in dom.getElementsByTagName('memberdef'):
- if member.getAttribute('static') == 'yes' or \
- member.getAttribute('kind') != 'function' or \
- member.getAttribute('prot') == 'private' or \
- get_text(get_child(member, 'name')).startswith('_'):
- continue
-
- loc = find_first(member, 'location')
- if 'private' in loc.getAttribute('file'):
- continue
-
- return_type = get_text(get_child(member, 'type'))
- if return_type == '':
- continue
-
- if 'local_function' in return_type: # Special from lua2dox.lua.
- continue
-
- istbl = return_type.startswith('table') # Special from lua2dox.lua.
- if istbl and not CONFIG[target].get('include_tables', True):
- continue
-
- if return_type.startswith(('ArrayOf', 'DictionaryOf')):
- parts = return_type.strip('_').split('_')
- return_type = '{}({})'.format(parts[0], ', '.join(parts[1:]))
-
- name = get_text(get_child(member, 'name'))
-
- annotations = get_text(get_child(member, 'argsstring'))
- if annotations and ')' in annotations:
- annotations = annotations.rsplit(')', 1)[-1].strip()
- # XXX: (doxygen 1.8.11) 'argsstring' only includes attributes of
- # non-void functions. Special-case void functions here.
- if name == 'nvim_get_mode' and len(annotations) == 0:
- annotations += 'FUNC_API_FAST'
- annotations = filter(None, map(lambda x: annotation_map.get(x),
- annotations.split()))
-
- params = []
- type_length = 0
-
- for param in iter_children(member, 'param'):
- param_type = get_text(get_child(param, 'type')).strip()
- param_name = ''
- declname = get_child(param, 'declname')
- if declname:
- param_name = get_text(declname).strip()
- elif CONFIG[target]['mode'] == 'lua':
- # XXX: this is what lua2dox gives us...
- param_name = param_type
- param_type = ''
-
- if param_name in param_exclude:
- continue
-
- if fmt_vimhelp and param_type.endswith('*'):
- param_type = param_type.strip('* ')
- param_name = '*' + param_name
-
- type_length = max(type_length, len(param_type))
- params.append((param_type, param_name))
-
- # Handle Object Oriented style functions here.
- # We make sure they have "self" in the parameters,
- # and a parent function
- if return_type.startswith('function') \
- and len(return_type.split(' ')) >= 2 \
- and any(x[1] == 'self' for x in params):
- split_return = return_type.split(' ')
- name = f'{split_return[1]}:{name}'
- params = [x for x in params if x[1] != 'self']
-
- c_args = []
- for param_type, param_name in params:
- c_args.append((' ' if fmt_vimhelp else '') + (
- '%s %s' % (param_type.ljust(type_length), param_name)).strip())
-
- if not fmt_vimhelp:
- pass
- else:
- fstem = '?'
- if '.' in compoundname:
- fstem = compoundname.split('.')[0]
- fstem = CONFIG[target]['module_override'].get(fstem, fstem)
- vimtag = CONFIG[target]['fn_helptag_fmt'](fstem, name, istbl)
-
- if 'fn_name_fmt' in CONFIG[target]:
- name = CONFIG[target]['fn_name_fmt'](fstem, name)
-
- if istbl:
- aopen, aclose = '', ''
- else:
- aopen, aclose = '(', ')'
-
- prefix = name + aopen
- suffix = ', '.join('{%s}' % a[1] for a in params
- if a[0] not in ('void', 'Error', 'Arena',
- 'lua_State')) + aclose
-
- if not fmt_vimhelp:
- c_decl = '%s %s(%s);' % (return_type, name, ', '.join(c_args))
- signature = prefix + suffix
- else:
- c_decl = textwrap.indent('%s %s(\n%s\n);' % (return_type, name,
- ',\n'.join(c_args)),
- ' ')
-
- # Minimum 8 chars between signature and vimtag
- lhs = (width - 8) - len(vimtag)
-
- if len(prefix) + len(suffix) > lhs:
- signature = vimtag.rjust(width) + '\n'
- signature += doc_wrap(suffix, width=width, prefix=prefix,
- func=True)
- else:
- signature = prefix + suffix
- signature += vimtag.rjust(width - len(signature))
-
- # Tracks `xrefsect` titles. As of this writing, used only for separating
- # deprecated functions.
- xrefs_all = set()
- paras = []
- brief_desc = find_first(member, 'briefdescription')
- if brief_desc:
- for child in brief_desc.childNodes:
- para, xrefs = para_as_map(child)
- xrefs_all.update(xrefs)
-
- desc = find_first(member, 'detaileddescription')
- if desc:
- for child in desc.childNodes:
- para, xrefs = para_as_map(child)
- paras.append(para)
- xrefs_all.update(xrefs)
- log.debug(
- textwrap.indent(
- re.sub(r'\n\s*\n+', '\n',
- desc.toprettyxml(indent=' ', newl='\n')),
- ' ' * indentation))
-
- fn = {
- 'annotations': list(annotations),
- 'signature': signature,
- 'parameters': params,
- 'parameters_doc': collections.OrderedDict(),
- 'doc': [],
- 'return': [],
- 'seealso': [],
- }
- if fmt_vimhelp:
- fn['desc_node'] = desc
- fn['brief_desc_node'] = brief_desc
-
- for m in paras:
- if 'text' in m:
- if not m['text'] == '':
- fn['doc'].append(m['text'])
- if 'params' in m:
- # Merge OrderedDicts.
- fn['parameters_doc'].update(m['params'])
- if 'return' in m and len(m['return']) > 0:
- fn['return'] += m['return']
- if 'seealso' in m and len(m['seealso']) > 0:
- fn['seealso'] += m['seealso']
-
- if INCLUDE_C_DECL:
- fn['c_decl'] = c_decl
-
- if 'Deprecated' in str(xrefs_all):
- deprecated_fns[name] = fn
- elif name.startswith(CONFIG[target]['fn_name_prefix']):
- fns[name] = fn
-
- fns = collections.OrderedDict(sorted(
- fns.items(),
- key=lambda key_item_tuple: key_item_tuple[0].lower()))
- deprecated_fns = collections.OrderedDict(sorted(deprecated_fns.items()))
- return fns, deprecated_fns
-
-
-def fmt_doxygen_xml_as_vimhelp(filename, target):
- """Entrypoint for generating Vim :help from from Doxygen XML.
-
- Returns 2 items:
- 1. Vim help text for functions found in `filename`.
- 2. Vim help text for deprecated functions.
- """
- fns_txt = {} # Map of func_name:vim-help-text.
- deprecated_fns_txt = {} # Map of func_name:vim-help-text.
- fns, _ = extract_from_xml(filename, target, text_width, True)
-
- for name, fn in fns.items():
- # Generate Vim :help for parameters.
- if fn['desc_node']:
- doc = fmt_node_as_vimhelp(fn['desc_node'], fmt_vimhelp=True)
- if not doc and fn['brief_desc_node']:
- doc = fmt_node_as_vimhelp(fn['brief_desc_node'])
- if not doc and name.startswith("nvim__"):
- continue
- if not doc:
- doc = 'TODO: Documentation'
-
- annotations = '\n'.join(fn['annotations'])
- if annotations:
- annotations = ('\n\nAttributes: ~\n' +
- textwrap.indent(annotations, ' '))
- i = doc.rfind('Parameters: ~')
- if i == -1:
- doc += annotations
- else:
- doc = doc[:i] + annotations + '\n\n' + doc[i:]
-
- if INCLUDE_C_DECL:
- doc += '\n\nC Declaration: ~\n>\n'
- doc += fn['c_decl']
- doc += '\n<'
-
- func_doc = fn['signature'] + '\n'
- func_doc += textwrap.indent(clean_lines(doc), ' ' * indentation)
-
- # Verbatim handling.
- func_doc = re.sub(r'^\s+([<>])$', r'\1', func_doc, flags=re.M)
-
- split_lines = func_doc.split('\n')
- start = 0
- while True:
- try:
- start = split_lines.index('>', start)
- except ValueError:
- break
-
- try:
- end = split_lines.index('<', start)
- except ValueError:
- break
-
- split_lines[start + 1:end] = [
- (' ' + x).rstrip()
- for x in textwrap.dedent(
- "\n".join(
- split_lines[start+1:end]
- )
- ).split("\n")
- ]
-
- start = end
-
- func_doc = "\n".join(map(align_tags, split_lines))
-
- if (name.startswith(CONFIG[target]['fn_name_prefix'])
- and name != "nvim_error_event"):
- fns_txt[name] = func_doc
-
- return ('\n\n'.join(list(fns_txt.values())),
- '\n\n'.join(list(deprecated_fns_txt.values())))
-
-
-def delete_lines_below(filename, tokenstr):
- """Deletes all lines below the line containing `tokenstr`, the line itself,
- and one line above it.
- """
- lines = open(filename).readlines()
- i = 0
- found = False
- for i, line in enumerate(lines, 1):
- if tokenstr in line:
- found = True
- break
- if not found:
- raise RuntimeError(f'not found: "{tokenstr}"')
- i = max(0, i - 2)
- with open(filename, 'wt') as fp:
- fp.writelines(lines[0:i])
-
-
-def extract_defgroups(base: str, dom: Document):
- '''Generate module-level (section) docs (@defgroup).'''
- section_docs = {}
-
- for compound in dom.getElementsByTagName('compound'):
- if compound.getAttribute('kind') != 'group':
- continue
-
- # Doxygen "@defgroup" directive.
- groupname = get_text(find_first(compound, 'name'))
- groupxml = os.path.join(base, '%s.xml' %
- compound.getAttribute('refid'))
-
- group_parsed = minidom.parse(groupxml)
- doc_list = []
- brief_desc = find_first(group_parsed, 'briefdescription')
- if brief_desc:
- for child in brief_desc.childNodes:
- doc_list.append(fmt_node_as_vimhelp(child))
-
- desc = find_first(group_parsed, 'detaileddescription')
- if desc:
- doc = fmt_node_as_vimhelp(desc)
-
- if doc:
- doc_list.append(doc)
-
- # Can't use '.' in @defgroup, so convert to '--'
- # "vim.json" => "vim-dot-json"
- groupname = groupname.replace('-dot-', '.')
-
- section_docs[groupname] = "\n".join(doc_list)
-
- return section_docs
-
-
-def main(doxygen_config, args):
- """Generates:
-
- 1. Vim :help docs
- 2. *.mpack files for use by API clients
-
- Doxygen is called and configured through stdin.
- """
- for target in CONFIG:
- if args.target is not None and target != args.target:
- continue
- mpack_file = os.path.join(
- base_dir, 'runtime', 'doc',
- CONFIG[target]['filename'].replace('.txt', '.mpack'))
- if os.path.exists(mpack_file):
- os.remove(mpack_file)
-
- output_dir = out_dir.format(target=target)
- log.info("Generating documentation for %s in folder %s",
- target, output_dir)
- debug = args.log_level >= logging.DEBUG
- p = subprocess.Popen(
- ['doxygen', '-'],
- stdin=subprocess.PIPE,
- # silence warnings
- # runtime/lua/vim/lsp.lua:209: warning: argument 'foo' not found
- stderr=(subprocess.STDOUT if debug else subprocess.DEVNULL))
- p.communicate(
- doxygen_config.format(
- input=' '.join(
- [f'"{file}"' for file in CONFIG[target]['files']]),
- output=output_dir,
- filter=filter_cmd,
- file_patterns=CONFIG[target]['file_patterns'])
- .encode('utf8')
- )
- if p.returncode:
- sys.exit(p.returncode)
-
- fn_map_full = {} # Collects all functions as each module is processed.
- sections = {}
- sep = '=' * text_width
-
- base = os.path.join(output_dir, 'xml')
- dom = minidom.parse(os.path.join(base, 'index.xml'))
-
- section_docs = extract_defgroups(base, dom)
-
- # Generate docs for all functions in the current module.
- for compound in dom.getElementsByTagName('compound'):
- if compound.getAttribute('kind') != 'file':
- continue
-
- filename = get_text(find_first(compound, 'name'))
- if filename.endswith('.c') or filename.endswith('.lua'):
- xmlfile = os.path.join(base, '{}.xml'.format(compound.getAttribute('refid')))
- # Extract unformatted (*.mpack).
- fn_map, _ = extract_from_xml(xmlfile, target, 9999, False)
- # Extract formatted (:help).
- functions_text, deprecated_text = fmt_doxygen_xml_as_vimhelp(
- os.path.join(base, '{}.xml'.format(compound.getAttribute('refid'))), target)
-
- if not functions_text and not deprecated_text:
- continue
- else:
- filename = os.path.basename(filename)
- name = os.path.splitext(filename)[0].lower()
- sectname = name.upper() if name == 'ui' else name.title()
- sectname = CONFIG[target]['section_name'].get(filename, sectname)
- title = CONFIG[target]['section_fmt'](sectname)
- section_tag = CONFIG[target]['helptag_fmt'](sectname)
- # Module/Section id matched against @defgroup.
- # "*api-buffer*" => "api-buffer"
- section_id = section_tag.strip('*')
-
- doc = ''
- section_doc = section_docs.get(section_id)
- if section_doc:
- doc += '\n\n' + section_doc
-
- if functions_text:
- doc += '\n\n' + functions_text
-
- if INCLUDE_DEPRECATED and deprecated_text:
- doc += f'\n\n\nDeprecated {sectname} Functions: ~\n\n'
- doc += deprecated_text
-
- if doc:
- sections[filename] = (title, section_tag, doc)
- fn_map_full.update(fn_map)
-
- if len(sections) == 0:
- fail(f'no sections for target: {target} (look for errors near "Preprocessing" log lines above)')
- if len(sections) > len(CONFIG[target]['section_order']):
- raise RuntimeError(
- 'found new modules "{}"; update the "section_order" map'.format(
- set(sections).difference(CONFIG[target]['section_order'])))
- first_section_tag = sections[CONFIG[target]['section_order'][0]][1]
-
- docs = ''
-
- for filename in CONFIG[target]['section_order']:
- try:
- title, section_tag, section_doc = sections.pop(filename)
- except KeyError:
- msg(f'warning: empty docs, skipping (target={target}): {filename}')
- msg(f' existing docs: {sections.keys()}')
- continue
- if filename not in CONFIG[target]['append_only']:
- docs += sep
- docs += '\n{}{}'.format(title, section_tag.rjust(text_width - len(title)))
- docs += section_doc
- docs += '\n\n\n'
-
- docs = docs.rstrip() + '\n\n'
- docs += f' vim:tw=78:ts=8:sw={indentation}:sts={indentation}:et:ft=help:norl:\n'
-
- doc_file = os.path.join(base_dir, 'runtime', 'doc',
- CONFIG[target]['filename'])
-
- if os.path.exists(doc_file):
- delete_lines_below(doc_file, first_section_tag)
- with open(doc_file, 'ab') as fp:
- fp.write(docs.encode('utf8'))
-
- fn_map_full = collections.OrderedDict(sorted(fn_map_full.items()))
- with open(mpack_file, 'wb') as fp:
- fp.write(msgpack.packb(fn_map_full, use_bin_type=True))
-
- if not args.keep_tmpfiles:
- shutil.rmtree(output_dir)
-
- msg_report()
-
-
-def filter_source(filename, keep_tmpfiles):
- output_dir = out_dir.format(target='lua2dox')
- name, extension = os.path.splitext(filename)
- if extension == '.lua':
- args = [str(nvim), '-l', lua2dox, filename] + (['--outdir', output_dir] if keep_tmpfiles else [])
- p = subprocess.run(args, stdout=subprocess.PIPE)
- op = ('?' if 0 != p.returncode else p.stdout.decode('utf-8'))
- print(op)
- else:
- """Filters the source to fix macros that confuse Doxygen."""
- with open(filename, 'rt') as fp:
- print(re.sub(r'^(ArrayOf|DictionaryOf)(\(.*?\))',
- lambda m: m.group(1)+'_'.join(
- re.split(r'[^\w]+', m.group(2))),
- fp.read(), flags=re.M))
-
-
-def parse_args():
- targets = ', '.join(CONFIG.keys())
- ap = argparse.ArgumentParser(
- description="Generate helpdoc from source code")
- ap.add_argument(
- "--log-level", "-l", choices=LOG_LEVELS.keys(),
- default=logging.getLevelName(logging.ERROR), help="Set log verbosity"
- )
- ap.add_argument('source_filter', nargs='*',
- help="Filter source file(s)")
- ap.add_argument('-k', '--keep-tmpfiles', action='store_true',
- help="Keep temporary files (tmp-xx-doc/ directories, including tmp-lua2dox-doc/ for lua2dox.lua quasi-C output)")
- ap.add_argument('-t', '--target',
- help=f'One of ({targets}), defaults to "all"')
- return ap.parse_args()
-
-
-Doxyfile = textwrap.dedent('''
- OUTPUT_DIRECTORY = {output}
- INPUT = {input}
- INPUT_ENCODING = UTF-8
- FILE_PATTERNS = {file_patterns}
- RECURSIVE = YES
- INPUT_FILTER = "{filter}"
- EXCLUDE =
- EXCLUDE_SYMLINKS = NO
- EXCLUDE_PATTERNS = */private/* */health.lua */_*.lua
- EXCLUDE_SYMBOLS =
- EXTENSION_MAPPING = lua=C
- EXTRACT_PRIVATE = NO
-
- GENERATE_HTML = NO
- GENERATE_DOCSET = NO
- GENERATE_HTMLHELP = NO
- GENERATE_QHP = NO
- GENERATE_TREEVIEW = NO
- GENERATE_LATEX = NO
- GENERATE_RTF = NO
- GENERATE_MAN = NO
- GENERATE_DOCBOOK = NO
- GENERATE_AUTOGEN_DEF = NO
-
- GENERATE_XML = YES
- XML_OUTPUT = xml
- XML_PROGRAMLISTING = NO
-
- ENABLE_PREPROCESSING = YES
- MACRO_EXPANSION = YES
- EXPAND_ONLY_PREDEF = NO
- MARKDOWN_SUPPORT = YES
-''')
-
-if __name__ == "__main__":
- args = parse_args()
- print("Setting log level to %s" % args.log_level)
- args.log_level = LOG_LEVELS[args.log_level]
- log.setLevel(args.log_level)
- log.addHandler(logging.StreamHandler())
-
- # When invoked as a filter, args won't be passed, so use an env var.
- if args.keep_tmpfiles:
- os.environ['NVIM_KEEP_TMPFILES'] = '1'
- keep_tmpfiles = ('NVIM_KEEP_TMPFILES' in os.environ)
-
- if len(args.source_filter) > 0:
- filter_source(args.source_filter[0], keep_tmpfiles)
- else:
- main(Doxyfile, args)
-
-# vim: set ft=python ts=4 sw=4 tw=79 et :
diff --git a/scripts/genappimage.sh b/scripts/genappimage.sh
index b0bf186f85..e8aac42a9c 100755
--- a/scripts/genappimage.sh
+++ b/scripts/genappimage.sh
@@ -26,8 +26,8 @@ APP_DIR="$APP.AppDir"
########################################################################
# Build and install nvim into the AppImage
-make CMAKE_BUILD_TYPE="${NVIM_BUILD_TYPE}" CMAKE_EXTRA_FLAGS="-DCMAKE_INSTALL_PREFIX=${APP_DIR}/usr -DCMAKE_INSTALL_MANDIR=man"
-make install
+make CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}"
+cmake --install build --prefix="$APP_BUILD_DIR/${APP_DIR}/usr"
########################################################################
# Get helper functions and move to AppDir
@@ -52,7 +52,7 @@ fi
chmod +x "$APP_BUILD_DIR"/linuxdeploy-x86_64.AppImage
# metainfo is not packaged automatically by linuxdeploy
-mkdir "$APP_DIR/usr/share/metainfo/"
+mkdir -p "$APP_DIR/usr/share/metainfo/"
cp "$ROOT_DIR/runtime/nvim.appdata.xml" "$APP_DIR/usr/share/metainfo/"
cd "$APP_DIR" || exit
diff --git a/scripts/lintcommit.lua b/scripts/lintcommit.lua
index d2c8601c25..96f6304247 100644
--- a/scripts/lintcommit.lua
+++ b/scripts/lintcommit.lua
@@ -16,7 +16,7 @@ local _trace = false
-- Print message
local function p(s)
vim.cmd('set verbose=1')
- vim.api.nvim_echo({{s, ''}}, false, {})
+ vim.api.nvim_echo({ { s, '' } }, false, {})
vim.cmd('set verbose=0')
end
@@ -25,7 +25,7 @@ end
-- Prints `cmd` if `trace` is enabled.
local function run(cmd, or_die)
if _trace then
- p('run: '..vim.inspect(cmd))
+ p('run: ' .. vim.inspect(cmd))
end
local rv = vim.trim(vim.fn.system(cmd)) or ''
if vim.v.shell_error ~= 0 then
@@ -43,14 +43,14 @@ end
local function validate_commit(commit_message)
-- Return nil if the commit message starts with "fixup" as it signifies it's
-- a work in progress and shouldn't be linted yet.
- if vim.startswith(commit_message, "fixup") then
+ if vim.startswith(commit_message, 'fixup') then
return nil
end
- local commit_split = vim.split(commit_message, ":", {plain = true})
+ local commit_split = vim.split(commit_message, ':', { plain = true })
-- Return nil if the type is vim-patch since most of the normal rules don't
-- apply.
- if commit_split[1] == "vim-patch" then
+ if commit_split[1] == 'vim-patch' then
return nil
end
@@ -74,35 +74,41 @@ local function validate_commit(commit_message)
if after_idx > vim.tbl_count(commit_split) then
return [[Commit message does not include colons.]]
end
- local after_colon = commit_split[after_idx]
+ local after_colon = ''
+ while after_idx <= vim.tbl_count(commit_split) do
+ after_colon = after_colon .. commit_split[after_idx]
+ after_idx = after_idx + 1
+ end
-- Check if commit introduces a breaking change.
- if vim.endswith(before_colon, "!") then
+ if vim.endswith(before_colon, '!') then
before_colon = before_colon:sub(1, -2)
end
-- Check if type is correct
- local type = vim.split(before_colon, "(", {plain = true})[1]
- local allowed_types = {'build', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'test', 'vim-patch'}
+ local type = vim.split(before_colon, '(', { plain = true })[1]
+ local allowed_types =
+ { 'build', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'test', 'vim-patch' }
if not vim.tbl_contains(allowed_types, type) then
return string.format(
[[Invalid commit type "%s". Allowed types are:
%s.
If none of these seem appropriate then use "fix"]],
type,
- vim.inspect(allowed_types))
+ vim.inspect(allowed_types)
+ )
end
-- Check if scope is appropriate
- if before_colon:match("%(") then
- local scope = vim.trim(commit_message:match("%((.-)%)"))
+ if before_colon:match('%(') then
+ local scope = vim.trim(commit_message:match('%((.-)%)'))
if scope == '' then
return [[Scope can't be empty]]
end
- if vim.startswith(scope, "nvim_") then
- return [[Scope should be "api" instead of "nvim_..."]]
+ if vim.startswith(scope, 'nvim_') then
+ return [[Scope should be "api" instead of "nvim_..."]]
end
local alternative_scope = {
@@ -119,17 +125,17 @@ local function validate_commit(commit_message)
end
-- Check that description doesn't end with a period
- if vim.endswith(after_colon, ".") then
+ if vim.endswith(after_colon, '.') then
return [[Description ends with a period (".").]]
end
-- Check that description starts with a whitespace.
- if after_colon:sub(1,1) ~= " " then
+ if after_colon:sub(1, 1) ~= ' ' then
return [[There should be a whitespace after the colon.]]
end
-- Check that description doesn't start with multiple whitespaces.
- if after_colon:sub(1,2) == " " then
+ if after_colon:sub(1, 2) == ' ' then
return [[There should only be one whitespace after the colon.]]
end
@@ -139,7 +145,7 @@ local function validate_commit(commit_message)
end
-- Check that description isn't just whitespaces
- if vim.trim(after_colon) == "" then
+ if vim.trim(after_colon) == '' then
return [[Description shouldn't be empty.]]
end
@@ -150,25 +156,25 @@ end
function M.main(opt)
_trace = not opt or not not opt.trace
- local branch = run({'git', 'rev-parse', '--abbrev-ref', 'HEAD'}, true)
+ local branch = run({ 'git', 'rev-parse', '--abbrev-ref', 'HEAD' }, true)
-- TODO(justinmk): check $GITHUB_REF
- local ancestor = run({'git', 'merge-base', 'origin/master', branch})
+ local ancestor = run({ 'git', 'merge-base', 'origin/master', branch })
if not ancestor then
- ancestor = run({'git', 'merge-base', 'upstream/master', branch})
+ ancestor = run({ 'git', 'merge-base', 'upstream/master', branch })
end
- local commits_str = run({'git', 'rev-list', ancestor..'..'..branch}, true)
+ local commits_str = run({ 'git', 'rev-list', ancestor .. '..' .. branch }, true)
assert(commits_str)
local commits = {} --- @type string[]
- for substring in commits_str:gmatch("%S+") do
- table.insert(commits, substring)
+ for substring in commits_str:gmatch('%S+') do
+ table.insert(commits, substring)
end
local failed = 0
for _, commit_id in ipairs(commits) do
- local msg = run({'git', 'show', '-s', '--format=%s' , commit_id})
+ local msg = run({ 'git', 'show', '-s', '--format=%s', commit_id })
if vim.v.shell_error ~= 0 then
- p('Invalid commit-id: '..commit_id..'"')
+ p('Invalid commit-id: ' .. commit_id .. '"')
else
local invalid_msg = validate_commit(msg)
if invalid_msg then
@@ -179,20 +185,22 @@ function M.main(opt)
p('\n')
end
- p(string.format([[
+ p(string.format(
+ [[
Invalid commit message: "%s"
Commit: %s
%s
]],
msg,
commit_id,
- invalid_msg))
+ invalid_msg
+ ))
end
end
end
if failed > 0 then
- p([[
+ p([[
See also:
https://github.com/neovim/neovim/blob/master/CONTRIBUTING.md#commit-messages
@@ -239,11 +247,14 @@ function M._test()
['refactor(): empty scope'] = false,
['ci( ): whitespace as scope'] = false,
['ci: period at end of sentence.'] = false,
+ ['ci: period: at end of sentence.'] = false,
['ci: Capitalized first word'] = false,
['ci: UPPER_CASE First Word'] = true,
['unknown: using unknown type'] = false,
['feat: foo:bar'] = true,
+ ['feat: :foo:bar'] = true,
['feat(something): foo:bar'] = true,
+ ['feat(something): :foo:bar'] = true,
['feat(:grep): read from pipe'] = true,
['feat(:grep/:make): read from pipe'] = true,
['feat(:grep): foo:bar'] = true,
@@ -252,7 +263,7 @@ function M._test()
['feat(:grep/:make)'] = false,
['feat(:grep'] = false,
['feat(:grep/:make'] = false,
- ['ci: you\'re saying this commit message just goes on and on and on and on and on and on for way too long?'] = false,
+ ["ci: you're saying this commit message just goes on and on and on and on and on and on for way too long?"] = false,
}
local failed = 0
@@ -260,14 +271,15 @@ function M._test()
local is_valid = (nil == validate_commit(message))
if is_valid ~= expected then
failed = failed + 1
- p(string.format('[ FAIL ]: expected=%s, got=%s\n input: "%s"', expected, is_valid, message))
+ p(
+ string.format('[ FAIL ]: expected=%s, got=%s\n input: "%s"', expected, is_valid, message)
+ )
end
end
if failed > 0 then
os.exit(1)
end
-
end
--- @class LintcommitOptions
diff --git a/scripts/lintdoc.lua b/scripts/lintdoc.lua
new file mode 100755
index 0000000000..5e78b4cdcb
--- /dev/null
+++ b/scripts/lintdoc.lua
@@ -0,0 +1,20 @@
+#!/usr/bin/env -S nvim -l
+
+-- Validate vimdoc files on $VIMRUNTIME/doc, and test generating HTML docs.
+-- Checks for duplicate/missing tags, parse errors, and invalid links/urls/spellings.
+-- See also `make lintdoc`.
+--
+-- Usage:
+-- $ nvim -l scripts/lintdoc.lua
+-- $ make lintdoc
+
+print('Running lintdoc ...')
+
+-- gen_help_html requires :helptags to be generated on $VIMRUNTIME/doc
+-- :helptags checks for duplicate tags.
+vim.cmd [[ helptags ALL ]]
+
+require('scripts.gen_help_html').run_validate()
+require('scripts.gen_help_html').test_gen()
+
+print('lintdoc PASSED.')
diff --git a/scripts/lua2dox.lua b/scripts/lua2dox.lua
deleted file mode 100644
index 1c8bc5a3cb..0000000000
--- a/scripts/lua2dox.lua
+++ /dev/null
@@ -1,475 +0,0 @@
------------------------------------------------------------------------------
--- Copyright (C) 2012 by Simon Dales --
--- simon@purrsoft.co.uk --
--- --
--- This program is free software; you can redistribute it and/or modify --
--- it under the terms of the GNU General Public License as published by --
--- the Free Software Foundation; either version 2 of the License, or --
--- (at your option) any later version. --
--- --
--- This program is distributed in the hope that it will be useful, --
--- but WITHOUT ANY WARRANTY; without even the implied warranty of --
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --
--- GNU General Public License for more details. --
--- --
--- You should have received a copy of the GNU General Public License --
--- along with this program; if not, write to the --
--- Free Software Foundation, Inc., --
--- 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. --
------------------------------------------------------------------------------
-
---[[!
-Lua-to-Doxygen converter
-
-Partially from lua2dox
-http://search.cpan.org/~alec/Doxygen-Lua-0.02/lib/Doxygen/Lua.pm
-
-RUNNING
--------
-
-This script "lua2dox.lua" gets called by "gen_vimdoc.py".
-
-DEBUGGING/DEVELOPING
----------------------
-
-1. To debug, run gen_vimdoc.py with --keep-tmpfiles:
- python3 scripts/gen_vimdoc.py -t treesitter --keep-tmpfiles
-2. The filtered result will be written to ./tmp-lua2dox-doc/….lua.c
-
-Doxygen must be on your system. You can experiment like so:
-
-- Run "doxygen -g" to create a default Doxyfile.
-- Then alter it to let it recognise lua. Add the following line:
- FILE_PATTERNS = *.lua
-- Then run "doxygen".
-
-The core function reads the input file (filename or stdin) and outputs some pseudo C-ish language.
-It only has to be good enough for doxygen to see it as legal.
-
-One limitation is that each line is treated separately (except for long comments).
-The implication is that class and function declarations must be on the same line.
-
-There is hack that will insert the "missing" close paren.
-The effect is that you will get the function documented, but not with the parameter list you might expect.
-]]
-
-local TYPES = { 'integer', 'number', 'string', 'table', 'list', 'boolean', 'function' }
-
-local TAGGED_TYPES = { 'TSNode', 'LanguageTree' }
-
--- Document these as 'table'
-local ALIAS_TYPES = {
- 'Range', 'Range4', 'Range6', 'TSMetadata',
- 'vim.filetype.add.filetypes',
- 'vim.filetype.match.args'
-}
-
-local debug_outfile = nil --- @type string?
-local debug_output = {}
-
---- write to stdout
---- @param str? string
-local function write(str)
- if not str then
- return
- end
-
- io.write(str)
- if debug_outfile then
- table.insert(debug_output, str)
- end
-end
-
---- write to stdout
---- @param str? string
-local function writeln(str)
- write(str)
- write('\n')
-end
-
---- an input file buffer
---- @class StreamRead
---- @field currentLine string?
---- @field contentsLen integer
---- @field currentLineNo integer
---- @field filecontents string[]
-local StreamRead = {}
-
---- @return StreamRead
---- @param filename string
-function StreamRead.new(filename)
- assert(filename, ('invalid file: %s'):format(filename))
- -- get lines from file
- -- syphon lines to our table
- local filecontents = {} --- @type string[]
- for line in io.lines(filename) do
- filecontents[#filecontents+1] = line
- end
-
- return setmetatable({
- filecontents = filecontents,
- contentsLen = #filecontents,
- currentLineNo = 1,
- }, { __index = StreamRead })
-end
-
--- get a line
-function StreamRead:getLine()
- if self.currentLine then
- self.currentLine = nil
- return self.currentLine
- end
-
- -- get line
- if self.currentLineNo <= self.contentsLen then
- local line = self.filecontents[self.currentLineNo]
- self.currentLineNo = self.currentLineNo + 1
- return line
- end
-
- return ''
-end
-
--- save line fragment
---- @param line_fragment string
-function StreamRead:ungetLine(line_fragment)
- self.currentLine = line_fragment
-end
-
--- is it eof?
-function StreamRead:eof()
- return not self.currentLine and self.currentLineNo > self.contentsLen
-end
-
--- input filter
---- @class Lua2DoxFilter
-local Lua2DoxFilter = {}
-setmetatable(Lua2DoxFilter, { __index = Lua2DoxFilter })
-
---- trim comment off end of string
----
---- @param line string
---- @return string, string?
-local function removeCommentFromLine(line)
- local pos_comment = line:find('%-%-')
- if not pos_comment then
- return line
- end
- return line:sub(1, pos_comment - 1), line:sub(pos_comment)
-end
-
---- Processes "@…" directives in a docstring line.
----
---- @param line string
---- @param generics table<string,string>
---- @return string?
-local function process_magic(line, generics)
- line = line:gsub('^%s+@', '@')
- line = line:gsub('@package', '@private')
- line = line:gsub('@nodoc', '@private')
-
- if not vim.startswith(line, '@') then -- it's a magic comment
- return '/// ' .. line
- end
-
- local magic = line:sub(2)
- local magic_split = vim.split(magic, ' ', { plain = true })
- local directive = magic_split[1]
-
- if vim.list_contains({
- 'cast', 'diagnostic', 'overload', 'meta', 'type'
- }, directive) then
- -- Ignore LSP directives
- return '// gg:"' .. line .. '"'
- end
-
- if directive == 'defgroup' or directive == 'addtogroup' then
- -- Can't use '.' in defgroup, so convert to '--'
- return '/// @' .. magic:gsub('%.', '-dot-')
- end
-
- if directive == 'generic' then
- local generic_name, generic_type = line:match('@generic%s*(%w+)%s*:?%s*(.*)')
- if generic_type == '' then
- generic_type = 'any'
- end
- generics[generic_name] = generic_type
- return
- end
-
- local type_index = 2
-
- if directive == 'param' then
- for _, type in ipairs(TYPES) do
- magic = magic:gsub('^param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. ')%)', 'param %1 %2')
- magic =
- magic:gsub('^param%s+([a-zA-Z_?]+)%s+.*%((' .. type .. '|nil)%)', 'param %1 %2')
- end
- magic_split = vim.split(magic, ' ', { plain = true })
- type_index = 3
- elseif directive == 'return' then
- for _, type in ipairs(TYPES) do
- magic = magic:gsub('^return%s+.*%((' .. type .. ')%)', 'return %1')
- magic = magic:gsub('^return%s+.*%((' .. type .. '|nil)%)', 'return %1')
- end
- -- Remove first "#" comment char, if any. https://github.com/LuaLS/lua-language-server/wiki/Annotations#return
- magic = magic:gsub('# ', '', 1)
- -- handle the return of vim.spell.check
- magic = magic:gsub('({.*}%[%])', '`%1`')
- magic_split = vim.split(magic, ' ', { plain = true })
- end
-
- local ty = magic_split[type_index]
-
- if ty then
- -- fix optional parameters
- if magic_split[2]:find('%?$') then
- if not ty:find('nil') then
- ty = ty .. '|nil'
- end
- magic_split[2] = magic_split[2]:sub(1, -2)
- end
-
- -- replace generic types
- for k, v in pairs(generics) do
- ty = ty:gsub(k, v) --- @type string
- end
-
- for _, type in ipairs(TAGGED_TYPES) do
- ty = ty:gsub(type, '|%1|')
- end
-
- for _, type in ipairs(ALIAS_TYPES) do
- ty = ty:gsub('^'..type..'$', 'table') --- @type string
- end
-
- -- surround some types by ()
- for _, type in ipairs(TYPES) do
- ty = ty
- :gsub('^(' .. type .. '|nil):?$', '(%1)')
- :gsub('^(' .. type .. '):?$', '(%1)')
- end
-
- magic_split[type_index] = ty
-
- end
-
- magic = table.concat(magic_split, ' ')
-
- return '/// @' .. magic
-end
-
---- @param line string
---- @param in_stream StreamRead
---- @return string
-local function process_block_comment(line, in_stream)
- local comment_parts = {} --- @type string[]
- local done --- @type boolean?
-
- while not done and not in_stream:eof() do
- local thisComment --- @type string?
- local closeSquare = line:find(']]')
- if not closeSquare then -- need to look on another line
- thisComment = line .. '\n'
- line = in_stream:getLine()
- else
- thisComment = line:sub(1, closeSquare - 1)
- done = true
-
- -- unget the tail of the line
- -- in most cases it's empty. This may make us less efficient but
- -- easier to program
- in_stream:ungetLine(vim.trim(line:sub(closeSquare + 2)))
- end
- comment_parts[#comment_parts+1] = thisComment
- end
-
- local comment = table.concat(comment_parts)
-
- if comment:sub(1, 1) == '@' then -- it's a long magic comment
- return '/*' .. comment .. '*/ '
- end
-
- -- discard
- return '/* zz:' .. comment .. '*/ '
-end
-
---- @param line string
---- @return string
-local function process_function_header(line)
- local pos_fn = assert(line:find('function'))
- -- we've got a function
- local fn = removeCommentFromLine(vim.trim(line:sub(pos_fn + 8)))
-
- if fn:sub(1, 1) == '(' then
- -- it's an anonymous function
- return '// ZZ: '..line
- end
- -- fn has a name, so is interesting
-
- -- want to fix for iffy declarations
- if fn:find('[%({]') then
- -- we might have a missing close paren
- if not fn:find('%)') then
- fn = fn .. ' ___MissingCloseParenHere___)'
- end
- end
-
- -- Big hax
- if fn:find(':') then
- fn = fn:gsub(':', '.', 1)
-
- local paren_start = fn:find('(', 1, true)
- local paren_finish = fn:find(')', 1, true)
-
- -- Nothing in between the parens
- local comma --- @type string
- if paren_finish == paren_start + 1 then
- comma = ''
- else
- comma = ', '
- end
-
- fn = fn:sub(1, paren_start)
- .. 'self'
- .. comma
- .. fn:sub(paren_start + 1)
- end
-
- if line:match('local') then
- -- Special: tell gen_vimdoc.py this is a local function.
- return 'local_function ' .. fn .. '{}'
- end
-
- -- add vanilla function
- return 'function ' .. fn .. '{}'
-end
-
---- @param line string
---- @param in_stream StreamRead
---- @param generics table<string,string>>
---- @return string?
-local function process_line(line, in_stream, generics)
- local line_raw = line
- line = vim.trim(line)
-
- if vim.startswith(line, '---') then
- return process_magic(line:sub(4), generics)
- end
-
- if vim.startswith(line, '--'..'[[') then -- it's a long comment
- return process_block_comment(line:sub(5), in_stream)
- end
-
- -- Hax... I'm sorry
- -- M.fun = vim.memoize(function(...)
- -- ->
- -- function M.fun(...)
- line = line:gsub('^(.+) = .*_memoize%([^,]+, function%((.*)%)$', 'function %1(%2)')
-
- if line:find('^function') or line:find('^local%s+function') then
- return process_function_header(line)
- end
-
- if not line:match('^local') then
- local v = line_raw:match('^([A-Za-z][.a-zA-Z_]*)%s+%=')
- if v and v:match('%.') then
- -- Special: this lets gen_vimdoc.py handle tables.
- return 'table '..v..'() {}'
- end
- end
-
- if #line > 0 then -- we don't know what this line means, so just comment it out
- return '// zz: ' .. line
- end
-
- return ''
-end
-
--- Processes the file and writes filtered output to stdout.
----@param filename string
-function Lua2DoxFilter:filter(filename)
- local in_stream = StreamRead.new(filename)
-
- local generics = {} --- @type table<string,string>
-
- while not in_stream:eof() do
- local line = in_stream:getLine()
-
- local out_line = process_line(line, in_stream, generics)
-
- if not vim.startswith(vim.trim(line), '---') then
- generics = {}
- end
-
- if out_line then
- writeln(out_line)
- end
- end
-end
-
---- @class TApp
---- @field timestamp string|osdate
---- @field name string
---- @field version string
---- @field copyright string
---- this application
-local TApp = {
- timestamp = os.date('%c %Z', os.time()),
- name = 'Lua2DoX',
- version = '0.2 20130128',
- copyright = 'Copyright (c) Simon Dales 2012-13'
-}
-
-setmetatable(TApp, { __index = TApp })
-
-function TApp:getRunStamp()
- return self.name .. ' (' .. self.version .. ') ' .. self.timestamp
-end
-
-function TApp:getVersion()
- return self.name .. ' (' .. self.version .. ') '
-end
-
---main
-
-if arg[1] == '--help' then
- writeln(TApp:getVersion())
- writeln(TApp.copyright)
- writeln([[
- run as:
- nvim -l scripts/lua2dox.lua <param>
- --------------
- Param:
- <filename> : interprets filename
- --version : show version/copyright info
- --help : this help text]])
-elseif arg[1] == '--version' then
- writeln(TApp:getVersion())
- writeln(TApp.copyright)
-else -- It's a filter.
- local filename = arg[1]
-
- if arg[2] == '--outdir' then
- local outdir = arg[3]
- if type(outdir) ~= 'string' or (0 ~= vim.fn.filereadable(outdir) and 0 == vim.fn.isdirectory(outdir)) then
- error(('invalid --outdir: "%s"'):format(tostring(outdir)))
- end
- vim.fn.mkdir(outdir, 'p')
- debug_outfile = string.format('%s/%s.c', outdir, vim.fs.basename(filename))
- end
-
- Lua2DoxFilter:filter(filename)
-
- -- output the tail
- writeln('// #######################')
- writeln('// app run:' .. TApp:getRunStamp())
- writeln('// #######################')
- writeln()
-
- if debug_outfile then
- local f = assert(io.open(debug_outfile, 'w'))
- f:write(table.concat(debug_output))
- f:close()
- end
-end
diff --git a/scripts/luacats_grammar.lua b/scripts/luacats_grammar.lua
new file mode 100644
index 0000000000..ca26c70156
--- /dev/null
+++ b/scripts/luacats_grammar.lua
@@ -0,0 +1,184 @@
+--[[!
+LPEG grammar for LuaCATS
+]]
+
+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 ws = rep1(S(' \t'))
+local fill = opt(ws)
+
+local any = P(1) -- (consume one character)
+local letter = R('az', 'AZ') + S('_$')
+local num = R('09')
+local ident = letter * rep(letter + num + S '-.')
+local string_single = P "'" * rep(any - P "'") * P "'"
+local string_double = P('"') * rep(any - P('"')) * P('"')
+
+local literal = (string_single + string_double + (opt(P('-')) * num) + P('false') + P('true'))
+
+local lname = (ident + P('...')) * opt(P('?'))
+
+--- @param x string
+local function Pf(x)
+ return fill * P(x) * fill
+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,
+})
+
+local colon = Pf(':')
+local opt_exact = opt(Cg(Pf('(exact)'), 'access'))
+local access = P('private') + P('protected') + P('package')
+local caccess = Cg(access, 'access')
+local desc_delim = Sf '#:' + ws
+local desc = Cg(rep(any), 'desc')
+local opt_desc = opt(desc_delim * desc)
+local cname = Cg(ident, 'name')
+local opt_parent = opt(colon * Cg(ident, 'parent'))
+
+--- @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 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 * cname))) * opt_desc)
+ + annot('type', comma1(Ct(v.ctype)) * opt_desc)
+ + annot('cast', cname * ws * opt(Sf('+-')) * v.ctype)
+ + annot('generic', cname * opt(colon * v.ctype))
+ + annot('class', opt_exact * opt(paren(caccess)) * fill * cname * opt_parent)
+ + annot('field', opt(caccess * ws) * v.field_name * ws * v.ctype * opt_desc)
+ + annot('operator', cname * opt(paren(Cg(v.ltype, 'argtype'))) * colon * v.ctype)
+ + annot(access)
+ + annot('deprecated')
+ + annot('alias', cname * opt(ws * v.ctype))
+ + annot('enum', cname)
+ + 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'),
+
+ ctype = parenOpt(Cg(v.ltype, 'type')),
+ ltype = parenOpt(v.ty_union),
+
+ ty_union = v.ty_opt * rep(Pf('|') * v.ty_opt),
+ ty = v.ty_fun + ident + v.ty_table + literal + paren(v.ty),
+ ty_param = Pf('<') * comma1(v.ltype) * fill * P('>'),
+ ty_opt = v.ty * opt(v.ty_param) * opt(P('[]')) * opt(P('?')),
+ ty_index = (Pf('[') * (v.ltype + ident + rep1(num)) * fill * P(']')),
+ table_key = v.ty_index + lname,
+ table_elem = v.table_key * colon * v.ltype,
+ ty_table = Pf('{') * comma1(v.table_elem) * fill * P('}'),
+ fun_param = lname * opt(colon * v.ltype),
+ ty_fun = Pf('fun') * paren(comma(lname * opt(colon * v.ltype))) * opt(colon * comma1(v.ltype)),
+}
+
+return grammar --[[@as nvim.luacats.grammar]]
diff --git a/scripts/luacats_parser.lua b/scripts/luacats_parser.lua
new file mode 100644
index 0000000000..cd671fb9dc
--- /dev/null
+++ b/scripts/luacats_parser.lua
@@ -0,0 +1,537 @@
+local luacats_grammar = require('scripts.luacats_grammar')
+
+--- @class nvim.luacats.parser.param
+--- @field name string
+--- @field type string
+--- @field desc string
+
+--- @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
+--- @field name string
+--- @field type string
+--- @field desc string
+--- @field access? 'private'|'package'|'protected'
+
+--- @class nvim.luacats.parser.class
+--- @field kind 'class'
+--- @field parent? string
+--- @field name string
+--- @field desc string
+--- @field nodoc? true
+--- @field inlinedoc? true
+--- @field access? 'private'|'package'|'protected'
+--- @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(' }
+ for _, p in ipairs(fun.params or {}) do
+ parts[#parts + 1] = string.format('%s: %s', p.name, p.type)
+ end
+ 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,
+ }
+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
+ table.insert(classes[class].fields, fun2field(cur_obj))
+ 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(assert(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/text_utils.lua b/scripts/text_utils.lua
new file mode 100644
index 0000000000..937408c546
--- /dev/null
+++ b/scripts/text_utils.lua
@@ -0,0 +1,361 @@
+local fmt = string.format
+
+--- @class nvim.text_utils.MDNode
+--- @field [integer] nvim.text_utils.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
+
+--- @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.text_utils.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.text_utils.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.text_utils.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.text_utils.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
+
+--- @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.text_utils.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
+ 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, ' ')
+ local pad = string.rep(' ', text_width - #line - #tags_str)
+ 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?\n', ' >%1\n')
+ s = s:gsub('\n+%s*>\n?\n', ' >\n')
+
+ return s
+end
+
+return M
diff --git a/scripts/vim-patch.sh b/scripts/vim-patch.sh
index 47c6d293bc..45dd7f5fee 100755
--- a/scripts/vim-patch.sh
+++ b/scripts/vim-patch.sh
@@ -24,7 +24,7 @@ CREATED_FILES=()
usage() {
echo "Port Vim patches to Neovim"
- echo "https://github.com/neovim/neovim/wiki/Merging-patches-from-upstream-vim"
+ echo "https://neovim.io/doc/user/dev_vimpatch.html"
echo
echo "Usage: ${BASENAME} [-h | -l | -p vim-revision | -r pr-number]"
echo
@@ -174,7 +174,7 @@ assign_commit_details() {
vim_commit_url="https://github.com/vim/vim/commit/${vim_commit}"
vim_message="$(git -C "${VIM_SOURCE_DIR}" log -1 --pretty='format:%B' "${vim_commit}" \
- | sed -Ee 's/(#[0-9]{1,})/vim\/vim\1/g')"
+ | sed -Ee 's/([^A-Za-z0-9])(#[0-9]{1,})/\1vim\/vim\2/g')"
local vim_coauthor0
vim_coauthor0="$(git -C "${VIM_SOURCE_DIR}" log -1 --pretty='format:Co-authored-by: %an <%ae>' "${vim_commit}")"
# Extract co-authors from the commit message.
@@ -427,7 +427,7 @@ stage_patch() {
or "%s -s --draft" to create a draft pull request.
See the wiki for more information:
- * https://github.com/neovim/neovim/wiki/Merging-patches-from-upstream-vim
+ * https://neovim.io/doc/user/dev_vimpatch.html
' "${vim_version}" "${BASENAME}" "${BASENAME}" "${BASENAME}"
return $ret
}
diff --git a/scripts/vimpatch.lua b/scripts/vimpatch.lua
index 836f672f6e..cbec50fc17 100755
--- a/scripts/vimpatch.lua
+++ b/scripts/vimpatch.lua
@@ -10,13 +10,13 @@ local function systemlist(...)
local err = nvim.nvim_get_vvar('shell_error')
local args_str = nvim.nvim_call_function('string', ...)
if 0 ~= err then
- error('command failed: '..args_str)
+ error('command failed: ' .. args_str)
end
return rv
end
local function vimpatch_sh_list_numbers()
- return systemlist( { { 'bash', '-c', 'scripts/vim-patch.sh -M', } } )
+ return systemlist({ { 'bash', '-c', 'scripts/vim-patch.sh -M' } })
end
-- Generates the lines to be inserted into the src/version.c
@@ -55,9 +55,9 @@ local function patch_version_c()
nvim.nvim_command('silent normal! j0d/};\rk')
-- Insert the lines.
nvim.nvim_call_function('append', {
- nvim.nvim_eval('line(".")'),
- lines,
- })
+ nvim.nvim_eval('line(".")'),
+ lines,
+ })
nvim.nvim_command('silent write')
end
diff --git a/scripts/windows.ti b/scripts/windows.ti
index c3a367e6d4..34028b8e00 100644
--- a/scripts/windows.ti
+++ b/scripts/windows.ti
@@ -34,7 +34,7 @@ conemu|ANIS X3.64 and Xterm 256 colors for ConEmu with libuv,
smcup=\E[?1049h, rmir@, rmkx@, rmm@, rs1@, rs2@,
setab=\E[48;5;%p1%dm, setaf=\E[38;5;%p1%dm,
sgr=\E[0%?%p1%p3%|%t;7%;%?%p2%t;4%;%?%p6%t;1%;m,
- sgr0=\E[0m, smam@, smglr@, smir@, smkx@, smm@, tbc@, u6@, u7@, u8@, u9@,
+ sgr0=\E[0m, smam@, smglp@, smgrp@, smglr@, smir@, smkx@, smm@, tbc@, u6@, u7@, u8@, u9@,
Cr@, Cs@, Ms@, XM@, kDC3@, kDC4@, kDC5@, kDC6@, kDC7@,
kDN@, kDN3@, kDN4@, kDN5@, kDN6@, kDN7@,
kEND3@, kEND4@, kEND5@, kEND6@, kEND7@,
@@ -57,7 +57,7 @@ vtpcon|ANIS emulation for console virtual terminal sequence with libuv,
mc0@, mc4@, mc5@, meml@, memu@, oc@, rmam@, rmcup=\E[?1049l,
smcup=\E[?1049h, rmir@, rmkx@, rmm@, rs1@, rs2@,
sgr=\E[0%?%p1%p3%|%t;7%;%?%p2%t;4%;%?%p6%t;1%;m,
- sgr0=\E[0m, smam@, smglr@, smir@, smkx@, smm@, tbc@, u6@, u7@, u8@, u9@,
+ sgr0=\E[0m, smam@, smglp@, smgrp@, smglr@, smir@, smkx@, smm@, tbc@, u6@, u7@, u8@, u9@,
Cr@, Cs@, Ms@, XM@, kDC3@, kDC4@, kDC5@, kDC6@, kDC7@,
kDN@, kDN3@, kDN4@, kDN5@, kDN6@, kDN7@,
kEND3@, kEND4@, kEND5@, kEND6@, kEND7@,
@@ -68,4 +68,4 @@ vtpcon|ANIS emulation for console virtual terminal sequence with libuv,
kPRV3@, kPRV4@, kPRV5@, kPRV6@, kPRV7@,
kRIT3@, kRIT4@, kRIT5@, kRIT6@, kRIT7@,
kUP3@, kUP4@, kUP5@, kUP6@, kUP7@, rmxx@, smxx@, xm@,
- use=libuv+basekey, use=libuv+exkey, use=xterm+256color, use=xterm-new,
+ use=libuv+basekey, use=libuv+exkey, use=xterm+256color, use=xterm+sl, use=xterm-new,