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