refactor(help): move local-additions to Lua #37831

Problem:
- ~200 line function of hard-to-maintain C code.
- Local Addition section looks messy because of the varying description
  formats.

Solution:
- Move code to Lua.
- Have a best-effort approach where short descriptions are right
  aligned, giving a cleaner look. Long descriptions are untouched.
This commit is contained in:
Yochem van Rosmalen
2026-02-14 11:30:18 +01:00
committed by GitHub
parent 29c81ba27e
commit b5ce7e74dc
4 changed files with 84 additions and 167 deletions

View File

@@ -143,7 +143,7 @@ Standard plugins ~
See |standard-plugin-list|.
Local additions ~
*local-additions*
*local-additions*
------------------------------------------------------------------------------
Bars example *bars*

View File

@@ -122,10 +122,77 @@ function M.escape_subject(word)
-- E.g. '`command`,' --> 'command' (backticks are removed too, but '``' stays '``')
word = word:gsub([[^'([^']*)'.*]], [['%1']])
word = word:gsub([[^{([^}]*)}.*]], '{%1}')
word = word:gsub([[^`([^`]+)`.*]], '%1')
word = word:gsub([[.*`([^`]+)`.*]], '%1')
end
return word
end
---Populates the |local-additions| section of a help buffer with references to locally-installed
---help files. These are help files outside of $VIMRUNTIME (typically from plugins) whose first
---line contains a tag (e.g. *plugin-name.txt*) and a short description.
---
---For each help file found in 'runtimepath', the first line is extracted and added to the buffer
---as a reference (converting '*tag*' to '|tag|'). If a translated version of a help file exists
---in the same language as the current buffer (e.g. 'plugin.nlx' alongside 'plugin.txt'), the
---translated version is preferred over the '.txt' file.
function M.local_additions()
local buf = vim.api.nvim_get_current_buf()
local bufname = vim.fs.basename(vim.api.nvim_buf_get_name(buf))
-- "help.txt" or "help.??x" where ?? is a language code, see |help-translated|.
local lang = bufname:match('^help%.(%a%a)x$')
if bufname ~= 'help.txt' and not lang then
return
end
-- Find local help files
---@type table<string, string>
local plugins = {}
local pattern = lang and ('doc/*.{txt,%sx}'):format(lang) or 'doc/*.txt'
for _, docpath in ipairs(vim.api.nvim_get_runtime_file(pattern, true)) do
if not vim.fs.relpath(vim.env.VIMRUNTIME, docpath) then
-- '/path/to/doc/plugin.txt' --> 'plugin'
local plugname = vim.fs.basename(docpath):sub(1, -5)
-- prefer language-specific files over .txt
if not plugins[plugname] or vim.endswith(plugins[plugname], '.txt') then
plugins[plugname] = docpath
end
end
end
-- Format plugin list lines
-- Default to 78 if 'textwidth' is not set (e.g. in sandbox)
local textwidth = math.max(vim.bo[buf].textwidth, 78)
local lines = {}
for _, path in vim.spairs(plugins) do
local fp = io.open(path, 'r')
if fp then
local tagline = fp:read('*l') or ''
fp:close()
---@type string, string
local plugname, desc = tagline:match('^%*([^*]+)%*%s*(.*)$')
if plugname and desc then
-- left-align taglink and right-align description by inserting spaces in between
local plug_width = vim.fn.strdisplaywidth(plugname)
local desc_width = vim.fn.strdisplaywidth(desc)
-- max(l, 1) forces at least one space for if the description is too long
local spaces = string.rep(' ', math.max(textwidth - desc_width - plug_width - 2, 1))
local fmt = string.format('|%s|%s%s', plugname, spaces, desc)
table.insert(lines, fmt)
end
end
end
-- Add plugin list to local-additions section
for linenr, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
if line:find('*local-additions*', 1, true) then
vim._with({ buf = buf, bo = { modifiable = true, readonly = false } }, function()
vim.api.nvim_buf_set_lines(buf, linenr, linenr, true, lines)
end)
break
end
end
end
return M

View File

