@markusylisiurunen/tau 0.2.76 → 0.2.77
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/core/version.js +1 -1
- package/dist/tui/chat_controller.js +1 -0
- package/dist/tui/chat_controller.js.map +1 -1
- package/dist/tui/ui/components/editor.js +302 -234
- package/dist/tui/ui/components/editor.js.map +1 -1
- package/dist/tui/ui/custom_editor.js +10 -8
- package/dist/tui/ui/custom_editor.js.map +1 -1
- package/dist/tui/ui/rewind_picker.js +7 -2
- package/dist/tui/ui/rewind_picker.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { getEditorKeybindings, matchesKey, SelectList, visibleWidth, } from "@mariozechner/pi-tui";
|
|
1
|
+
import { decodeKittyPrintable, getEditorKeybindings, matchesKey, SelectList, truncateToWidth, visibleWidth, } from "@mariozechner/pi-tui";
|
|
2
2
|
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
3
|
+
const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]/g;
|
|
4
|
+
const PASTE_MARKER_SINGLE = /^\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]$/;
|
|
3
5
|
const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;
|
|
4
6
|
const CSI_PATTERN = "\\x1b\\[[0-9;]*[A-Za-z]";
|
|
5
7
|
const OSC_PATTERN = "\\x1b\\][^\\x07]*(?:\\x07|\\x1b\\\\)";
|
|
@@ -13,6 +15,45 @@ function isWhitespaceChar(char) {
|
|
|
13
15
|
function isPunctuationChar(char) {
|
|
14
16
|
return PUNCTUATION_REGEX.test(char);
|
|
15
17
|
}
|
|
18
|
+
function isPasteMarker(segment) {
|
|
19
|
+
return segment.length >= 10 && PASTE_MARKER_SINGLE.test(segment);
|
|
20
|
+
}
|
|
21
|
+
function segmentWithMarkers(text, validIds) {
|
|
22
|
+
if (validIds.size === 0 || !text.includes("[paste #")) {
|
|
23
|
+
return segmenter.segment(text);
|
|
24
|
+
}
|
|
25
|
+
const markers = [];
|
|
26
|
+
for (const match of text.matchAll(PASTE_MARKER_REGEX)) {
|
|
27
|
+
const id = Number.parseInt(match[1] ?? "", 10);
|
|
28
|
+
if (!validIds.has(id))
|
|
29
|
+
continue;
|
|
30
|
+
markers.push({ start: match.index ?? 0, end: (match.index ?? 0) + match[0].length });
|
|
31
|
+
}
|
|
32
|
+
if (markers.length === 0) {
|
|
33
|
+
return segmenter.segment(text);
|
|
34
|
+
}
|
|
35
|
+
const baseSegments = segmenter.segment(text);
|
|
36
|
+
const result = [];
|
|
37
|
+
let markerIndex = 0;
|
|
38
|
+
for (const seg of baseSegments) {
|
|
39
|
+
while (markerIndex < markers.length && (markers[markerIndex]?.end ?? 0) <= seg.index) {
|
|
40
|
+
markerIndex++;
|
|
41
|
+
}
|
|
42
|
+
const marker = markerIndex < markers.length ? markers[markerIndex] : null;
|
|
43
|
+
if (marker && seg.index >= marker.start && seg.index < marker.end) {
|
|
44
|
+
if (seg.index === marker.start) {
|
|
45
|
+
result.push({
|
|
46
|
+
segment: text.slice(marker.start, marker.end),
|
|
47
|
+
index: marker.start,
|
|
48
|
+
input: text,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
result.push(seg);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
16
57
|
function stripAnsiSequences(text) {
|
|
17
58
|
if (!text.includes("\x1b"))
|
|
18
59
|
return text;
|
|
@@ -25,6 +66,11 @@ function stripAnsiSequences(text) {
|
|
|
25
66
|
function sanitizeInputText(text) {
|
|
26
67
|
return text ? stripAnsiSequences(text) : text;
|
|
27
68
|
}
|
|
69
|
+
const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
|
|
70
|
+
minPrimaryColumnWidth: 12,
|
|
71
|
+
maxPrimaryColumnWidth: 40,
|
|
72
|
+
truncatePrimary: ({ text, maxWidth }) => truncateToWidth(text, maxWidth, "…"),
|
|
73
|
+
};
|
|
28
74
|
/**
|
|
29
75
|
* Split a line into word-wrapped chunks.
|
|
30
76
|
* Wraps at word boundaries when possible, falling back to character-level
|
|
@@ -34,7 +80,7 @@ function sanitizeInputText(text) {
|
|
|
34
80
|
* @param maxWidth - Maximum visible width per chunk
|
|
35
81
|
* @returns Array of chunks with text and position information
|
|
36
82
|
*/
|
|
37
|
-
function wordWrapLine(line, maxWidth) {
|
|
83
|
+
function wordWrapLine(line, maxWidth, preSegmented) {
|
|
38
84
|
if (!line || maxWidth <= 0) {
|
|
39
85
|
return [{ text: "", startIndex: 0, endIndex: 0 }];
|
|
40
86
|
}
|
|
@@ -43,141 +89,69 @@ function wordWrapLine(line, maxWidth) {
|
|
|
43
89
|
return [{ text: line, startIndex: 0, endIndex: line.length }];
|
|
44
90
|
}
|
|
45
91
|
const chunks = [];
|
|
46
|
-
|
|
47
|
-
const tokens = [];
|
|
48
|
-
let currentToken = "";
|
|
49
|
-
let tokenStart = 0;
|
|
50
|
-
let inWhitespace = false;
|
|
51
|
-
let charIndex = 0;
|
|
52
|
-
for (const seg of segmenter.segment(line)) {
|
|
53
|
-
const grapheme = seg.segment;
|
|
54
|
-
const graphemeIsWhitespace = isWhitespaceChar(grapheme);
|
|
55
|
-
if (currentToken === "") {
|
|
56
|
-
inWhitespace = graphemeIsWhitespace;
|
|
57
|
-
tokenStart = charIndex;
|
|
58
|
-
}
|
|
59
|
-
else if (graphemeIsWhitespace !== inWhitespace) {
|
|
60
|
-
// Token type changed - save current token
|
|
61
|
-
tokens.push({
|
|
62
|
-
text: currentToken,
|
|
63
|
-
startIndex: tokenStart,
|
|
64
|
-
endIndex: charIndex,
|
|
65
|
-
isWhitespace: inWhitespace,
|
|
66
|
-
});
|
|
67
|
-
currentToken = "";
|
|
68
|
-
tokenStart = charIndex;
|
|
69
|
-
inWhitespace = graphemeIsWhitespace;
|
|
70
|
-
}
|
|
71
|
-
currentToken += grapheme;
|
|
72
|
-
charIndex += grapheme.length;
|
|
73
|
-
}
|
|
74
|
-
// Push final token
|
|
75
|
-
if (currentToken) {
|
|
76
|
-
tokens.push({
|
|
77
|
-
text: currentToken,
|
|
78
|
-
startIndex: tokenStart,
|
|
79
|
-
endIndex: charIndex,
|
|
80
|
-
isWhitespace: inWhitespace,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
// Build chunks using word wrapping
|
|
84
|
-
let currentChunk = "";
|
|
92
|
+
const segments = preSegmented ?? [...segmenter.segment(line)];
|
|
85
93
|
let currentWidth = 0;
|
|
86
|
-
let
|
|
87
|
-
let
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
chunkStartIndex = token.endIndex;
|
|
94
|
+
let chunkStart = 0;
|
|
95
|
+
let wrapOppIndex = -1;
|
|
96
|
+
let wrapOppWidth = 0;
|
|
97
|
+
for (let i = 0; i < segments.length; i++) {
|
|
98
|
+
const seg = segments[i];
|
|
99
|
+
if (!seg)
|
|
93
100
|
continue;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
101
|
+
const grapheme = seg.segment;
|
|
102
|
+
const graphemeWidth = visibleWidth(grapheme);
|
|
103
|
+
const charIndex = seg.index;
|
|
104
|
+
const isWhitespace = !isPasteMarker(grapheme) && isWhitespaceChar(grapheme);
|
|
105
|
+
if (currentWidth + graphemeWidth > maxWidth) {
|
|
106
|
+
if (wrapOppIndex >= 0 && currentWidth - wrapOppWidth + graphemeWidth <= maxWidth) {
|
|
100
107
|
chunks.push({
|
|
101
|
-
text:
|
|
102
|
-
startIndex:
|
|
103
|
-
endIndex:
|
|
108
|
+
text: line.slice(chunkStart, wrapOppIndex),
|
|
109
|
+
startIndex: chunkStart,
|
|
110
|
+
endIndex: wrapOppIndex,
|
|
104
111
|
});
|
|
105
|
-
|
|
106
|
-
currentWidth
|
|
107
|
-
chunkStartIndex = token.startIndex;
|
|
108
|
-
}
|
|
109
|
-
// Break the long token by grapheme
|
|
110
|
-
let tokenChunk = "";
|
|
111
|
-
let tokenChunkWidth = 0;
|
|
112
|
-
let tokenChunkStart = token.startIndex;
|
|
113
|
-
let tokenCharIndex = token.startIndex;
|
|
114
|
-
for (const seg of segmenter.segment(token.text)) {
|
|
115
|
-
const grapheme = seg.segment;
|
|
116
|
-
const graphemeWidth = visibleWidth(grapheme);
|
|
117
|
-
if (tokenChunkWidth + graphemeWidth > maxWidth && tokenChunk) {
|
|
118
|
-
chunks.push({
|
|
119
|
-
text: tokenChunk,
|
|
120
|
-
startIndex: tokenChunkStart,
|
|
121
|
-
endIndex: tokenCharIndex,
|
|
122
|
-
});
|
|
123
|
-
tokenChunk = grapheme;
|
|
124
|
-
tokenChunkWidth = graphemeWidth;
|
|
125
|
-
tokenChunkStart = tokenCharIndex;
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
tokenChunk += grapheme;
|
|
129
|
-
tokenChunkWidth += graphemeWidth;
|
|
130
|
-
}
|
|
131
|
-
tokenCharIndex += grapheme.length;
|
|
112
|
+
chunkStart = wrapOppIndex;
|
|
113
|
+
currentWidth -= wrapOppWidth;
|
|
132
114
|
}
|
|
133
|
-
|
|
134
|
-
if (tokenChunk) {
|
|
135
|
-
currentChunk = tokenChunk;
|
|
136
|
-
currentWidth = tokenChunkWidth;
|
|
137
|
-
chunkStartIndex = tokenChunkStart;
|
|
138
|
-
}
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
// Check if adding this token would exceed width
|
|
142
|
-
if (currentWidth + tokenWidth > maxWidth) {
|
|
143
|
-
// Push current chunk (trimming trailing whitespace for display)
|
|
144
|
-
const trimmedChunk = currentChunk.trimEnd();
|
|
145
|
-
if (trimmedChunk || chunks.length === 0) {
|
|
115
|
+
else if (chunkStart < charIndex) {
|
|
146
116
|
chunks.push({
|
|
147
|
-
text:
|
|
148
|
-
startIndex:
|
|
149
|
-
endIndex:
|
|
117
|
+
text: line.slice(chunkStart, charIndex),
|
|
118
|
+
startIndex: chunkStart,
|
|
119
|
+
endIndex: charIndex,
|
|
150
120
|
});
|
|
151
|
-
|
|
152
|
-
// Start new line - skip leading whitespace
|
|
153
|
-
atLineStart = true;
|
|
154
|
-
if (token.isWhitespace) {
|
|
155
|
-
currentChunk = "";
|
|
121
|
+
chunkStart = charIndex;
|
|
156
122
|
currentWidth = 0;
|
|
157
|
-
chunkStartIndex = token.endIndex;
|
|
158
123
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
124
|
+
wrapOppIndex = -1;
|
|
125
|
+
}
|
|
126
|
+
if (graphemeWidth > maxWidth) {
|
|
127
|
+
const subChunks = wordWrapLine(grapheme, maxWidth);
|
|
128
|
+
for (let j = 0; j < subChunks.length - 1; j++) {
|
|
129
|
+
const subChunk = subChunks[j];
|
|
130
|
+
if (!subChunk)
|
|
131
|
+
continue;
|
|
132
|
+
chunks.push({
|
|
133
|
+
text: subChunk.text,
|
|
134
|
+
startIndex: charIndex + subChunk.startIndex,
|
|
135
|
+
endIndex: charIndex + subChunk.endIndex,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const lastSubChunk = subChunks[subChunks.length - 1];
|
|
139
|
+
if (lastSubChunk) {
|
|
140
|
+
chunkStart = charIndex + lastSubChunk.startIndex;
|
|
141
|
+
currentWidth = visibleWidth(lastSubChunk.text);
|
|
164
142
|
}
|
|
143
|
+
wrapOppIndex = -1;
|
|
144
|
+
continue;
|
|
165
145
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
146
|
+
currentWidth += graphemeWidth;
|
|
147
|
+
const next = segments[i + 1];
|
|
148
|
+
if (isWhitespace && next && (isPasteMarker(next.segment) || !isWhitespaceChar(next.segment))) {
|
|
149
|
+
wrapOppIndex = next.index;
|
|
150
|
+
wrapOppWidth = currentWidth;
|
|
170
151
|
}
|
|
171
152
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
chunks.push({
|
|
175
|
-
text: currentChunk,
|
|
176
|
-
startIndex: chunkStartIndex,
|
|
177
|
-
endIndex: line.length,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
return chunks.length > 0 ? chunks : [{ text: "", startIndex: 0, endIndex: 0 }];
|
|
153
|
+
chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
|
|
154
|
+
return chunks;
|
|
181
155
|
}
|
|
182
156
|
export class Editor {
|
|
183
157
|
state = {
|
|
@@ -193,7 +167,7 @@ export class Editor {
|
|
|
193
167
|
// Autocomplete support
|
|
194
168
|
autocompleteProvider;
|
|
195
169
|
autocompleteList;
|
|
196
|
-
|
|
170
|
+
autocompleteState = null;
|
|
197
171
|
autocompletePrefix = "";
|
|
198
172
|
// Paste tracking for large pastes
|
|
199
173
|
pastes = new Map();
|
|
@@ -211,6 +185,15 @@ export class Editor {
|
|
|
211
185
|
this.theme = theme;
|
|
212
186
|
this.borderColor = theme.borderColor;
|
|
213
187
|
}
|
|
188
|
+
validPasteIds() {
|
|
189
|
+
return new Set(this.pastes.keys());
|
|
190
|
+
}
|
|
191
|
+
segment(text) {
|
|
192
|
+
return segmentWithMarkers(text, this.validPasteIds());
|
|
193
|
+
}
|
|
194
|
+
normalizeText(text) {
|
|
195
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ");
|
|
196
|
+
}
|
|
214
197
|
/** Wraps text in cursor styling (inverse video). Override in subclasses for theme-aware cursor. */
|
|
215
198
|
cursorStyle(text) {
|
|
216
199
|
return `\x1b[7m${text}\x1b[0m`;
|
|
@@ -300,11 +283,11 @@ export class Editor {
|
|
|
300
283
|
const after = displayText.slice(layoutLine.cursorPos);
|
|
301
284
|
if (after.length > 0) {
|
|
302
285
|
// Cursor is on a character (grapheme) - replace it with highlighted version
|
|
303
|
-
// Get the first
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
const restAfter = after.slice(
|
|
307
|
-
displayText = before + this.cursorStyle(
|
|
286
|
+
// Get the first segment from 'after'
|
|
287
|
+
const afterSegments = [...this.segment(after)];
|
|
288
|
+
const firstSegment = afterSegments[0]?.segment || "";
|
|
289
|
+
const restAfter = after.slice(firstSegment.length);
|
|
290
|
+
displayText = before + this.cursorStyle(firstSegment) + restAfter;
|
|
308
291
|
// lineVisibleWidth stays the same - we're replacing, not adding
|
|
309
292
|
}
|
|
310
293
|
else {
|
|
@@ -316,17 +299,17 @@ export class Editor {
|
|
|
316
299
|
lineVisibleWidth = lineVisibleWidth + 1;
|
|
317
300
|
}
|
|
318
301
|
else {
|
|
319
|
-
// Line is at full width - use reverse video on last
|
|
302
|
+
// Line is at full width - use reverse video on last segment if possible
|
|
320
303
|
// or just show cursor at the end without adding space
|
|
321
|
-
const
|
|
322
|
-
if (
|
|
323
|
-
const
|
|
324
|
-
// Rebuild 'before' without the last
|
|
325
|
-
const beforeWithoutLast =
|
|
304
|
+
const beforeSegments = [...this.segment(before)];
|
|
305
|
+
if (beforeSegments.length > 0) {
|
|
306
|
+
const lastSegment = beforeSegments[beforeSegments.length - 1]?.segment || "";
|
|
307
|
+
// Rebuild 'before' without the last segment
|
|
308
|
+
const beforeWithoutLast = beforeSegments
|
|
326
309
|
.slice(0, -1)
|
|
327
310
|
.map((g) => g.segment)
|
|
328
311
|
.join("");
|
|
329
|
-
displayText = beforeWithoutLast + this.cursorStyle(
|
|
312
|
+
displayText = beforeWithoutLast + this.cursorStyle(lastSegment);
|
|
330
313
|
}
|
|
331
314
|
// lineVisibleWidth stays the same
|
|
332
315
|
}
|
|
@@ -340,7 +323,7 @@ export class Editor {
|
|
|
340
323
|
// Render bottom border
|
|
341
324
|
result.push(horizontal.repeat(width));
|
|
342
325
|
// Add autocomplete list if active
|
|
343
|
-
if (this.
|
|
326
|
+
if (this.autocompleteState && this.autocompleteList) {
|
|
344
327
|
const autocompleteResult = this.autocompleteList.render(width);
|
|
345
328
|
result.push(...autocompleteResult);
|
|
346
329
|
}
|
|
@@ -381,7 +364,7 @@ export class Editor {
|
|
|
381
364
|
return;
|
|
382
365
|
}
|
|
383
366
|
// Handle autocomplete mode
|
|
384
|
-
if (this.
|
|
367
|
+
if (this.autocompleteState && this.autocompleteList) {
|
|
385
368
|
if (kb.matches(data, "selectCancel")) {
|
|
386
369
|
this.cancelAutocomplete();
|
|
387
370
|
return;
|
|
@@ -434,7 +417,7 @@ export class Editor {
|
|
|
434
417
|
}
|
|
435
418
|
}
|
|
436
419
|
// Tab - trigger completion
|
|
437
|
-
if (kb.matches(data, "tab") && !this.
|
|
420
|
+
if (kb.matches(data, "tab") && !this.autocompleteState) {
|
|
438
421
|
this.handleTabCompletion();
|
|
439
422
|
return;
|
|
440
423
|
}
|
|
@@ -538,6 +521,11 @@ export class Editor {
|
|
|
538
521
|
this.insertCharacter(" ");
|
|
539
522
|
return;
|
|
540
523
|
}
|
|
524
|
+
const kittyPrintable = decodeKittyPrintable(data);
|
|
525
|
+
if (kittyPrintable !== undefined) {
|
|
526
|
+
this.insertCharacter(kittyPrintable);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
541
529
|
// Regular characters
|
|
542
530
|
if (data.charCodeAt(0) >= 32) {
|
|
543
531
|
this.insertCharacter(data);
|
|
@@ -578,7 +566,7 @@ export class Editor {
|
|
|
578
566
|
}
|
|
579
567
|
else {
|
|
580
568
|
// Line needs wrapping - use word-aware wrapping
|
|
581
|
-
const chunks = wordWrapLine(line, contentWidth);
|
|
569
|
+
const chunks = wordWrapLine(line, contentWidth, [...this.segment(line)]);
|
|
582
570
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
583
571
|
const chunk = chunks[chunkIndex];
|
|
584
572
|
if (!chunk)
|
|
@@ -660,8 +648,40 @@ export class Editor {
|
|
|
660
648
|
* Used for programmatic insertion (e.g., clipboard image markers).
|
|
661
649
|
*/
|
|
662
650
|
insertTextAtCursor(text) {
|
|
663
|
-
|
|
664
|
-
|
|
651
|
+
this.insertTextAtCursorInternal(text);
|
|
652
|
+
}
|
|
653
|
+
insertTextAtCursorInternal(text) {
|
|
654
|
+
if (!text)
|
|
655
|
+
return;
|
|
656
|
+
const sanitized = sanitizeInputText(text);
|
|
657
|
+
if (!sanitized)
|
|
658
|
+
return;
|
|
659
|
+
this.historyIndex = -1;
|
|
660
|
+
const normalized = this.normalizeText(sanitized);
|
|
661
|
+
const insertedLines = normalized.split("\n");
|
|
662
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
663
|
+
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
664
|
+
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
665
|
+
if (insertedLines.length === 1) {
|
|
666
|
+
this.state.lines[this.state.cursorLine] = beforeCursor + normalized + afterCursor;
|
|
667
|
+
this.state.cursorCol += normalized.length;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
this.state.lines = [
|
|
671
|
+
...this.state.lines.slice(0, this.state.cursorLine),
|
|
672
|
+
beforeCursor + (insertedLines[0] || ""),
|
|
673
|
+
...insertedLines.slice(1, -1),
|
|
674
|
+
(insertedLines[insertedLines.length - 1] || "") + afterCursor,
|
|
675
|
+
...this.state.lines.slice(this.state.cursorLine + 1),
|
|
676
|
+
];
|
|
677
|
+
this.state.cursorLine += insertedLines.length - 1;
|
|
678
|
+
this.state.cursorCol = (insertedLines[insertedLines.length - 1] || "").length;
|
|
679
|
+
}
|
|
680
|
+
if (this.onChange) {
|
|
681
|
+
this.onChange(this.getText());
|
|
682
|
+
}
|
|
683
|
+
if (this.autocompleteState) {
|
|
684
|
+
this.updateAutocomplete();
|
|
665
685
|
}
|
|
666
686
|
}
|
|
667
687
|
// All the editor methods from before...
|
|
@@ -679,7 +699,7 @@ export class Editor {
|
|
|
679
699
|
this.onChange(this.getText());
|
|
680
700
|
}
|
|
681
701
|
// Check if we should trigger or update autocomplete
|
|
682
|
-
if (!this.
|
|
702
|
+
if (!this.autocompleteState) {
|
|
683
703
|
// Auto-trigger for "/" at the start of a line (slash commands)
|
|
684
704
|
if (sanitized === "/" && this.isAtStartOfMessage()) {
|
|
685
705
|
this.tryTriggerAutocomplete();
|
|
@@ -698,11 +718,9 @@ export class Editor {
|
|
|
698
718
|
else if (/[a-zA-Z0-9.\-_]/.test(sanitized)) {
|
|
699
719
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
700
720
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
701
|
-
|
|
702
|
-
if (textBeforeCursor.trimStart().startsWith("/")) {
|
|
721
|
+
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
703
722
|
this.tryTriggerAutocomplete();
|
|
704
723
|
}
|
|
705
|
-
// Check if we're in an @ mention context
|
|
706
724
|
else if (this.isMentionAutocompleteContext(textBeforeCursor)) {
|
|
707
725
|
this.tryTriggerAutocomplete();
|
|
708
726
|
}
|
|
@@ -716,11 +734,9 @@ export class Editor {
|
|
|
716
734
|
this.historyIndex = -1; // Exit history browsing mode
|
|
717
735
|
// Clean the pasted text
|
|
718
736
|
const sanitizedText = sanitizeInputText(pastedText);
|
|
719
|
-
const cleanText =
|
|
720
|
-
// Convert tabs to spaces (4 spaces per tab)
|
|
721
|
-
const tabExpandedText = cleanText.replace(/\t/g, " ");
|
|
737
|
+
const cleanText = this.normalizeText(sanitizedText);
|
|
722
738
|
// Filter out non-printable characters except newlines
|
|
723
|
-
let filteredText =
|
|
739
|
+
let filteredText = cleanText
|
|
724
740
|
.split("")
|
|
725
741
|
.filter((char) => char === "\n" || char.charCodeAt(0) >= 32)
|
|
726
742
|
.join("");
|
|
@@ -746,50 +762,10 @@ export class Editor {
|
|
|
746
762
|
const marker = pastedLines.length > 32
|
|
747
763
|
? `[paste #${pasteId} +${pastedLines.length} lines]`
|
|
748
764
|
: `[paste #${pasteId} ${totalChars} chars]`;
|
|
749
|
-
|
|
750
|
-
this.insertCharacter(char);
|
|
751
|
-
}
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
if (pastedLines.length === 1) {
|
|
755
|
-
// Single line - just insert each character
|
|
756
|
-
const text = pastedLines[0] || "";
|
|
757
|
-
for (const char of text) {
|
|
758
|
-
this.insertCharacter(char);
|
|
759
|
-
}
|
|
765
|
+
this.insertTextAtCursorInternal(marker);
|
|
760
766
|
return;
|
|
761
767
|
}
|
|
762
|
-
|
|
763
|
-
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
764
|
-
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
765
|
-
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
766
|
-
// Build the new lines array step by step
|
|
767
|
-
const newLines = [];
|
|
768
|
-
// Add all lines before current line
|
|
769
|
-
for (let i = 0; i < this.state.cursorLine; i++) {
|
|
770
|
-
newLines.push(this.state.lines[i] || "");
|
|
771
|
-
}
|
|
772
|
-
// Add the first pasted line merged with before cursor text
|
|
773
|
-
newLines.push(beforeCursor + (pastedLines[0] || ""));
|
|
774
|
-
// Add all middle pasted lines
|
|
775
|
-
for (let i = 1; i < pastedLines.length - 1; i++) {
|
|
776
|
-
newLines.push(pastedLines[i] || "");
|
|
777
|
-
}
|
|
778
|
-
// Add the last pasted line with after cursor text
|
|
779
|
-
newLines.push((pastedLines[pastedLines.length - 1] || "") + afterCursor);
|
|
780
|
-
// Add all lines after current line
|
|
781
|
-
for (let i = this.state.cursorLine + 1; i < this.state.lines.length; i++) {
|
|
782
|
-
newLines.push(this.state.lines[i] || "");
|
|
783
|
-
}
|
|
784
|
-
// Replace the entire lines array
|
|
785
|
-
this.state.lines = newLines;
|
|
786
|
-
// Update cursor position to end of pasted content
|
|
787
|
-
this.state.cursorLine += pastedLines.length - 1;
|
|
788
|
-
this.state.cursorCol = (pastedLines[pastedLines.length - 1] || "").length;
|
|
789
|
-
// Notify of change
|
|
790
|
-
if (this.onChange) {
|
|
791
|
-
this.onChange(this.getText());
|
|
792
|
-
}
|
|
768
|
+
this.insertTextAtCursorInternal(filteredText);
|
|
793
769
|
}
|
|
794
770
|
addNewLine() {
|
|
795
771
|
this.historyIndex = -1; // Exit history browsing mode
|
|
@@ -813,7 +789,7 @@ export class Editor {
|
|
|
813
789
|
const line = this.state.lines[this.state.cursorLine] || "";
|
|
814
790
|
const beforeCursor = line.slice(0, this.state.cursorCol);
|
|
815
791
|
// Find the last grapheme in the text before cursor
|
|
816
|
-
const graphemes = [...
|
|
792
|
+
const graphemes = [...this.segment(beforeCursor)];
|
|
817
793
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
818
794
|
const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
819
795
|
const before = line.slice(0, this.state.cursorCol - graphemeLength);
|
|
@@ -834,18 +810,16 @@ export class Editor {
|
|
|
834
810
|
this.onChange(this.getText());
|
|
835
811
|
}
|
|
836
812
|
// Update or re-trigger autocomplete after backspace
|
|
837
|
-
if (this.
|
|
813
|
+
if (this.autocompleteState) {
|
|
838
814
|
this.updateAutocomplete();
|
|
839
815
|
}
|
|
840
816
|
else {
|
|
841
817
|
// If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
|
|
842
818
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
843
819
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
844
|
-
|
|
845
|
-
if (textBeforeCursor.trimStart().startsWith("/")) {
|
|
820
|
+
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
846
821
|
this.tryTriggerAutocomplete();
|
|
847
822
|
}
|
|
848
|
-
// @ mention context
|
|
849
823
|
else if (this.isMentionAutocompleteContext(textBeforeCursor)) {
|
|
850
824
|
this.tryTriggerAutocomplete();
|
|
851
825
|
}
|
|
@@ -928,7 +902,7 @@ export class Editor {
|
|
|
928
902
|
// Delete grapheme at cursor position (handles emojis, combining characters, etc.)
|
|
929
903
|
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
930
904
|
// Find the first grapheme at cursor
|
|
931
|
-
const graphemes = [...
|
|
905
|
+
const graphemes = [...this.segment(afterCursor)];
|
|
932
906
|
const firstGrapheme = graphemes[0];
|
|
933
907
|
const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
|
|
934
908
|
const before = currentLine.slice(0, this.state.cursorCol);
|
|
@@ -945,17 +919,15 @@ export class Editor {
|
|
|
945
919
|
this.onChange(this.getText());
|
|
946
920
|
}
|
|
947
921
|
// Update or re-trigger autocomplete after forward delete
|
|
948
|
-
if (this.
|
|
922
|
+
if (this.autocompleteState) {
|
|
949
923
|
this.updateAutocomplete();
|
|
950
924
|
}
|
|
951
925
|
else {
|
|
952
926
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
953
927
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
954
|
-
|
|
955
|
-
if (textBeforeCursor.trimStart().startsWith("/")) {
|
|
928
|
+
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
956
929
|
this.tryTriggerAutocomplete();
|
|
957
930
|
}
|
|
958
|
-
// @ mention context
|
|
959
931
|
else if (this.isMentionAutocompleteContext(textBeforeCursor)) {
|
|
960
932
|
this.tryTriggerAutocomplete();
|
|
961
933
|
}
|
|
@@ -982,7 +954,7 @@ export class Editor {
|
|
|
982
954
|
}
|
|
983
955
|
else {
|
|
984
956
|
// Line needs wrapping - use word-aware wrapping
|
|
985
|
-
const chunks = wordWrapLine(line, width);
|
|
957
|
+
const chunks = wordWrapLine(line, width, [...this.segment(line)]);
|
|
986
958
|
for (const chunk of chunks) {
|
|
987
959
|
visualLines.push({
|
|
988
960
|
logicalLine: i,
|
|
@@ -1016,6 +988,18 @@ export class Editor {
|
|
|
1016
988
|
// Fallback: return last visual line
|
|
1017
989
|
return visualLines.length - 1;
|
|
1018
990
|
}
|
|
991
|
+
snapCursorToSegmentBoundary(logicalLine, movingUp) {
|
|
992
|
+
for (const seg of this.segment(logicalLine)) {
|
|
993
|
+
if (seg.index > this.state.cursorCol)
|
|
994
|
+
break;
|
|
995
|
+
if (seg.segment.length <= 1)
|
|
996
|
+
continue;
|
|
997
|
+
if (this.state.cursorCol < seg.index + seg.segment.length) {
|
|
998
|
+
this.state.cursorCol = movingUp ? seg.index : seg.index + seg.segment.length;
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1019
1003
|
moveCursor(deltaLine, deltaCol) {
|
|
1020
1004
|
const width = this.lastWidth;
|
|
1021
1005
|
if (deltaLine !== 0) {
|
|
@@ -1035,6 +1019,7 @@ export class Editor {
|
|
|
1035
1019
|
const targetCol = targetVL.startCol + Math.min(visualCol, targetVL.length);
|
|
1036
1020
|
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
|
1037
1021
|
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
|
1022
|
+
this.snapCursorToSegmentBoundary(logicalLine, deltaLine < 0);
|
|
1038
1023
|
}
|
|
1039
1024
|
}
|
|
1040
1025
|
}
|
|
@@ -1044,7 +1029,7 @@ export class Editor {
|
|
|
1044
1029
|
// Moving right - move by one grapheme (handles emojis, combining characters, etc.)
|
|
1045
1030
|
if (this.state.cursorCol < currentLine.length) {
|
|
1046
1031
|
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
1047
|
-
const graphemes = [...
|
|
1032
|
+
const graphemes = [...this.segment(afterCursor)];
|
|
1048
1033
|
const firstGrapheme = graphemes[0];
|
|
1049
1034
|
this.state.cursorCol += firstGrapheme ? firstGrapheme.segment.length : 1;
|
|
1050
1035
|
}
|
|
@@ -1058,7 +1043,7 @@ export class Editor {
|
|
|
1058
1043
|
// Moving left - move by one grapheme (handles emojis, combining characters, etc.)
|
|
1059
1044
|
if (this.state.cursorCol > 0) {
|
|
1060
1045
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1061
|
-
const graphemes = [...
|
|
1046
|
+
const graphemes = [...this.segment(beforeCursor)];
|
|
1062
1047
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
1063
1048
|
this.state.cursorCol -= lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
1064
1049
|
}
|
|
@@ -1083,19 +1068,24 @@ export class Editor {
|
|
|
1083
1068
|
return;
|
|
1084
1069
|
}
|
|
1085
1070
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1086
|
-
const graphemes = [...
|
|
1071
|
+
const graphemes = [...this.segment(textBeforeCursor)];
|
|
1087
1072
|
let newCol = this.state.cursorCol;
|
|
1088
1073
|
// Skip trailing whitespace
|
|
1089
1074
|
while (graphemes.length > 0 &&
|
|
1075
|
+
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1090
1076
|
isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1091
1077
|
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1092
1078
|
}
|
|
1093
1079
|
if (graphemes.length > 0) {
|
|
1094
1080
|
const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
|
|
1095
|
-
if (
|
|
1081
|
+
if (isPasteMarker(lastGrapheme)) {
|
|
1082
|
+
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1083
|
+
}
|
|
1084
|
+
else if (isPunctuationChar(lastGrapheme)) {
|
|
1096
1085
|
// Skip punctuation run
|
|
1097
1086
|
while (graphemes.length > 0 &&
|
|
1098
|
-
isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "")
|
|
1087
|
+
isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1088
|
+
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1099
1089
|
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1100
1090
|
}
|
|
1101
1091
|
}
|
|
@@ -1103,7 +1093,8 @@ export class Editor {
|
|
|
1103
1093
|
// Skip word run
|
|
1104
1094
|
while (graphemes.length > 0 &&
|
|
1105
1095
|
!isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1106
|
-
!isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "")
|
|
1096
|
+
!isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1097
|
+
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1107
1098
|
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1108
1099
|
}
|
|
1109
1100
|
}
|
|
@@ -1121,20 +1112,28 @@ export class Editor {
|
|
|
1121
1112
|
return;
|
|
1122
1113
|
}
|
|
1123
1114
|
const textAfterCursor = currentLine.slice(this.state.cursorCol);
|
|
1124
|
-
const segments =
|
|
1115
|
+
const segments = this.segment(textAfterCursor);
|
|
1125
1116
|
const iterator = segments[Symbol.iterator]();
|
|
1126
1117
|
let next = iterator.next();
|
|
1118
|
+
let newCol = this.state.cursorCol;
|
|
1127
1119
|
// Skip leading whitespace
|
|
1128
|
-
while (!next.done &&
|
|
1129
|
-
|
|
1120
|
+
while (!next.done &&
|
|
1121
|
+
!isPasteMarker(next.value.segment) &&
|
|
1122
|
+
isWhitespaceChar(next.value.segment)) {
|
|
1123
|
+
newCol += next.value.segment.length;
|
|
1130
1124
|
next = iterator.next();
|
|
1131
1125
|
}
|
|
1132
1126
|
if (!next.done) {
|
|
1133
1127
|
const firstGrapheme = next.value.segment;
|
|
1134
|
-
if (
|
|
1128
|
+
if (isPasteMarker(firstGrapheme)) {
|
|
1129
|
+
newCol += firstGrapheme.length;
|
|
1130
|
+
}
|
|
1131
|
+
else if (isPunctuationChar(firstGrapheme)) {
|
|
1135
1132
|
// Skip punctuation run
|
|
1136
|
-
while (!next.done &&
|
|
1137
|
-
|
|
1133
|
+
while (!next.done &&
|
|
1134
|
+
isPunctuationChar(next.value.segment) &&
|
|
1135
|
+
!isPasteMarker(next.value.segment)) {
|
|
1136
|
+
newCol += next.value.segment.length;
|
|
1138
1137
|
next = iterator.next();
|
|
1139
1138
|
}
|
|
1140
1139
|
}
|
|
@@ -1142,20 +1141,29 @@ export class Editor {
|
|
|
1142
1141
|
// Skip word run
|
|
1143
1142
|
while (!next.done &&
|
|
1144
1143
|
!isWhitespaceChar(next.value.segment) &&
|
|
1145
|
-
!isPunctuationChar(next.value.segment)
|
|
1146
|
-
|
|
1144
|
+
!isPunctuationChar(next.value.segment) &&
|
|
1145
|
+
!isPasteMarker(next.value.segment)) {
|
|
1146
|
+
newCol += next.value.segment.length;
|
|
1147
1147
|
next = iterator.next();
|
|
1148
1148
|
}
|
|
1149
1149
|
}
|
|
1150
1150
|
}
|
|
1151
|
+
this.state.cursorCol = newCol;
|
|
1152
|
+
}
|
|
1153
|
+
isSlashMenuAllowed() {
|
|
1154
|
+
return this.state.lines.length === 1 && this.state.cursorLine === 0;
|
|
1151
1155
|
}
|
|
1152
1156
|
// Helper method to check if cursor is at start of message (for slash command detection)
|
|
1153
1157
|
isAtStartOfMessage() {
|
|
1158
|
+
if (!this.isSlashMenuAllowed())
|
|
1159
|
+
return false;
|
|
1154
1160
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1155
1161
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1156
|
-
// At start if line is empty, only contains whitespace, or is just "/"
|
|
1157
1162
|
return beforeCursor.trim() === "" || beforeCursor.trim() === "/";
|
|
1158
1163
|
}
|
|
1164
|
+
isInSlashCommandContext(textBeforeCursor) {
|
|
1165
|
+
return this.isSlashMenuAllowed() && textBeforeCursor.trimStart().startsWith("/");
|
|
1166
|
+
}
|
|
1159
1167
|
isMentionAutocompleteContext(textBeforeCursor) {
|
|
1160
1168
|
return /(?:^|[\s])@[^\s]*$/.test(textBeforeCursor);
|
|
1161
1169
|
}
|
|
@@ -1164,11 +1172,44 @@ export class Editor {
|
|
|
1164
1172
|
const beforeCursor = line.slice(0, cursorCol);
|
|
1165
1173
|
return /(?:^|[\s])@@[a-z-]+:$/.test(beforeCursor);
|
|
1166
1174
|
}
|
|
1175
|
+
getAutocompleteMatchPrefix(prefix) {
|
|
1176
|
+
if (prefix.startsWith("@@")) {
|
|
1177
|
+
const mentionMatch = prefix.match(/^@@[^:\s]+:(.*)$/);
|
|
1178
|
+
return mentionMatch ? (mentionMatch[1] ?? "") : prefix.slice(2);
|
|
1179
|
+
}
|
|
1180
|
+
if (prefix.startsWith("@") || prefix.startsWith("/")) {
|
|
1181
|
+
return prefix.slice(1);
|
|
1182
|
+
}
|
|
1183
|
+
return prefix;
|
|
1184
|
+
}
|
|
1185
|
+
getBestAutocompleteMatchIndex(items, prefix) {
|
|
1186
|
+
const matchPrefix = this.getAutocompleteMatchPrefix(prefix);
|
|
1187
|
+
if (!matchPrefix)
|
|
1188
|
+
return -1;
|
|
1189
|
+
let firstPrefixIndex = -1;
|
|
1190
|
+
for (let i = 0; i < items.length; i++) {
|
|
1191
|
+
const value = items[i]?.value;
|
|
1192
|
+
if (!value)
|
|
1193
|
+
continue;
|
|
1194
|
+
if (value === matchPrefix) {
|
|
1195
|
+
return i;
|
|
1196
|
+
}
|
|
1197
|
+
if (firstPrefixIndex === -1 && value.startsWith(matchPrefix)) {
|
|
1198
|
+
firstPrefixIndex = i;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return firstPrefixIndex;
|
|
1202
|
+
}
|
|
1203
|
+
createAutocompleteList(prefix, items) {
|
|
1204
|
+
const layout = this.isInSlashCommandContext(prefix)
|
|
1205
|
+
? SLASH_COMMAND_SELECT_LIST_LAYOUT
|
|
1206
|
+
: undefined;
|
|
1207
|
+
return new SelectList(items, 5, this.theme.selectList, layout);
|
|
1208
|
+
}
|
|
1167
1209
|
// Autocomplete methods
|
|
1168
1210
|
tryTriggerAutocomplete(explicitTab = false) {
|
|
1169
1211
|
if (!this.autocompleteProvider)
|
|
1170
1212
|
return;
|
|
1171
|
-
// Check if we should trigger file completion on Tab
|
|
1172
1213
|
if (explicitTab) {
|
|
1173
1214
|
const provider = this.autocompleteProvider;
|
|
1174
1215
|
const shouldTrigger = !provider.shouldTriggerFileCompletion ||
|
|
@@ -1180,8 +1221,12 @@ export class Editor {
|
|
|
1180
1221
|
const suggestions = this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
1181
1222
|
if (suggestions && suggestions.items.length > 0) {
|
|
1182
1223
|
this.autocompletePrefix = suggestions.prefix;
|
|
1183
|
-
this.autocompleteList =
|
|
1184
|
-
this.
|
|
1224
|
+
this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
1225
|
+
const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
|
|
1226
|
+
if (bestMatchIndex >= 0) {
|
|
1227
|
+
this.autocompleteList.setSelectedIndex(bestMatchIndex);
|
|
1228
|
+
}
|
|
1229
|
+
this.autocompleteState = "regular";
|
|
1185
1230
|
}
|
|
1186
1231
|
else {
|
|
1187
1232
|
this.cancelAutocomplete();
|
|
@@ -1192,12 +1237,11 @@ export class Editor {
|
|
|
1192
1237
|
return;
|
|
1193
1238
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1194
1239
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1195
|
-
|
|
1196
|
-
if (beforeCursor.trimStart().startsWith("/") && !beforeCursor.trimStart().includes(" ")) {
|
|
1240
|
+
if (this.isInSlashCommandContext(beforeCursor) && !beforeCursor.trimStart().includes(" ")) {
|
|
1197
1241
|
this.handleSlashCommandCompletion();
|
|
1198
1242
|
}
|
|
1199
1243
|
else {
|
|
1200
|
-
this.forceFileAutocomplete();
|
|
1244
|
+
this.forceFileAutocomplete(true);
|
|
1201
1245
|
}
|
|
1202
1246
|
}
|
|
1203
1247
|
handleSlashCommandCompletion() {
|
|
@@ -1208,10 +1252,9 @@ export class Editor {
|
|
|
1208
1252
|
17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19
|
|
1209
1253
|
536643416/job/55932288317 havea look at .gi
|
|
1210
1254
|
*/
|
|
1211
|
-
forceFileAutocomplete() {
|
|
1255
|
+
forceFileAutocomplete(explicitTab = false) {
|
|
1212
1256
|
if (!this.autocompleteProvider)
|
|
1213
1257
|
return;
|
|
1214
|
-
// Check if provider supports force file suggestions via runtime check
|
|
1215
1258
|
const provider = this.autocompleteProvider;
|
|
1216
1259
|
if (typeof provider.getForceFileSuggestions !== "function") {
|
|
1217
1260
|
this.tryTriggerAutocomplete(true);
|
|
@@ -1219,30 +1262,55 @@ export class Editor {
|
|
|
1219
1262
|
}
|
|
1220
1263
|
const suggestions = provider.getForceFileSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
1221
1264
|
if (suggestions && suggestions.items.length > 0) {
|
|
1265
|
+
if (explicitTab && suggestions.items.length === 1) {
|
|
1266
|
+
const item = suggestions.items[0];
|
|
1267
|
+
if (!item) {
|
|
1268
|
+
this.cancelAutocomplete();
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, item, suggestions.prefix);
|
|
1272
|
+
this.state.lines = result.lines;
|
|
1273
|
+
this.state.cursorLine = result.cursorLine;
|
|
1274
|
+
this.state.cursorCol = result.cursorCol;
|
|
1275
|
+
if (this.onChange)
|
|
1276
|
+
this.onChange(this.getText());
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1222
1279
|
this.autocompletePrefix = suggestions.prefix;
|
|
1223
|
-
this.autocompleteList =
|
|
1224
|
-
this.
|
|
1280
|
+
this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
1281
|
+
const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
|
|
1282
|
+
if (bestMatchIndex >= 0) {
|
|
1283
|
+
this.autocompleteList.setSelectedIndex(bestMatchIndex);
|
|
1284
|
+
}
|
|
1285
|
+
this.autocompleteState = "force";
|
|
1225
1286
|
}
|
|
1226
1287
|
else {
|
|
1227
1288
|
this.cancelAutocomplete();
|
|
1228
1289
|
}
|
|
1229
1290
|
}
|
|
1230
1291
|
cancelAutocomplete() {
|
|
1231
|
-
this.
|
|
1292
|
+
this.autocompleteState = null;
|
|
1232
1293
|
this.autocompleteList = undefined;
|
|
1233
1294
|
this.autocompletePrefix = "";
|
|
1234
1295
|
}
|
|
1235
1296
|
isShowingAutocomplete() {
|
|
1236
|
-
return this.
|
|
1297
|
+
return this.autocompleteState !== null;
|
|
1237
1298
|
}
|
|
1238
1299
|
updateAutocomplete() {
|
|
1239
|
-
if (!this.
|
|
1300
|
+
if (!this.autocompleteState || !this.autocompleteProvider)
|
|
1301
|
+
return;
|
|
1302
|
+
if (this.autocompleteState === "force") {
|
|
1303
|
+
this.forceFileAutocomplete();
|
|
1240
1304
|
return;
|
|
1305
|
+
}
|
|
1241
1306
|
const suggestions = this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
1242
1307
|
if (suggestions && suggestions.items.length > 0) {
|
|
1243
1308
|
this.autocompletePrefix = suggestions.prefix;
|
|
1244
|
-
|
|
1245
|
-
|
|
1309
|
+
this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
1310
|
+
const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
|
|
1311
|
+
if (bestMatchIndex >= 0) {
|
|
1312
|
+
this.autocompleteList.setSelectedIndex(bestMatchIndex);
|
|
1313
|
+
}
|
|
1246
1314
|
}
|
|
1247
1315
|
else {
|
|
1248
1316
|
this.cancelAutocomplete();
|