From 0446d4d6916d27041de5ac24ba0c741ae4ad5a39 Mon Sep 17 00:00:00 2001 From: Gabriel Holodak Date: Fri, 10 Nov 2017 23:27:00 -0500 Subject: [PATCH 1/5] Highlight backspaced characters --- runtime/autoload/man.vim | 57 ++++++++++++++++++++++++++++++++++++---- runtime/syntax/man.vim | 12 +++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim index dd71ede680..3ea0b734c0 100644 --- a/runtime/autoload/man.vim +++ b/runtime/autoload/man.vim @@ -148,7 +148,8 @@ function! s:get_page(path) abort let manwidth = empty($MANWIDTH) ? winwidth(0) : $MANWIDTH " Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db). " http://comments.gmane.org/gmane.editors.vim.devel/29085 - let cmd = ['env', 'MANPAGER=cat', 'MANWIDTH='.manwidth, 'man'] + " Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces. + let cmd = ['env', 'MANPAGER=cat', 'MANWIDTH='.manwidth, 'MAN_KEEP_FORMATTING=1', 'man'] return s:system(cmd + (s:localfile_arg ? ['-l', a:path] : [a:path])) endfunction @@ -157,11 +158,10 @@ function! s:put_page(page) abort setlocal noreadonly silent keepjumps %delete _ silent put =a:page - " Remove all backspaced/escape characters. - execute 'silent keeppatterns keepjumps %substitute,.\b\|\e\[\d\+m,,e'.(&gdefault?'':'g') while getline(1) =~# '^\s*$' silent keepjumps 1delete _ endwhile + call man#highlight_backspaced_text() setlocal filetype=man endfunction @@ -370,13 +370,12 @@ function! s:format_candidate(path, psect) abort endfunction function! man#init_pager() abort - " Remove all backspaced/escape characters. - execute 'silent keeppatterns keepjumps %substitute,.\b\|\e\[\d\+m,,e'.(&gdefault?'':'g') if getline(1) =~# '^\s*$' silent keepjumps 1delete _ else keepjumps 1 endif + call man#highlight_backspaced_text() " This is not perfect. See `man glDrawArraysInstanced`. Since the title is " all caps it is impossible to tell what the original capitilization was. let ref = substitute(matchstr(getline(1), '^[^)]\+)'), ' ', '_', 'g') @@ -388,4 +387,52 @@ function! man#init_pager() abort execute 'silent file man://'.fnameescape(ref) endfunction +function! man#highlight_backspaced_text() abort + let l:modifiable = &modifiable + set modifiable + + let l:lines = getline(1, line('$')) + call map(l:lines, function('s:highlight_backspaced_line')) + call setline(1, l:lines) + + let &modifiable = l:modifiable +endfunction + +" This pattern is for "overstruck" text containing backspaces. It matches bold +" text first, so a word beginning with "_^H_" is bold and text such as +" "_^Hf_^Ho_^Ho_^H__^Hb_^Ha_^Hr" is entirely underlined. +" +" Bolded text can also be mixed with whitespace as a performance tweak, since +" it's visually identical. +let s:backspace_pattern = '\v%((.)\b\1\s*)+|%(_\b.)+' + +function! s:highlight_backspaced_line(index, val) abort + let l:line = a:val + let l:search_pos = 0 + + while 1 + " Scanning for the next backspace without matching the entire pattern is + " slightly faster + let l:match_start = stridx(l:line, "\b", l:search_pos) + if l:match_start == -1 + break + endif + + let l:match = matchstrpos(l:line, s:backspace_pattern, l:match_start - 1) + if l:match[0] =~# '^_\b[^_]' + let l:hlgroup = 'manUnderline' + else + let l:hlgroup = 'manBold' + endif + + let l:stripped = substitute(l:match[0], '.\b', '', 'g') + let l:search_pos = l:match[1] + len(l:stripped) + let l:line = strpart(l:line, 0, l:match[1]) . l:stripped . strpart(l:line, l:match[2]) + + call nvim_buf_add_highlight(0, -1, l:hlgroup, a:index, l:match[1], l:search_pos) + endwhile + + return l:line +endfunction + call s:init() diff --git a/runtime/syntax/man.vim b/runtime/syntax/man.vim index 0975b160ae..9eb613169c 100644 --- a/runtime/syntax/man.vim +++ b/runtime/syntax/man.vim @@ -18,6 +18,18 @@ highlight default link manOptionDesc Constant highlight default link manReference PreProc highlight default link manSubHeading Function +function! s:init_highlight_groups() + highlight default manUnderline cterm=underline gui=underline + highlight default manBold cterm=bold gui=bold +endfunction + +augroup man_init_highlight_groups + autocmd! + autocmd ColorScheme * call s:init_highlight_groups() +augroup END + +call s:init_highlight_groups() + if &filetype != 'man' " May have been included by some other filetype. finish From c28ce5f6191d233228d1688de300727e1ae42831 Mon Sep 17 00:00:00 2001 From: Gabriel Holodak Date: Mon, 20 Nov 2017 00:19:08 -0500 Subject: [PATCH 2/5] Switch to processing in Lua --- runtime/autoload/man.vim | 42 ++-------------------- runtime/lua/man.lua | 75 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 40 deletions(-) create mode 100644 runtime/lua/man.lua diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim index 3ea0b734c0..12c1bf3c86 100644 --- a/runtime/autoload/man.vim +++ b/runtime/autoload/man.vim @@ -391,48 +391,10 @@ function! man#highlight_backspaced_text() abort let l:modifiable = &modifiable set modifiable - let l:lines = getline(1, line('$')) - call map(l:lines, function('s:highlight_backspaced_line')) - call setline(1, l:lines) + lua man = require("man") + luado return man.highlight_backspaced(line, linenr) let &modifiable = l:modifiable endfunction -" This pattern is for "overstruck" text containing backspaces. It matches bold -" text first, so a word beginning with "_^H_" is bold and text such as -" "_^Hf_^Ho_^Ho_^H__^Hb_^Ha_^Hr" is entirely underlined. -" -" Bolded text can also be mixed with whitespace as a performance tweak, since -" it's visually identical. -let s:backspace_pattern = '\v%((.)\b\1\s*)+|%(_\b.)+' - -function! s:highlight_backspaced_line(index, val) abort - let l:line = a:val - let l:search_pos = 0 - - while 1 - " Scanning for the next backspace without matching the entire pattern is - " slightly faster - let l:match_start = stridx(l:line, "\b", l:search_pos) - if l:match_start == -1 - break - endif - - let l:match = matchstrpos(l:line, s:backspace_pattern, l:match_start - 1) - if l:match[0] =~# '^_\b[^_]' - let l:hlgroup = 'manUnderline' - else - let l:hlgroup = 'manBold' - endif - - let l:stripped = substitute(l:match[0], '.\b', '', 'g') - let l:search_pos = l:match[1] + len(l:stripped) - let l:line = strpart(l:line, 0, l:match[1]) . l:stripped . strpart(l:line, l:match[2]) - - call nvim_buf_add_highlight(0, -1, l:hlgroup, a:index, l:match[1], l:search_pos) - endwhile - - return l:line -endfunction - call s:init() diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua new file mode 100644 index 0000000000..63c2c14480 --- /dev/null +++ b/runtime/lua/man.lua @@ -0,0 +1,75 @@ +local function highlight_backspaced(line, linenr) + local chars = {} + local prev_char = '' + local overstrike = false + local hls = {} -- Store highlight groups as { attr, start, end } + local NONE, BOLD, UNDERLINE = 0, 1, 2 + local attr = NONE + local byte = 0 -- byte offset + + -- Break input into UTF8 characters + for char in line:gmatch("[^\128-\191][\128-\191]*") do + if overstrike then + local last_hl = hls[#hls] + if char == prev_char then + if char == '_' and attr == UNDERLINE and last_hl and last_hl[3] == byte then + -- This underscore is in the middle of an underlined word + attr = UNDERLINE + else + attr = BOLD + end + elseif prev_char == '_' then + -- char is underlined + attr = UNDERLINE + elseif prev_char == '+' and char == 'o' then + -- bullet (overstrike text '+^Ho') + attr = BOLD + char = [[·]] + elseif prev_char == [[·]] and char == 'o' then + -- bullet (additional handling for '+^H+^Ho^Ho') + attr = BOLD + char = [[·]] + else + -- use plain char + attr = NONE + end + + -- Grow the previous highlight group if possible + if last_hl and last_hl[1] == attr and last_hl[3] == byte then + last_hl[3] = byte + #char + else + hls[#hls + 1] = {attr, byte, byte + #char} + end + + overstrike = false + prev_char = '' + byte = byte + #char + chars[#chars + 1] = char + elseif char == "\b" then + overstrike = true + prev_char = chars[#chars] + byte = byte - #prev_char + chars[#chars] = nil + else + byte = byte + #char + chars[#chars + 1] = char + end + end + + for i, hl in ipairs(hls) do + if hl[1] ~= NONE then + vim.api.nvim_buf_add_highlight( + 0, + -1, + hl[1] == BOLD and "manBold" or "manUnderline", + linenr - 1, + hl[2], + hl[3] + ) + end + end + + return table.concat(chars, '') +end + +return { highlight_backspaced = highlight_backspaced } From 6740c94562cc1b271a6ea2458ebb847a6e3665e2 Mon Sep 17 00:00:00 2001 From: Gabriel Holodak Date: Thu, 30 Nov 2017 22:15:39 -0500 Subject: [PATCH 3/5] Add support for escape sequences --- runtime/autoload/man.vim | 8 ++--- runtime/lua/man.lua | 76 +++++++++++++++++++++++++++++++++++++--- runtime/syntax/man.vim | 1 + 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim index 12c1bf3c86..0a502be4c7 100644 --- a/runtime/autoload/man.vim +++ b/runtime/autoload/man.vim @@ -161,7 +161,7 @@ function! s:put_page(page) abort while getline(1) =~# '^\s*$' silent keepjumps 1delete _ endwhile - call man#highlight_backspaced_text() + call man#highlight_formatted_text() setlocal filetype=man endfunction @@ -375,7 +375,7 @@ function! man#init_pager() abort else keepjumps 1 endif - call man#highlight_backspaced_text() + call man#highlight_formatted_text() " This is not perfect. See `man glDrawArraysInstanced`. Since the title is " all caps it is impossible to tell what the original capitilization was. let ref = substitute(matchstr(getline(1), '^[^)]\+)'), ' ', '_', 'g') @@ -387,12 +387,12 @@ function! man#init_pager() abort execute 'silent file man://'.fnameescape(ref) endfunction -function! man#highlight_backspaced_text() abort +function! man#highlight_formatted_text() abort let l:modifiable = &modifiable set modifiable lua man = require("man") - luado return man.highlight_backspaced(line, linenr) + luado return man.highlight_formatted(line, linenr) let &modifiable = l:modifiable endfunction diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index 63c2c14480..c3d7df2a2a 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -1,12 +1,60 @@ -local function highlight_backspaced(line, linenr) +local function highlight_formatted(line, linenr) local chars = {} local prev_char = '' - local overstrike = false + local overstrike, escape = false, false local hls = {} -- Store highlight groups as { attr, start, end } - local NONE, BOLD, UNDERLINE = 0, 1, 2 + local NONE, BOLD, UNDERLINE, ITALIC = 0, 1, 2, 3 + local hl_groups = {[BOLD]="manBold", [UNDERLINE]="manUnderline", [ITALIC]="manItalic"} local attr = NONE local byte = 0 -- byte offset + local function end_attr_hl(attr) + for i, hl in ipairs(hls) do + if hl[1] == attr and hl[3] == -1 then + hl[3] = byte + hls[i] = hl + end + end + end + + local function add_attr_hl(code) + local on = true + if code == 0 then + attr = NONE + on = false + elseif code == 1 then + attr = BOLD + elseif code == 21 or code == 22 then + attr = BOLD + on = false + elseif code == 3 then + attr = ITALIC + elseif code == 23 then + attr = ITALIC + on = false + elseif code == 4 then + attr = UNDERLINE + elseif code == 24 then + attr = UNDERLINE + on = false + else + attr = NONE + return + end + + if on then + hls[#hls + 1] = {attr, byte, -1} + else + if attr == NONE then + for a, _ in pairs(hl_groups) do + end_attr_hl(a) + end + else + end_attr_hl(attr) + end + end + end + -- Break input into UTF8 characters for char in line:gmatch("[^\128-\191][\128-\191]*") do if overstrike then @@ -45,6 +93,24 @@ local function highlight_backspaced(line, linenr) prev_char = '' byte = byte + #char chars[#chars + 1] = char + elseif escape then + -- Use prev_char to store the escape sequence + prev_char = prev_char .. char + local sgr = prev_char:match("^%[([\020-\063]*)m$") + if sgr then + local match = '' + while sgr and #sgr > 0 do + match, sgr = sgr:match("^(%d*);?(.*)") + add_attr_hl(match + 0) -- coerce to number + end + escape = false + elseif not prev_char:match("^%[[\020-\063]*$") then + -- Stop looking if this isn't a partial CSI sequence + escape = false + end + elseif char == "\027" then + escape = true + prev_char = '' elseif char == "\b" then overstrike = true prev_char = chars[#chars] @@ -61,7 +127,7 @@ local function highlight_backspaced(line, linenr) vim.api.nvim_buf_add_highlight( 0, -1, - hl[1] == BOLD and "manBold" or "manUnderline", + hl_groups[hl[1]], linenr - 1, hl[2], hl[3] @@ -72,4 +138,4 @@ local function highlight_backspaced(line, linenr) return table.concat(chars, '') end -return { highlight_backspaced = highlight_backspaced } +return { highlight_formatted = highlight_formatted } diff --git a/runtime/syntax/man.vim b/runtime/syntax/man.vim index 9eb613169c..0544fb29e2 100644 --- a/runtime/syntax/man.vim +++ b/runtime/syntax/man.vim @@ -21,6 +21,7 @@ highlight default link manSubHeading Function function! s:init_highlight_groups() highlight default manUnderline cterm=underline gui=underline highlight default manBold cterm=bold gui=bold + highlight default manItalic cterm=italic gui=italic endfunction augroup man_init_highlight_groups From 134c0f0bdb50b16d443b103fc1a81c9ae5c7c017 Mon Sep 17 00:00:00 2001 From: Gabriel Holodak Date: Fri, 1 Dec 2017 01:06:35 -0500 Subject: [PATCH 4/5] Add functional tests for man highlighting --- test/functional/plugin/man_spec.lua | 181 ++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 test/functional/plugin/man_spec.lua diff --git a/test/functional/plugin/man_spec.lua b/test/functional/plugin/man_spec.lua new file mode 100644 index 0000000000..53384a9d47 --- /dev/null +++ b/test/functional/plugin/man_spec.lua @@ -0,0 +1,181 @@ +local helpers = require('test.functional.helpers')(after_each) +local plugin_helpers = require('test.functional.plugin.helpers') + +local Screen = require('test.functional.ui.screen') + +local buffer, command, eval = helpers.buffer, helpers.command, helpers.eval + +before_each(function() + plugin_helpers.reset() + helpers.clear() + command('syntax on') + command('set filetype=man') +end) + +describe('In autoload/man.vim', function() + describe('function man#highlight_formatted_text', function() + local screen + + before_each(function() + command('syntax off') -- Ignore syntax groups + screen = Screen.new(52, 5) + screen:attach() + end) + + after_each(function() + screen:detach() + end) + + local function expect(string) + screen:expect(string, + { + b = { bold = true }, + i = { italic = true }, + u = { underline = true }, + bi = { bold = true, italic = true }, + biu = { bold = true, italic = true, underline = true }, + }, + {{ bold = true, foreground = Screen.colors.Blue }}) + end + + local function expect_without_highlights(string) + screen:expect(string, nil, true) + end + + local function insert_lines(...) + buffer('set_lines', 0, 0, 1, false, { ... }) + end + + it('clears backspaces from text', function() + insert_lines( + "this i\bis\bs a\ba test", + "with _\bo_\bv_\be_\br_\bs_\bt_\br_\bu_\bc_\bk text" + ) + + expect_without_highlights([[ + ^this i^His^Hs a^Ha test | + with _^Ho_^Hv_^He_^Hr_^Hs_^Ht_^Hr_^Hu_^Hc_^Hk text | + ~ | + ~ | + | + ]]) + + eval('man#highlight_formatted_text()') + + expect_without_highlights([[ + ^this is a test | + with overstruck text | + ~ | + ~ | + | + ]]) + end) + + it('clears escape sequences from text', function() + insert_lines( + "this \027[1mis \027[3ma \027[4mtest\027[0m", + "\027[4mwith\027[24m \027[4mescaped\027[24m \027[4mtext\027[24m" + ) + + expect_without_highlights([[ + ^this ^[[1mis ^[[3ma ^[[4mtest^[[0m | + ^[[4mwith^[[24m ^[[4mescaped^[[24m ^[[4mtext^[[24m | + ~ | + ~ | + | + ]]) + + eval('man#highlight_formatted_text()') + + expect_without_highlights([[ + ^this is a test | + with escaped text | + ~ | + ~ | + | + ]]) + end) + + it('highlights overstruck text', function() + insert_lines( + "this i\bis\bs a\ba test", + "with _\bo_\bv_\be_\br_\bs_\bt_\br_\bu_\bc_\bk text" + ) + eval('man#highlight_formatted_text()') + + expect([[ + ^this {b:is} {b:a} test | + with {u:overstruck} text | + ~ | + ~ | + | + ]]) + end) + + it('highlights escape sequences in text', function() + insert_lines( + "this \027[1mis \027[3ma \027[4mtest\027[0m", + "\027[4mwith\027[24m \027[4mescaped\027[24m \027[4mtext\027[24m" + ) + eval('man#highlight_formatted_text()') + + expect([[ + ^this {b:is }{bi:a }{biu:test} | + {u:with} {u:escaped} {u:text} | + ~ | + ~ | + | + ]]) + end) + + it('highlights multibyte text', function() + insert_lines( + "this i\bis\bs あ\bあ test", + "with _\bö_\bv_\be_\br_\bs_\bt_\br_\bu_\bc_\bk te\027[3mxt¶\027[0m" + ) + eval('man#highlight_formatted_text()') + + expect([[ + ^this {b:is} {b:あ} test | + with {u:överstruck} te{i:xt¶} | + ~ | + ~ | + | + ]]) + end) + + it('highlights underscores based on context', function() + insert_lines( + "_\b_b\bbe\beg\bgi\bin\bns\bs", + "m\bmi\bid\bd_\b_d\bdl\ble\be", + "_\bm_\bi_\bd_\b__\bd_\bl_\be" + ) + eval('man#highlight_formatted_text()') + + expect([[ + {b:^_begins} | + {b:mid_dle} | + {u:mid_dle} | + ~ | + | + ]]) + end) + + it('highlights various bullet formats', function() + insert_lines( + "· ·\b·", + "+\bo", + "+\b+\bo\bo double" + ) + eval('man#highlight_formatted_text()') + + expect([[ + ^· {b:·} | + {b:·} | + {b:·} double | + ~ | + | + ]]) + end) + end) +end) From eb44519b5debf740f692bb4ea19ad83b29749484 Mon Sep 17 00:00:00 2001 From: Gabriel Holodak Date: Sun, 24 Dec 2017 12:16:58 -0500 Subject: [PATCH 5/5] Address PR comments --- runtime/autoload/man.vim | 14 +-- runtime/lua/man.lua | 87 ++++++++++------ runtime/syntax/man.vim | 15 +-- test/functional/plugin/man_spec.lua | 154 ++++++++++------------------ 4 files changed, 116 insertions(+), 154 deletions(-) diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim index 0a502be4c7..d20b8c05b0 100644 --- a/runtime/autoload/man.vim +++ b/runtime/autoload/man.vim @@ -161,7 +161,7 @@ function! s:put_page(page) abort while getline(1) =~# '^\s*$' silent keepjumps 1delete _ endwhile - call man#highlight_formatted_text() + lua require("man").highlight_man_page() setlocal filetype=man endfunction @@ -375,7 +375,7 @@ function! man#init_pager() abort else keepjumps 1 endif - call man#highlight_formatted_text() + lua require("man").highlight_man_page() " This is not perfect. See `man glDrawArraysInstanced`. Since the title is " all caps it is impossible to tell what the original capitilization was. let ref = substitute(matchstr(getline(1), '^[^)]\+)'), ' ', '_', 'g') @@ -387,14 +387,4 @@ function! man#init_pager() abort execute 'silent file man://'.fnameescape(ref) endfunction -function! man#highlight_formatted_text() abort - let l:modifiable = &modifiable - set modifiable - - lua man = require("man") - luado return man.highlight_formatted(line, linenr) - - let &modifiable = l:modifiable -endfunction - call s:init() diff --git a/runtime/lua/man.lua b/runtime/lua/man.lua index c3d7df2a2a..baa522f343 100644 --- a/runtime/lua/man.lua +++ b/runtime/lua/man.lua @@ -1,8 +1,10 @@ -local function highlight_formatted(line, linenr) +local buf_hls = {} + +local function highlight_line(line, linenr) local chars = {} local prev_char = '' local overstrike, escape = false, false - local hls = {} -- Store highlight groups as { attr, start, end } + local hls = {} -- Store highlight groups as { attr, start, final } local NONE, BOLD, UNDERLINE, ITALIC = 0, 1, 2, 3 local hl_groups = {[BOLD]="manBold", [UNDERLINE]="manUnderline", [ITALIC]="manItalic"} local attr = NONE @@ -10,40 +12,40 @@ local function highlight_formatted(line, linenr) local function end_attr_hl(attr) for i, hl in ipairs(hls) do - if hl[1] == attr and hl[3] == -1 then - hl[3] = byte + if hl.attr == attr and hl.final == -1 then + hl.final = byte hls[i] = hl end end end local function add_attr_hl(code) - local on = true + local continue_hl = true if code == 0 then attr = NONE - on = false + continue_hl = false elseif code == 1 then attr = BOLD - elseif code == 21 or code == 22 then + elseif code == 22 then attr = BOLD - on = false + continue_hl = false elseif code == 3 then attr = ITALIC elseif code == 23 then attr = ITALIC - on = false + continue_hl = false elseif code == 4 then attr = UNDERLINE elseif code == 24 then attr = UNDERLINE - on = false + continue_hl = false else attr = NONE return end - if on then - hls[#hls + 1] = {attr, byte, -1} + if continue_hl then + hls[#hls + 1] = {attr=attr, start=byte, final=-1} else if attr == NONE then for a, _ in pairs(hl_groups) do @@ -55,12 +57,15 @@ local function highlight_formatted(line, linenr) end end - -- Break input into UTF8 characters + -- Break input into UTF8 code points. ASCII code points (from 0x00 to 0x7f) + -- can be represented in one byte. Any code point above that is represented by + -- a leading byte (0xc0 and above) and continuation bytes (0x80 to 0xbf, or + -- decimal 128 to 191). for char in line:gmatch("[^\128-\191][\128-\191]*") do if overstrike then local last_hl = hls[#hls] if char == prev_char then - if char == '_' and attr == UNDERLINE and last_hl and last_hl[3] == byte then + if char == '_' and attr == UNDERLINE and last_hl and last_hl.final == byte then -- This underscore is in the middle of an underlined word attr = UNDERLINE else @@ -72,21 +77,21 @@ local function highlight_formatted(line, linenr) elseif prev_char == '+' and char == 'o' then -- bullet (overstrike text '+^Ho') attr = BOLD - char = [[·]] - elseif prev_char == [[·]] and char == 'o' then + char = '·' + elseif prev_char == '·' and char == 'o' then -- bullet (additional handling for '+^H+^Ho^Ho') attr = BOLD - char = [[·]] + char = '·' else -- use plain char attr = NONE end -- Grow the previous highlight group if possible - if last_hl and last_hl[1] == attr and last_hl[3] == byte then - last_hl[3] = byte + #char + if last_hl and last_hl.attr == attr and last_hl.final == byte then + last_hl.final = byte + #char else - hls[#hls + 1] = {attr, byte, byte + #char} + hls[#hls + 1] = {attr=attr, start=byte, final=byte + #char} end overstrike = false @@ -96,15 +101,19 @@ local function highlight_formatted(line, linenr) elseif escape then -- Use prev_char to store the escape sequence prev_char = prev_char .. char - local sgr = prev_char:match("^%[([\020-\063]*)m$") + -- We only want to match against SGR sequences, which consist of ESC + -- followed by '[', then a series of parameter and intermediate bytes in + -- the range 0x20 - 0x3f, then 'm'. (See ECMA-48, sections 5.4 & 8.3.117) + local sgr = prev_char:match("^%[([\032-\063]*)m$") if sgr then local match = '' while sgr and #sgr > 0 do + -- Match against SGR parameters, which may be separated by ';' match, sgr = sgr:match("^(%d*);?(.*)") add_attr_hl(match + 0) -- coerce to number end escape = false - elseif not prev_char:match("^%[[\020-\063]*$") then + elseif not prev_char:match("^%[[\032-\063]*$") then -- Stop looking if this isn't a partial CSI sequence escape = false end @@ -122,20 +131,38 @@ local function highlight_formatted(line, linenr) end end - for i, hl in ipairs(hls) do - if hl[1] ~= NONE then - vim.api.nvim_buf_add_highlight( + for _, hl in ipairs(hls) do + if hl.attr ~= NONE then + buf_hls[#buf_hls + 1] = { 0, -1, - hl_groups[hl[1]], + hl_groups[hl.attr], linenr - 1, - hl[2], - hl[3] - ) + hl.start, + hl.final + } end end return table.concat(chars, '') end -return { highlight_formatted = highlight_formatted } +local function highlight_man_page() + local mod = vim.api.nvim_eval("&modifiable") + vim.api.nvim_command("set modifiable") + + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + for i, line in ipairs(lines) do + lines[i] = highlight_line(line, i) + end + vim.api.nvim_buf_set_lines(0, 0, -1, false, lines) + + for _, args in ipairs(buf_hls) do + vim.api.nvim_buf_add_highlight(unpack(args)) + end + buf_hls = {} + + vim.api.nvim_command("let &modifiable = "..mod) +end + +return { highlight_man_page = highlight_man_page } diff --git a/runtime/syntax/man.vim b/runtime/syntax/man.vim index 0544fb29e2..b8e605cb9a 100644 --- a/runtime/syntax/man.vim +++ b/runtime/syntax/man.vim @@ -18,18 +18,9 @@ highlight default link manOptionDesc Constant highlight default link manReference PreProc highlight default link manSubHeading Function -function! s:init_highlight_groups() - highlight default manUnderline cterm=underline gui=underline - highlight default manBold cterm=bold gui=bold - highlight default manItalic cterm=italic gui=italic -endfunction - -augroup man_init_highlight_groups - autocmd! - autocmd ColorScheme * call s:init_highlight_groups() -augroup END - -call s:init_highlight_groups() +highlight default manUnderline cterm=underline gui=underline +highlight default manBold cterm=bold gui=bold +highlight default manItalic cterm=italic gui=italic if &filetype != 'man' " May have been included by some other filetype. diff --git a/test/functional/plugin/man_spec.lua b/test/functional/plugin/man_spec.lua index 53384a9d47..479fd6e7a5 100644 --- a/test/functional/plugin/man_spec.lua +++ b/test/functional/plugin/man_spec.lua @@ -3,7 +3,7 @@ local plugin_helpers = require('test.functional.plugin.helpers') local Screen = require('test.functional.ui.screen') -local buffer, command, eval = helpers.buffer, helpers.command, helpers.eval +local command, eval, rawfeed = helpers.command, helpers.eval, helpers.rawfeed before_each(function() plugin_helpers.reset() @@ -19,6 +19,17 @@ describe('In autoload/man.vim', function() before_each(function() command('syntax off') -- Ignore syntax groups screen = Screen.new(52, 5) + screen:set_default_attr_ids({ + b = { bold = true }, + i = { italic = true }, + u = { underline = true }, + bi = { bold = true, italic = true }, + biu = { bold = true, italic = true, underline = true }, + }) + screen:set_default_attr_ignore({ + { foreground = Screen.colors.Blue }, -- control chars + { bold = true, foreground = Screen.colors.Blue } -- empty line '~'s + }) screen:attach() end) @@ -26,84 +37,22 @@ describe('In autoload/man.vim', function() screen:detach() end) - local function expect(string) - screen:expect(string, - { - b = { bold = true }, - i = { italic = true }, - u = { underline = true }, - bi = { bold = true, italic = true }, - biu = { bold = true, italic = true, underline = true }, - }, - {{ bold = true, foreground = Screen.colors.Blue }}) - end + it('clears backspaces from text and adds highlights', function() + rawfeed([[ + ithis iiss aa test + with _o_v_e_r_s_t_r_u_c_k text]]) - local function expect_without_highlights(string) - screen:expect(string, nil, true) - end - - local function insert_lines(...) - buffer('set_lines', 0, 0, 1, false, { ... }) - end - - it('clears backspaces from text', function() - insert_lines( - "this i\bis\bs a\ba test", - "with _\bo_\bv_\be_\br_\bs_\bt_\br_\bu_\bc_\bk text" - ) - - expect_without_highlights([[ - ^this i^His^Hs a^Ha test | - with _^Ho_^Hv_^He_^Hr_^Hs_^Ht_^Hr_^Hu_^Hc_^Hk text | + screen:expect([[ + this i^His^Hs a^Ha test | + with _^Ho_^Hv_^He_^Hr_^Hs_^Ht_^Hr_^Hu_^Hc_^Hk tex^t | ~ | ~ | | ]]) - eval('man#highlight_formatted_text()') + eval('man#init_pager()') - expect_without_highlights([[ - ^this is a test | - with overstruck text | - ~ | - ~ | - | - ]]) - end) - - it('clears escape sequences from text', function() - insert_lines( - "this \027[1mis \027[3ma \027[4mtest\027[0m", - "\027[4mwith\027[24m \027[4mescaped\027[24m \027[4mtext\027[24m" - ) - - expect_without_highlights([[ - ^this ^[[1mis ^[[3ma ^[[4mtest^[[0m | - ^[[4mwith^[[24m ^[[4mescaped^[[24m ^[[4mtext^[[24m | - ~ | - ~ | - | - ]]) - - eval('man#highlight_formatted_text()') - - expect_without_highlights([[ - ^this is a test | - with escaped text | - ~ | - ~ | - | - ]]) - end) - - it('highlights overstruck text', function() - insert_lines( - "this i\bis\bs a\ba test", - "with _\bo_\bv_\be_\br_\bs_\bt_\br_\bu_\bc_\bk text" - ) - eval('man#highlight_formatted_text()') - - expect([[ + screen:expect([[ ^this {b:is} {b:a} test | with {u:overstruck} text | ~ | @@ -112,14 +61,22 @@ describe('In autoload/man.vim', function() ]]) end) - it('highlights escape sequences in text', function() - insert_lines( - "this \027[1mis \027[3ma \027[4mtest\027[0m", - "\027[4mwith\027[24m \027[4mescaped\027[24m \027[4mtext\027[24m" - ) - eval('man#highlight_formatted_text()') + it('clears escape sequences from text and adds highlights', function() + rawfeed([[ + ithis [1mis [3ma [4mtest[0m + [4mwith[24m [4mescaped[24m [4mtext[24m]]) - expect([[ + screen:expect([[ + this ^[[1mis ^[[3ma ^[[4mtest^[[0m | + ^[[4mwith^[[24m ^[[4mescaped^[[24m ^[[4mtext^[[24^m | + ~ | + ~ | + | + ]]) + + eval('man#init_pager()') + + screen:expect([[ ^this {b:is }{bi:a }{biu:test} | {u:with} {u:escaped} {u:text} | ~ | @@ -129,15 +86,14 @@ describe('In autoload/man.vim', function() end) it('highlights multibyte text', function() - insert_lines( - "this i\bis\bs あ\bあ test", - "with _\bö_\bv_\be_\br_\bs_\bt_\br_\bu_\bc_\bk te\027[3mxt¶\027[0m" - ) - eval('man#highlight_formatted_text()') + rawfeed([[ + ithis iiss ああ test + with _ö_v_e_r_s_t_r_u_̃_c_k te[3mxt¶[0m]]) + eval('man#init_pager()') - expect([[ + screen:expect([[ ^this {b:is} {b:あ} test | - with {u:överstruck} te{i:xt¶} | + with {u:överstrũck} te{i:xt¶} | ~ | ~ | | @@ -145,14 +101,13 @@ describe('In autoload/man.vim', function() end) it('highlights underscores based on context', function() - insert_lines( - "_\b_b\bbe\beg\bgi\bin\bns\bs", - "m\bmi\bid\bd_\b_d\bdl\ble\be", - "_\bm_\bi_\bd_\b__\bd_\bl_\be" - ) - eval('man#highlight_formatted_text()') + rawfeed([[ + i__bbeeggiinnss + mmiidd__ddllee + _m_i_d___d_l_e]]) + eval('man#init_pager()') - expect([[ + screen:expect([[ {b:^_begins} | {b:mid_dle} | {u:mid_dle} | @@ -162,14 +117,13 @@ describe('In autoload/man.vim', function() end) it('highlights various bullet formats', function() - insert_lines( - "· ·\b·", - "+\bo", - "+\b+\bo\bo double" - ) - eval('man#highlight_formatted_text()') + rawfeed([[ + i· ·· + +o + ++oo double]]) + eval('man#init_pager()') - expect([[ + screen:expect([[ ^· {b:·} | {b:·} | {b:·} double |