@sayknow-cli/tui 0.3.6 → 0.3.8
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/types/animation-scheduler.d.ts +13 -0
- package/dist/types/autocomplete.d.ts +83 -0
- package/dist/types/bracketed-paste.d.ts +26 -0
- package/dist/types/components/box.d.ts +20 -0
- package/dist/types/components/cancellable-loader.d.ts +21 -0
- package/dist/types/components/editor.d.ts +126 -0
- package/dist/types/components/image.d.ts +16 -0
- package/dist/types/components/input.d.ts +16 -0
- package/dist/types/components/loader.d.ts +23 -0
- package/dist/types/components/markdown.d.ts +77 -0
- package/dist/types/components/select-list.d.ts +46 -0
- package/dist/types/components/settings-list.d.ts +39 -0
- package/dist/types/components/spacer.d.ts +11 -0
- package/dist/types/components/tab-bar.d.ts +56 -0
- package/dist/types/components/text.d.ts +13 -0
- package/dist/types/components/truncated-text.d.ts +10 -0
- package/dist/types/editor-component.d.ts +36 -0
- package/dist/types/fuzzy.d.ts +15 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/keybindings.d.ts +201 -0
- package/dist/types/keys.d.ts +208 -0
- package/dist/types/kill-ring.d.ts +27 -0
- package/dist/types/metrics.d.ts +85 -0
- package/dist/types/stdin-buffer.d.ts +50 -0
- package/dist/types/symbols.d.ts +23 -0
- package/dist/types/terminal-capabilities.d.ts +75 -0
- package/dist/types/terminal.d.ts +88 -0
- package/dist/types/ttyid.d.ts +9 -0
- package/dist/types/tui.d.ts +206 -0
- package/dist/types/utils.d.ts +87 -0
- package/package.json +10 -9
- package/src/animation-scheduler.ts +99 -0
- package/src/autocomplete.ts +119 -96
- package/src/components/editor.ts +310 -128
- package/src/components/input.ts +2 -1
- package/src/components/loader.ts +36 -37
- package/src/components/markdown.ts +79 -2
- package/src/components/select-list.ts +8 -1
- package/src/index.ts +1 -0
- package/src/stdin-buffer.ts +89 -11
- package/src/terminal.ts +44 -8
- package/src/tui.ts +362 -64
- package/src/utils.ts +77 -11
package/src/components/editor.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { getProjectDir, logger } from "@sayknow-cli/utils";
|
|
2
|
-
import
|
|
1
|
+
import { getProjectDir, logger, onDefaultTabWidthChange } from "@sayknow-cli/utils";
|
|
2
|
+
import {
|
|
3
|
+
type AutocompleteProvider,
|
|
4
|
+
type CombinedAutocompleteProvider,
|
|
5
|
+
extractSlashCommandTokenPrefix,
|
|
6
|
+
} from "../autocomplete";
|
|
3
7
|
import { BracketedPasteHandler } from "../bracketed-paste";
|
|
4
8
|
import { getKeybindings, type KeybindingsManager } from "../keybindings";
|
|
5
9
|
import { extractPrintableText, matchesKey } from "../keys";
|
|
@@ -340,10 +344,27 @@ interface EditorState {
|
|
|
340
344
|
|
|
341
345
|
interface LayoutLine {
|
|
342
346
|
text: string;
|
|
347
|
+
visibleWidth: number;
|
|
348
|
+
logicalLine: number;
|
|
343
349
|
hasCursor: boolean;
|
|
344
350
|
cursorPos?: number;
|
|
345
351
|
}
|
|
346
352
|
|
|
353
|
+
interface LayoutCacheKey {
|
|
354
|
+
docVersion: number;
|
|
355
|
+
contentWidth: number;
|
|
356
|
+
optionsKey: string;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
interface LayoutCache {
|
|
360
|
+
key: LayoutCacheKey;
|
|
361
|
+
cursorLine: number;
|
|
362
|
+
cursorCol: number;
|
|
363
|
+
lines: LayoutLine[];
|
|
364
|
+
lineStarts: number[];
|
|
365
|
+
lineCounts: number[];
|
|
366
|
+
}
|
|
367
|
+
|
|
347
368
|
export interface EditorTheme {
|
|
348
369
|
borderColor: (str: string) => string;
|
|
349
370
|
selectList: SelectListTheme;
|
|
@@ -373,6 +394,18 @@ interface HistoryStorage {
|
|
|
373
394
|
|
|
374
395
|
type HistoryCursorAnchor = "start" | "end";
|
|
375
396
|
|
|
397
|
+
/** Test-only performance counters for advisory baseline tests. */
|
|
398
|
+
export const __editorPerfCounters = {
|
|
399
|
+
layoutTextInvocations: 0,
|
|
400
|
+
layoutLogicalLinesProcessed: 0,
|
|
401
|
+
visibleWidthMeasurements: 0,
|
|
402
|
+
reset(): void {
|
|
403
|
+
this.layoutTextInvocations = 0;
|
|
404
|
+
this.layoutLogicalLinesProcessed = 0;
|
|
405
|
+
this.visibleWidthMeasurements = 0;
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
|
|
376
409
|
export class Editor implements Component, Focusable {
|
|
377
410
|
#state: EditorState = {
|
|
378
411
|
lines: [""],
|
|
@@ -400,9 +433,12 @@ export class Editor implements Component, Focusable {
|
|
|
400
433
|
// Store last layout width for cursor navigation
|
|
401
434
|
#lastLayoutWidth: number = 80;
|
|
402
435
|
#paddingXOverride: number | undefined;
|
|
436
|
+
#rightGutterWidth = 0;
|
|
403
437
|
#maxHeight?: number;
|
|
404
438
|
#scrollOffset: number = 0;
|
|
405
439
|
#wrappedLineCache: CachedWrappedLine[] = [];
|
|
440
|
+
#docVersion = 0;
|
|
441
|
+
#layoutCache: LayoutCache | undefined;
|
|
406
442
|
|
|
407
443
|
// Emacs-style kill ring
|
|
408
444
|
#killRing = new KillRing();
|
|
@@ -450,6 +486,11 @@ export class Editor implements Component, Focusable {
|
|
|
450
486
|
onChange?: (text: string) => void;
|
|
451
487
|
onAutocompleteCancel?: () => void;
|
|
452
488
|
onTabDeclined?: (text: string) => void;
|
|
489
|
+
/**
|
|
490
|
+
* Called before Tab opens/applies autocomplete. Return true to consume Tab
|
|
491
|
+
* for app-level behavior (for example, queueing a draft while a turn runs).
|
|
492
|
+
*/
|
|
493
|
+
onTab?: (text: string) => boolean | undefined;
|
|
453
494
|
disableSubmit: boolean = false;
|
|
454
495
|
|
|
455
496
|
// Custom top border (for status line integration)
|
|
@@ -458,9 +499,25 @@ export class Editor implements Component, Focusable {
|
|
|
458
499
|
#borderStyle: EditorBorderStyle = "round";
|
|
459
500
|
#closedBorderBox = false;
|
|
460
501
|
|
|
502
|
+
#disposeTabWidthListener?: () => void;
|
|
503
|
+
|
|
461
504
|
constructor(theme: EditorTheme) {
|
|
462
505
|
this.#theme = theme;
|
|
463
506
|
this.borderColor = theme.borderColor;
|
|
507
|
+
// Raw tabs can reach editor state via insertText()/autocomplete results
|
|
508
|
+
// (setText expands tabs by contract). visibleWidth + wrapping depend on the
|
|
509
|
+
// default tab width, so a runtime tab-width change must drop both caches.
|
|
510
|
+
this.#disposeTabWidthListener = onDefaultTabWidthChange(() => {
|
|
511
|
+
this.invalidate();
|
|
512
|
+
if (this.#inputPrefix !== undefined) {
|
|
513
|
+
this.#inputPrefixWidth = visibleWidth(this.#inputPrefix);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
dispose(): void {
|
|
519
|
+
this.#disposeTabWidthListener?.();
|
|
520
|
+
this.#disposeTabWidthListener = undefined;
|
|
464
521
|
}
|
|
465
522
|
|
|
466
523
|
setAutocompleteProvider(provider: AutocompleteProvider): void {
|
|
@@ -482,6 +539,7 @@ export class Editor implements Component, Focusable {
|
|
|
482
539
|
*/
|
|
483
540
|
setTopBorder(content: EditorTopBorder | undefined): void {
|
|
484
541
|
this.#topBorderContent = content;
|
|
542
|
+
this.#invalidateLayoutCache();
|
|
485
543
|
}
|
|
486
544
|
|
|
487
545
|
/**
|
|
@@ -489,38 +547,46 @@ export class Editor implements Component, Focusable {
|
|
|
489
547
|
*/
|
|
490
548
|
setBorderVisible(borderVisible: boolean): void {
|
|
491
549
|
this.#borderVisible = borderVisible;
|
|
550
|
+
this.#invalidateLayoutCache();
|
|
492
551
|
}
|
|
493
552
|
|
|
494
553
|
setBorderStyle(borderStyle: EditorBorderStyle): void {
|
|
495
554
|
this.#borderStyle = borderStyle;
|
|
555
|
+
this.#invalidateLayoutCache();
|
|
496
556
|
}
|
|
497
557
|
|
|
498
558
|
setClosedBorderBox(closedBorderBox: boolean): void {
|
|
499
559
|
this.#closedBorderBox = closedBorderBox;
|
|
560
|
+
this.#invalidateLayoutCache();
|
|
500
561
|
}
|
|
501
562
|
|
|
502
563
|
setPromptGutter(promptGutter: string | undefined): void {
|
|
503
564
|
this.#promptGutter = promptGutter;
|
|
565
|
+
this.#invalidateLayoutCache();
|
|
504
566
|
}
|
|
505
567
|
|
|
506
568
|
setInputPrefix(inputPrefix: string | undefined): void {
|
|
507
569
|
this.#inputPrefix = inputPrefix;
|
|
508
570
|
this.#inputPrefixWidth = inputPrefix ? visibleWidth(inputPrefix) : 0;
|
|
571
|
+
this.#invalidateLayoutCache();
|
|
509
572
|
}
|
|
510
573
|
|
|
511
574
|
setPlaceholder(placeholder: string | undefined): void {
|
|
512
575
|
const trimmed = placeholder?.trim();
|
|
513
576
|
this.#placeholder = trimmed ? trimmed : undefined;
|
|
577
|
+
this.#invalidateLayoutCache();
|
|
514
578
|
}
|
|
515
579
|
|
|
516
580
|
/**
|
|
517
581
|
* Get the available width for top border content given a total terminal width.
|
|
518
|
-
* Accounts for
|
|
582
|
+
* Accounts for right gutter, border characters, and horizontal padding when visible.
|
|
519
583
|
*/
|
|
520
584
|
getTopBorderAvailableWidth(terminalWidth: number): number {
|
|
585
|
+
const rightGutterWidth = Math.min(this.#rightGutterWidth, Math.max(0, terminalWidth - 1));
|
|
586
|
+
const renderWidth = Math.max(1, terminalWidth - rightGutterWidth);
|
|
521
587
|
const paddingX = this.#getEditorPaddingX();
|
|
522
588
|
const borderWidth = this.#getHorizontalChromeWidth(paddingX);
|
|
523
|
-
return Math.max(0,
|
|
589
|
+
return Math.max(0, renderWidth - borderWidth * 2);
|
|
524
590
|
}
|
|
525
591
|
|
|
526
592
|
/**
|
|
@@ -528,6 +594,7 @@ export class Editor implements Component, Focusable {
|
|
|
528
594
|
*/
|
|
529
595
|
setUseTerminalCursor(useTerminalCursor: boolean): void {
|
|
530
596
|
this.#useTerminalCursor = useTerminalCursor;
|
|
597
|
+
this.#invalidateLayoutCache();
|
|
531
598
|
}
|
|
532
599
|
|
|
533
600
|
getUseTerminalCursor(): boolean {
|
|
@@ -542,6 +609,11 @@ export class Editor implements Component, Focusable {
|
|
|
542
609
|
|
|
543
610
|
setPaddingX(paddingX: number): void {
|
|
544
611
|
this.#paddingXOverride = Math.max(0, paddingX);
|
|
612
|
+
this.#invalidateLayoutCache();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
setRightGutterWidth(width: number): void {
|
|
616
|
+
this.#rightGutterWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
|
|
545
617
|
}
|
|
546
618
|
|
|
547
619
|
getAutocompleteMaxVisible(): number {
|
|
@@ -631,10 +703,21 @@ export class Editor implements Component, Focusable {
|
|
|
631
703
|
this.onChange(this.getText());
|
|
632
704
|
}
|
|
633
705
|
this.#wrappedLineCache.length = 0;
|
|
706
|
+
this.#bumpDocumentVersion();
|
|
634
707
|
}
|
|
635
708
|
|
|
636
709
|
invalidate(): void {
|
|
637
710
|
this.#wrappedLineCache.length = 0;
|
|
711
|
+
this.#layoutCache = undefined;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
#bumpDocumentVersion(): void {
|
|
715
|
+
this.#docVersion += 1;
|
|
716
|
+
this.#layoutCache = undefined;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
#invalidateLayoutCache(): void {
|
|
720
|
+
this.#layoutCache = undefined;
|
|
638
721
|
}
|
|
639
722
|
|
|
640
723
|
#getEditorPaddingX(): number {
|
|
@@ -780,12 +863,14 @@ export class Editor implements Component, Focusable {
|
|
|
780
863
|
}
|
|
781
864
|
|
|
782
865
|
render(width: number): string[] {
|
|
866
|
+
const rightGutterWidth = Math.min(this.#rightGutterWidth, Math.max(0, width - 1));
|
|
867
|
+
const renderWidth = Math.max(1, width - rightGutterWidth);
|
|
783
868
|
const paddingX = this.#getEditorPaddingX();
|
|
784
869
|
const borderVisible = this.#borderVisible;
|
|
785
|
-
const promptGutter = this.#getPromptGutter(
|
|
786
|
-
const contentAreaWidth = this.#getContentWidth(
|
|
870
|
+
const promptGutter = this.#getPromptGutter(renderWidth, paddingX);
|
|
871
|
+
const contentAreaWidth = this.#getContentWidth(renderWidth, paddingX);
|
|
787
872
|
const inputPrefixWidth = this.#inputPrefixWidth;
|
|
788
|
-
const layoutWidth = Math.max(1, this.#getLayoutWidth(
|
|
873
|
+
const layoutWidth = Math.max(1, this.#getLayoutWidth(renderWidth, paddingX) - inputPrefixWidth);
|
|
789
874
|
this.#lastLayoutWidth = layoutWidth;
|
|
790
875
|
|
|
791
876
|
// Box-drawing characters for the configured input box shape.
|
|
@@ -806,7 +891,7 @@ export class Editor implements Component, Focusable {
|
|
|
806
891
|
|
|
807
892
|
if (borderVisible) {
|
|
808
893
|
// Render top border: ╭─ [status content] ────────────────╮
|
|
809
|
-
const topFillWidth = Math.max(0,
|
|
894
|
+
const topFillWidth = Math.max(0, renderWidth - borderWidth * 2);
|
|
810
895
|
if (this.#topBorderContent) {
|
|
811
896
|
const { content, width: statusWidth } = this.#topBorderContent;
|
|
812
897
|
if (statusWidth <= topFillWidth) {
|
|
@@ -838,7 +923,7 @@ export class Editor implements Component, Focusable {
|
|
|
838
923
|
for (let visibleIndex = 0; visibleIndex < visibleLayoutLines.length; visibleIndex++) {
|
|
839
924
|
const layoutLine = visibleLayoutLines[visibleIndex]!;
|
|
840
925
|
let displayText = layoutLine.text;
|
|
841
|
-
let displayWidth =
|
|
926
|
+
let displayWidth = layoutLine.visibleWidth;
|
|
842
927
|
let cursorInPadding = false;
|
|
843
928
|
const absoluteVisibleIndex = this.#scrollOffset + visibleIndex;
|
|
844
929
|
const showPromptGutter = promptGutter !== undefined && visibleIndex === 0;
|
|
@@ -1015,7 +1100,7 @@ export class Editor implements Component, Focusable {
|
|
|
1015
1100
|
}
|
|
1016
1101
|
|
|
1017
1102
|
if (borderVisible && this.#closedBorderBox) {
|
|
1018
|
-
const bottomFillWidth = Math.max(0,
|
|
1103
|
+
const bottomFillWidth = Math.max(0, renderWidth - borderWidth * 2);
|
|
1019
1104
|
const bottomLeftClosed = this.borderColor(`${box.bottomLeft}${box.horizontal.repeat(paddingX)}`);
|
|
1020
1105
|
const bottomRightClosed = this.borderColor(`${box.horizontal.repeat(paddingX)}${box.bottomRight}`);
|
|
1021
1106
|
result.push(bottomLeftClosed + horizontal.repeat(bottomFillWidth) + bottomRightClosed);
|
|
@@ -1023,10 +1108,15 @@ export class Editor implements Component, Focusable {
|
|
|
1023
1108
|
|
|
1024
1109
|
// Add autocomplete list if active
|
|
1025
1110
|
if (this.#autocompleteState && this.#autocompleteList) {
|
|
1026
|
-
const autocompleteResult = this.#autocompleteList.render(
|
|
1111
|
+
const autocompleteResult = this.#autocompleteList.render(renderWidth);
|
|
1027
1112
|
result.push(...autocompleteResult);
|
|
1028
1113
|
}
|
|
1029
1114
|
|
|
1115
|
+
if (rightGutterWidth > 0) {
|
|
1116
|
+
const rightGutter = padding(rightGutterWidth);
|
|
1117
|
+
return result.map(line => line + rightGutter);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1030
1120
|
return result;
|
|
1031
1121
|
}
|
|
1032
1122
|
|
|
@@ -1080,6 +1170,10 @@ export class Editor implements Component, Focusable {
|
|
|
1080
1170
|
return;
|
|
1081
1171
|
}
|
|
1082
1172
|
|
|
1173
|
+
if (kb.matches(data, "tui.input.tab") && this.onTab?.(this.getText())) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1083
1177
|
// Handle autocomplete special keys first (but don't block other input)
|
|
1084
1178
|
if (this.#autocompleteState && this.#autocompleteList) {
|
|
1085
1179
|
// Escape - cancel autocomplete
|
|
@@ -1123,6 +1217,7 @@ export class Editor implements Component, Focusable {
|
|
|
1123
1217
|
);
|
|
1124
1218
|
|
|
1125
1219
|
this.#state.lines = result.lines;
|
|
1220
|
+
this.#bumpDocumentVersion();
|
|
1126
1221
|
this.#state.cursorLine = result.cursorLine;
|
|
1127
1222
|
this.#setCursorCol(result.cursorCol);
|
|
1128
1223
|
|
|
@@ -1147,7 +1242,7 @@ export class Editor implements Component, Focusable {
|
|
|
1147
1242
|
// Check for stale autocomplete state due to debounce
|
|
1148
1243
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
1149
1244
|
const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1150
|
-
if (currentTextBeforeCursor
|
|
1245
|
+
if (!currentTextBeforeCursor.endsWith(this.#autocompletePrefix)) {
|
|
1151
1246
|
// Autocomplete is stale - cancel and fall through to normal submission
|
|
1152
1247
|
this.#cancelAutocomplete();
|
|
1153
1248
|
} else {
|
|
@@ -1162,6 +1257,7 @@ export class Editor implements Component, Focusable {
|
|
|
1162
1257
|
);
|
|
1163
1258
|
|
|
1164
1259
|
this.#state.lines = result.lines;
|
|
1260
|
+
this.#bumpDocumentVersion();
|
|
1165
1261
|
this.#state.cursorLine = result.cursorLine;
|
|
1166
1262
|
this.#setCursorCol(result.cursorCol);
|
|
1167
1263
|
result.onApplied?.();
|
|
@@ -1183,6 +1279,7 @@ export class Editor implements Component, Focusable {
|
|
|
1183
1279
|
);
|
|
1184
1280
|
|
|
1185
1281
|
this.#state.lines = result.lines;
|
|
1282
|
+
this.#bumpDocumentVersion();
|
|
1186
1283
|
this.#state.cursorLine = result.cursorLine;
|
|
1187
1284
|
this.#setCursorCol(result.cursorCol);
|
|
1188
1285
|
|
|
@@ -1285,8 +1382,8 @@ export class Editor implements Component, Focusable {
|
|
|
1285
1382
|
const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
|
|
1286
1383
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1287
1384
|
if (
|
|
1288
|
-
|
|
1289
|
-
|
|
1385
|
+
(this.#isInSubmittedSlashCommandContext() ||
|
|
1386
|
+
this.#getSlashTokenBeforeCursor()?.startsWith("/skill") === true) &&
|
|
1290
1387
|
this.#autocompleteProvider?.trySyncSlashCompletion
|
|
1291
1388
|
) {
|
|
1292
1389
|
const syncResult = this.#autocompleteProvider.trySyncSlashCompletion(textBeforeCursor);
|
|
@@ -1303,6 +1400,7 @@ export class Editor implements Component, Focusable {
|
|
|
1303
1400
|
syncResult.prefix,
|
|
1304
1401
|
);
|
|
1305
1402
|
this.#state.lines = result.lines;
|
|
1403
|
+
this.#bumpDocumentVersion();
|
|
1306
1404
|
this.#state.cursorLine = result.cursorLine;
|
|
1307
1405
|
this.#setCursorCol(result.cursorCol);
|
|
1308
1406
|
result.onApplied?.();
|
|
@@ -1436,95 +1534,175 @@ export class Editor implements Component, Focusable {
|
|
|
1436
1534
|
return this.#wrappedLineCache.length;
|
|
1437
1535
|
}
|
|
1438
1536
|
|
|
1439
|
-
#
|
|
1537
|
+
#makeLayoutCacheKey(contentWidth: number): LayoutCacheKey {
|
|
1538
|
+
return {
|
|
1539
|
+
docVersion: this.#docVersion,
|
|
1540
|
+
contentWidth,
|
|
1541
|
+
optionsKey: JSON.stringify({
|
|
1542
|
+
borderVisible: this.#borderVisible,
|
|
1543
|
+
borderStyle: this.#borderStyle,
|
|
1544
|
+
closedBorderBox: this.#closedBorderBox,
|
|
1545
|
+
inputPrefix: this.#inputPrefix,
|
|
1546
|
+
inputPrefixWidth: this.#inputPrefixWidth,
|
|
1547
|
+
placeholder: this.#placeholder,
|
|
1548
|
+
promptGutter: this.#promptGutter,
|
|
1549
|
+
useTerminalCursor: this.#useTerminalCursor,
|
|
1550
|
+
cursorOverride: this.cursorOverride,
|
|
1551
|
+
cursorOverrideWidth: this.cursorOverrideWidth,
|
|
1552
|
+
autocompleteState: this.#autocompleteState,
|
|
1553
|
+
autocompletePrefix: this.#autocompletePrefix,
|
|
1554
|
+
autocompleteHint: this.#autocompleteList?.getSelectedItem()?.hint,
|
|
1555
|
+
}),
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
#layoutLine(text: string, logicalLine: number, hasCursor: boolean, cursorPos?: number): LayoutLine {
|
|
1560
|
+
__editorPerfCounters.visibleWidthMeasurements += 1;
|
|
1561
|
+
return {
|
|
1562
|
+
text,
|
|
1563
|
+
visibleWidth: visibleWidth(text),
|
|
1564
|
+
logicalLine,
|
|
1565
|
+
hasCursor,
|
|
1566
|
+
cursorPos,
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
#layoutLogicalLine(lineIndex: number, contentWidth: number): LayoutLine[] {
|
|
1571
|
+
__editorPerfCounters.layoutLogicalLinesProcessed += 1;
|
|
1572
|
+
const line = this.#state.lines[lineIndex] || "";
|
|
1573
|
+
const isCurrentLine = lineIndex === this.#state.cursorLine;
|
|
1574
|
+
const wrappedLine = this.#getWrappedLine(lineIndex, contentWidth);
|
|
1440
1575
|
const layoutLines: LayoutLine[] = [];
|
|
1441
1576
|
|
|
1442
|
-
if (
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
this.#wrappedLineCache.length = this.#state.lines.length;
|
|
1447
|
-
layoutLines.push({
|
|
1448
|
-
text: "",
|
|
1449
|
-
hasCursor: true,
|
|
1450
|
-
cursorPos: 0,
|
|
1451
|
-
});
|
|
1577
|
+
if (wrappedLine.width <= contentWidth) {
|
|
1578
|
+
layoutLines.push(
|
|
1579
|
+
this.#layoutLine(line, lineIndex, isCurrentLine, isCurrentLine ? this.#state.cursorCol : undefined),
|
|
1580
|
+
);
|
|
1452
1581
|
return layoutLines;
|
|
1453
1582
|
}
|
|
1454
1583
|
|
|
1455
|
-
|
|
1456
|
-
for (let
|
|
1457
|
-
const
|
|
1458
|
-
|
|
1459
|
-
const wrappedLine = this.#getWrappedLine(i, contentWidth);
|
|
1460
|
-
|
|
1461
|
-
if (wrappedLine.width <= contentWidth) {
|
|
1462
|
-
// Line fits in one layout line
|
|
1463
|
-
if (isCurrentLine) {
|
|
1464
|
-
layoutLines.push({
|
|
1465
|
-
text: line,
|
|
1466
|
-
hasCursor: true,
|
|
1467
|
-
cursorPos: this.#state.cursorCol,
|
|
1468
|
-
});
|
|
1469
|
-
} else {
|
|
1470
|
-
layoutLines.push({
|
|
1471
|
-
text: line,
|
|
1472
|
-
hasCursor: false,
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
} else {
|
|
1476
|
-
// Line needs wrapping - use word-aware wrapping
|
|
1477
|
-
const chunks = wrappedLine.chunks;
|
|
1478
|
-
|
|
1479
|
-
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
1480
|
-
const chunk = chunks[chunkIndex];
|
|
1481
|
-
if (!chunk) continue;
|
|
1482
|
-
|
|
1483
|
-
const cursorPos = this.#state.cursorCol;
|
|
1484
|
-
const isLastChunk = chunkIndex === chunks.length - 1;
|
|
1584
|
+
const chunks = wrappedLine.chunks;
|
|
1585
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
1586
|
+
const chunk = chunks[chunkIndex];
|
|
1587
|
+
if (!chunk) continue;
|
|
1485
1588
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
let adjustedCursorPos = 0;
|
|
1589
|
+
const cursorPos = this.#state.cursorCol;
|
|
1590
|
+
const isLastChunk = chunkIndex === chunks.length - 1;
|
|
1591
|
+
let hasCursorInChunk = false;
|
|
1592
|
+
let adjustedCursorPos = 0;
|
|
1491
1593
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
adjustedCursorPos = cursorPos - chunk.startIndex;
|
|
1503
|
-
// Clamp to text length (in case cursor was in trimmed whitespace)
|
|
1504
|
-
if (adjustedCursorPos > chunk.text.length) {
|
|
1505
|
-
adjustedCursorPos = chunk.text.length;
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1594
|
+
if (isCurrentLine) {
|
|
1595
|
+
if (isLastChunk) {
|
|
1596
|
+
hasCursorInChunk = cursorPos >= chunk.startIndex;
|
|
1597
|
+
adjustedCursorPos = cursorPos - chunk.startIndex;
|
|
1598
|
+
} else {
|
|
1599
|
+
hasCursorInChunk = cursorPos >= chunk.startIndex && cursorPos < chunk.endIndex;
|
|
1600
|
+
if (hasCursorInChunk) {
|
|
1601
|
+
adjustedCursorPos = cursorPos - chunk.startIndex;
|
|
1602
|
+
if (adjustedCursorPos > chunk.text.length) {
|
|
1603
|
+
adjustedCursorPos = chunk.text.length;
|
|
1508
1604
|
}
|
|
1509
1605
|
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1510
1608
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1609
|
+
if (hasCursorInChunk) {
|
|
1610
|
+
let displayChunkText = chunk.text;
|
|
1611
|
+
let displayCursorPos = adjustedCursorPos;
|
|
1612
|
+
if (displayCursorPos > displayChunkText.length) {
|
|
1613
|
+
let hiddenWhitespaceWidth = displayCursorPos - displayChunkText.length;
|
|
1614
|
+
const displayChunkWidth = visibleWidth(displayChunkText);
|
|
1615
|
+
__editorPerfCounters.visibleWidthMeasurements += 1;
|
|
1616
|
+
if (displayChunkWidth + hiddenWhitespaceWidth <= contentWidth) {
|
|
1617
|
+
displayChunkText += padding(hiddenWhitespaceWidth);
|
|
1517
1618
|
} else {
|
|
1518
|
-
layoutLines.push(
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1619
|
+
layoutLines.push(this.#layoutLine(displayChunkText, lineIndex, false));
|
|
1620
|
+
hiddenWhitespaceWidth -= Math.max(0, contentWidth - displayChunkWidth);
|
|
1621
|
+
while (hiddenWhitespaceWidth > contentWidth) {
|
|
1622
|
+
layoutLines.push(this.#layoutLine(padding(contentWidth), lineIndex, false));
|
|
1623
|
+
hiddenWhitespaceWidth -= contentWidth;
|
|
1624
|
+
}
|
|
1625
|
+
displayChunkText = padding(hiddenWhitespaceWidth);
|
|
1626
|
+
displayCursorPos = hiddenWhitespaceWidth;
|
|
1522
1627
|
}
|
|
1523
1628
|
}
|
|
1629
|
+
layoutLines.push(this.#layoutLine(displayChunkText, lineIndex, true, displayCursorPos));
|
|
1630
|
+
} else {
|
|
1631
|
+
layoutLines.push(this.#layoutLine(chunk.text, lineIndex, false));
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
return layoutLines;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
#sameLayoutCacheKey(a: LayoutCacheKey, b: LayoutCacheKey): boolean {
|
|
1639
|
+
return a.docVersion === b.docVersion && a.contentWidth === b.contentWidth && a.optionsKey === b.optionsKey;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
#replaceCachedLogicalLine(cache: LayoutCache, lineIndex: number, contentWidth: number): void {
|
|
1643
|
+
const start = cache.lineStarts[lineIndex] ?? cache.lines.length;
|
|
1644
|
+
const oldCount = cache.lineCounts[lineIndex] ?? 0;
|
|
1645
|
+
const replacement = this.#layoutLogicalLine(lineIndex, contentWidth);
|
|
1646
|
+
cache.lines.splice(start, oldCount, ...replacement);
|
|
1647
|
+
cache.lineCounts[lineIndex] = replacement.length;
|
|
1648
|
+
const delta = replacement.length - oldCount;
|
|
1649
|
+
if (delta !== 0) {
|
|
1650
|
+
for (let i = lineIndex + 1; i < cache.lineStarts.length; i++) {
|
|
1651
|
+
cache.lineStarts[i] = (cache.lineStarts[i] ?? 0) + delta;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
#patchCursorInCachedLayout(cache: LayoutCache, contentWidth: number): LayoutLine[] {
|
|
1657
|
+
const previousLine = cache.cursorLine;
|
|
1658
|
+
const currentLine = this.#state.cursorLine;
|
|
1659
|
+
this.#replaceCachedLogicalLine(cache, previousLine, contentWidth);
|
|
1660
|
+
if (currentLine !== previousLine) {
|
|
1661
|
+
this.#replaceCachedLogicalLine(cache, currentLine, contentWidth);
|
|
1662
|
+
}
|
|
1663
|
+
cache.cursorLine = currentLine;
|
|
1664
|
+
cache.cursorCol = this.#state.cursorCol;
|
|
1665
|
+
return cache.lines;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
#layoutText(contentWidth: number): LayoutLine[] {
|
|
1669
|
+
__editorPerfCounters.layoutTextInvocations += 1;
|
|
1670
|
+
const key = this.#makeLayoutCacheKey(contentWidth);
|
|
1671
|
+
const cached = this.#layoutCache;
|
|
1672
|
+
if (cached && this.#sameLayoutCacheKey(cached.key, key)) {
|
|
1673
|
+
if (cached.cursorLine === this.#state.cursorLine && cached.cursorCol === this.#state.cursorCol) {
|
|
1674
|
+
return cached.lines;
|
|
1675
|
+
}
|
|
1676
|
+
return this.#patchCursorInCachedLayout(cached, contentWidth);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
const layoutLines: LayoutLine[] = [];
|
|
1680
|
+
const lineStarts: number[] = [];
|
|
1681
|
+
const lineCounts: number[] = [];
|
|
1682
|
+
|
|
1683
|
+
if (this.#state.lines.length === 0 || (this.#state.lines.length === 1 && this.#state.lines[0] === "")) {
|
|
1684
|
+
this.#wrappedLineCache.length = this.#state.lines.length;
|
|
1685
|
+
lineStarts[0] = 0;
|
|
1686
|
+
lineCounts[0] = 1;
|
|
1687
|
+
layoutLines.push(this.#layoutLine("", 0, true, 0));
|
|
1688
|
+
} else {
|
|
1689
|
+
for (let i = 0; i < this.#state.lines.length; i++) {
|
|
1690
|
+
lineStarts[i] = layoutLines.length;
|
|
1691
|
+
const logicalLayout = this.#layoutLogicalLine(i, contentWidth);
|
|
1692
|
+
lineCounts[i] = logicalLayout.length;
|
|
1693
|
+
layoutLines.push(...logicalLayout);
|
|
1524
1694
|
}
|
|
1525
1695
|
}
|
|
1526
1696
|
|
|
1527
1697
|
this.#wrappedLineCache.length = this.#state.lines.length;
|
|
1698
|
+
this.#layoutCache = {
|
|
1699
|
+
key,
|
|
1700
|
+
cursorLine: this.#state.cursorLine,
|
|
1701
|
+
cursorCol: this.#state.cursorCol,
|
|
1702
|
+
lines: layoutLines,
|
|
1703
|
+
lineStarts,
|
|
1704
|
+
lineCounts,
|
|
1705
|
+
};
|
|
1528
1706
|
return layoutLines;
|
|
1529
1707
|
}
|
|
1530
1708
|
|
|
@@ -1596,6 +1774,7 @@ export class Editor implements Component, Focusable {
|
|
|
1596
1774
|
this.#resetKillSequence();
|
|
1597
1775
|
this.#preferredVisualCol = null;
|
|
1598
1776
|
this.#state.lines[this.#state.cursorLine] = beforeTransient + afterTransient;
|
|
1777
|
+
this.#bumpDocumentVersion();
|
|
1599
1778
|
this.#setCursorCol(transientStartCol);
|
|
1600
1779
|
|
|
1601
1780
|
while (true) {
|
|
@@ -1644,18 +1823,7 @@ export class Editor implements Component, Focusable {
|
|
|
1644
1823
|
/** Insert text at the current cursor position */
|
|
1645
1824
|
insertText(text: string): void {
|
|
1646
1825
|
this.#exitHistoryForEditing();
|
|
1647
|
-
this.#
|
|
1648
|
-
this.#recordUndoState();
|
|
1649
|
-
|
|
1650
|
-
const line = this.#state.lines[this.#state.cursorLine] || "";
|
|
1651
|
-
const inserted = insertTextNfcAt(line, this.#state.cursorCol, text);
|
|
1652
|
-
|
|
1653
|
-
this.#state.lines[this.#state.cursorLine] = inserted.line;
|
|
1654
|
-
this.#setCursorCol(inserted.cursorCol);
|
|
1655
|
-
|
|
1656
|
-
if (this.onChange) {
|
|
1657
|
-
this.onChange(this.getText());
|
|
1658
|
-
}
|
|
1826
|
+
this.#insertTextAtCursor(text);
|
|
1659
1827
|
}
|
|
1660
1828
|
|
|
1661
1829
|
// All the editor methods from before...
|
|
@@ -1699,8 +1867,8 @@ export class Editor implements Component, Focusable {
|
|
|
1699
1867
|
|
|
1700
1868
|
// Check if we should trigger or update autocomplete
|
|
1701
1869
|
if (!this.#autocompleteState) {
|
|
1702
|
-
// Auto-trigger for
|
|
1703
|
-
if (char === "/" && this.#isAtStartOfSubmittedMessage()) {
|
|
1870
|
+
// Auto-trigger for slash command tokens.
|
|
1871
|
+
if (char === "/" && (this.#isAtStartOfSubmittedMessage() || this.#isInSlashTokenContext())) {
|
|
1704
1872
|
this.#tryTriggerAutocomplete();
|
|
1705
1873
|
}
|
|
1706
1874
|
// Auto-trigger for "@" file reference (fuzzy search)
|
|
@@ -1721,8 +1889,8 @@ export class Editor implements Component, Focusable {
|
|
|
1721
1889
|
else if (/[a-zA-Z0-9.\-_/]/.test(char)) {
|
|
1722
1890
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
1723
1891
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1724
|
-
// Check if we're in a slash command
|
|
1725
|
-
if (this.#isInSubmittedSlashCommandContext()) {
|
|
1892
|
+
// Check if we're in a slash command or inline slash token
|
|
1893
|
+
if (this.#isInSubmittedSlashCommandContext() || this.#isInSlashTokenContext()) {
|
|
1726
1894
|
this.#tryTriggerAutocomplete();
|
|
1727
1895
|
}
|
|
1728
1896
|
// Check if we're in an @ file reference context
|
|
@@ -1747,6 +1915,11 @@ export class Editor implements Component, Focusable {
|
|
|
1747
1915
|
this.#historyIndex = -1; // Exit history browsing mode
|
|
1748
1916
|
this.#resetKillSequence();
|
|
1749
1917
|
this.#recordUndoState();
|
|
1918
|
+
const hadAutocomplete = this.#autocompleteState !== null;
|
|
1919
|
+
this.#cancelAutocomplete();
|
|
1920
|
+
if (hadAutocomplete) {
|
|
1921
|
+
this.onAutocompleteUpdate?.();
|
|
1922
|
+
}
|
|
1750
1923
|
|
|
1751
1924
|
this.#withUndoSuspended(() => {
|
|
1752
1925
|
// Some terminals (e.g. tmux popups with extended-keys-format=csi-u) re-encode
|
|
@@ -1774,21 +1947,12 @@ export class Editor implements Component, Focusable {
|
|
|
1774
1947
|
const tabExpandedText = cleanText.replace(/\t/g, " ");
|
|
1775
1948
|
|
|
1776
1949
|
// Filter out non-printable characters except newlines
|
|
1777
|
-
|
|
1950
|
+
const filteredText = tabExpandedText
|
|
1778
1951
|
.split("")
|
|
1779
1952
|
.filter(char => char === "\n" || char.charCodeAt(0) >= 32)
|
|
1780
1953
|
.join("");
|
|
1781
1954
|
|
|
1782
|
-
|
|
1783
|
-
// the cursor is a word character, prepend a space for better readability
|
|
1784
|
-
if (/^[/~.]/.test(filteredText)) {
|
|
1785
|
-
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
1786
|
-
const charBeforeCursor = this.#state.cursorCol > 0 ? currentLine[this.#state.cursorCol - 1] : "";
|
|
1787
|
-
if (charBeforeCursor && /\w/.test(charBeforeCursor)) {
|
|
1788
|
-
filteredText = ` ${filteredText}`;
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1955
|
+
if (filteredText.length === 0) return;
|
|
1792
1956
|
// Split into lines
|
|
1793
1957
|
const pastedLines = filteredText.split("\n");
|
|
1794
1958
|
|
|
@@ -1810,15 +1974,10 @@ export class Editor implements Component, Focusable {
|
|
|
1810
1974
|
return;
|
|
1811
1975
|
}
|
|
1812
1976
|
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1818
|
-
return;
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
// Multi-line paste - use insertTextAtCursor for proper handling
|
|
1977
|
+
// Paste is literal input, not typed input. Insert atomically so leading
|
|
1978
|
+
// trigger characters such as "/", "#", "@", ":", or path-like text do
|
|
1979
|
+
// not open or update autocomplete lists while preserving normal typed
|
|
1980
|
+
// trigger behavior.
|
|
1822
1981
|
this.#insertTextAtCursor(filteredText);
|
|
1823
1982
|
});
|
|
1824
1983
|
}
|
|
@@ -1863,6 +2022,7 @@ export class Editor implements Component, Focusable {
|
|
|
1863
2022
|
const result = this.#expandPasteMarkers(this.#state.lines.join("\n")).trim();
|
|
1864
2023
|
|
|
1865
2024
|
this.#state = { lines: [""], cursorLine: 0, cursorCol: 0 };
|
|
2025
|
+
this.#bumpDocumentVersion();
|
|
1866
2026
|
this.#pastes.clear();
|
|
1867
2027
|
this.#pasteCounter = 0;
|
|
1868
2028
|
this.#historyIndex = -1;
|
|
@@ -1917,8 +2077,8 @@ export class Editor implements Component, Focusable {
|
|
|
1917
2077
|
// If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
|
|
1918
2078
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
1919
2079
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
1920
|
-
// Slash command context
|
|
1921
|
-
if (this.#isInSubmittedSlashCommandContext()) {
|
|
2080
|
+
// Slash command or inline slash token context
|
|
2081
|
+
if (this.#isInSubmittedSlashCommandContext() || this.#isInSlashTokenContext()) {
|
|
1922
2082
|
this.#tryTriggerAutocomplete();
|
|
1923
2083
|
}
|
|
1924
2084
|
// @ file reference context
|
|
@@ -2054,6 +2214,7 @@ export class Editor implements Component, Focusable {
|
|
|
2054
2214
|
#recordUndoState(): void {
|
|
2055
2215
|
if (this.#suspendUndo) return;
|
|
2056
2216
|
this.#undoStack.push(structuredClone(this.#state));
|
|
2217
|
+
this.#bumpDocumentVersion();
|
|
2057
2218
|
}
|
|
2058
2219
|
|
|
2059
2220
|
#applyUndo(): void {
|
|
@@ -2064,6 +2225,7 @@ export class Editor implements Component, Focusable {
|
|
|
2064
2225
|
this.#resetKillSequence();
|
|
2065
2226
|
this.#preferredVisualCol = null;
|
|
2066
2227
|
Object.assign(this.#state, snapshot);
|
|
2228
|
+
this.#bumpDocumentVersion();
|
|
2067
2229
|
|
|
2068
2230
|
if (this.onChange) {
|
|
2069
2231
|
this.onChange(this.getText());
|
|
@@ -2074,7 +2236,7 @@ export class Editor implements Component, Focusable {
|
|
|
2074
2236
|
} else {
|
|
2075
2237
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2076
2238
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2077
|
-
if (this.#isInSubmittedSlashCommandContext()) {
|
|
2239
|
+
if (this.#isInSubmittedSlashCommandContext() || this.#isInSlashTokenContext()) {
|
|
2078
2240
|
this.#tryTriggerAutocomplete();
|
|
2079
2241
|
} else if (textBeforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
|
|
2080
2242
|
this.#tryTriggerAutocomplete();
|
|
@@ -2388,8 +2550,8 @@ export class Editor implements Component, Focusable {
|
|
|
2388
2550
|
} else {
|
|
2389
2551
|
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2390
2552
|
const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2391
|
-
// Slash command context
|
|
2392
|
-
if (this.#isInSubmittedSlashCommandContext()) {
|
|
2553
|
+
// Slash command or inline slash token context
|
|
2554
|
+
if (this.#isInSubmittedSlashCommandContext() || this.#isInSlashTokenContext()) {
|
|
2393
2555
|
this.#tryTriggerAutocomplete();
|
|
2394
2556
|
}
|
|
2395
2557
|
// @ file reference context
|
|
@@ -2569,6 +2731,12 @@ export class Editor implements Component, Focusable {
|
|
|
2569
2731
|
: this.#state.cursorCol - 1
|
|
2570
2732
|
: undefined;
|
|
2571
2733
|
|
|
2734
|
+
// Backward from column 0: lastIndexOf clamps a negative position to 0,
|
|
2735
|
+
// which would match the character under the cursor instead of skipping it
|
|
2736
|
+
if (!isForward && searchFrom !== undefined && searchFrom < 0) {
|
|
2737
|
+
continue;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2572
2740
|
const idx = isForward ? line.indexOf(char, searchFrom) : line.lastIndexOf(char, searchFrom);
|
|
2573
2741
|
|
|
2574
2742
|
if (idx !== -1) {
|
|
@@ -2618,6 +2786,16 @@ export class Editor implements Component, Focusable {
|
|
|
2618
2786
|
return this.#hasOnlyWhitespaceBeforeCursorLine() && beforeCursor.trimStart().startsWith("/");
|
|
2619
2787
|
}
|
|
2620
2788
|
|
|
2789
|
+
#getSlashTokenBeforeCursor(): string | null {
|
|
2790
|
+
const currentLine = this.#state.lines[this.#state.cursorLine] || "";
|
|
2791
|
+
const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2792
|
+
return extractSlashCommandTokenPrefix(beforeCursor);
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
#isInSlashTokenContext(): boolean {
|
|
2796
|
+
return this.#getSlashTokenBeforeCursor() !== null;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2621
2799
|
#isSlashCommandNameAutocompleteSelection(): boolean {
|
|
2622
2800
|
if (this.#autocompleteState !== "regular") {
|
|
2623
2801
|
return false;
|
|
@@ -2699,7 +2877,10 @@ export class Editor implements Component, Focusable {
|
|
|
2699
2877
|
const beforeCursor = currentLine.slice(0, this.#state.cursorCol);
|
|
2700
2878
|
|
|
2701
2879
|
// Check if we're in a slash command context
|
|
2702
|
-
if (
|
|
2880
|
+
if (
|
|
2881
|
+
(this.#isInSubmittedSlashCommandContext() && !beforeCursor.trimStart().includes(" ")) ||
|
|
2882
|
+
this.#isInSlashTokenContext()
|
|
2883
|
+
) {
|
|
2703
2884
|
this.#handleSlashCommandCompletion();
|
|
2704
2885
|
} else {
|
|
2705
2886
|
this.#forceFileAutocomplete(true);
|
|
@@ -2748,6 +2929,7 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/
|
|
|
2748
2929
|
);
|
|
2749
2930
|
|
|
2750
2931
|
this.#state.lines = result.lines;
|
|
2932
|
+
this.#bumpDocumentVersion();
|
|
2751
2933
|
this.#state.cursorLine = result.cursorLine;
|
|
2752
2934
|
this.#setCursorCol(result.cursorCol);
|
|
2753
2935
|
|