@jecruz/merc-tui 0.1.4 → 0.1.6
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/README.md +14 -1
- package/dist/autocomplete.d.ts +1 -0
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +85 -24
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/box.d.ts +1 -0
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +6 -1
- package/dist/components/box.js.map +1 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +80 -115
- package/dist/components/editor.js.map +1 -1
- package/dist/components/image.d.ts +12 -0
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +45 -6
- package/dist/components/image.js.map +1 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +7 -55
- package/dist/components/input.js.map +1 -1
- package/dist/components/markdown.d.ts +1 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +18 -6
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +4 -7
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/text.d.ts +1 -0
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +11 -5
- package/dist/components/text.js.map +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +23 -22
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +9 -1
- package/dist/keys.js.map +1 -1
- package/dist/native-modifier-digests.d.ts +6 -0
- package/dist/native-modifier-digests.d.ts.map +1 -0
- package/dist/native-modifier-digests.js +6 -0
- package/dist/native-modifier-digests.js.map +1 -0
- package/dist/native-modifiers.d.ts +11 -0
- package/dist/native-modifiers.d.ts.map +1 -0
- package/dist/native-modifiers.js +98 -0
- package/dist/native-modifiers.js.map +1 -0
- package/dist/stdin-buffer.d.ts +25 -2
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +176 -3
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal-image.d.ts +10 -0
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +102 -17
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts +17 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +165 -56
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +28 -7
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +322 -125
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +16 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +115 -29
- package/dist/utils.js.map +1 -1
- package/dist/word-navigation.d.ts +25 -0
- package/dist/word-navigation.d.ts.map +1 -0
- package/dist/word-navigation.js +96 -0
- package/dist/word-navigation.js.map +1 -0
- package/native/darwin/README.md +13 -0
- package/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/native/darwin/prebuilds/manifest.json +24 -0
- package/package.json +4 -1
|
@@ -3,9 +3,11 @@ import { decodePrintableKey, 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 {
|
|
6
|
+
import { autocompleteBoundaryRegex, autocompleteSeparatorRegex, cjkBreakRegex, getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth, } from "../utils.js";
|
|
7
|
+
import { findWordBackward, findWordForward } from "../word-navigation.js";
|
|
7
8
|
import { SelectList } from "./select-list.js";
|
|
8
|
-
const
|
|
9
|
+
const graphemeSegmenter = getGraphemeSegmenter();
|
|
10
|
+
const wordSegmenter = getWordSegmenter();
|
|
9
11
|
/** Regex matching paste markers like `[paste #1 +123 lines]` or `[paste #2 1234 chars]`. */
|
|
10
12
|
const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]/g;
|
|
11
13
|
/** Non-global version for single-segment testing. */
|
|
@@ -21,7 +23,7 @@ function isPasteMarker(segment) {
|
|
|
21
23
|
*
|
|
22
24
|
* Only markers whose numeric ID exists in `validIds` are merged.
|
|
23
25
|
*/
|
|
24
|
-
function segmentWithMarkers(text, validIds) {
|
|
26
|
+
function segmentWithMarkers(text, baseSegmenter, validIds) {
|
|
25
27
|
// Fast path: no paste markers in the text or no valid IDs.
|
|
26
28
|
if (validIds.size === 0 || !text.includes("[paste #")) {
|
|
27
29
|
return baseSegmenter.segment(text);
|
|
@@ -86,7 +88,7 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
|
|
|
86
88
|
return [{ text: line, startIndex: 0, endIndex: line.length }];
|
|
87
89
|
}
|
|
88
90
|
const chunks = [];
|
|
89
|
-
const segments = preSegmented ?? [...
|
|
91
|
+
const segments = preSegmented ?? [...graphemeSegmenter.segment(line)];
|
|
90
92
|
let currentWidth = 0;
|
|
91
93
|
let chunkStart = 0;
|
|
92
94
|
// Wrap opportunity: the position after the last whitespace before a non-whitespace
|
|
@@ -138,14 +140,23 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
|
|
|
138
140
|
}
|
|
139
141
|
// Advance.
|
|
140
142
|
currentWidth += gWidth;
|
|
141
|
-
// Record wrap opportunity: whitespace followed by non-whitespace
|
|
142
|
-
//
|
|
143
|
-
//
|
|
143
|
+
// Record wrap opportunity: whitespace followed by non-whitespace
|
|
144
|
+
// (multiple spaces join; the break point is after the last space),
|
|
145
|
+
// or at a boundary where either side is CJK (CJK allows breaking
|
|
146
|
+
// between any adjacent characters).
|
|
144
147
|
const next = segments[i + 1];
|
|
145
148
|
if (isWs && next && (isPasteMarker(next.segment) || !isWhitespaceChar(next.segment))) {
|
|
146
149
|
wrapOppIndex = next.index;
|
|
147
150
|
wrapOppWidth = currentWidth;
|
|
148
151
|
}
|
|
152
|
+
else if (!isWs && next && !isWhitespaceChar(next.segment)) {
|
|
153
|
+
const isCjk = !isPasteMarker(grapheme) && cjkBreakRegex.test(grapheme);
|
|
154
|
+
const nextIsCjk = !isPasteMarker(next.segment) && cjkBreakRegex.test(next.segment);
|
|
155
|
+
if (isCjk || nextIsCjk) {
|
|
156
|
+
wrapOppIndex = next.index;
|
|
157
|
+
wrapOppWidth = currentWidth;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
149
160
|
}
|
|
150
161
|
// Push final chunk.
|
|
151
162
|
chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
|
|
@@ -156,6 +167,22 @@ const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
|
|
|
156
167
|
maxPrimaryColumnWidth: 32,
|
|
157
168
|
};
|
|
158
169
|
const ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS = 20;
|
|
170
|
+
// Unquoted completions end at whitespace or CJK punctuation; quoted paths may contain either.
|
|
171
|
+
const unquotedAutocompleteSuffixRegex = new RegExp(`(?:(?!${autocompleteSeparatorRegex.source}).)*`, "u");
|
|
172
|
+
// Trigger tokens may be wrapped in prose, e.g. "(@src/foo" or "`@src/foo".
|
|
173
|
+
const autocompleteTokenStartSource = `${autocompleteBoundaryRegex.source}[([{<\`]*`;
|
|
174
|
+
const symbolTriggerPattern = new RegExp(`${autocompleteTokenStartSource}(?:@"[^"]*|[@#]${unquotedAutocompleteSuffixRegex.source})$`, "u");
|
|
175
|
+
const symbolDebouncePattern = new RegExp(`${autocompleteTokenStartSource}(?:@(?:"[^"]*|${unquotedAutocompleteSuffixRegex.source})|#${unquotedAutocompleteSuffixRegex.source})$`, "u");
|
|
176
|
+
function createScrollBorder(direction, hiddenLineCount, width) {
|
|
177
|
+
const availableWidth = Math.max(0, width);
|
|
178
|
+
const indicator = `─── ${direction} ${hiddenLineCount} more `;
|
|
179
|
+
const remaining = availableWidth - visibleWidth(indicator);
|
|
180
|
+
if (remaining >= 0)
|
|
181
|
+
return indicator + "─".repeat(remaining);
|
|
182
|
+
const ellipsis = "...".slice(0, availableWidth);
|
|
183
|
+
const indicatorWidth = availableWidth - visibleWidth(ellipsis);
|
|
184
|
+
return sliceByColumn(indicator, 0, indicatorWidth, true) + ellipsis;
|
|
185
|
+
}
|
|
159
186
|
export class Editor {
|
|
160
187
|
state = {
|
|
161
188
|
lines: [""],
|
|
@@ -226,8 +253,8 @@ export class Editor {
|
|
|
226
253
|
return new Set(this.pastes.keys());
|
|
227
254
|
}
|
|
228
255
|
/** Segment text with paste-marker awareness, only merging markers with valid IDs. */
|
|
229
|
-
segment(text) {
|
|
230
|
-
return segmentWithMarkers(text, this.validPasteIds());
|
|
256
|
+
segment(text, mode) {
|
|
257
|
+
return segmentWithMarkers(text, mode === "word" ? wordSegmenter : graphemeSegmenter, this.validPasteIds());
|
|
231
258
|
}
|
|
232
259
|
getPaddingX() {
|
|
233
260
|
return this.paddingX;
|
|
@@ -370,14 +397,8 @@ export class Editor {
|
|
|
370
397
|
const rightPadding = leftPadding;
|
|
371
398
|
// Render top border (with scroll indicator if scrolled down)
|
|
372
399
|
if (this.scrollOffset > 0) {
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
if (remaining >= 0) {
|
|
376
|
-
result.push(this.borderColor(indicator + "─".repeat(remaining)));
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
result.push(this.borderColor(truncateToWidth(indicator, width)));
|
|
380
|
-
}
|
|
400
|
+
const border = createScrollBorder("↑", this.scrollOffset, width);
|
|
401
|
+
result.push(this.borderColor(border));
|
|
381
402
|
}
|
|
382
403
|
else {
|
|
383
404
|
result.push(horizontal.repeat(width));
|
|
@@ -400,7 +421,7 @@ export class Editor {
|
|
|
400
421
|
if (after.length > 0) {
|
|
401
422
|
// Cursor is on a character (grapheme) - replace it with highlighted version
|
|
402
423
|
// Get the first grapheme from 'after'
|
|
403
|
-
const afterGraphemes = [...this.segment(after)];
|
|
424
|
+
const afterGraphemes = [...this.segment(after, "grapheme")];
|
|
404
425
|
const firstGrapheme = afterGraphemes[0]?.segment || "";
|
|
405
426
|
const restAfter = after.slice(firstGrapheme.length);
|
|
406
427
|
const cursor = `\x1b[7m${firstGrapheme}\x1b[0m`;
|
|
@@ -427,9 +448,8 @@ export class Editor {
|
|
|
427
448
|
// Render bottom border (with scroll indicator if more content below)
|
|
428
449
|
const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
|
|
429
450
|
if (linesBelow > 0) {
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
|
|
451
|
+
const border = createScrollBorder("↓", linesBelow, width);
|
|
452
|
+
result.push(this.borderColor(border));
|
|
433
453
|
}
|
|
434
454
|
else {
|
|
435
455
|
result.push(horizontal.repeat(width));
|
|
@@ -734,7 +754,7 @@ export class Editor {
|
|
|
734
754
|
}
|
|
735
755
|
else {
|
|
736
756
|
// Line needs wrapping - use word-aware wrapping
|
|
737
|
-
const chunks = wordWrapLine(line, contentWidth, [...this.segment(line)]);
|
|
757
|
+
const chunks = wordWrapLine(line, contentWidth, [...this.segment(line, "grapheme")]);
|
|
738
758
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
739
759
|
const chunk = chunks[chunkIndex];
|
|
740
760
|
if (!chunk)
|
|
@@ -811,13 +831,13 @@ export class Editor {
|
|
|
811
831
|
this.cancelAutocomplete();
|
|
812
832
|
this.lastAction = null;
|
|
813
833
|
this.exitHistoryBrowsing();
|
|
814
|
-
this.pastes.clear();
|
|
815
|
-
this.pasteCounter = 0;
|
|
816
834
|
const normalized = this.normalizeText(text);
|
|
817
835
|
// Push undo snapshot if content differs (makes programmatic changes undoable)
|
|
818
836
|
if (this.getText() !== normalized) {
|
|
819
837
|
this.pushUndoSnapshot();
|
|
820
838
|
}
|
|
839
|
+
this.pastes.clear();
|
|
840
|
+
this.pasteCounter = 0;
|
|
821
841
|
this.setTextInternal(normalized);
|
|
822
842
|
}
|
|
823
843
|
/**
|
|
@@ -914,13 +934,12 @@ export class Editor {
|
|
|
914
934
|
else if (char === "@" || char === "#") {
|
|
915
935
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
916
936
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
917
|
-
|
|
918
|
-
if (textBeforeCursor.length === 1 || charBeforeSymbol === " " || charBeforeSymbol === "\t") {
|
|
937
|
+
if (symbolTriggerPattern.test(textBeforeCursor)) {
|
|
919
938
|
this.tryTriggerAutocomplete();
|
|
920
939
|
}
|
|
921
940
|
}
|
|
922
941
|
// Also auto-trigger when typing letters in a slash command or symbol completion context
|
|
923
|
-
else if (/[a-zA-Z0-9.\-_]/.test(char)) {
|
|
942
|
+
else if (/[a-zA-Z0-9.\-_]/.test(char) || cjkBreakRegex.test(char)) {
|
|
924
943
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
925
944
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
926
945
|
// Check if we're in a slash command (with or without space for arguments)
|
|
@@ -928,7 +947,7 @@ export class Editor {
|
|
|
928
947
|
this.tryTriggerAutocomplete();
|
|
929
948
|
}
|
|
930
949
|
// Check if we're in a symbol-based completion context like @ or #
|
|
931
|
-
else if (
|
|
950
|
+
else if (symbolTriggerPattern.test(textBeforeCursor)) {
|
|
932
951
|
this.tryTriggerAutocomplete();
|
|
933
952
|
}
|
|
934
953
|
}
|
|
@@ -1049,7 +1068,7 @@ export class Editor {
|
|
|
1049
1068
|
let line = this.state.lines[this.state.cursorLine] || "";
|
|
1050
1069
|
const beforeCursor = line.slice(0, this.state.cursorCol);
|
|
1051
1070
|
// Find the last grapheme in the text before cursor
|
|
1052
|
-
const graphemes = [...this.segment(beforeCursor)];
|
|
1071
|
+
const graphemes = [...this.segment(beforeCursor, "grapheme")];
|
|
1053
1072
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
1054
1073
|
const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
1055
1074
|
const isPastedSegment = PASTE_MARKER_SINGLE.exec(lastGrapheme.segment);
|
|
@@ -1057,15 +1076,20 @@ export class Editor {
|
|
|
1057
1076
|
const targetId = Number(isPastedSegment[1]);
|
|
1058
1077
|
this.pastes.delete(targetId);
|
|
1059
1078
|
this.pasteCounter--;
|
|
1060
|
-
//
|
|
1079
|
+
// Shift registry entries down in ascending id order, independent
|
|
1080
|
+
// of marker order in the text ([paste #3] becomes [paste #2] when
|
|
1081
|
+
// [paste #1] is removed).
|
|
1082
|
+
const higherIds = [...this.pastes.keys()].filter((id) => id > targetId).sort((a, b) => a - b);
|
|
1083
|
+
for (const id of higherIds) {
|
|
1084
|
+
this.pastes.set(id - 1, this.pastes.get(id));
|
|
1085
|
+
this.pastes.delete(id);
|
|
1086
|
+
}
|
|
1087
|
+
// Renumber markers with ids greater than the removed one.
|
|
1061
1088
|
this.state.lines = this.state.lines.map((line) => line.replace(PASTE_MARKER_REGEX, (fullMatch, idGroup, suffixGroup) => {
|
|
1062
|
-
const
|
|
1063
|
-
if (
|
|
1089
|
+
const x = Number(idGroup);
|
|
1090
|
+
if (x <= targetId)
|
|
1064
1091
|
return fullMatch;
|
|
1065
|
-
|
|
1066
|
-
this.pastes.set(id - 1, this.pastes.get(id) ?? newText);
|
|
1067
|
-
this.pastes.delete(id);
|
|
1068
|
-
return newText;
|
|
1092
|
+
return `[paste #${x - 1}${suffixGroup}]`;
|
|
1069
1093
|
}));
|
|
1070
1094
|
}
|
|
1071
1095
|
line = this.state.lines[this.state.cursorLine] || "";
|
|
@@ -1100,7 +1124,7 @@ export class Editor {
|
|
|
1100
1124
|
this.tryTriggerAutocomplete();
|
|
1101
1125
|
}
|
|
1102
1126
|
// Symbol-based completion context like @ or #
|
|
1103
|
-
else if (
|
|
1127
|
+
else if (symbolTriggerPattern.test(textBeforeCursor)) {
|
|
1104
1128
|
this.tryTriggerAutocomplete();
|
|
1105
1129
|
}
|
|
1106
1130
|
}
|
|
@@ -1150,7 +1174,7 @@ export class Editor {
|
|
|
1150
1174
|
// Snap cursor to atomic segment boundary (e.g. paste markers)
|
|
1151
1175
|
// so the cursor never lands in the middle of a multi-grapheme unit.
|
|
1152
1176
|
// Single-grapheme segments don't need snapping.
|
|
1153
|
-
const segments = [...this.segment(logicalLine)];
|
|
1177
|
+
const segments = [...this.segment(logicalLine, "grapheme")];
|
|
1154
1178
|
for (const seg of segments) {
|
|
1155
1179
|
if (seg.index > this.state.cursorCol)
|
|
1156
1180
|
break;
|
|
@@ -1370,7 +1394,7 @@ export class Editor {
|
|
|
1370
1394
|
// Delete grapheme at cursor position (handles emojis, combining characters, etc.)
|
|
1371
1395
|
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
1372
1396
|
// Find the first grapheme at cursor
|
|
1373
|
-
const graphemes = [...this.segment(afterCursor)];
|
|
1397
|
+
const graphemes = [...this.segment(afterCursor, "grapheme")];
|
|
1374
1398
|
const firstGrapheme = graphemes[0];
|
|
1375
1399
|
const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
|
|
1376
1400
|
const before = currentLine.slice(0, this.state.cursorCol);
|
|
@@ -1399,7 +1423,7 @@ export class Editor {
|
|
|
1399
1423
|
this.tryTriggerAutocomplete();
|
|
1400
1424
|
}
|
|
1401
1425
|
// Symbol-based completion context like @ or #
|
|
1402
|
-
else if (
|
|
1426
|
+
else if (symbolTriggerPattern.test(textBeforeCursor)) {
|
|
1403
1427
|
this.tryTriggerAutocomplete();
|
|
1404
1428
|
}
|
|
1405
1429
|
}
|
|
@@ -1425,7 +1449,7 @@ export class Editor {
|
|
|
1425
1449
|
}
|
|
1426
1450
|
else {
|
|
1427
1451
|
// Line needs wrapping - use word-aware wrapping
|
|
1428
|
-
const chunks = wordWrapLine(line, width, [...this.segment(line)]);
|
|
1452
|
+
const chunks = wordWrapLine(line, width, [...this.segment(line, "grapheme")]);
|
|
1429
1453
|
for (const chunk of chunks) {
|
|
1430
1454
|
visualLines.push({
|
|
1431
1455
|
logicalLine: i,
|
|
@@ -1477,7 +1501,7 @@ export class Editor {
|
|
|
1477
1501
|
// Moving right - move by one grapheme (handles emojis, combining characters, etc.)
|
|
1478
1502
|
if (this.state.cursorCol < currentLine.length) {
|
|
1479
1503
|
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
1480
|
-
const graphemes = [...this.segment(afterCursor)];
|
|
1504
|
+
const graphemes = [...this.segment(afterCursor, "grapheme")];
|
|
1481
1505
|
const firstGrapheme = graphemes[0];
|
|
1482
1506
|
this.setCursorCol(this.state.cursorCol + (firstGrapheme ? firstGrapheme.segment.length : 1));
|
|
1483
1507
|
}
|
|
@@ -1498,7 +1522,7 @@ export class Editor {
|
|
|
1498
1522
|
// Moving left - move by one grapheme (handles emojis, combining characters, etc.)
|
|
1499
1523
|
if (this.state.cursorCol > 0) {
|
|
1500
1524
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1501
|
-
const graphemes = [...this.segment(beforeCursor)];
|
|
1525
|
+
const graphemes = [...this.segment(beforeCursor, "grapheme")];
|
|
1502
1526
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
1503
1527
|
this.setCursorCol(this.state.cursorCol - (lastGrapheme ? lastGrapheme.segment.length : 1));
|
|
1504
1528
|
}
|
|
@@ -1547,40 +1571,10 @@ export class Editor {
|
|
|
1547
1571
|
}
|
|
1548
1572
|
return;
|
|
1549
1573
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
while (graphemes.length > 0 &&
|
|
1555
|
-
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1556
|
-
isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1557
|
-
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1558
|
-
}
|
|
1559
|
-
if (graphemes.length > 0) {
|
|
1560
|
-
const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
|
|
1561
|
-
if (isPasteMarker(lastGrapheme)) {
|
|
1562
|
-
// Paste marker is a single atomic word
|
|
1563
|
-
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1564
|
-
}
|
|
1565
|
-
else if (isPunctuationChar(lastGrapheme)) {
|
|
1566
|
-
// Skip punctuation run
|
|
1567
|
-
while (graphemes.length > 0 &&
|
|
1568
|
-
isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1569
|
-
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1570
|
-
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
else {
|
|
1574
|
-
// Skip word run
|
|
1575
|
-
while (graphemes.length > 0 &&
|
|
1576
|
-
!isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1577
|
-
!isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1578
|
-
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1579
|
-
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
this.setCursorCol(newCol);
|
|
1574
|
+
this.setCursorCol(findWordBackward(currentLine, this.state.cursorCol, {
|
|
1575
|
+
segment: (text) => this.segment(text, "word"),
|
|
1576
|
+
isAtomicSegment: isPasteMarker,
|
|
1577
|
+
}));
|
|
1584
1578
|
}
|
|
1585
1579
|
/**
|
|
1586
1580
|
* Yank (paste) the most recent kill ring entry at cursor position.
|
|
@@ -1684,14 +1678,16 @@ export class Editor {
|
|
|
1684
1678
|
}
|
|
1685
1679
|
}
|
|
1686
1680
|
pushUndoSnapshot() {
|
|
1687
|
-
this.undoStack.push(this.state);
|
|
1681
|
+
this.undoStack.push({ state: this.state, pastes: this.pastes, pasteCounter: this.pasteCounter });
|
|
1688
1682
|
}
|
|
1689
1683
|
undo() {
|
|
1690
1684
|
this.exitHistoryBrowsing();
|
|
1691
1685
|
const snapshot = this.undoStack.pop();
|
|
1692
1686
|
if (!snapshot)
|
|
1693
1687
|
return;
|
|
1694
|
-
Object.assign(this.state, snapshot);
|
|
1688
|
+
Object.assign(this.state, snapshot.state);
|
|
1689
|
+
this.pastes = snapshot.pastes;
|
|
1690
|
+
this.pasteCounter = snapshot.pasteCounter;
|
|
1695
1691
|
this.lastAction = null;
|
|
1696
1692
|
this.preferredVisualCol = null;
|
|
1697
1693
|
if (this.onChange) {
|
|
@@ -1737,41 +1733,10 @@ export class Editor {
|
|
|
1737
1733
|
}
|
|
1738
1734
|
return;
|
|
1739
1735
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
let newCol = this.state.cursorCol;
|
|
1745
|
-
// Skip leading whitespace
|
|
1746
|
-
while (!next.done && !isPasteMarker(next.value.segment) && isWhitespaceChar(next.value.segment)) {
|
|
1747
|
-
newCol += next.value.segment.length;
|
|
1748
|
-
next = iterator.next();
|
|
1749
|
-
}
|
|
1750
|
-
if (!next.done) {
|
|
1751
|
-
const firstGrapheme = next.value.segment;
|
|
1752
|
-
if (isPasteMarker(firstGrapheme)) {
|
|
1753
|
-
// Paste marker is a single atomic word
|
|
1754
|
-
newCol += firstGrapheme.length;
|
|
1755
|
-
}
|
|
1756
|
-
else if (isPunctuationChar(firstGrapheme)) {
|
|
1757
|
-
// Skip punctuation run
|
|
1758
|
-
while (!next.done && isPunctuationChar(next.value.segment) && !isPasteMarker(next.value.segment)) {
|
|
1759
|
-
newCol += next.value.segment.length;
|
|
1760
|
-
next = iterator.next();
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
else {
|
|
1764
|
-
// Skip word run
|
|
1765
|
-
while (!next.done &&
|
|
1766
|
-
!isWhitespaceChar(next.value.segment) &&
|
|
1767
|
-
!isPunctuationChar(next.value.segment) &&
|
|
1768
|
-
!isPasteMarker(next.value.segment)) {
|
|
1769
|
-
newCol += next.value.segment.length;
|
|
1770
|
-
next = iterator.next();
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
this.setCursorCol(newCol);
|
|
1736
|
+
this.setCursorCol(findWordForward(currentLine, this.state.cursorCol, {
|
|
1737
|
+
segment: (text) => this.segment(text, "word"),
|
|
1738
|
+
isAtomicSegment: isPasteMarker,
|
|
1739
|
+
}));
|
|
1775
1740
|
}
|
|
1776
1741
|
// Slash menu only allowed on the first line of the editor
|
|
1777
1742
|
isSlashMenuAllowed() {
|
|
@@ -1885,7 +1850,7 @@ export class Editor {
|
|
|
1885
1850
|
}
|
|
1886
1851
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1887
1852
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1888
|
-
const isSymbolAutocompleteContext =
|
|
1853
|
+
const isSymbolAutocompleteContext = symbolDebouncePattern.test(textBeforeCursor);
|
|
1889
1854
|
return isSymbolAutocompleteContext ? ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS : 0;
|
|
1890
1855
|
}
|
|
1891
1856
|
async runAutocompleteRequest(requestId, controller, snapshotText, snapshotLine, snapshotCol, options) {
|