feat(Man): port to Lua (#19912)

Co-authored-by: zeertzjq <zeertzjq@outlook.com>
This commit is contained in:
Lewis Russell 2022-09-02 15:20:29 +01:00 committed by GitHub
parent e085d0be31
commit 2afcdbd63a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 651 additions and 566 deletions

View File

@ -28,6 +28,13 @@ read_globals = {
globals = {
"vim.g",
"vim.b",
"vim.w",
"vim.o",
"vim.bo",
"vim.wo",
"vim.go",
"vim.env"
}
exclude_files = {

View File

@ -1,529 +0,0 @@
" Maintainer: Anmol Sethi <hi@nhooyr.io>
if exists('s:loaded_man')
finish
endif
let s:loaded_man = 1
let s:find_arg = '-w'
let s:localfile_arg = v:true " Always use -l if possible. #6683
function! man#init() abort
try
" Check for -l support.
call s:get_page(s:get_path('', 'man'))
catch /command error .*/
let s:localfile_arg = v:false
endtry
endfunction
function! man#open_page(count, mods, ...) abort
if a:0 > 2
call s:error('too many arguments')
return
elseif a:0 == 0
let ref = &filetype ==# 'man' ? expand('<cWORD>') : expand('<cword>')
if empty(ref)
call s:error('no identifier under cursor')
return
endif
elseif a:0 ==# 1
let ref = a:1
else
" Combine the name and sect into a manpage reference so that all
" verification/extraction can be kept in a single function.
" If a:2 is a reference as well, that is fine because it is the only
" reference that will match.
let ref = a:2.'('.a:1.')'
endif
try
let [sect, name] = s:extract_sect_and_name_ref(ref)
if a:count >= 0
let sect = string(a:count)
endif
let path = s:verify_exists(sect, name)
let [sect, name] = s:extract_sect_and_name_path(path)
catch
call s:error(v:exception)
return
endtry
let [l:buf, l:save_tfu] = [bufnr(), &tagfunc]
try
setlocal tagfunc=man#goto_tag
let l:target = l:name . '(' . l:sect . ')'
if a:mods !~# 'tab' && s:find_man()
execute 'silent keepalt tag' l:target
else
execute 'silent keepalt' a:mods 'stag' l:target
endif
call s:set_options(v:false)
finally
call setbufvar(l:buf, '&tagfunc', l:save_tfu)
endtry
let b:man_sect = sect
endfunction
" Called when a man:// buffer is opened.
function! man#read_page(ref) abort
try
let [sect, name] = s:extract_sect_and_name_ref(a:ref)
let path = s:verify_exists(sect, name)
let [sect, name] = s:extract_sect_and_name_path(path)
let page = s:get_page(path)
catch
call s:error(v:exception)
return
endtry
let b:man_sect = sect
call s:put_page(page)
endfunction
" Handler for s:system() function.
function! s:system_handler(jobid, data, event) dict abort
if a:event is# 'stdout' || a:event is# 'stderr'
let self[a:event] .= join(a:data, "\n")
else
let self.exit_code = a:data
endif
endfunction
" Run a system command and timeout after 30 seconds.
function! s:system(cmd, ...) abort
let opts = {
\ 'stdout': '',
\ 'stderr': '',
\ 'exit_code': 0,
\ 'on_stdout': function('s:system_handler'),
\ 'on_stderr': function('s:system_handler'),
\ 'on_exit': function('s:system_handler'),
\ }
let jobid = jobstart(a:cmd, opts)
if jobid < 1
throw printf('command error %d: %s', jobid, join(a:cmd))
endif
let res = jobwait([jobid], 30000)
if res[0] == -1
try
call jobstop(jobid)
throw printf('command timed out: %s', join(a:cmd))
catch /^Vim(call):E900:/
endtry
elseif res[0] == -2
throw printf('command interrupted: %s', join(a:cmd))
endif
if opts.exit_code != 0
throw printf("command error (%d) %s: %s", jobid, join(a:cmd), substitute(opts.stderr, '\_s\+$', '', &gdefault ? '' : 'g'))
endif
return opts.stdout
endfunction
function! s:set_options(pager) abort
setlocal noswapfile buftype=nofile bufhidden=hide
setlocal nomodified readonly nomodifiable
let b:pager = a:pager
setlocal filetype=man
endfunction
function! s:get_page(path) abort
" Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065).
" Soft-wrap: ftplugin/man.vim sets wrap/breakindent/….
" Hard-wrap: driven by `man`.
let manwidth = !get(g:, 'man_hardwrap', 1) ? 999 : (empty($MANWIDTH) ? winwidth(0) : $MANWIDTH)
" Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db).
" http://comments.gmane.org/gmane.editors.vim.devel/29085
" Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces.
let cmd = ['env', 'MANPAGER=cat', 'MANWIDTH='.manwidth, 'MAN_KEEP_FORMATTING=1', 'man']
return s:system(cmd + (s:localfile_arg ? ['-l', a:path] : [a:path]))
endfunction
function! s:put_page(page) abort
setlocal modifiable noreadonly noswapfile
silent keepjumps %delete _
silent put =a:page
while getline(1) =~# '^\s*$'
silent keepjumps 1delete _
endwhile
" XXX: nroff justifies text by filling it with whitespace. That interacts
" badly with our use of $MANWIDTH=999. Hack around this by using a fixed
" size for those whitespace regions.
silent! keeppatterns keepjumps %s/\s\{199,}/\=repeat(' ', 10)/g
1
lua require("man").highlight_man_page()
call s:set_options(v:false)
endfunction
function! man#show_toc() abort
let bufname = bufname('%')
let info = getloclist(0, {'winid': 1})
if !empty(info) && getwinvar(info.winid, 'qf_toc') ==# bufname
lopen
return
endif
let toc = []
let lnum = 2
let last_line = line('$') - 1
while lnum && lnum < last_line
let text = getline(lnum)
if text =~# '^\%( \{3\}\)\=\S.*$'
" if text is a section title
call add(toc, {'bufnr': bufnr('%'), 'lnum': lnum, 'text': text})
elseif text =~# '^\s\+\%(+\|-\)\S\+'
" if text is a flag title. we strip whitespaces and prepend two
" spaces to have a consistent format in the loclist.
let text = ' ' .. substitute(text, '^\s*\(.\{-}\)\s*$', '\1', '')
call add(toc, {'bufnr': bufnr('%'), 'lnum': lnum, 'text': text})
endif
let lnum = nextnonblank(lnum + 1)
endwhile
call setloclist(0, toc, ' ')
call setloclist(0, [], 'a', {'title': 'Man TOC'})
lopen
let w:qf_toc = bufname
endfunction
" attempt to extract the name and sect out of 'name(sect)'
" otherwise just return the largest string of valid characters in ref
function! s:extract_sect_and_name_ref(ref) abort
if a:ref[0] ==# '-' " try ':Man -pandoc' with this disabled.
throw 'manpage name cannot start with ''-'''
endif
let ref = matchstr(a:ref, '[^()]\+([^()]\+)')
if empty(ref)
let name = matchstr(a:ref, '[^()]\+')
if empty(name)
throw 'manpage reference cannot contain only parentheses'
endif
return ['', s:spaces_to_underscores(name)]
endif
let left = split(ref, '(')
" see ':Man 3X curses' on why tolower.
" TODO(nhooyr) Not sure if this is portable across OSs
" but I have not seen a single uppercase section.
return [tolower(split(left[1], ')')[0]), s:spaces_to_underscores(left[0])]
endfunction
" replace spaces in a man page name with underscores
" intended for PostgreSQL, which has man pages like 'CREATE_TABLE(7)';
" while editing SQL source code, it's nice to visually select 'CREATE TABLE'
" and hit 'K', which requires this transformation
function! s:spaces_to_underscores(str)
return substitute(a:str, ' ', '_', 'g')
endfunction
function! s:get_path(sect, name) abort
" Some man implementations (OpenBSD) return all available paths from the
" search command. Previously, this function would simply select the first one.
"
" However, some searches will report matches that are incorrect:
" man -w strlen may return string.3 followed by strlen.3, and therefore
" selecting the first would get us the wrong page. Thus, we must find the
" first matching one.
"
" There's yet another special case here. Consider the following:
" If you run man -w strlen and string.3 comes up first, this is a problem. We
" should search for a matching named one in the results list.
" However, if you search for man -w clock_gettime, you will *only* get
" clock_getres.2, which is the right page. Searching the resuls for
" clock_gettime will no longer work. In this case, we should just use the
" first one that was found in the correct section.
"
" Finally, we can avoid relying on -S or -s here since they are very
" inconsistently supported. Instead, call -w with a section and a name.
if empty(a:sect)
let results = split(s:system(['man', s:find_arg, a:name]))
else
let results = split(s:system(['man', s:find_arg, a:sect, a:name]))
endif
if empty(results)
return ''
endif
" find any that match the specified name
let namematches = filter(copy(results), 'fnamemodify(v:val, ":t") =~ a:name')
let sectmatches = []
if !empty(namematches) && !empty(a:sect)
let sectmatches = filter(copy(namematches), 'fnamemodify(v:val, ":e") == a:sect')
endif
return substitute(get(sectmatches, 0, get(namematches, 0, results[0])), '\n\+$', '', '')
endfunction
" s:verify_exists attempts to find the path to a manpage
" based on the passed section and name.
"
" 1. If the passed section is empty, b:man_default_sects is used.
" 2. If manpage could not be found with the given sect and name,
" then another attempt is made with b:man_default_sects.
" 3. If it still could not be found, then we try again without a section.
" 4. If still not found but $MANSECT is set, then we try again with $MANSECT
" unset.
"
" This function is careful to avoid duplicating a search if a previous
" step has already done it. i.e if we use b:man_default_sects in step 1,
" then we don't do it again in step 2.
function! s:verify_exists(sect, name) abort
let sect = a:sect
if empty(sect)
" no section specified, so search with b:man_default_sects
if exists('b:man_default_sects')
let sects = split(b:man_default_sects, ',')
for sec in sects
try
let res = s:get_path(sec, a:name)
if !empty(res)
return res
endif
catch /^command error (/
endtry
endfor
endif
else
" try with specified section
try
let res = s:get_path(sect, a:name)
if !empty(res)
return res
endif
catch /^command error (/
endtry
" try again with b:man_default_sects
if exists('b:man_default_sects')
let sects = split(b:man_default_sects, ',')
for sec in sects
try
let res = s:get_path(sec, a:name)
if !empty(res)
return res
endif
catch /^command error (/
endtry
endfor
endif
endif
" if none of the above worked, we will try with no section
try
let res = s:get_path('', a:name)
if !empty(res)
return res
endif
catch /^command error (/
endtry
" if that still didn't work, we will check for $MANSECT and try again with it
" unset
if !empty($MANSECT)
try
let MANSECT = $MANSECT
call setenv('MANSECT', v:null)
let res = s:get_path('', a:name)
if !empty(res)
return res
endif
catch /^command error (/
finally
call setenv('MANSECT', MANSECT)
endtry
endif
" finally, if that didn't work, there is no hope
throw 'no manual entry for ' . a:name
endfunction
" Extracts the name/section from the 'path/name.sect', because sometimes the actual section is
" more specific than what we provided to `man` (try `:Man 3 App::CLI`).
" Also on linux, name seems to be case-insensitive. So for `:Man PRIntf`, we
" still want the name of the buffer to be 'printf'.
function! s:extract_sect_and_name_path(path) abort
let tail = fnamemodify(a:path, ':t')
if a:path =~# '\.\%([glx]z\|bz2\|lzma\|Z\)$' " valid extensions
let tail = fnamemodify(tail, ':r')
endif
let sect = matchstr(tail, '\.\zs[^.]\+$')
let name = matchstr(tail, '^.\+\ze\.')
return [sect, name]
endfunction
function! s:find_man() abort
let l:win = 1
while l:win <= winnr('$')
let l:buf = winbufnr(l:win)
if getbufvar(l:buf, '&filetype', '') ==# 'man'
execute l:win.'wincmd w'
return 1
endif
let l:win += 1
endwhile
return 0
endfunction
function! s:error(msg) abort
redraw
echohl ErrorMsg
echon 'man.vim: ' a:msg
echohl None
endfunction
" see s:extract_sect_and_name_ref on why tolower(sect)
function! man#complete(arg_lead, cmd_line, cursor_pos) abort
let args = split(a:cmd_line)
let cmd_offset = index(args, 'Man')
if cmd_offset > 0
" Prune all arguments up to :Man itself. Otherwise modifier commands like
" :tab, :vertical, etc. would lead to a wrong length.
let args = args[cmd_offset:]
endif
let l = len(args)
if l > 3
return
elseif l ==# 1
let name = ''
let sect = ''
elseif a:arg_lead =~# '^[^()]\+([^()]*$'
" cursor (|) is at ':Man printf(|' or ':Man 1 printf(|'
" The later is is allowed because of ':Man pri<TAB>'.
" It will offer 'priclass.d(1m)' even though section is specified as 1.
let tmp = split(a:arg_lead, '(')
let name = tmp[0]
let sect = tolower(get(tmp, 1, ''))
return s:complete(sect, '', name)
elseif args[1] !~# '^[^()]\+$'
" cursor (|) is at ':Man 3() |' or ':Man (3|' or ':Man 3() pri|'
" or ':Man 3() pri |'
return
elseif l ==# 2
if empty(a:arg_lead)
" cursor (|) is at ':Man 1 |'
let name = ''
let sect = tolower(args[1])
else
" cursor (|) is at ':Man pri|'
if a:arg_lead =~# '\/'
" if the name is a path, complete files
" TODO(nhooyr) why does this complete the last one automatically
return glob(a:arg_lead.'*', 0, 1)
endif
let name = a:arg_lead
let sect = ''
endif
elseif a:arg_lead !~# '^[^()]\+$'
" cursor (|) is at ':Man 3 printf |' or ':Man 3 (pr)i|'
return
else
" cursor (|) is at ':Man 3 pri|'
let name = a:arg_lead
let sect = tolower(args[1])
endif
return s:complete(sect, sect, name)
endfunction
function! s:get_paths(sect, name, do_fallback) abort
" callers must try-catch this, as some `man` implementations don't support `s:find_arg`
try
let mandirs = join(split(s:system(['man', s:find_arg]), ':\|\n'), ',')
let paths = globpath(mandirs, 'man?/'.a:name.'*.'.a:sect.'*', 0, 1)
try
" Prioritize the result from verify_exists as it obeys b:man_default_sects.
let first = s:verify_exists(a:sect, a:name)
let paths = filter(paths, 'v:val !=# first')
let paths = [first] + paths
catch
endtry
return paths
catch
if !a:do_fallback
throw v:exception
endif
" Fallback to a single path, with the page we're trying to find.
try
return [s:verify_exists(a:sect, a:name)]
catch
return []
endtry
endtry
endfunction
function! s:complete(sect, psect, name) abort
let pages = s:get_paths(a:sect, a:name, v:false)
" We remove duplicates in case the same manpage in different languages was found.
return uniq(sort(map(pages, 's:format_candidate(v:val, a:psect)'), 'i'))
endfunction
function! s:format_candidate(path, psect) abort
if a:path =~# '\.\%(pdf\|in\)$' " invalid extensions
return
endif
let [sect, name] = s:extract_sect_and_name_path(a:path)
if sect ==# a:psect
return name
elseif sect =~# a:psect.'.\+$'
" We include the section if the user provided section is a prefix
" of the actual section.
return name.'('.sect.')'
endif
endfunction
" Called when Nvim is invoked as $MANPAGER.
function! man#init_pager() abort
if getline(1) =~# '^\s*$'
silent keepjumps 1delete _
else
keepjumps 1
endif
lua require("man").highlight_man_page()
" Guess the ref from the heading (which is usually uppercase, so we cannot
" know the correct casing, cf. `man glDrawArraysInstanced`).
let ref = substitute(matchstr(getline(1), '^[^)]\+)'), ' ', '_', 'g')
try
let b:man_sect = s:extract_sect_and_name_ref(ref)[0]
catch
let b:man_sect = ''
endtry
if -1 == match(bufname('%'), 'man:\/\/') " Avoid duplicate buffers, E95.
execute 'silent file man://'.tolower(fnameescape(ref))
endif
call s:set_options(v:true)
endfunction
function! man#goto_tag(pattern, flags, info) abort
let [l:sect, l:name] = s:extract_sect_and_name_ref(a:pattern)
let l:paths = s:get_paths(l:sect, l:name, v:true)
let l:structured = []
for l:path in l:paths
let [l:sect, l:name] = s:extract_sect_and_name_path(l:path)
let l:structured += [{
\ 'name': l:name,
\ 'title': l:name . '(' . l:sect . ')'
\ }]
endfor
if &cscopetag
" return only a single entry so we work well with :cstag (#11675)
let l:structured = l:structured[:0]
endif
return map(l:structured, {
\ _, entry -> {
\ 'name': entry.name,
\ 'filename': 'man://' . entry.title,
\ 'cmd': '1'
\ }
\ })
endfunction
call man#init()

View File

@ -586,12 +586,12 @@ 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* *man.lua*
View manpages in Nvim. Supports highlighting, completion, locales, and
navigation. Also see |find-manpage|.
man.vim will always attempt to reuse the closest man window (above/left) but
man.lua will always attempt to reuse the closest man window (above/left) but
otherwise create a split.
The case sensitivity of completion is controlled by 'fileignorecase'.

View File

@ -73,7 +73,7 @@ centralized reference of the differences.
- 'wildmenu' is enabled
- 'wildoptions' defaults to "pum,tagfile"
- |man.vim| plugin is enabled, so |:Man| is available by default.
- |man.lua| plugin is enabled, so |:Man| is available by default.
- |matchit| plugin is enabled. To disable it in your config: >
:let loaded_matchit = 1

View File

@ -17,12 +17,12 @@ setlocal iskeyword=@-@,:,a-z,A-Z,48-57,_,.,-,(,)
setlocal nonumber norelativenumber
setlocal foldcolumn=0 colorcolumn=0 nolist nofoldenable
setlocal tagfunc=man#goto_tag
setlocal tagfunc=v:lua.require'man'.goto_tag
if !exists('g:no_plugin_maps') && !exists('g:no_man_maps')
nnoremap <silent> <buffer> j gj
nnoremap <silent> <buffer> k gk
nnoremap <silent> <buffer> gO :call man#show_toc()<CR>
nnoremap <silent> <buffer> gO :lua require'man'.show_toc()<CR>
nnoremap <silent> <buffer> <2-LeftMouse> :Man<CR>
if get(b:, 'pager')
nnoremap <silent> <buffer> <nowait> q :lclose<CR><C-W>q

View File

@ -1,7 +1,75 @@
require('vim.compat')
local api, fn = vim.api, vim.fn
local find_arg = '-w'
local localfile_arg = true -- Always use -l if possible. #6683
local buf_hls = {}
local M = {}
local function man_error(msg)
M.errormsg = 'man.lua: ' .. vim.inspect(msg)
error(M.errormsg)
end
-- Run a system command and timeout after 30 seconds.
local function man_system(cmd, silent)
local stdout_data = {}
local stderr_data = {}
local stdout = vim.loop.new_pipe(false)
local stderr = vim.loop.new_pipe(false)
local done = false
local exit_code
local handle = vim.loop.spawn(cmd[1], {
args = vim.list_slice(cmd, 2),
stdio = { nil, stdout, stderr },
}, function(code)
exit_code = code
stdout:close()
stderr:close()
done = true
end)
if handle then
stdout:read_start(function(_, data)
stdout_data[#stdout_data + 1] = data
end)
stderr:read_start(function(_, data)
stderr_data[#stderr_data + 1] = data
end)
else
stdout:close()
stderr:close()
if not silent then
man_error(string.format('command error: %s', table.concat(cmd)))
end
end
vim.wait(30000, function()
return done
end)
if not done then
if handle then
vim.loop.shutdown(handle)
stdout:close()
stderr:close()
end
man_error(string.format('command timed out: %s', table.concat(cmd, ' ')))
end
if exit_code ~= 0 and not silent then
man_error(
string.format("command error '%s': %s", table.concat(cmd, ' '), table.concat(stderr_data))
)
end
return table.concat(stdout_data)
end
local function highlight_line(line, linenr)
local chars = {}
local prev_char = ''
@ -152,21 +220,540 @@ local function highlight_line(line, linenr)
end
local function highlight_man_page()
local mod = vim.api.nvim_buf_get_option(0, 'modifiable')
vim.api.nvim_buf_set_option(0, 'modifiable', true)
local mod = vim.bo.modifiable
vim.bo.modifiable = true
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local lines = api.nvim_buf_get_lines(0, 0, -1, false)
for i, line in ipairs(lines) do
lines[i] = highlight_line(line, i)
end
vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)
api.nvim_buf_set_lines(0, 0, -1, false, lines)
for _, args in ipairs(buf_hls) do
vim.api.nvim_buf_add_highlight(unpack(args))
api.nvim_buf_add_highlight(unpack(args))
end
buf_hls = {}
vim.api.nvim_buf_set_option(0, 'modifiable', mod)
vim.bo.modifiable = mod
end
return { highlight_man_page = highlight_man_page }
-- replace spaces in a man page name with underscores
-- intended for PostgreSQL, which has man pages like 'CREATE_TABLE(7)';
-- while editing SQL source code, it's nice to visually select 'CREATE TABLE'
-- and hit 'K', which requires this transformation
local function spaces_to_underscores(str)
local res = str:gsub('%s', '_')
return res
end
local function get_path(sect, name, silent)
name = name or ''
sect = sect or ''
-- Some man implementations (OpenBSD) return all available paths from the
-- search command. Previously, this function would simply select the first one.
--
-- However, some searches will report matches that are incorrect:
-- man -w strlen may return string.3 followed by strlen.3, and therefore
-- selecting the first would get us the wrong page. Thus, we must find the
-- first matching one.
--
-- There's yet another special case here. Consider the following:
-- If you run man -w strlen and string.3 comes up first, this is a problem. We
-- should search for a matching named one in the results list.
-- However, if you search for man -w clock_gettime, you will *only* get
-- clock_getres.2, which is the right page. Searching the resuls for
-- clock_gettime will no longer work. In this case, we should just use the
-- first one that was found in the correct section.
--
-- Finally, we can avoid relying on -S or -s here since they are very
-- inconsistently supported. Instead, call -w with a section and a name.
local cmd
if sect == '' then
cmd = { 'man', find_arg, name }
else
cmd = { 'man', find_arg, sect, name }
end
local lines = man_system(cmd, silent)
if lines == nil then
return nil
end
local results = vim.split(lines, '\n', { trimempty = true })
if #results == 0 then
return
end
-- find any that match the specified name
local namematches = vim.tbl_filter(function(v)
return fn.fnamemodify(v, ':t'):match(name)
end, results) or {}
local sectmatches = {}
if #namematches > 0 and sect ~= '' then
sectmatches = vim.tbl_filter(function(v)
return fn.fnamemodify(v, ':e') == sect
end, namematches)
end
return fn.substitute(sectmatches[1] or namematches[1] or results[1], [[\n\+$]], '', '')
end
local function matchstr(text, pat_or_re)
local re = type(pat_or_re) == 'string' and vim.regex(pat_or_re) or pat_or_re
local s, e = re:match_str(text)
if s == nil then
return
end
return text:sub(vim.str_utfindex(text, s) + 1, vim.str_utfindex(text, e))
end
-- attempt to extract the name and sect out of 'name(sect)'
-- otherwise just return the largest string of valid characters in ref
local function extract_sect_and_name_ref(ref)
ref = ref or ''
if ref:sub(1, 1) == '-' then -- try ':Man -pandoc' with this disabled.
man_error("manpage name cannot start with '-'")
end
local ref1 = ref:match('[^()]+%([^()]+%)')
if not ref1 then
local name = ref:match('[^()]+')
if not name then
man_error('manpage reference cannot contain only parentheses: ' .. ref)
end
return '', spaces_to_underscores(name)
end
local parts = vim.split(ref1, '(', { plain = true })
-- see ':Man 3X curses' on why tolower.
-- TODO(nhooyr) Not sure if this is portable across OSs
-- but I have not seen a single uppercase section.
local sect = vim.split(parts[2] or '', ')', { plain = true })[1]:lower()
local name = spaces_to_underscores(parts[1])
return sect, name
end
-- verify_exists attempts to find the path to a manpage
-- based on the passed section and name.
--
-- 1. If manpage could not be found with the given sect and name,
-- then try all the sections in b:man_default_sects.
-- 2. If it still could not be found, then we try again without a section.
-- 3. If still not found but $MANSECT is set, then we try again with $MANSECT
-- unset.
local function verify_exists(sect, name)
if sect and sect ~= '' then
local ret = get_path(sect, name, true)
if ret then
return ret
end
end
if vim.b.man_default_sects ~= nil then
local sects = vim.split(vim.b.man_default_sects, ',', { plain = true, trimempty = true })
for _, sec in ipairs(sects) do
local ret = get_path(sec, name, true)
if ret then
return ret
end
end
end
-- if none of the above worked, we will try with no section
local res_empty_sect = get_path('', name, true)
if res_empty_sect then
return res_empty_sect
end
-- if that still didn't work, we will check for $MANSECT and try again with it
-- unset
if vim.env.MANSECT then
local mansect = vim.env.MANSECT
vim.env.MANSECT = nil
local res = get_path('', name, true)
vim.env.MANSECT = mansect
if res then
return res
end
end
-- finally, if that didn't work, there is no hope
man_error('no manual entry for ' .. name)
end
local EXT_RE = vim.regex([[\.\%([glx]z\|bz2\|lzma\|Z\)$]])
-- Extracts the name/section from the 'path/name.sect', because sometimes the actual section is
-- more specific than what we provided to `man` (try `:Man 3 App::CLI`).
-- Also on linux, name seems to be case-insensitive. So for `:Man PRIntf`, we
-- still want the name of the buffer to be 'printf'.
local function extract_sect_and_name_path(path)
local tail = fn.fnamemodify(path, ':t')
if EXT_RE:match_str(path) then -- valid extensions
tail = fn.fnamemodify(tail, ':r')
end
local name, sect = tail:match('^(.+)%.([^.]+)$')
return sect, name
end
local function find_man()
local win = 1
while win <= fn.winnr('$') do
local buf = fn.winbufnr(win)
if vim.bo[buf].filetype == 'man' then
vim.cmd(win .. 'wincmd w')
return true
end
win = win + 1
end
return false
end
local function set_options(pager)
vim.bo.swapfile = false
vim.bo.buftype = 'nofile'
vim.bo.bufhidden = 'hide'
vim.bo.modified = false
vim.bo.readonly = true
vim.bo.modifiable = false
vim.b.pager = pager
vim.bo.filetype = 'man'
end
local function get_page(path, silent)
-- Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065).
-- Soft-wrap: ftplugin/man.lua sets wrap/breakindent/….
-- Hard-wrap: driven by `man`.
local manwidth
if (vim.g.man_hardwrap or 1) ~= 1 then
manwidth = 999
elseif vim.env.MANWIDTH then
manwidth = vim.env.MANWIDTH
else
manwidth = api.nvim_win_get_width(0)
end
-- Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db).
-- http://comments.gmane.org/gmane.editors.vim.devel/29085
-- Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces.
local cmd = { 'env', 'MANPAGER=cat', 'MANWIDTH=' .. manwidth, 'MAN_KEEP_FORMATTING=1', 'man' }
if localfile_arg then
cmd[#cmd + 1] = '-l'
end
cmd[#cmd + 1] = path
return man_system(cmd, silent)
end
local function put_page(page)
vim.bo.modified = true
vim.bo.readonly = false
vim.bo.swapfile = false
api.nvim_buf_set_lines(0, 0, -1, false, vim.split(page, '\n'))
while fn.getline(1):match('^%s*$') do
api.nvim_buf_set_lines(0, 0, 1, false, {})
end
-- XXX: nroff justifies text by filling it with whitespace. That interacts
-- badly with our use of $MANWIDTH=999. Hack around this by using a fixed
-- size for those whitespace regions.
vim.cmd([[silent! keeppatterns keepjumps %s/\s\{199,}/\=repeat(' ', 10)/g]])
vim.cmd('1') -- Move cursor to first line
highlight_man_page()
set_options(false)
end
local function format_candidate(path, psect)
if matchstr(path, [[\.\%(pdf\|in\)$]]) then -- invalid extensions
return ''
end
local sect, name = extract_sect_and_name_path(path)
if sect == psect then
return name
elseif sect and name and matchstr(sect, psect .. '.\\+$') then -- invalid extensions
-- We include the section if the user provided section is a prefix
-- of the actual section.
return ('%s(%s)'):format(name, sect)
end
return ''
end
local function get_paths(sect, name, do_fallback)
-- callers must try-catch this, as some `man` implementations don't support `s:find_arg`
local ok, ret = pcall(function()
local mandirs =
table.concat(vim.split(man_system({ 'man', find_arg }), '[:\n]', { trimempty = true }), ',')
local paths = fn.globpath(mandirs, 'man?/' .. name .. '*.' .. sect .. '*', false, true)
pcall(function()
-- Prioritize the result from verify_exists as it obeys b:man_default_sects.
local first = verify_exists(sect, name)
paths = vim.tbl_filter(function(v)
return v ~= first
end, paths)
paths = { first, unpack(paths) }
end)
return paths
end)
if not ok then
if not do_fallback then
error(ret)
end
-- Fallback to a single path, with the page we're trying to find.
ok, ret = pcall(verify_exists, sect, name)
return { ok and ret or nil }
end
return ret or {}
end
local function complete(sect, psect, name)
local pages = get_paths(sect, name, false)
-- We remove duplicates in case the same manpage in different languages was found.
return fn.uniq(fn.sort(vim.tbl_map(function(v)
return format_candidate(v, psect)
end, pages) or {}, 'i'))
end
-- see extract_sect_and_name_ref on why tolower(sect)
function M.man_complete(arg_lead, cmd_line, _)
local args = vim.split(cmd_line, '%s+', { trimempty = true })
local cmd_offset = fn.index(args, 'Man')
if cmd_offset > 0 then
-- Prune all arguments up to :Man itself. Otherwise modifier commands like
-- :tab, :vertical, etc. would lead to a wrong length.
args = vim.list_slice(args, cmd_offset + 1)
end
if #args > 3 then
return {}
end
if #args == 1 then
-- returning full completion is laggy. Require some arg_lead to complete
-- return complete('', '', '')
return {}
end
if arg_lead:match('^[^()]+%([^()]*$') then
-- cursor (|) is at ':Man printf(|' or ':Man 1 printf(|'
-- The later is is allowed because of ':Man pri<TAB>'.
-- It will offer 'priclass.d(1m)' even though section is specified as 1.
local tmp = vim.split(arg_lead, '(', { plain = true })
local name = tmp[1]
local sect = (tmp[2] or ''):lower()
return complete(sect, '', name)
end
if not args[2]:match('^[^()]+$') then
-- cursor (|) is at ':Man 3() |' or ':Man (3|' or ':Man 3() pri|'
-- or ':Man 3() pri |'
return {}
end
if #args == 2 then
local name, sect
if arg_lead == '' then
-- cursor (|) is at ':Man 1 |'
name = ''
sect = args[1]:lower()
else
-- cursor (|) is at ':Man pri|'
if arg_lead:match('/') then
-- if the name is a path, complete files
-- TODO(nhooyr) why does this complete the last one automatically
return fn.glob(arg_lead .. '*', false, true)
end
name = arg_lead
sect = ''
end
return complete(sect, sect, name)
end
if not arg_lead:match('[^()]+$') then
-- cursor (|) is at ':Man 3 printf |' or ':Man 3 (pr)i|'
return {}
end
-- cursor (|) is at ':Man 3 pri|'
local name = arg_lead
local sect = args[2]:lower()
return complete(sect, sect, name)
end
function M.goto_tag(pattern, _, _)
local sect, name = extract_sect_and_name_ref(pattern)
local paths = get_paths(sect, name, true)
local structured = {}
for _, path in ipairs(paths) do
sect, name = extract_sect_and_name_path(path)
if sect and name then
structured[#structured + 1] = {
name = name,
title = name .. '(' .. sect .. ')',
}
end
end
if vim.o.cscopetag then
-- return only a single entry so we work well with :cstag (#11675)
structured = { structured[1] }
end
return vim.tbl_map(function(entry)
return {
name = entry.name,
filename = 'man://' .. entry.title,
cmd = '1',
}
end, structured)
end
-- Called when Nvim is invoked as $MANPAGER.
function M.init_pager()
if fn.getline(1):match('^%s*$') then
api.nvim_buf_set_lines(0, 0, 1, false, {})
else
vim.cmd('keepjumps 1')
end
highlight_man_page()
-- Guess the ref from the heading (which is usually uppercase, so we cannot
-- know the correct casing, cf. `man glDrawArraysInstanced`).
local ref = fn.substitute(matchstr(fn.getline(1), [[^[^)]\+)]]) or '', ' ', '_', 'g')
local ok, res = pcall(extract_sect_and_name_ref, ref)
vim.b.man_sect = ok and res or ''
if not fn.bufname('%'):match('man://') then -- Avoid duplicate buffers, E95.
vim.cmd.file({ 'man://' .. fn.fnameescape(ref):lower(), mods = { silent = true } })
end
set_options(true)
end
function M.open_page(count, smods, args)
if #args > 2 then
man_error('too many arguments')
end
local ref
if #args == 0 then
ref = vim.bo.filetype == 'man' and fn.expand('<cWORD>') or fn.expand('<cword>')
if ref == '' then
man_error('no identifier under cursor')
end
elseif #args == 1 then
ref = args[1]
else
-- Combine the name and sect into a manpage reference so that all
-- verification/extraction can be kept in a single function.
-- If args[2] is a reference as well, that is fine because it is the only
-- reference that will match.
ref = ('%s(%s)'):format(args[2], args[1])
end
local sect, name = extract_sect_and_name_ref(ref)
if count >= 0 then
sect = tostring(count)
end
local path = verify_exists(sect, name)
sect, name = extract_sect_and_name_path(path)
local buf = fn.bufnr()
local save_tfu = vim.bo[buf].tagfunc
vim.bo[buf].tagfunc = "v:lua.require'man'.goto_tag"
local target = ('%s(%s)'):format(name, sect)
local ok, ret = pcall(function()
if not smods.tab and find_man() then
vim.cmd.tag({ target, mods = { silent = true, keepalt = true } })
else
smods.silent = true
smods.keepalt = true
vim.cmd.stag({ target, mods = smods })
end
end)
vim.bo[buf].tagfunc = save_tfu
if not ok then
error(ret)
else
set_options(false)
end
vim.b.man_sect = sect
end
-- Called when a man:// buffer is opened.
function M.read_page(ref)
local sect, name = extract_sect_and_name_ref(ref)
local path = verify_exists(sect, name)
sect = extract_sect_and_name_path(path)
local page = get_page(path)
vim.b.man_sect = sect
put_page(page)
end
function M.show_toc()
local bufname = fn.bufname('%')
local info = fn.getloclist(0, { winid = 1 })
if info ~= '' and vim.w[info.winid].qf_toc == bufname then
vim.cmd.lopen()
return
end
local toc = {}
local lnum = 2
local last_line = fn.line('$') - 1
local section_title_re = vim.regex([[^\%( \{3\}\)\=\S.*$]])
local flag_title_re = vim.regex([[^\s\+\%(+\|-\)\S\+]])
while lnum and lnum < last_line do
local text = fn.getline(lnum)
if section_title_re:match_str(text) then
-- if text is a section title
toc[#toc + 1] = {
bufnr = fn.bufnr('%'),
lnum = lnum,
text = text,
}
elseif flag_title_re:match_str(text) then
-- if text is a flag title. we strip whitespaces and prepend two
-- spaces to have a consistent format in the loclist.
toc[#toc + 1] = {
bufnr = fn.bufnr('%'),
lnum = lnum,
text = ' ' .. fn.substitute(text, [[^\s*\(.\{-}\)\s*$]], [[\1]], ''),
}
end
lnum = fn.nextnonblank(lnum + 1)
end
fn.setloclist(0, toc, ' ')
fn.setloclist(0, {}, 'a', { title = 'Man TOC' })
vim.cmd.lopen()
vim.w.qf_toc = bufname
end
local function init()
local path = get_path('', 'man', true)
local page
if path ~= nil then
-- Check for -l support.
page = get_page(path, true)
end
if page == '' or page == nil then
localfile_arg = false
end
end
init()
return M

34
runtime/plugin/man.lua Normal file
View File

@ -0,0 +1,34 @@
if vim.g.loaded_man ~= nil then
return
end
vim.g.loaded_man = true
vim.api.nvim_create_user_command('Man', function(params)
local man = require('man')
if params.bang then
man.init_pager()
else
local ok, err = pcall(man.open_page, params.count, params.smods, params.fargs)
if not ok then
vim.notify(man.errormsg or err, vim.log.levels.ERROR)
end
end
end, {
bang = true,
bar = true,
addr = 'other',
nargs = '*',
complete = function(...)
return require('man').man_complete(...)
end,
})
local augroup = vim.api.nvim_create_augroup('man', {})
vim.api.nvim_create_autocmd('BufReadCmd', {
group = augroup,
pattern = 'man://*',
callback = function(params)
require('man').read_page(vim.fn.matchstr(params.match, 'man://\\zs.*'))
end,
})

View File

@ -1,15 +0,0 @@
" Maintainer: Anmol Sethi <hi@nhooyr.io>
if exists('g:loaded_man')
finish
endif
let g:loaded_man = 1
command! -bang -bar -addr=other -complete=customlist,man#complete -nargs=* Man
\ if <bang>0 | call man#init_pager() |
\ else | call man#open_page(<count>, <q-mods>, <f-args>) | endif
augroup man
autocmd!
autocmd BufReadCmd man://* call man#read_page(matchstr(expand('<amatch>'), 'man://\zs.*'))
augroup END

View File

@ -40,8 +40,8 @@ func Test_profile_func()
call writefile(lines, 'Xprofile_func.vim')
call system(GetVimCommand()
\ . ' -es --clean'
\ . ' -c "so Xprofile_func.vim"'
\ . ' -c "qall!"')
\ . ' --cmd "so Xprofile_func.vim"'
\ . ' --cmd "qall!"')
call assert_equal(0, v:shell_error)
let lines = readfile('Xprofile_func.log')
@ -475,7 +475,7 @@ func Test_profdel_func()
call Foo3()
[CODE]
call writefile(lines, 'Xprofile_file.vim')
call system(GetVimCommandClean() . ' -es -c "so Xprofile_file.vim" -c q')
call system(GetVimCommandClean() . ' -es --cmd "so Xprofile_file.vim" --cmd q')
call assert_equal(0, v:shell_error)
let lines = readfile('Xprofile_file.log')

View File

@ -1,7 +1,8 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local command, eval, rawfeed = helpers.command, helpers.eval, helpers.rawfeed
local command, rawfeed = helpers.command, helpers.rawfeed
local clear = helpers.clear
local exec_lua = helpers.exec_lua
local funcs = helpers.funcs
local nvim_prog = helpers.nvim_prog
local matches = helpers.matches
@ -50,7 +51,7 @@ describe(':Man', function()
|
]]}
eval('man#init_pager()')
exec_lua[[require'man'.init_pager()]]
screen:expect([[
^this {b:is} {b:a} test |
@ -74,7 +75,7 @@ describe(':Man', function()
|
]=]}
eval('man#init_pager()')
exec_lua[[require'man'.init_pager()]]
screen:expect([[
^this {b:is }{bi:a }{biu:test} |
@ -89,7 +90,7 @@ describe(':Man', function()
rawfeed([[
ithis i<C-v><C-h>is<C-v><C-h>s <C-v><C-h> test
with _<C-v><C-h>ö_<C-v><C-h>v_<C-v><C-h>e_<C-v><C-h>r_<C-v><C-h>s_<C-v><C-h>t_<C-v><C-h>r_<C-v><C-h>u_<C-v><C-h>̃_<C-v><C-h>c_<C-v><C-h>k te<C-v><ESC>[3mxt<C-v><ESC>[0m<ESC>]])
eval('man#init_pager()')
exec_lua[[require'man'.init_pager()]]
screen:expect([[
^this {b:is} {b:} test |
@ -105,7 +106,7 @@ describe(':Man', function()
i_<C-v><C-h>_b<C-v><C-h>be<C-v><C-h>eg<C-v><C-h>gi<C-v><C-h>in<C-v><C-h>ns<C-v><C-h>s
m<C-v><C-h>mi<C-v><C-h>id<C-v><C-h>d_<C-v><C-h>_d<C-v><C-h>dl<C-v><C-h>le<C-v><C-h>e
_<C-v><C-h>m_<C-v><C-h>i_<C-v><C-h>d_<C-v><C-h>__<C-v><C-h>d_<C-v><C-h>l_<C-v><C-h>e<ESC>]])
eval('man#init_pager()')
exec_lua[[require'man'.init_pager()]]
screen:expect([[
{b:^_begins} |
@ -121,7 +122,7 @@ describe(':Man', function()
i· ·<C-v><C-h>·
+<C-v><C-h>o
+<C-v><C-h>+<C-v><C-h>o<C-v><C-h>o double<ESC>]])
eval('man#init_pager()')
exec_lua[[require'man'.init_pager()]]
screen:expect([[
^· {b:·} |
@ -138,7 +139,7 @@ describe(':Man', function()
<C-v><C-[>[44m 4 <C-v><C-[>[45m 5 <C-v><C-[>[46m 6 <C-v><C-[>[47m 7 <C-v><C-[>[100m 8 <C-v><C-[>[101m 9
<C-v><C-[>[102m 10 <C-v><C-[>[103m 11 <C-v><C-[>[104m 12 <C-v><C-[>[105m 13 <C-v><C-[>[106m 14 <C-v><C-[>[107m 15
<C-v><C-[>[48:5:16m 16 <ESC>]])
eval('man#init_pager()')
exec_lua[[require'man'.init_pager()]]
screen:expect([[
^ 0 1 2 3 |