@mariozechner/pi-tui 0.57.1 → 0.58.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.
- package/dist/components/editor.d.ts +13 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +171 -37
- package/dist/components/editor.js.map +1 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +24 -41
- package/dist/components/input.js.map +1 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +5 -2
- package/dist/keys.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,9 +3,69 @@ import { decodeKittyPrintable, matchesKey } from "../keys.js";
|
|
|
3
3
|
import { KillRing } from "../kill-ring.js";
|
|
4
4
|
import { CURSOR_MARKER } from "../tui.js";
|
|
5
5
|
import { UndoStack } from "../undo-stack.js";
|
|
6
|
-
import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";
|
|
6
|
+
import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils.js";
|
|
7
7
|
import { SelectList } from "./select-list.js";
|
|
8
|
-
const
|
|
8
|
+
const baseSegmenter = getSegmenter();
|
|
9
|
+
/** Regex matching paste markers like `[paste #1 +123 lines]` or `[paste #2 1234 chars]`. */
|
|
10
|
+
const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]/g;
|
|
11
|
+
/** Non-global version for single-segment testing. */
|
|
12
|
+
const PASTE_MARKER_SINGLE = /^\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]$/;
|
|
13
|
+
/** Check if a segment is a paste marker (i.e. was merged by segmentWithMarkers). */
|
|
14
|
+
function isPasteMarker(segment) {
|
|
15
|
+
return segment.length >= 10 && PASTE_MARKER_SINGLE.test(segment);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A segmenter that wraps Intl.Segmenter and merges graphemes that fall
|
|
19
|
+
* within paste markers into single atomic segments. This makes cursor
|
|
20
|
+
* movement, deletion, word-wrap, etc. treat paste markers as single units.
|
|
21
|
+
*
|
|
22
|
+
* Only markers whose numeric ID exists in `validIds` are merged.
|
|
23
|
+
*/
|
|
24
|
+
function segmentWithMarkers(text, validIds) {
|
|
25
|
+
// Fast path: no paste markers in the text or no valid IDs.
|
|
26
|
+
if (validIds.size === 0 || !text.includes("[paste #")) {
|
|
27
|
+
return baseSegmenter.segment(text);
|
|
28
|
+
}
|
|
29
|
+
// Find all marker spans with valid IDs.
|
|
30
|
+
const markers = [];
|
|
31
|
+
for (const m of text.matchAll(PASTE_MARKER_REGEX)) {
|
|
32
|
+
const id = Number.parseInt(m[1], 10);
|
|
33
|
+
if (!validIds.has(id))
|
|
34
|
+
continue;
|
|
35
|
+
markers.push({ start: m.index, end: m.index + m[0].length });
|
|
36
|
+
}
|
|
37
|
+
if (markers.length === 0) {
|
|
38
|
+
return baseSegmenter.segment(text);
|
|
39
|
+
}
|
|
40
|
+
// Build merged segment list.
|
|
41
|
+
const baseSegments = baseSegmenter.segment(text);
|
|
42
|
+
const result = [];
|
|
43
|
+
let markerIdx = 0;
|
|
44
|
+
for (const seg of baseSegments) {
|
|
45
|
+
// Skip past markers that are entirely before this segment.
|
|
46
|
+
while (markerIdx < markers.length && markers[markerIdx].end <= seg.index) {
|
|
47
|
+
markerIdx++;
|
|
48
|
+
}
|
|
49
|
+
const marker = markerIdx < markers.length ? markers[markerIdx] : null;
|
|
50
|
+
if (marker && seg.index >= marker.start && seg.index < marker.end) {
|
|
51
|
+
// This segment falls inside a marker.
|
|
52
|
+
// If this is the first segment of the marker, emit a merged segment.
|
|
53
|
+
if (seg.index === marker.start) {
|
|
54
|
+
const markerText = text.slice(marker.start, marker.end);
|
|
55
|
+
result.push({
|
|
56
|
+
segment: markerText,
|
|
57
|
+
index: marker.start,
|
|
58
|
+
input: text,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Otherwise skip (already merged into the first segment).
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
result.push(seg);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
9
69
|
/**
|
|
10
70
|
* Split a line into word-wrapped chunks.
|
|
11
71
|
* Wraps at word boundaries when possible, falling back to character-level
|
|
@@ -13,9 +73,11 @@ const segmenter = getSegmenter();
|
|
|
13
73
|
*
|
|
14
74
|
* @param line - The text line to wrap
|
|
15
75
|
* @param maxWidth - Maximum visible width per chunk
|
|
76
|
+
* @param preSegmented - Optional pre-segmented graphemes (e.g. with paste-marker awareness).
|
|
77
|
+
* When omitted the default Intl.Segmenter is used.
|
|
16
78
|
* @returns Array of chunks with text and position information
|
|
17
79
|
*/
|
|
18
|
-
export function wordWrapLine(line, maxWidth) {
|
|
80
|
+
export function wordWrapLine(line, maxWidth, preSegmented) {
|
|
19
81
|
if (!line || maxWidth <= 0) {
|
|
20
82
|
return [{ text: "", startIndex: 0, endIndex: 0 }];
|
|
21
83
|
}
|
|
@@ -24,7 +86,7 @@ export function wordWrapLine(line, maxWidth) {
|
|
|
24
86
|
return [{ text: line, startIndex: 0, endIndex: line.length }];
|
|
25
87
|
}
|
|
26
88
|
const chunks = [];
|
|
27
|
-
const segments = [...
|
|
89
|
+
const segments = preSegmented ?? [...baseSegmenter.segment(line)];
|
|
28
90
|
let currentWidth = 0;
|
|
29
91
|
let chunkStart = 0;
|
|
30
92
|
// Wrap opportunity: the position after the last whitespace before a non-whitespace
|
|
@@ -36,30 +98,51 @@ export function wordWrapLine(line, maxWidth) {
|
|
|
36
98
|
const grapheme = seg.segment;
|
|
37
99
|
const gWidth = visibleWidth(grapheme);
|
|
38
100
|
const charIndex = seg.index;
|
|
39
|
-
const isWs = isWhitespaceChar(grapheme);
|
|
101
|
+
const isWs = !isPasteMarker(grapheme) && isWhitespaceChar(grapheme);
|
|
40
102
|
// Overflow check before advancing.
|
|
41
103
|
if (currentWidth + gWidth > maxWidth) {
|
|
42
|
-
if (wrapOppIndex >= 0) {
|
|
43
|
-
// Backtrack to last wrap opportunity
|
|
104
|
+
if (wrapOppIndex >= 0 && currentWidth - wrapOppWidth + gWidth <= maxWidth) {
|
|
105
|
+
// Backtrack to last wrap opportunity (the remaining content
|
|
106
|
+
// plus the current grapheme still fits within maxWidth).
|
|
44
107
|
chunks.push({ text: line.slice(chunkStart, wrapOppIndex), startIndex: chunkStart, endIndex: wrapOppIndex });
|
|
45
108
|
chunkStart = wrapOppIndex;
|
|
46
109
|
currentWidth -= wrapOppWidth;
|
|
47
110
|
}
|
|
48
111
|
else if (chunkStart < charIndex) {
|
|
49
|
-
// No wrap opportunity: force-break at current position.
|
|
112
|
+
// No viable wrap opportunity: force-break at current position.
|
|
113
|
+
// This also handles the case where backtracking to a word
|
|
114
|
+
// boundary wouldn't help because the remaining content plus
|
|
115
|
+
// the current grapheme (e.g. a wide character) still exceeds
|
|
116
|
+
// maxWidth.
|
|
50
117
|
chunks.push({ text: line.slice(chunkStart, charIndex), startIndex: chunkStart, endIndex: charIndex });
|
|
51
118
|
chunkStart = charIndex;
|
|
52
119
|
currentWidth = 0;
|
|
53
120
|
}
|
|
54
121
|
wrapOppIndex = -1;
|
|
55
122
|
}
|
|
123
|
+
if (gWidth > maxWidth) {
|
|
124
|
+
// Single atomic segment wider than maxWidth (e.g. paste marker
|
|
125
|
+
// in a narrow terminal). Re-wrap it at grapheme granularity.
|
|
126
|
+
// The segment remains logically atomic for cursor
|
|
127
|
+
// movement / editing — the split is purely visual for word-wrap layout.
|
|
128
|
+
const subChunks = wordWrapLine(grapheme, maxWidth);
|
|
129
|
+
for (let j = 0; j < subChunks.length - 1; j++) {
|
|
130
|
+
const sc = subChunks[j];
|
|
131
|
+
chunks.push({ text: sc.text, startIndex: charIndex + sc.startIndex, endIndex: charIndex + sc.endIndex });
|
|
132
|
+
}
|
|
133
|
+
const last = subChunks[subChunks.length - 1];
|
|
134
|
+
chunkStart = charIndex + last.startIndex;
|
|
135
|
+
currentWidth = visibleWidth(last.text);
|
|
136
|
+
wrapOppIndex = -1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
56
139
|
// Advance.
|
|
57
140
|
currentWidth += gWidth;
|
|
58
141
|
// Record wrap opportunity: whitespace followed by non-whitespace.
|
|
59
142
|
// Multiple spaces join (no break between them); the break point is
|
|
60
143
|
// after the last space before the next word.
|
|
61
144
|
const next = segments[i + 1];
|
|
62
|
-
if (isWs && next && !isWhitespaceChar(next.segment)) {
|
|
145
|
+
if (isWs && next && (isPasteMarker(next.segment) || !isWhitespaceChar(next.segment))) {
|
|
63
146
|
wrapOppIndex = next.index;
|
|
64
147
|
wrapOppWidth = currentWidth;
|
|
65
148
|
}
|
|
@@ -121,6 +204,14 @@ export class Editor {
|
|
|
121
204
|
const maxVisible = options.autocompleteMaxVisible ?? 5;
|
|
122
205
|
this.autocompleteMaxVisible = Number.isFinite(maxVisible) ? Math.max(3, Math.min(20, Math.floor(maxVisible))) : 5;
|
|
123
206
|
}
|
|
207
|
+
/** Set of currently valid paste IDs, for marker-aware segmentation. */
|
|
208
|
+
validPasteIds() {
|
|
209
|
+
return new Set(this.pastes.keys());
|
|
210
|
+
}
|
|
211
|
+
/** Segment text with paste-marker awareness, only merging markers with valid IDs. */
|
|
212
|
+
segment(text) {
|
|
213
|
+
return segmentWithMarkers(text, this.validPasteIds());
|
|
214
|
+
}
|
|
124
215
|
getPaddingX() {
|
|
125
216
|
return this.paddingX;
|
|
126
217
|
}
|
|
@@ -196,7 +287,7 @@ export class Editor {
|
|
|
196
287
|
}
|
|
197
288
|
/** Internal setText that doesn't reset history state - used by navigateHistory */
|
|
198
289
|
setTextInternal(text) {
|
|
199
|
-
const lines = text.
|
|
290
|
+
const lines = text.split("\n");
|
|
200
291
|
this.state.lines = lines.length === 0 ? [""] : lines;
|
|
201
292
|
this.state.cursorLine = this.state.lines.length - 1;
|
|
202
293
|
this.setCursorCol(this.state.lines[this.state.cursorLine]?.length || 0);
|
|
@@ -247,7 +338,12 @@ export class Editor {
|
|
|
247
338
|
if (this.scrollOffset > 0) {
|
|
248
339
|
const indicator = `─── ↑ ${this.scrollOffset} more `;
|
|
249
340
|
const remaining = width - visibleWidth(indicator);
|
|
250
|
-
|
|
341
|
+
if (remaining >= 0) {
|
|
342
|
+
result.push(this.borderColor(indicator + "─".repeat(remaining)));
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
result.push(this.borderColor(truncateToWidth(indicator, width)));
|
|
346
|
+
}
|
|
251
347
|
}
|
|
252
348
|
else {
|
|
253
349
|
result.push(horizontal.repeat(width));
|
|
@@ -268,7 +364,7 @@ export class Editor {
|
|
|
268
364
|
if (after.length > 0) {
|
|
269
365
|
// Cursor is on a character (grapheme) - replace it with highlighted version
|
|
270
366
|
// Get the first grapheme from 'after'
|
|
271
|
-
const afterGraphemes = [...
|
|
367
|
+
const afterGraphemes = [...this.segment(after)];
|
|
272
368
|
const firstGrapheme = afterGraphemes[0]?.segment || "";
|
|
273
369
|
const restAfter = after.slice(firstGrapheme.length);
|
|
274
370
|
const cursor = `\x1b[7m${firstGrapheme}\x1b[0m`;
|
|
@@ -607,7 +703,7 @@ export class Editor {
|
|
|
607
703
|
}
|
|
608
704
|
else {
|
|
609
705
|
// Line needs wrapping - use word-aware wrapping
|
|
610
|
-
const chunks = wordWrapLine(line, contentWidth);
|
|
706
|
+
const chunks = wordWrapLine(line, contentWidth, [...this.segment(line)]);
|
|
611
707
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
612
708
|
const chunk = chunks[chunkIndex];
|
|
613
709
|
if (!chunk)
|
|
@@ -680,11 +776,12 @@ export class Editor {
|
|
|
680
776
|
setText(text) {
|
|
681
777
|
this.lastAction = null;
|
|
682
778
|
this.historyIndex = -1; // Exit history browsing mode
|
|
779
|
+
const normalized = this.normalizeText(text);
|
|
683
780
|
// Push undo snapshot if content differs (makes programmatic changes undoable)
|
|
684
|
-
if (this.getText() !==
|
|
781
|
+
if (this.getText() !== normalized) {
|
|
685
782
|
this.pushUndoSnapshot();
|
|
686
783
|
}
|
|
687
|
-
this.setTextInternal(
|
|
784
|
+
this.setTextInternal(normalized);
|
|
688
785
|
}
|
|
689
786
|
/**
|
|
690
787
|
* Insert text at the current cursor position.
|
|
@@ -699,6 +796,14 @@ export class Editor {
|
|
|
699
796
|
this.historyIndex = -1;
|
|
700
797
|
this.insertTextAtCursorInternal(text);
|
|
701
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* Normalize text for editor storage:
|
|
801
|
+
* - Normalize line endings (\r\n and \r -> \n)
|
|
802
|
+
* - Expand tabs to 4 spaces
|
|
803
|
+
*/
|
|
804
|
+
normalizeText(text) {
|
|
805
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ");
|
|
806
|
+
}
|
|
702
807
|
/**
|
|
703
808
|
* Internal text insertion at cursor. Handles single and multi-line text.
|
|
704
809
|
* Does not push undo snapshots or trigger autocomplete - caller is responsible.
|
|
@@ -707,8 +812,8 @@ export class Editor {
|
|
|
707
812
|
insertTextAtCursorInternal(text) {
|
|
708
813
|
if (!text)
|
|
709
814
|
return;
|
|
710
|
-
// Normalize line endings
|
|
711
|
-
const normalized =
|
|
815
|
+
// Normalize line endings and tabs
|
|
816
|
+
const normalized = this.normalizeText(text);
|
|
712
817
|
const insertedLines = normalized.split("\n");
|
|
713
818
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
714
819
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
@@ -799,12 +904,10 @@ export class Editor {
|
|
|
799
904
|
this.historyIndex = -1; // Exit history browsing mode
|
|
800
905
|
this.lastAction = null;
|
|
801
906
|
this.pushUndoSnapshot();
|
|
802
|
-
// Clean the pasted text
|
|
803
|
-
const cleanText =
|
|
804
|
-
// Convert tabs to spaces (4 spaces per tab)
|
|
805
|
-
const tabExpandedText = cleanText.replace(/\t/g, " ");
|
|
907
|
+
// Clean the pasted text: normalize line endings, expand tabs
|
|
908
|
+
const cleanText = this.normalizeText(pastedText);
|
|
806
909
|
// Filter out non-printable characters except newlines
|
|
807
|
-
let filteredText =
|
|
910
|
+
let filteredText = cleanText
|
|
808
911
|
.split("")
|
|
809
912
|
.filter((char) => char === "\n" || char.charCodeAt(0) >= 32)
|
|
810
913
|
.join("");
|
|
@@ -897,7 +1000,7 @@ export class Editor {
|
|
|
897
1000
|
const line = this.state.lines[this.state.cursorLine] || "";
|
|
898
1001
|
const beforeCursor = line.slice(0, this.state.cursorCol);
|
|
899
1002
|
// Find the last grapheme in the text before cursor
|
|
900
|
-
const graphemes = [...
|
|
1003
|
+
const graphemes = [...this.segment(beforeCursor)];
|
|
901
1004
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
902
1005
|
const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
903
1006
|
const before = line.slice(0, this.state.cursorCol - graphemeLength);
|
|
@@ -966,6 +1069,21 @@ export class Editor {
|
|
|
966
1069
|
const targetCol = targetVL.startCol + moveToVisualCol;
|
|
967
1070
|
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
|
968
1071
|
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
|
1072
|
+
// Snap cursor to atomic segment boundary (e.g. paste markers)
|
|
1073
|
+
// so the cursor never lands in the middle of a multi-grapheme unit.
|
|
1074
|
+
// Single-grapheme segments don't need snapping.
|
|
1075
|
+
const segments = [...this.segment(logicalLine)];
|
|
1076
|
+
for (const seg of segments) {
|
|
1077
|
+
if (seg.index > this.state.cursorCol)
|
|
1078
|
+
break;
|
|
1079
|
+
if (seg.segment.length <= 1)
|
|
1080
|
+
continue;
|
|
1081
|
+
if (this.state.cursorCol < seg.index + seg.segment.length) {
|
|
1082
|
+
// jump to the start of the segment when moving up, to the end when moving down.
|
|
1083
|
+
this.state.cursorCol = currentVisualLine > targetVisualLine ? seg.index : seg.index + seg.segment.length;
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
969
1087
|
}
|
|
970
1088
|
}
|
|
971
1089
|
/**
|
|
@@ -1152,7 +1270,7 @@ export class Editor {
|
|
|
1152
1270
|
// Delete grapheme at cursor position (handles emojis, combining characters, etc.)
|
|
1153
1271
|
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
1154
1272
|
// Find the first grapheme at cursor
|
|
1155
|
-
const graphemes = [...
|
|
1273
|
+
const graphemes = [...this.segment(afterCursor)];
|
|
1156
1274
|
const firstGrapheme = graphemes[0];
|
|
1157
1275
|
const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
|
|
1158
1276
|
const before = currentLine.slice(0, this.state.cursorCol);
|
|
@@ -1207,7 +1325,7 @@ export class Editor {
|
|
|
1207
1325
|
}
|
|
1208
1326
|
else {
|
|
1209
1327
|
// Line needs wrapping - use word-aware wrapping
|
|
1210
|
-
const chunks = wordWrapLine(line, width);
|
|
1328
|
+
const chunks = wordWrapLine(line, width, [...this.segment(line)]);
|
|
1211
1329
|
for (const chunk of chunks) {
|
|
1212
1330
|
visualLines.push({
|
|
1213
1331
|
logicalLine: i,
|
|
@@ -1256,7 +1374,7 @@ export class Editor {
|
|
|
1256
1374
|
// Moving right - move by one grapheme (handles emojis, combining characters, etc.)
|
|
1257
1375
|
if (this.state.cursorCol < currentLine.length) {
|
|
1258
1376
|
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
1259
|
-
const graphemes = [...
|
|
1377
|
+
const graphemes = [...this.segment(afterCursor)];
|
|
1260
1378
|
const firstGrapheme = graphemes[0];
|
|
1261
1379
|
this.setCursorCol(this.state.cursorCol + (firstGrapheme ? firstGrapheme.segment.length : 1));
|
|
1262
1380
|
}
|
|
@@ -1277,7 +1395,7 @@ export class Editor {
|
|
|
1277
1395
|
// Moving left - move by one grapheme (handles emojis, combining characters, etc.)
|
|
1278
1396
|
if (this.state.cursorCol > 0) {
|
|
1279
1397
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1280
|
-
const graphemes = [...
|
|
1398
|
+
const graphemes = [...this.segment(beforeCursor)];
|
|
1281
1399
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
1282
1400
|
this.setCursorCol(this.state.cursorCol - (lastGrapheme ? lastGrapheme.segment.length : 1));
|
|
1283
1401
|
}
|
|
@@ -1316,17 +1434,25 @@ export class Editor {
|
|
|
1316
1434
|
return;
|
|
1317
1435
|
}
|
|
1318
1436
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1319
|
-
const graphemes = [...
|
|
1437
|
+
const graphemes = [...this.segment(textBeforeCursor)];
|
|
1320
1438
|
let newCol = this.state.cursorCol;
|
|
1321
1439
|
// Skip trailing whitespace
|
|
1322
|
-
while (graphemes.length > 0 &&
|
|
1440
|
+
while (graphemes.length > 0 &&
|
|
1441
|
+
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1442
|
+
isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1323
1443
|
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1324
1444
|
}
|
|
1325
1445
|
if (graphemes.length > 0) {
|
|
1326
1446
|
const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
|
|
1327
|
-
if (
|
|
1447
|
+
if (isPasteMarker(lastGrapheme)) {
|
|
1448
|
+
// Paste marker is a single atomic word
|
|
1449
|
+
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1450
|
+
}
|
|
1451
|
+
else if (isPunctuationChar(lastGrapheme)) {
|
|
1328
1452
|
// Skip punctuation run
|
|
1329
|
-
while (graphemes.length > 0 &&
|
|
1453
|
+
while (graphemes.length > 0 &&
|
|
1454
|
+
isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1455
|
+
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1330
1456
|
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1331
1457
|
}
|
|
1332
1458
|
}
|
|
@@ -1334,7 +1460,8 @@ export class Editor {
|
|
|
1334
1460
|
// Skip word run
|
|
1335
1461
|
while (graphemes.length > 0 &&
|
|
1336
1462
|
!isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1337
|
-
!isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "")
|
|
1463
|
+
!isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1464
|
+
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1338
1465
|
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1339
1466
|
}
|
|
1340
1467
|
}
|
|
@@ -1497,27 +1624,34 @@ export class Editor {
|
|
|
1497
1624
|
return;
|
|
1498
1625
|
}
|
|
1499
1626
|
const textAfterCursor = currentLine.slice(this.state.cursorCol);
|
|
1500
|
-
const segments =
|
|
1627
|
+
const segments = this.segment(textAfterCursor);
|
|
1501
1628
|
const iterator = segments[Symbol.iterator]();
|
|
1502
1629
|
let next = iterator.next();
|
|
1503
1630
|
let newCol = this.state.cursorCol;
|
|
1504
1631
|
// Skip leading whitespace
|
|
1505
|
-
while (!next.done && isWhitespaceChar(next.value.segment)) {
|
|
1632
|
+
while (!next.done && !isPasteMarker(next.value.segment) && isWhitespaceChar(next.value.segment)) {
|
|
1506
1633
|
newCol += next.value.segment.length;
|
|
1507
1634
|
next = iterator.next();
|
|
1508
1635
|
}
|
|
1509
1636
|
if (!next.done) {
|
|
1510
1637
|
const firstGrapheme = next.value.segment;
|
|
1511
|
-
if (
|
|
1638
|
+
if (isPasteMarker(firstGrapheme)) {
|
|
1639
|
+
// Paste marker is a single atomic word
|
|
1640
|
+
newCol += firstGrapheme.length;
|
|
1641
|
+
}
|
|
1642
|
+
else if (isPunctuationChar(firstGrapheme)) {
|
|
1512
1643
|
// Skip punctuation run
|
|
1513
|
-
while (!next.done && isPunctuationChar(next.value.segment)) {
|
|
1644
|
+
while (!next.done && isPunctuationChar(next.value.segment) && !isPasteMarker(next.value.segment)) {
|
|
1514
1645
|
newCol += next.value.segment.length;
|
|
1515
1646
|
next = iterator.next();
|
|
1516
1647
|
}
|
|
1517
1648
|
}
|
|
1518
1649
|
else {
|
|
1519
1650
|
// Skip word run
|
|
1520
|
-
while (!next.done &&
|
|
1651
|
+
while (!next.done &&
|
|
1652
|
+
!isWhitespaceChar(next.value.segment) &&
|
|
1653
|
+
!isPunctuationChar(next.value.segment) &&
|
|
1654
|
+
!isPasteMarker(next.value.segment)) {
|
|
1521
1655
|
newCol += next.value.segment.length;
|
|
1522
1656
|
next = iterator.next();
|
|
1523
1657
|
}
|