Skip to content

Commit f457fb6

Browse files
committed
fix(cli): fix multiline input navigation and history bugs
- Fix up/down arrow losing column position by adding cursorPosition to getOrSetStickyColumn deps - Fix Cmd+Left/Right going to buffer edges by reading lineInfo fresh inside callback - Restore combined Ctrl+A/E with Cmd+Left/Right key bindings for macOS terminal compatibility - Fix Ctrl+K incorrectly enabling history navigation (lastEditDueToNav should be false)
1 parent fac3349 commit f457fb6

File tree

1 file changed

+27
-12
lines changed

1 file changed

+27
-12
lines changed

cli/src/components/multiline-input.tsx

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,8 @@ export const MultilineInput = forwardRef<
168168
const stickyColumnRef = useRef<number | null>(null)
169169

170170
// Helper to get or set the sticky column for vertical navigation.
171-
// Note: Empty dependency array is intentional - we don't want to recreate this
172-
// on every cursor move since it reads cursorPosition from closure only when
173-
// stickyColumnRef.current is null.
171+
// When stickyColumnRef.current is set, we return it (preserving column across
172+
// multiple up/down presses). When null, we calculate from current cursor position.
174173
const getOrSetStickyColumn = useCallback(
175174
(lineStarts: number[], cursorIsChar: boolean): number => {
176175
if (stickyColumnRef.current != null) {
@@ -186,7 +185,7 @@ export const MultilineInput = forwardRef<
186185
stickyColumnRef.current = Math.max(0, column)
187186
return stickyColumnRef.current
188187
},
189-
[],
188+
[cursorPosition],
190189
)
191190

192191
// Update last activity on value or cursor changes
@@ -640,7 +639,7 @@ export const MultilineInput = forwardRef<
640639
preventKeyDefault(key)
641640
if (handleSelectionDeletion()) return true
642641
const newValue = value.slice(0, cursorPosition) + value.slice(lineEnd)
643-
onChange({ text: newValue, cursorPosition, lastEditDueToNav: true })
642+
onChange({ text: newValue, cursorPosition, lastEditDueToNav: false })
644643
return true
645644
}
646645

@@ -718,11 +717,29 @@ export const MultilineInput = forwardRef<
718717
(key: KeyEvent): boolean => {
719718
const lowerKeyName = (key.name ?? '').toLowerCase()
720719
const isAltLikeModifier = isAltModifier(key)
721-
const lineStart = findLineStart(value, cursorPosition)
722-
const lineEnd = findLineEnd(value, cursorPosition)
720+
const logicalLineStart = findLineStart(value, cursorPosition)
721+
const logicalLineEnd = findLineEnd(value, cursorPosition)
723722
const wordStart = findPreviousWordBoundary(value, cursorPosition)
724723
const wordEnd = findNextWordBoundary(value, cursorPosition)
725724

725+
// Read lineInfo inside the callback to get current value (not stale from closure)
726+
const currentLineInfo = textRef.current
727+
? ((textRef.current as any).textBufferView as TextBufferView)?.lineInfo
728+
: null
729+
730+
// Calculate visual line boundaries from lineInfo (accounts for word wrap)
731+
// Fall back to logical line boundaries if visual info is unavailable
732+
const lineStarts = currentLineInfo?.lineStarts ?? []
733+
const visualLineIndex = lineStarts.findLastIndex(
734+
(start) => start <= cursorPosition,
735+
)
736+
const visualLineStart = visualLineIndex >= 0
737+
? lineStarts[visualLineIndex]
738+
: logicalLineStart
739+
const visualLineEnd = lineStarts[visualLineIndex + 1] !== undefined
740+
? lineStarts[visualLineIndex + 1] - 1
741+
: logicalLineEnd
742+
726743
// Alt+Left/B: Word left
727744
if (
728745
isAltLikeModifier &&
@@ -760,7 +777,7 @@ export const MultilineInput = forwardRef<
760777
preventKeyDefault(key)
761778
onChange({
762779
text: value,
763-
cursorPosition: lineStart,
780+
cursorPosition: visualLineStart,
764781
lastEditDueToNav: false,
765782
})
766783
return true
@@ -775,7 +792,7 @@ export const MultilineInput = forwardRef<
775792
preventKeyDefault(key)
776793
onChange({
777794
text: value,
778-
cursorPosition: lineEnd,
795+
cursorPosition: visualLineEnd,
779796
lastEditDueToNav: false,
780797
})
781798
return true
@@ -844,7 +861,6 @@ export const MultilineInput = forwardRef<
844861
// Up arrow (no modifiers)
845862
if (key.name === 'up' && !key.ctrl && !key.meta && !key.option) {
846863
preventKeyDefault(key)
847-
const lineStarts = lineInfo?.lineStarts ?? []
848864
const desiredIndex = getOrSetStickyColumn(lineStarts, !shouldHighlight)
849865
onChange({
850866
text: value,
@@ -863,7 +879,6 @@ export const MultilineInput = forwardRef<
863879
// Down arrow (no modifiers)
864880
if (key.name === 'down' && !key.ctrl && !key.meta && !key.option) {
865881
preventKeyDefault(key)
866-
const lineStarts = lineInfo?.lineStarts ?? []
867882
const desiredIndex = getOrSetStickyColumn(lineStarts, !shouldHighlight)
868883
onChange({
869884
text: value,
@@ -881,7 +896,7 @@ export const MultilineInput = forwardRef<
881896

882897
return false
883898
},
884-
[value, cursorPosition, onChange, moveCursor, lineInfo, shouldHighlight, getOrSetStickyColumn],
899+
[value, cursorPosition, onChange, moveCursor, shouldHighlight, getOrSetStickyColumn],
885900
)
886901

887902
// Handle character input (regular chars and tab)

0 commit comments

Comments
 (0)