mirror of
https://github.com/neovim/nvim-lspconfig.git
synced 2025-12-24 23:11:00 +01:00
Problem:
If a config throws an error it fails the entire doc generation.
Solution:
Handle config error in docgen. Unfortunately, this doesn't show the
error message, it shows:
loop or previous error loading module 'lsp.volar'
instead of the actual `error('…')` message.
So meanwhile, document the current deprecation/rename pattern and use
`vim.notify()` instead.
292 lines
7.8 KiB
Lua
Executable File
292 lines
7.8 KiB
Lua
Executable File
#!/usr/bin/env -S nvim -l
|
|
|
|
-- Usage:
|
|
--
|
|
-- HOME=./ nvim --clean -R -Es -V1 +'set rtp+=$PWD' +'luafile scripts/docgen.lua'
|
|
|
|
local root = vim.trim(vim.system({ 'git', 'rev-parse', '--show-toplevel' }):wait().stdout)
|
|
vim.opt.rtp:append(root)
|
|
|
|
local util = require 'lspconfig.util'
|
|
|
|
local function template(s, params)
|
|
return (s:gsub('{{([^{}]+)}}', params))
|
|
end
|
|
|
|
local function map_list(t, func)
|
|
local res = {}
|
|
for i, v in ipairs(t) do
|
|
local x = func(v, i)
|
|
if x ~= nil then
|
|
table.insert(res, x)
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
local function map_sorted(t, func)
|
|
local res = {}
|
|
for k, v in vim.spairs(t) do
|
|
local x = func(k, v)
|
|
if x ~= nil then
|
|
table.insert(res, x)
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
local function indent(n, s)
|
|
local prefix = string.rep(' ', n)
|
|
return s:gsub('([^\n]+)', prefix .. '%1')
|
|
end
|
|
|
|
local function make_parts(fns)
|
|
return util.tbl_flatten(map_list(fns, function(v)
|
|
if type(v) == 'function' then
|
|
v = v()
|
|
end
|
|
return { v }
|
|
end))
|
|
end
|
|
|
|
--- @return string
|
|
local function make_section(indentlvl, sep, parts)
|
|
local flat = make_parts(parts)
|
|
if not flat or #flat == 0 then
|
|
return ''
|
|
end
|
|
return indent(indentlvl, table.concat(flat, sep))
|
|
end
|
|
|
|
local function readfile(path)
|
|
assert((vim.uv.fs_stat(path) or {}).type == 'file')
|
|
return io.open(path):read '*a'
|
|
end
|
|
|
|
local section_template_txt = [[
|
|
------------------------------------------------------------------------------
|
|
{{config_name}}
|
|
|
|
{{preamble}}
|
|
|
|
Snippet to enable the language server: >lua
|
|
vim.lsp.enable('{{config_name}}')
|
|
|
|
{{commands}}
|
|
Default config:
|
|
{{default_values}}
|
|
]]
|
|
|
|
local section_template_md = [[
|
|
## {{config_name}}
|
|
|
|
{{preamble}}
|
|
|
|
Snippet to enable the language server:
|
|
```lua
|
|
vim.lsp.enable('{{config_name}}')
|
|
```
|
|
{{commands}}
|
|
Default config:
|
|
{{default_values}}
|
|
|
|
---
|
|
]]
|
|
|
|
--- Converts markdown "```" codeblock to vimdoc format.
|
|
---
|
|
--- @return string
|
|
local function codeblock_to_vimdoc(doc)
|
|
local function make_fn(before, extra)
|
|
return function(lang, code)
|
|
if not code then
|
|
code = lang
|
|
lang = ''
|
|
end
|
|
-- Indent code by 2 spaces.
|
|
return before .. '>' .. lang .. extra .. code:gsub('[^\n]+', ' %0')
|
|
end
|
|
end
|
|
|
|
doc = doc
|
|
-- "```lang" following a nonblank line.
|
|
:gsub('[^%s]\n```(%w+)\n(.-)\n```', make_fn(' ', '\n'))
|
|
-- "```lang" following a blank line.
|
|
:gsub('\n```(%w+)\n(.-)\n```', make_fn('', '\n'))
|
|
-- "```" (no language)
|
|
:gsub('\n```\n(..-)\n```', make_fn('', '\n'))
|
|
|
|
return doc
|
|
end
|
|
|
|
--- Gets docstring by looking for "@brief" in a Lua code docstring.
|
|
---
|
|
--- @return string
|
|
local function extract_brief(text, is_markdown)
|
|
local doc = text:match('%-%-+ *%@brief.-(\n%-%-.*)')
|
|
if not doc then
|
|
return ''
|
|
end
|
|
-- Remove all lines following the last comment ("-- …").
|
|
doc = doc:match('(.-)\n[^-]+')
|
|
-- Remove "--" prefix from all lines.
|
|
doc = doc:gsub('\n%-%-+', '\n')
|
|
-- Remove leading whitespace (shared indent).
|
|
doc = vim.trim(vim.text.indent(0, doc))
|
|
|
|
-- Convert codeblocks for vimdoc.
|
|
if not is_markdown then
|
|
doc = codeblock_to_vimdoc(doc)
|
|
end
|
|
|
|
return doc
|
|
end
|
|
|
|
local function make_lsp_section(config_sections, config_name, config_file, is_markdown)
|
|
local t = is_markdown and section_template_md or section_template_txt
|
|
local params = {
|
|
config_name = config_name,
|
|
preamble = '',
|
|
commands = '',
|
|
default_values = '',
|
|
}
|
|
|
|
local ok, config = pcall(require, 'lsp.' .. config_name)
|
|
-- If the config throws an error (e.g. "renamed to …"), just show the error message.
|
|
if not ok then
|
|
params.preamble = config
|
|
table.insert(config_sections, template(t, params))
|
|
return
|
|
end
|
|
|
|
params.preamble = extract_brief(readfile(config_file), is_markdown)
|
|
|
|
-- TODO: get commands by parsing `nvim_buf_create_user_command` calls.
|
|
params.commands = make_section(0, '\n', {
|
|
function()
|
|
if not config.commands or #vim.tbl_keys(config.commands) == 0 then
|
|
return
|
|
end
|
|
return ('\nCommands:\n%s\n'):format(make_section(0, '\n', {
|
|
map_sorted(config.commands, function(name, _)
|
|
return string.format('- %s', name)
|
|
end),
|
|
}))
|
|
end,
|
|
})
|
|
|
|
params.default_values = make_section(0, '\n', {
|
|
function()
|
|
return make_section(0, '\n', {
|
|
map_sorted(config, function(k, v)
|
|
if type(v) == 'boolean' then
|
|
return ('- `%s` : `%s`'):format(k, v)
|
|
elseif type(v) ~= 'function' and k ~= 'root_dir' then
|
|
if is_markdown then
|
|
return ('- `%s` :\n ```lua\n%s\n ```'):format(k, indent(2, vim.inspect(v)))
|
|
else
|
|
return ('- %s: >lua\n%s'):format(k, indent(2, vim.inspect(v)))
|
|
end
|
|
end
|
|
|
|
local file = assert(io.open(config_file, 'r'))
|
|
local linenr = 0
|
|
-- Find the `return` line, where the config starts.
|
|
for line in file:lines() do
|
|
linenr = linenr + 1
|
|
if line:find('^return') then
|
|
break
|
|
end
|
|
end
|
|
io.close(file)
|
|
local config_relpath = vim.fs.relpath(root, config_file)
|
|
|
|
-- XXX: "../" because the path is outside of the doc/ dir.
|
|
if is_markdown then
|
|
return ('- `%s`: [../%s:%d](../%s#L%d)'):format(k, config_relpath, linenr, config_relpath, linenr)
|
|
else
|
|
return ('- %s (use "gF" to view): ../%s:%d'):format(k, config_relpath, linenr)
|
|
end
|
|
end),
|
|
})
|
|
end,
|
|
}) .. (is_markdown and '' or '\n<') -- Workaround tree-sitter-vimdoc bug.
|
|
|
|
table.insert(config_sections, template(t, params))
|
|
end
|
|
|
|
local function make_lsp_sections(is_markdown)
|
|
local lsp_files = vim.fs.find(function(f)
|
|
return f:match('.*%.lua$')
|
|
end, { type = 'file', path = vim.fs.joinpath(root, 'lsp'), limit = math.huge })
|
|
local config_sections = {}
|
|
|
|
for _, config_file in ipairs(lsp_files) do
|
|
-- "lua/xx.lua"
|
|
local config_name = config_file:match('lsp/(.-)%.lua$')
|
|
if config_name then
|
|
-- HACK: Avoid variable data (username, pid) in the generated document.
|
|
-- local old_home = vim.env.HOME
|
|
-- local old_cache_home = vim.env.XDG_CACHE_HOME
|
|
-- vim.env.HOME = '/home/user'
|
|
-- vim.env.XDG_CACHE_HOME = '/home/user/.cache'
|
|
local old_fn = vim.fn
|
|
vim.fn = setmetatable({}, {
|
|
__index = function(_t, key)
|
|
if key == 'getpid' then
|
|
return function()
|
|
return 12345
|
|
end
|
|
end
|
|
return old_fn[key]
|
|
end,
|
|
})
|
|
|
|
make_lsp_section(config_sections, config_name, config_file, is_markdown)
|
|
|
|
-- Reset.
|
|
-- vim.env.HOME = old_home
|
|
-- vim.env.XDG_CACHE_HOME = old_cache_home
|
|
vim.fn = old_fn
|
|
end
|
|
end
|
|
|
|
return make_section(0, '\n', config_sections)
|
|
end
|
|
|
|
--- Gets the list of config names and returns "table of contents" as markdown.
|
|
local function make_toc()
|
|
local lsp_files = vim.fs.find(function(f)
|
|
return f:match('.*%.lua$')
|
|
end, { type = 'file', path = vim.fs.joinpath(root, 'lsp'), limit = math.huge })
|
|
local server_list = {}
|
|
|
|
for _, file_path in ipairs(lsp_files) do
|
|
local config_name = file_path:match('lsp/(.-)%.lua$')
|
|
if config_name then
|
|
table.insert(server_list, template('- [{{server}}](#{{server}})', { server = config_name }))
|
|
end
|
|
end
|
|
|
|
return make_section(0, '\n', server_list)
|
|
end
|
|
|
|
local function generate_readme(template_file, params, output_file)
|
|
local input_template = readfile(template_file)
|
|
local readme_data = template(input_template, params)
|
|
|
|
local writer = assert(io.open(output_file, 'w'))
|
|
writer:write(readme_data)
|
|
writer:close()
|
|
end
|
|
|
|
generate_readme(vim.fs.joinpath(root, 'scripts/docs_template.md'), {
|
|
toc = make_toc(),
|
|
lsp_server_details = make_lsp_sections(true),
|
|
}, vim.fs.joinpath(root, 'doc/configs.md'))
|
|
|
|
generate_readme(vim.fs.joinpath(root, 'scripts/docs_template.txt'), {
|
|
toc = make_toc(),
|
|
lsp_server_details = make_lsp_sections(false),
|
|
}, vim.fs.joinpath(root, 'doc/configs.txt'))
|