1-- Functions to use neovim as a pager.
2
3-- This code is a rewrite of two sources: vimcat and vimpager (which also
4-- conatins a version of vimcat).
5-- Vimcat back to Matthew J. Wozniski and can be found at
6-- https://github.com/godlygeek/vim-files/blob/master/macros/vimcat.sh
7-- Vimpager was written by Rafael Kitover and can be found at
8-- https://github.com/rkitover/vimpager
9
10-- Information about terminal escape codes:
11-- https://en.wikipedia.org/wiki/ANSI_escape_code
12
13-- Neovim defines this object but luacheck doesn't know it.  So we define a
14-- shortcut and tell luacheck to ignore it.
15local nvim = vim.api -- luacheck: ignore
16local vim = vim      -- luacheck: ignore
17
18-- names that will be exported from this module
19local nvimpager = {
20  -- user facing options
21  maps = true,  -- if the default mappings should be defined
22}
23
24-- A mapping of ansi color numbers to neovim color names
25local colors = {
26  [0] = "black",     [8] = "darkgray",
27  [1] = "red",       [9] = "lightred",
28  [2] = "green",     [10] = "lightgreen",
29  [3] = "yellow",    [11] = "lightyellow",
30  [4] = "blue",      [12] = "lightblue",
31  [5] = "magenta",   [13] = "lightmagenta",
32  [6] = "cyan",      [14] = "lightcyan",
33  [7] = "lightgray", [15] = "white",
34}
35
36-- the names of neovim's highlighting attributes that are handled by this
37-- module
38-- Most attributes are refered to by their highlighting attribute name in
39-- neovim's :highlight command.
40local attributes = {
41  [1] = "bold",
42  --[2] = "faint", -- not handled by neovim
43  [3] = "italic",
44  [4] = "underline",
45  --[5] = "slow blink", -- not handled by neovim
46  --[6] = "underline", -- not handled by neovim
47  [7] = "reverse",
48  [8] = "conceal",
49  [9] = "strikethrough",
50  -- TODO when to use the gui attribute "standout"?
51}
52
53-- These variables will be initialized during the first call to cat_mode() or
54-- pager_mode().
55--
56-- A cache to map syntax groups to ansi escape sequences in cat mode or
57-- remember defined syntax groups in the ansi rendering functions.
58local cache = {}
59-- A local copy of the termguicolors option, used for color output in cat
60-- mode.
61local colors_24_bit
62local color2escape
63-- This variable holds the name of the detected parent process for pager mode.
64local doc
65-- A neovim highlight namespace to group together all highlights added to
66-- buffers by this module.
67local namespace
68
69-- Split a 24 bit color number into the three red, green and blue values
70local function split_rgb_number(color_number)
71  -- The lua implementation of these bit shift operations is taken from
72  -- http://nova-fusion.com/2011/03/21/simulate-bitwise-shift-operators-in-lua
73  local r = math.floor(color_number / 2 ^ 16)
74  local g = math.floor(math.floor(color_number / 2 ^ 8) % 2 ^ 8)
75  local b = math.floor(color_number % 2 ^ 8)
76  return r, g, b
77end
78
79local function hexformat_rgb_numbers(r, g, b)
80  return string.format("#%06x", r * 2^16 + g * 2^8 + b)
81end
82
83local function split_predifined_terminal_color(color_number)
84  local r = math.floor(color_number / 36)
85  local g = math.floor(math.floor(color_number / 6) % 6)
86  local b = math.floor(color_number % 6)
87  local lookup = {[0]=0, [1]=95, [2]=135, [3]=175, [4]=215, [5]=255}
88  return lookup[r], lookup[g], lookup[b]
89end
90
91-- Compute the escape sequences for a 24 bit color number.
92local function color2escape_24bit(color_number, foreground)
93  local red, green, blue = split_rgb_number(color_number)
94  local escape
95  if foreground then
96    escape = '38;2;'
97  else
98    escape = '48;2;'
99  end
100  return escape .. red .. ';' .. green .. ';' .. blue
101end
102
103-- Compute the escape sequences for a 8 bit color number.
104local function color2escape_8bit(color_number, foreground)
105  local prefix
106  if color_number < 8 then
107    if foreground then
108      prefix = '3'
109    else
110      prefix = '4'
111    end
112  elseif color_number < 16 then
113    color_number = color_number - 8
114    if foreground then
115      prefix = '9'
116    else
117      prefix = '10'
118    end
119  elseif foreground then
120    prefix = '38;5;'
121  else
122    prefix = '48;5;'
123  end
124  return prefix .. color_number
125end
126
127-- Compute a ansi escape sequences to render a syntax group on the terminal.
128local function group2ansi(groupid)
129  if cache[groupid] then
130    return cache[groupid]
131  end
132  local info = nvim.nvim_get_hl_by_id(groupid, colors_24_bit)
133  if info.reverse then
134    info.foreground, info.background = info.background, info.foreground
135  end
136  -- Reset all attributes before setting new ones.  The vimscript version did
137  -- use sevel explicit reset codes: 22, 24, 25, 27 and 28.  If no foreground
138  -- or background color was defined for a syntax item they were reset with
139  -- 39 or 49.
140  local escape = '\27[0'
141
142  if info.bold then escape = escape .. ';1' end
143  if info.italic then escape = escape .. ';3' end
144  if info.underline then escape = escape .. ';4' end
145
146  if info.foreground then
147    escape = escape .. ';' .. color2escape(info.foreground, true)
148  end
149  if info.background then
150    escape = escape .. ';' .. color2escape(info.background, false)
151  end
152
153  escape = escape .. 'm'
154  cache[groupid] = escape
155  return escape
156end
157
158-- Initialize some module level variables for cat mode.
159local function init_cat_mode()
160  -- Initialize the ansi group to color cache with the "Normal" hl group.
161  cache[0] = group2ansi(nvim.nvim_call_function('hlID', {'Normal'}))
162  -- Get the value of &termguicolors from neovim.
163  colors_24_bit = nvim.nvim_get_option('termguicolors')
164  -- Select the correct coloe escaping function.
165  if colors_24_bit then
166    color2escape = color2escape_24bit
167  else
168    color2escape = color2escape_8bit
169  end
170end
171
172-- Check if the begining of the current buffer contains ansi escape sequences.
173local function check_escape_sequences()
174  local filetype = nvim.nvim_buf_get_option(0, 'filetype')
175  if filetype == '' or filetype == 'text' then
176    for _, line in ipairs(nvim.nvim_buf_get_lines(0, 0, 100, false)) do
177      if line:find('\27%[[;?]*[0-9.;]*[A-Za-z]') ~= nil then return true end
178    end
179  end
180  return false
181end
182
183-- Savely get the listchars option on different nvim versions
184--
185-- From release 0.4.3 to 0.4.4 the listchars option was changed from window
186-- local to global-local.  This affects the calls to either
187-- nvim_win_get_option or nvim_get_option so that there is no save way to call
188-- just one in all versions.
189--
190-- returns: string -- the listchars value
191local function get_listchars()
192  -- this works for newer versions of neovim
193  local status, data = pcall(nvim.nvim_get_option, 'listchars')
194  if status then return data end
195  -- this works for old neovim versions
196  return nvim.nvim_win_get_option(0, 'listchars')
197end
198
199-- turn a listchars string into a table
200local function parse_listchars(listchars)
201  local t = {}
202  for item in vim.gsplit(listchars, ",", true) do
203    local kv = vim.split(item, ":", true)
204    t[kv[1]] = kv[2]
205  end
206  return t
207end
208
209-- Iterate through the current buffer and print it to stdout with terminal
210-- color codes for highlighting.
211local function highlight()
212  -- Detect an empty buffer, see :help line2byte().  We can not use
213  -- nvim_buf_get_lines as the table will contain one empty string for both an
214  -- empty file and a file with just one empty line.
215  if nvim.nvim_buf_line_count(0) == 1 and
216    nvim.nvim_call_function("line2byte", {2}) == -1 then
217    return
218  elseif check_escape_sequences() then
219    for _, line in ipairs(nvim.nvim_buf_get_lines(0, 0, -1, false)) do
220      io.write(line, '\n')
221    end
222    return
223  end
224  local conceallevel = nvim.nvim_win_get_option(0, 'conceallevel')
225  local syntax_id_conceal = nvim.nvim_call_function('hlID', {'Conceal'})
226  local syntax_id_whitespace = nvim.nvim_call_function('hlID', {'Whitespace'})
227  local syntax_id_non_text = nvim.nvim_call_function('hlID', {'NonText'})
228  local list = nvim.nvim_win_get_option(0, "list")
229  local listchars = list and parse_listchars(get_listchars()) or {}
230  local last_syntax_id = -1
231  local last_conceal_id = -1
232  local linecount = nvim.nvim_buf_line_count(0)
233  for lnum, line in ipairs(nvim.nvim_buf_get_lines(0, 0, -1, false)) do
234    local outline = ''
235    local skip_next_char = false
236    local syntax_id
237    for cnum = 1, line:len() do
238      local conceal_info = nvim.nvim_call_function('synconcealed',
239	{lnum, cnum})
240      local conceal = conceal_info[1] == 1
241      local replace = conceal_info[2]
242      local conceal_id = conceal_info[3]
243      if skip_next_char then
244	skip_next_char = false
245      elseif conceal and last_conceal_id == conceal_id then -- luacheck: ignore
246	-- skip this char
247      else
248	local append
249	if conceal then
250	  syntax_id = syntax_id_conceal
251	  if replace == '' and conceallevel == 1 then replace = ' ' end
252	  append = replace
253	  last_conceal_id = conceal_id
254	else
255	  append = line:sub(cnum, cnum)
256	  if list and string.find(" \194", append, 1, true) ~= nil then
257	    syntax_id = syntax_id_whitespace
258	    if append == " " then
259	      if line:find("^ +$", cnum) ~= nil then
260		append = listchars.trail or listchars.space or append
261	      else
262		append = listchars.space or append
263	      end
264	    elseif append == "\194" and line:sub(cnum + 1, cnum + 1) == "\160" then
265	      -- Utf8 non breaking space is "\194\160", neovim represents all
266	      -- files as utf8 internally, regardless of the actual encoding.
267	      -- See :help 'encoding'.
268	      append = listchars.nbsp or "\194\160"
269	      skip_next_char = true
270	    end
271	  else
272	    syntax_id = nvim.nvim_call_function('synID', {lnum, cnum, true})
273	  end
274	end
275	if syntax_id ~= last_syntax_id then
276	  outline = outline .. group2ansi(syntax_id)
277	  last_syntax_id = syntax_id
278	end
279	outline = outline .. append
280      end
281    end
282    -- append a eol listchar if &list is set
283    if list and listchars.eol ~= nil then
284      syntax_id = syntax_id_non_text
285      if syntax_id ~= last_syntax_id then
286	outline = outline .. group2ansi(syntax_id)
287	last_syntax_id = syntax_id
288      end
289      outline = outline .. listchars.eol
290    end
291    -- Write the whole line and a newline char.  If this was the last line
292    -- also reset the terminal attributes.
293    io.write(outline, lnum == linecount and cache[0] or '', '\n')
294  end
295end
296
297-- Call the highlight function to write the highlighted version of all buffers
298-- to stdout and quit nvim.
299function nvimpager.cat_mode()
300  init_cat_mode()
301  highlight()
302  -- We can not use nvim_list_bufs() as a file might appear on the command
303  -- line twice.  In this case we want to behave like cat(1) and display the
304  -- file twice.
305  for _ = 2, nvim.nvim_call_function('argc', {}) do
306    nvim.nvim_command('next')
307    highlight()
308  end
309  nvim.nvim_command('quitall!')
310end
311
312-- Replace a string prefix in all items in a list
313local function replace_prefix(table, old_prefix, new_prefix)
314  -- Escape all punctuation chars to protect from lua pattern chars.
315  old_prefix = old_prefix:gsub('[^%w]', '%%%0')
316  for index, value in ipairs(table) do
317    table[index] = value:gsub('^' .. old_prefix, new_prefix, 1)
318  end
319  return table
320end
321
322-- Fix the runtimepath.  All user nvim folders are replaced by corresponding
323-- nvimpager folders.
324local function fix_runtime_path()
325  local runtimepath = nvim.nvim_list_runtime_paths()
326  -- Remove the custom nvimpager entry that was added on the command line.
327  runtimepath[#runtimepath] = nil
328  local new
329  for _, name in ipairs({"config", "data"}) do
330    local original = nvim.nvim_call_function("stdpath", {name})
331    new = original .."pager"
332    runtimepath = replace_prefix(runtimepath, original, new)
333  end
334  runtimepath = table.concat(runtimepath, ",")
335  nvim.nvim_set_option("packpath", runtimepath)
336  runtimepath = os.getenv("RUNTIME") .. "," .. runtimepath
337  nvim.nvim_set_option("runtimepath", runtimepath)
338  new = new .. '/rplugin.vim'
339  nvim.nvim_command("let $NVIM_RPLUGIN_MANIFEST = '" .. new .. "'")
340end
341
342-- Parse the command of the calling process to detect some common
343-- documentation programs (man, pydoc, perldoc, git, ...).  $PARENT was
344-- exported by the calling bash script and points to the calling program.
345local function detect_parent_process()
346  local ppid = os.getenv('PARENT')
347  if not ppid then return nil end
348  local proc = nvim.nvim_get_proc(tonumber(ppid))
349  if proc == nil then return 'none' end
350  local command = proc.name
351  if command == 'man' then
352    return 'man'
353  elseif command:find('^[Pp]ython[0-9.]*') ~= nil or
354	 command:find('^[Pp]ydoc[0-9.]*') ~= nil then
355    return 'pydoc'
356  elseif command == 'ruby' or command == 'irb' or command == 'ri' then
357    return 'ri'
358  elseif command == 'perl' or command == 'perldoc' then
359    return 'perldoc'
360  elseif command == 'git' then
361    return 'git'
362  end
363  return nil
364end
365
366-- Search the begining of the current buffer to detect if it contains a man
367-- page.
368local function detect_man_page_in_current_buffer()
369  -- Only check the first twelve lines (for speed).
370  for _, line in ipairs(nvim.nvim_buf_get_lines(0, 0, 12, false)) do
371    -- Check if the line contains the string "NAME" or "NAME" with every
372    -- character overwritten by itself.
373    -- An earlier version of this code did also check if there are whitespace
374    -- characters at the end of the line.  I could not find a man pager where
375    -- this was the case.
376    -- FIXME This only works for man pages in languages where "NAME" is used
377    -- as the headline.  Some (not all!) German man pages use "BBEZEICHNUNG"
378    -- instead.
379    if line == 'NAME' or line == 'N\bNA\bAM\bME\bE' or line == "Name"
380      or line == 'N\bNa\bam\bme\be' then
381      return true
382    end
383  end
384  return false
385end
386
387-- Remove ansi escape sequences from the current buffer.
388local function strip_ansi_escape_sequences_from_current_buffer()
389  local modifiable = nvim.nvim_buf_get_option(0, "modifiable")
390  nvim.nvim_buf_set_option(0, "modifiable", true)
391  nvim.nvim_command(
392    [=[keepjumps silent %substitute/\v\e\[[;?]*[0-9.;]*[a-z]//egi]=])
393  nvim.nvim_win_set_cursor(0, {1, 0})
394  nvim.nvim_buf_set_option(0, "modifiable", modifiable)
395end
396
397-- Detect possible filetypes for the current buffer by looking at the pstree
398-- or ansi escape sequences or manpage sequences in the current buffer.
399local function detect_filetype()
400  if not doc and detect_man_page_in_current_buffer() then doc = 'man' end
401  if doc == 'git' then
402    -- Use nvim's syntax highlighting for git buffers instead of git's
403    -- internal highlighting.
404    strip_ansi_escape_sequences_from_current_buffer()
405  end
406  if doc == 'man' then
407    nvim.nvim_buf_set_option(0, 'readonly', false)
408    nvim.nvim_command("Man!")
409    nvim.nvim_buf_set_option(0, 'readonly', true)
410  elseif doc == 'pydoc' or doc == 'perldoc' or doc == 'ri' then
411    doc = 'man' -- only set the syntax, not the full :Man plugin
412  end
413  if doc ~= nil then
414    nvim.nvim_buf_set_option(0, 'filetype', doc)
415  end
416end
417
418-- Create an iterator that tokenizes the given input string into ansi escape
419-- sequences.
420--
421-- Lua patterns for string.gmatch
422local function tokenize(input_string)
423  -- The empty input string is a special case where we return one single
424  -- token.
425  if input_string == "" then return string.gmatch("", "") end
426  -- we keep track of the position in the input with a local variable so that
427  -- our "next" function does not need to rely on the second argument.
428  -- Especially if a token appears twice in the input that might be of
429  -- importance.
430  local position = 1
431  local function next(input)
432    -- If the position we are currently tokenizing is beyond the input string
433    -- return nil => stop tokenizing.
434    if input:len() < position then return nil end
435
436    -- If we are on the last character and it is a semicolon, return an empty
437    -- token and move position beyond the input to stop on the next call.
438    -- This is hard to handle properly in the tokenizer below.
439    if input:len() == position and input:sub(-1) == ";" then
440      position = position + 1
441      return ""
442    end
443
444    -- first check for the special sequences "38;" "48;"
445    local init = input:sub(position, position+2)
446    if init == "38;" or init == "48;" then
447      -- Try to match an 8 bit or a 24 bit color sequence
448      local patterns = {"([34])8;5;(%d+);?", "([34])8;2;(%d+);(%d+);(%d+);?"}
449      for _, pattern in ipairs(patterns) do
450	local start, stop, token, c1, c2, c3 = input:find(pattern, position)
451	if start == position then
452	  position = stop + 1
453	  return token == "3" and "foreground" or "background", c1, c2, c3
454	end
455      end
456      -- If no valid special sequence was found we fall through to the normal
457      -- tokenization.
458    end
459
460    -- handle all other tokens, we expect a simple number followed by either a
461    -- semicolon or the end of the string, or the end of the input string
462    -- directly.
463    local oldpos = position
464    local next_pos = input:find(";", position)
465    if next_pos == nil then
466      -- no further semicolon was found, we reached the end of the input
467      -- string, the next call to this function will return nil
468      position = input:len() + 1
469      return input:sub(oldpos, -1)
470    else
471      position = next_pos
472      -- We only skip the semicolon if it was not at the end of the input
473      -- string.
474      if next_pos < input:len() then
475	position = next_pos + 1
476      end
477      return input:sub(oldpos, next_pos - 1)
478    end
479  end
480
481  return next, input_string, nil
482end
483
484local state = {
485  -- The line and column where the currently described state starts
486  line = 1,
487  column = 1,
488}
489
490function state.clear(self)
491  self.foreground = ""
492  self.background = ""
493  self.ctermfg = ""
494  self.ctermbg = ""
495  for _, k in pairs(attributes) do self[k] = false end
496end
497
498function state.state2highlight_group_name(self)
499  if self.conceal then return "NvimPagerConceal" end
500  local name = "NvimPagerFG_" .. self.foreground:gsub("#", "") ..
501	       "_BG_" .. self.background:gsub("#", "")
502  for _, field in pairs(attributes) do
503    if self[field] then
504      name = name .. "_" .. field
505    end
506  end
507  return name
508end
509
510function state.parse(self, string)
511  for token, c1, c2, c3 in tokenize(string) do
512    -- First we check for 256 colors and 24 bit color sequences.
513    if c3 ~= nil then
514	self[token] = hexformat_rgb_numbers(tonumber(c1), tonumber(c2),
515					    tonumber(c3))
516    elseif c1 ~= nil then
517      self:parse8bit(token, c1)
518      self["cterm"..token:sub(1, 1).."g"] = tonumber(c1)
519    else
520      if token == "" then token = 0 else token = tonumber(token) end
521      if token == 0 then
522	self:clear()
523      elseif token == 1 or token == 3 or token == 4 or token == 7
524	  or token == 8 or token == 9 then
525	-- 2, 5 and 6 could be handled here if they were supported.
526	self[attributes[token]] = true
527      elseif token == 21 then
528	-- 22 means "doubley underline" or "bold off", we could implement
529	-- doubley underline by undercurl.
530	--self.undercurl = true
531      elseif token == 22 then
532	self.bold = false
533	--self.faint = false
534      elseif token == 23 or token == 24 or token == 27 or token == 28
535	  or token == 29 then
536	-- 25 means blink off so it could also be handled here if it was
537	-- supported.
538	self[attributes[token - 20]] = false
539      elseif token >= 30 and token <= 37 then -- foreground color
540	self.foreground = colors[token - 30]
541	self.ctermfg = token - 30
542      elseif token == 39 then -- reset foreground
543	self.foreground = ""
544	self.ctermfg = ""
545      elseif token >= 40 and token <= 47 then -- background color
546	self.background = colors[token - 40]
547	self.ctermbg = token - 40
548      elseif token == 49 then -- reset background
549	self.background = ""
550	self.ctermbg = ""
551      elseif token >= 90 and token <= 97 then -- bright foreground color
552	self.foreground = colors[token - 82]
553      elseif token >= 100 and token <= 107 then -- bright background color
554	self.background = colors[token - 92]
555      end
556    end
557  end
558end
559
560function state.parse8bit(self, fgbg, color)
561  local colornr = tonumber(color)
562  if colornr >= 0 and colornr <= 7 then
563    color = colors[colornr]
564  elseif colornr >= 8 and colornr <= 15 then -- high pallet colors
565    color = colors[colornr] -- + 82 + 10 * (fgbg == "background" and 1 or 0)
566  elseif colornr >= 16 and colornr <= 231 then -- color cube
567    color = hexformat_rgb_numbers(split_predifined_terminal_color(colornr-16))
568  else -- grayscale ramp
569    colornr = 8 + 10 * (colornr - 232)
570    color = hexformat_rgb_numbers(colornr, colornr, colornr)
571  end
572  self[fgbg] = ""..color
573end
574
575function state.compute_highlight_command(self, groupname)
576  local args = ""
577  if self.foreground ~= "" then
578    args = args.." guifg="..self.foreground
579    if self.ctermfg ~= "" then
580      args = args .. " ctermfg=" .. self.ctermfg
581    end
582  end
583  if self.background ~= "" then
584    args = args.." guibg="..self.background
585    if self.ctermbg ~= "" then
586      args = args .. " ctermbg=" .. self.ctermbg
587    end
588  end
589  local attrs = ""
590  for _, key in pairs(attributes) do
591    if self[key] then
592      attrs = attrs .. "," .. key
593    end
594  end
595  attrs = attrs:sub(2)
596  if attrs ~= "" then
597    args = args .. " gui=" .. attrs .. " cterm=" .. attrs
598  end
599  if args == "" then
600    return "highlight default link " .. groupname .. " Normal"
601  else
602    return "highlight default " .. groupname .. args
603  end
604end
605
606-- Wrapper around nvim_buf_add_highlight to fix index offsets
607--
608-- The function nvim_buf_add_highlight expects 0 based line numbers and column
609-- numbers.  Set the start column to 0, the end column to -1 if not given.
610local function add_highlight(groupname, line, from, to)
611  local line_0 = line - 1
612  local from_0 = (from or 1) - 1
613  local to_0 = (to or 0) - 1
614  nvim.nvim_buf_add_highlight(0, namespace, groupname, line_0, from_0, to_0)
615end
616
617function state.render(self, from_line, from_column, to_line, to_column)
618  if from_line == to_line and from_column == to_column then
619    return
620  end
621  local groupname = self:state2highlight_group_name()
622  -- check if the hl group already exists
623  if cache[groupname] == nil then
624    nvim.nvim_command(self:compute_highlight_command(groupname))
625    cache[groupname] = true
626  end
627
628  if from_line == to_line then
629    add_highlight(groupname, from_line, from_column, to_column)
630  else
631    add_highlight(groupname, from_line, from_column)
632    for line = from_line+1, to_line-1 do
633      add_highlight(groupname, line)
634    end
635    add_highlight(groupname, to_line, 1, to_column)
636  end
637end
638
639-- Parse the current buffer for ansi escape sequences and add buffer
640-- highlights to the buffer instead.
641local function ansi2highlight()
642  nvim.nvim_command(
643    "syntax match NvimPagerEscapeSequence conceal '\\e\\[[0-9;]*m'")
644  nvim.nvim_command("highlight NvimPagerConceal gui=NONE guisp=NONE " ..
645		    "guifg=background guibg=background")
646  nvim.nvim_win_set_option(0, "conceallevel", 3)
647  nvim.nvim_win_set_option(0, "concealcursor", "nv")
648  local pattern = "\27%[([0-9;]*)m"
649  state:clear()
650  namespace = nvim.nvim_create_namespace("")
651  for lnum, line in ipairs(nvim.nvim_buf_get_lines(0, 0, -1, false)) do
652    local start, end_, spec
653    local col = 1
654    repeat
655      start, end_, spec = line:find(pattern, col)
656      if start ~= nil then
657	state:render(state.line, state.column, lnum, start)
658	state.line = lnum
659	state.column = end_
660	state:parse(spec)
661	-- update the position to find the next match in the line
662	col = end_
663      end
664    until start == nil
665  end
666end
667
668-- Set up mappings to make nvim behave a little more like a pager.
669local function set_maps()
670  local function map(mode, lhs, rhs)
671    nvim.nvim_set_keymap(mode, lhs, rhs, {noremap = true})
672    nvim.nvim_buf_set_keymap(0, mode, lhs, rhs, {noremap = true})
673  end
674  map('n', 'q', '<CMD>quitall!<CR>')
675  map('v', 'q', '<CMD>quitall!<CR>')
676  map('n', '<Space>', '<PageDown>')
677  map('n', '<S-Space>', '<PageUp>')
678  map('n', 'g', 'gg')
679  map('n', '<Up>', '<C-Y>')
680  map('n', '<Down>', '<C-E>')
681  map('n', 'k', '<C-Y>')
682  map('n', 'j', '<C-E>')
683end
684
685-- Setup function for the VimEnter autocmd.
686-- This function will be called for each buffer once
687function nvimpager.pager_mode()
688  if check_escape_sequences() then
689    -- Try to highlight ansi escape sequences.
690    ansi2highlight()
691    -- Lines with concealed ansi esc sequences seem shorter than they are (by
692    -- character count) so it looks like they wrap to early and the concealing
693    -- of escape sequences only works for the first &synmaxcol chars.
694    nvim.nvim_buf_set_option(0, "synmaxcol", 0) -- unlimited
695    nvim.nvim_win_set_option(0, "wrap", false)
696  end
697  nvim.nvim_buf_set_option(0, 'modifiable', false)
698  nvim.nvim_buf_set_option(0, 'modified', false)
699end
700
701-- Setup function to be called from --cmd.
702function nvimpager.stage1()
703  fix_runtime_path()
704  -- Don't remember file names and positions
705  nvim.nvim_set_option('shada', '')
706  -- prevent messages when opening files (especially for the cat version)
707  nvim.nvim_set_option('shortmess', nvim.nvim_get_option('shortmess')..'F')
708  -- Define autocmd group for nvimpager.
709  nvim.nvim_command('augroup NvimPager')
710  nvim.nvim_command('  autocmd!')
711  nvim.nvim_command('augroup END')
712  doc = detect_parent_process()
713  if doc == 'git' then
714    -- We disable modelines for this buffer as they could disturb the git
715    -- highlighting in diffs.
716    nvim.nvim_buf_set_option(0, 'modeline', false)
717    nvim.nvim_set_option('modelines', 0)
718  end
719  -- Theoretically these options only affect the pager mode so they could also
720  -- be set in stage2() but that would overwrite user settings from the init
721  -- file.
722  nvim.nvim_set_option('mouse', 'a')
723  nvim.nvim_set_option('laststatus', 0)
724end
725
726-- Set up autocomands to start the correct mode after startup or for each
727-- file.  This function assumes that in "cat mode" we are called with
728-- --headless and hence do not have a user interface.  This also means that
729-- this function can only be called with -c or later as the user interface
730-- would not be available in --cmd.
731function nvimpager.stage2()
732  detect_filetype()
733  local mode, events
734  if #nvim.nvim_list_uis() == 0 then
735    mode, events = 'cat', 'VimEnter'
736  else
737    if nvimpager.maps then
738      set_maps()
739    end
740    mode, events = 'pager', 'VimEnter,BufWinEnter'
741  end
742  -- The "nested" in these autocomands enables nested executions of
743  -- autocomands inside the *_mode() functions.  See :h autocmd-nested, for
744  -- compatibility with nvim < 0.4 we use "nested" and not "++nested".
745  nvim.nvim_command(
746    'autocmd NvimPager '..events..' * nested lua nvimpager.'..mode..'_mode()')
747end
748
749-- functions only exported for tests
750nvimpager._testable = {
751  color2escape_24bit = color2escape_24bit,
752  color2escape_8bit = color2escape_8bit,
753  detect_parent_process = detect_parent_process,
754  group2ansi = group2ansi,
755  hexformat_rgb_numbers = hexformat_rgb_numbers,
756  init_cat_mode = init_cat_mode,
757  replace_prefix = replace_prefix,
758  split_predifined_terminal_color = split_predifined_terminal_color,
759  split_rgb_number = split_rgb_number,
760  state = state,
761  tokenize = tokenize,
762}
763
764return nvimpager
765