diff options
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/gen_eval_files.lua | 2 | ||||
-rw-r--r-- | scripts/gen_help_html.lua | 2 | ||||
-rwxr-xr-x | scripts/gen_vimdoc.lua | 315 | ||||
-rw-r--r-- | scripts/luacats_grammar.lua | 182 | ||||
-rw-r--r-- | scripts/luacats_parser.lua | 66 | ||||
-rw-r--r-- | scripts/text_utils.lua | 168 |
6 files changed, 511 insertions, 224 deletions
diff --git a/scripts/gen_eval_files.lua b/scripts/gen_eval_files.lua index 83e55b3bc4..f1bba5c0a2 100755 --- a/scripts/gen_eval_files.lua +++ b/scripts/gen_eval_files.lua @@ -40,7 +40,7 @@ local LUA_API_RETURN_OVERRIDES = { 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.float_config', + nvim_win_get_config = 'vim.api.keyset.win_config', } local LUA_META_HEADER = { diff --git a/scripts/gen_help_html.lua b/scripts/gen_help_html.lua index 5cda16bbe6..43040151eb 100644 --- a/scripts/gen_help_html.lua +++ b/scripts/gen_help_html.lua @@ -752,7 +752,7 @@ end --- --- @param fname string help file to parse --- @param parser_path string? path to non-default vimdoc.so ---- @return LanguageTree, integer (lang_tree, bufnr) +--- @return vim.treesitter.LanguageTree, integer (lang_tree, bufnr) local function parse_buf(fname, parser_path) local buf ---@type integer if type(fname) == 'string' then diff --git a/scripts/gen_vimdoc.lua b/scripts/gen_vimdoc.lua index 8acae66f49..b3b211d5a6 100755 --- a/scripts/gen_vimdoc.lua +++ b/scripts/gen_vimdoc.lua @@ -94,12 +94,12 @@ end 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) + 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) + return fmt('%s.%s%s', fun.module, fun.name, fn_sfx) end - return fmt('*%s%s*', fun.name, fn_sfx) + return fun.name .. fn_sfx end --- @type table<string,nvim.gen_vimdoc.Config> @@ -129,7 +129,7 @@ local config = { return name .. ' Functions' end, helptag_fmt = function(name) - return fmt('*api-%s*', name:lower()) + return fmt('api-%s', name:lower()) end, }, lua = { @@ -241,22 +241,22 @@ local config = { end, helptag_fmt = function(name) if name == '_editor' then - return '*lua-vim*' + return 'lua-vim' elseif name == '_options' then - return '*lua-vimscript*' + return 'lua-vimscript' elseif name == 'tohtml' then - return '*tohtml*' + return 'tohtml' end - return '*vim.' .. name:lower() .. '*' + 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) + return name .. fn_sfx elseif fun.classvar == 'Option' then - return fmt('*vim.opt:%s()*', name) + return fmt('vim.opt:%s()', name) end return fn_helptag_fmt_common(fun) @@ -269,6 +269,7 @@ local config = { filename = 'lsp.txt', section_order = { 'lsp.lua', + 'client.lua', 'buf.lua', 'diagnostic.lua', 'codelens.lua', @@ -296,9 +297,9 @@ local config = { end, helptag_fmt = function(name) if name:lower() == 'lsp' then - return '*lsp-core*' + return 'lsp-core' end - return fmt('*lsp-%s*', name:lower()) + return fmt('lsp-%s', name:lower()) end, }, diagnostic = { @@ -311,7 +312,7 @@ local config = { return 'Lua module: vim.diagnostic' end, helptag_fmt = function() - return '*diagnostic-api*' + return 'diagnostic-api' end, }, treesitter = { @@ -336,9 +337,28 @@ local config = { end, helptag_fmt = function(name) if name:lower() == 'treesitter' then - return '*lua-treesitter-core*' + return 'lua-treesitter-core' end - return '*lua-treesitter-' .. name:lower() .. '*' + return 'lua-treesitter-' .. name:lower() + end, + }, + editorconfig = { + filename = 'editorconfig.txt', + files = { + 'runtime/lua/editorconfig.lua', + }, + section_order = { + 'editorconfig.lua', + }, + section_fmt = function(_name) + return 'EditorConfig integration' + end, + helptag_fmt = function(name) + return name:lower() + end, + fn_xform = function(fun) + fun.table = true + fun.name = vim.split(fun.name, '.', { plain = true })[2] end, }, } @@ -362,15 +382,25 @@ local function replace_generics(ty, generics) 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> -local function render_type(ty, generics) +--- @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 @@ -379,10 +409,101 @@ 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, exclude_types) +local function render_fields_or_params(xs, generics, classes, exclude_types) local ret = {} --- @type string[] xs = vim.tbl_filter(should_render_param, xs) @@ -398,15 +519,27 @@ local function render_fields_or_params(xs, generics, exclude_types) end for _, p in ipairs(xs) do - local nm, ty = p.name, p.type - local desc = p.desc - local pnm = fmt(' • %-' .. indent .. 's', '{' .. nm .. '}') + 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) + local pty = render_type(ty, generics, default) + 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)) + 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 @@ -421,24 +554,47 @@ local function render_fields_or_params(xs, generics, exclude_types) 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 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 @@ -448,7 +604,7 @@ local function render_fun_header(fun, cfg) 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('%?$', '')) + args[#args + 1] = fmt_field_name(p.name) end end @@ -463,7 +619,7 @@ local function render_fun_header(fun, cfg) cfg.fn_helptag_fmt = fn_helptag_fmt_common end - local tag = cfg.fn_helptag_fmt(fun) + local tag = '*' .. cfg.fn_helptag_fmt(fun) .. '*' if #proto + #tag > TEXT_WIDTH - 8 then table.insert(ret, fmt('%78s\n', tag)) @@ -480,8 +636,9 @@ 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, exclude_types) +local function render_returns(returns, generics, classes, exclude_types) local ret = {} --- @type string[] returns = vim.deepcopy(returns) @@ -498,26 +655,26 @@ local function render_returns(returns, generics, exclude_types) end for _, p in ipairs(returns) do + inline_type(p, classes) local rnm, ty, desc = p.name, p.type, p.desc - local blk = '' + + local blk = {} --- @type string[] if ty then - blk = render_type(ty, generics) + blk[#blk + 1] = 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)) + 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, cfg) +local function render_fun(fun, classes, cfg) if fun.access or fun.deprecated or fun.nodoc then return end @@ -570,7 +727,7 @@ local function render_fun(fun, cfg) end if fun.params and #fun.params > 0 then - local param_txt = render_fields_or_params(fun.params, fun.generics, cfg.exclude_types) + 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 @@ -578,7 +735,7 @@ local function render_fun(fun, cfg) end if fun.returns then - local txt = render_returns(fun.returns, fun.generics, cfg.exclude_types) + 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 @@ -597,15 +754,16 @@ local function render_fun(fun, cfg) 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, cfg) +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, cfg) + ret[#ret + 1] = render_fun(f, classes, cfg) end -- Sort via prototype @@ -677,7 +835,7 @@ local function make_section(filename, cfg, section_docs, funs_txt) 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) + local help_tag = '*' .. cfg.helptag_fmt(sectname) .. '*' if funs_txt == '' and #section_docs == 0 then return @@ -706,9 +864,9 @@ local function render_section(section, add_header) }) end - if section.doc and #section.doc > 0 then - table.insert(doc, '\n\n') - vim.list_extend(doc, section.doc) + local sdoc = '\n\n' .. table.concat(section.doc or {}, '\n') + if sdoc:find('[^%s]') then + doc[#doc + 1] = sdoc end if section.funs_txt then @@ -741,19 +899,41 @@ end --- @param cfg nvim.gen_vimdoc.Config local function gen_target(cfg) + print('Target:', cfg.filename) local sections = {} --- @type table<string,nvim.gen_vimdoc.Section> expand_files(cfg.files) - for _, f in pairs(cfg.files) do + --- @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 vim.spairs(cfg.files) do local ext = assert(f:match('%.([^.]+)$')) --[[@as 'h'|'c'|'lua']] local parser = assert(parsers[ext]) - local _, funs, briefs = parser(f) + local classes, funs, briefs = parser(f) + file_results[f] = { classes, funs, briefs } + all_classes = vim.tbl_extend('error', all_classes, classes) + end + + for f, r in vim.spairs(file_results) do + local classes, funs, briefs = r[1], r[2], r[3] + local briefs_txt = {} --- @type string[] for _, b in ipairs(briefs) do briefs_txt[#briefs_txt + 1] = md_to_vimdoc(b, 0, 0, TEXT_WIDTH) end - local funs_txt = render_funs(funs, cfg) + print(' Processing file:', f) + 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) @@ -764,8 +944,9 @@ local function gen_target(cfg) for _, f in ipairs(cfg.section_order) do local section = sections[f] if section then + print(string.format(" Rendering section: '%s'", section.title)) local add_sep_and_header = not vim.tbl_contains(cfg.append_only or {}, f) - table.insert(docs, render_section(section, add_sep_and_header)) + docs[#docs + 1] = render_section(section, add_sep_and_header) end end @@ -786,7 +967,7 @@ local function gen_target(cfg) end local function run() - for _, cfg in pairs(config) do + for _, cfg in vim.spairs(config) do gen_target(cfg) end end diff --git a/scripts/luacats_grammar.lua b/scripts/luacats_grammar.lua index ee0f9d8e87..ca26c70156 100644 --- a/scripts/luacats_grammar.lua +++ b/scripts/luacats_grammar.lua @@ -21,8 +21,7 @@ local function opt(x) return x ^ -1 end -local nl = P('\r\n') + P('\n') -local ws = rep1(S(' \t') + nl) +local ws = rep1(S(' \t')) local fill = opt(ws) local any = P(1) -- (consume one character) @@ -30,11 +29,11 @@ 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 string_double = P('"') * rep(any - P('"')) * P('"') -local literal = (string_single + string_double + (opt(P '-') * num) + P 'false' + P 'true') +local literal = (string_single + string_double + (opt(P('-')) * num) + P('false') + P('true')) -local lname = (ident + P '...') * opt(P '?') +local lname = (ident + P('...')) * opt(P('?')) --- @param x string local function Pf(x) @@ -47,13 +46,23 @@ local function Sf(x) end --- @param x vim.lpeg.Pattern -local function comma(x) - return x * rep(Pf ',' * x) +local function paren(x) + return Pf('(') * x * fill * P(')') end --- @param x vim.lpeg.Pattern local function parenOpt(x) - return (Pf('(') * x * fill * P(')')) + 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> @@ -63,7 +72,15 @@ local v = setmetatable({}, { 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' @@ -85,6 +102,7 @@ local desc_delim = Sf '#:' + ws --- @field kind 'class' --- @field name string --- @field parent? string +--- @field access? 'private'|'protected'|'package' --- @class nvim.luacats.Field --- @field kind 'field' @@ -107,112 +125,60 @@ local desc_delim = Sf '#:' + ws --- @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 = 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')), + 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_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 '}', + ext_ats = ( + annot('note', desc) + + annot('since', desc) + + annot('nodoc') + + annot('inlinedoc') + + annot('brief', desc) + ), - fun_param = lname * opt(Pf ':' * v.ltype), - ty_fun = Pf 'fun(' * rep(comma(v.fun_param)) * fill * P ')' * opt(Pf ':' * comma(v.ltype)), + 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 index 520272d1dc..cd671fb9dc 100644 --- a/scripts/luacats_parser.lua +++ b/scripts/luacats_parser.lua @@ -19,7 +19,7 @@ local luacats_grammar = require('scripts.luacats_grammar') --- @class nvim.luacats.parser.alias --- @field kind 'alias' ---- @field type string +--- @field type string[] --- @field desc string --- @class nvim.luacats.parser.fun @@ -49,8 +49,12 @@ local luacats_grammar = require('scripts.luacats_grammar') --- @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[] @@ -64,6 +68,7 @@ local luacats_grammar = require('scripts.luacats_grammar') --- | 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: @@ -142,22 +147,27 @@ local function process_doc_line(line, state) } elseif kind == 'class' then --- @cast parsed nvim.luacats.Class - state.cur_obj = { - kind = 'class', - name = parsed.name, - parent = parsed.parent, - desc = '', - fields = {}, - } + 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 - 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) + 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 @@ -191,6 +201,8 @@ local function process_doc_line(line, state) 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 @@ -383,11 +395,11 @@ end --- Determine the table name used to export functions of a module --- Usually this is `M`. ---- @param filename string +--- @param str string --- @return string? -local function determine_modvar(filename) +local function determine_modvar(str) local modvar --- @type string? - for line in io.lines(filename) do + for line in vim.gsplit(str, '\n') do do --- @type string? local m = line:match('^return%s+([a-zA-Z_]+)') @@ -462,17 +474,12 @@ 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) +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(filename) + local mod_return = determine_modvar(str) --- @type string local module = filename:match('.*/lua/([a-z_][a-z0-9_/]+)%.lua') or filename @@ -485,7 +492,7 @@ function M.parse(filename) -- Keep track of any partial objects we don't commit local uncommitted = {} --- @type nvim.luacats.parser.obj[] - for line in io.lines(filename) do + for line in vim.gsplit(str, '\n') do local has_indent = line:match('^%s+') ~= nil line = vim.trim(line) if vim.startswith(line, '---') then @@ -518,4 +525,13 @@ function M.parse(filename) 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 index 5167ec42f2..75b3bfedd5 100644 --- a/scripts/text_utils.lua +++ b/scripts/text_utils.lua @@ -7,12 +7,99 @@ local fmt = string.format 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) @@ -47,6 +134,10 @@ local function parse_md(text) 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 @@ -101,20 +192,47 @@ local function render_md(node, start_indent, indent, text_width, level, is_list) local add_tag = false -- local add_tag = true + local ntype = node.type + if add_tag then - parts[#parts + 1] = '<' .. node.type .. '>' + parts[#parts + 1] = '<' .. ntype .. '>' 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) + 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 node.type == 'code_fence_content' then + elseif ntype == 'code_fence_content' then local lines = vim.split(node.text:gsub('\n%s*$', ''), '\n') local cindent = indent + INDENTATION @@ -137,7 +255,7 @@ local function render_md(node, start_indent, indent, text_width, level, is_list) end parts[#parts + 1] = '\n' end - elseif node.type == 'fenced_code_block' then + elseif ntype == 'fenced_code_block' then parts[#parts + 1] = '>' for _, child in ipairs(node) do if child.type == 'info_string' then @@ -152,15 +270,15 @@ local function render_md(node, start_indent, indent, text_width, level, is_list) end end parts[#parts + 1] = '<\n' - elseif node.type == 'html_block' then + elseif ntype == '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 + elseif ntype == 'list_marker_dot' then parts[#parts + 1] = node.text - elseif contains(node.type, { 'list_marker_minus', 'list_marker_star' }) then + elseif contains(ntype, { 'list_marker_minus', 'list_marker_star' }) then parts[#parts + 1] = '• ' - elseif node.type == 'list_item' then + 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 @@ -175,8 +293,12 @@ local function render_md(node, start_indent, indent, text_width, level, is_list) 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 + 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 @@ -185,7 +307,7 @@ local function render_md(node, start_indent, indent, text_width, level, is_list) end if add_tag then - parts[#parts + 1] = '</' .. node.type .. '>' + parts[#parts + 1] = '</' .. ntype .. '>' end return parts @@ -196,7 +318,7 @@ local function align_tags(text_width) --- @param line string --- @return string return function(line) - local tag_pat = '%s+(%*[^ ]+%*)%s*$' + local tag_pat = '%s*(%*.+%*)%s*$' local tags = {} for m in line:gmatch(tag_pat) do table.insert(tags, m) @@ -205,7 +327,9 @@ local function align_tags(text_width) if #tags > 0 then line = line:gsub(tag_pat, '') local tags_str = ' ' .. table.concat(tags, ' ') - local pad = string.rep(' ', text_width - #line - #tags_str) + --- @type integer + local conceal_offset = select(2, tags_str:gsub('%*', '')) - 2 + local pad = string.rep(' ', text_width - #line - #tags_str + conceal_offset) return line .. pad .. tags_str end @@ -223,14 +347,14 @@ function M.md_to_vimdoc(text, start_indent, indent, text_width, is_list) 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') + 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*>([a-z]+)\n', ' >%1\n') s = s:gsub('\n+%s*>\n?\n', ' >\n') return s |