From b3324be0d8a17ed600a2ea840827cc4db042d32a Mon Sep 17 00:00:00 2001 From: MP430 <57238920+MP430@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:26:40 +0000 Subject: [PATCH] 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. --- runtime/lua/man.lua | 60 ++++++++++++++++++++--------- test/functional/plugin/man_spec.lua | 47 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 18 deletions(-) diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 5c4a80f9f0..9efc7e96f9 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -217,6 +217,30 @@ end --- @param name? string --- @param sect? string local function get_path(name, sect) + name = name or '' + sect = sect or '' + -- We can avoid relying on -S or -s here since they are very + -- inconsistently supported. Instead, call -w with a section and a name. + local cmd --- @type string[] + if sect == '' then + cmd = { 'man', '-w', name } + else + cmd = { 'man', '-w', sect, name } + end + + local lines = system(cmd, true) + local results = vim.split(lines, '\n', { trimempty = true }) + + return M._match_manpage_path(results, name, sect) +end + +--- Given an array of paths returned by man -w, +--- find the correct path for the given name and section. +--- @param paths? string[] +--- @param name? string +--- @param sect? string +function M._match_manpage_path(paths, name, sect) + paths = paths or {} name = name or '' sect = sect or '' -- Some man implementations (OpenBSD) return all available paths from the @@ -234,20 +258,7 @@ local function get_path(name, sect) -- clock_getres.2, which is the right page. Searching the results for -- clock_gettime will no longer work. In this case, we should just use the -- first one that was found in the correct section. - -- - -- Finally, we can avoid relying on -S or -s here since they are very - -- inconsistently supported. Instead, call -w with a section and a name. - local cmd --- @type string[] - if sect == '' then - cmd = { 'man', '-w', name } - else - cmd = { 'man', '-w', sect, name } - end - - local lines = system(cmd, true) - local results = vim.split(lines, '\n', { trimempty = true }) - - if #results == 0 then + if #paths == 0 then return end @@ -255,7 +266,7 @@ local function get_path(name, sect) -- stops us from actually determining if a path has a corresponding man file. -- Since `:Man /some/path/to/man/file` isn't supported anyway, we should just -- error out here if we detect this is the case. - if sect == '' and #results == 1 and results[1] == name then + if sect == '' and #paths == 1 and paths[1] == name then return end @@ -264,17 +275,30 @@ local function get_path(name, sect) local namematches = vim.tbl_filter(function(v) local tail = vim.fs.basename(v) return tail:find(name, 1, true) ~= nil - end, results) or {} + end, paths) or {} local sectmatches = {} if #namematches > 0 and sect ~= '' then --- @param v string sectmatches = vim.tbl_filter(function(v) - return fn.fnamemodify(v, ':e') == sect + -- On some systems, there are multiple extensions, e.g. strlen.3.gz + -- We must test all of the extensions to make sure we get the correct match. + -- Limit to 3 tests to avoid the risk of getting stuck. + local root = v + for _ = 1, 3 do + local extension = fn.fnamemodify(root, ':e') + if extension == sect then + return true + elseif extension == '' then + return false + end + root = fn.fnamemodify(root, ':r') + end + return false end, namematches) end - return (sectmatches[1] or namematches[1] or results[1]):gsub('\n+$', '') + return (sectmatches[1] or namematches[1] or paths[1]):gsub('\n+$', '') end --- Attempt to extract the name and sect out of 'name(sect)' diff --git a/test/functional/plugin/man_spec.lua b/test/functional/plugin/man_spec.lua index 2e2c6d4050..6e435c283f 100644 --- a/test/functional/plugin/man_spec.lua +++ b/test/functional/plugin/man_spec.lua @@ -259,6 +259,53 @@ describe(':Man', function() 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' },