From 2f4e331ba685818589e885fbf01f1057a8441de7 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 23 May 2026 09:13:04 -0300 Subject: [PATCH] fix(selection): skip wide-character continuation cells when copying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the selection range covered text containing wide characters (CJK, fullwidth Latin, etc.), copying the selection inserted a stray space between every wide glyph — e.g. "안녕하" came out as "안 녕 하 ". Root cause: wide characters occupy two terminal cells. The first cell has the codepoint and width=2; the second cell is a continuation marker with codepoint=0 and width=0. SelectionManager.getSelection's empty-cell branch treated both empty cells AND continuation cells the same way and appended a space. Fix: skip continuation cells (cell exists with width===0) in the empty-cell branch. Only truly empty cells (no cell, or cell.width!==0 with codepoint===0) get a space. Ports only the selection-manager subset of upstream PR #120 — the rest of that PR (IME composition routing, textarea-focus refactor, removal of contenteditable) needs more analysis around regressions with browser extensions and is deferred to a separate port. Adds one regression test asserting that selecting "안녕하" copies as "안녕하", not "안 녕 하". Co-authored-by: Seungwoo Hong Inspired-by: https://github.com/coder/ghostty-web/pull/120 --- lib/selection-manager.test.ts | 23 +++++++++++++++++++++++ lib/selection-manager.ts | 10 +++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/selection-manager.test.ts b/lib/selection-manager.test.ts index 663abc01..1e4e6d7c 100644 --- a/lib/selection-manager.test.ts +++ b/lib/selection-manager.test.ts @@ -189,6 +189,29 @@ describe('SelectionManager', () => { term.dispose(); }); + test('getSelection does not insert spaces between wide (CJK) characters', async () => { + if (!container) return; + + const term = await createIsolatedTerminal({ cols: 80, rows: 24 }); + term.open(container); + + // Three Korean wide characters — each occupies 2 terminal cells: + // leading cell {codepoint: ..., width: 2} + continuation cell + // {codepoint: 0, width: 0}. The fix ensures we skip continuation + // cells instead of treating them as empty cells (which would + // produce "안 녕 하" with stray spaces between glyphs). + term.write('안녕하\r\n'); + + const scrollbackLen = term.wasmTerm!.getScrollbackLength(); + // Select the 6 cells covering all three wide chars + setSelectionAbsolute(term, 0, scrollbackLen, 5, scrollbackLen); + + const selMgr = (term as any).selectionManager; + expect(selMgr.getSelection()).toBe('안녕하'); + + term.dispose(); + }); + test('getSelection extracts multi-line text', async () => { if (!container) return; diff --git a/lib/selection-manager.ts b/lib/selection-manager.ts index 56d46059..86900fe1 100644 --- a/lib/selection-manager.ts +++ b/lib/selection-manager.ts @@ -183,7 +183,15 @@ export class SelectionManager { if (char.trim()) { lastNonEmpty = lineText.length; } - } else { + } else if (!cell || cell.width !== 0) { + // Only add a space for truly empty cells, NOT for wide-character + // continuation cells. Wide characters (CJK, fullwidth Latin, etc.) + // occupy 2 terminal cells: + // - First cell: codepoint set, width=2 + // - Second cell: codepoint=0, width=0 (continuation marker) + // The first branch above handles the leading cell. We must skip + // the trailing continuation cell here, otherwise the copied text + // gets a stray space between every wide character. lineText += ' '; } }