mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 06:04:40 +00:00
feat(tui): add sticky column for vertical cursor navigation (#1120)
When moving up/down through lines of varying lengths, the editor now remembers the original column position and restores it when reaching a line long enough to accommodate it. Example: cursor at column 10, move up to a shorter line (cursor clamps to end), move up again to a longer line - cursor returns to column 10. Implementation: - Add preferredVisualCol instance property (nullable) - Set it when moving to a shorter line during vertical navigation - Clear it when arriving at a line that fits the preferred column - Clear it on any horizontal movement or editing via setCursorCol() - Detect line rewrap by checking if cursor is in middle of line - When pressing right at end of prompt, set preferredVisualCol so subsequent up/down navigation uses that column position
This commit is contained in:
parent
675136f009
commit
d075291b08
2 changed files with 592 additions and 68 deletions
|
|
@ -199,6 +199,9 @@ export class Editor implements Component, Focusable {
|
|||
// Character jump mode
|
||||
private jumpMode: "forward" | "backward" | null = null;
|
||||
|
||||
// Preferred visual column for vertical cursor movement (sticky column)
|
||||
private preferredVisualCol: number | null = null;
|
||||
|
||||
// Undo support
|
||||
private undoStack: EditorState[] = [];
|
||||
|
||||
|
|
@ -303,7 +306,7 @@ export class Editor implements Component, Focusable {
|
|||
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
||||
this.state.lines = lines.length === 0 ? [""] : lines;
|
||||
this.state.cursorLine = this.state.lines.length - 1;
|
||||
this.state.cursorCol = this.state.lines[this.state.cursorLine]?.length || 0;
|
||||
this.setCursorCol(this.state.lines[this.state.cursorLine]?.length || 0);
|
||||
// Reset scroll - render() will adjust to show cursor
|
||||
this.scrollOffset = 0;
|
||||
|
||||
|
|
@ -523,7 +526,7 @@ export class Editor implements Component, Focusable {
|
|||
);
|
||||
this.state.lines = result.lines;
|
||||
this.state.cursorLine = result.cursorLine;
|
||||
this.state.cursorCol = result.cursorCol;
|
||||
this.setCursorCol(result.cursorCol);
|
||||
this.cancelAutocomplete();
|
||||
if (this.onChange) this.onChange(this.getText());
|
||||
}
|
||||
|
|
@ -544,7 +547,7 @@ export class Editor implements Component, Focusable {
|
|||
);
|
||||
this.state.lines = result.lines;
|
||||
this.state.cursorLine = result.cursorLine;
|
||||
this.state.cursorCol = result.cursorCol;
|
||||
this.setCursorCol(result.cursorCol);
|
||||
|
||||
if (this.autocompletePrefix.startsWith("/")) {
|
||||
this.cancelAutocomplete();
|
||||
|
|
@ -890,7 +893,7 @@ export class Editor implements Component, Focusable {
|
|||
if (insertedLines.length === 1) {
|
||||
// Single line - insert at cursor position
|
||||
this.state.lines[this.state.cursorLine] = beforeCursor + normalized + afterCursor;
|
||||
this.state.cursorCol += normalized.length;
|
||||
this.setCursorCol(this.state.cursorCol + normalized.length);
|
||||
} else {
|
||||
// Multi-line insertion
|
||||
this.state.lines = [
|
||||
|
|
@ -911,7 +914,7 @@ export class Editor implements Component, Focusable {
|
|||
];
|
||||
|
||||
this.state.cursorLine += insertedLines.length - 1;
|
||||
this.state.cursorCol = (insertedLines[insertedLines.length - 1] || "").length;
|
||||
this.setCursorCol((insertedLines[insertedLines.length - 1] || "").length);
|
||||
}
|
||||
|
||||
if (this.onChange) {
|
||||
|
|
@ -941,7 +944,7 @@ export class Editor implements Component, Focusable {
|
|||
const after = line.slice(this.state.cursorCol);
|
||||
|
||||
this.state.lines[this.state.cursorLine] = before + char + after;
|
||||
this.state.cursorCol += char.length; // Fix: increment by the length of the inserted string
|
||||
this.setCursorCol(this.state.cursorCol + char.length);
|
||||
|
||||
if (this.onChange) {
|
||||
this.onChange(this.getText());
|
||||
|
|
@ -1058,7 +1061,7 @@ export class Editor implements Component, Focusable {
|
|||
|
||||
// Move cursor to start of new line
|
||||
this.state.cursorLine++;
|
||||
this.state.cursorCol = 0;
|
||||
this.setCursorCol(0);
|
||||
|
||||
if (this.onChange) {
|
||||
this.onChange(this.getText());
|
||||
|
|
@ -1085,7 +1088,7 @@ export class Editor implements Component, Focusable {
|
|||
const after = line.slice(this.state.cursorCol);
|
||||
|
||||
this.state.lines[this.state.cursorLine] = before + after;
|
||||
this.state.cursorCol -= graphemeLength;
|
||||
this.setCursorCol(this.state.cursorCol - graphemeLength);
|
||||
} else if (this.state.cursorLine > 0) {
|
||||
this.pushUndoSnapshot();
|
||||
|
||||
|
|
@ -1097,7 +1100,7 @@ export class Editor implements Component, Focusable {
|
|||
this.state.lines.splice(this.state.cursorLine, 1);
|
||||
|
||||
this.state.cursorLine--;
|
||||
this.state.cursorCol = previousLine.length;
|
||||
this.setCursorCol(previousLine.length);
|
||||
}
|
||||
|
||||
if (this.onChange) {
|
||||
|
|
@ -1122,15 +1125,117 @@ export class Editor implements Component, Focusable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cursor column and clear preferredVisualCol.
|
||||
* Use this for all non-vertical cursor movements to reset sticky column behavior.
|
||||
*/
|
||||
private setCursorCol(col: number): void {
|
||||
this.state.cursorCol = col;
|
||||
this.preferredVisualCol = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move cursor to a target visual line, applying sticky column logic.
|
||||
* Shared by moveCursor() and pageScroll().
|
||||
*/
|
||||
private moveToVisualLine(
|
||||
visualLines: Array<{ logicalLine: number; startCol: number; length: number }>,
|
||||
currentVisualLine: number,
|
||||
targetVisualLine: number,
|
||||
): void {
|
||||
const currentVL = visualLines[currentVisualLine];
|
||||
const targetVL = visualLines[targetVisualLine];
|
||||
|
||||
if (currentVL && targetVL) {
|
||||
const currentVisualCol = this.state.cursorCol - currentVL.startCol;
|
||||
|
||||
// For non-last segments, clamp to length-1 to stay within the segment
|
||||
const isLastSourceSegment =
|
||||
currentVisualLine === visualLines.length - 1 ||
|
||||
visualLines[currentVisualLine + 1]?.logicalLine !== currentVL.logicalLine;
|
||||
const sourceMaxVisualCol = isLastSourceSegment ? currentVL.length : Math.max(0, currentVL.length - 1);
|
||||
|
||||
const isLastTargetSegment =
|
||||
targetVisualLine === visualLines.length - 1 ||
|
||||
visualLines[targetVisualLine + 1]?.logicalLine !== targetVL.logicalLine;
|
||||
const targetMaxVisualCol = isLastTargetSegment ? targetVL.length : Math.max(0, targetVL.length - 1);
|
||||
|
||||
const moveToVisualCol = this.computeVerticalMoveColumn(
|
||||
currentVisualCol,
|
||||
sourceMaxVisualCol,
|
||||
targetMaxVisualCol,
|
||||
);
|
||||
|
||||
// Set cursor position
|
||||
this.state.cursorLine = targetVL.logicalLine;
|
||||
const targetCol = targetVL.startCol + moveToVisualCol;
|
||||
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
||||
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the target visual column for vertical cursor movement.
|
||||
* Implements the sticky column decision table:
|
||||
*
|
||||
* | P | S | T | U | Scenario | Set Preferred | Move To |
|
||||
* |---|---|---|---| ---------------------------------------------------- |---------------|-------------|
|
||||
* | 0 | * | 0 | - | Start nav, target fits | null | current |
|
||||
* | 0 | * | 1 | - | Start nav, target shorter | current | target end |
|
||||
* | 1 | 0 | 0 | 0 | Clamped, target fits preferred | null | preferred |
|
||||
* | 1 | 0 | 0 | 1 | Clamped, target longer but still can't fit preferred | keep | target end |
|
||||
* | 1 | 0 | 1 | - | Clamped, target even shorter | keep | target end |
|
||||
* | 1 | 1 | 0 | - | Rewrapped, target fits current | null | current |
|
||||
* | 1 | 1 | 1 | - | Rewrapped, target shorter than current | current | target end |
|
||||
*
|
||||
* Where:
|
||||
* - P = preferred col is set
|
||||
* - S = cursor in middle of source line (not clamped to end)
|
||||
* - T = target line shorter than current visual col
|
||||
* - U = target line shorter than preferred col
|
||||
*/
|
||||
private computeVerticalMoveColumn(
|
||||
currentVisualCol: number,
|
||||
sourceMaxVisualCol: number,
|
||||
targetMaxVisualCol: number,
|
||||
): number {
|
||||
const hasPreferred = this.preferredVisualCol !== null; // P
|
||||
const cursorInMiddle = currentVisualCol < sourceMaxVisualCol; // S
|
||||
const targetTooShort = targetMaxVisualCol < currentVisualCol; // T
|
||||
|
||||
if (!hasPreferred || cursorInMiddle) {
|
||||
if (targetTooShort) {
|
||||
// Cases 2 and 7
|
||||
this.preferredVisualCol = currentVisualCol;
|
||||
return targetMaxVisualCol;
|
||||
}
|
||||
|
||||
// Cases 1 and 6
|
||||
this.preferredVisualCol = null;
|
||||
return currentVisualCol;
|
||||
}
|
||||
|
||||
const targetCantFitPreferred = targetMaxVisualCol < this.preferredVisualCol!; // U
|
||||
if (targetTooShort || targetCantFitPreferred) {
|
||||
// Cases 4 and 5
|
||||
return targetMaxVisualCol;
|
||||
}
|
||||
|
||||
// Case 3
|
||||
const result = this.preferredVisualCol!;
|
||||
this.preferredVisualCol = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
private moveToLineStart(): void {
|
||||
this.lastAction = null;
|
||||
this.state.cursorCol = 0;
|
||||
this.setCursorCol(0);
|
||||
}
|
||||
|
||||
private moveToLineEnd(): void {
|
||||
this.lastAction = null;
|
||||
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
||||
this.state.cursorCol = currentLine.length;
|
||||
this.setCursorCol(currentLine.length);
|
||||
}
|
||||
|
||||
private deleteToStartOfLine(): void {
|
||||
|
|
@ -1148,7 +1253,7 @@ export class Editor implements Component, Focusable {
|
|||
|
||||
// Delete from start of line up to cursor
|
||||
this.state.lines[this.state.cursorLine] = currentLine.slice(this.state.cursorCol);
|
||||
this.state.cursorCol = 0;
|
||||
this.setCursorCol(0);
|
||||
} else if (this.state.cursorLine > 0) {
|
||||
this.pushUndoSnapshot();
|
||||
|
||||
|
|
@ -1160,7 +1265,7 @@ export class Editor implements Component, Focusable {
|
|||
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
||||
this.state.lines.splice(this.state.cursorLine, 1);
|
||||
this.state.cursorLine--;
|
||||
this.state.cursorCol = previousLine.length;
|
||||
this.setCursorCol(previousLine.length);
|
||||
}
|
||||
|
||||
if (this.onChange) {
|
||||
|
|
@ -1218,7 +1323,7 @@ export class Editor implements Component, Focusable {
|
|||
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
||||
this.state.lines.splice(this.state.cursorLine, 1);
|
||||
this.state.cursorLine--;
|
||||
this.state.cursorCol = previousLine.length;
|
||||
this.setCursorCol(previousLine.length);
|
||||
}
|
||||
} else {
|
||||
this.pushUndoSnapshot();
|
||||
|
|
@ -1229,7 +1334,7 @@ export class Editor implements Component, Focusable {
|
|||
const oldCursorCol = this.state.cursorCol;
|
||||
this.moveWordBackwards();
|
||||
const deleteFrom = this.state.cursorCol;
|
||||
this.state.cursorCol = oldCursorCol;
|
||||
this.setCursorCol(oldCursorCol);
|
||||
|
||||
// Restore kill state for accumulation check, then save to kill ring
|
||||
this.lastAction = wasKill ? "kill" : null;
|
||||
|
|
@ -1239,7 +1344,7 @@ export class Editor implements Component, Focusable {
|
|||
|
||||
this.state.lines[this.state.cursorLine] =
|
||||
currentLine.slice(0, deleteFrom) + currentLine.slice(this.state.cursorCol);
|
||||
this.state.cursorCol = deleteFrom;
|
||||
this.setCursorCol(deleteFrom);
|
||||
}
|
||||
|
||||
if (this.onChange) {
|
||||
|
|
@ -1274,7 +1379,7 @@ export class Editor implements Component, Focusable {
|
|||
const oldCursorCol = this.state.cursorCol;
|
||||
this.moveWordForwards();
|
||||
const deleteTo = this.state.cursorCol;
|
||||
this.state.cursorCol = oldCursorCol;
|
||||
this.setCursorCol(oldCursorCol);
|
||||
|
||||
// Restore kill state for accumulation check, then save to kill ring
|
||||
this.lastAction = wasKill ? "kill" : null;
|
||||
|
|
@ -1401,29 +1506,14 @@ export class Editor implements Component, Focusable {
|
|||
|
||||
private moveCursor(deltaLine: number, deltaCol: number): void {
|
||||
this.lastAction = null;
|
||||
const width = this.lastWidth;
|
||||
const visualLines = this.buildVisualLineMap(this.lastWidth);
|
||||
const currentVisualLine = this.findCurrentVisualLine(visualLines);
|
||||
|
||||
if (deltaLine !== 0) {
|
||||
// Build visual line map for navigation
|
||||
const visualLines = this.buildVisualLineMap(width);
|
||||
const currentVisualLine = this.findCurrentVisualLine(visualLines);
|
||||
|
||||
// Calculate column position within current visual line
|
||||
const currentVL = visualLines[currentVisualLine];
|
||||
const visualCol = currentVL ? this.state.cursorCol - currentVL.startCol : 0;
|
||||
|
||||
// Move to target visual line
|
||||
const targetVisualLine = currentVisualLine + deltaLine;
|
||||
|
||||
if (targetVisualLine >= 0 && targetVisualLine < visualLines.length) {
|
||||
const targetVL = visualLines[targetVisualLine];
|
||||
if (targetVL) {
|
||||
this.state.cursorLine = targetVL.logicalLine;
|
||||
// Try to maintain visual column position, clamped to line length
|
||||
const targetCol = targetVL.startCol + Math.min(visualCol, targetVL.length);
|
||||
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
||||
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
||||
}
|
||||
this.moveToVisualLine(visualLines, currentVisualLine, targetVisualLine);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1436,11 +1526,17 @@ export class Editor implements Component, Focusable {
|
|||
const afterCursor = currentLine.slice(this.state.cursorCol);
|
||||
const graphemes = [...segmenter.segment(afterCursor)];
|
||||
const firstGrapheme = graphemes[0];
|
||||
this.state.cursorCol += firstGrapheme ? firstGrapheme.segment.length : 1;
|
||||
this.setCursorCol(this.state.cursorCol + (firstGrapheme ? firstGrapheme.segment.length : 1));
|
||||
} else if (this.state.cursorLine < this.state.lines.length - 1) {
|
||||
// Wrap to start of next logical line
|
||||
this.state.cursorLine++;
|
||||
this.state.cursorCol = 0;
|
||||
this.setCursorCol(0);
|
||||
} else {
|
||||
// At end of last line - can't move, but set preferredVisualCol for up/down navigation
|
||||
const currentVL = visualLines[currentVisualLine];
|
||||
if (currentVL) {
|
||||
this.preferredVisualCol = this.state.cursorCol - currentVL.startCol;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Moving left - move by one grapheme (handles emojis, combining characters, etc.)
|
||||
|
|
@ -1448,12 +1544,12 @@ export class Editor implements Component, Focusable {
|
|||
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
||||
const graphemes = [...segmenter.segment(beforeCursor)];
|
||||
const lastGrapheme = graphemes[graphemes.length - 1];
|
||||
this.state.cursorCol -= lastGrapheme ? lastGrapheme.segment.length : 1;
|
||||
this.setCursorCol(this.state.cursorCol - (lastGrapheme ? lastGrapheme.segment.length : 1));
|
||||
} else if (this.state.cursorLine > 0) {
|
||||
// Wrap to end of previous logical line
|
||||
this.state.cursorLine--;
|
||||
const prevLine = this.state.lines[this.state.cursorLine] || "";
|
||||
this.state.cursorCol = prevLine.length;
|
||||
this.setCursorCol(prevLine.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1465,29 +1561,14 @@ export class Editor implements Component, Focusable {
|
|||
*/
|
||||
private pageScroll(direction: -1 | 1): void {
|
||||
this.lastAction = null;
|
||||
const width = this.lastWidth;
|
||||
const terminalRows = this.tui.terminal.rows;
|
||||
const pageSize = Math.max(5, Math.floor(terminalRows * 0.3));
|
||||
|
||||
// Build visual line map
|
||||
const visualLines = this.buildVisualLineMap(width);
|
||||
const visualLines = this.buildVisualLineMap(this.lastWidth);
|
||||
const currentVisualLine = this.findCurrentVisualLine(visualLines);
|
||||
|
||||
// Calculate target visual line
|
||||
const targetVisualLine = Math.max(0, Math.min(visualLines.length - 1, currentVisualLine + direction * pageSize));
|
||||
|
||||
// Move cursor to target visual line
|
||||
const targetVL = visualLines[targetVisualLine];
|
||||
if (targetVL) {
|
||||
// Preserve column position within the line
|
||||
const currentVL = visualLines[currentVisualLine];
|
||||
const visualCol = currentVL ? this.state.cursorCol - currentVL.startCol : 0;
|
||||
|
||||
this.state.cursorLine = targetVL.logicalLine;
|
||||
const targetCol = targetVL.startCol + Math.min(visualCol, targetVL.length);
|
||||
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
||||
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
||||
}
|
||||
this.moveToVisualLine(visualLines, currentVisualLine, targetVisualLine);
|
||||
}
|
||||
|
||||
private moveWordBackwards(): void {
|
||||
|
|
@ -1499,7 +1580,7 @@ export class Editor implements Component, Focusable {
|
|||
if (this.state.cursorLine > 0) {
|
||||
this.state.cursorLine--;
|
||||
const prevLine = this.state.lines[this.state.cursorLine] || "";
|
||||
this.state.cursorCol = prevLine.length;
|
||||
this.setCursorCol(prevLine.length);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -1532,7 +1613,7 @@ export class Editor implements Component, Focusable {
|
|||
}
|
||||
}
|
||||
|
||||
this.state.cursorCol = newCol;
|
||||
this.setCursorCol(newCol);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1586,7 +1667,7 @@ export class Editor implements Component, Focusable {
|
|||
const before = currentLine.slice(0, this.state.cursorCol);
|
||||
const after = currentLine.slice(this.state.cursorCol);
|
||||
this.state.lines[this.state.cursorLine] = before + text + after;
|
||||
this.state.cursorCol += text.length;
|
||||
this.setCursorCol(this.state.cursorCol + text.length);
|
||||
} else {
|
||||
// Multi-line insert
|
||||
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
||||
|
|
@ -1607,7 +1688,7 @@ export class Editor implements Component, Focusable {
|
|||
|
||||
// Update cursor position
|
||||
this.state.cursorLine = lastLineIndex;
|
||||
this.state.cursorCol = (lines[lines.length - 1] || "").length;
|
||||
this.setCursorCol((lines[lines.length - 1] || "").length);
|
||||
}
|
||||
|
||||
if (this.onChange) {
|
||||
|
|
@ -1632,7 +1713,7 @@ export class Editor implements Component, Focusable {
|
|||
const before = currentLine.slice(0, this.state.cursorCol - deleteLen);
|
||||
const after = currentLine.slice(this.state.cursorCol);
|
||||
this.state.lines[this.state.cursorLine] = before + after;
|
||||
this.state.cursorCol -= deleteLen;
|
||||
this.setCursorCol(this.state.cursorCol - deleteLen);
|
||||
} else {
|
||||
// Multi-line delete - cursor is at end of last yanked line
|
||||
const startLine = this.state.cursorLine - (yankLines.length - 1);
|
||||
|
|
@ -1649,7 +1730,7 @@ export class Editor implements Component, Focusable {
|
|||
|
||||
// Update cursor
|
||||
this.state.cursorLine = startLine;
|
||||
this.state.cursorCol = startCol;
|
||||
this.setCursorCol(startCol);
|
||||
}
|
||||
|
||||
if (this.onChange) {
|
||||
|
|
@ -1698,6 +1779,7 @@ export class Editor implements Component, Focusable {
|
|||
const snapshot = this.undoStack.pop()!;
|
||||
this.restoreUndoSnapshot(snapshot);
|
||||
this.lastAction = null;
|
||||
this.preferredVisualCol = null;
|
||||
if (this.onChange) {
|
||||
this.onChange(this.getText());
|
||||
}
|
||||
|
|
@ -1730,7 +1812,7 @@ export class Editor implements Component, Focusable {
|
|||
|
||||
if (idx !== -1) {
|
||||
this.state.cursorLine = lineIdx;
|
||||
this.state.cursorCol = idx;
|
||||
this.setCursorCol(idx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1745,7 +1827,7 @@ export class Editor implements Component, Focusable {
|
|||
if (this.state.cursorCol >= currentLine.length) {
|
||||
if (this.state.cursorLine < this.state.lines.length - 1) {
|
||||
this.state.cursorLine++;
|
||||
this.state.cursorCol = 0;
|
||||
this.setCursorCol(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -1754,10 +1836,11 @@ export class Editor implements Component, Focusable {
|
|||
const segments = segmenter.segment(textAfterCursor);
|
||||
const iterator = segments[Symbol.iterator]();
|
||||
let next = iterator.next();
|
||||
let newCol = this.state.cursorCol;
|
||||
|
||||
// Skip leading whitespace
|
||||
while (!next.done && isWhitespaceChar(next.value.segment)) {
|
||||
this.state.cursorCol += next.value.segment.length;
|
||||
newCol += next.value.segment.length;
|
||||
next = iterator.next();
|
||||
}
|
||||
|
||||
|
|
@ -1766,17 +1849,19 @@ export class Editor implements Component, Focusable {
|
|||
if (isPunctuationChar(firstGrapheme)) {
|
||||
// Skip punctuation run
|
||||
while (!next.done && isPunctuationChar(next.value.segment)) {
|
||||
this.state.cursorCol += next.value.segment.length;
|
||||
newCol += next.value.segment.length;
|
||||
next = iterator.next();
|
||||
}
|
||||
} else {
|
||||
// Skip word run
|
||||
while (!next.done && !isWhitespaceChar(next.value.segment) && !isPunctuationChar(next.value.segment)) {
|
||||
this.state.cursorCol += next.value.segment.length;
|
||||
newCol += next.value.segment.length;
|
||||
next = iterator.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setCursorCol(newCol);
|
||||
}
|
||||
|
||||
// Slash menu only allowed when all other lines are empty (no mixed content)
|
||||
|
|
@ -1886,7 +1971,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|||
);
|
||||
this.state.lines = result.lines;
|
||||
this.state.cursorLine = result.cursorLine;
|
||||
this.state.cursorCol = result.cursorCol;
|
||||
this.setCursorCol(result.cursorCol);
|
||||
if (this.onChange) this.onChange(this.getText());
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2186,4 +2186,443 @@ describe("Editor component", () => {
|
|||
assert.strictEqual(editor.getText(), "xhello world");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sticky column", () => {
|
||||
it("preserves target column when moving up through a shorter line", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
// Line 0: "2222222222x222" (x at col 10)
|
||||
// Line 1: "" (empty)
|
||||
// Line 2: "1111111111_111111111111" (_ at col 10)
|
||||
editor.setText("2222222222x222\n\n1111111111_111111111111");
|
||||
|
||||
// Position cursor on _ (line 2, col 10)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 23 }); // At end
|
||||
editor.handleInput("\x01"); // Ctrl+A - go to start of line
|
||||
for (let i = 0; i < 10; i++) editor.handleInput("\x1b[C"); // Move right to col 10
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 10 });
|
||||
|
||||
// Press Up - should move to empty line (col clamped to 0)
|
||||
editor.handleInput("\x1b[A"); // Up arrow
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 1, col: 0 });
|
||||
|
||||
// Press Up again - should move to line 0 at col 10 (on 'x')
|
||||
editor.handleInput("\x1b[A"); // Up arrow
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 10 });
|
||||
});
|
||||
|
||||
it("preserves target column when moving down through a shorter line", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("1111111111_111\n\n2222222222x222222222222");
|
||||
|
||||
// Position cursor on _ (line 0, col 10)
|
||||
editor.handleInput("\x1b[A"); // Up to line 1
|
||||
editor.handleInput("\x1b[A"); // Up to line 0
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 10; i++) editor.handleInput("\x1b[C");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 10 });
|
||||
|
||||
// Press Down - should move to empty line (col clamped to 0)
|
||||
editor.handleInput("\x1b[B"); // Down arrow
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 1, col: 0 });
|
||||
|
||||
// Press Down again - should move to line 2 at col 10 (on 'x')
|
||||
editor.handleInput("\x1b[B"); // Down arrow
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 10 });
|
||||
});
|
||||
|
||||
it("resets sticky column on horizontal movement (left arrow)", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("1234567890\n\n1234567890");
|
||||
|
||||
// Start at line 2, col 5
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 5; i++) editor.handleInput("\x1b[C");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 5 });
|
||||
|
||||
// Move up through empty line
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 0
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 5 (sticky)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 5 });
|
||||
|
||||
// Move left - resets sticky column
|
||||
editor.handleInput("\x1b[D"); // Left
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 4 });
|
||||
|
||||
// Move down twice
|
||||
editor.handleInput("\x1b[B"); // Down - line 1, col 0
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col 4 (new sticky from col 4)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 4 });
|
||||
});
|
||||
|
||||
it("resets sticky column on horizontal movement (right arrow)", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("1234567890\n\n1234567890");
|
||||
|
||||
// Start at line 0, col 5
|
||||
editor.handleInput("\x1b[A"); // Up to line 1
|
||||
editor.handleInput("\x1b[A"); // Up to line 0
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 5; i++) editor.handleInput("\x1b[C");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 5 });
|
||||
|
||||
// Move down through empty line
|
||||
editor.handleInput("\x1b[B"); // Down - line 1, col 0
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col 5 (sticky)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 5 });
|
||||
|
||||
// Move right - resets sticky column
|
||||
editor.handleInput("\x1b[C"); // Right
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 6 });
|
||||
|
||||
// Move up twice
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 0
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 6 (new sticky from col 6)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 6 });
|
||||
});
|
||||
|
||||
it("resets sticky column on typing", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("1234567890\n\n1234567890");
|
||||
|
||||
// Start at line 2, col 8
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 8; i++) editor.handleInput("\x1b[C");
|
||||
|
||||
// Move up through empty line
|
||||
editor.handleInput("\x1b[A"); // Up
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 8
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 8 });
|
||||
|
||||
// Type a character - resets sticky column
|
||||
editor.handleInput("X");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 9 });
|
||||
|
||||
// Move down twice
|
||||
editor.handleInput("\x1b[B"); // Down - line 1, col 0
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col 9 (new sticky from col 9)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 9 });
|
||||
});
|
||||
|
||||
it("resets sticky column on backspace", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("1234567890\n\n1234567890");
|
||||
|
||||
// Start at line 2, col 8
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 8; i++) editor.handleInput("\x1b[C");
|
||||
|
||||
// Move up through empty line
|
||||
editor.handleInput("\x1b[A"); // Up
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 8
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 8 });
|
||||
|
||||
// Backspace - resets sticky column
|
||||
editor.handleInput("\x7f"); // Backspace
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 7 });
|
||||
|
||||
// Move down twice
|
||||
editor.handleInput("\x1b[B"); // Down - line 1, col 0
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col 7 (new sticky from col 7)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 7 });
|
||||
});
|
||||
|
||||
it("resets sticky column on Ctrl+A (move to line start)", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("1234567890\n\n1234567890");
|
||||
|
||||
// Start at line 2, col 8
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 8; i++) editor.handleInput("\x1b[C");
|
||||
|
||||
// Move up - establishes sticky col 8
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 0
|
||||
|
||||
// Ctrl+A - resets sticky column to 0
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 1, col: 0 });
|
||||
|
||||
// Move up
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 0 (new sticky from col 0)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 0 });
|
||||
});
|
||||
|
||||
it("resets sticky column on Ctrl+E (move to line end)", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("12345\n\n1234567890");
|
||||
|
||||
// Start at line 2, col 3
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 3; i++) editor.handleInput("\x1b[C");
|
||||
|
||||
// Move up through empty line - establishes sticky col 3
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 0
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 3
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 3 });
|
||||
|
||||
// Ctrl+E - resets sticky column to end
|
||||
editor.handleInput("\x05"); // Ctrl+E
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 5 });
|
||||
|
||||
// Move down twice
|
||||
editor.handleInput("\x1b[B"); // Down - line 1, col 0
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col 5 (new sticky from col 5)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 5 });
|
||||
});
|
||||
|
||||
it("resets sticky column on word movement (Ctrl+Left)", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("hello world\n\nhello world");
|
||||
|
||||
// Start at end of line 2 (col 11)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 11 });
|
||||
|
||||
// Move up through empty line - establishes sticky col 11
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 0
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 11
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 11 });
|
||||
|
||||
// Ctrl+Left - word movement resets sticky column
|
||||
editor.handleInput("\x1b[1;5D"); // Ctrl+Left
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 6 }); // Before "world"
|
||||
|
||||
// Move down twice
|
||||
editor.handleInput("\x1b[B"); // Down - line 1, col 0
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col 6 (new sticky from col 6)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 6 });
|
||||
});
|
||||
|
||||
it("resets sticky column on word movement (Ctrl+Right)", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("hello world\n\nhello world");
|
||||
|
||||
// Start at line 0, col 0
|
||||
editor.handleInput("\x1b[A"); // Up
|
||||
editor.handleInput("\x1b[A"); // Up
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 0 });
|
||||
|
||||
// Move down through empty line - establishes sticky col 0
|
||||
editor.handleInput("\x1b[B"); // Down - line 1, col 0
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col 0
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 0 });
|
||||
|
||||
// Ctrl+Right - word movement resets sticky column
|
||||
editor.handleInput("\x1b[1;5C"); // Ctrl+Right
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 5 }); // After "hello"
|
||||
|
||||
// Move up twice
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 0
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 5 (new sticky from col 5)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 5 });
|
||||
});
|
||||
|
||||
it("resets sticky column on undo", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("1234567890\n\n1234567890");
|
||||
|
||||
// Go to line 0, col 8
|
||||
editor.handleInput("\x1b[A"); // Up to line 1
|
||||
editor.handleInput("\x1b[A"); // Up to line 0
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 8; i++) editor.handleInput("\x1b[C");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 8 });
|
||||
|
||||
// Move down through empty line - establishes sticky col 8
|
||||
editor.handleInput("\x1b[B"); // Down - line 1, col 0
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col 8 (sticky)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 8 });
|
||||
|
||||
// Type something to create undo state - this clears sticky and sets col to 9
|
||||
editor.handleInput("X");
|
||||
assert.strictEqual(editor.getText(), "1234567890\n\n12345678X90");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 9 });
|
||||
|
||||
// Move up - establishes new sticky col 9
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 0
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 9
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 9 });
|
||||
|
||||
// Undo - resets sticky column and restores cursor to line 2, col 8
|
||||
editor.handleInput("\x1b[45;5u"); // Ctrl+- (undo)
|
||||
assert.strictEqual(editor.getText(), "1234567890\n\n1234567890");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 8 });
|
||||
|
||||
// Move up - should capture new sticky from restored col 8, not old col 9
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 0
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 8 (new sticky from restored position)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 8 });
|
||||
});
|
||||
|
||||
it("handles multiple consecutive up/down movements", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("1234567890\nab\ncd\nef\n1234567890");
|
||||
|
||||
// Start at line 4, col 7
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 7; i++) editor.handleInput("\x1b[C");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 4, col: 7 });
|
||||
|
||||
// Move up multiple times through short lines
|
||||
editor.handleInput("\x1b[A"); // Up - line 3, col 2 (clamped)
|
||||
editor.handleInput("\x1b[A"); // Up - line 2, col 2 (clamped)
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 2 (clamped)
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 7 (restored)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 7 });
|
||||
|
||||
// Move down multiple times - sticky should still be 7
|
||||
editor.handleInput("\x1b[B"); // Down - line 1, col 2
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col 2
|
||||
editor.handleInput("\x1b[B"); // Down - line 3, col 2
|
||||
editor.handleInput("\x1b[B"); // Down - line 4, col 7 (restored)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 4, col: 7 });
|
||||
});
|
||||
|
||||
it("moves correctly through wrapped visual lines without getting stuck", () => {
|
||||
const tui = createTestTUI(15, 24); // Narrow terminal
|
||||
const editor = new Editor(tui, defaultEditorTheme);
|
||||
|
||||
// Line 0: short
|
||||
// Line 1: 30 chars = wraps to 3 visual lines at width 10 (after padding)
|
||||
editor.setText("short\n123456789012345678901234567890");
|
||||
editor.render(15); // This gives 14 layout width
|
||||
|
||||
// Position at end of line 1 (col 30)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 1, col: 30 });
|
||||
|
||||
// Move up repeatedly - should traverse all visual lines of the wrapped text
|
||||
// and eventually reach line 0
|
||||
editor.handleInput("\x1b[A"); // Up - to previous visual line within line 1
|
||||
assert.strictEqual(editor.getCursor().line, 1);
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - another visual line
|
||||
assert.strictEqual(editor.getCursor().line, 1);
|
||||
|
||||
editor.handleInput("\x1b[A"); // Up - should reach line 0
|
||||
assert.strictEqual(editor.getCursor().line, 0);
|
||||
});
|
||||
|
||||
it("handles setText resetting sticky column", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.setText("1234567890\n\n1234567890");
|
||||
|
||||
// Establish sticky column
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 8; i++) editor.handleInput("\x1b[C");
|
||||
editor.handleInput("\x1b[A"); // Up
|
||||
|
||||
// setText should reset sticky column
|
||||
editor.setText("abcdefghij\n\nabcdefghij");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 10 }); // At end
|
||||
|
||||
// Move up - should capture new sticky from current position (10)
|
||||
editor.handleInput("\x1b[A"); // Up - line 1, col 0
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 10
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 10 });
|
||||
});
|
||||
|
||||
it("sets preferredVisualCol when pressing right at end of prompt (last line)", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
// Line 0: 20 chars with 'x' at col 10
|
||||
// Line 1: empty
|
||||
// Line 2: 10 chars ending with '_'
|
||||
editor.setText("111111111x1111111111\n\n333333333_");
|
||||
|
||||
// Go to line 0, press Ctrl+E (end of line) - col 20
|
||||
editor.handleInput("\x1b[A"); // Up to line 1
|
||||
editor.handleInput("\x1b[A"); // Up to line 0
|
||||
editor.handleInput("\x05"); // Ctrl+E - move to end of line
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 20 });
|
||||
|
||||
// Move down to line 2 - cursor clamped to col 10 (end of line)
|
||||
editor.handleInput("\x1b[B"); // Down to line 1, col 0
|
||||
editor.handleInput("\x1b[B"); // Down to line 2, col 10 (clamped)
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 10 });
|
||||
|
||||
// Press Right at end of prompt - nothing visible happens, but sets preferredVisualCol to 10
|
||||
editor.handleInput("\x1b[C"); // Right - can't move, but sets preferredVisualCol
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 2, col: 10 }); // Still at same position
|
||||
|
||||
// Move up twice to line 0 - should use preferredVisualCol (10) to land on 'x'
|
||||
editor.handleInput("\x1b[A"); // Up to line 1, col 0
|
||||
editor.handleInput("\x1b[A"); // Up to line 0, col 10 (on 'x')
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 10 });
|
||||
});
|
||||
|
||||
it("handles editor resizes when preferredVisualCol is on the same line", () => {
|
||||
// Create editor with wider terminal
|
||||
const tui = createTestTUI(80, 24);
|
||||
const editor = new Editor(tui, defaultEditorTheme);
|
||||
|
||||
editor.setText("12345678901234567890\n\n12345678901234567890");
|
||||
|
||||
// Start at line 2, col 15
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 15; i++) editor.handleInput("\x1b[C");
|
||||
|
||||
// Move up through empty line - establishes sticky col 15
|
||||
editor.handleInput("\x1b[A"); // Up
|
||||
editor.handleInput("\x1b[A"); // Up - line 0, col 15
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 15 });
|
||||
|
||||
// Render with narrower width to simulate resize
|
||||
editor.render(12); // Width 12
|
||||
|
||||
// Move down - sticky should be clamped to new width
|
||||
editor.handleInput("\x1b[B"); // Down - line 1
|
||||
editor.handleInput("\x1b[B"); // Down - line 2, col should be clamped
|
||||
assert.equal(editor.getCursor().col, 4);
|
||||
});
|
||||
|
||||
it("handles editor resizes when preferredVisualCol is on a different line", () => {
|
||||
const tui = createTestTUI(80, 24);
|
||||
const editor = new Editor(tui, defaultEditorTheme);
|
||||
|
||||
// Create a line that wraps into multiple visual lines at width 10
|
||||
// "12345678901234567890" = 20 chars, wraps to 2 visual lines at width 10
|
||||
editor.setText("short\n12345678901234567890");
|
||||
|
||||
// Go to line 1, col 15
|
||||
editor.handleInput("\x01"); // Ctrl+A
|
||||
for (let i = 0; i < 15; i++) editor.handleInput("\x1b[C");
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 1, col: 15 });
|
||||
|
||||
// Move up to establish sticky col 15
|
||||
editor.handleInput("\x1b[A"); // Up to line 0
|
||||
// Line 0 has only 5 chars, so cursor at col 5
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 5 });
|
||||
|
||||
// Narrow the editor
|
||||
editor.render(10);
|
||||
|
||||
// Move down - preferredVisualCol was 15, but width is 10
|
||||
// Should land on line 1, clamped to width (visual col 9, which is logical col 9)
|
||||
editor.handleInput("\x1b[B"); // Down to line 1
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 1, col: 8 });
|
||||
|
||||
// Move up
|
||||
editor.handleInput("\x1b[A"); // Up - should go to line 0
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 5 }); // Line 0 only has 5 chars
|
||||
|
||||
// Restore the original width
|
||||
editor.render(80);
|
||||
|
||||
// Move down - preferredVisualCol was kept at 15
|
||||
editor.handleInput("\x1b[B"); // Down to line 1
|
||||
assert.deepStrictEqual(editor.getCursor(), { line: 1, col: 15 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue