commit 9cbc430cfb9284d824b316a508b278c6339ad4ef
parent 289695c14efcb2cb61ea70a0dbaf0025f869b21c
Author: zeertzjq <zeertzjq@outlook.com>
Date: Sat, 14 Feb 2026 08:00:27 +0800
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.
Diffstat:
3 files changed, 72 insertions(+), 1 deletion(-)
diff --git a/src/nvim/normal.c b/src/nvim/normal.c
@@ -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);
diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c
@@ -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);
diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua
@@ -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)',