Files
neovim/test/functional/plugin/man_spec.lua
MP430 b3324be0d8 fix(man.lua): :Man ignores section of gzipped manpage #38235
Problem:
Under certain circumstances (e.g. gzipped manpages with mandoc),
:Man will not find the correct page because it does not process
multiple extensions correctly.
For example, with a file named strcpy.3p.gz, it will only check the .gz
part to try to check the section.
This leads to some pages being inaccessible because it will return the
page from the wrong section.

Solution:
Loop and try multiple extensions to try to find one which matches
the name of the section.
Also refactor the man.get_path function so that it can be tested.
2026-03-10 17:26:40 -04:00

349 lines
11 KiB
Lua

local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local command, feed = n.command, n.feed
local dedent = t.dedent
local clear = n.clear
local exec_lua = n.exec_lua
local fn = n.fn
local nvim_prog = n.nvim_prog
local matches = t.matches
local tmpname = t.tmpname
local eq = t.eq
local pesc = vim.pesc
local skip = t.skip
local is_ci = t.is_ci
-- Collects all names passed to find_path() after attempting ":Man foo".
local function get_search_history(name)
return exec_lua(function()
local args = vim.split(name, ' ')
local man = require('man')
local res = {}
--- @diagnostic disable-next-line:duplicate-set-field
man._find_path = function(name0, sect)
table.insert(res, { sect, name0 })
return nil
end
local err = man.open_page(-1, { tab = 0 }, args)
assert(err and err:match('no manual entry'))
return res
end)
end
clear()
if fn.executable('man') == 0 then
pending('missing "man" command', function() end)
return
end
describe(':Man', function()
before_each(function()
clear()
end)
describe('man.lua: highlight_line()', function()
local screen --- @type test.functional.ui.screen
before_each(function()
command('syntax on')
command('set filetype=man')
command('syntax off') -- Ignore syntax groups
screen = Screen.new(52, 5)
screen:set_default_attr_ids({
b = { bold = true },
i = { italic = true },
u = { underline = true },
bi = { bold = true, italic = true },
biu = { bold = true, italic = true, underline = true },
c = { foreground = Screen.colors.Blue }, -- control chars
eob = { bold = true, foreground = Screen.colors.Blue }, -- empty line '~'s
})
end)
it('clears backspaces from text and adds highlights', function()
feed(
dedent(
[[
ithis i<C-v><C-h>is<C-v><C-h>s a<C-v><C-h>a test
with _<C-v><C-h>o_<C-v><C-h>v_<C-v><C-h>e_<C-v><C-h>r_<C-v><C-h>s_<C-v><C-h>t_<C-v><C-h>r_<C-v><C-h>u_<C-v><C-h>c_<C-v><C-h>k text<ESC>]]
)
)
screen:expect {
grid = [[
this i{c:^H}is{c:^H}s a{c:^H}a test |
with _{c:^H}o_{c:^H}v_{c:^H}e_{c:^H}r_{c:^H}s_{c:^H}t_{c:^H}r_{c:^H}u_{c:^H}c_{c:^H}k tex^t |
{eob:~ }|*2
|
]],
}
exec_lua [[require'man'.init_pager()]]
screen:expect([[
^this {b:is} {b:a} test |
with {i:overstruck} text |
{eob:~ }|*2
|
]])
end)
it('clears escape sequences from text and adds highlights', function()
feed(
dedent(
[[
ithis <C-v><ESC>[1mis <C-v><ESC>[3ma <C-v><ESC>[4mtest<C-v><ESC>[0m
<C-v><ESC>[4mwith<C-v><ESC>[24m <C-v><ESC>[4mescaped<C-v><ESC>[24m <C-v><ESC>[4mtext<C-v><ESC>[24m<ESC>]]
)
)
screen:expect {
grid = [=[
this {c:^[}[1mis {c:^[}[3ma {c:^[}[4mtest{c:^[}[0m |
{c:^[}[4mwith{c:^[}[24m {c:^[}[4mescaped{c:^[}[24m {c:^[}[4mtext{c:^[}[24^m |
{eob:~ }|*2
|
]=],
}
exec_lua [[require'man'.init_pager()]]
screen:expect([[
^this {b:is }{bi:a }{biu:test} |
{u:with} {u:escaped} {u:text} |
{eob:~ }|*2
|
]])
end)
it('clears OSC 8 hyperlink markup from text', function()
feed(
dedent(
[[
ithis <C-v><ESC>]8;;http://example.com<C-v><ESC>\Link Title<C-v><ESC>]8;;<C-v><ESC>\<ESC>]]
)
)
screen:expect {
grid = [=[
this {c:^[}]8;;http://example.com{c:^[}\Link Title{c:^[}]8;;{c:^[}^\ |
{eob:~ }|*3
|
]=],
}
exec_lua [[require'man'.init_pager()]]
screen:expect([[
^this Link Title |
{eob:~ }|*3
|
]])
end)
it('highlights multibyte text', function()
feed(
dedent(
[[
ithis i<C-v><C-h>is<C-v><C-h>s あ<C-v><C-h>あ test
with _<C-v><C-h>ö_<C-v><C-h>v_<C-v><C-h>e_<C-v><C-h>r_<C-v><C-h>s_<C-v><C-h>t_<C-v><C-h>r_<C-v><C-h>u_<C-v><C-h>̃_<C-v><C-h>c_<C-v><C-h>k te<C-v><ESC>[3mxt¶<C-v><ESC>[0m<ESC>]]
)
)
exec_lua [[require'man'.init_pager()]]
screen:expect([[
^this {b:is} {b:あ} test |
with {i:överstrũck} te{i:xt¶} |
{eob:~ }|*2
|
]])
end)
it('highlights underscores based on context', function()
feed(
dedent(
[[
i_<C-v><C-h>_b<C-v><C-h>be<C-v><C-h>eg<C-v><C-h>gi<C-v><C-h>in<C-v><C-h>ns<C-v><C-h>s
m<C-v><C-h>mi<C-v><C-h>id<C-v><C-h>d_<C-v><C-h>_d<C-v><C-h>dl<C-v><C-h>le<C-v><C-h>e
_<C-v><C-h>m_<C-v><C-h>i_<C-v><C-h>d_<C-v><C-h>__<C-v><C-h>d_<C-v><C-h>l_<C-v><C-h>e<ESC>]]
)
)
exec_lua [[require'man'.init_pager()]]
screen:expect([[
{b:^_begins} |
{b:mid_dle} |
{i:mid_dle} |
{eob:~ }|
|
]])
end)
it('highlights various bullet formats', function()
feed(dedent([[
i· ·<C-v><C-h>·
+<C-v><C-h>o
+<C-v><C-h>+<C-v><C-h>o<C-v><C-h>o double<ESC>]]))
exec_lua [[require'man'.init_pager()]]
screen:expect([[
^· {b:·} |
{b:·} |
{b:·} double |
{eob:~ }|
|
]])
end)
it('handles : characters in input', function()
feed(dedent([[
i<C-v><C-[>[40m 0 <C-v><C-[>[41m 1 <C-v><C-[>[42m 2 <C-v><C-[>[43m 3
<C-v><C-[>[44m 4 <C-v><C-[>[45m 5 <C-v><C-[>[46m 6 <C-v><C-[>[47m 7 <C-v><C-[>[100m 8 <C-v><C-[>[101m 9
<C-v><C-[>[102m 10 <C-v><C-[>[103m 11 <C-v><C-[>[104m 12 <C-v><C-[>[105m 13 <C-v><C-[>[106m 14 <C-v><C-[>[107m 15
<C-v><C-[>[48:5:16m 16 <ESC>]]))
exec_lua [[require'man'.init_pager()]]
screen:expect([[
^ 0 1 2 3 |
4 5 6 7 8 9 |
10 11 12 13 14 15 |
16 |
|
]])
end)
end)
it('q quits in "$MANPAGER mode" (:Man!) #18281', function()
-- This will hang if #18281 regresses.
local args = {
nvim_prog,
'--headless',
'+autocmd VimLeave * echo "quit works!!"',
'+Man!',
'+tag ls',
'+call nvim_input("q")',
}
matches('quit works!!', fn.system(args, { 'manpage contents' }))
end)
it('raw manpage into (:Man!) creates a new buffer #30132', function()
local args = {
nvim_prog,
'--headless',
'+Man! foo',
'+echo bufname()',
'+enew',
'+Man! foo',
'+echo bufname()',
'+enew',
'+Man! foo',
'+echo bufname()',
'+q',
}
local out = fn.system(args, { 'manpage contents' })
assert(out and out:match('man://%?new=%d'))
end)
it('reports non-existent man pages for absolute paths', function()
skip(is_ci('cirrus'))
local actual_file = tmpname()
-- actual_file must be an absolute path to an existent file for us to test against it
matches('^/.+', actual_file)
local args = { nvim_prog, '--headless', '+:Man ' .. actual_file, '+q' }
matches(
('Error in command line:\r\n' .. 'man.lua: no manual entry for %s'):format(pesc(actual_file)),
fn.system(args, { '' })
)
os.remove(actual_file)
end)
-- if man -w returns multiple results, :Man should select the correct one.
it('matches correct manpage section and name', function()
-- mock the system function to return the results we want to test.
local function _test(results, name, sect)
return exec_lua(function()
local man = require 'man'
return man._match_manpage_path(results, name, sect)
end)
end
eq(
'/usr/share/man/man3/strcpy.3',
_test({
'/usr/share/man/man3/strcpy.3',
'/usr/share/man/man3/string.3',
}, 'strcpy')
)
eq(
'/usr/share/man/man3/strcpy.3',
_test({
'/usr/share/man/man3/string.3',
'/usr/share/man/man3/strcpy.3',
}, 'strcpy')
)
eq(
'/usr/share/man/man3/strcpy.3',
_test({
'/usr/share/man/man3p/strcpy.3p',
'/usr/share/man/man3/strcpy.3',
'/usr/share/man/man3/string.3',
'/usr/share/man/man7/string_copying.7',
}, 'strcpy', '3')
)
eq(
'/usr/share/man/man3/strcpy.3.gz',
_test({
'/usr/share/man/man3p/strcpy.3p.gz',
'/usr/share/man/man3/strcpy.3.gz',
'/usr/share/man/man3/string.3.gz',
'/usr/share/man/man7/string_copying.7.gz',
}, 'strcpy', '3')
)
end)
it('tries variants with spaces, underscores #22503', function()
eq({
{ vim.NIL, 'NAME WITH SPACES' },
{ vim.NIL, 'NAME_WITH_SPACES' },
}, get_search_history('NAME WITH SPACES'))
eq({
{ '3', 'some other man' },
{ '3', 'some_other_man' },
}, get_search_history('3 some other man'))
eq({
{ '3x', 'some other man' },
{ '3x', 'some_other_man' },
}, get_search_history('3X some other man'))
eq({
{ '3tcl', 'some other man' },
{ '3tcl', 'some_other_man' },
}, get_search_history('3tcl some other man'))
eq({
{ 'n', 'some other man' },
{ 'n', 'some_other_man' },
}, get_search_history('n some other man'))
eq({
{ vim.NIL, '123some other man' },
{ vim.NIL, '123some_other_man' },
}, get_search_history('123some other man'))
eq({
{ '1', 'other_man' },
{ '1', 'other_man' },
}, get_search_history('other_man(1)'))
end)
it('can complete', function()
eq(
true,
exec_lua(function()
return #require('man').man_complete('f', 'Man f') > 0
end)
)
end)
end)