mirror of
https://github.com/neovim/neovim.git
synced 2025-12-16 03:15:39 +00:00
feat(docs): replace lua2dox.lua
Problem: The documentation flow (`gen_vimdoc.py`) has several issues: - it's not very versatile - depends on doxygen - doesn't work well with Lua code as it requires an awkward filter script to convert it into pseudo-C. - The intermediate XML files and filters makes it too much like a rube goldberg machine. Solution: Re-implement the flow using Lua, LPEG and treesitter. - `gen_vimdoc.py` is now replaced with `gen_vimdoc.lua` and replicates a portion of the logic. - `lua2dox.lua` is gone! - No more XML files. - Doxygen is now longer used and instead we now use: - LPEG for comment parsing (see `scripts/luacats_grammar.lua` and `scripts/cdoc_grammar.lua`). - LPEG for C parsing (see `scripts/cdoc_parser.lua`) - Lua patterns for Lua parsing (see `scripts/luacats_parser.lua`). - Treesitter for Markdown parsing (see `scripts/text_utils.lua`). - The generated `runtime/doc/*.mpack` files have been removed. - `scripts/gen_eval_files.lua` now instead uses `scripts/cdoc_parser.lua` directly. - Text wrapping is implemented in `scripts/text_utils.lua` and appears to produce more consistent results (the main contributer to the diff of this change).
This commit is contained in:
committed by
Lewis Russell
parent
7ad2e3c645
commit
9beb40a4db
87
scripts/cdoc_grammar.lua
Normal file
87
scripts/cdoc_grammar.lua
Normal file
@@ -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]]
|
||||
223
scripts/cdoc_parser.lua
Normal file
223
scripts/cdoc_parser.lua
Normal file
@@ -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
|
||||
@@ -3,7 +3,6 @@
|
||||
-- Generator for various vimdoc and Lua type files
|
||||
|
||||
local DEP_API_METADATA = 'build/api_metadata.mpack'
|
||||
local DEP_API_DOC = 'runtime/doc/api.mpack'
|
||||
|
||||
--- @class vim.api.metadata
|
||||
--- @field name string
|
||||
@@ -210,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>]]
|
||||
local cdoc_parser = require('scripts.cdoc_parser')
|
||||
|
||||
for _, fun in ipairs(metadata) do
|
||||
if fun.lua then
|
||||
local fdoc = doc_metadata[fun.name]
|
||||
local f = 'src/nvim/api'
|
||||
|
||||
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 r = {
|
||||
signature = 'NA',
|
||||
name = fun.name,
|
||||
params = params,
|
||||
returns = api_type(fun.return_type),
|
||||
deprecated = fun.deprecated_since ~= nil,
|
||||
}
|
||||
|
||||
if fdoc then
|
||||
if #fdoc.doc > 0 then
|
||||
r.desc = table.concat(fdoc.doc, '\n')
|
||||
end
|
||||
r.return_desc = (fdoc['return'] or {})[1]
|
||||
end
|
||||
|
||||
ret[fun.name] = r
|
||||
local function include(fun)
|
||||
if not vim.startswith(fun.name, 'nvim_') then
|
||||
return false
|
||||
end
|
||||
if vim.tbl_contains(fun.attrs or {}, 'lua_only') then
|
||||
return true
|
||||
end
|
||||
if vim.tbl_contains(fun.attrs or {}, 'remote_only') then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- @type table<string,nvim.cdoc.parser.fun>
|
||||
local functions = {}
|
||||
for path, ty in vim.fs.dir(f) do
|
||||
if ty == 'file' then
|
||||
local filename = vim.fs.joinpath(f, path)
|
||||
local _, funs = cdoc_parser.parse(filename)
|
||||
for _, fn in ipairs(funs) do
|
||||
if include(fn) then
|
||||
functions[fn.name] = fn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, fun in pairs(functions) do
|
||||
local deprecated = fun.deprecated_since ~= nil
|
||||
|
||||
local 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
|
||||
@@ -275,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
|
||||
@@ -291,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[]
|
||||
@@ -303,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
|
||||
@@ -317,6 +338,7 @@ local function render_api_meta(_f, fun, write)
|
||||
end
|
||||
if fun.returns ~= '' then
|
||||
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
|
||||
@@ -328,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>
|
||||
|
||||
787
scripts/gen_vimdoc.lua
Executable file
787
scripts/gen_vimdoc.lua
Executable file
@@ -0,0 +1,787 @@
|
||||
#!/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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
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
|
||||
return 'Lua module: vim.' .. name
|
||||
end,
|
||||
helptag_fmt = function(name)
|
||||
if name == '_editor' then
|
||||
return '*lua-vim*'
|
||||
elseif name == '_options' then
|
||||
return '*lua-vimscript*'
|
||||
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',
|
||||
'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 ty string
|
||||
--- @param generics? table<string,string>
|
||||
local function render_type(ty, generics)
|
||||
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*', '|')
|
||||
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 xs (nvim.luacats.parser.param|nvim.luacats.parser.field)[]
|
||||
--- @param generics? table<string,string>
|
||||
--- @param exclude_types? true
|
||||
local function render_fields_or_params(xs, generics, 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 nm, ty = p.name, p.type
|
||||
local desc = p.desc
|
||||
local pnm = fmt(' • %-' .. indent .. 's', '{' .. nm .. '}')
|
||||
if ty then
|
||||
local pty = render_type(ty, generics)
|
||||
if desc then
|
||||
desc = fmt('%s %s', pty, desc)
|
||||
table.insert(ret, pnm)
|
||||
table.insert(ret, md_to_vimdoc(desc, 1, 9 + indent, TEXT_WIDTH, true))
|
||||
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 lua2vimdoc.class
|
||||
-- local function render_class(class)
|
||||
-- writeln(fmt('*%s*', class.name))
|
||||
-- writeln()
|
||||
-- if #class.fields > 0 then
|
||||
-- writeln(' Fields: ~')
|
||||
-- render_fields_or_params(class.fields)
|
||||
-- end
|
||||
-- writeln()
|
||||
-- end
|
||||
|
||||
-- --- @param cls table<string,lua2vimdoc.class>
|
||||
-- local function render_classes(cls)
|
||||
-- --- @diagnostic disable-next-line:no-unknown
|
||||
-- for _, class in vim.spairs(cls) do
|
||||
-- render_class(class)
|
||||
-- end
|
||||
-- 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('{%s}', p.name:gsub('%?$', ''))
|
||||
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 exclude_types boolean
|
||||
local function render_returns(returns, generics, 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
|
||||
local rnm, ty, desc = p.name, p.type, p.desc
|
||||
local blk = ''
|
||||
if ty then
|
||||
blk = render_type(ty, generics)
|
||||
end
|
||||
if rnm then
|
||||
blk = blk .. ' ' .. rnm
|
||||
end
|
||||
if desc then
|
||||
blk = blk .. ' ' .. desc
|
||||
end
|
||||
table.insert(ret, md_to_vimdoc(blk, 8, 8, TEXT_WIDTH, true))
|
||||
end
|
||||
|
||||
return table.concat(ret)
|
||||
end
|
||||
|
||||
--- @param fun nvim.luacats.parser.fun
|
||||
--- @param cfg nvim.gen_vimdoc.Config
|
||||
local function render_fun(fun, 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, 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, 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 cfg nvim.gen_vimdoc.Config
|
||||
local function render_funs(funs, 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, 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)
|
||||
|
||||
for _, f in pairs(cfg.files) do
|
||||
local ext = assert(f:match('%.([^.]+)$')) --[[@as 'h'|'c'|'lua']]
|
||||
local parser = assert(parsers[ext])
|
||||
local _, funs, briefs = parser(f)
|
||||
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, cfg)
|
||||
-- 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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,544 +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 luacats_parser = require('src/nvim/generators/luacats_grammar')
|
||||
|
||||
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 = {
|
||||
generics = {}, --- @type table<string,string>
|
||||
block_ignore = false, --- @type boolean
|
||||
}
|
||||
setmetatable(Lua2DoxFilter, { __index = Lua2DoxFilter })
|
||||
|
||||
function Lua2DoxFilter:reset()
|
||||
self.generics = {}
|
||||
self.block_ignore = false
|
||||
end
|
||||
|
||||
--- 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
|
||||
|
||||
--- @param parsed luacats.Return
|
||||
--- @return string
|
||||
local function get_return_type(parsed)
|
||||
local elems = {} --- @type string[]
|
||||
for _, v in ipairs(parsed) do
|
||||
local e = v.type --- @type string
|
||||
if v.name then
|
||||
e = e .. ' ' .. v.name --- @type string
|
||||
end
|
||||
elems[#elems + 1] = e
|
||||
end
|
||||
return '(' .. table.concat(elems, ', ') .. ')'
|
||||
end
|
||||
|
||||
--- @param name string
|
||||
--- @return string
|
||||
local function process_name(name, optional)
|
||||
if optional then
|
||||
name = name:sub(1, -2) --- @type string
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
--- @param ty string
|
||||
--- @param generics table<string,string>
|
||||
--- @return string
|
||||
local function process_type(ty, generics, optional)
|
||||
-- replace generic types
|
||||
for k, v in pairs(generics) do
|
||||
ty = ty:gsub(k, v) --- @type string
|
||||
end
|
||||
|
||||
-- strip parens
|
||||
ty = ty:gsub('^%((.*)%)$', '%1')
|
||||
|
||||
if optional and not ty:find('nil') then
|
||||
ty = ty .. '?'
|
||||
end
|
||||
|
||||
-- remove whitespace in unions
|
||||
ty = ty:gsub('%s*|%s*', '|')
|
||||
|
||||
-- replace '|nil' with '?'
|
||||
ty = ty:gsub('|nil', '?')
|
||||
ty = ty:gsub('nil|(.*)', '%1?')
|
||||
|
||||
return '(`' .. ty .. '`)'
|
||||
end
|
||||
|
||||
--- @param parsed luacats.Param
|
||||
--- @param generics table<string,string>
|
||||
--- @return string
|
||||
local function process_param(parsed, generics)
|
||||
local name, ty = parsed.name, parsed.type
|
||||
local optional = vim.endswith(name, '?')
|
||||
|
||||
return table.concat({
|
||||
'/// @param',
|
||||
process_name(name, optional),
|
||||
process_type(ty, generics, optional),
|
||||
parsed.desc,
|
||||
}, ' ')
|
||||
end
|
||||
|
||||
--- @param parsed luacats.Return
|
||||
--- @param generics table<string,string>
|
||||
--- @return string
|
||||
local function process_return(parsed, generics)
|
||||
local ty, name --- @type string, string
|
||||
if #parsed == 1 then
|
||||
ty, name = parsed[1].type, parsed[1].name or ''
|
||||
else
|
||||
ty, name = get_return_type(parsed), ''
|
||||
end
|
||||
|
||||
local optional = vim.endswith(name, '?')
|
||||
|
||||
return table.concat({
|
||||
'/// @return',
|
||||
process_type(ty, generics, optional),
|
||||
process_name(name, optional),
|
||||
parsed.desc,
|
||||
}, ' ')
|
||||
end
|
||||
|
||||
--- Processes "@…" directives in a docstring line.
|
||||
---
|
||||
--- @param line string
|
||||
--- @return string?
|
||||
function Lua2DoxFilter:process_magic(line)
|
||||
line = line:gsub('^%s+@', '@')
|
||||
line = line:gsub('@package', '@private')
|
||||
line = line:gsub('@nodoc', '@private')
|
||||
|
||||
if self.block_ignore then
|
||||
return '// gg:" ' .. line .. '"'
|
||||
end
|
||||
|
||||
if not vim.startswith(line, '@') then -- it's a magic comment
|
||||
return '/// ' .. line
|
||||
end
|
||||
|
||||
local magic_split = vim.split(line, ' ', { plain = true })
|
||||
local directive = magic_split[1]
|
||||
|
||||
if
|
||||
vim.list_contains({
|
||||
'@cast',
|
||||
'@diagnostic',
|
||||
'@overload',
|
||||
'@meta',
|
||||
'@type',
|
||||
}, directive)
|
||||
then
|
||||
-- Ignore LSP directives
|
||||
return '// gg:"' .. line .. '"'
|
||||
elseif directive == '@defgroup' or directive == '@addtogroup' then
|
||||
-- Can't use '.' in defgroup, so convert to '--'
|
||||
return '/// ' .. line:gsub('%.', '-dot-')
|
||||
end
|
||||
|
||||
if directive == '@alias' then
|
||||
-- this contiguous block should be all ignored.
|
||||
self.block_ignore = true
|
||||
return '// gg:"' .. line .. '"'
|
||||
end
|
||||
|
||||
-- preprocess line before parsing
|
||||
if directive == '@param' or directive == '@return' then
|
||||
for _, type in ipairs(TYPES) 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
|
||||
end
|
||||
|
||||
local parsed = luacats_parser:match(line)
|
||||
|
||||
if not parsed then
|
||||
return '/// ' .. line
|
||||
end
|
||||
|
||||
local kind = parsed.kind
|
||||
|
||||
if kind == 'generic' then
|
||||
self.generics[parsed.name] = parsed.type or 'any'
|
||||
return
|
||||
elseif kind == 'param' then
|
||||
return process_param(parsed --[[@as luacats.Param]], self.generics)
|
||||
elseif kind == 'return' then
|
||||
return process_return(parsed --[[@as luacats.Return]], self.generics)
|
||||
end
|
||||
|
||||
error(string.format('unhandled parsed line %q: %s', line, parsed))
|
||||
end
|
||||
|
||||
--- @param line string
|
||||
--- @param in_stream StreamRead
|
||||
--- @return string
|
||||
function Lua2DoxFilter: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
|
||||
function Lua2DoxFilter: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
|
||||
--- @return string?
|
||||
function Lua2DoxFilter:process_line(line, in_stream)
|
||||
local line_raw = line
|
||||
line = vim.trim(line)
|
||||
|
||||
if vim.startswith(line, '---') then
|
||||
return Lua2DoxFilter:process_magic(line:sub(4))
|
||||
end
|
||||
|
||||
if vim.startswith(line, '--' .. '[[') then -- it's a long comment
|
||||
return Lua2DoxFilter: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 Lua2DoxFilter: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 last_was_magic = false
|
||||
|
||||
while not in_stream:eof() do
|
||||
local line = in_stream:getLine()
|
||||
|
||||
local out_line = self:process_line(line, in_stream)
|
||||
|
||||
if not vim.startswith(vim.trim(line), '---') then
|
||||
self:reset()
|
||||
end
|
||||
|
||||
if out_line then
|
||||
-- Ensure all magic blocks associate with some object to prevent doxygen
|
||||
-- from getting confused.
|
||||
if vim.startswith(out_line, '///') then
|
||||
last_was_magic = true
|
||||
else
|
||||
if last_was_magic and out_line:match('^// zz: [^-]+') then
|
||||
writeln('local_function _ignore() {}')
|
||||
end
|
||||
last_was_magic = false
|
||||
end
|
||||
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
|
||||
218
scripts/luacats_grammar.lua
Normal file
218
scripts/luacats_grammar.lua
Normal file
@@ -0,0 +1,218 @@
|
||||
--[[!
|
||||
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 nl = P('\r\n') + P('\n')
|
||||
local ws = rep1(S(' \t') + nl)
|
||||
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 comma(x)
|
||||
return x * rep(Pf ',' * x)
|
||||
end
|
||||
|
||||
--- @param x vim.lpeg.Pattern
|
||||
local function parenOpt(x)
|
||||
return (Pf('(') * x * fill * P(')')) + x
|
||||
end
|
||||
|
||||
--- @type table<string,vim.lpeg.Pattern>
|
||||
local v = setmetatable({}, {
|
||||
__index = function(_, k)
|
||||
return lpeg.V(k)
|
||||
end,
|
||||
})
|
||||
|
||||
local desc_delim = Sf '#:' + ws
|
||||
|
||||
--- @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
|
||||
|
||||
--- @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 grammar = P {
|
||||
rep1(P('@') * (v.ats + v.ext_ats)),
|
||||
|
||||
ats = v.at_param
|
||||
+ v.at_return
|
||||
+ v.at_type
|
||||
+ v.at_cast
|
||||
+ v.at_generic
|
||||
+ v.at_class
|
||||
+ v.at_field
|
||||
+ v.at_access
|
||||
+ v.at_deprecated
|
||||
+ v.at_alias
|
||||
+ v.at_enum
|
||||
+ v.at_see
|
||||
+ v.at_diagnostic
|
||||
+ v.at_overload
|
||||
+ v.at_meta,
|
||||
|
||||
ext_ats = v.ext_at_note + v.ext_at_since + v.ext_at_nodoc + v.ext_at_brief,
|
||||
|
||||
at_param = Ct(
|
||||
Cg(P('param'), 'kind')
|
||||
* ws
|
||||
* Cg(lname, 'name')
|
||||
* ws
|
||||
* parenOpt(Cg(v.ltype, 'type'))
|
||||
* opt(desc_delim * Cg(rep(any), 'desc'))
|
||||
),
|
||||
|
||||
at_return = Ct(
|
||||
Cg(P('return'), 'kind')
|
||||
* ws
|
||||
* parenOpt(comma(Ct(Cg(v.ltype, 'type') * opt(ws * Cg(ident, 'name')))))
|
||||
* opt(desc_delim * Cg(rep(any), 'desc'))
|
||||
),
|
||||
|
||||
at_type = Ct(
|
||||
Cg(P('type'), 'kind')
|
||||
* ws
|
||||
* parenOpt(comma(Ct(Cg(v.ltype, 'type'))))
|
||||
* opt(desc_delim * Cg(rep(any), 'desc'))
|
||||
),
|
||||
|
||||
at_cast = Ct(
|
||||
Cg(P('cast'), 'kind') * ws * Cg(lname, 'name') * ws * opt(Sf('+-')) * Cg(v.ltype, 'type')
|
||||
),
|
||||
|
||||
at_generic = Ct(
|
||||
Cg(P('generic'), 'kind') * ws * Cg(ident, 'name') * opt(Pf ':' * Cg(v.ltype, 'type'))
|
||||
),
|
||||
|
||||
at_class = Ct(
|
||||
Cg(P('class'), 'kind')
|
||||
* ws
|
||||
* opt(P('(exact)') * ws)
|
||||
* Cg(lname, 'name')
|
||||
* opt(Pf(':') * Cg(lname, 'parent'))
|
||||
),
|
||||
|
||||
at_field = Ct(
|
||||
Cg(P('field'), 'kind')
|
||||
* ws
|
||||
* opt(Cg(Pf('private') + Pf('package') + Pf('protected'), 'access'))
|
||||
* Cg(lname, 'name')
|
||||
* ws
|
||||
* Cg(v.ltype, 'type')
|
||||
* opt(desc_delim * Cg(rep(any), 'desc'))
|
||||
),
|
||||
|
||||
at_access = Ct(Cg(P('private') + P('protected') + P('package'), 'kind')),
|
||||
|
||||
at_deprecated = Ct(Cg(P('deprecated'), 'kind')),
|
||||
|
||||
-- Types may be provided on subsequent lines
|
||||
at_alias = Ct(Cg(P('alias'), 'kind') * ws * Cg(lname, 'name') * opt(ws * Cg(v.ltype, 'type'))),
|
||||
|
||||
at_enum = Ct(Cg(P('enum'), 'kind') * ws * Cg(lname, 'name')),
|
||||
|
||||
at_see = Ct(Cg(P('see'), 'kind') * ws * opt(Pf('#')) * Cg(rep(any), 'desc')),
|
||||
at_diagnostic = Ct(Cg(P('diagnostic'), 'kind') * ws * opt(Pf('#')) * Cg(rep(any), 'desc')),
|
||||
at_overload = Ct(Cg(P('overload'), 'kind') * ws * Cg(v.ltype, 'type')),
|
||||
at_meta = Ct(Cg(P('meta'), 'kind')),
|
||||
|
||||
--- Custom extensions
|
||||
ext_at_note = Ct(Cg(P('note'), 'kind') * ws * Cg(rep(any), 'desc')),
|
||||
|
||||
-- TODO only consume 1 line
|
||||
ext_at_since = Ct(Cg(P('since'), 'kind') * ws * Cg(rep(any), 'desc')),
|
||||
|
||||
ext_at_nodoc = Ct(Cg(P('nodoc'), 'kind')),
|
||||
ext_at_brief = Ct(Cg(P('brief'), 'kind') * opt(ws * Cg(rep(any), 'desc'))),
|
||||
|
||||
ltype = v.ty_union + Pf '(' * v.ty_union * fill * P ')',
|
||||
|
||||
ty_union = v.ty_opt * rep(Pf '|' * v.ty_opt),
|
||||
ty = v.ty_fun + ident + v.ty_table + literal,
|
||||
ty_param = Pf '<' * comma(v.ltype) * fill * P '>',
|
||||
ty_opt = v.ty * opt(v.ty_param) * opt(P '[]') * opt(P '?'),
|
||||
|
||||
table_key = (Pf '[' * literal * Pf ']') + lname,
|
||||
table_elem = v.table_key * Pf ':' * v.ltype,
|
||||
ty_table = Pf '{' * comma(v.table_elem) * Pf '}',
|
||||
|
||||
fun_param = lname * opt(Pf ':' * v.ltype),
|
||||
ty_fun = Pf 'fun(' * rep(comma(v.fun_param)) * fill * P ')' * opt(Pf ':' * comma(v.ltype)),
|
||||
}
|
||||
|
||||
return grammar --[[@as nvim.luacats.grammar]]
|
||||
521
scripts/luacats_parser.lua
Normal file
521
scripts/luacats_parser.lua
Normal file
@@ -0,0 +1,521 @@
|
||||
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 name string
|
||||
--- @field desc string
|
||||
--- @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
|
||||
|
||||
-- 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
|
||||
state.cur_obj = {
|
||||
kind = 'class',
|
||||
name = parsed.name,
|
||||
parent = parsed.parent,
|
||||
desc = '',
|
||||
fields = {},
|
||||
}
|
||||
elseif kind == 'field' then
|
||||
--- @cast parsed nvim.luacats.Field
|
||||
if not parsed.access 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)
|
||||
end
|
||||
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 == '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 filename string
|
||||
--- @return string?
|
||||
local function determine_modvar(filename)
|
||||
local modvar --- @type string?
|
||||
for line in io.lines(filename) 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 = {}
|
||||
|
||||
--- @param filename string
|
||||
--- @return table<string,nvim.luacats.parser.class> classes
|
||||
--- @return nvim.luacats.parser.fun[] funs
|
||||
--- @return string[] briefs
|
||||
--- @return nvim.luacats.parser.obj[]
|
||||
function M.parse(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(filename)
|
||||
|
||||
--- @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 io.lines(filename) 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
|
||||
|
||||
return M
|
||||
239
scripts/text_utils.lua
Normal file
239
scripts/text_utils.lua
Normal file
@@ -0,0 +1,239 @@
|
||||
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 M = {}
|
||||
|
||||
local function contains(t, xs)
|
||||
return vim.tbl_contains(xs, t)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
if add_tag then
|
||||
parts[#parts + 1] = '<' .. node.type .. '>'
|
||||
end
|
||||
|
||||
if node.type == 'paragraph' then
|
||||
local text = assert(node.text)
|
||||
text = text:gsub('(%s)%*(%w+)%*(%s)', '%1%2%3')
|
||||
text = text:gsub('(%s)_(%w+)_(%s)', '%1%2%3')
|
||||
text = text:gsub('\\|', '|')
|
||||
text = text:gsub('\\%*', '*')
|
||||
text = text:gsub('\\_', '_')
|
||||
parts[#parts + 1] = M.wrap(text, start_indent, indent, text_width)
|
||||
parts[#parts + 1] = '\n'
|
||||
elseif node.type == '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 node.type == '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 node.type == 'html_block' then
|
||||
local text = node.text:gsub('^<pre>help', '')
|
||||
text = text:gsub('</pre>%s*$', '')
|
||||
parts[#parts + 1] = text
|
||||
elseif node.type == 'list_marker_dot' then
|
||||
parts[#parts + 1] = node.text
|
||||
elseif contains(node.type, { 'list_marker_minus', 'list_marker_star' }) then
|
||||
parts[#parts + 1] = '• '
|
||||
elseif node.type == '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
|
||||
vim.list_extend(parts, render_md(child, start_indent, indent, text_width, level + 1, is_list))
|
||||
if node.type ~= '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] = '</' .. node.type .. '>'
|
||||
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), '\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
|
||||
Reference in New Issue
Block a user