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