feat(help): generate :helptags using Treesitter

Problem:
Tags are manually parsed in C which is not flexible and prone to errors.
Extending the help system to allow for other formats (e.g. Markdown)
would require a large rewrite in the C core, while with Treesitter it
only needs a query update.

Solution:
Use the power of treesitter to extract the tags from helpfiles.

- build: set `$VIMRUNTIME` when generating helptags, like
  `cmake/Util.cmake` already does for other generators.
- fix(help): only accept tags delimited by whitespace. The old C parser
  only accepted a `*tag*` preceded by start-of-line or whitespace and
  followed by whitespace or end-of-line. The vimdoc parser also captures
  tags followed by other text, e.g. `*$XDG_STATE_HOME*/.../logs` in
  starting.txt, which caused an E154 duplicate tag error for docs that
  were previously fine.
This commit is contained in:
Yochem van Rosmalen
2026-01-14 12:03:21 +01:00
committed by Justin M. Keyes
parent a1de07418b
commit b36b3d7f3a
5 changed files with 243 additions and 304 deletions

View File

@@ -1,11 +1,12 @@
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local describe, it, before_each, finally = t.describe, t.it, t.before_each, t.finally
local describe, it, before_each, after_each, finally =
t.describe, t.it, t.before_each, t.after_each, t.finally
local clear = n.clear
local eval = n.eval
local command = n.command
local eq = t.eq
local pcall_err = t.pcall_err
local fn = n.fn
local api = n.api
local mkdir = t.mkdir
@@ -324,3 +325,83 @@ describe(':help', function()
eq('*…*', api.nvim_get_current_line())
end)
end)
describe(':helptags', function()
before_each(function()
for _, sfx in ipairs({ '', '2' }) do
fn.mkdir(('Xhelptags%s/doc'):format(sfx), 'p')
for _, tag in ipairs({ 'Xa', 'Xb' }) do
write_file(('Xhelptags%s/doc/%s%s.txt'):format(sfx, tag, sfx), ('*%s%s*'):format(tag, sfx))
end
end
clear()
command('set rtp+=Xhelptags,Xhelptags2')
end)
after_each(function()
rmdir('Xhelptags')
rmdir('Xhelptags2')
end)
it('requires an argument', function()
local msg = t.pcall_err(command, 'helptags')
eq(true, msg:find('E471') ~= nil)
end)
it('{dir}', function()
command('helptags Xhelptags/doc')
eq(eval("['Xa Xa.txt /*Xa*','Xb Xb.txt /*Xb*']"), eval("readfile('Xhelptags/doc/tags')"))
command('help Xa')
eq('*Xa*', api.nvim_get_current_line())
end)
it('ALL', function()
command('helptags ALL')
eq(eval("['Xa Xa.txt /*Xa*','Xb Xb.txt /*Xb*']"), eval("readfile('Xhelptags/doc/tags')"))
eq(eval("['Xa2 Xa2.txt /*Xa2*','Xb2 Xb2.txt /*Xb2*']"), eval("readfile('Xhelptags2/doc/tags')"))
command('help Xa2')
eq('*Xa2*', api.nvim_get_current_line())
end)
it('++t', function()
command('helptags ++t Xhelptags/doc')
eq('help-tags tags 1', eval("readfile('Xhelptags/doc/tags')[-1]"))
end)
it('generates help-tag tag for VIMRUNTIME', function()
command('let $VIMRUNTIME="Xhelptags"')
command('helptags Xhelptags/doc')
eq('help-tags tags 1', eval("readfile('Xhelptags/doc/tags')[-1]"))
end)
it('errors on duplicate tags', function()
-- duplicate tags in different files
write_file('Xhelptags/doc/Xd.txt', '*Xa*', nil, true)
local msg = t.pcall_err(command, 'helptags Xhelptags/doc')
eq(true, msg:find('E154') ~= nil)
-- tags file should still be generated
eq(1, eval("filereadable('Xhelptags/doc/tags')"))
os.remove('Xhelptags/doc/Xd.txt')
-- duplicate tags in same file
write_file('Xhelptags/doc/Xa.txt', '\n*Xa*', nil, true)
msg = t.pcall_err(command, 'helptags Xhelptags/doc')
eq(true, msg:find('E154') ~= nil)
eq(1, eval("filereadable('Xhelptags/doc/tags')"))
end)
it('with translated help files', function()
write_file('Xhelptags/doc/Xa.nlx', '*Xa*', nil, true)
command('helptags Xhelptags/doc')
eq(1, eval("filereadable('Xhelptags/doc/tags-nl')"))
end)
end)