man.lua 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. local api, fn = vim.api, vim.fn
  2. local find_arg = '-w'
  3. local localfile_arg = true -- Always use -l if possible. #6683
  4. local buf_hls = {}
  5. local M = {}
  6. local function man_error(msg)
  7. M.errormsg = 'man.lua: ' .. vim.inspect(msg)
  8. error(M.errormsg)
  9. end
  10. -- Run a system command and timeout after 30 seconds.
  11. local function man_system(cmd, silent)
  12. local stdout_data = {}
  13. local stderr_data = {}
  14. local stdout = vim.loop.new_pipe(false)
  15. local stderr = vim.loop.new_pipe(false)
  16. local done = false
  17. local exit_code
  18. local handle
  19. handle = vim.loop.spawn(cmd[1], {
  20. args = vim.list_slice(cmd, 2),
  21. stdio = { nil, stdout, stderr },
  22. }, function(code)
  23. exit_code = code
  24. stdout:close()
  25. stderr:close()
  26. handle:close()
  27. done = true
  28. end)
  29. if handle then
  30. stdout:read_start(function(_, data)
  31. stdout_data[#stdout_data + 1] = data
  32. end)
  33. stderr:read_start(function(_, data)
  34. stderr_data[#stderr_data + 1] = data
  35. end)
  36. else
  37. stdout:close()
  38. stderr:close()
  39. if not silent then
  40. man_error(string.format('command error: %s', table.concat(cmd)))
  41. end
  42. end
  43. vim.wait(30000, function()
  44. return done
  45. end)
  46. if not done then
  47. if handle then
  48. handle:close()
  49. stdout:close()
  50. stderr:close()
  51. end
  52. man_error(string.format('command timed out: %s', table.concat(cmd, ' ')))
  53. end
  54. if exit_code ~= 0 and not silent then
  55. man_error(
  56. string.format("command error '%s': %s", table.concat(cmd, ' '), table.concat(stderr_data))
  57. )
  58. end
  59. return table.concat(stdout_data)
  60. end
  61. local function highlight_line(line, linenr)
  62. local chars = {}
  63. local prev_char = ''
  64. local overstrike, escape = false, false
  65. local hls = {} -- Store highlight groups as { attr, start, final }
  66. local NONE, BOLD, UNDERLINE, ITALIC = 0, 1, 2, 3
  67. local hl_groups = { [BOLD] = 'manBold', [UNDERLINE] = 'manUnderline', [ITALIC] = 'manItalic' }
  68. local attr = NONE
  69. local byte = 0 -- byte offset
  70. local function end_attr_hl(attr_)
  71. for i, hl in ipairs(hls) do
  72. if hl.attr == attr_ and hl.final == -1 then
  73. hl.final = byte
  74. hls[i] = hl
  75. end
  76. end
  77. end
  78. local function add_attr_hl(code)
  79. local continue_hl = true
  80. if code == 0 then
  81. attr = NONE
  82. continue_hl = false
  83. elseif code == 1 then
  84. attr = BOLD
  85. elseif code == 22 then
  86. attr = BOLD
  87. continue_hl = false
  88. elseif code == 3 then
  89. attr = ITALIC
  90. elseif code == 23 then
  91. attr = ITALIC
  92. continue_hl = false
  93. elseif code == 4 then
  94. attr = UNDERLINE
  95. elseif code == 24 then
  96. attr = UNDERLINE
  97. continue_hl = false
  98. else
  99. attr = NONE
  100. return
  101. end
  102. if continue_hl then
  103. hls[#hls + 1] = { attr = attr, start = byte, final = -1 }
  104. else
  105. if attr == NONE then
  106. for a, _ in pairs(hl_groups) do
  107. end_attr_hl(a)
  108. end
  109. else
  110. end_attr_hl(attr)
  111. end
  112. end
  113. end
  114. -- Break input into UTF8 code points. ASCII code points (from 0x00 to 0x7f)
  115. -- can be represented in one byte. Any code point above that is represented by
  116. -- a leading byte (0xc0 and above) and continuation bytes (0x80 to 0xbf, or
  117. -- decimal 128 to 191).
  118. for char in line:gmatch('[^\128-\191][\128-\191]*') do
  119. if overstrike then
  120. local last_hl = hls[#hls]
  121. if char == prev_char then
  122. if char == '_' and attr == UNDERLINE and last_hl and last_hl.final == byte then
  123. -- This underscore is in the middle of an underlined word
  124. attr = UNDERLINE
  125. else
  126. attr = BOLD
  127. end
  128. elseif prev_char == '_' then
  129. -- char is underlined
  130. attr = UNDERLINE
  131. elseif prev_char == '+' and char == 'o' then
  132. -- bullet (overstrike text '+^Ho')
  133. attr = BOLD
  134. char = '·'
  135. elseif prev_char == '·' and char == 'o' then
  136. -- bullet (additional handling for '+^H+^Ho^Ho')
  137. attr = BOLD
  138. char = '·'
  139. else
  140. -- use plain char
  141. attr = NONE
  142. end
  143. -- Grow the previous highlight group if possible
  144. if last_hl and last_hl.attr == attr and last_hl.final == byte then
  145. last_hl.final = byte + #char
  146. else
  147. hls[#hls + 1] = { attr = attr, start = byte, final = byte + #char }
  148. end
  149. overstrike = false
  150. prev_char = ''
  151. byte = byte + #char
  152. chars[#chars + 1] = char
  153. elseif escape then
  154. -- Use prev_char to store the escape sequence
  155. prev_char = prev_char .. char
  156. -- We only want to match against SGR sequences, which consist of ESC
  157. -- followed by '[', then a series of parameter and intermediate bytes in
  158. -- the range 0x20 - 0x3f, then 'm'. (See ECMA-48, sections 5.4 & 8.3.117)
  159. local sgr = prev_char:match('^%[([\032-\063]*)m$')
  160. -- Ignore escape sequences with : characters, as specified by ITU's T.416
  161. -- Open Document Architecture and interchange format.
  162. if sgr and not string.find(sgr, ':') then
  163. local match
  164. while sgr and #sgr > 0 do
  165. -- Match against SGR parameters, which may be separated by ';'
  166. match, sgr = sgr:match('^(%d*);?(.*)')
  167. add_attr_hl(match + 0) -- coerce to number
  168. end
  169. escape = false
  170. elseif not prev_char:match('^%[[\032-\063]*$') then
  171. -- Stop looking if this isn't a partial CSI sequence
  172. escape = false
  173. end
  174. elseif char == '\027' then
  175. escape = true
  176. prev_char = ''
  177. elseif char == '\b' then
  178. overstrike = true
  179. prev_char = chars[#chars]
  180. byte = byte - #prev_char
  181. chars[#chars] = nil
  182. else
  183. byte = byte + #char
  184. chars[#chars + 1] = char
  185. end
  186. end
  187. for _, hl in ipairs(hls) do
  188. if hl.attr ~= NONE then
  189. buf_hls[#buf_hls + 1] = {
  190. 0,
  191. -1,
  192. hl_groups[hl.attr],
  193. linenr - 1,
  194. hl.start,
  195. hl.final,
  196. }
  197. end
  198. end
  199. return table.concat(chars, '')
  200. end
  201. local function highlight_man_page()
  202. local mod = vim.bo.modifiable
  203. vim.bo.modifiable = true
  204. local lines = api.nvim_buf_get_lines(0, 0, -1, false)
  205. for i, line in ipairs(lines) do
  206. lines[i] = highlight_line(line, i)
  207. end
  208. api.nvim_buf_set_lines(0, 0, -1, false, lines)
  209. for _, args in ipairs(buf_hls) do
  210. api.nvim_buf_add_highlight(unpack(args))
  211. end
  212. buf_hls = {}
  213. vim.bo.modifiable = mod
  214. end
  215. -- replace spaces in a man page name with underscores
  216. -- intended for PostgreSQL, which has man pages like 'CREATE_TABLE(7)';
  217. -- while editing SQL source code, it's nice to visually select 'CREATE TABLE'
  218. -- and hit 'K', which requires this transformation
  219. local function spaces_to_underscores(str)
  220. local res = str:gsub('%s', '_')
  221. return res
  222. end
  223. local function get_path(sect, name, silent)
  224. name = name or ''
  225. sect = sect or ''
  226. -- Some man implementations (OpenBSD) return all available paths from the
  227. -- search command. Previously, this function would simply select the first one.
  228. --
  229. -- However, some searches will report matches that are incorrect:
  230. -- man -w strlen may return string.3 followed by strlen.3, and therefore
  231. -- selecting the first would get us the wrong page. Thus, we must find the
  232. -- first matching one.
  233. --
  234. -- There's yet another special case here. Consider the following:
  235. -- If you run man -w strlen and string.3 comes up first, this is a problem. We
  236. -- should search for a matching named one in the results list.
  237. -- However, if you search for man -w clock_gettime, you will *only* get
  238. -- clock_getres.2, which is the right page. Searching the resuls for
  239. -- clock_gettime will no longer work. In this case, we should just use the
  240. -- first one that was found in the correct section.
  241. --
  242. -- Finally, we can avoid relying on -S or -s here since they are very
  243. -- inconsistently supported. Instead, call -w with a section and a name.
  244. local cmd
  245. if sect == '' then
  246. cmd = { 'man', find_arg, name }
  247. else
  248. cmd = { 'man', find_arg, sect, name }
  249. end
  250. local lines = man_system(cmd, silent)
  251. if lines == nil then
  252. return nil
  253. end
  254. local results = vim.split(lines, '\n', { trimempty = true })
  255. if #results == 0 then
  256. return
  257. end
  258. -- find any that match the specified name
  259. local namematches = vim.tbl_filter(function(v)
  260. return fn.fnamemodify(v, ':t'):match(name)
  261. end, results) or {}
  262. local sectmatches = {}
  263. if #namematches > 0 and sect ~= '' then
  264. sectmatches = vim.tbl_filter(function(v)
  265. return fn.fnamemodify(v, ':e') == sect
  266. end, namematches)
  267. end
  268. return fn.substitute(sectmatches[1] or namematches[1] or results[1], [[\n\+$]], '', '')
  269. end
  270. local function matchstr(text, pat_or_re)
  271. local re = type(pat_or_re) == 'string' and vim.regex(pat_or_re) or pat_or_re
  272. local s, e = re:match_str(text)
  273. if s == nil then
  274. return
  275. end
  276. return text:sub(vim.str_utfindex(text, s) + 1, vim.str_utfindex(text, e))
  277. end
  278. -- attempt to extract the name and sect out of 'name(sect)'
  279. -- otherwise just return the largest string of valid characters in ref
  280. local function extract_sect_and_name_ref(ref)
  281. ref = ref or ''
  282. if ref:sub(1, 1) == '-' then -- try ':Man -pandoc' with this disabled.
  283. man_error("manpage name cannot start with '-'")
  284. end
  285. local ref1 = ref:match('[^()]+%([^()]+%)')
  286. if not ref1 then
  287. local name = ref:match('[^()]+')
  288. if not name then
  289. man_error('manpage reference cannot contain only parentheses: ' .. ref)
  290. end
  291. return '', spaces_to_underscores(name)
  292. end
  293. local parts = vim.split(ref1, '(', { plain = true })
  294. -- see ':Man 3X curses' on why tolower.
  295. -- TODO(nhooyr) Not sure if this is portable across OSs
  296. -- but I have not seen a single uppercase section.
  297. local sect = vim.split(parts[2] or '', ')', { plain = true })[1]:lower()
  298. local name = spaces_to_underscores(parts[1])
  299. return sect, name
  300. end
  301. -- verify_exists attempts to find the path to a manpage
  302. -- based on the passed section and name.
  303. --
  304. -- 1. If manpage could not be found with the given sect and name,
  305. -- then try all the sections in b:man_default_sects.
  306. -- 2. If it still could not be found, then we try again without a section.
  307. -- 3. If still not found but $MANSECT is set, then we try again with $MANSECT
  308. -- unset.
  309. local function verify_exists(sect, name)
  310. if sect and sect ~= '' then
  311. local ret = get_path(sect, name, true)
  312. if ret then
  313. return ret
  314. end
  315. end
  316. if vim.b.man_default_sects ~= nil then
  317. local sects = vim.split(vim.b.man_default_sects, ',', { plain = true, trimempty = true })
  318. for _, sec in ipairs(sects) do
  319. local ret = get_path(sec, name, true)
  320. if ret then
  321. return ret
  322. end
  323. end
  324. end
  325. -- if none of the above worked, we will try with no section
  326. local res_empty_sect = get_path('', name, true)
  327. if res_empty_sect then
  328. return res_empty_sect
  329. end
  330. -- if that still didn't work, we will check for $MANSECT and try again with it
  331. -- unset
  332. if vim.env.MANSECT then
  333. local mansect = vim.env.MANSECT
  334. vim.env.MANSECT = nil
  335. local res = get_path('', name, true)
  336. vim.env.MANSECT = mansect
  337. if res then
  338. return res
  339. end
  340. end
  341. -- finally, if that didn't work, there is no hope
  342. man_error('no manual entry for ' .. name)
  343. end
  344. local EXT_RE = vim.regex([[\.\%([glx]z\|bz2\|lzma\|Z\)$]])
  345. -- Extracts the name/section from the 'path/name.sect', because sometimes the actual section is
  346. -- more specific than what we provided to `man` (try `:Man 3 App::CLI`).
  347. -- Also on linux, name seems to be case-insensitive. So for `:Man PRIntf`, we
  348. -- still want the name of the buffer to be 'printf'.
  349. local function extract_sect_and_name_path(path)
  350. local tail = fn.fnamemodify(path, ':t')
  351. if EXT_RE:match_str(path) then -- valid extensions
  352. tail = fn.fnamemodify(tail, ':r')
  353. end
  354. local name, sect = tail:match('^(.+)%.([^.]+)$')
  355. return sect, name
  356. end
  357. local function find_man()
  358. local win = 1
  359. while win <= fn.winnr('$') do
  360. local buf = fn.winbufnr(win)
  361. if vim.bo[buf].filetype == 'man' then
  362. vim.cmd(win .. 'wincmd w')
  363. return true
  364. end
  365. win = win + 1
  366. end
  367. return false
  368. end
  369. local function set_options(pager)
  370. vim.bo.swapfile = false
  371. vim.bo.buftype = 'nofile'
  372. vim.bo.bufhidden = 'hide'
  373. vim.bo.modified = false
  374. vim.bo.readonly = true
  375. vim.bo.modifiable = false
  376. vim.b.pager = pager
  377. vim.bo.filetype = 'man'
  378. end
  379. local function get_page(path, silent)
  380. -- Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065).
  381. -- Soft-wrap: ftplugin/man.lua sets wrap/breakindent/….
  382. -- Hard-wrap: driven by `man`.
  383. local manwidth
  384. if (vim.g.man_hardwrap or 1) ~= 1 then
  385. manwidth = 999
  386. elseif vim.env.MANWIDTH then
  387. manwidth = vim.env.MANWIDTH
  388. else
  389. manwidth = api.nvim_win_get_width(0)
  390. end
  391. -- Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db).
  392. -- http://comments.gmane.org/gmane.editors.vim.devel/29085
  393. -- Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces.
  394. local cmd = { 'env', 'MANPAGER=cat', 'MANWIDTH=' .. manwidth, 'MAN_KEEP_FORMATTING=1', 'man' }
  395. if localfile_arg then
  396. cmd[#cmd + 1] = '-l'
  397. end
  398. cmd[#cmd + 1] = path
  399. return man_system(cmd, silent)
  400. end
  401. local function put_page(page)
  402. vim.bo.modifiable = true
  403. vim.bo.readonly = false
  404. vim.bo.swapfile = false
  405. api.nvim_buf_set_lines(0, 0, -1, false, vim.split(page, '\n'))
  406. while fn.getline(1):match('^%s*$') do
  407. api.nvim_buf_set_lines(0, 0, 1, false, {})
  408. end
  409. -- XXX: nroff justifies text by filling it with whitespace. That interacts
  410. -- badly with our use of $MANWIDTH=999. Hack around this by using a fixed
  411. -- size for those whitespace regions.
  412. vim.cmd([[silent! keeppatterns keepjumps %s/\s\{199,}/\=repeat(' ', 10)/g]])
  413. vim.cmd('1') -- Move cursor to first line
  414. highlight_man_page()
  415. set_options(false)
  416. end
  417. local function format_candidate(path, psect)
  418. if matchstr(path, [[\.\%(pdf\|in\)$]]) then -- invalid extensions
  419. return ''
  420. end
  421. local sect, name = extract_sect_and_name_path(path)
  422. if sect == psect then
  423. return name
  424. elseif sect and name and matchstr(sect, psect .. '.\\+$') then -- invalid extensions
  425. -- We include the section if the user provided section is a prefix
  426. -- of the actual section.
  427. return ('%s(%s)'):format(name, sect)
  428. end
  429. return ''
  430. end
  431. local function get_paths(sect, name, do_fallback)
  432. -- callers must try-catch this, as some `man` implementations don't support `s:find_arg`
  433. local ok, ret = pcall(function()
  434. local mandirs =
  435. table.concat(vim.split(man_system({ 'man', find_arg }), '[:\n]', { trimempty = true }), ',')
  436. local paths = fn.globpath(mandirs, 'man?/' .. name .. '*.' .. sect .. '*', false, true)
  437. pcall(function()
  438. -- Prioritize the result from verify_exists as it obeys b:man_default_sects.
  439. local first = verify_exists(sect, name)
  440. paths = vim.tbl_filter(function(v)
  441. return v ~= first
  442. end, paths)
  443. paths = { first, unpack(paths) }
  444. end)
  445. return paths
  446. end)
  447. if not ok then
  448. if not do_fallback then
  449. error(ret)
  450. end
  451. -- Fallback to a single path, with the page we're trying to find.
  452. ok, ret = pcall(verify_exists, sect, name)
  453. return { ok and ret or nil }
  454. end
  455. return ret or {}
  456. end
  457. local function complete(sect, psect, name)
  458. local pages = get_paths(sect, name, false)
  459. -- We remove duplicates in case the same manpage in different languages was found.
  460. return fn.uniq(fn.sort(vim.tbl_map(function(v)
  461. return format_candidate(v, psect)
  462. end, pages) or {}, 'i'))
  463. end
  464. -- see extract_sect_and_name_ref on why tolower(sect)
  465. function M.man_complete(arg_lead, cmd_line, _)
  466. local args = vim.split(cmd_line, '%s+', { trimempty = true })
  467. local cmd_offset = fn.index(args, 'Man')
  468. if cmd_offset > 0 then
  469. -- Prune all arguments up to :Man itself. Otherwise modifier commands like
  470. -- :tab, :vertical, etc. would lead to a wrong length.
  471. args = vim.list_slice(args, cmd_offset + 1)
  472. end
  473. if #args > 3 then
  474. return {}
  475. end
  476. if #args == 1 then
  477. -- returning full completion is laggy. Require some arg_lead to complete
  478. -- return complete('', '', '')
  479. return {}
  480. end
  481. if arg_lead:match('^[^()]+%([^()]*$') then
  482. -- cursor (|) is at ':Man printf(|' or ':Man 1 printf(|'
  483. -- The later is is allowed because of ':Man pri<TAB>'.
  484. -- It will offer 'priclass.d(1m)' even though section is specified as 1.
  485. local tmp = vim.split(arg_lead, '(', { plain = true })
  486. local name = tmp[1]
  487. local sect = (tmp[2] or ''):lower()
  488. return complete(sect, '', name)
  489. end
  490. if not args[2]:match('^[^()]+$') then
  491. -- cursor (|) is at ':Man 3() |' or ':Man (3|' or ':Man 3() pri|'
  492. -- or ':Man 3() pri |'
  493. return {}
  494. end
  495. if #args == 2 then
  496. local name, sect
  497. if arg_lead == '' then
  498. -- cursor (|) is at ':Man 1 |'
  499. name = ''
  500. sect = args[1]:lower()
  501. else
  502. -- cursor (|) is at ':Man pri|'
  503. if arg_lead:match('/') then
  504. -- if the name is a path, complete files
  505. -- TODO(nhooyr) why does this complete the last one automatically
  506. return fn.glob(arg_lead .. '*', false, true)
  507. end
  508. name = arg_lead
  509. sect = ''
  510. end
  511. return complete(sect, sect, name)
  512. end
  513. if not arg_lead:match('[^()]+$') then
  514. -- cursor (|) is at ':Man 3 printf |' or ':Man 3 (pr)i|'
  515. return {}
  516. end
  517. -- cursor (|) is at ':Man 3 pri|'
  518. local name = arg_lead
  519. local sect = args[2]:lower()
  520. return complete(sect, sect, name)
  521. end
  522. function M.goto_tag(pattern, _, _)
  523. local sect, name = extract_sect_and_name_ref(pattern)
  524. local paths = get_paths(sect, name, true)
  525. local structured = {}
  526. for _, path in ipairs(paths) do
  527. sect, name = extract_sect_and_name_path(path)
  528. if sect and name then
  529. structured[#structured + 1] = {
  530. name = name,
  531. title = name .. '(' .. sect .. ')',
  532. }
  533. end
  534. end
  535. if vim.o.cscopetag then
  536. -- return only a single entry so we work well with :cstag (#11675)
  537. structured = { structured[1] }
  538. end
  539. return vim.tbl_map(function(entry)
  540. return {
  541. name = entry.name,
  542. filename = 'man://' .. entry.title,
  543. cmd = '1',
  544. }
  545. end, structured)
  546. end
  547. -- Called when Nvim is invoked as $MANPAGER.
  548. function M.init_pager()
  549. if fn.getline(1):match('^%s*$') then
  550. api.nvim_buf_set_lines(0, 0, 1, false, {})
  551. else
  552. vim.cmd('keepjumps 1')
  553. end
  554. highlight_man_page()
  555. -- Guess the ref from the heading (which is usually uppercase, so we cannot
  556. -- know the correct casing, cf. `man glDrawArraysInstanced`).
  557. local ref = fn.substitute(matchstr(fn.getline(1), [[^[^)]\+)]]) or '', ' ', '_', 'g')
  558. local ok, res = pcall(extract_sect_and_name_ref, ref)
  559. vim.b.man_sect = ok and res or ''
  560. if not fn.bufname('%'):match('man://') then -- Avoid duplicate buffers, E95.
  561. vim.cmd.file({ 'man://' .. fn.fnameescape(ref):lower(), mods = { silent = true } })
  562. end
  563. set_options(true)
  564. end
  565. function M.open_page(count, smods, args)
  566. if #args > 2 then
  567. man_error('too many arguments')
  568. end
  569. local ref
  570. if #args == 0 then
  571. ref = vim.bo.filetype == 'man' and fn.expand('<cWORD>') or fn.expand('<cword>')
  572. if ref == '' then
  573. man_error('no identifier under cursor')
  574. end
  575. elseif #args == 1 then
  576. ref = args[1]
  577. else
  578. -- Combine the name and sect into a manpage reference so that all
  579. -- verification/extraction can be kept in a single function.
  580. -- If args[2] is a reference as well, that is fine because it is the only
  581. -- reference that will match.
  582. ref = ('%s(%s)'):format(args[2], args[1])
  583. end
  584. local sect, name = extract_sect_and_name_ref(ref)
  585. if count >= 0 then
  586. sect = tostring(count)
  587. end
  588. local path = verify_exists(sect, name)
  589. sect, name = extract_sect_and_name_path(path)
  590. local buf = fn.bufnr()
  591. local save_tfu = vim.bo[buf].tagfunc
  592. vim.bo[buf].tagfunc = "v:lua.require'man'.goto_tag"
  593. local target = ('%s(%s)'):format(name, sect)
  594. local ok, ret = pcall(function()
  595. if smods.tab == -1 and find_man() then
  596. vim.cmd.tag({ target, mods = { silent = true, keepalt = true } })
  597. else
  598. smods.silent = true
  599. smods.keepalt = true
  600. vim.cmd.stag({ target, mods = smods })
  601. end
  602. end)
  603. vim.bo[buf].tagfunc = save_tfu
  604. if not ok then
  605. error(ret)
  606. else
  607. set_options(false)
  608. end
  609. vim.b.man_sect = sect
  610. end
  611. -- Called when a man:// buffer is opened.
  612. function M.read_page(ref)
  613. local sect, name = extract_sect_and_name_ref(ref)
  614. local path = verify_exists(sect, name)
  615. sect = extract_sect_and_name_path(path)
  616. local page = get_page(path)
  617. vim.b.man_sect = sect
  618. put_page(page)
  619. end
  620. function M.show_toc()
  621. local bufname = fn.bufname('%')
  622. local info = fn.getloclist(0, { winid = 1 })
  623. if info ~= '' and vim.w[info.winid].qf_toc == bufname then
  624. vim.cmd.lopen()
  625. return
  626. end
  627. local toc = {}
  628. local lnum = 2
  629. local last_line = fn.line('$') - 1
  630. local section_title_re = vim.regex([[^\%( \{3\}\)\=\S.*$]])
  631. local flag_title_re = vim.regex([[^\s\+\%(+\|-\)\S\+]])
  632. while lnum and lnum < last_line do
  633. local text = fn.getline(lnum)
  634. if section_title_re:match_str(text) then
  635. -- if text is a section title
  636. toc[#toc + 1] = {
  637. bufnr = fn.bufnr('%'),
  638. lnum = lnum,
  639. text = text,
  640. }
  641. elseif flag_title_re:match_str(text) then
  642. -- if text is a flag title. we strip whitespaces and prepend two
  643. -- spaces to have a consistent format in the loclist.
  644. toc[#toc + 1] = {
  645. bufnr = fn.bufnr('%'),
  646. lnum = lnum,
  647. text = ' ' .. fn.substitute(text, [[^\s*\(.\{-}\)\s*$]], [[\1]], ''),
  648. }
  649. end
  650. lnum = fn.nextnonblank(lnum + 1)
  651. end
  652. fn.setloclist(0, toc, ' ')
  653. fn.setloclist(0, {}, 'a', { title = 'Man TOC' })
  654. vim.cmd.lopen()
  655. vim.w.qf_toc = bufname
  656. end
  657. local function init()
  658. local path = get_path('', 'man', true)
  659. local page
  660. if path ~= nil then
  661. -- Check for -l support.
  662. page = get_page(path, true)
  663. end
  664. if page == '' or page == nil then
  665. localfile_arg = false
  666. end
  667. end
  668. init()
  669. return M