fix(terminal): missing refresh with partial mappings (#37839)

Problem:  Terminal buffers are not refreshed when processing keys that
          trigger partial mappings.
Solution: Process due terminal refreshes before redrawing.
This commit is contained in:
zeertzjq
2026-02-14 08:00:27 +08:00
committed by GitHub
parent 289695c14e
commit 9cbc430cfb
3 changed files with 72 additions and 1 deletions

View File

@@ -82,6 +82,7 @@
#include "nvim/strings.h"
#include "nvim/syntax.h"
#include "nvim/tag.h"
#include "nvim/terminal.h"
#include "nvim/textformat.h"
#include "nvim/textobject.h"
#include "nvim/types_defs.h"
@@ -1445,6 +1446,8 @@ static int normal_check(VimState *state)
skip_redraw = false;
setcursor();
} else if (do_redraw || stuff_empty()) {
terminal_check_refresh();
// Ensure curwin->w_topline and curwin->w_leftcol are up to date
// before triggering a WinScrolled autocommand.
update_topline(curwin);

View File

@@ -746,7 +746,7 @@ void terminal_set_state(Terminal *term, bool suspended)
{
if (term->suspended != suspended) {
// Trigger a main loop iteration to redraw the buffer.
multiqueue_put(main_loop.events, terminal_state_change_event,
multiqueue_put(refresh_timer.events, terminal_state_change_event,
(void *)(intptr_t)term->buf_handle);
}
term->suspended = suspended;
@@ -1033,6 +1033,8 @@ static int terminal_check(VimState *state)
return 0;
}
terminal_check_refresh();
// Validate topline and cursor position for autocommands. Especially important for WinScrolled.
terminal_check_cursor();
validate_cursor(curwin);
@@ -2283,6 +2285,14 @@ static void invalidate_terminal(Terminal *term, int start_row, int end_row)
}
}
/// Normally refresh_timer_cb() is called when processing main_loop.events, but with
/// partial mappings main_loop.events isn't processed, while terminal buffers still
/// need refreshing after processing a key, so call this function before redrawing.
void terminal_check_refresh(void)
{
multiqueue_process_events(refresh_timer.events);
}
static void refresh_terminal(Terminal *term)
{
buf_T *buf = handle_get_buffer(term->buf_handle);

View File

@@ -296,6 +296,64 @@ describe(':terminal buffer', function()
eq('t', fn.mode(1))
end)
it('is refreshed with partial mappings in Terminal mode #9167', function()
command([[set timeoutlen=20000 | tnoremap jk <C-\><C-N>]])
feed('j') -- Won't reach the terminal until the next character is typed
screen:expect_unchanged()
feed('j') -- Refresh scheduled for the first 'j' but not processed
screen:expect_unchanged()
for i = 1, 10 do
eq({ mode = 't', blocking = true }, api.nvim_get_mode())
vim.uv.sleep(10) -- Wait for the previously scheduled refresh timer to arrive
feed('j') -- Refresh scheduled for the last 'j' and processed for the one before
screen:expect(([[
tty ready |
%s^%s|
|*4
{5:-- TERMINAL --} |
]]):format(('j'):rep(i), (' '):rep(50 - i)))
end
feed('l') -- No partial mapping, so all pending refreshes should be processed
screen:expect([[
tty ready |
jjjjjjjjjjjjl^ |
|*4
{5:-- TERMINAL --} |
]])
end)
it('is refreshed with partial mappings in Normal mode', function()
command('set timeoutlen=20000 | nnoremap jk :')
command('nnoremap j <Cmd>call chansend(&channel, "j")<CR>')
feed([[<C-\><C-N>]])
screen:expect([[
tty ready |
^ |
|*5
]])
feed('j') -- Won't reach the terminal until the next character is typed
screen:expect_unchanged()
feed('j') -- Refresh scheduled for the first 'j' but not processed
screen:expect_unchanged()
for i = 1, 10 do
eq({ mode = 'nt', blocking = true }, api.nvim_get_mode())
vim.uv.sleep(10) -- Wait for the previously scheduled refresh timer to arrive
feed('j') -- Refresh scheduled for the last 'j' and processed for the one before
screen:expect(([[
tty ready |
^%s%s|
|*4
|
]]):format(('j'):rep(i), (' '):rep(50 - i)))
end
feed('l') -- No partial mapping, so all pending refreshes should be processed
screen:expect([[
tty ready |
j^jjjjjjjjjjj |
|*5
]])
end)
it('writing to an existing file with :w fails #13549', function()
eq(
'Vim(write):E13: File exists (add ! to override)',