Skip to content

Commit

Permalink
patch 9.1.0343: 'showcmd' wrong for partial mapping with multibyte
Browse files Browse the repository at this point in the history
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: #14572

Signed-off-by: zeertzjq <zeertzjq@outlook.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
  • Loading branch information
zeertzjq authored and chrisbra committed Apr 17, 2024
1 parent ae7e61c commit acdfb8a
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 66 deletions.
207 changes: 145 additions & 62 deletions src/getchar.c
Expand Up @@ -1281,6 +1281,80 @@ del_typebuf(int len, int offset)
typebuf.tb_change_cnt = 1;
}

typedef struct
{
int prev_c;
char_u buf[MB_MAXBYTES * 3 + 4];
size_t buflen;
unsigned pending;
int in_special;
int in_mbyte;
} gotchars_state_T;

/*
* Add a single byte to a recording or 'showcmd'.
* Return TRUE if a full key has been received, FALSE otherwise.
*/
static int
gotchars_add_byte(gotchars_state_T *state, char_u byte)
{
int c = state->buf[state->buflen++] = byte;
int 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
#ifdef FEAT_GUI
|| c == CSI
#endif
))
{
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);
if (c == K_FOCUSGAINED || c == K_FOCUSLOST)
// Drop K_FOCUSGAINED and K_FOCUSLOST, they are not useful
// in a recording.
state->buflen = 0;
}
// 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.
Expand All @@ -1290,78 +1364,26 @@ gotchars(char_u *chars, int len)
{
char_u *s = chars;
size_t i;
int c = NUL;
static int prev_c = NUL;
static char_u buf[MB_MAXBYTES * 3 + 4];
static size_t buflen = 0;
static unsigned pending = 0;
static int in_special = FALSE;
static int in_mbyte = FALSE;
int todo = len;
static gotchars_state_T state;

for (; todo--; prev_c = c)
while (todo-- > 0)
{
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
#ifdef FEAT_GUI
|| c == CSI
#endif
))
{
pending += 2;
if (!in_mbyte)
in_special = TRUE;
}

if (pending > 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);
if (c == K_FOCUSGAINED || c == K_FOCUSLOST)
// Drop K_FOCUSGAINED and K_FOCUSLOST, they are not useful
// in a recording.
buflen = 0;
}
// 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 (i = 0; i < buflen; ++i)
updatescript(buf[i]);
for (i = 0; i < state.buflen; ++i)
updatescript(state.buf[i]);

if (reg_recording != 0)
{
buf[buflen] = NUL;
add_buff(&recordbuff, buf, (long)buflen);
state.buf[state.buflen] = NUL;
add_buff(&recordbuff, state.buf, (long)state.buflen);
// remember how many chars were last recorded
last_recorded_len += buflen;
last_recorded_len += state.buflen;
}
buflen = 0;
state.buflen = 0;
}

may_sync_undo();
Expand Down Expand Up @@ -1750,6 +1772,67 @@ merge_modifyOtherKeys(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(char_u byte)
{
static gotchars_state_T state;
char_u *ptr;
int modifiers = 0;
int c = NUL;

if (!p_sc || msg_silent != 0)
return;

if (!gotchars_add_byte(&state, byte))
return;

state.buf[state.buflen] = NUL;
state.buflen = 0;

ptr = state.buf;
if (ptr[0] == K_SPECIAL && ptr[1] == KS_MODIFIER && ptr[2] != NUL)
{
modifiers = ptr[2];
ptr += 3;
}

if (*ptr != NUL)
{
char_u *mb_ptr = mb_unescape(&ptr);

c = mb_ptr != NULL ? (*mb_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_modifyOtherKeys(c, &modifiers_after);

if (modifiers_after == 0)
{
modifiers = 0;
c = mod_c;
}
}
}

// TODO: 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.
Expand Down Expand Up @@ -3582,7 +3665,7 @@ vgetorpeek(int advance)
if (typebuf.tb_len > SHOWCMD_COLS)
showcmd_idx = typebuf.tb_len - SHOWCMD_COLS;
while (showcmd_idx < typebuf.tb_len)
(void)add_to_showcmd(
add_byte_to_showcmd(
typebuf.tb_buf[typebuf.tb_off + showcmd_idx++]);
curwin->w_wcol = old_wcol;
curwin->w_wrow = old_wrow;
Expand Down
17 changes: 13 additions & 4 deletions src/normal.c
Expand Up @@ -1708,6 +1708,7 @@ add_to_showcmd(int c)
int extra_len;
int overflow;
int i;
char_u mbyte_buf[MB_MAXBYTES];
static int ignore[] =
{
#ifdef FEAT_GUI
Expand Down Expand Up @@ -1739,9 +1740,17 @@ add_to_showcmd(int c)
if (ignore[i] == c)
return FALSE;

p = transchar(c);
if (*p == ' ')
STRCPY(p, "<20>");
if (c <= 0x7f || !vim_isprintc(c))
{
p = transchar(c);
if (*p == ' ')
STRCPY(p, "<20>");
}
else
{
mbyte_buf[(*mb_char2bytes)(c, mbyte_buf)] = NUL;
p = mbyte_buf;
}
old_len = (int)STRLEN(showcmd_buf);
extra_len = (int)STRLEN(p);
overflow = old_len + extra_len - SHOWCMD_COLS;
Expand Down Expand Up @@ -1810,7 +1819,7 @@ 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);
cursor_off();
Expand Down
42 changes: 42 additions & 0 deletions src/testdir/test_mapping.vim
Expand Up @@ -1807,6 +1807,48 @@ func Test_map_after_timed_out_nop()
call StopVimInTerminal(buf)
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()
nnoremap :00 0
exe "norm :set \x80\xfb0=0\<CR>"
Expand Down
2 changes: 2 additions & 0 deletions src/version.c
Expand Up @@ -704,6 +704,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
/**/
343,
/**/
342,
/**/
Expand Down

0 comments on commit acdfb8a

Please sign in to comment.