@mariozechner/pi-tui 0.66.1 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -200,6 +200,12 @@ export class Editor {
|
|
|
200
200
|
jumpMode = null;
|
|
201
201
|
// Preferred visual column for vertical cursor movement (sticky column)
|
|
202
202
|
preferredVisualCol = null;
|
|
203
|
+
// When the cursor is snapped to the start of an atomic segment, e.g. a
|
|
204
|
+
// paste marker, cursorCol no longer reflects where the cursor would have
|
|
205
|
+
// landed. This field stores the pre-snap cursorCol so that the next
|
|
206
|
+
// vertical move can resolve it to a visual column on whatever VL it belongs
|
|
207
|
+
// to.
|
|
208
|
+
snappedFromCursorCol = null;
|
|
203
209
|
// Undo support
|
|
204
210
|
undoStack = new UndoStack();
|
|
205
211
|
onSubmit;
|
|
@@ -1057,6 +1063,7 @@ export class Editor {
|
|
|
1057
1063
|
setCursorCol(col) {
|
|
1058
1064
|
this.state.cursorCol = col;
|
|
1059
1065
|
this.preferredVisualCol = null;
|
|
1066
|
+
this.snappedFromCursorCol = null;
|
|
1060
1067
|
}
|
|
1061
1068
|
/**
|
|
1062
1069
|
* Move cursor to a target visual line, applying sticky column logic.
|
|
@@ -1065,37 +1072,70 @@ export class Editor {
|
|
|
1065
1072
|
moveToVisualLine(visualLines, currentVisualLine, targetVisualLine) {
|
|
1066
1073
|
const currentVL = visualLines[currentVisualLine];
|
|
1067
1074
|
const targetVL = visualLines[targetVisualLine];
|
|
1068
|
-
if (currentVL && targetVL)
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
this.state.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1075
|
+
if (!(currentVL && targetVL))
|
|
1076
|
+
return;
|
|
1077
|
+
// When the cursor was snapped to a segment start, resolve the pre-snap
|
|
1078
|
+
// position against the VL it belongs to. This gives the correct visual
|
|
1079
|
+
// column even after a resize reshuffles VLs.
|
|
1080
|
+
let currentVisualCol;
|
|
1081
|
+
if (this.snappedFromCursorCol !== null) {
|
|
1082
|
+
const vlIndex = this.findVisualLineAt(visualLines, currentVL.logicalLine, this.snappedFromCursorCol);
|
|
1083
|
+
currentVisualCol = this.snappedFromCursorCol - visualLines[vlIndex].startCol;
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
currentVisualCol = this.state.cursorCol - currentVL.startCol;
|
|
1087
|
+
}
|
|
1088
|
+
// For non-last segments, clamp to length-1 to stay within the segment
|
|
1089
|
+
const isLastSourceSegment = currentVisualLine === visualLines.length - 1 ||
|
|
1090
|
+
visualLines[currentVisualLine + 1]?.logicalLine !== currentVL.logicalLine;
|
|
1091
|
+
const sourceMaxVisualCol = isLastSourceSegment ? currentVL.length : Math.max(0, currentVL.length - 1);
|
|
1092
|
+
const isLastTargetSegment = targetVisualLine === visualLines.length - 1 ||
|
|
1093
|
+
visualLines[targetVisualLine + 1]?.logicalLine !== targetVL.logicalLine;
|
|
1094
|
+
const targetMaxVisualCol = isLastTargetSegment ? targetVL.length : Math.max(0, targetVL.length - 1);
|
|
1095
|
+
const moveToVisualCol = this.computeVerticalMoveColumn(currentVisualCol, sourceMaxVisualCol, targetMaxVisualCol);
|
|
1096
|
+
// Set cursor position
|
|
1097
|
+
this.state.cursorLine = targetVL.logicalLine;
|
|
1098
|
+
const targetCol = targetVL.startCol + moveToVisualCol;
|
|
1099
|
+
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
|
1100
|
+
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
|
1101
|
+
// Snap cursor to atomic segment boundary (e.g. paste markers)
|
|
1102
|
+
// so the cursor never lands in the middle of a multi-grapheme unit.
|
|
1103
|
+
// Single-grapheme segments don't need snapping.
|
|
1104
|
+
const segments = [...this.segment(logicalLine)];
|
|
1105
|
+
for (const seg of segments) {
|
|
1106
|
+
if (seg.index > this.state.cursorCol)
|
|
1107
|
+
break;
|
|
1108
|
+
if (seg.segment.length <= 1)
|
|
1109
|
+
continue;
|
|
1110
|
+
if (this.state.cursorCol < seg.index + seg.segment.length) {
|
|
1111
|
+
const isContinuation = seg.index < targetVL.startCol;
|
|
1112
|
+
const isMovingDown = targetVisualLine > currentVisualLine;
|
|
1113
|
+
if (isContinuation && isMovingDown) {
|
|
1114
|
+
// The segment started on a previous visual line, and we
|
|
1115
|
+
// already visited it on the way down. Skip all remaining
|
|
1116
|
+
// continuation VLs and land on the first VL past it.
|
|
1117
|
+
const segEnd = seg.index + seg.segment.length;
|
|
1118
|
+
let next = targetVisualLine + 1;
|
|
1119
|
+
while (next < visualLines.length &&
|
|
1120
|
+
visualLines[next].logicalLine === targetVL.logicalLine &&
|
|
1121
|
+
visualLines[next].startCol < segEnd) {
|
|
1122
|
+
next++;
|
|
1123
|
+
}
|
|
1124
|
+
if (next < visualLines.length) {
|
|
1125
|
+
this.moveToVisualLine(visualLines, currentVisualLine, next);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1096
1128
|
}
|
|
1129
|
+
// Snap to the start of the segment so it gets highlighted.
|
|
1130
|
+
// Store the pre-snap position so the next vertical move can
|
|
1131
|
+
// resolve it to the correct visual column.
|
|
1132
|
+
this.snappedFromCursorCol = this.state.cursorCol;
|
|
1133
|
+
this.state.cursorCol = seg.index;
|
|
1134
|
+
return;
|
|
1097
1135
|
}
|
|
1098
1136
|
}
|
|
1137
|
+
// No snap occurred – we moved out of the atomic segment.
|
|
1138
|
+
this.snappedFromCursorCol = null;
|
|
1099
1139
|
}
|
|
1100
1140
|
/**
|
|
1101
1141
|
* Compute the target visual column for vertical cursor movement.
|
|
@@ -1349,26 +1389,29 @@ export class Editor {
|
|
|
1349
1389
|
return visualLines;
|
|
1350
1390
|
}
|
|
1351
1391
|
/**
|
|
1352
|
-
* Find the visual line index
|
|
1392
|
+
* Find the visual line index that contains the given logical position.
|
|
1353
1393
|
*/
|
|
1354
|
-
|
|
1394
|
+
findVisualLineAt(visualLines, line, col) {
|
|
1355
1395
|
for (let i = 0; i < visualLines.length; i++) {
|
|
1356
1396
|
const vl = visualLines[i];
|
|
1357
|
-
if (!vl)
|
|
1397
|
+
if (!vl || vl.logicalLine !== line)
|
|
1358
1398
|
continue;
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
return i;
|
|
1366
|
-
}
|
|
1399
|
+
const offset = col - vl.startCol;
|
|
1400
|
+
// Cursor is in this segment if it's within range. For the last
|
|
1401
|
+
// segment of a logical line, cursor can be at length (end position)
|
|
1402
|
+
const isLastSegmentOfLine = i === visualLines.length - 1 || visualLines[i + 1]?.logicalLine !== vl.logicalLine;
|
|
1403
|
+
if (offset >= 0 && (offset < vl.length || (isLastSegmentOfLine && offset === vl.length))) {
|
|
1404
|
+
return i;
|
|
1367
1405
|
}
|
|
1368
1406
|
}
|
|
1369
|
-
// Fallback: return last visual line
|
|
1370
1407
|
return visualLines.length - 1;
|
|
1371
1408
|
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Find the visual line index for the current cursor position.
|
|
1411
|
+
*/
|
|
1412
|
+
findCurrentVisualLine(visualLines) {
|
|
1413
|
+
return this.findVisualLineAt(visualLines, this.state.cursorLine, this.state.cursorCol);
|
|
1414
|
+
}
|
|
1372
1415
|
moveCursor(deltaLine, deltaCol) {
|
|
1373
1416
|
this.lastAction = null;
|
|
1374
1417
|
const visualLines = this.buildVisualLineMap(this.lastWidth);
|