509 lines
13 KiB
Lua
Executable File
509 lines
13 KiB
Lua
Executable File
#!/usr/bin/env -S nvim -l
|
|
-- Simple script to update the help doc by reading the config schema.
|
|
|
|
local inspect = vim.inspect
|
|
local list_extend = vim.list_extend
|
|
local startswith = vim.startswith
|
|
|
|
local config = require('lua.gitsigns.config')
|
|
|
|
-- To make sure the output is consistent between runs (to minimise diffs), we
|
|
-- need to iterate through the schema keys in a deterministic way. To do this we
|
|
-- do a smple scan over the file the schema is defined in and collect the keys
|
|
-- in the order they are defined.
|
|
--- @return string[]
|
|
local function get_ordered_schema_keys()
|
|
local ci = io.lines('lua/gitsigns/config.lua') --- @type Iterator[string]
|
|
|
|
for l in ci do
|
|
if startswith(l, 'M.schema = {') then
|
|
break
|
|
end
|
|
end
|
|
|
|
local keys = {}
|
|
for l in ci do
|
|
if startswith(l, '}') then
|
|
break
|
|
end
|
|
if l:find('^ (%w+).*') then
|
|
local lc = l:gsub('^%s*([%w_]+).*', '%1')
|
|
table.insert(keys, lc)
|
|
end
|
|
end
|
|
|
|
return keys
|
|
end
|
|
|
|
--- @param dep_info boolean|{new_field: string, message: string, hard: boolean}
|
|
--- @param out fun(_: string?)
|
|
local function gen_config_doc_deprecated(dep_info, out)
|
|
if type(dep_info) == 'table' and dep_info.hard then
|
|
out(' HARD-DEPRECATED')
|
|
else
|
|
out(' DEPRECATED')
|
|
end
|
|
if type(dep_info) == 'table' then
|
|
if dep_info.message then
|
|
out(' ' .. dep_info.message)
|
|
end
|
|
if dep_info.new_field then
|
|
out('')
|
|
local opts_key, field = dep_info.new_field:match('(.*)%.(.*)')
|
|
if opts_key and field then
|
|
out(
|
|
(' Please instead use the field `%s` in |gitsigns-config-%s|.'):format(field, opts_key)
|
|
)
|
|
else
|
|
out((' Please instead use |gitsigns-config-%s|.'):format(dep_info.new_field))
|
|
end
|
|
end
|
|
end
|
|
out('')
|
|
end
|
|
|
|
--- @param field string
|
|
--- @param out fun(_: string?)
|
|
local function gen_config_doc_field(field, out)
|
|
local v = config.schema[field]
|
|
|
|
-- Field heading and tag
|
|
local t = ('*gitsigns-config-%s*'):format(field)
|
|
if #field + #t < 80 then
|
|
out(('%-29s %48s'):format(field, t))
|
|
else
|
|
out(('%-29s'):format(field))
|
|
out(('%78s'):format(t))
|
|
end
|
|
|
|
local deprecated = v.deprecated
|
|
if deprecated then
|
|
gen_config_doc_deprecated(deprecated, out)
|
|
end
|
|
|
|
if v.description then
|
|
local d --- @type string
|
|
local default_help = v.default_help
|
|
if default_help ~= nil then
|
|
d = default_help
|
|
else
|
|
d = inspect(v.default):gsub('\n', '\n ')
|
|
d = ('`%s`'):format(d)
|
|
end
|
|
|
|
local vtype = (function()
|
|
local ty = v.type_help or v.type
|
|
if ty == 'table' and v.deep_extend then
|
|
return 'table[extended]'
|
|
end
|
|
if type(ty) == 'table' then
|
|
v.type = table.concat(ty, '|')
|
|
end
|
|
return v.type
|
|
end)()
|
|
|
|
if d:find('\n') then
|
|
out((' Type: `%s`'):format(vtype))
|
|
out(' Default: >')
|
|
out(' ' .. d:gsub('\n([^\n\r])', '\n %1'))
|
|
out('<')
|
|
else
|
|
out((' Type: `%s`, Default: %s'):format(vtype, d))
|
|
out()
|
|
end
|
|
|
|
out(v.description:gsub(' +$', ''))
|
|
end
|
|
end
|
|
|
|
--- @return string
|
|
local function gen_config_doc()
|
|
local res = {} ---@type string[]
|
|
|
|
local function out(line)
|
|
res[#res + 1] = line or ''
|
|
end
|
|
|
|
for _, k in ipairs(get_ordered_schema_keys()) do
|
|
gen_config_doc_field(k, out)
|
|
end
|
|
return table.concat(res, '\n')
|
|
end
|
|
|
|
--- @param line string
|
|
--- @return string
|
|
local function parse_func_header(line)
|
|
-- match:
|
|
-- prefix.name = ...
|
|
-- function prefix.name(...
|
|
local func = line:match('^%w+%.([%w_]+) =')
|
|
or line:match('^function %w+%.([%w_]+)%(')
|
|
if not func then
|
|
error('Unable to parse: ' .. line)
|
|
end
|
|
local args_raw = line:match('function%((.*)%)') -- M.name = function(args)
|
|
or line:match('function%s+%w+%.[%w_]+%((.*)%)') -- function M.name(args)
|
|
local args = {} --- @type string[]
|
|
for k in string.gmatch(args_raw, '([%w_]+)') do
|
|
if k:sub(1, 1) ~= '_' then
|
|
args[#args + 1] = string.format('{%s}', k)
|
|
end
|
|
end
|
|
|
|
if line:match('async.create%(%d, function%(') then
|
|
args[#args + 1] = '{callback?}'
|
|
end
|
|
|
|
return string.format(
|
|
'%-40s%38s',
|
|
string.format('%s(%s)', func, table.concat(args, ', ')),
|
|
'*gitsigns.' .. func .. '()*'
|
|
)
|
|
end
|
|
|
|
--- @param x string
|
|
--- @return string? name
|
|
--- @return string? type
|
|
--- @return string? description
|
|
local function parse_param(x)
|
|
local name, ty, des = x:match('([^ ]+) +([^ ]+) *(.*)')
|
|
return name, ty, des
|
|
end
|
|
|
|
--- @param x string[]
|
|
--- @return string[]
|
|
local function trim_lines(x)
|
|
local min_pad --- @type integer?
|
|
for _, e in ipairs(x) do
|
|
local _, i = e:find('^ *')
|
|
if not min_pad or min_pad > i then
|
|
min_pad = i
|
|
end
|
|
end
|
|
|
|
local r = {} --- @type string[]
|
|
for _, e in ipairs(x) do
|
|
r[#r + 1] = e:sub(min_pad + 1)
|
|
end
|
|
|
|
return r
|
|
end
|
|
|
|
--- @param name string
|
|
--- @param ty string
|
|
--- @param desc string[]
|
|
--- @param name_pad? integer
|
|
--- @return string[]
|
|
local function render_param_or_return(name, ty, desc, name_pad)
|
|
ty = ty:gsub('Gitsigns%.%w+', 'table')
|
|
|
|
name_pad = name_pad and (name_pad + 3) or 0
|
|
local name_str --- @type string
|
|
|
|
if name == ':' then
|
|
name_str = ''
|
|
else
|
|
local nf = '%-' .. tostring(name_pad) .. 's'
|
|
name_str = nf:format(string.format('{%s} ', name))
|
|
end
|
|
|
|
if #desc == 0 then
|
|
return { string.format(' %s(%s)', name_str, ty) }
|
|
end
|
|
|
|
local r = {} --- @type string[]
|
|
|
|
local desc1 = desc[1] == '' and '' or ' ' .. desc[1]
|
|
r[#r + 1] = string.format(' %s(%s):%s', name_str, ty, desc1)
|
|
|
|
local remain_desc = trim_lines(vim.list_slice(desc, 2))
|
|
for _, d in ipairs(remain_desc) do
|
|
r[#r + 1] = ' ' .. string.rep(' ', name_pad) .. d
|
|
end
|
|
|
|
return r
|
|
end
|
|
|
|
--- @param x string[]
|
|
--- @param amount integer
|
|
--- @return string[]
|
|
local function pad(x, amount)
|
|
local pad_str = string.rep(' ', amount)
|
|
|
|
local r = {} --- @type string[]
|
|
for _, e in ipairs(x) do
|
|
r[#r + 1] = pad_str .. e
|
|
end
|
|
return r
|
|
end
|
|
|
|
--- @param state EmmyState
|
|
--- @param doc_comment string
|
|
--- @param desc string[]
|
|
--- @param params {[1]: string, [2]: string, [3]: string[]}[]
|
|
--- @param returns {[1]: string, [2]: string, [3]: string[]}[]
|
|
--- @return EmmyState
|
|
local function process_doc_comment(state, doc_comment, desc, params, returns)
|
|
if state == 'none' then
|
|
state = 'in_block'
|
|
end
|
|
|
|
local emmy_type, emmy_str = doc_comment:match(' ?@([a-z]+) (.*)')
|
|
|
|
if emmy_type == 'param' then
|
|
local name, ty, pdesc = parse_param(emmy_str)
|
|
params[#params + 1] = { name, ty, { pdesc } }
|
|
return 'in_param'
|
|
end
|
|
|
|
if emmy_type == 'return' then
|
|
local ty, name, rdesc = parse_param(emmy_str)
|
|
returns[#returns + 1] = { name, ty, { rdesc } }
|
|
return 'in_return'
|
|
end
|
|
|
|
if state == 'in_param' then
|
|
-- Consume any remaining doc document lines as the description for the
|
|
-- last parameter
|
|
local lastdes = params[#params][3]
|
|
lastdes[#lastdes + 1] = doc_comment
|
|
elseif state == 'in_return' then
|
|
-- Consume any remaining doc document lines as the description for the
|
|
-- last return
|
|
local lastdes = returns[#returns][3]
|
|
lastdes[#lastdes + 1] = doc_comment
|
|
else
|
|
if doc_comment ~= '' and doc_comment ~= '<' then
|
|
doc_comment = string.rep(' ', 16) .. doc_comment
|
|
end
|
|
desc[#desc + 1] = doc_comment
|
|
end
|
|
|
|
return state
|
|
end
|
|
|
|
--- @param header string
|
|
--- @param block string[]
|
|
--- @param params {[1]: string, [2]: string, [3]: string[]}[]
|
|
--- @param returns {[1]: string, [2]: string, [3]: string[]}[]
|
|
--- @param deprecated string?
|
|
--- @return string[]?
|
|
local function render_block(header, block, params, returns, deprecated)
|
|
if vim.startswith(header, '_') then
|
|
return
|
|
end
|
|
|
|
local res = { header }
|
|
|
|
if deprecated then
|
|
list_extend(res, {
|
|
' DEPRECATED: '..deprecated,
|
|
''
|
|
})
|
|
end
|
|
|
|
list_extend(res, block)
|
|
|
|
-- filter arguments beginning with '_'
|
|
params = vim.tbl_filter(
|
|
--- @param v {[1]: string, [2]: string, [3]: string[]}
|
|
--- @return boolean
|
|
function(v)
|
|
return not startswith(v[1], '_')
|
|
end,
|
|
params
|
|
)
|
|
|
|
if #params > 0 then
|
|
local param_block = { 'Parameters: ~' }
|
|
|
|
local name_pad = 0
|
|
for _, v in ipairs(params) do
|
|
if #v[1] > name_pad then
|
|
name_pad = #v[1]
|
|
end
|
|
end
|
|
|
|
for _, v in ipairs(params) do
|
|
local name, ty, desc = v[1], v[2], v[3]
|
|
list_extend(param_block, render_param_or_return(name, ty, desc, name_pad))
|
|
end
|
|
list_extend(res, pad(param_block, 16))
|
|
end
|
|
|
|
if #returns > 0 then
|
|
res[#res + 1] = ''
|
|
local param_block = { 'Returns: ~' }
|
|
for _, v in ipairs(returns) do
|
|
local name, ty, desc = v[1], v[2], v[3]
|
|
list_extend(param_block, render_param_or_return(name, ty, desc))
|
|
end
|
|
list_extend(res, pad(param_block, 16))
|
|
end
|
|
|
|
return res
|
|
end
|
|
|
|
--- @param path string
|
|
--- @return string
|
|
local function gen_functions_doc_from_file(path)
|
|
local i = io.lines(path) --- @type Iterator[string]
|
|
|
|
local blocks = {} --- @type string[][]
|
|
|
|
--- @alias EmmyState 'none'|'in_block'|'in_param'|'in_return'
|
|
local state = 'none' --- @type EmmyState
|
|
local desc = {} --- @type string[]
|
|
local params = {} --- @type {[1]: string, [2]: string, [3]: string[]}[]
|
|
local returns = {} --- @type {[1]: string, [2]: string, [3]: string[]}[]
|
|
local deprecated --- @type string?
|
|
|
|
for l in i do
|
|
local doc_comment = l:match('^%-%-%- ?(.*)') --- @type string?
|
|
if doc_comment then
|
|
local depre = doc_comment:match('@deprecated ?(.*)')
|
|
if depre then
|
|
deprecated = depre
|
|
else
|
|
state = process_doc_comment(state, doc_comment, desc, params, returns)
|
|
end
|
|
elseif state ~= 'none' then
|
|
-- First line after block
|
|
local ok, header = pcall(parse_func_header, l)
|
|
if ok then
|
|
blocks[#blocks + 1] = render_block(header, desc, params, returns, deprecated)
|
|
end
|
|
state = 'none'
|
|
desc = {}
|
|
params = {}
|
|
returns = {}
|
|
deprecated = nil
|
|
end
|
|
end
|
|
|
|
local res = {} --- @type string[]
|
|
for j = #blocks, 1, -1 do
|
|
local b = blocks[j]
|
|
for k = 1, #b do
|
|
res[#res + 1] = b[k]:match('^ *$') and '' or b[k]
|
|
end
|
|
res[#res + 1] = ''
|
|
end
|
|
|
|
return table.concat(res, '\n')
|
|
end
|
|
|
|
--- @param files string[]
|
|
--- @return string
|
|
local function gen_functions_doc(files)
|
|
local res = {} --- @type string[]
|
|
for _, path in ipairs(files) do
|
|
res[#res + 1] = gen_functions_doc_from_file(path)
|
|
end
|
|
return table.concat(res, '\n')
|
|
end
|
|
|
|
--- @return string
|
|
local function gen_highlights_doc()
|
|
local res = {} --- @type string[]
|
|
local highlights = require('lua.gitsigns.highlight')
|
|
|
|
local name_max = 0
|
|
for _, hl in ipairs(highlights.hls) do
|
|
for name, _ in pairs(hl) do
|
|
if name:len() > name_max then
|
|
name_max = name:len()
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, hl in ipairs(highlights.hls) do
|
|
for name, spec in pairs(hl) do
|
|
if not spec.hidden then
|
|
local fallbacks_tbl = {} --- @type string[]
|
|
for _, f in ipairs(spec) do
|
|
fallbacks_tbl[#fallbacks_tbl + 1] = string.format('`%s`', f)
|
|
end
|
|
local fallbacks = table.concat(fallbacks_tbl, ', ')
|
|
res[#res + 1] = string.format('%s*hl-%s*', string.rep(' ', 56), name)
|
|
res[#res + 1] = string.format('%s', name)
|
|
if spec.desc then
|
|
res[#res + 1] = string.format('%s%s', string.rep(' ', 8), spec.desc)
|
|
res[#res + 1] = ''
|
|
end
|
|
res[#res + 1] = string.format('%sFallbacks: %s', string.rep(' ', 8), fallbacks)
|
|
end
|
|
end
|
|
end
|
|
|
|
return table.concat(res, '\n')
|
|
end
|
|
|
|
--- @return string
|
|
local function get_setup_from_readme()
|
|
local readme = io.lines('README.md') --- @type Iterator[string]
|
|
local res = {} --- @type string[]
|
|
|
|
local function append(line)
|
|
res[#res + 1] = line ~= '' and ' ' .. line or ''
|
|
end
|
|
|
|
for l in readme do
|
|
if l:match("require%('gitsigns'%).setup {") then
|
|
append(l)
|
|
break
|
|
end
|
|
end
|
|
|
|
for l in readme do
|
|
append(l)
|
|
if l == '}' then
|
|
break
|
|
end
|
|
end
|
|
|
|
return table.concat(res, '\n')
|
|
end
|
|
|
|
--- @param marker string
|
|
--- @return string|fun():string
|
|
local function get_marker_text(marker)
|
|
return ({
|
|
VERSION = 'v0.9.0', -- x-release-please-version
|
|
CONFIG = gen_config_doc,
|
|
FUNCTIONS = function()
|
|
return gen_functions_doc({
|
|
'lua/gitsigns.lua',
|
|
'lua/gitsigns/attach.lua',
|
|
'lua/gitsigns/actions.lua',
|
|
})
|
|
end,
|
|
HIGHLIGHTS = gen_highlights_doc,
|
|
SETUP = get_setup_from_readme,
|
|
})[marker]
|
|
end
|
|
|
|
local function main()
|
|
local template = io.lines('etc/doc_template.txt') --- @type Iterator[string]
|
|
|
|
local out = assert(io.open('doc/gitsigns.txt', 'w'))
|
|
|
|
for l in template do
|
|
local marker = l:match('{{(.*)}}')
|
|
if marker then
|
|
local sub = get_marker_text(marker)
|
|
if sub then
|
|
if type(sub) == 'function' then
|
|
sub = sub()
|
|
end
|
|
--- @type string
|
|
sub = sub:gsub('%%', '%%%%')
|
|
l = l:gsub('{{' .. marker .. '}}', sub)
|
|
end
|
|
end
|
|
out:write(l or '', '\n')
|
|
end
|
|
end
|
|
|
|
main()
|