@@ -444,167 +444,17 @@ void prepare_help_buffer(void)
set_buflisted(false);
}
/// After reading a help file: if help.txt, populate *local-additions*
/// Populate *local-additions* in help.txt
void get_local_additions(void)
{
// In the "help.txt" and "help.abx" file, add the locally added help
// files. This uses the very first line in the help file.
char *const fname = path_tail(curbuf->b_fname);
if (path_fnamecmp(fname, "help.txt") == 0
|| (path_fnamencmp(fname, "help.", 5) == 0
&& ASCII_ISALPHA(fname[5])
&& ASCII_ISALPHA(fname[6])
&& TOLOWER_ASC(fname[7]) == 'x'
&& fname[8] == NUL)) {
for (linenr_T lnum = 1; lnum < curbuf->b_ml.ml_line_count; lnum++) {
char *line = ml_get_buf(curbuf, lnum);
if (strstr(line, "*local-additions*") == NULL) {
continue;
}
int lnum_start = lnum;
// Go through all directories in 'runtimepath', skipping
// $VIMRUNTIME.
char *p = p_rtp;
while (*p != NUL) {
copy_option_part(&p, NameBuff, MAXPATHL, ",");
char *const rt = vim_getenv("VIMRUNTIME");
if (rt != NULL
&& path_full_compare(rt, NameBuff, false, true) != kEqualFiles) {
int fcount;
char **fnames;
vimconv_T vc;
// Find all "doc/ *.txt" files in this directory.
if (!add_pathsep(NameBuff)
|| xstrlcat(NameBuff, "doc/*.??[tx]", // NOLINT
sizeof(NameBuff)) >= MAXPATHL) {
emsg(_(e_fnametoolong));
continue;
}
// Note: We cannot just do `&NameBuff` because it is a statically sized array
// so `NameBuff == &NameBuff` according to C semantics.
char *buff_list[1] = { NameBuff };
if (gen_expand_wildcards(1, buff_list, &fcount,
&fnames, EW_FILE|EW_SILENT) == OK
&& fcount > 0) {
char *s;
char *cp;
// If foo.abx is found use it instead of foo.txt in
// the same directory.
for (int i1 = 0; i1 < fcount; i1++) {
const char *const f1 = fnames[i1];
const char *const t1 = path_tail(f1);
const char *const e1 = strrchr(t1, '.');
if (e1 == NULL) {
continue;
}
if (path_fnamecmp(e1, ".txt") != 0
&& path_fnamecmp(e1, fname + 4) != 0) {
// Not .txt and not .abx, remove it.
XFREE_CLEAR(fnames[i1]);
continue;
}
for (int i2 = i1 + 1; i2 < fcount; i2++) {
const char *const f2 = fnames[i2];
if (f2 == NULL) {
continue;
}
const char *const t2 = path_tail(f2);
const char *const e2 = strrchr(t2, '.');
if (e2 == NULL) {
continue;
}
if (e1 - f1 != e2 - f2
|| path_fnamencmp(f1, f2, (size_t)(e1 - f1)) != 0) {
continue;
}
if (path_fnamecmp(e1, ".txt") == 0
&& path_fnamecmp(e2, fname + 4) == 0) {
// use .abx instead of .txt
XFREE_CLEAR(fnames[i1]);
}
}
}
for (int fi = 0; fi < fcount; fi++) {
if (fnames[fi] == NULL) {
continue;
}
FILE *const fd = os_fopen(fnames[fi], "r");
if (fd == NULL) {
continue;
}
vim_fgets(IObuff, IOSIZE, fd);
if (IObuff[0] == '*'
&& (s = vim_strchr(IObuff + 1, '*'))
!= NULL) {
TriState this_utf = kNone;
// Change tag definition to a
// reference and remove <CR>/<NL>.
IObuff[0] = '|';
*s = '|';
while (*s != NUL) {
if (*s == '\r' || *s == '\n') {
*s = NUL;
}
// The text is utf-8 when a byte
// above 127 is found and no
// illegal byte sequence is found.
if ((uint8_t)(*s) >= 0x80 && this_utf != kFalse) {
this_utf = kTrue;
const int l = utf_ptr2len(s);
if (l == 1) {
this_utf = kFalse;
}
s += l - 1;
}
s++;
}
// The help file is latin1 or utf-8;
// conversion to the current
// 'encoding' may be required.
vc.vc_type = CONV_NONE;
convert_setup(&vc,
(this_utf == kTrue ? "utf-8" : "latin1"),
p_enc);
if (vc.vc_type == CONV_NONE) {
// No conversion needed.
cp = IObuff;
} else {
// Do the conversion. If it fails
// use the unconverted text.
cp = string_convert(&vc, IObuff, NULL);
if (cp == NULL) {
cp = IObuff;
}
}
convert_setup(&vc, NULL, NULL);
ml_append(lnum, cp, 0, false);
if (cp != IObuff) {
xfree(cp);
}
lnum++;
}
fclose(fd);
}
FreeWild(fcount, fnames);
}
}
xfree(rt);
}
linenr_T appended = lnum - lnum_start;
if (appended) {
mark_adjust(lnum_start + 1, (linenr_T)MAXLNUM, appended, 0, kExtmarkUndo);
changed_lines_redraw_buf(curbuf, lnum_start + 1, lnum_start + 1, appended);
}
break;
}
Error err = ERROR_INIT;
Object res = NLUA_EXEC_STATIC("return require'vim._core.help'.local_additions()",
(Array)ARRAY_DICT_INIT, kRetNilBool, NULL, &err);
if (ERROR_SET(&err)) {
emsg_multiline(err.msg, "lua_error", HLF_E, true);
}
api_free_object(res);
api_clear_error(&err);
}
/// ":exusage"

View File

@@ -107,8 +107,8 @@ func Test_help_local_additions()
help local-additions
let lines = getline(line(".") + 1, search("^$") - 1)
call assert_equal([
\ '|mydoc-ext.txt| my extended awesome doc',
\ '|mydoc.txt| my awesome doc'
\ '|mydoc.txt| my awesome doc',
\ '|mydoc-ext.txt| my extended awesome doc'
\ ], lines)
call delete('Xruntime/doc/mydoc-ext.txt')
close
@@ -124,17 +124,17 @@ func Test_help_local_additions()
help local-additions@en
let lines = getline(line(".") + 1, search("^$") - 1)
call assert_equal([
\ '|mydoc.txt| my awesome doc'
\ '|mydoc.txt| my awesome doc'
\ ], lines)
close
help local-additions@ja
let lines = getline(line(".") + 1, search("^$") - 1)
call assert_equal([
\ '|mydoc.txt| my awesome doc',
\ '|help.txt| This is jax file',
\ '|work.txt| This is jax file',
\ '|work2.txt| This is jax file',
\ '|help.txt| This is jax file',
\ '|mydoc.txt| my awesome doc',
\ '|work.txt| This is jax file',
\ '|work2.txt| This is jax file',
\ ], lines)
close