diff --git a/src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.test.ts b/src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.test.ts index 55816bfb8e9..8ed8dbc5072 100644 --- a/src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.test.ts +++ b/src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.test.ts @@ -193,6 +193,21 @@ describe('installTerminalImeCandidateAnchor', () => { expect(harness.style).toEqual({ top: `${2 * CELL_HEIGHT}px`, left: `${7 * CELL_WIDTH}px` }) }) + it('refreshes the deferred metrics and anchor after a refit', () => { + const harness = createHarness() + harness.setLines(['Cursor Agent', '', '→ hello']) + installTerminalImeCandidateAnchor(harness.terminal) + typeHangulSyllable(harness, 0, 5, 1) + + Object.assign(harness.terminal, { cols: 40 }) + harness.setLines(['Cursor Agent', '', '', '→ hello']) + harness.style.top = '0px' + vi.runAllTimers() + + expect(harness.counts.rectReads).toBe(2) + expect(harness.style).toEqual({ top: `${3 * CELL_HEIGHT}px`, left: `${14 * CELL_WIDTH}px` }) + }) + it('stops writing once the textarea has been detached', () => { const harness = createHarness() harness.setLines(['Cursor Agent', '', '→ hello']) diff --git a/src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.ts b/src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.ts index 25b039480b6..e920dde780f 100644 --- a/src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.ts +++ b/src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.ts @@ -1,5 +1,5 @@ import type { Terminal } from '@xterm/xterm' -import { resolveCursorAgentImeAnchor } from './terminal-ime-anchor' +import { resolveCursorAgentImeAnchor, type TerminalImeAnchor } from './terminal-ime-anchor' type ImeAnchorCellMetrics = { cellWidth: number @@ -40,7 +40,6 @@ export function installTerminalImeCandidateAnchor(terminal: Terminal): (() => vo const textarea = terminal.textarea let metrics: ImeAnchorCellMetrics | null = null let deferredApply: number | null = null - let deferredAnchor: { row: number; column: number } | null = null const measureCells = (): ImeAnchorCellMetrics | null => { if (!screenElement) { @@ -69,6 +68,26 @@ export function installTerminalImeCandidateAnchor(terminal: Terminal): (() => vo writeOffset('left', `${column * cells.cellWidth}px`) } + const resolveAnchor = (): { anchor: TerminalImeAnchor; isCursorAgent: boolean } => { + const buf = terminal.buffer.active + // Why: Cursor Agent draws its prompt UI while leaving xterm's public cursor + // on a blank row, so the OS IME anchor needs the rendered prompt row instead. + const cursorAgentAnchor = resolveCursorAgentImeAnchor({ + buffer: buf, + rows: terminal.rows, + cols: terminal.cols, + cursorX: buf.cursorX, + cursorY: buf.cursorY + }) + return { + anchor: cursorAgentAnchor ?? { + row: buf.cursorY, + column: Math.min(buf.cursorX, terminal.cols - 1) + }, + isCursorAgent: cursorAgentAnchor !== null + } + } + const handler = (event?: Event): void => { if (!screenElement) { return @@ -84,33 +103,26 @@ export function installTerminalImeCandidateAnchor(terminal: Terminal): (() => vo if (!cells) { return } - const buf = terminal.buffer.active - // Why: Cursor Agent draws its prompt UI while leaving xterm's public cursor - // on a blank row, so the OS IME anchor needs the rendered prompt row instead. - const cursorAgentAnchor = resolveCursorAgentImeAnchor({ - buffer: buf, - rows: terminal.rows, - cols: terminal.cols, - cursorX: buf.cursorX, - cursorY: buf.cursorY - }) - const anchor = cursorAgentAnchor ?? { - row: buf.cursorY, - column: Math.min(buf.cursorX, terminal.cols - 1) - } + const { anchor, isCursorAgent } = resolveAnchor() applyAnchor(anchor.row, anchor.column, cells) // Why: xterm re-positions the textarea from a setTimeout(0) of its own after // each compositionupdate, so the correction has to land after that timer — // one pending timer per burst, re-reading the anchor when it fires. - if (!cursorAgentAnchor) { + if (!isCursorAgent) { return } - deferredAnchor = anchor if (deferredApply === null) { deferredApply = window.setTimeout(() => { deferredApply = null - if (textarea.isConnected && metrics && deferredAnchor) { - applyAnchor(deferredAnchor.row, deferredAnchor.column, metrics) + if (!textarea.isConnected) { + return + } + if (!metrics || metrics.cols !== terminal.cols || metrics.rows !== terminal.rows) { + metrics = measureCells() + } + if (metrics) { + const currentAnchor = resolveAnchor().anchor + applyAnchor(currentAnchor.row, currentAnchor.column, metrics) } }, 0) }