runtime(man): improve :Man completion for man-db
On man-db systems, complete with actual man sections and pages, instead
of shell commands.
I tried to come up with a portable solution for multiple man
implementations in https://github.com/vim/vim/discussions/16794 but I
think the differences between implementations were too large to do that
without overly complicated code. So instead, I implemented it for man-db
(which I think is common on Linux) and hopefully left it easier for
other people to implement it on other systems in the future if they want
to.
closes: #16843
Signed-off-by: David Mandelberg <david@mandelberg.org>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/autoload/dist/man.vim b/runtime/autoload/dist/man.vim
index 281b751..32bf80c 100644
--- a/runtime/autoload/dist/man.vim
+++ b/runtime/autoload/dist/man.vim
@@ -6,6 +6,7 @@
" Last Change: 2024 Jan 17 (make it work on AIX, see #13847)
" 2024 Jul 06 (honor command modifiers, #15117)
" 2025 Mar 05 (add :keepjumps, #16791)
+" 2025 Mar 09 (improve :Man completion for man-db, #16843)
let s:cpo_save = &cpo
set cpo-=C
@@ -35,6 +36,88 @@
unlet! uname_s
+let s:man_db_pages_by_section = v:null
+func! s:ManDbPagesBySection() abort
+ if s:man_db_pages_by_section isnot v:null
+ return s:man_db_pages_by_section
+ endif
+ let s:man_db_pages_by_section = {}
+ let list_command = 'apropos --long .'
+ let unparsed_lines = []
+ for line in systemlist(list_command)
+ " Typical lines:
+ " vim (1) - Vi IMproved, a programmer's text editor
+ "
+ " Unusual lines:
+ " pgm_read_ T _ (3avr) - (unknown subject)
+ "
+ " Code that shows the line's format:
+ " https://gitlab.com/man-db/man-db/-/blob/2607d203472efb036d888e9e7997724a41a53876/src/whatis.c#L409
+ let match = matchlist(line, '^\(.\{-1,}\) (\(\S\+\)) ')
+ if empty(match)
+ call add(unparsed_lines, line)
+ continue
+ endif
+ let [page, section] = match[1:2]
+ if !has_key(s:man_db_pages_by_section, section)
+ let s:man_db_pages_by_section[section] = []
+ endif
+ call add(s:man_db_pages_by_section[section], page)
+ endfor
+ if !empty(unparsed_lines)
+ echomsg 'Unable to parse ' .. string(len(unparsed_lines)) .. ' lines ' ..
+ \ 'from the output of `' .. list_command .. '`. Example lines:'
+ for line in unparsed_lines[:9]
+ echomsg line
+ endfor
+ endif
+ return s:man_db_pages_by_section
+endfunc
+
+func! dist#man#Reload() abort
+ if g:ft_man_implementation ==# 'man-db'
+ let s:man_db_pages_by_section = v:null
+ call s:ManDbPagesBySection()
+ endif
+endfunc
+
+func! s:StartsWithCaseInsensitive(haystack, needle) abort
+ if empty(a:needle)
+ return v:true
+ endif
+ return a:haystack[:len(a:needle)-1] ==? a:needle
+endfunc
+
+func! dist#man#ManDbComplete(arg_lead, cmd_line, cursor_pos) abort
+ let args = split(trim(a:cmd_line[: a:cursor_pos - 1], '', 1), '', v:true)
+ let pages_by_section = s:ManDbPagesBySection()
+ if len(args) > 2
+ " Page in the section args[1]. At least on Debian testing as of
+ " 2025-03-06, man seems to match sections case-insensitively and match any
+ " prefix of the section. E.g., `man 3 sigprocmask` and `man 3PoSi
+ " sigprocmask` with both load sigprocmask(3posix) even though the 3 in the
+ " first command is also the name of a different section.
+ let results = []
+ for [section, pages] in items(pages_by_section)
+ if s:StartsWithCaseInsensitive(section, args[1])
+ call extend(results, pages)
+ endif
+ endfor
+ else
+ " Could be a section, or a page in any section. Add space after sections
+ " since there has to be a second argument in that case.
+ let results = flattennew(values(pages_by_section), 1)
+ for section in keys(pages_by_section)
+ call add(results, section .. ' ')
+ endfor
+ endif
+ call sort(results)
+ call uniq(results)
+ call filter(results,
+ \ {_, val -> s:StartsWithCaseInsensitive(val, a:arg_lead)})
+ return results
+endfunc
+
func s:ParseIntoPageAndSection()
" Accommodate a reference that terminates in a hyphen.
"