diff options
-rw-r--r-- | src/nvim/getchar.c | 187 | ||||
-rw-r--r-- | src/nvim/normal.c | 15 | ||||
-rw-r--r-- | test/functional/legacy/mapping_spec.lua | 76 | ||||
-rw-r--r-- | test/old/testdir/test_mapping.vim | 45 |
4 files changed, 258 insertions, 65 deletions
diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index 665f60a49e..bb50ba31a9 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -84,6 +84,15 @@ static FileDescriptor scriptin[NSCRIPT] = { 0 }; #define MINIMAL_SIZE 20 // minimal size for b_str +typedef struct { + int prev_c; + uint8_t buf[MB_MAXBYTES * 3 + 4]; + size_t buflen; + unsigned pending; + bool in_special; + bool in_mbyte; +} gotchars_state_T; + static buffheader_T redobuff = { { NULL, { NUL } }, NULL, 0, 0 }; static buffheader_T old_redobuff = { { NULL, { NUL } }, NULL, 0, 0 }; static buffheader_T recordbuff = { { NULL, { NUL } }, NULL, 0, 0 }; @@ -1112,84 +1121,95 @@ void del_typebuf(int len, int offset) } } -/// Write typed characters to script file. -/// If recording is on put the character in the record buffer. -static void gotchars(const uint8_t *chars, size_t len) +/// Add a single byte to a recording or 'showcmd'. +/// Return true if a full key has been received, false otherwise. +static bool gotchars_add_byte(gotchars_state_T *state, uint8_t byte) FUNC_ATTR_NONNULL_ALL { - const uint8_t *s = chars; - int c = NUL; - static int prev_c = NUL; - static uint8_t buf[MB_MAXBYTES * 3 + 4] = { 0 }; - static size_t buflen = 0; - static unsigned pending = 0; - static bool in_special = false; - static bool in_mbyte = false; - size_t todo = len; + int c = state->buf[state->buflen++] = byte; + bool retval = false; - for (; todo--; prev_c = c) { - c = buf[buflen++] = *s++; - if (pending > 0) { - pending--; + if (state->pending > 0) { + state->pending--; + } + + // When receiving a special key sequence, store it until we have all + // the bytes and we can decide what to do with it. + if ((state->pending == 0 || state->in_mbyte) && c == K_SPECIAL) { + state->pending += 2; + if (!state->in_mbyte) { + state->in_special = true; } + } - // When receiving a special key sequence, store it until we have all - // the bytes and we can decide what to do with it. - if ((pending == 0 || in_mbyte) && c == K_SPECIAL) { - pending += 2; - if (!in_mbyte) { - in_special = true; + if (state->pending > 0) { + goto ret_false; + } + + if (!state->in_mbyte) { + if (state->in_special) { + state->in_special = false; + if (state->prev_c == KS_MODIFIER) { + // When receiving a modifier, wait for the modified key. + goto ret_false; } + c = TO_SPECIAL(state->prev_c, c); } - - if (pending > 0) { - continue; + // When receiving a multibyte character, store it until we have all + // the bytes, so that it won't be split between two buffer blocks, + // and delete_buff_tail() will work properly. + state->pending = MB_BYTE2LEN_CHECK(c) - 1; + if (state->pending > 0) { + state->in_mbyte = true; + goto ret_false; } + } else { + // Stored all bytes of a multibyte character. + state->in_mbyte = false; + } - if (!in_mbyte) { - if (in_special) { - in_special = false; - if (prev_c == KS_MODIFIER) { - // When receiving a modifier, wait for the modified key. - continue; - } - c = TO_SPECIAL(prev_c, c); - } - // When receiving a multibyte character, store it until we have all - // the bytes, so that it won't be split between two buffer blocks, - // and delete_buff_tail() will work properly. - pending = MB_BYTE2LEN_CHECK(c) - 1; - if (pending > 0) { - in_mbyte = true; - continue; - } - } else { - // Stored all bytes of a multibyte character. - in_mbyte = false; + retval = true; +ret_false: + state->prev_c = c; + return retval; +} + +/// Write typed characters to script file. +/// If recording is on put the character in the record buffer. +static void gotchars(const uint8_t *chars, size_t len) + FUNC_ATTR_NONNULL_ALL +{ + const uint8_t *s = chars; + size_t todo = len; + static gotchars_state_T state; + + while (todo-- > 0) { + if (!gotchars_add_byte(&state, *s++)) { + continue; } // Handle one byte at a time; no translation to be done. - for (size_t i = 0; i < buflen; i++) { - updatescript(buf[i]); + for (size_t i = 0; i < state.buflen; i++) { + updatescript(state.buf[i]); } - buf[buflen] = NUL; + state.buf[state.buflen] = NUL; if (reg_recording != 0) { - add_buff(&recordbuff, (char *)buf, (ptrdiff_t)buflen); + add_buff(&recordbuff, (char *)state.buf, (ptrdiff_t)state.buflen); // remember how many chars were last recorded - last_recorded_len += buflen; + last_recorded_len += state.buflen; } - if (buflen > no_on_key_len) { - vim_unescape_ks((char *)buf + no_on_key_len); - kvi_concat(on_key_buf, (char *)buf + no_on_key_len); + if (state.buflen > no_on_key_len) { + vim_unescape_ks((char *)state.buf + no_on_key_len); + kvi_concat(on_key_buf, (char *)state.buf + no_on_key_len); no_on_key_len = 0; } else { - no_on_key_len -= buflen; + no_on_key_len -= state.buflen; } - buflen = 0; + state.buflen = 0; } may_sync_undo(); @@ -1496,6 +1516,61 @@ int merge_modifiers(int c_arg, int *modifiers) return c; } +/// Add a single byte to 'showcmd' for a partially matched mapping. +/// Call add_to_showcmd() if a full key has been received. +static void add_byte_to_showcmd(uint8_t byte) +{ + static gotchars_state_T state; + + if (!p_sc || msg_silent != 0) { + return; + } + + if (!gotchars_add_byte(&state, byte)) { + return; + } + + state.buf[state.buflen] = NUL; + state.buflen = 0; + + int modifiers = 0; + int c = NUL; + + const uint8_t *ptr = state.buf; + if (ptr[0] == K_SPECIAL && ptr[1] == KS_MODIFIER && ptr[2] != NUL) { + modifiers = ptr[2]; + ptr += 3; + } + + if (*ptr != NUL) { + const char *mb_ptr = mb_unescape((const char **)&ptr); + c = mb_ptr != NULL ? utf_ptr2char(mb_ptr) : *ptr++; + if (c <= 0x7f) { + // Merge modifiers into the key to make the result more readable. + int modifiers_after = modifiers; + int mod_c = merge_modifiers(c, &modifiers_after); + if (modifiers_after == 0) { + modifiers = 0; + c = mod_c; + } + } + } + + // TODO(zeertzjq): is there a more readable and yet compact representation of + // modifiers and special keys? + if (modifiers != 0) { + add_to_showcmd(K_SPECIAL); + add_to_showcmd(KS_MODIFIER); + add_to_showcmd(modifiers); + } + if (c != NUL) { + add_to_showcmd(c); + } + while (*ptr != NUL) { + add_to_showcmd(*ptr++); + } +} + /// Get the next input character. /// Can return a special key or a multi-byte character. /// Can return NUL when called recursively, use safe_vgetc() if that's not @@ -2726,7 +2801,7 @@ static int vgetorpeek(bool advance) showcmd_idx = typebuf.tb_len - SHOWCMD_COLS; } while (showcmd_idx < typebuf.tb_len) { - add_to_showcmd(typebuf.tb_buf[typebuf.tb_off + showcmd_idx++]); + add_byte_to_showcmd(typebuf.tb_buf[typebuf.tb_off + showcmd_idx++]); } curwin->w_wcol = old_wcol; curwin->w_wrow = old_wrow; diff --git a/src/nvim/normal.c b/src/nvim/normal.c index b40a11f6d8..22b1c8dffa 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -1968,9 +1968,16 @@ bool add_to_showcmd(int c) } } - char *p = transchar(c); - if (*p == ' ') { - STRCPY(p, "<20>"); + char *p; + char mbyte_buf[MB_MAXCHAR + 1]; + if (c <= 0x7f || !vim_isprintc(c)) { + p = transchar(c); + if (*p == ' ') { + STRCPY(p, "<20>"); + } + } else { + mbyte_buf[utf_char2bytes(c, mbyte_buf)] = NUL; + p = mbyte_buf; } size_t old_len = strlen(showcmd_buf); size_t extra_len = strlen(p); @@ -2036,7 +2043,7 @@ void pop_showcmd(void) static void display_showcmd(void) { - int len = (int)strlen(showcmd_buf); + int len = vim_strsize(showcmd_buf); showcmd_is_clear = (len == 0); if (*p_sloc == 's') { diff --git a/test/functional/legacy/mapping_spec.lua b/test/functional/legacy/mapping_spec.lua index 272a79fa29..1078daec64 100644 --- a/test/functional/legacy/mapping_spec.lua +++ b/test/functional/legacy/mapping_spec.lua @@ -1,9 +1,11 @@ -- Test for mappings and abbreviations local t = require('test.functional.testutil')() +local Screen = require('test.functional.ui.screen') local clear, feed, insert = t.clear, t.feed, t.insert local expect, poke_eventloop = t.expect, t.poke_eventloop local command, eq, eval, api = t.command, t.eq, t.eval, t.api +local exec = t.exec local sleep = vim.uv.sleep describe('mapping', function() @@ -23,6 +25,7 @@ describe('mapping', function() vim ]]) end) + -- oldtest: Test_map_ctrl_c_insert() it('Ctrl-c works in Insert mode', function() -- Mapping of ctrl-c in insert mode command('set cpo-=< cpo-=k') @@ -41,6 +44,7 @@ describe('mapping', function() ]]) end) + -- oldtest: Test_map_ctrl_c_visual() it('Ctrl-c works in Visual mode', function() command([[vnoremap <c-c> :<C-u>$put ='vmap works'<cr>]]) feed('GV') @@ -83,6 +87,7 @@ describe('mapping', function() +]]) end) + -- oldtest: Test_map_feedkeys() it('feedkeys', function() insert([[ a b c d @@ -100,6 +105,7 @@ describe('mapping', function() ]]) end) + -- oldtest: Test_map_cursor() it('i_CTRL-G_U', function() -- <c-g>U<cursor> works only within a single line command('imapclear') @@ -128,7 +134,8 @@ describe('mapping', function() ]]) end) - it('dragging starts Select mode even if coming from mapping vim-patch:8.2.4806', function() + -- oldtest: Test_mouse_drag_mapped_start_select() + it('dragging starts Select mode even if coming from mapping', function() command('set mouse=a') command('set selectmode=mouse') @@ -141,7 +148,8 @@ describe('mapping', function() eq('s', eval('mode()')) end) - it('<LeftDrag> mapping in Insert mode works correctly vim-patch:8.2.4692', function() + -- oldtest: Test_mouse_drag_insert_map() + it('<LeftDrag> mapping in Insert mode works correctly', function() command('set mouse=a') command('inoremap <LeftDrag> <LeftDrag><Cmd>let g:dragged = 1<CR>') @@ -165,7 +173,8 @@ describe('mapping', function() eq('n', eval('mode()')) end) - it('timeout works after an <Nop> mapping is triggered on timeout vim-patch:8.1.0052', function() + -- oldtest: Test_map_after_timed_out_nop() + it('timeout works after an <Nop> mapping is triggered on timeout', function() command('set timeout timeoutlen=400') command('inoremap ab TEST') command('inoremap a <Nop>') @@ -181,4 +190,65 @@ describe('mapping', function() feed('b') expect('TEST') end) + + -- oldtest: Test_showcmd_part_map() + it("'showcmd' with a partial mapping", function() + local screen = Screen.new(60, 6) + screen:attach() + exec([[ + set notimeout showcmd + nnoremap ,a <Ignore> + nnoremap ;a <Ignore> + nnoremap Àa <Ignore> + nnoremap Ëa <Ignore> + nnoremap βa <Ignore> + nnoremap ωa <Ignore> + nnoremap …a <Ignore> + nnoremap <C-W>a <Ignore> + ]]) + + for _, c in ipairs({ ',', ';', 'À', 'Ë', 'β', 'ω', '…' }) do + feed(c) + screen:expect(([[ + ^ | + {1:~ }|*4 + %s | + ]]):format(c)) + feed('<C-C>') + command('echo') + screen:expect([[ + ^ | + {1:~ }|*4 + | + ]]) + end + + feed('\23') + screen:expect([[ + ^ | + {1:~ }|*4 + ^W | + ]]) + feed('<C-C>') + command('echo') + screen:expect([[ + ^ | + {1:~ }|*4 + | + ]]) + + feed('<C-W>') + screen:expect([[ + ^ | + {1:~ }|*4 + ^W | + ]]) + feed('<C-C>') + command('echo') + screen:expect([[ + ^ | + {1:~ }|*4 + | + ]]) + end) end) diff --git a/test/old/testdir/test_mapping.vim b/test/old/testdir/test_mapping.vim index 623228b347..811a260238 100644 --- a/test/old/testdir/test_mapping.vim +++ b/test/old/testdir/test_mapping.vim @@ -1693,7 +1693,7 @@ func Test_map_after_timed_out_nop() inoremap ab TEST inoremap a <Nop> END - call writefile(lines, 'Xtest_map_after_timed_out_nop') + call writefile(lines, 'Xtest_map_after_timed_out_nop', 'D') let buf = RunVimInTerminal('-S Xtest_map_after_timed_out_nop', #{rows: 6}) " Enter Insert mode @@ -1710,7 +1710,48 @@ func Test_map_after_timed_out_nop() " clean up call StopVimInTerminal(buf) - call delete('Xtest_map_after_timed_out_nop') +endfunc + +" Test 'showcmd' behavior with a partial mapping +func Test_showcmd_part_map() + CheckRunVimInTerminal + + let lines =<< trim eval END + set notimeout showcmd + nnoremap ,a <Ignore> + nnoremap ;a <Ignore> + nnoremap Àa <Ignore> + nnoremap Ëa <Ignore> + nnoremap βa <Ignore> + nnoremap ωa <Ignore> + nnoremap …a <Ignore> + nnoremap <C-W>a <Ignore> + END + call writefile(lines, 'Xtest_showcmd_part_map', 'D') + let buf = RunVimInTerminal('-S Xtest_showcmd_part_map', #{rows: 6}) + + call term_sendkeys(buf, ":set noruler | echo\<CR>") + call WaitForAssert({-> assert_equal('', term_getline(buf, 6))}) + + for c in [',', ';', 'À', 'Ë', 'β', 'ω', '…'] + call term_sendkeys(buf, c) + call WaitForAssert({-> assert_equal(c, trim(term_getline(buf, 6)))}) + call term_sendkeys(buf, "\<C-C>:echo\<CR>") + call WaitForAssert({-> assert_equal('', term_getline(buf, 6))}) + endfor + + call term_sendkeys(buf, "\<C-W>") + call WaitForAssert({-> assert_equal('^W', trim(term_getline(buf, 6)))}) + call term_sendkeys(buf, "\<C-C>:echo\<CR>") + call WaitForAssert({-> assert_equal('', term_getline(buf, 6))}) + + " Use feedkeys() as terminal buffer cannot forward this + call term_sendkeys(buf, ':call feedkeys("\<*C-W>", "m")' .. " | echo\<CR>") + call WaitForAssert({-> assert_equal('^W', trim(term_getline(buf, 6)))}) + call term_sendkeys(buf, "\<C-C>:echo\<CR>") + call WaitForAssert({-> assert_equal('', term_getline(buf, 6))}) + + call StopVimInTerminal(buf) endfunc func Test_using_past_typeahead() |