man.lua (24468B)
1 local api, fn = vim.api, vim.fn 2 3 local M = {} 4 5 --- Run a system command and timeout after 10 seconds. 6 --- @param cmd string[] 7 --- @param silent boolean? 8 --- @param env? table<string,string|number> 9 --- @return string 10 local function system(cmd, silent, env) 11 if fn.executable(cmd[1]) == 0 then 12 error(string.format('executable not found: "%s"', cmd[1]), 0) 13 end 14 15 local r = vim.system(cmd, { env = env, timeout = 10000 }):wait() 16 17 if not silent then 18 if r.code ~= 0 then 19 local cmd_str = table.concat(cmd, ' ') 20 error(string.format("command error '%s': %s", cmd_str, r.stderr)) 21 end 22 assert(r.stdout ~= '') 23 end 24 25 return assert(r.stdout) 26 end 27 28 --- @enum Man.Attribute 29 local Attrs = { 30 None = 0, 31 Bold = 1, 32 Underline = 2, 33 Italic = 3, 34 } 35 36 --- @param line string 37 --- @param row integer 38 --- @param hls {attr:Man.Attribute,row:integer,start:integer,final:integer}[] 39 --- @return string 40 local function render_line(line, row, hls) 41 --- @type string[] 42 local chars = {} 43 local prev_char = '' 44 local overstrike, escape, osc8 = false, false, false 45 46 local attr = Attrs.None 47 local byte = 0 -- byte offset 48 49 local hls_start = #hls + 1 50 51 --- @param code integer 52 local function add_attr_hl(code) 53 local continue_hl = true 54 if code == 0 then 55 attr = Attrs.None 56 continue_hl = false 57 elseif code == 1 then 58 attr = Attrs.Bold 59 elseif code == 22 then 60 attr = Attrs.Bold 61 continue_hl = false 62 elseif code == 3 then 63 attr = Attrs.Italic 64 elseif code == 23 then 65 attr = Attrs.Italic 66 continue_hl = false 67 elseif code == 4 then 68 attr = Attrs.Underline 69 elseif code == 24 then 70 attr = Attrs.Underline 71 continue_hl = false 72 else 73 attr = Attrs.None 74 return 75 end 76 77 if continue_hl then 78 hls[#hls + 1] = { attr = attr, row = row, start = byte, final = -1 } 79 else 80 for _, a in pairs(attr == Attrs.None and Attrs or { attr }) do 81 for i = hls_start, #hls do 82 if hls[i].attr == a and hls[i].final == -1 then 83 hls[i].final = byte 84 end 85 end 86 end 87 end 88 end 89 90 -- Break input into UTF8 code points. ASCII code points (from 0x00 to 0x7f) 91 -- can be represented in one byte. Any code point above that is represented by 92 -- a leading byte (0xc0 and above) and continuation bytes (0x80 to 0xbf, or 93 -- decimal 128 to 191). 94 for char in line:gmatch('[^\128-\191][\128-\191]*') do 95 if overstrike then 96 local last_hl = hls[#hls] 97 if char == prev_char then 98 if char == '_' and attr == Attrs.Italic and last_hl and last_hl.final == byte then 99 -- This underscore is in the middle of an italic word 100 attr = Attrs.Italic 101 else 102 attr = Attrs.Bold 103 end 104 elseif prev_char == '_' then 105 -- Even though underline is strictly what this should be. <bs>_ was used by nroff to 106 -- indicate italics which wasn't possible on old typewriters so underline was used. Modern 107 -- terminals now support italics so lets use that now. 108 -- See: 109 -- - https://unix.stackexchange.com/questions/274658/purpose-of-ascii-text-with-overstriking-file-format/274795#274795 110 -- - https://cmd.inp.nsk.su/old/cmd2/manuals/unix/UNIX_Unleashed/ch08.htm 111 -- attr = Attrs.Underline 112 attr = Attrs.Italic 113 elseif prev_char == '+' and char == 'o' then 114 -- bullet (overstrike text '+^Ho') 115 attr = Attrs.Bold 116 char = '·' 117 elseif prev_char == '·' and char == 'o' then 118 -- bullet (additional handling for '+^H+^Ho^Ho') 119 attr = Attrs.Bold 120 char = '·' 121 else 122 -- use plain char 123 attr = Attrs.None 124 end 125 126 -- Grow the previous highlight group if possible 127 if last_hl and last_hl.attr == attr and last_hl.final == byte then 128 last_hl.final = byte + #char 129 else 130 hls[#hls + 1] = { attr = attr, row = row, start = byte, final = byte + #char } 131 end 132 133 overstrike = false 134 prev_char = '' 135 byte = byte + #char 136 chars[#chars + 1] = char 137 elseif osc8 then 138 -- eat characters until String Terminator or bell 139 if (prev_char == '\027' and char == '\\') or char == '\a' then 140 osc8 = false 141 end 142 prev_char = char 143 elseif escape then 144 -- Use prev_char to store the escape sequence 145 prev_char = prev_char .. char 146 -- We only want to match against SGR sequences, which consist of ESC 147 -- followed by '[', then a series of parameter and intermediate bytes in 148 -- the range 0x20 - 0x3f, then 'm'. (See ECMA-48, sections 5.4 & 8.3.117) 149 --- @type string? 150 local sgr = prev_char:match('^%[([\032-\063]*)m$') 151 -- Ignore escape sequences with : characters, as specified by ITU's T.416 152 -- Open Document Architecture and interchange format. 153 if sgr and not sgr:find(':') then 154 local match --- @type string? 155 while sgr and #sgr > 0 do 156 -- Match against SGR parameters, which may be separated by ';' 157 --- @type string?, string? 158 match, sgr = sgr:match('^(%d*);?(.*)') 159 add_attr_hl(match + 0) -- coerce to number 160 end 161 escape = false 162 elseif prev_char == ']8;' then 163 osc8 = true 164 escape = false 165 elseif not prev_char:match('^[][][\032-\063]*$') then 166 -- Stop looking if this isn't a partial CSI or OSC sequence 167 escape = false 168 end 169 elseif char == '\027' then 170 escape = true 171 prev_char = '' 172 elseif char == '\b' then 173 overstrike = true 174 prev_char = chars[#chars] 175 byte = byte - #prev_char 176 chars[#chars] = nil 177 else 178 byte = byte + #char 179 chars[#chars + 1] = char 180 end 181 end 182 183 return table.concat(chars, '') 184 end 185 186 local HlGroups = { 187 [Attrs.Bold] = 'manBold', 188 [Attrs.Underline] = 'manUnderline', 189 [Attrs.Italic] = 'manItalic', 190 } 191 192 local function highlight_man_page() 193 local mod = vim.bo.modifiable 194 vim.bo.modifiable = true 195 196 local lines = api.nvim_buf_get_lines(0, 0, -1, false) 197 198 --- @type {attr:Man.Attribute,row:integer,start:integer,final:integer}[] 199 local hls = {} 200 201 for i, line in ipairs(lines) do 202 lines[i] = render_line(line, i - 1, hls) 203 end 204 205 api.nvim_buf_set_lines(0, 0, -1, false, lines) 206 207 for _, hl in ipairs(hls) do 208 if hl.attr ~= Attrs.None then 209 --- @diagnostic disable-next-line: deprecated 210 api.nvim_buf_add_highlight(0, -1, HlGroups[hl.attr], hl.row, hl.start, hl.final) 211 end 212 end 213 214 vim.bo.modifiable = mod 215 end 216 217 --- @param name? string 218 --- @param sect? string 219 local function get_path(name, sect) 220 name = name or '' 221 sect = sect or '' 222 -- Some man implementations (OpenBSD) return all available paths from the 223 -- search command. Previously, this function would simply select the first one. 224 -- 225 -- However, some searches will report matches that are incorrect: 226 -- man -w strlen may return string.3 followed by strlen.3, and therefore 227 -- selecting the first would get us the wrong page. Thus, we must find the 228 -- first matching one. 229 -- 230 -- There's yet another special case here. Consider the following: 231 -- If you run man -w strlen and string.3 comes up first, this is a problem. We 232 -- should search for a matching named one in the results list. 233 -- However, if you search for man -w clock_gettime, you will *only* get 234 -- clock_getres.2, which is the right page. Searching the results for 235 -- clock_gettime will no longer work. In this case, we should just use the 236 -- first one that was found in the correct section. 237 -- 238 -- Finally, we can avoid relying on -S or -s here since they are very 239 -- inconsistently supported. Instead, call -w with a section and a name. 240 local cmd --- @type string[] 241 if sect == '' then 242 cmd = { 'man', '-w', name } 243 else 244 cmd = { 'man', '-w', sect, name } 245 end 246 247 local lines = system(cmd, true) 248 local results = vim.split(lines, '\n', { trimempty = true }) 249 250 if #results == 0 then 251 return 252 end 253 254 -- `man -w /some/path` will return `/some/path` for any existent file, which 255 -- stops us from actually determining if a path has a corresponding man file. 256 -- Since `:Man /some/path/to/man/file` isn't supported anyway, we should just 257 -- error out here if we detect this is the case. 258 if sect == '' and #results == 1 and results[1] == name then 259 return 260 end 261 262 -- find any that match the specified name 263 --- @param v string 264 local namematches = vim.tbl_filter(function(v) 265 local tail = vim.fs.basename(v) 266 return tail:find(name, 1, true) ~= nil 267 end, results) or {} 268 local sectmatches = {} 269 270 if #namematches > 0 and sect ~= '' then 271 --- @param v string 272 sectmatches = vim.tbl_filter(function(v) 273 return fn.fnamemodify(v, ':e') == sect 274 end, namematches) 275 end 276 277 return (sectmatches[1] or namematches[1] or results[1]):gsub('\n+$', '') 278 end 279 280 --- Attempt to extract the name and sect out of 'name(sect)' 281 --- otherwise just return the largest string of valid characters in ref 282 --- @param ref string 283 --- @return string? name 284 --- @return string? sect 285 --- @return string? err 286 local function parse_ref(ref) 287 if ref == '' or ref:sub(1, 1) == '-' then 288 return nil, nil, ('invalid manpage reference "%s"'):format(ref) 289 end 290 291 -- match "<name>(<sect>)" 292 -- note: name can contain spaces 293 local name, sect = ref:match('([^()]+)%(([^()]+)%)') 294 if name then 295 -- see ':Man 3X curses' on why tolower. 296 -- TODO(nhooyr) Not sure if this is portable across OSs 297 -- but I have not seen a single uppercase section. 298 return name, sect:lower() 299 end 300 301 name = ref:match('[^()]+') 302 if not name then 303 return nil, nil, ('invalid manpage reference "%s"'):format(ref) 304 end 305 return name 306 end 307 308 --- Attempts to find the path to a manpage based on the passed section and name. 309 --- 310 --- 1. If manpage could not be found with the given sect and name, 311 --- then try all the sections in b:man_default_sects. 312 --- 2. If it still could not be found, then we try again without a section. 313 --- 3. If still not found but $MANSECT is set, then we try again with $MANSECT 314 --- unset. 315 --- 4. If a path still wasn't found, return nil. 316 --- @param name string? 317 --- @param sect string? 318 --- @return string? path 319 function M._find_path(name, sect) 320 if sect and sect ~= '' then 321 local ret = get_path(name, sect) 322 if ret then 323 return ret 324 end 325 end 326 327 if vim.b.man_default_sects ~= nil then 328 for sec in vim.gsplit(vim.b.man_default_sects, ',', { trimempty = true }) do 329 local ret = get_path(name, sec) 330 if ret then 331 return ret 332 end 333 end 334 end 335 336 -- if none of the above worked, we will try with no section 337 local ret = get_path(name) 338 if ret then 339 return ret 340 end 341 342 -- if that still didn't work, we will check for $MANSECT and try again with it 343 -- unset 344 if vim.env.MANSECT then 345 --- @type string 346 local mansect = vim.env.MANSECT 347 vim.env.MANSECT = nil 348 local res = get_path(name) 349 vim.env.MANSECT = mansect 350 if res then 351 return res 352 end 353 end 354 355 -- finally, if that didn't work, there is no hope 356 return nil 357 end 358 359 --- Extracts the name/section from the 'path/name.sect', because sometimes the 360 --- actual section is more specific than what we provided to `man` 361 --- (try `:Man 3 App::CLI`). Also on linux, name seems to be case-insensitive. 362 --- So for `:Man PRIntf`, we still want the name of the buffer to be 'printf'. 363 --- @param path string 364 --- @return string name 365 --- @return string sect 366 local function parse_path(path) 367 local tail = vim.fs.basename(path) 368 if 369 path:match('%.[glx]z$') 370 or path:match('%.bz2$') 371 or path:match('%.lzma$') 372 or path:match('%.Z$') 373 then 374 tail = fn.fnamemodify(tail, ':r') 375 end 376 return tail:match('^(.+)%.([^.]+)$') 377 end 378 379 --- @return boolean 380 local function find_man() 381 if vim.bo.filetype == 'man' then 382 return true 383 end 384 385 local win = 1 386 while win <= fn.winnr('$') do 387 local buf = fn.winbufnr(win) 388 if vim.bo[buf].filetype == 'man' then 389 vim.cmd(win .. 'wincmd w') 390 return true 391 end 392 win = win + 1 393 end 394 return false 395 end 396 397 local function set_options() 398 vim.bo.swapfile = false 399 vim.bo.buftype = 'nofile' 400 vim.bo.bufhidden = 'unload' 401 vim.bo.modified = false 402 vim.bo.readonly = true 403 vim.bo.modifiable = false 404 vim.bo.filetype = 'man' 405 end 406 407 --- Always use -l if possible. #6683 408 --- @type boolean? 409 local localfile_arg 410 411 --- @param path string 412 --- @param silent boolean? 413 --- @return string 414 local function get_page(path, silent) 415 -- Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065). 416 -- Soft-wrap: ftplugin/man.lua sets wrap/breakindent/…. 417 -- Hard-wrap: driven by `man`. 418 local manwidth --- @type integer 419 if (vim.g.man_hardwrap or 1) ~= 1 then 420 manwidth = 999 421 elseif vim.env.MANWIDTH then 422 vim.env.MANWIDTH = tonumber(vim.env.MANWIDTH) or 0 423 manwidth = math.min(vim.env.MANWIDTH, api.nvim_win_get_width(0) - vim.o.wrapmargin) 424 else 425 manwidth = api.nvim_win_get_width(0) - vim.o.wrapmargin 426 end 427 428 if localfile_arg == nil then 429 local mpath = get_path('man') 430 -- Check for -l support. 431 localfile_arg = (mpath and system({ 'man', '-l', mpath }, true, { MANPAGER = 'cat' }) or '') 432 ~= '' 433 end 434 435 local cmd = localfile_arg and { 'man', '-l', path } or { 'man', path } 436 437 -- Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db). 438 -- http://comments.gmane.org/gmane.editors.vim.devel/29085 439 -- Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces. 440 return system(cmd, silent, { 441 MANPAGER = 'cat', 442 MANWIDTH = manwidth, 443 MAN_KEEP_FORMATTING = 1, 444 }) 445 end 446 447 --- @param path string 448 --- @param psect string 449 local function format_candidate(path, psect) 450 if vim.endswith(path, '.pdf') or vim.endswith(path, '.in') then 451 -- invalid extensions 452 return '' 453 end 454 local name, sect = parse_path(path) 455 if sect == psect then 456 return name 457 elseif sect:match(psect .. '.+$') then -- invalid extensions 458 -- We include the section if the user provided section is a prefix 459 -- of the actual section. 460 return ('%s(%s)'):format(name, sect) 461 end 462 return '' 463 end 464 465 --- @param name string 466 --- @param sect? string 467 --- @return string[] paths 468 --- @return string? err 469 local function get_paths(name, sect) 470 -- Try several sources for getting the list man directories: 471 -- 1. `manpath -q` 472 -- 2. `man -w` (works on most systems) 473 -- 3. $MANPATH 474 -- 475 -- Note we prefer `manpath -q` because `man -w`: 476 -- - does not work on MacOS 14 and later. 477 -- - only returns '/usr/bin/man' on MacOS 13 and earlier. 478 --- @type string? 479 local mandirs_raw = vim.F.npcall(system, { 'manpath', '-q' }) 480 or vim.F.npcall(system, { 'man', '-w' }) 481 or vim.env.MANPATH 482 483 if not mandirs_raw then 484 return {}, "Could not determine man directories from: 'man -w', 'manpath' or $MANPATH" 485 end 486 487 local mandirs = table.concat(vim.split(mandirs_raw, '[:\n]', { trimempty = true }), ',') 488 489 sect = sect or '' 490 491 --- @type string[] 492 local paths = fn.globpath(mandirs, 'man[^\\/]*/' .. name .. '*.' .. sect .. '*', false, true) 493 494 -- Prioritize the result from find_path as it obeys b:man_default_sects. 495 local first = M._find_path(name, sect) 496 if first then 497 --- @param v string 498 paths = vim.tbl_filter(function(v) 499 return v ~= first 500 end, paths) 501 table.insert(paths, 1, first) 502 end 503 504 return paths 505 end 506 507 --- @param arg_lead string 508 --- @param cmd_line string 509 --- @return string? sect 510 --- @return string? psect 511 --- @return string? name 512 local function parse_cmdline(arg_lead, cmd_line) 513 local args = vim.split(cmd_line, '%s+', { trimempty = true }) 514 local cmd_offset = fn.index(args, 'Man') 515 if cmd_offset > 0 then 516 -- Prune all arguments up to :Man itself. Otherwise modifier commands like 517 -- :tab, :vertical, etc. would lead to a wrong length. 518 args = vim.list_slice(args, cmd_offset + 1) 519 end 520 521 if #args > 3 then 522 return 523 end 524 525 if #args == 1 then 526 -- returning full completion is laggy. Require some arg_lead to complete 527 -- return '', '', '' 528 return 529 end 530 531 if arg_lead:match('^[^()]+%([^()]*$') then 532 -- cursor (|) is at ':Man printf(|' or ':Man 1 printf(|' 533 -- The later is is allowed because of ':Man pri<TAB>'. 534 -- It will offer 'priclass.d(1m)' even though section is specified as 1. 535 local tmp = vim.split(arg_lead, '(', { plain = true }) 536 local name = tmp[1] 537 -- See extract_sect_and_name_ref on why :lower() 538 local sect = (tmp[2] or ''):lower() 539 return sect, '', name 540 end 541 542 if not args[2]:match('^[^()]+$') then 543 -- cursor (|) is at ':Man 3() |' or ':Man (3|' or ':Man 3() pri|' 544 -- or ':Man 3() pri |' 545 return 546 end 547 548 if #args == 2 then 549 --- @type string, string 550 local name, sect 551 if arg_lead == '' then 552 -- cursor (|) is at ':Man 1 |' 553 name = '' 554 sect = args[1]:lower() 555 else 556 -- cursor (|) is at ':Man pri|' 557 if arg_lead:match('/') then 558 -- if the name is a path, complete files 559 -- TODO(nhooyr) why does this complete the last one automatically 560 return fn.glob(arg_lead .. '*', false, true) 561 end 562 name = arg_lead 563 sect = '' 564 end 565 return sect, sect, name 566 end 567 568 if not arg_lead:match('[^()]+$') then 569 -- cursor (|) is at ':Man 3 printf |' or ':Man 3 (pr)i|' 570 return 571 end 572 573 -- cursor (|) is at ':Man 3 pri|' 574 local name, sect = arg_lead, args[2]:lower() 575 return sect, sect, name 576 end 577 578 --- @param arg_lead string 579 --- @param cmd_line string 580 function M.man_complete(arg_lead, cmd_line) 581 local sect, psect, name = parse_cmdline(arg_lead, cmd_line) 582 if not (sect and psect and name) then 583 return {} 584 end 585 586 local ok, pages = pcall(get_paths, name, sect) 587 if not ok then 588 return nil 589 end 590 591 -- We check for duplicates in case the same manpage in different languages 592 -- was found. 593 local pages_fmt = {} --- @type string[] 594 local pages_fmt_keys = {} --- @type table<string,true> 595 for _, v in ipairs(pages) do 596 local x = format_candidate(v, psect) 597 local xl = x:lower() -- ignore case when searching avoiding duplicates 598 if not pages_fmt_keys[xl] then 599 pages_fmt[#pages_fmt + 1] = x 600 pages_fmt_keys[xl] = true 601 end 602 end 603 table.sort(pages_fmt) 604 605 return pages_fmt 606 end 607 608 --- @param pattern string 609 --- @return {name:string,filename:string,cmd:string}[] 610 function M.goto_tag(pattern, _, _) 611 local name, sect, err = parse_ref(pattern) 612 if err then 613 error(err) 614 end 615 616 local paths, err2 = get_paths(assert(name), sect) 617 if err2 then 618 error(err2) 619 end 620 621 --- @type table[] 622 local ret = {} 623 624 for _, path in ipairs(paths) do 625 local pname, psect = parse_path(path) 626 ret[#ret + 1] = { 627 name = pname, 628 filename = ('man://%s(%s)'):format(pname, psect), 629 cmd = '1', 630 } 631 end 632 633 return ret 634 end 635 636 --- Called when Nvim is invoked as $MANPAGER. 637 function M.init_pager() 638 if fn.getline(1):match('^%s*$') then 639 api.nvim_buf_set_lines(0, 0, 1, false, {}) 640 else 641 vim.cmd('keepjumps 1') 642 end 643 highlight_man_page() 644 -- Guess the ref from the heading (which is usually uppercase, so we cannot 645 -- know the correct casing, cf. `man glDrawArraysInstanced`). 646 --- @type string 647 local ref = (fn.getline(1):match('^[^)]+%)') or ''):gsub(' ', '_') 648 local _, sect, err = pcall(parse_ref, ref) 649 vim.b.man_sect = err ~= nil and sect or '' 650 651 local man_bufname = 'man://' .. fn.fnameescape(ref):lower() 652 653 -- Raw manpage into (:Man!) overlooks `match('man://')` condition, 654 -- so if the buffer already exists, create new with a non existing name. 655 if fn.bufexists(man_bufname) == 1 then 656 local new_bufname = man_bufname 657 for i = 1, 100 do 658 if fn.bufexists(new_bufname) == 0 then 659 break 660 end 661 new_bufname = ('%s?new=%s'):format(man_bufname, i) 662 end 663 vim.cmd.file({ new_bufname, mods = { silent = true } }) 664 elseif not fn.bufname('%'):match('man://') then -- Avoid duplicate buffers, E95. 665 vim.cmd.file({ man_bufname, mods = { silent = true } }) 666 end 667 668 set_options() 669 end 670 671 --- Combine the name and sect into a manpage reference so that all 672 --- verification/extraction can be kept in a single function. 673 --- @param args string[] 674 --- @return string? ref 675 local function ref_from_args(args) 676 if #args <= 1 then 677 return args[1] 678 elseif args[1]:match('^%d$') or args[1]:match('^%d%a') or args[1]:match('^%a$') then 679 -- NB: Valid sections are not only digits, but also: 680 -- - <digit><word> (see POSIX mans), 681 -- - and even <letter> and <word> (see, for example, by tcl/tk) 682 -- NB2: don't optimize to :match("^%d"), as it will match manpages like 683 -- 441toppm and others whose name starts with digit 684 local sect = args[1] 685 table.remove(args, 1) 686 local name = table.concat(args, ' ') 687 return ('%s(%s)'):format(name, sect) 688 end 689 690 return table.concat(args, ' ') 691 end 692 693 --- @param count integer 694 --- @param args string[] 695 --- @return string? err 696 function M.open_page(count, smods, args) 697 local ref = ref_from_args(args) 698 if not ref then 699 ref = vim.bo.filetype == 'man' and fn.expand('<cWORD>') or fn.expand('<cword>') 700 if ref == '' then 701 return 'no identifier under cursor' 702 end 703 end 704 705 local name, sect, err = parse_ref(ref) 706 if err then 707 return err 708 end 709 assert(name) 710 711 if count >= 0 then 712 sect = tostring(count) 713 end 714 715 -- Try both spaces and underscores, use the first that exists. 716 local path = M._find_path(name, sect) 717 if not path then 718 --- Replace spaces in a man page name with underscores 719 --- intended for PostgreSQL, which has man pages like 'CREATE_TABLE(7)'; 720 --- while editing SQL source code, it's nice to visually select 'CREATE TABLE' 721 --- and hit 'K', which requires this transformation 722 path = M._find_path(name:gsub('%s', '_'), sect) 723 if not path then 724 return 'no manual entry for ' .. name 725 end 726 end 727 728 name, sect = parse_path(path) 729 local buf = api.nvim_get_current_buf() 730 local save_tfu = vim.bo[buf].tagfunc 731 vim.bo[buf].tagfunc = "v:lua.require'man'.goto_tag" 732 733 local target = ('%s(%s)'):format(name, sect) 734 735 local ok, ret = pcall(function() 736 smods.silent = true 737 smods.keepalt = true 738 if smods.hide or (smods.tab == -1 and find_man()) then 739 vim.cmd.tag({ target, mods = smods }) 740 else 741 vim.cmd.stag({ target, mods = smods }) 742 end 743 end) 744 745 if api.nvim_buf_is_valid(buf) then 746 vim.bo[buf].tagfunc = save_tfu 747 end 748 749 if not ok then 750 error(ret) 751 end 752 set_options() 753 754 vim.b.man_sect = sect 755 end 756 757 --- Called when a man:// buffer is opened. 758 --- @return string? err 759 function M.read_page(ref) 760 local name, sect, err = parse_ref(ref) 761 if err then 762 return err 763 end 764 765 local path = M._find_path(name, sect) 766 if not path then 767 return 'no manual entry for ' .. name 768 end 769 770 local _, sect1 = parse_path(path) 771 local page = get_page(path) 772 773 vim.b.man_sect = sect1 774 vim.bo.modifiable = true 775 vim.bo.readonly = false 776 vim.bo.swapfile = false 777 778 api.nvim_buf_set_lines(0, 0, -1, false, vim.split(page, '\n')) 779 780 while fn.getline(1):match('^%s*$') do 781 api.nvim_buf_set_lines(0, 0, 1, false, {}) 782 end 783 -- XXX: nroff justifies text by filling it with whitespace. That interacts 784 -- badly with our use of $MANWIDTH=999. Hack around this by using a fixed 785 -- size for those whitespace regions. 786 -- Use try/catch to avoid setting v:errmsg. 787 vim.cmd([[ 788 try 789 keeppatterns keepjumps %s/\s\{199,}/\=repeat(' ', 10)/g 790 catch 791 endtry 792 ]]) 793 vim.cmd('1') -- Move cursor to first line 794 highlight_man_page() 795 set_options() 796 end 797 798 function M.show_toc() 799 local bufnr = api.nvim_get_current_buf() 800 local bufname = api.nvim_buf_get_name(bufnr) 801 local info = fn.getloclist(0, { winid = 1 }) 802 if info ~= '' and vim.w[info.winid].qf_toc == bufname then 803 vim.cmd.lopen() 804 return 805 end 806 807 --- @type {bufnr:integer, lnum:integer, text:string}[] 808 local toc = {} 809 810 local lnum = 2 811 local last_line = fn.line('$') - 1 812 while lnum > 0 and lnum < last_line do 813 local text = fn.getline(lnum) 814 if text:match('^%s+[-+]%S') or text:match('^ %S') or text:match('^%S') then 815 toc[#toc + 1] = { 816 bufnr = bufnr, 817 lnum = lnum, 818 text = text:gsub('^%s+', ''):gsub('%s+$', ''), 819 } 820 end 821 lnum = fn.nextnonblank(lnum + 1) 822 end 823 824 fn.setloclist(0, toc, ' ') 825 fn.setloclist(0, {}, 'a', { title = 'Table of contents' }) 826 vim.cmd.lopen() 827 vim.w.qf_toc = bufname 828 -- reload syntax file after setting qf_toc variable 829 vim.bo.filetype = 'qf' 830 end 831 832 return M