From 8cae90f404f999a50c7c653ce334cbf034bf95e7 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Mon, 2 Feb 2026 13:55:45 -0800 Subject: [PATCH] Fix up/down arrow regression and add test. (#18108) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../ui/components/shared/text-buffer.test.ts | 55 +++++++++++++++++++ .../src/ui/components/shared/text-buffer.ts | 6 +- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts index bec6cc5f58..93bed18c52 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.test.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts @@ -1384,6 +1384,61 @@ describe('useTextBuffer', () => { expect(state.visualCursor).toEqual([0, 1]); }); + it('move: up/down should work on wrapped lines (regression test)', () => { + // Line that wraps into two visual lines + // Viewport width 10. "0123456789ABCDE" (15 chars) + // Visual Line 0: "0123456789" + // Visual Line 1: "ABCDE" + const { result } = renderHook(() => + useTextBuffer({ + viewport: { width: 10, height: 5 }, + isValidPath: () => false, + }), + ); + + act(() => { + result.current.setText('0123456789ABCDE'); + }); + + // Cursor should be at the end: logical [0, 15], visual [1, 5] + expect(getBufferState(result).cursor).toEqual([0, 15]); + expect(getBufferState(result).visualCursor).toEqual([1, 5]); + + // Press Up arrow - should move to first visual line + // This currently fails because handleInput returns false if cursorRow === 0 + let handledUp = false; + act(() => { + handledUp = result.current.handleInput({ + name: 'up', + shift: false, + alt: false, + ctrl: false, + cmd: false, + insertable: false, + sequence: '\x1b[A', + }); + }); + expect(handledUp).toBe(true); + expect(getBufferState(result).visualCursor[0]).toBe(0); + + // Press Down arrow - should move back to second visual line + // This would also fail if cursorRow is the last logical row + let handledDown = false; + act(() => { + handledDown = result.current.handleInput({ + name: 'down', + shift: false, + alt: false, + ctrl: false, + cmd: false, + insertable: false, + sequence: '\x1b[B', + }); + }); + expect(handledDown).toBe(true); + expect(getBufferState(result).visualCursor[0]).toBe(1); + }); + it('moveToVisualPosition: should correctly handle wide characters (Chinese)', () => { const { result } = renderHook(() => useTextBuffer({ diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts index 6243f9d6d1..4d0956298c 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.ts @@ -2905,12 +2905,12 @@ export function useTextBuffer({ return true; } if (keyMatchers[Command.MOVE_UP](key)) { - if (cursorRow === 0) return false; + if (visualCursor[0] === 0) return false; move('up'); return true; } if (keyMatchers[Command.MOVE_DOWN](key)) { - if (cursorRow === lines.length - 1) return false; + if (visualCursor[0] === visualLines.length - 1) return false; move('down'); return true; } @@ -2990,6 +2990,8 @@ export function useTextBuffer({ singleLine, setText, text, + visualCursor, + visualLines, ], );