From c2623824a7f38ef9cefc9d9ca016b897000a8db4 Mon Sep 17 00:00:00 2001 From: David Mandelberg Date: Mon, 10 Mar 2025 21:26:50 +0100 Subject: [PATCH] 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 Signed-off-by: Christian Brabandt --- runtime/autoload/dist/man.vim | 83 +++++++++++++++++++++++++++++++++++ runtime/doc/filetype.txt | 14 +++++- runtime/doc/tags | 2 + runtime/ftplugin/man.vim | 19 +++++++- 4 files changed, 115 insertions(+), 3 deletions(-) diff --git a/runtime/autoload/dist/man.vim b/runtime/autoload/dist/man.vim index 281b751583..32bf80c765 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 39402ee24a..8a7f0c538f 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: 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 9c6c9e1e69..0f4804b515 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 45c2bb239a..3edbb27e78 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 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(, ) + if g:ft_man_implementation ==# 'man-db' + com -nargs=+ -complete=customlist,dist#man#ManDbComplete Man call dist#man#GetPage(, ) + else + com -nargs=+ -complete=shellcmd Man call dist#man#GetPage(, ) + endif nnoremap K :call dist#man#PreGetPage(0) nnoremap ManPreGetPage :call dist#man#PreGetPage(0) endif +if exists(":ManReload") != 2 + com ManReload call dist#man#Reload() +endif + let &cpo = s:cpo_save unlet s:cpo_save