From 2afcdbd63a5b0cbeaad9d83b096a3af5201c67a9 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 2 Sep 2022 15:20:29 +0100 Subject: feat(Man): port to Lua (#19912) Co-authored-by: zeertzjq --- runtime/lua/man.lua | 601 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 594 insertions(+), 7 deletions(-) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 5da3d2a92f..4b8239ce74 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -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 + +-- 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 -return { highlight_man_page = highlight_man_page } +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'. + -- 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('') or fn.expand('') + 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 -- cgit From 1ef7720567b08caec0c077605fb2a01a9d6eafbc Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Thu, 1 Sep 2022 18:46:34 +0800 Subject: fix(api)!: correctly deal with number before :tab Now nvim_parse_cmd and nvim_create_user_command use a "tab" value which is the same as the number passed before :tab modifier instead of the number plus 1, and "tab" value is -1 if :tab modifier is not used. --- runtime/lua/man.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 4b8239ce74..82ed0305f7 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -671,7 +671,7 @@ function M.open_page(count, smods, args) local target = ('%s(%s)'):format(name, sect) local ok, ret = pcall(function() - if not smods.tab and find_man() then + if smods.tab == -1 and find_man() then vim.cmd.tag({ target, mods = { silent = true, keepalt = true } }) else smods.silent = true -- cgit From abe2d90693e5cec3428c0162c48f0ea38972ff31 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 20 Sep 2022 11:15:32 +0100 Subject: feat(lua): move compat module from runtime to test (#20257) --- runtime/lua/man.lua | 2 -- 1 file changed, 2 deletions(-) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 82ed0305f7..f0306f4871 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -1,5 +1,3 @@ -require('vim.compat') - local api, fn = vim.api, vim.fn local find_arg = '-w' -- cgit From 02f8ca59a80cd3570593c717ff6ceadc33239b89 Mon Sep 17 00:00:00 2001 From: bfredl Date: Tue, 20 Sep 2022 22:03:16 +0200 Subject: fix(tests): indicate in test logs when nvim exit times out When it happens it wastes 2 seconds which is NOT included in the normal busted timing info. It is hard to correct this, but we can at least print a warning when this happens. --- runtime/lua/man.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index f0306f4871..88535321ac 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -21,13 +21,15 @@ local function man_system(cmd, silent) local done = false local exit_code - local handle = vim.loop.spawn(cmd[1], { + local handle + 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() + handle:close() done = true end) @@ -52,7 +54,7 @@ local function man_system(cmd, silent) if not done then if handle then - vim.loop.shutdown(handle) + handle:close() stdout:close() stderr:close() end -- cgit From cba05c541d613dc05916b262c0d7d3a10ec08c67 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 11 Oct 2022 13:25:42 +0100 Subject: refactor(man): pass env directly to spawn() (#20591) --- runtime/lua/man.lua | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 88535321ac..8ca1bb90f5 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -12,7 +12,7 @@ local function man_error(msg) end -- Run a system command and timeout after 30 seconds. -local function man_system(cmd, silent) +local function man_system(args, silent, env) local stdout_data = {} local stderr_data = {} local stdout = vim.loop.new_pipe(false) @@ -22,9 +22,10 @@ local function man_system(cmd, silent) local exit_code local handle - handle = vim.loop.spawn(cmd[1], { - args = vim.list_slice(cmd, 2), + handle = vim.loop.spawn('man', { + args = args, stdio = { nil, stdout, stderr }, + env = env, }, function(code) exit_code = code stdout:close() @@ -44,7 +45,8 @@ local function man_system(cmd, silent) stdout:close() stderr:close() if not silent then - man_error(string.format('command error: %s', table.concat(cmd))) + local cmd = table.concat({ 'man', unpack(args) }, ' ') + man_error(string.format('command error: %s', cmd)) end end @@ -58,13 +60,13 @@ local function man_system(cmd, silent) stdout:close() stderr:close() end - man_error(string.format('command timed out: %s', table.concat(cmd, ' '))) + local cmd = table.concat({ 'man', unpack(args) }, ' ') + man_error(string.format('command timed out: %s', 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)) - ) + local cmd = table.concat({ 'man', unpack(args) }, ' ') + man_error(string.format("command error '%s': %s", cmd, table.concat(stderr_data))) end return table.concat(stdout_data) @@ -267,19 +269,15 @@ local function get_path(sect, name, silent) -- -- 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 + local args if sect == '' then - cmd = { 'man', find_arg, name } + args = { find_arg, name } else - cmd = { 'man', find_arg, sect, name } + args = { 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 }) + local lines = man_system(args, silent) + local results = vim.split(lines or {}, '\n', { trimempty = true }) if #results == 0 then return @@ -435,15 +433,17 @@ local function get_page(path, silent) else manwidth = api.nvim_win_get_width(0) end + + local args = localfile_arg and { '-l', path } or { path } + -- 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) + return man_system(args, silent, { + 'MANPAGER=cat', + 'MANWIDTH=' .. manwidth, + 'MAN_KEEP_FORMATTING=1', + }) end local function put_page(page) @@ -484,7 +484,7 @@ 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 }), ',') + table.concat(vim.split(man_system({ 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. -- cgit From b126b118405b90c30c47119862e6c040738ac540 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 11 Oct 2022 13:25:51 +0100 Subject: fix(man): support MacOS 13 MacOS 13 has changed its version of `man` to an version that doesn't properly support `man -w` (without arguments). In order to workaround this we simply fallback to $MANPATH. Fixes #20579 --- runtime/lua/man.lua | 94 ++++++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 45 deletions(-) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 8ca1bb90f5..6477786dbb 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -1,6 +1,6 @@ local api, fn = vim.api, vim.fn -local find_arg = '-w' +local FIND_ARG = '-w' local localfile_arg = true -- Always use -l if possible. #6683 local buf_hls = {} @@ -12,7 +12,7 @@ local function man_error(msg) end -- Run a system command and timeout after 30 seconds. -local function man_system(args, silent, env) +local function system(cmd, silent, env) local stdout_data = {} local stderr_data = {} local stdout = vim.loop.new_pipe(false) @@ -22,8 +22,8 @@ local function man_system(args, silent, env) local exit_code local handle - handle = vim.loop.spawn('man', { - args = args, + handle = vim.loop.spawn(cmd[1], { + args = vim.list_slice(cmd, 2), stdio = { nil, stdout, stderr }, env = env, }, function(code) @@ -45,8 +45,8 @@ local function man_system(args, silent, env) stdout:close() stderr:close() if not silent then - local cmd = table.concat({ 'man', unpack(args) }, ' ') - man_error(string.format('command error: %s', cmd)) + local cmd_str = table.concat(cmd, ' ') + man_error(string.format('command error: %s', cmd_str)) end end @@ -60,13 +60,13 @@ local function man_system(args, silent, env) stdout:close() stderr:close() end - local cmd = table.concat({ 'man', unpack(args) }, ' ') - man_error(string.format('command timed out: %s', cmd)) + local cmd_str = table.concat(cmd, ' ') + man_error(string.format('command timed out: %s', cmd_str)) end if exit_code ~= 0 and not silent then - local cmd = table.concat({ 'man', unpack(args) }, ' ') - man_error(string.format("command error '%s': %s", cmd, table.concat(stderr_data))) + local cmd_str = table.concat(cmd, ' ') + man_error(string.format("command error '%s': %s", cmd_str, table.concat(stderr_data))) end return table.concat(stdout_data) @@ -269,14 +269,14 @@ local function get_path(sect, name, silent) -- -- 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 args + local cmd if sect == '' then - args = { find_arg, name } + cmd = { 'man', FIND_ARG, name } else - args = { find_arg, sect, name } + cmd = { 'man', FIND_ARG, sect, name } end - local lines = man_system(args, silent) + local lines = system(cmd, silent) local results = vim.split(lines or {}, '\n', { trimempty = true }) if #results == 0 then @@ -342,7 +342,7 @@ end -- 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) +local function verify_exists(sect, name, silent) if sect and sect ~= '' then local ret = get_path(sect, name, true) if ret then @@ -378,8 +378,10 @@ local function verify_exists(sect, name) end end - -- finally, if that didn't work, there is no hope - man_error('no manual entry for ' .. name) + if not silent then + -- finally, if that didn't work, there is no hope + man_error('no manual entry for ' .. name) + end end local EXT_RE = vim.regex([[\.\%([glx]z\|bz2\|lzma\|Z\)$]]) @@ -434,12 +436,12 @@ local function get_page(path, silent) manwidth = api.nvim_win_get_width(0) end - local args = localfile_arg and { '-l', path } or { path } + local cmd = localfile_arg and { 'man', '-l', path } or { 'man', path } -- 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. - return man_system(args, silent, { + return system(cmd, silent, { 'MANPAGER=cat', 'MANWIDTH=' .. manwidth, 'MAN_KEEP_FORMATTING=1', @@ -480,38 +482,40 @@ local function format_candidate(path, psect) 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({ 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) +local function move_elem_to_head(list, elem) + local list1 = vim.tbl_filter(function(v) + return v ~= elem + end, list) + return { elem, unpack(list1) } +end - if not ok then - if not do_fallback then - error(ret) - end +local function get_paths(sect, name) + -- Try several sources for getting the list man directories: + -- 1. `man -w` (works on most systems) + -- 2. `manpath` + -- 3. $MANPATH + local mandirs_raw = vim.F.npcall(system, { 'man', FIND_ARG }) + or vim.F.npcall(system, { 'manpath', '-q' }) + or vim.env.MANPATH - -- Fallback to a single path, with the page we're trying to find. - ok, ret = pcall(verify_exists, sect, name) + if not mandirs_raw then + man_error("Could not determine man directories from: 'man -w', 'manpath' or $MANPATH") + end - return { ok and ret or nil } + local mandirs = table.concat(vim.split(mandirs_raw, '[:\n]', { trimempty = true }), ',') + local paths = fn.globpath(mandirs, 'man?/' .. name .. '*.' .. sect .. '*', false, true) + + -- Prioritize the result from verify_exists as it obeys b:man_default_sects. + local first = verify_exists(sect, name, true) + if first then + paths = move_elem_to_head(paths, first) end - return ret or {} + + return paths end local function complete(sect, psect, name) - local pages = get_paths(sect, name, false) + local pages = get_paths(sect, name) -- 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) @@ -587,7 +591,7 @@ end function M.goto_tag(pattern, _, _) local sect, name = extract_sect_and_name_ref(pattern) - local paths = get_paths(sect, name, true) + local paths = get_paths(sect, name) local structured = {} for _, path in ipairs(paths) do -- cgit From 288208257c8d6b3c8dcce7ee6c7b6c7bb7bafb27 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 8 Oct 2022 15:48:07 +0100 Subject: feat(cscope)!: remove --- runtime/lua/man.lua | 5 ----- 1 file changed, 5 deletions(-) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 6477786dbb..18d08ad1a8 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -604,11 +604,6 @@ function M.goto_tag(pattern, _, _) 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, -- cgit From 39911d76be560c998cc7dee51c5d94f811164f66 Mon Sep 17 00:00:00 2001 From: Mahmoud Al-Qudsi Date: Mon, 17 Oct 2022 04:37:44 -0500 Subject: fix(man): handle absolute paths as `:Man` targets (#20624) * fix(man): handle absolute paths as :Man targets Previously, attempting to provide `:Man` with an absolute path as the name would cause neovim to return the following error: ``` Error detected while processing command line: /usr/local/share/nvim/runtime/lua/man.lua:690: /usr/local/share/nvim/runtime/lua/man.lua:683: Vim:E426: tag not found: nil(nil) Press ENTER or type command to continue ``` ..because it would try to validate the existence of a man page for the provided name by executing `man -w /some/path` which (on at least some Linux machines [0]) returns `/some/path` instead of the path to the nroff files that would be formatted to satisfy the man(1) lookup. While man pages are not normally named after absolute paths, users shouldn't be blamed for trying. Given such a name/path, neovim would **not** complain that the path didn't have a corresponding man file but would error out when trying to call the tag function for the null-propagated name-and-section `nil(nil)`. (The same underlying error existed before this function was ported to lua, but did not exhibit the lua-specific `nil(nil)` name; instead a tag lookup for `()` would fail and error out.) With this patch, we detect the case where `man -w ...` returns the same value as the provided name to not only prevent invoking the tag function for a non-existent/malformed name+sect but also to properly report the non-existence of a man page for the provided lookup (the absolute path). While man(1) can be used to directly read an nroff-formatted document via `man /path/to/nroff.doc`, `:Man /path/to/nroff.doc` never supported this behavior so no functionality is lost in case the provided path _was_ an nroff file. [0]: `man -w /absolute/path` returning `/absolute/path` observed on an Ubuntu 18.04 installation. * test: add regression test for #20624 Add a functional test to `man_spec.lua` to check for a regression for #20624 by first obtaining an absolute path to a random file and materializing it to disk, then attempting to query `:Man` for an entry by that same name/path. The test passes if nvim correctly reports that there is no man page correspending to the provided name/path and fails if any other error (or no error) is shown. --- runtime/lua/man.lua | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 18d08ad1a8..6eebee6376 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -283,6 +283,14 @@ local function get_path(sect, name, silent) return end + -- `man -w /some/path` will return `/some/path` for any existent file, which + -- stops us from actually determining if a path has a corresponding man file. + -- Since `:Man /some/path/to/man/file` isn't supported anyway, we should just + -- error out here if we detect this is the case. + if sect == '' and #results == 1 and results[1] == name then + return + end + -- find any that match the specified name local namematches = vim.tbl_filter(function(v) return fn.fnamemodify(v, ':t'):match(name) -- cgit From cc5b7368d61cfcd775dd02803dbdb8d4d05b5d5d Mon Sep 17 00:00:00 2001 From: Kevin Hwang Date: Thu, 3 Nov 2022 09:13:29 +0800 Subject: fix(man.lua): set modifiable before writing page (#20914) --- runtime/lua/man.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 6eebee6376..5951884e07 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -457,7 +457,7 @@ local function get_page(path, silent) end local function put_page(page) - vim.bo.modified = true + vim.bo.modifiable = true vim.bo.readonly = false vim.bo.swapfile = false -- cgit From 0faf007a236c9b51f151790f79ee59366b501c55 Mon Sep 17 00:00:00 2001 From: euclidianAce Date: Wed, 9 Nov 2022 17:26:02 -0600 Subject: fix(man.lua): use `env` command (#21007) Previously man.lua would use the `env` field in the parameters of `vim.loop.spawn` to override things like MANPAGER. This caused issues on NixOS since `spawn` will _override_ the environment rather than _append_ to it (and NixOS relies on a heavily modified environment). Using the `env` command to append to the environment solves this issue. --- runtime/lua/man.lua | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'runtime/lua/man.lua') diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 5951884e07..a644dd68b8 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -12,7 +12,7 @@ local function man_error(msg) end -- Run a system command and timeout after 30 seconds. -local function system(cmd, silent, env) +local function system(cmd_, silent, env) local stdout_data = {} local stderr_data = {} local stdout = vim.loop.new_pipe(false) @@ -21,11 +21,23 @@ local function system(cmd, silent, env) local done = false local exit_code + -- We use the `env` command here rather than the env option to vim.loop.spawn since spawn will + -- completely overwrite the environment when we just want to modify the existing one. + -- + -- Overwriting mainly causes problems NixOS which relies heavily on a non-standard environment. + local cmd + if env then + cmd = { 'env' } + vim.list_extend(cmd, env) + vim.list_extend(cmd, cmd_) + else + cmd = cmd_ + end + local handle handle = vim.loop.spawn(cmd[1], { args = vim.list_slice(cmd, 2), stdio = { nil, stdout, stderr }, - env = env, }, function(code) exit_code = code stdout:close() -- cgit