aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/nvim/getchar.c187
-rw-r--r--src/nvim/normal.c15
-rw-r--r--test/functional/legacy/mapping_spec.lua76
-rw-r--r--test/old/testdir/test_mapping.vim45
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()