man.lua 24 KB

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