local helpers = require('test.functional.helpers')(after_each) local Screen = require('test.functional.ui.screen') local eq = helpers.eq local feed = helpers.feed local clear = helpers.clear local meths = helpers.meths local funcs = helpers.funcs local source = helpers.source local exec_capture = helpers.exec_capture local dedent = helpers.dedent local command = helpers.command local curbufmeths = helpers.curbufmeths local screen -- Bug in input() handling: :redraw! will erase the whole prompt up until -- user types something. It exists in Vim as well, so using `h` as -- a workaround. local function redraw_input() feed('{REDRAW}h') end before_each(function() clear() screen = Screen.new(40, 8) screen:attach() source([[ highlight RBP1 guibg=Red highlight RBP2 guibg=Yellow highlight RBP3 guibg=Green highlight RBP4 guibg=Blue let g:NUM_LVLS = 4 function Redraw() mode return "\" endfunction let g:id = '' cnoremap {REDRAW} Redraw() function DoPrompt(do_return) abort let id = g:id let Cb = g:Nvim_color_input{g:id} let out = input({'prompt': ':', 'highlight': Cb}) let g:out{id} = out return (a:do_return ? out : "\") endfunction nnoremap {PROMPT} DoPrompt(0) cnoremap {PROMPT} DoPrompt(1) function RainBowParens(cmdline) let ret = [] let i = 0 let lvl = 0 while i < len(a:cmdline) if a:cmdline[i] is# '(' call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) let lvl += 1 elseif a:cmdline[i] is# ')' let lvl -= 1 call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) endif let i += 1 endwhile return ret endfunction function SplitMultibyteStart(cmdline) let ret = [] let i = 0 while i < len(a:cmdline) let char = nr2char(char2nr(a:cmdline[i:])) if a:cmdline[i:i + len(char) - 1] is# char if len(char) > 1 call add(ret, [i + 1, i + len(char), 'RBP2']) endif let i += len(char) else let i += 1 endif endwhile return ret endfunction function SplitMultibyteEnd(cmdline) let ret = [] let i = 0 while i < len(a:cmdline) let char = nr2char(char2nr(a:cmdline[i:])) if a:cmdline[i:i + len(char) - 1] is# char if len(char) > 1 call add(ret, [i, i + 1, 'RBP1']) endif let i += len(char) else let i += 1 endif endwhile return ret endfunction function Echoing(cmdline) echo 'HERE' return v:_null_list endfunction function Echoning(cmdline) echon 'HERE' return v:_null_list endfunction function Echomsging(cmdline) echomsg 'HERE' return v:_null_list endfunction function Echoerring(cmdline) echoerr 'HERE' return v:_null_list endfunction function Redrawing(cmdline) redraw! return v:_null_list endfunction function Throwing(cmdline) throw "ABC" return v:_null_list endfunction function Halting(cmdline) while 1 endwhile endfunction function ReturningGlobal(cmdline) return g:callback_return endfunction function ReturningGlobal2(cmdline) return g:callback_return[:len(a:cmdline)-1] endfunction function ReturningGlobalN(n, cmdline) return g:callback_return{a:n} endfunction let g:recording_calls = [] function Recording(cmdline) call add(g:recording_calls, a:cmdline) return [] endfunction ]]) screen:set_default_attr_ids({ RBP1={background = Screen.colors.Red}, RBP2={background = Screen.colors.Yellow}, RBP3={background = Screen.colors.Green}, RBP4={background = Screen.colors.Blue}, EOB={bold = true, foreground = Screen.colors.Blue1}, ERR={foreground = Screen.colors.Grey100, background = Screen.colors.Red}, SK={foreground = Screen.colors.Blue}, PE={bold = true, foreground = Screen.colors.SeaGreen4}, NUM={foreground = Screen.colors.Blue2}, NPAR={foreground = Screen.colors.Yellow}, SQ={foreground = Screen.colors.Blue3}, SB={foreground = Screen.colors.Blue4}, E={foreground = Screen.colors.Red, background = Screen.colors.Blue}, M={bold = true}, MSEP={bold = true, reverse = true}; }) end) local function set_color_cb(funcname, callback_return, id) meths.set_var('id', id or '') if id and id ~= '' and funcs.exists('*' .. funcname .. 'N') then command(('let g:Nvim_color_input%s = {cmdline -> %sN(%s, cmdline)}'):format( id, funcname, id)) if callback_return then meths.set_var('callback_return' .. id, callback_return) end else meths.set_var('Nvim_color_input', funcname) if callback_return then meths.set_var('callback_return', callback_return) end end end local function start_prompt(text) feed('{PROMPT}' .. (text or '')) end describe('Command-line coloring', function() it('works', function() set_color_cb('RainBowParens') meths.set_option_value('more', false, {}) start_prompt() screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :^ | ]]) feed('e') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :e^ | ]]) feed('cho ') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo ^ | ]]) feed('(') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo {RBP1:(}^ | ]]) feed('(') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo {RBP1:(}{RBP2:(}^ | ]]) feed('42') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo {RBP1:(}{RBP2:(}42^ | ]]) feed('))') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo {RBP1:(}{RBP2:(}42{RBP2:)}{RBP1:)}^ | ]]) feed('') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo {RBP1:(}{RBP2:(}42{RBP2:)}^ | ]]) redraw_input() screen:expect{grid=[[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo {RBP1:(}{RBP2:(}42{RBP2:)}^ | ]], reset=true} end) for _, func_part in ipairs({'', 'n', 'msg'}) do it('disables :echo' .. func_part .. ' messages', function() set_color_cb('Echo' .. func_part .. 'ing') start_prompt('echo') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo^ | ]]) end) end it('does the right thing when hl start appears to split multibyte char', function() set_color_cb('SplitMultibyteStart') start_prompt('echo "«') screen:expect{grid=[[ | {EOB:~ }| {EOB:~ }| {MSEP: }| :echo " | {ERR:E5405: Chunk 0 start 7 splits multibyte }| {ERR:character} | :echo "«^ | ]]} feed('»') screen:expect([[ | {EOB:~ }| {EOB:~ }| {MSEP: }| :echo " | {ERR:E5405: Chunk 0 start 7 splits multibyte }| {ERR:character} | :echo "«»^ | ]]) end) it('does the right thing when hl end appears to split multibyte char', function() set_color_cb('SplitMultibyteEnd') start_prompt('echo "«') screen:expect([[ | {EOB:~ }| {EOB:~ }| {MSEP: }| :echo " | {ERR:E5406: Chunk 0 end 7 splits multibyte ch}| {ERR:aracter} | :echo "«^ | ]]) end) it('does the right thing when erroring', function() set_color_cb('Echoerring') start_prompt('e') screen:expect([[ | {EOB:~ }| {MSEP: }| : | {ERR:E5407: Callback has thrown an exception:}| {ERR: function DoPrompt[3]..Echoerring, line }| {ERR:1: Vim(echoerr):HERE} | :e^ | ]]) end) it('silences :echo', function() set_color_cb('Echoing') start_prompt('e') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :e^ | ]]) eq('', exec_capture('messages')) end) it('silences :echon', function() set_color_cb('Echoning') start_prompt('e') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :e^ | ]]) eq('', exec_capture('messages')) end) it('silences :echomsg', function() set_color_cb('Echomsging') start_prompt('e') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :e^ | ]]) eq('', exec_capture('messages')) end) it('does the right thing when throwing', function() set_color_cb('Throwing') start_prompt('e') screen:expect([[ | {EOB:~ }| {MSEP: }| : | {ERR:E5407: Callback has thrown an exception:}| {ERR: function DoPrompt[3]..Throwing, line 1:}| {ERR: ABC} | :e^ | ]]) end) it('stops executing callback after a number of errors', function() set_color_cb('SplitMultibyteStart') start_prompt('let x = "«»«»«»«»«»"') screen:expect([[ | {EOB:~ }| {EOB:~ }| {MSEP: }| :let x = " | {ERR:E5405: Chunk 0 start 10 splits multibyte}| {ERR: character} | :let x = "«»«»«»«»«»"^ | ]]) feed('\n') screen:expect([[ ^ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| | ]]) feed('\n') eq('let x = "«»«»«»«»«»"', meths.get_var('out')) local msg = '\nE5405: Chunk 0 start 10 splits multibyte character' eq(msg:rep(1), funcs.execute('messages')) end) it('allows interrupting callback with ', function() set_color_cb('Halting') start_prompt('echo 42') screen:expect([[ ^ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| | ]]) screen:sleep(500) feed('') screen:expect([[ | {EOB:~ }| {EOB:~ }| {MSEP: }| : | {ERR:E5407: Callback has thrown an exception:}| {ERR: Keyboard interrupt} | :echo 42^ | ]]) redraw_input() screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo 42^ | ]]) feed('\n') screen:expect([[ ^ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo 42 | ]]) feed('\n') eq('echo 42', meths.get_var('out')) feed('') screen:expect([[ ^ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| Type :qa and pre...nter> to exit Nvim | ]]) end) it('works fine with NUL, NL, CR', function() set_color_cb('RainBowParens') start_prompt('echo ("")') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo {RBP1:(}"{SK:^M^@^@}"{RBP1:)}^ | ]]) end) it('errors out when callback returns something wrong', function() command('cnoremap + ++') set_color_cb('ReturningGlobal', '') start_prompt('#') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {MSEP: }| : | {ERR:E5400: Callback should return list} | :#^ | ]]) feed('') set_color_cb('ReturningGlobal', {{0, 1, 'Normal'}, 42}) start_prompt('#') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {MSEP: }| : | {ERR:E5401: List item 1 is not a List} | :#^ | ]]) feed('') set_color_cb('ReturningGlobal2', {{0, 1, 'Normal'}, {1}}) start_prompt('+') screen:expect([[ | {EOB:~ }| {EOB:~ }| {MSEP: }| :+ | {ERR:E5402: List item 1 has incorrect length:}| {ERR: 1 /= 3} | :++^ | ]]) feed('') set_color_cb('ReturningGlobal2', {{0, 1, 'Normal'}, {2, 3, 'Normal'}}) start_prompt('+') screen:expect([[ | {EOB:~ }| {EOB:~ }| {MSEP: }| :+ | {ERR:E5403: Chunk 1 start 2 not in range [1, }| {ERR:2)} | :++^ | ]]) feed('') set_color_cb('ReturningGlobal2', {{0, 1, 'Normal'}, {1, 3, 'Normal'}}) start_prompt('+') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {MSEP: }| :+ | {ERR:E5404: Chunk 1 end 3 not in range (1, 2]}| :++^ | ]]) end) it('does not error out when called from a errorred out cycle', function() set_color_cb('ReturningGlobal', {{0, 1, 'Normal'}}) feed(dedent([[ :set regexpengine=2 :for pat in [' \ze*', ' \zs*'] : try : let l = matchlist('x x', pat) : $put =input({'prompt':'>','highlight':'ReturningGlobal'}) : : $put ='E888 NOT detected for ' . pat : catch : $put =input({'prompt':'>','highlight':'ReturningGlobal'}) : : $put ='E888 detected for ' . pat : endtry :endfor : : : : : : ]])) eq({'', ':', 'E888 detected for \\ze*', ':', 'E888 detected for \\zs*'}, curbufmeths.get_lines(0, -1, false)) eq('', funcs.execute('messages')) end) it('allows nesting input()s', function() set_color_cb('ReturningGlobal', {{0, 1, 'RBP1'}}, '') start_prompt('1') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :{RBP1:1}^ | ]]) set_color_cb('ReturningGlobal', {{0, 1, 'RBP2'}}, '1') start_prompt('2') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :{RBP2:2}^ | ]]) set_color_cb('ReturningGlobal', {{0, 1, 'RBP3'}}, '2') start_prompt('3') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :{RBP3:3}^ | ]]) set_color_cb('ReturningGlobal', {{0, 1, 'RBP4'}}, '3') start_prompt('4') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :{RBP4:4}^ | ]]) feed('') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :{RBP3:3}4^ | ]]) feed('') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :{RBP2:2}34^ | ]]) feed('') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :{RBP1:1}234^ | ]]) feed('') screen:expect([[ ^ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| | ]]) eq('1234', meths.get_var('out')) eq('234', meths.get_var('out1')) eq('34', meths.get_var('out2')) eq('4', meths.get_var('out3')) eq(0, funcs.exists('g:out4')) end) it('runs callback with the same data only once', function() local function new_recording_calls(...) eq({...}, meths.get_var('recording_calls')) meths.set_var('recording_calls', {}) end set_color_cb('Recording') start_prompt('') -- Regression test. Disambiguation: -- -- new_recording_calls(expected_result) -- (actual_before_fix) -- feed('a') new_recording_calls('a') -- ('a', 'a') feed('b') new_recording_calls('ab') -- ('a', 'ab', 'ab') feed('c') new_recording_calls('abc') -- ('ab', 'abc', 'abc') feed('') new_recording_calls('ab') -- ('abc', 'ab', 'ab') feed('') new_recording_calls('a') -- ('ab', 'a', 'a') feed('') new_recording_calls() -- ('a') feed('') eq('', meths.get_var('out')) end) it('does not crash when callback has caught not-a-editor-command exception', function() source([[ function CaughtExc(cmdline) abort try gibberish catch " Do nothing endtry return [] endfunction ]]) set_color_cb('CaughtExc') start_prompt('1') eq(1, meths.eval('1')) end) end) describe('Ex commands coloring', function() it('works', function() meths.set_var('Nvim_color_cmdline', 'RainBowParens') feed(':echo (((1)))') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :echo {RBP1:(}{RBP2:(}{RBP3:(}1{RBP3:)}{RBP2:)}{RBP1:)}^ | ]]) end) it('still executes command-line even if errored out', function() meths.set_var('Nvim_color_cmdline', 'SplitMultibyteStart') feed(':let x = "«"\n') eq('«', meths.get_var('x')) local msg = 'E5405: Chunk 0 start 10 splits multibyte character' eq('\n'..msg, funcs.execute('messages')) end) it('does not error out when called from a errorred out cycle', function() -- Apparently when there is a cycle in which one of the commands errors out -- this error may be caught by color_cmdline before it is presented to the -- user. feed(dedent([[ :set regexpengine=2 :for pat in [' \ze*', ' \zs*'] : try : let l = matchlist('x x', pat) : $put ='E888 NOT detected for ' . pat : catch : $put ='E888 detected for ' . pat : endtry :endfor ]])) eq({'', 'E888 detected for \\ze*', 'E888 detected for \\zs*'}, curbufmeths.get_lines(0, -1, false)) eq('', funcs.execute('messages')) end) it('does not crash when using `n` in debug mode', function() feed(':debug execute "echo 1"\n') screen:expect([[ | {EOB:~ }| {EOB:~ }| {MSEP: }| Entering Debug mode. Type "cont" to con| tinue. | cmd: execute "echo 1" | >^ | ]]) feed('n\n') screen:expect([[ | {MSEP: }| Entering Debug mode. Type "cont" to con| tinue. | cmd: execute "echo 1" | >n | 1 | {PE:Press ENTER or type command to continue}^ | ]]) feed('\n') screen:expect([[ ^ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| | ]]) end) it('mapping error does not cancel prompt', function() command("cnoremap x execute('throw 42')[-1]") feed(':#x') screen:expect([[ | {EOB:~ }| {EOB:~ }| {MSEP: }| :# | {ERR:Error detected while processing :} | {ERR:E605: Exception not caught: 42} | :#^ | ]]) feed('') screen:expect([[ | {EOB:~ }| {MSEP: }| :# | {ERR:Error detected while processing :} | {ERR:E605: Exception not caught: 42} | {ERR:E749: Empty buffer} | {PE:Press ENTER or type command to continue}^ | ]]) feed('') eq('Error detected while processing :\nE605: Exception not caught: 42\nE749: Empty buffer', exec_capture('messages')) end) it('errors out when failing to get callback', function() meths.set_var('Nvim_color_cmdline', 42) feed(':#') screen:expect([[ | {EOB:~ }| {MSEP: }| : | {ERR:E5408: Unable to get g:Nvim_color_cmdlin}| {ERR:e callback: Vim:E6000: Argument is not a}| {ERR: function or function name} | :#^ | ]]) end) end) describe('Expressions coloring support', function() it('works', function() meths.command('hi clear NvimNumber') meths.command('hi clear NvimNestingParenthesis') meths.command('hi NvimNumber guifg=Blue2') meths.command('hi NvimNestingParenthesis guifg=Yellow') feed(':echo =(((1)))') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| ={NPAR:(((}{NUM:1}{NPAR:)))}^ | ]]) end) it('does not use Nvim_color_expr', function() meths.set_var('Nvim_color_expr', 42) -- Used to error out due to failing to get callback. meths.command('hi clear NvimNumber') meths.command('hi NvimNumber guifg=Blue2') feed(':=1') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| ={NUM:1}^ | ]]) end) it('works correctly with non-ASCII and control characters', function() meths.command('hi clear NvimStringBody') meths.command('hi clear NvimStringQuote') meths.command('hi clear NvimInvalid') meths.command('hi NvimStringQuote guifg=Blue3') meths.command('hi NvimStringBody guifg=Blue4') meths.command('hi NvimInvalid guifg=Red guibg=Blue') feed('i="«»"«»') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| ={SQ:"}{SB:«»}{SQ:"}{E:«»}^ | ]]) feed('') screen:expect([[ ^ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {M:-- INSERT --} | ]]) feed('') screen:expect([[ ^ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| | ]]) feed(':e""') -- TODO(ZyX-I): Parser highlighting should not override special character -- highlighting. screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| ={SQ:"}{SB:^X}{SQ:"}{ERR:^X}^ | ]]) feed('') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| :^ | ]]) funcs.setreg('a', {'\192'}) feed('="a"a"foo"') screen:expect([[ | {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| {EOB:~ }| ={SQ:"}{SB:}{SQ:"}{E:"}{SB:foo}{E:"}^ | ]]) end) end)