vim-patch:9.1.0343: 'showcmd' wrong for partial mapping with multibyte (#28392)

Problem:  'showcmd' is wrong for partial mapping with multibyte char,
          and isn't very readable with modifyOtherKeys.
Solution: Decode multibyte char and merge modifiers into the char.
          (zeertzjq)

This improves the following situations:
- Multibyte chars whose individual bytes are considered unprintable are
  now shown properly in 'showcmd' area.
- Ctrl-W with modifyOtherKeys now shows ^W in 'showcmd' area.

The following situation may still need improvement:
- If the char is a special key or has modifiers that cannot be merged
  into it, internal keycodes are shown in 'showcmd' area like before.
  This applies to keys typed in Normal mode commands as well, and it's
  hard to decide how to make it more readable due to the limited space
  taken by 'showcmd', so I'll leave it for later.

closes: vim/vim#14572

acdfb8a979
This commit is contained in:
zeertzjq 2024-04-18 06:23:11 +08:00 committed by GitHub
parent de6eb96fc9
commit 562719033e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 258 additions and 65 deletions

View File

@ -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)
}
}
/// 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
{
int c = state->buf[state->buflen++] = byte;
bool retval = false;
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;
}
}
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);
}
// 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;
}
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;
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;
static gotchars_state_T state;
for (; todo--; prev_c = c) {
c = buf[buflen++] = *s++;
if (pending > 0) {
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 ((pending == 0 || in_mbyte) && c == K_SPECIAL) {
pending += 2;
if (!in_mbyte) {
in_special = true;
}
}
if (pending > 0) {
while (todo-- > 0) {
if (!gotchars_add_byte(&state, *s++)) {
continue;
}
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;
}
// 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;

View File

@ -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') {

View File

@ -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)

View File

@ -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>
nnoremapa <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()