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.
   "