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.
"
diff --git a/runtime/doc/filetype.txt b/runtime/doc/filetype.txt
index 39402ee..8a7f0c5 100644
--- a/runtime/doc/filetype.txt
+++ b/runtime/doc/filetype.txt
@@ -1,4 +1,4 @@
-*filetype.txt* For Vim version 9.1. Last change: 2025 Mar 09
+*filetype.txt* For Vim version 9.1. Last change: 2025 Mar 10
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -776,7 +776,7 @@
to the end of the file in Normal mode. This means "> " is inserted in
each line.
-MAN *ft-man-plugin* *:Man* *man.vim*
+MAN *ft-man-plugin* *:Man* *:ManReload* *man.vim*
This plugin displays a manual page in a nice way. See |find-manpage| in the
user manual for more information.
@@ -793,6 +793,8 @@
Man {name} Display the manual page for {name} in a window.
Man {number} {name}
Display the manual page for {name} in a section {number}.
+ManReload Reload the cache of available man pages used for |:Man| argument
+ completion.
Global mapping:
<Leader>K Displays the manual page for the word under the cursor.
@@ -823,6 +825,14 @@
If you would like :Man {number} {name} to behave like man {number} {name} by
not running man {name} if no page is found, then use this: >
let g:ft_man_no_sect_fallback = 1
+<
+ *g:ft_man_implementation*
+The completion for the :Man command tries to guess which implementation of man
+the system has. If it guesses wrong, you can set g:ft_man_implementation to
+one of these values:
+ 'man-db' https://man-db.nongnu.org/
+ '' Unknown, fall back to completing shell commands
+ instead of man pages.
You may also want to set 'keywordprg' to make the |K| command open a manual
page in a Vim window: >
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 9c6c9e1..0f4804b 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -2142,6 +2142,7 @@
:Lfilter quickfix.txt /*:Lfilter*
:LogiPat pi_logipat.txt /*:LogiPat*
:Man filetype.txt /*:Man*
+:ManReload filetype.txt /*:ManReload*
:MkVimball pi_vimball.txt /*:MkVimball*
:N editing.txt /*:N*
:Nexplore pi_netrw.txt /*:Nexplore*
@@ -7581,6 +7582,7 @@
g:filetype_haredoc ft_hare.txt /*g:filetype_haredoc*
g:filetype_md syntax.txt /*g:filetype_md*
g:filetype_r syntax.txt /*g:filetype_r*
+g:ft_man_implementation filetype.txt /*g:ft_man_implementation*
g:ftplugin_rust_source_path ft_rust.txt /*g:ftplugin_rust_source_path*
g:gnat ft_ada.txt /*g:gnat*
g:gnat.Error_Format ft_ada.txt /*g:gnat.Error_Format*
diff --git a/runtime/ftplugin/man.vim b/runtime/ftplugin/man.vim
index 45c2bb2..3edbb27 100644
--- a/runtime/ftplugin/man.vim
+++ b/runtime/ftplugin/man.vim
@@ -6,6 +6,7 @@
" Last Change: 2024 Jun 06 (disabled the q mapping, #8210)
" 2024 Jul 06 (use nnoremap, #15130)
" 2024 Aug 23 (improve the <Plug>ManBS mapping, #15547, #15556)
+" 2025 Mar 09 (improve :Man completion for man-db, #16843)
" To make the ":Man" command available before editing a manual page, source
" this script from your startup vimrc file.
@@ -24,6 +25,14 @@
let s:cpo_save = &cpo
set cpo-=C
+if !exists('g:ft_man_implementation')
+ if executable('mandb') > 0
+ let g:ft_man_implementation = 'man-db'
+ else
+ let g:ft_man_implementation = ''
+ endif
+endif
+
if &filetype == "man"
" Allow hyphen, plus, colon, dot, and commercial at in manual page name.
" Parentheses are not here but in dist#man#PreGetPage()
@@ -60,11 +69,19 @@
endif
if exists(":Man") != 2
- com -nargs=+ -complete=shellcmd Man call dist#man#GetPage(<q-mods>, <f-args>)
+ if g:ft_man_implementation ==# 'man-db'
+ com -nargs=+ -complete=customlist,dist#man#ManDbComplete Man call dist#man#GetPage(<q-mods>, <f-args>)
+ else
+ com -nargs=+ -complete=shellcmd Man call dist#man#GetPage(<q-mods>, <f-args>)
+ endif
nnoremap <Leader>K :call dist#man#PreGetPage(0)<CR>
nnoremap <Plug>ManPreGetPage :call dist#man#PreGetPage(0)<CR>
endif
+if exists(":ManReload") != 2
+ com ManReload call dist#man#Reload()
+endif
+
let &cpo = s:cpo_save
unlet s:cpo_save