From 814f2629cbcf02b18ff08d394d15835419e563e4 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Thu, 5 Feb 2026 08:19:11 +0800 Subject: [PATCH] fix(terminal): handle split composing chars at right edge (#37694) Problem: Recombining composing chars in terminal doesn't work at right edge. Solution: Check for the case where printing the previous char didn't advance the cursor. Reset at_phantom when returning to combine_pos. --- src/nvim/vterm/state.c | 4 +++- test/functional/terminal/buffer_spec.lua | 21 ++++++++++++++++-- test/unit/vterm_spec.lua | 27 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/nvim/vterm/state.c b/src/nvim/vterm/state.c index 65cb29ca4f..c0bfb803ba 100644 --- a/src/nvim/vterm/state.c +++ b/src/nvim/vterm/state.c @@ -342,13 +342,15 @@ static int on_text(const char bytes[], size_t len, void *user) // See if the cursor has moved since if (state->pos.row == state->combine_pos.row - && state->pos.col == state->combine_pos.col + state->combine_width) { + && state->pos.col >= state->combine_pos.col + && state->pos.col <= state->combine_pos.col + state->combine_width) { // This is a combining char. that needs to be merged with the previous glyph output if (utf_iscomposing((int)state->grapheme_last, (int)codepoints[i], &state->grapheme_state)) { // Find where we need to append these combining chars grapheme_len = state->grapheme_len; grapheme_state = state->grapheme_state; state->pos.col = state->combine_pos.col; + state->at_phantom = 0; recombine = true; } else { DEBUG_LOG("libvterm: TODO: Skip over split char+combining\n"); diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index fff0985051..d31cee89dd 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -758,7 +758,7 @@ describe(':terminal buffer', function() end) it('handles split UTF-8 sequences #16245', function() - local screen = Screen.new(50, 7) + local screen = Screen.new(50, 10) fn.jobstart({ testprg('shell-test'), 'UTF-8' }, { term = true }) screen:expect([[ ^å | @@ -766,7 +766,24 @@ describe(':terminal buffer', function() 1: å̲ | 2: å̲ | 3: å̲ | - |*2 + | + [Process exited 0] | + |*3 + ]]) + -- Test with Unicode char at right edge using a 4-wide terminal + command('bwipe! | set laststatus=0 | 4vnew') + fn.jobstart({ testprg('shell-test'), 'UTF-8' }, { term = true }) + screen:expect([[ + ^å │ | + ref:│{1:~ }| + å̲ │{1:~ }| + 1: å̲│{1:~ }| + 2: å̲│{1:~ }| + 3: å̲│{1:~ }| + │{1:~ }| + [Pro│{1:~ }| + cess│{1:~ }| + | ]]) end) diff --git a/test/unit/vterm_spec.lua b/test/unit/vterm_spec.lua index fe4bc93722..988ddb7503 100644 --- a/test/unit/vterm_spec.lua +++ b/test/unit/vterm_spec.lua @@ -992,6 +992,7 @@ describe('vterm', function() push('e' .. string.rep('\xCC\x81', 20), vt) expect('putglyph 65,301,301,301,301,301,301,301,301,301,301,301,301,301,301 1 0,0') -- and nothing more + -- Combining across buffers multiple times reset(state, nil) push('e', vt) expect('putglyph 65 1 0,0') @@ -1000,6 +1001,32 @@ describe('vterm', function() push('\xCC\x82', vt) expect('putglyph 65,301,302 1 0,0') + -- Combining across buffers at right edge + reset(state, nil) + push('\x1b[5;80H', vt) + push('e', vt) + expect('putglyph 65 1 4,79') + push('\xCC\x81', vt) + expect('putglyph 65,301 1 4,79') + push('\xCC\x82Z', vt) + expect('putglyph 65,301,302 1 4,79\nputglyph 5a 1 5,0') + + -- Combining regional indicators + reset(state, nil) + push('\x1b[5;77H', vt) + push('🇦', vt) + expect('putglyph 1f1e6 2 4,76') + push('🇩', vt) + expect('putglyph 1f1e6,1f1e9 2 4,76') + push('🇱', vt) + expect('putglyph 1f1f1 2 4,78') + push('🇮', vt) + expect('putglyph 1f1f1,1f1ee 2 4,78') + push('🇲', vt) + expect('putglyph 1f1f2 2 5,0') + push('🇨', vt) + expect('putglyph 1f1f2,1f1e8 2 5,0') + -- emoji with ZWJ and variant selectors, as one chunk reset(state, nil) push('🏳️‍🌈🏳️‍⚧️🏴‍☠️', vt)