runtime(man): improve :Man completion for man-db

Commit: 
https://github.com/vim/vim/commit/c2623824a7f38ef9cefc9d9ca016b897000a8db4
Author: David Mandelberg <da...@mandelberg.org>
Date:   Mon Mar 10 21:26:50 2025 +0100

    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 <da...@mandelberg.org>
    Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/autoload/dist/man.vim b/runtime/autoload/dist/man.vim
index 281b75158..32bf80c76 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 @@ endtry
 
 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 39402ee24..8a7f0c538 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 @@ Local mappings:
        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 @@ Commands:
 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 @@ desired folding style instead.  For example: >
 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 9c6c9e1e6..0f4804b51 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -2142,6 +2142,7 @@ $quote    eval.txt        /*$quote*
 :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_csh    syntax.txt      /*g:filetype_csh*
 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 45c2bb239..3edbb27e7 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 @@ endif
 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 @@ if &filetype == "man"
 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
 

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to vim_dev+unsubscr...@googlegroups.com.
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1trjlF-002gzL-Pa%40256bit.org.

Raspunde prin e-mail lui