@dungle-scrubs/tallow 0.9.4 → 0.9.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/dist/cli.js +7 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +24 -12
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +229 -146
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/interactive-reset.d.ts +49 -0
- package/dist/interactive-reset.d.ts.map +1 -0
- package/dist/interactive-reset.js +40 -0
- package/dist/interactive-reset.js.map +1 -0
- package/dist/pi-tui-editor-patch.d.ts +10 -0
- package/dist/pi-tui-editor-patch.d.ts.map +1 -0
- package/dist/pi-tui-editor-patch.js +159 -0
- package/dist/pi-tui-editor-patch.js.map +1 -0
- package/dist/pi-tui-patch.d.ts +2 -0
- package/dist/pi-tui-patch.d.ts.map +1 -0
- package/dist/pi-tui-patch.js +563 -0
- package/dist/pi-tui-patch.js.map +1 -0
- package/dist/pi-tui-settings-list-patch.d.ts +11 -0
- package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
- package/dist/pi-tui-settings-list-patch.js +38 -0
- package/dist/pi-tui-settings-list-patch.js.map +1 -0
- package/dist/reset-diagnostics.d.ts +69 -0
- package/dist/reset-diagnostics.d.ts.map +1 -0
- package/dist/reset-diagnostics.js +41 -0
- package/dist/reset-diagnostics.js.map +1 -0
- package/dist/sdk.d.ts +5 -21
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +180 -149
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts +1 -0
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +7 -17
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +4 -5
- package/extensions/_icons/index.ts +2 -4
- package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
- package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
- package/extensions/_shared/image-metadata.ts +99 -0
- package/extensions/_shared/inline-preview.ts +1 -1
- package/extensions/_shared/terminal-links.ts +22 -0
- package/extensions/ask-user-question-tool/index.ts +0 -3
- package/extensions/clear/__tests__/clear.test.ts +269 -2
- package/extensions/command-expansion/index.ts +1 -1
- package/extensions/context-files/index.ts +5 -1
- package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
- package/extensions/context-fork/extension.json +1 -1
- package/extensions/context-fork/index.ts +32 -0
- package/extensions/edit-tool-enhanced/index.ts +2 -1
- package/extensions/hooks/index.ts +33 -11
- package/extensions/loop/index.ts +14 -1
- package/extensions/lsp/index.ts +64 -13
- package/extensions/lsp/package.json +2 -2
- package/extensions/random-spinner/index.ts +7 -642
- package/extensions/read-tool-enhanced/index.ts +6 -8
- package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +2 -3
- package/extensions/render-stabilizer/index.ts +6 -6
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
- package/extensions/slash-command-bridge/index.ts +14 -2
- package/extensions/subagent-tool/model-resolver.ts +274 -7
- package/extensions/tasks/commands/register-tasks-extension.ts +9 -9
- package/extensions/teams-tool/tools/register-extension.ts +1 -3
- package/extensions/web-search-tool/index.ts +2 -1
- package/extensions/write-tool-enhanced/index.ts +2 -1
- package/node_modules/@mariozechner/pi-tui/README.md +56 -34
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
- package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
- package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
- package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
- package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
- package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
- package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
- package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
- package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
- package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +1 -25
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +13 -50
- package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
- package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
- package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
- package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +6 -6
- package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
- package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
- package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
- package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
- package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
- package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
- package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
- package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
- package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
- package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
- package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
- package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
- package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +12 -51
- package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
- package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
- package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
- package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
- package/package.json +13 -13
- package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
- package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
- package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
- package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
- package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
- package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
- package/packages/tallow-tui/node_modules/marked/README.md +5 -4
- package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
- package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
- package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
- package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
- package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
- package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
- package/packages/tallow-tui/node_modules/marked/package.json +26 -34
- package/skills/tallow-expert/SKILL.md +1 -3
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
- package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
- package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
- package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
- package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
- package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -81
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
- package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
- package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
- package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
- package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
- package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
- package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
|
@@ -1,11 +1,71 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { matchesKey } from "../keys.js";
|
|
1
|
+
import { getKeybindings } from "../keybindings.js";
|
|
2
|
+
import { decodeKittyPrintable, matchesKey } from "../keys.js";
|
|
3
3
|
import { KillRing } from "../kill-ring.js";
|
|
4
4
|
import { CURSOR_MARKER } from "../tui.js";
|
|
5
5
|
import { UndoStack } from "../undo-stack.js";
|
|
6
|
-
import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";
|
|
6
|
+
import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth, } from "../utils.js";
|
|
7
7
|
import { SelectList } from "./select-list.js";
|
|
8
|
-
const
|
|
8
|
+
const baseSegmenter = getSegmenter();
|
|
9
|
+
/** Regex matching paste markers like `[paste #1 +123 lines]` or `[paste #2 1234 chars]`. */
|
|
10
|
+
const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]/g;
|
|
11
|
+
/** Non-global version for single-segment testing. */
|
|
12
|
+
const PASTE_MARKER_SINGLE = /^\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]$/;
|
|
13
|
+
/** Check if a segment is a paste marker (i.e. was merged by segmentWithMarkers). */
|
|
14
|
+
function isPasteMarker(segment) {
|
|
15
|
+
return segment.length >= 10 && PASTE_MARKER_SINGLE.test(segment);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A segmenter that wraps Intl.Segmenter and merges graphemes that fall
|
|
19
|
+
* within paste markers into single atomic segments. This makes cursor
|
|
20
|
+
* movement, deletion, word-wrap, etc. treat paste markers as single units.
|
|
21
|
+
*
|
|
22
|
+
* Only markers whose numeric ID exists in `validIds` are merged.
|
|
23
|
+
*/
|
|
24
|
+
function segmentWithMarkers(text, validIds) {
|
|
25
|
+
// Fast path: no paste markers in the text or no valid IDs.
|
|
26
|
+
if (validIds.size === 0 || !text.includes("[paste #")) {
|
|
27
|
+
return baseSegmenter.segment(text);
|
|
28
|
+
}
|
|
29
|
+
// Find all marker spans with valid IDs.
|
|
30
|
+
const markers = [];
|
|
31
|
+
for (const m of text.matchAll(PASTE_MARKER_REGEX)) {
|
|
32
|
+
const id = Number.parseInt(m[1], 10);
|
|
33
|
+
if (!validIds.has(id))
|
|
34
|
+
continue;
|
|
35
|
+
markers.push({ start: m.index, end: m.index + m[0].length });
|
|
36
|
+
}
|
|
37
|
+
if (markers.length === 0) {
|
|
38
|
+
return baseSegmenter.segment(text);
|
|
39
|
+
}
|
|
40
|
+
// Build merged segment list.
|
|
41
|
+
const baseSegments = baseSegmenter.segment(text);
|
|
42
|
+
const result = [];
|
|
43
|
+
let markerIdx = 0;
|
|
44
|
+
for (const seg of baseSegments) {
|
|
45
|
+
// Skip past markers that are entirely before this segment.
|
|
46
|
+
while (markerIdx < markers.length && markers[markerIdx].end <= seg.index) {
|
|
47
|
+
markerIdx++;
|
|
48
|
+
}
|
|
49
|
+
const marker = markerIdx < markers.length ? markers[markerIdx] : null;
|
|
50
|
+
if (marker && seg.index >= marker.start && seg.index < marker.end) {
|
|
51
|
+
// This segment falls inside a marker.
|
|
52
|
+
// If this is the first segment of the marker, emit a merged segment.
|
|
53
|
+
if (seg.index === marker.start) {
|
|
54
|
+
const markerText = text.slice(marker.start, marker.end);
|
|
55
|
+
result.push({
|
|
56
|
+
segment: markerText,
|
|
57
|
+
index: marker.start,
|
|
58
|
+
input: text,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Otherwise skip (already merged into the first segment).
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
result.push(seg);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
9
69
|
/**
|
|
10
70
|
* Split a line into word-wrapped chunks.
|
|
11
71
|
* Wraps at word boundaries when possible, falling back to character-level
|
|
@@ -13,9 +73,11 @@ const segmenter = getSegmenter();
|
|
|
13
73
|
*
|
|
14
74
|
* @param line - The text line to wrap
|
|
15
75
|
* @param maxWidth - Maximum visible width per chunk
|
|
76
|
+
* @param preSegmented - Optional pre-segmented graphemes (e.g. with paste-marker awareness).
|
|
77
|
+
* When omitted the default Intl.Segmenter is used.
|
|
16
78
|
* @returns Array of chunks with text and position information
|
|
17
79
|
*/
|
|
18
|
-
export function wordWrapLine(line, maxWidth) {
|
|
80
|
+
export function wordWrapLine(line, maxWidth, preSegmented) {
|
|
19
81
|
if (!line || maxWidth <= 0) {
|
|
20
82
|
return [{ text: "", startIndex: 0, endIndex: 0 }];
|
|
21
83
|
}
|
|
@@ -24,7 +86,7 @@ export function wordWrapLine(line, maxWidth) {
|
|
|
24
86
|
return [{ text: line, startIndex: 0, endIndex: line.length }];
|
|
25
87
|
}
|
|
26
88
|
const chunks = [];
|
|
27
|
-
const segments = [...
|
|
89
|
+
const segments = preSegmented ?? [...baseSegmenter.segment(line)];
|
|
28
90
|
let currentWidth = 0;
|
|
29
91
|
let chunkStart = 0;
|
|
30
92
|
// Wrap opportunity: the position after the last whitespace before a non-whitespace
|
|
@@ -36,11 +98,12 @@ export function wordWrapLine(line, maxWidth) {
|
|
|
36
98
|
const grapheme = seg.segment;
|
|
37
99
|
const gWidth = visibleWidth(grapheme);
|
|
38
100
|
const charIndex = seg.index;
|
|
39
|
-
const isWs = isWhitespaceChar(grapheme);
|
|
101
|
+
const isWs = !isPasteMarker(grapheme) && isWhitespaceChar(grapheme);
|
|
40
102
|
// Overflow check before advancing.
|
|
41
103
|
if (currentWidth + gWidth > maxWidth) {
|
|
42
|
-
if (wrapOppIndex >= 0) {
|
|
43
|
-
// Backtrack to last wrap opportunity
|
|
104
|
+
if (wrapOppIndex >= 0 && currentWidth - wrapOppWidth + gWidth <= maxWidth) {
|
|
105
|
+
// Backtrack to last wrap opportunity (the remaining content
|
|
106
|
+
// plus the current grapheme still fits within maxWidth).
|
|
44
107
|
chunks.push({
|
|
45
108
|
text: line.slice(chunkStart, wrapOppIndex),
|
|
46
109
|
startIndex: chunkStart,
|
|
@@ -50,7 +113,11 @@ export function wordWrapLine(line, maxWidth) {
|
|
|
50
113
|
currentWidth -= wrapOppWidth;
|
|
51
114
|
}
|
|
52
115
|
else if (chunkStart < charIndex) {
|
|
53
|
-
// No wrap opportunity: force-break at current position.
|
|
116
|
+
// No viable wrap opportunity: force-break at current position.
|
|
117
|
+
// This also handles the case where backtracking to a word
|
|
118
|
+
// boundary wouldn't help because the remaining content plus
|
|
119
|
+
// the current grapheme (e.g. a wide character) still exceeds
|
|
120
|
+
// maxWidth.
|
|
54
121
|
chunks.push({
|
|
55
122
|
text: line.slice(chunkStart, charIndex),
|
|
56
123
|
startIndex: chunkStart,
|
|
@@ -61,13 +128,33 @@ export function wordWrapLine(line, maxWidth) {
|
|
|
61
128
|
}
|
|
62
129
|
wrapOppIndex = -1;
|
|
63
130
|
}
|
|
131
|
+
if (gWidth > maxWidth) {
|
|
132
|
+
// Single atomic segment wider than maxWidth (e.g. paste marker
|
|
133
|
+
// in a narrow terminal). Re-wrap it at grapheme granularity.
|
|
134
|
+
// The segment remains logically atomic for cursor
|
|
135
|
+
// movement / editing — the split is purely visual for word-wrap layout.
|
|
136
|
+
const subChunks = wordWrapLine(grapheme, maxWidth);
|
|
137
|
+
for (let j = 0; j < subChunks.length - 1; j++) {
|
|
138
|
+
const sc = subChunks[j];
|
|
139
|
+
chunks.push({
|
|
140
|
+
text: sc.text,
|
|
141
|
+
startIndex: charIndex + sc.startIndex,
|
|
142
|
+
endIndex: charIndex + sc.endIndex,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const last = subChunks[subChunks.length - 1];
|
|
146
|
+
chunkStart = charIndex + last.startIndex;
|
|
147
|
+
currentWidth = visibleWidth(last.text);
|
|
148
|
+
wrapOppIndex = -1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
64
151
|
// Advance.
|
|
65
152
|
currentWidth += gWidth;
|
|
66
153
|
// Record wrap opportunity: whitespace followed by non-whitespace.
|
|
67
154
|
// Multiple spaces join (no break between them); the break point is
|
|
68
155
|
// after the last space before the next word.
|
|
69
156
|
const next = segments[i + 1];
|
|
70
|
-
if (isWs && next && !isWhitespaceChar(next.segment)) {
|
|
157
|
+
if (isWs && next && (isPasteMarker(next.segment) || !isWhitespaceChar(next.segment))) {
|
|
71
158
|
wrapOppIndex = next.index;
|
|
72
159
|
wrapOppWidth = currentWidth;
|
|
73
160
|
}
|
|
@@ -76,42 +163,11 @@ export function wordWrapLine(line, maxWidth) {
|
|
|
76
163
|
chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
|
|
77
164
|
return chunks;
|
|
78
165
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
// Decode a printable CSI-u sequence, preferring the shifted key when present.
|
|
85
|
-
function decodeKittyPrintable(data) {
|
|
86
|
-
const match = data.match(KITTY_CSI_U_REGEX);
|
|
87
|
-
if (!match)
|
|
88
|
-
return undefined;
|
|
89
|
-
// CSI-u groups: <codepoint>[:<shifted>[:<base>]];<mod>u
|
|
90
|
-
const codepoint = Number.parseInt(match[1] ?? "", 10);
|
|
91
|
-
if (!Number.isFinite(codepoint))
|
|
92
|
-
return undefined;
|
|
93
|
-
const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined;
|
|
94
|
-
const modValue = match[4] ? Number.parseInt(match[4], 10) : 1;
|
|
95
|
-
// Modifiers are 1-indexed in CSI-u; normalize to our bitmask.
|
|
96
|
-
const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
|
|
97
|
-
// Ignore CSI-u sequences used for Alt/Ctrl shortcuts.
|
|
98
|
-
if (modifier & (KITTY_MOD_ALT | KITTY_MOD_CTRL))
|
|
99
|
-
return undefined;
|
|
100
|
-
// Prefer the shifted keycode when Shift is held.
|
|
101
|
-
let effectiveCodepoint = codepoint;
|
|
102
|
-
if (modifier & KITTY_MOD_SHIFT && typeof shiftedKey === "number") {
|
|
103
|
-
effectiveCodepoint = shiftedKey;
|
|
104
|
-
}
|
|
105
|
-
// Drop control characters or invalid codepoints.
|
|
106
|
-
if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32)
|
|
107
|
-
return undefined;
|
|
108
|
-
try {
|
|
109
|
-
return String.fromCodePoint(effectiveCodepoint);
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
166
|
+
const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
|
|
167
|
+
minPrimaryColumnWidth: 12,
|
|
168
|
+
maxPrimaryColumnWidth: 32,
|
|
169
|
+
};
|
|
170
|
+
const ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS = 20;
|
|
115
171
|
export class Editor {
|
|
116
172
|
state = {
|
|
117
173
|
lines: [""],
|
|
@@ -135,6 +191,11 @@ export class Editor {
|
|
|
135
191
|
autocompleteState = null;
|
|
136
192
|
autocompletePrefix = "";
|
|
137
193
|
autocompleteMaxVisible = 5;
|
|
194
|
+
autocompleteAbort;
|
|
195
|
+
autocompleteDebounceTimer;
|
|
196
|
+
autocompleteRequestTask = Promise.resolve();
|
|
197
|
+
autocompleteStartToken = 0;
|
|
198
|
+
autocompleteRequestId = 0;
|
|
138
199
|
// Paste tracking for large pastes
|
|
139
200
|
pastes = new Map();
|
|
140
201
|
pasteCounter = 0;
|
|
@@ -151,12 +212,14 @@ export class Editor {
|
|
|
151
212
|
jumpMode = null;
|
|
152
213
|
// Preferred visual column for vertical cursor movement (sticky column)
|
|
153
214
|
preferredVisualCol = null;
|
|
215
|
+
// When the cursor is snapped to the start of an atomic segment, e.g. a
|
|
216
|
+
// paste marker, cursorCol no longer reflects where the cursor would have
|
|
217
|
+
// landed. This field stores the pre-snap cursorCol so that the next
|
|
218
|
+
// vertical move can resolve it to a visual column on whatever VL it belongs
|
|
219
|
+
// to.
|
|
220
|
+
snappedFromCursorCol = null;
|
|
154
221
|
// Undo support
|
|
155
222
|
undoStack = new UndoStack();
|
|
156
|
-
// Ghost text (inline suggestion shown as dim text after cursor)
|
|
157
|
-
ghostTextValue = null;
|
|
158
|
-
/** Additional change listeners that won't be overwritten by framework wiring. */
|
|
159
|
-
changeListeners = [];
|
|
160
223
|
onSubmit;
|
|
161
224
|
onChange;
|
|
162
225
|
disableSubmit = false;
|
|
@@ -171,6 +234,14 @@ export class Editor {
|
|
|
171
234
|
? Math.max(3, Math.min(20, Math.floor(maxVisible)))
|
|
172
235
|
: 5;
|
|
173
236
|
}
|
|
237
|
+
/** Set of currently valid paste IDs, for marker-aware segmentation. */
|
|
238
|
+
validPasteIds() {
|
|
239
|
+
return new Set(this.pastes.keys());
|
|
240
|
+
}
|
|
241
|
+
/** Segment text with paste-marker awareness, only merging markers with valid IDs. */
|
|
242
|
+
segment(text) {
|
|
243
|
+
return segmentWithMarkers(text, this.validPasteIds());
|
|
244
|
+
}
|
|
174
245
|
getPaddingX() {
|
|
175
246
|
return this.paddingX;
|
|
176
247
|
}
|
|
@@ -194,6 +265,7 @@ export class Editor {
|
|
|
194
265
|
}
|
|
195
266
|
}
|
|
196
267
|
setAutocompleteProvider(provider) {
|
|
268
|
+
this.cancelAutocomplete();
|
|
197
269
|
this.autocompleteProvider = provider;
|
|
198
270
|
}
|
|
199
271
|
/**
|
|
@@ -248,13 +320,15 @@ export class Editor {
|
|
|
248
320
|
}
|
|
249
321
|
/** Internal setText that doesn't reset history state - used by navigateHistory */
|
|
250
322
|
setTextInternal(text) {
|
|
251
|
-
const lines = text.
|
|
323
|
+
const lines = text.split("\n");
|
|
252
324
|
this.state.lines = lines.length === 0 ? [""] : lines;
|
|
253
325
|
this.state.cursorLine = this.state.lines.length - 1;
|
|
254
326
|
this.setCursorCol(this.state.lines[this.state.cursorLine]?.length || 0);
|
|
255
327
|
// Reset scroll - render() will adjust to show cursor
|
|
256
328
|
this.scrollOffset = 0;
|
|
257
|
-
this.
|
|
329
|
+
if (this.onChange) {
|
|
330
|
+
this.onChange(this.getText());
|
|
331
|
+
}
|
|
258
332
|
}
|
|
259
333
|
invalidate() {
|
|
260
334
|
// No cached state to invalidate currently
|
|
@@ -297,7 +371,12 @@ export class Editor {
|
|
|
297
371
|
if (this.scrollOffset > 0) {
|
|
298
372
|
const indicator = `─── ↑ ${this.scrollOffset} more `;
|
|
299
373
|
const remaining = width - visibleWidth(indicator);
|
|
300
|
-
|
|
374
|
+
if (remaining >= 0) {
|
|
375
|
+
result.push(this.borderColor(indicator + "─".repeat(remaining)));
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
result.push(this.borderColor(truncateToWidth(indicator, width)));
|
|
379
|
+
}
|
|
301
380
|
}
|
|
302
381
|
else {
|
|
303
382
|
result.push(horizontal.repeat(width));
|
|
@@ -318,7 +397,7 @@ export class Editor {
|
|
|
318
397
|
if (after.length > 0) {
|
|
319
398
|
// Cursor is on a character (grapheme) - replace it with highlighted version
|
|
320
399
|
// Get the first grapheme from 'after'
|
|
321
|
-
const afterGraphemes = [...
|
|
400
|
+
const afterGraphemes = [...this.segment(after)];
|
|
322
401
|
const firstGrapheme = afterGraphemes[0]?.segment || "";
|
|
323
402
|
const restAfter = after.slice(firstGrapheme.length);
|
|
324
403
|
const cursor = `\x1b[7m${firstGrapheme}\x1b[0m`;
|
|
@@ -336,21 +415,6 @@ export class Editor {
|
|
|
336
415
|
}
|
|
337
416
|
}
|
|
338
417
|
}
|
|
339
|
-
// Ghost text: show dim suggestion after cursor on the cursor line (end of input only)
|
|
340
|
-
if (layoutLine.hasCursor &&
|
|
341
|
-
this.ghostTextValue &&
|
|
342
|
-
layoutLine.cursorPos !== undefined &&
|
|
343
|
-
layoutLine.cursorPos >= layoutLine.text.length) {
|
|
344
|
-
// Truncate ghost text to fit remaining content width
|
|
345
|
-
const available = contentWidth - lineVisibleWidth;
|
|
346
|
-
if (available > 0) {
|
|
347
|
-
const truncated = this.ghostTextValue.length > available
|
|
348
|
-
? this.ghostTextValue.slice(0, available)
|
|
349
|
-
: this.ghostTextValue;
|
|
350
|
-
displayText += `\x1b[38;5;242m${truncated}\x1b[0m`;
|
|
351
|
-
lineVisibleWidth += truncated.length;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
418
|
// Calculate padding based on actual visible width
|
|
355
419
|
const padding = " ".repeat(Math.max(0, contentWidth - lineVisibleWidth));
|
|
356
420
|
const lineRightPadding = cursorInPadding ? rightPadding.slice(1) : rightPadding;
|
|
@@ -379,11 +443,12 @@ export class Editor {
|
|
|
379
443
|
return result;
|
|
380
444
|
}
|
|
381
445
|
handleInput(data) {
|
|
382
|
-
const kb =
|
|
446
|
+
const kb = getKeybindings();
|
|
383
447
|
// Handle character jump mode (awaiting next character to jump to)
|
|
384
448
|
if (this.jumpMode !== null) {
|
|
385
449
|
// Cancel if the hotkey is pressed again
|
|
386
|
-
if (kb.matches(data, "jumpForward") ||
|
|
450
|
+
if (kb.matches(data, "tui.editor.jumpForward") ||
|
|
451
|
+
kb.matches(data, "tui.editor.jumpBackward")) {
|
|
387
452
|
this.jumpMode = null;
|
|
388
453
|
return;
|
|
389
454
|
}
|
|
@@ -422,25 +487,25 @@ export class Editor {
|
|
|
422
487
|
return;
|
|
423
488
|
}
|
|
424
489
|
// Ctrl+C - let parent handle (exit/clear)
|
|
425
|
-
if (kb.matches(data, "copy")) {
|
|
490
|
+
if (kb.matches(data, "tui.input.copy")) {
|
|
426
491
|
return;
|
|
427
492
|
}
|
|
428
493
|
// Undo
|
|
429
|
-
if (kb.matches(data, "undo")) {
|
|
494
|
+
if (kb.matches(data, "tui.editor.undo")) {
|
|
430
495
|
this.undo();
|
|
431
496
|
return;
|
|
432
497
|
}
|
|
433
498
|
// Handle autocomplete mode
|
|
434
499
|
if (this.autocompleteState && this.autocompleteList) {
|
|
435
|
-
if (kb.matches(data, "
|
|
500
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
436
501
|
this.cancelAutocomplete();
|
|
437
502
|
return;
|
|
438
503
|
}
|
|
439
|
-
if (kb.matches(data, "
|
|
504
|
+
if (kb.matches(data, "tui.select.up") || kb.matches(data, "tui.select.down")) {
|
|
440
505
|
this.autocompleteList.handleInput(data);
|
|
441
506
|
return;
|
|
442
507
|
}
|
|
443
|
-
if (kb.matches(data, "tab")) {
|
|
508
|
+
if (kb.matches(data, "tui.input.tab")) {
|
|
444
509
|
const selected = this.autocompleteList.getSelectedItem();
|
|
445
510
|
if (selected && this.autocompleteProvider) {
|
|
446
511
|
this.pushUndoSnapshot();
|
|
@@ -450,11 +515,12 @@ export class Editor {
|
|
|
450
515
|
this.state.cursorLine = result.cursorLine;
|
|
451
516
|
this.setCursorCol(result.cursorCol);
|
|
452
517
|
this.cancelAutocomplete();
|
|
453
|
-
this.
|
|
518
|
+
if (this.onChange)
|
|
519
|
+
this.onChange(this.getText());
|
|
454
520
|
}
|
|
455
521
|
return;
|
|
456
522
|
}
|
|
457
|
-
if (kb.matches(data, "
|
|
523
|
+
if (kb.matches(data, "tui.select.confirm")) {
|
|
458
524
|
const selected = this.autocompleteList.getSelectedItem();
|
|
459
525
|
if (selected && this.autocompleteProvider) {
|
|
460
526
|
this.pushUndoSnapshot();
|
|
@@ -469,80 +535,71 @@ export class Editor {
|
|
|
469
535
|
}
|
|
470
536
|
else {
|
|
471
537
|
this.cancelAutocomplete();
|
|
472
|
-
this.
|
|
538
|
+
if (this.onChange)
|
|
539
|
+
this.onChange(this.getText());
|
|
473
540
|
return;
|
|
474
541
|
}
|
|
475
542
|
}
|
|
476
543
|
}
|
|
477
544
|
}
|
|
478
|
-
//
|
|
479
|
-
if (kb.matches(data, "
|
|
480
|
-
this.ghostTextValue = null;
|
|
481
|
-
this.tui.requestRender();
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
// Tab - accept ghost text or trigger completion
|
|
485
|
-
if (kb.matches(data, "tab") && !this.autocompleteState) {
|
|
486
|
-
if (this.ghostTextValue) {
|
|
487
|
-
this.acceptGhostText();
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
545
|
+
// Tab - trigger completion
|
|
546
|
+
if (kb.matches(data, "tui.input.tab") && !this.autocompleteState) {
|
|
490
547
|
this.handleTabCompletion();
|
|
491
548
|
return;
|
|
492
549
|
}
|
|
493
550
|
// Deletion actions
|
|
494
|
-
if (kb.matches(data, "deleteToLineEnd")) {
|
|
551
|
+
if (kb.matches(data, "tui.editor.deleteToLineEnd")) {
|
|
495
552
|
this.deleteToEndOfLine();
|
|
496
553
|
return;
|
|
497
554
|
}
|
|
498
|
-
if (kb.matches(data, "deleteToLineStart")) {
|
|
555
|
+
if (kb.matches(data, "tui.editor.deleteToLineStart")) {
|
|
499
556
|
this.deleteToStartOfLine();
|
|
500
557
|
return;
|
|
501
558
|
}
|
|
502
|
-
if (kb.matches(data, "deleteWordBackward")) {
|
|
559
|
+
if (kb.matches(data, "tui.editor.deleteWordBackward")) {
|
|
503
560
|
this.deleteWordBackwards();
|
|
504
561
|
return;
|
|
505
562
|
}
|
|
506
|
-
if (kb.matches(data, "deleteWordForward")) {
|
|
563
|
+
if (kb.matches(data, "tui.editor.deleteWordForward")) {
|
|
507
564
|
this.deleteWordForward();
|
|
508
565
|
return;
|
|
509
566
|
}
|
|
510
|
-
if (kb.matches(data, "deleteCharBackward") || matchesKey(data, "shift+backspace")) {
|
|
567
|
+
if (kb.matches(data, "tui.editor.deleteCharBackward") || matchesKey(data, "shift+backspace")) {
|
|
511
568
|
this.handleBackspace();
|
|
512
569
|
return;
|
|
513
570
|
}
|
|
514
|
-
if (kb.matches(data, "deleteCharForward") || matchesKey(data, "shift+delete")) {
|
|
571
|
+
if (kb.matches(data, "tui.editor.deleteCharForward") || matchesKey(data, "shift+delete")) {
|
|
515
572
|
this.handleForwardDelete();
|
|
516
573
|
return;
|
|
517
574
|
}
|
|
518
575
|
// Kill ring actions
|
|
519
|
-
if (kb.matches(data, "yank")) {
|
|
576
|
+
if (kb.matches(data, "tui.editor.yank")) {
|
|
520
577
|
this.yank();
|
|
521
578
|
return;
|
|
522
579
|
}
|
|
523
|
-
if (kb.matches(data, "yankPop")) {
|
|
580
|
+
if (kb.matches(data, "tui.editor.yankPop")) {
|
|
524
581
|
this.yankPop();
|
|
525
582
|
return;
|
|
526
583
|
}
|
|
527
584
|
// Cursor movement actions
|
|
528
|
-
if (kb.matches(data, "cursorLineStart")) {
|
|
585
|
+
if (kb.matches(data, "tui.editor.cursorLineStart")) {
|
|
529
586
|
this.moveToLineStart();
|
|
530
587
|
return;
|
|
531
588
|
}
|
|
532
|
-
if (kb.matches(data, "cursorLineEnd")) {
|
|
589
|
+
if (kb.matches(data, "tui.editor.cursorLineEnd")) {
|
|
533
590
|
this.moveToLineEnd();
|
|
534
591
|
return;
|
|
535
592
|
}
|
|
536
|
-
if (kb.matches(data, "cursorWordLeft")) {
|
|
593
|
+
if (kb.matches(data, "tui.editor.cursorWordLeft")) {
|
|
537
594
|
this.moveWordBackwards();
|
|
538
595
|
return;
|
|
539
596
|
}
|
|
540
|
-
if (kb.matches(data, "cursorWordRight")) {
|
|
597
|
+
if (kb.matches(data, "tui.editor.cursorWordRight")) {
|
|
541
598
|
this.moveWordForwards();
|
|
542
599
|
return;
|
|
543
600
|
}
|
|
544
601
|
// New line
|
|
545
|
-
if (kb.matches(data, "newLine") ||
|
|
602
|
+
if (kb.matches(data, "tui.input.newLine") ||
|
|
546
603
|
(data.charCodeAt(0) === 10 && data.length > 1) ||
|
|
547
604
|
data === "\x1b\r" ||
|
|
548
605
|
data === "\x1b[13;2~" ||
|
|
@@ -557,15 +614,9 @@ export class Editor {
|
|
|
557
614
|
return;
|
|
558
615
|
}
|
|
559
616
|
// Submit (Enter)
|
|
560
|
-
if (kb.matches(data, "submit")) {
|
|
617
|
+
if (kb.matches(data, "tui.input.submit")) {
|
|
561
618
|
if (this.disableSubmit)
|
|
562
619
|
return;
|
|
563
|
-
// Accept ghost text on Enter when input is empty — "just hit Enter" experience
|
|
564
|
-
if (this.isEditorEmpty() && this.ghostTextValue) {
|
|
565
|
-
this.acceptGhostText();
|
|
566
|
-
this.submitValue();
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
620
|
// Workaround for terminals without Shift+Enter support:
|
|
570
621
|
// If char before cursor is \, delete it and insert newline instead of submitting.
|
|
571
622
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
@@ -578,7 +629,7 @@ export class Editor {
|
|
|
578
629
|
return;
|
|
579
630
|
}
|
|
580
631
|
// Arrow key navigation (with history support)
|
|
581
|
-
if (kb.matches(data, "cursorUp")) {
|
|
632
|
+
if (kb.matches(data, "tui.editor.cursorUp")) {
|
|
582
633
|
if (this.isEditorEmpty()) {
|
|
583
634
|
this.navigateHistory(-1);
|
|
584
635
|
}
|
|
@@ -594,7 +645,7 @@ export class Editor {
|
|
|
594
645
|
}
|
|
595
646
|
return;
|
|
596
647
|
}
|
|
597
|
-
if (kb.matches(data, "cursorDown")) {
|
|
648
|
+
if (kb.matches(data, "tui.editor.cursorDown")) {
|
|
598
649
|
if (this.historyIndex > -1 && this.isOnLastVisualLine()) {
|
|
599
650
|
this.navigateHistory(1);
|
|
600
651
|
}
|
|
@@ -607,29 +658,29 @@ export class Editor {
|
|
|
607
658
|
}
|
|
608
659
|
return;
|
|
609
660
|
}
|
|
610
|
-
if (kb.matches(data, "cursorRight")) {
|
|
661
|
+
if (kb.matches(data, "tui.editor.cursorRight")) {
|
|
611
662
|
this.moveCursor(0, 1);
|
|
612
663
|
return;
|
|
613
664
|
}
|
|
614
|
-
if (kb.matches(data, "cursorLeft")) {
|
|
665
|
+
if (kb.matches(data, "tui.editor.cursorLeft")) {
|
|
615
666
|
this.moveCursor(0, -1);
|
|
616
667
|
return;
|
|
617
668
|
}
|
|
618
669
|
// Page up/down - scroll by page and move cursor
|
|
619
|
-
if (kb.matches(data, "pageUp")) {
|
|
670
|
+
if (kb.matches(data, "tui.editor.pageUp")) {
|
|
620
671
|
this.pageScroll(-1);
|
|
621
672
|
return;
|
|
622
673
|
}
|
|
623
|
-
if (kb.matches(data, "pageDown")) {
|
|
674
|
+
if (kb.matches(data, "tui.editor.pageDown")) {
|
|
624
675
|
this.pageScroll(1);
|
|
625
676
|
return;
|
|
626
677
|
}
|
|
627
678
|
// Character jump mode triggers
|
|
628
|
-
if (kb.matches(data, "jumpForward")) {
|
|
679
|
+
if (kb.matches(data, "tui.editor.jumpForward")) {
|
|
629
680
|
this.jumpMode = "forward";
|
|
630
681
|
return;
|
|
631
682
|
}
|
|
632
|
-
if (kb.matches(data, "jumpBackward")) {
|
|
683
|
+
if (kb.matches(data, "tui.editor.jumpBackward")) {
|
|
633
684
|
this.jumpMode = "backward";
|
|
634
685
|
return;
|
|
635
686
|
}
|
|
@@ -683,7 +734,7 @@ export class Editor {
|
|
|
683
734
|
}
|
|
684
735
|
else {
|
|
685
736
|
// Line needs wrapping - use word-aware wrapping
|
|
686
|
-
const chunks = wordWrapLine(line, contentWidth);
|
|
737
|
+
const chunks = wordWrapLine(line, contentWidth, [...this.segment(line)]);
|
|
687
738
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
688
739
|
const chunk = chunks[chunkIndex];
|
|
689
740
|
if (!chunk)
|
|
@@ -735,17 +786,20 @@ export class Editor {
|
|
|
735
786
|
getText() {
|
|
736
787
|
return this.state.lines.join("\n");
|
|
737
788
|
}
|
|
789
|
+
expandPasteMarkers(text) {
|
|
790
|
+
let result = text;
|
|
791
|
+
for (const [pasteId, pasteContent] of this.pastes) {
|
|
792
|
+
const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
|
|
793
|
+
result = result.replace(markerRegex, () => pasteContent);
|
|
794
|
+
}
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
738
797
|
/**
|
|
739
798
|
* Get text with paste markers expanded to their actual content.
|
|
740
799
|
* Use this when you need the full content (e.g., for external editor).
|
|
741
800
|
*/
|
|
742
801
|
getExpandedText() {
|
|
743
|
-
|
|
744
|
-
for (const [pasteId, pasteContent] of this.pastes) {
|
|
745
|
-
const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
|
|
746
|
-
result = result.replace(markerRegex, pasteContent);
|
|
747
|
-
}
|
|
748
|
-
return result;
|
|
802
|
+
return this.expandPasteMarkers(this.state.lines.join("\n"));
|
|
749
803
|
}
|
|
750
804
|
getLines() {
|
|
751
805
|
return [...this.state.lines];
|
|
@@ -754,69 +808,15 @@ export class Editor {
|
|
|
754
808
|
return { line: this.state.cursorLine, col: this.state.cursorCol };
|
|
755
809
|
}
|
|
756
810
|
setText(text) {
|
|
811
|
+
this.cancelAutocomplete();
|
|
757
812
|
this.lastAction = null;
|
|
758
813
|
this.historyIndex = -1; // Exit history browsing mode
|
|
814
|
+
const normalized = this.normalizeText(text);
|
|
759
815
|
// Push undo snapshot if content differs (makes programmatic changes undoable)
|
|
760
|
-
if (this.getText() !==
|
|
816
|
+
if (this.getText() !== normalized) {
|
|
761
817
|
this.pushUndoSnapshot();
|
|
762
818
|
}
|
|
763
|
-
this.setTextInternal(
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Set ghost text (inline suggestion shown as dim text after the cursor).
|
|
767
|
-
* Pass null to clear. Ghost text is purely visual — not part of the buffer.
|
|
768
|
-
*
|
|
769
|
-
* @param text - Suggestion to display, or null to clear
|
|
770
|
-
*/
|
|
771
|
-
setGhostText(text) {
|
|
772
|
-
if (this.ghostTextValue !== text) {
|
|
773
|
-
this.ghostTextValue = text;
|
|
774
|
-
this.tui.requestRender();
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Get the current ghost text, or null if none.
|
|
779
|
-
*
|
|
780
|
-
* @returns Current ghost text string, or null
|
|
781
|
-
*/
|
|
782
|
-
getGhostText() {
|
|
783
|
-
return this.ghostTextValue;
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* Register a change listener that fires alongside onChange.
|
|
787
|
-
* Unlike onChange, listeners aren't overwritten by framework wiring.
|
|
788
|
-
*
|
|
789
|
-
* @param fn - Callback receiving the new text content
|
|
790
|
-
*/
|
|
791
|
-
addChangeListener(fn) {
|
|
792
|
-
this.changeListeners.push(fn);
|
|
793
|
-
}
|
|
794
|
-
/**
|
|
795
|
-
* Notify onChange and all registered change listeners.
|
|
796
|
-
* Centralises all text-change notifications.
|
|
797
|
-
*/
|
|
798
|
-
notifyChange() {
|
|
799
|
-
const text = this.getText();
|
|
800
|
-
this.onChange?.(text);
|
|
801
|
-
for (const fn of this.changeListeners) {
|
|
802
|
-
fn(text);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* Accept ghost text into the editor buffer at the cursor position.
|
|
807
|
-
* Clears ghost text and triggers onChange.
|
|
808
|
-
*
|
|
809
|
-
* @returns true if ghost text was accepted, false if none was showing
|
|
810
|
-
*/
|
|
811
|
-
acceptGhostText() {
|
|
812
|
-
if (!this.ghostTextValue)
|
|
813
|
-
return false;
|
|
814
|
-
const text = this.ghostTextValue;
|
|
815
|
-
this.ghostTextValue = null;
|
|
816
|
-
this.pushUndoSnapshot();
|
|
817
|
-
this.insertTextAtCursorInternal(text);
|
|
818
|
-
this.notifyChange();
|
|
819
|
-
return true;
|
|
819
|
+
this.setTextInternal(normalized);
|
|
820
820
|
}
|
|
821
821
|
/**
|
|
822
822
|
* Insert text at the current cursor position.
|
|
@@ -826,11 +826,20 @@ export class Editor {
|
|
|
826
826
|
insertTextAtCursor(text) {
|
|
827
827
|
if (!text)
|
|
828
828
|
return;
|
|
829
|
+
this.cancelAutocomplete();
|
|
829
830
|
this.pushUndoSnapshot();
|
|
830
831
|
this.lastAction = null;
|
|
831
832
|
this.historyIndex = -1;
|
|
832
833
|
this.insertTextAtCursorInternal(text);
|
|
833
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* Normalize text for editor storage:
|
|
837
|
+
* - Normalize line endings (\r\n and \r -> \n)
|
|
838
|
+
* - Expand tabs to 4 spaces
|
|
839
|
+
*/
|
|
840
|
+
normalizeText(text) {
|
|
841
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ");
|
|
842
|
+
}
|
|
834
843
|
/**
|
|
835
844
|
* Internal text insertion at cursor. Handles single and multi-line text.
|
|
836
845
|
* Does not push undo snapshots or trigger autocomplete - caller is responsible.
|
|
@@ -839,8 +848,8 @@ export class Editor {
|
|
|
839
848
|
insertTextAtCursorInternal(text) {
|
|
840
849
|
if (!text)
|
|
841
850
|
return;
|
|
842
|
-
// Normalize line endings
|
|
843
|
-
const normalized =
|
|
851
|
+
// Normalize line endings and tabs
|
|
852
|
+
const normalized = this.normalizeText(text);
|
|
844
853
|
const insertedLines = normalized.split("\n");
|
|
845
854
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
846
855
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
@@ -867,13 +876,13 @@ export class Editor {
|
|
|
867
876
|
this.state.cursorLine += insertedLines.length - 1;
|
|
868
877
|
this.setCursorCol((insertedLines[insertedLines.length - 1] || "").length);
|
|
869
878
|
}
|
|
870
|
-
this.
|
|
879
|
+
if (this.onChange) {
|
|
880
|
+
this.onChange(this.getText());
|
|
881
|
+
}
|
|
871
882
|
}
|
|
872
883
|
// All the editor methods from before...
|
|
873
884
|
insertCharacter(char, skipUndoCoalescing) {
|
|
874
885
|
this.historyIndex = -1; // Exit history browsing mode
|
|
875
|
-
// Dismiss ghost text on any character input
|
|
876
|
-
this.ghostTextValue = null;
|
|
877
886
|
// Undo coalescing (fish-style):
|
|
878
887
|
// - Consecutive word chars coalesce into one undo unit
|
|
879
888
|
// - Space captures state before itself (so undo removes space+following word together)
|
|
@@ -890,7 +899,9 @@ export class Editor {
|
|
|
890
899
|
const after = line.slice(this.state.cursorCol);
|
|
891
900
|
this.state.lines[this.state.cursorLine] = before + char + after;
|
|
892
901
|
this.setCursorCol(this.state.cursorCol + char.length);
|
|
893
|
-
this.
|
|
902
|
+
if (this.onChange) {
|
|
903
|
+
this.onChange(this.getText());
|
|
904
|
+
}
|
|
894
905
|
// Check if we should trigger or update autocomplete
|
|
895
906
|
if (!this.autocompleteState) {
|
|
896
907
|
// Auto-trigger for "/" at the start of a line (slash commands)
|
|
@@ -926,15 +937,14 @@ export class Editor {
|
|
|
926
937
|
}
|
|
927
938
|
}
|
|
928
939
|
handlePaste(pastedText) {
|
|
940
|
+
this.cancelAutocomplete();
|
|
929
941
|
this.historyIndex = -1; // Exit history browsing mode
|
|
930
942
|
this.lastAction = null;
|
|
931
943
|
this.pushUndoSnapshot();
|
|
932
|
-
// Clean the pasted text
|
|
933
|
-
const cleanText =
|
|
934
|
-
// Convert tabs to spaces (4 spaces per tab)
|
|
935
|
-
const tabExpandedText = cleanText.replace(/\t/g, " ");
|
|
944
|
+
// Clean the pasted text: normalize line endings, expand tabs
|
|
945
|
+
const cleanText = this.normalizeText(pastedText);
|
|
936
946
|
// Filter out non-printable characters except newlines
|
|
937
|
-
let filteredText =
|
|
947
|
+
let filteredText = cleanText
|
|
938
948
|
.split("")
|
|
939
949
|
.filter((char) => char === "\n" || char.charCodeAt(0) >= 32)
|
|
940
950
|
.join("");
|
|
@@ -964,16 +974,15 @@ export class Editor {
|
|
|
964
974
|
return;
|
|
965
975
|
}
|
|
966
976
|
if (pastedLines.length === 1) {
|
|
967
|
-
// Single line - insert
|
|
968
|
-
|
|
969
|
-
this.insertCharacter(char, true);
|
|
970
|
-
}
|
|
977
|
+
// Single line - insert atomically (do not trigger autocomplete during paste)
|
|
978
|
+
this.insertTextAtCursorInternal(filteredText);
|
|
971
979
|
return;
|
|
972
980
|
}
|
|
973
981
|
// Multi-line paste - use direct state manipulation
|
|
974
982
|
this.insertTextAtCursorInternal(filteredText);
|
|
975
983
|
}
|
|
976
984
|
addNewLine() {
|
|
985
|
+
this.cancelAutocomplete();
|
|
977
986
|
this.historyIndex = -1; // Exit history browsing mode
|
|
978
987
|
this.lastAction = null;
|
|
979
988
|
this.pushUndoSnapshot();
|
|
@@ -986,14 +995,16 @@ export class Editor {
|
|
|
986
995
|
// Move cursor to start of new line
|
|
987
996
|
this.state.cursorLine++;
|
|
988
997
|
this.setCursorCol(0);
|
|
989
|
-
this.
|
|
998
|
+
if (this.onChange) {
|
|
999
|
+
this.onChange(this.getText());
|
|
1000
|
+
}
|
|
990
1001
|
}
|
|
991
1002
|
shouldSubmitOnBackslashEnter(data, kb) {
|
|
992
1003
|
if (this.disableSubmit)
|
|
993
1004
|
return false;
|
|
994
1005
|
if (!matchesKey(data, "enter"))
|
|
995
1006
|
return false;
|
|
996
|
-
const submitKeys = kb.getKeys("submit");
|
|
1007
|
+
const submitKeys = kb.getKeys("tui.input.submit");
|
|
997
1008
|
const hasShiftEnter = submitKeys.includes("shift+enter") || submitKeys.includes("shift+return");
|
|
998
1009
|
if (!hasShiftEnter)
|
|
999
1010
|
return false;
|
|
@@ -1001,11 +1012,8 @@ export class Editor {
|
|
|
1001
1012
|
return this.state.cursorCol > 0 && currentLine[this.state.cursorCol - 1] === "\\";
|
|
1002
1013
|
}
|
|
1003
1014
|
submitValue() {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
|
|
1007
|
-
result = result.replace(markerRegex, pasteContent);
|
|
1008
|
-
}
|
|
1015
|
+
this.cancelAutocomplete();
|
|
1016
|
+
const result = this.expandPasteMarkers(this.state.lines.join("\n")).trim();
|
|
1009
1017
|
this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
|
|
1010
1018
|
this.pastes.clear();
|
|
1011
1019
|
this.pasteCounter = 0;
|
|
@@ -1013,21 +1021,21 @@ export class Editor {
|
|
|
1013
1021
|
this.scrollOffset = 0;
|
|
1014
1022
|
this.undoStack.clear();
|
|
1015
1023
|
this.lastAction = null;
|
|
1016
|
-
this.
|
|
1024
|
+
if (this.onChange)
|
|
1025
|
+
this.onChange("");
|
|
1017
1026
|
if (this.onSubmit)
|
|
1018
1027
|
this.onSubmit(result);
|
|
1019
1028
|
}
|
|
1020
1029
|
handleBackspace() {
|
|
1021
1030
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1022
1031
|
this.lastAction = null;
|
|
1023
|
-
this.ghostTextValue = null;
|
|
1024
1032
|
if (this.state.cursorCol > 0) {
|
|
1025
1033
|
this.pushUndoSnapshot();
|
|
1026
1034
|
// Delete grapheme before cursor (handles emojis, combining characters, etc.)
|
|
1027
1035
|
const line = this.state.lines[this.state.cursorLine] || "";
|
|
1028
1036
|
const beforeCursor = line.slice(0, this.state.cursorCol);
|
|
1029
1037
|
// Find the last grapheme in the text before cursor
|
|
1030
|
-
const graphemes = [...
|
|
1038
|
+
const graphemes = [...this.segment(beforeCursor)];
|
|
1031
1039
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
1032
1040
|
const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
1033
1041
|
const before = line.slice(0, this.state.cursorCol - graphemeLength);
|
|
@@ -1045,7 +1053,9 @@ export class Editor {
|
|
|
1045
1053
|
this.state.cursorLine--;
|
|
1046
1054
|
this.setCursorCol(previousLine.length);
|
|
1047
1055
|
}
|
|
1048
|
-
this.
|
|
1056
|
+
if (this.onChange) {
|
|
1057
|
+
this.onChange(this.getText());
|
|
1058
|
+
}
|
|
1049
1059
|
// Update or re-trigger autocomplete after backspace
|
|
1050
1060
|
if (this.autocompleteState) {
|
|
1051
1061
|
this.updateAutocomplete();
|
|
@@ -1071,6 +1081,7 @@ export class Editor {
|
|
|
1071
1081
|
setCursorCol(col) {
|
|
1072
1082
|
this.state.cursorCol = col;
|
|
1073
1083
|
this.preferredVisualCol = null;
|
|
1084
|
+
this.snappedFromCursorCol = null;
|
|
1074
1085
|
}
|
|
1075
1086
|
/**
|
|
1076
1087
|
* Move cursor to a target visual line, applying sticky column logic.
|
|
@@ -1079,26 +1090,74 @@ export class Editor {
|
|
|
1079
1090
|
moveToVisualLine(visualLines, currentVisualLine, targetVisualLine) {
|
|
1080
1091
|
const currentVL = visualLines[currentVisualLine];
|
|
1081
1092
|
const targetVL = visualLines[targetVisualLine];
|
|
1082
|
-
if (currentVL && targetVL)
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
visualLines[targetVisualLine + 1]?.logicalLine !== targetVL.logicalLine;
|
|
1092
|
-
const targetMaxVisualCol = isLastTargetSegment
|
|
1093
|
-
? targetVL.length
|
|
1094
|
-
: Math.max(0, targetVL.length - 1);
|
|
1095
|
-
const moveToVisualCol = this.computeVerticalMoveColumn(currentVisualCol, sourceMaxVisualCol, targetMaxVisualCol);
|
|
1096
|
-
// Set cursor position
|
|
1097
|
-
this.state.cursorLine = targetVL.logicalLine;
|
|
1098
|
-
const targetCol = targetVL.startCol + moveToVisualCol;
|
|
1099
|
-
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
|
1100
|
-
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
|
1093
|
+
if (!(currentVL && targetVL))
|
|
1094
|
+
return;
|
|
1095
|
+
// When the cursor was snapped to a segment start, resolve the pre-snap
|
|
1096
|
+
// position against the VL it belongs to. This gives the correct visual
|
|
1097
|
+
// column even after a resize reshuffles VLs.
|
|
1098
|
+
let currentVisualCol;
|
|
1099
|
+
if (this.snappedFromCursorCol !== null) {
|
|
1100
|
+
const vlIndex = this.findVisualLineAt(visualLines, currentVL.logicalLine, this.snappedFromCursorCol);
|
|
1101
|
+
currentVisualCol = this.snappedFromCursorCol - visualLines[vlIndex].startCol;
|
|
1101
1102
|
}
|
|
1103
|
+
else {
|
|
1104
|
+
currentVisualCol = this.state.cursorCol - currentVL.startCol;
|
|
1105
|
+
}
|
|
1106
|
+
// For non-last segments, clamp to length-1 to stay within the segment
|
|
1107
|
+
const isLastSourceSegment = currentVisualLine === visualLines.length - 1 ||
|
|
1108
|
+
visualLines[currentVisualLine + 1]?.logicalLine !== currentVL.logicalLine;
|
|
1109
|
+
const sourceMaxVisualCol = isLastSourceSegment
|
|
1110
|
+
? currentVL.length
|
|
1111
|
+
: Math.max(0, currentVL.length - 1);
|
|
1112
|
+
const isLastTargetSegment = targetVisualLine === visualLines.length - 1 ||
|
|
1113
|
+
visualLines[targetVisualLine + 1]?.logicalLine !== targetVL.logicalLine;
|
|
1114
|
+
const targetMaxVisualCol = isLastTargetSegment
|
|
1115
|
+
? targetVL.length
|
|
1116
|
+
: Math.max(0, targetVL.length - 1);
|
|
1117
|
+
const moveToVisualCol = this.computeVerticalMoveColumn(currentVisualCol, sourceMaxVisualCol, targetMaxVisualCol);
|
|
1118
|
+
// Set cursor position
|
|
1119
|
+
this.state.cursorLine = targetVL.logicalLine;
|
|
1120
|
+
const targetCol = targetVL.startCol + moveToVisualCol;
|
|
1121
|
+
const logicalLine = this.state.lines[targetVL.logicalLine] || "";
|
|
1122
|
+
this.state.cursorCol = Math.min(targetCol, logicalLine.length);
|
|
1123
|
+
// Snap cursor to atomic segment boundary (e.g. paste markers)
|
|
1124
|
+
// so the cursor never lands in the middle of a multi-grapheme unit.
|
|
1125
|
+
// Single-grapheme segments don't need snapping.
|
|
1126
|
+
const segments = [...this.segment(logicalLine)];
|
|
1127
|
+
for (const seg of segments) {
|
|
1128
|
+
if (seg.index > this.state.cursorCol)
|
|
1129
|
+
break;
|
|
1130
|
+
if (seg.segment.length <= 1)
|
|
1131
|
+
continue;
|
|
1132
|
+
if (this.state.cursorCol < seg.index + seg.segment.length) {
|
|
1133
|
+
const isContinuation = seg.index < targetVL.startCol;
|
|
1134
|
+
const isMovingDown = targetVisualLine > currentVisualLine;
|
|
1135
|
+
if (isContinuation && isMovingDown) {
|
|
1136
|
+
// The segment started on a previous visual line, and we
|
|
1137
|
+
// already visited it on the way down. Skip all remaining
|
|
1138
|
+
// continuation VLs and land on the first VL past it.
|
|
1139
|
+
const segEnd = seg.index + seg.segment.length;
|
|
1140
|
+
let next = targetVisualLine + 1;
|
|
1141
|
+
while (next < visualLines.length &&
|
|
1142
|
+
visualLines[next].logicalLine === targetVL.logicalLine &&
|
|
1143
|
+
visualLines[next].startCol < segEnd) {
|
|
1144
|
+
next++;
|
|
1145
|
+
}
|
|
1146
|
+
if (next < visualLines.length) {
|
|
1147
|
+
this.moveToVisualLine(visualLines, currentVisualLine, next);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
// Snap to the start of the segment so it gets highlighted.
|
|
1152
|
+
// Store the pre-snap position so the next vertical move can
|
|
1153
|
+
// resolve it to the correct visual column.
|
|
1154
|
+
this.snappedFromCursorCol = this.state.cursorCol;
|
|
1155
|
+
this.state.cursorCol = seg.index;
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
// No snap occurred – we moved out of the atomic segment.
|
|
1160
|
+
this.snappedFromCursorCol = null;
|
|
1102
1161
|
}
|
|
1103
1162
|
/**
|
|
1104
1163
|
* Compute the target visual column for vertical cursor movement.
|
|
@@ -1177,7 +1236,9 @@ export class Editor {
|
|
|
1177
1236
|
this.state.cursorLine--;
|
|
1178
1237
|
this.setCursorCol(previousLine.length);
|
|
1179
1238
|
}
|
|
1180
|
-
this.
|
|
1239
|
+
if (this.onChange) {
|
|
1240
|
+
this.onChange(this.getText());
|
|
1241
|
+
}
|
|
1181
1242
|
}
|
|
1182
1243
|
deleteToEndOfLine() {
|
|
1183
1244
|
this.historyIndex = -1; // Exit history browsing mode
|
|
@@ -1200,7 +1261,9 @@ export class Editor {
|
|
|
1200
1261
|
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
1201
1262
|
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
1202
1263
|
}
|
|
1203
|
-
this.
|
|
1264
|
+
if (this.onChange) {
|
|
1265
|
+
this.onChange(this.getText());
|
|
1266
|
+
}
|
|
1204
1267
|
}
|
|
1205
1268
|
deleteWordBackwards() {
|
|
1206
1269
|
this.historyIndex = -1; // Exit history browsing mode
|
|
@@ -1234,7 +1297,9 @@ export class Editor {
|
|
|
1234
1297
|
currentLine.slice(0, deleteFrom) + currentLine.slice(this.state.cursorCol);
|
|
1235
1298
|
this.setCursorCol(deleteFrom);
|
|
1236
1299
|
}
|
|
1237
|
-
this.
|
|
1300
|
+
if (this.onChange) {
|
|
1301
|
+
this.onChange(this.getText());
|
|
1302
|
+
}
|
|
1238
1303
|
}
|
|
1239
1304
|
deleteWordForward() {
|
|
1240
1305
|
this.historyIndex = -1; // Exit history browsing mode
|
|
@@ -1265,19 +1330,20 @@ export class Editor {
|
|
|
1265
1330
|
this.state.lines[this.state.cursorLine] =
|
|
1266
1331
|
currentLine.slice(0, this.state.cursorCol) + currentLine.slice(deleteTo);
|
|
1267
1332
|
}
|
|
1268
|
-
this.
|
|
1333
|
+
if (this.onChange) {
|
|
1334
|
+
this.onChange(this.getText());
|
|
1335
|
+
}
|
|
1269
1336
|
}
|
|
1270
1337
|
handleForwardDelete() {
|
|
1271
1338
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1272
1339
|
this.lastAction = null;
|
|
1273
|
-
this.ghostTextValue = null;
|
|
1274
1340
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1275
1341
|
if (this.state.cursorCol < currentLine.length) {
|
|
1276
1342
|
this.pushUndoSnapshot();
|
|
1277
1343
|
// Delete grapheme at cursor position (handles emojis, combining characters, etc.)
|
|
1278
1344
|
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
1279
1345
|
// Find the first grapheme at cursor
|
|
1280
|
-
const graphemes = [...
|
|
1346
|
+
const graphemes = [...this.segment(afterCursor)];
|
|
1281
1347
|
const firstGrapheme = graphemes[0];
|
|
1282
1348
|
const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
|
|
1283
1349
|
const before = currentLine.slice(0, this.state.cursorCol);
|
|
@@ -1291,7 +1357,9 @@ export class Editor {
|
|
|
1291
1357
|
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
1292
1358
|
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
1293
1359
|
}
|
|
1294
|
-
this.
|
|
1360
|
+
if (this.onChange) {
|
|
1361
|
+
this.onChange(this.getText());
|
|
1362
|
+
}
|
|
1295
1363
|
// Update or re-trigger autocomplete after forward delete
|
|
1296
1364
|
if (this.autocompleteState) {
|
|
1297
1365
|
this.updateAutocomplete();
|
|
@@ -1330,7 +1398,7 @@ export class Editor {
|
|
|
1330
1398
|
}
|
|
1331
1399
|
else {
|
|
1332
1400
|
// Line needs wrapping - use word-aware wrapping
|
|
1333
|
-
const chunks = wordWrapLine(line, width);
|
|
1401
|
+
const chunks = wordWrapLine(line, width, [...this.segment(line)]);
|
|
1334
1402
|
for (const chunk of chunks) {
|
|
1335
1403
|
visualLines.push({
|
|
1336
1404
|
logicalLine: i,
|
|
@@ -1343,27 +1411,29 @@ export class Editor {
|
|
|
1343
1411
|
return visualLines;
|
|
1344
1412
|
}
|
|
1345
1413
|
/**
|
|
1346
|
-
* Find the visual line index
|
|
1414
|
+
* Find the visual line index that contains the given logical position.
|
|
1347
1415
|
*/
|
|
1348
|
-
|
|
1416
|
+
findVisualLineAt(visualLines, line, col) {
|
|
1349
1417
|
for (let i = 0; i < visualLines.length; i++) {
|
|
1350
1418
|
const vl = visualLines[i];
|
|
1351
|
-
if (!vl)
|
|
1419
|
+
if (!vl || vl.logicalLine !== line)
|
|
1352
1420
|
continue;
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
(colInSegment < vl.length || (isLastSegmentOfLine && colInSegment <= vl.length))) {
|
|
1360
|
-
return i;
|
|
1361
|
-
}
|
|
1421
|
+
const offset = col - vl.startCol;
|
|
1422
|
+
// Cursor is in this segment if it's within range. For the last
|
|
1423
|
+
// segment of a logical line, cursor can be at length (end position)
|
|
1424
|
+
const isLastSegmentOfLine = i === visualLines.length - 1 || visualLines[i + 1]?.logicalLine !== vl.logicalLine;
|
|
1425
|
+
if (offset >= 0 && (offset < vl.length || (isLastSegmentOfLine && offset === vl.length))) {
|
|
1426
|
+
return i;
|
|
1362
1427
|
}
|
|
1363
1428
|
}
|
|
1364
|
-
// Fallback: return last visual line
|
|
1365
1429
|
return visualLines.length - 1;
|
|
1366
1430
|
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Find the visual line index for the current cursor position.
|
|
1433
|
+
*/
|
|
1434
|
+
findCurrentVisualLine(visualLines) {
|
|
1435
|
+
return this.findVisualLineAt(visualLines, this.state.cursorLine, this.state.cursorCol);
|
|
1436
|
+
}
|
|
1367
1437
|
moveCursor(deltaLine, deltaCol) {
|
|
1368
1438
|
this.lastAction = null;
|
|
1369
1439
|
const visualLines = this.buildVisualLineMap(this.lastWidth);
|
|
@@ -1380,7 +1450,7 @@ export class Editor {
|
|
|
1380
1450
|
// Moving right - move by one grapheme (handles emojis, combining characters, etc.)
|
|
1381
1451
|
if (this.state.cursorCol < currentLine.length) {
|
|
1382
1452
|
const afterCursor = currentLine.slice(this.state.cursorCol);
|
|
1383
|
-
const graphemes = [...
|
|
1453
|
+
const graphemes = [...this.segment(afterCursor)];
|
|
1384
1454
|
const firstGrapheme = graphemes[0];
|
|
1385
1455
|
this.setCursorCol(this.state.cursorCol + (firstGrapheme ? firstGrapheme.segment.length : 1));
|
|
1386
1456
|
}
|
|
@@ -1401,7 +1471,7 @@ export class Editor {
|
|
|
1401
1471
|
// Moving left - move by one grapheme (handles emojis, combining characters, etc.)
|
|
1402
1472
|
if (this.state.cursorCol > 0) {
|
|
1403
1473
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1404
|
-
const graphemes = [...
|
|
1474
|
+
const graphemes = [...this.segment(beforeCursor)];
|
|
1405
1475
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
1406
1476
|
this.setCursorCol(this.state.cursorCol - (lastGrapheme ? lastGrapheme.segment.length : 1));
|
|
1407
1477
|
}
|
|
@@ -1440,19 +1510,25 @@ export class Editor {
|
|
|
1440
1510
|
return;
|
|
1441
1511
|
}
|
|
1442
1512
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1443
|
-
const graphemes = [...
|
|
1513
|
+
const graphemes = [...this.segment(textBeforeCursor)];
|
|
1444
1514
|
let newCol = this.state.cursorCol;
|
|
1445
1515
|
// Skip trailing whitespace
|
|
1446
1516
|
while (graphemes.length > 0 &&
|
|
1517
|
+
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1447
1518
|
isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1448
1519
|
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1449
1520
|
}
|
|
1450
1521
|
if (graphemes.length > 0) {
|
|
1451
1522
|
const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
|
|
1452
|
-
if (
|
|
1523
|
+
if (isPasteMarker(lastGrapheme)) {
|
|
1524
|
+
// Paste marker is a single atomic word
|
|
1525
|
+
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1526
|
+
}
|
|
1527
|
+
else if (isPunctuationChar(lastGrapheme)) {
|
|
1453
1528
|
// Skip punctuation run
|
|
1454
1529
|
while (graphemes.length > 0 &&
|
|
1455
|
-
isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "")
|
|
1530
|
+
isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1531
|
+
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1456
1532
|
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1457
1533
|
}
|
|
1458
1534
|
}
|
|
@@ -1460,7 +1536,8 @@ export class Editor {
|
|
|
1460
1536
|
// Skip word run
|
|
1461
1537
|
while (graphemes.length > 0 &&
|
|
1462
1538
|
!isWhitespaceChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1463
|
-
!isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "")
|
|
1539
|
+
!isPunctuationChar(graphemes[graphemes.length - 1]?.segment || "") &&
|
|
1540
|
+
!isPasteMarker(graphemes[graphemes.length - 1]?.segment || "")) {
|
|
1464
1541
|
newCol -= graphemes.pop()?.segment.length || 0;
|
|
1465
1542
|
}
|
|
1466
1543
|
}
|
|
@@ -1528,7 +1605,9 @@ export class Editor {
|
|
|
1528
1605
|
this.state.cursorLine = lastLineIndex;
|
|
1529
1606
|
this.setCursorCol((lines[lines.length - 1] || "").length);
|
|
1530
1607
|
}
|
|
1531
|
-
this.
|
|
1608
|
+
if (this.onChange) {
|
|
1609
|
+
this.onChange(this.getText());
|
|
1610
|
+
}
|
|
1532
1611
|
}
|
|
1533
1612
|
/**
|
|
1534
1613
|
* Delete the previously yanked text (used by yank-pop).
|
|
@@ -1562,7 +1641,9 @@ export class Editor {
|
|
|
1562
1641
|
this.state.cursorLine = startLine;
|
|
1563
1642
|
this.setCursorCol(startCol);
|
|
1564
1643
|
}
|
|
1565
|
-
this.
|
|
1644
|
+
if (this.onChange) {
|
|
1645
|
+
this.onChange(this.getText());
|
|
1646
|
+
}
|
|
1566
1647
|
}
|
|
1567
1648
|
pushUndoSnapshot() {
|
|
1568
1649
|
this.undoStack.push(this.state);
|
|
@@ -1575,7 +1656,9 @@ export class Editor {
|
|
|
1575
1656
|
Object.assign(this.state, snapshot);
|
|
1576
1657
|
this.lastAction = null;
|
|
1577
1658
|
this.preferredVisualCol = null;
|
|
1578
|
-
this.
|
|
1659
|
+
if (this.onChange) {
|
|
1660
|
+
this.onChange(this.getText());
|
|
1661
|
+
}
|
|
1579
1662
|
}
|
|
1580
1663
|
/**
|
|
1581
1664
|
* Jump to the first occurrence of a character in the specified direction.
|
|
@@ -1617,20 +1700,28 @@ export class Editor {
|
|
|
1617
1700
|
return;
|
|
1618
1701
|
}
|
|
1619
1702
|
const textAfterCursor = currentLine.slice(this.state.cursorCol);
|
|
1620
|
-
const segments =
|
|
1703
|
+
const segments = this.segment(textAfterCursor);
|
|
1621
1704
|
const iterator = segments[Symbol.iterator]();
|
|
1622
1705
|
let next = iterator.next();
|
|
1623
1706
|
let newCol = this.state.cursorCol;
|
|
1624
1707
|
// Skip leading whitespace
|
|
1625
|
-
while (!next.done &&
|
|
1708
|
+
while (!next.done &&
|
|
1709
|
+
!isPasteMarker(next.value.segment) &&
|
|
1710
|
+
isWhitespaceChar(next.value.segment)) {
|
|
1626
1711
|
newCol += next.value.segment.length;
|
|
1627
1712
|
next = iterator.next();
|
|
1628
1713
|
}
|
|
1629
1714
|
if (!next.done) {
|
|
1630
1715
|
const firstGrapheme = next.value.segment;
|
|
1631
|
-
if (
|
|
1716
|
+
if (isPasteMarker(firstGrapheme)) {
|
|
1717
|
+
// Paste marker is a single atomic word
|
|
1718
|
+
newCol += firstGrapheme.length;
|
|
1719
|
+
}
|
|
1720
|
+
else if (isPunctuationChar(firstGrapheme)) {
|
|
1632
1721
|
// Skip punctuation run
|
|
1633
|
-
while (!next.done &&
|
|
1722
|
+
while (!next.done &&
|
|
1723
|
+
isPunctuationChar(next.value.segment) &&
|
|
1724
|
+
!isPasteMarker(next.value.segment)) {
|
|
1634
1725
|
newCol += next.value.segment.length;
|
|
1635
1726
|
next = iterator.next();
|
|
1636
1727
|
}
|
|
@@ -1639,7 +1730,8 @@ export class Editor {
|
|
|
1639
1730
|
// Skip word run
|
|
1640
1731
|
while (!next.done &&
|
|
1641
1732
|
!isWhitespaceChar(next.value.segment) &&
|
|
1642
|
-
!isPunctuationChar(next.value.segment)
|
|
1733
|
+
!isPunctuationChar(next.value.segment) &&
|
|
1734
|
+
!isPasteMarker(next.value.segment)) {
|
|
1643
1735
|
newCol += next.value.segment.length;
|
|
1644
1736
|
next = iterator.next();
|
|
1645
1737
|
}
|
|
@@ -1663,34 +1755,44 @@ export class Editor {
|
|
|
1663
1755
|
return this.isSlashMenuAllowed() && textBeforeCursor.trimStart().startsWith("/");
|
|
1664
1756
|
}
|
|
1665
1757
|
// Autocomplete methods
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1758
|
+
/**
|
|
1759
|
+
* Find the best autocomplete item index for the given prefix.
|
|
1760
|
+
* Returns -1 if no match is found.
|
|
1761
|
+
*
|
|
1762
|
+
* Match priority:
|
|
1763
|
+
* 1. Exact match (prefix === item.value) -> always selected
|
|
1764
|
+
* 2. Prefix match -> first item whose value starts with prefix
|
|
1765
|
+
* 3. No match -> -1 (keep default highlight)
|
|
1766
|
+
*
|
|
1767
|
+
* Matching is case-sensitive and checks item.value only.
|
|
1768
|
+
*/
|
|
1769
|
+
getBestAutocompleteMatchIndex(items, prefix) {
|
|
1770
|
+
if (!prefix)
|
|
1771
|
+
return -1;
|
|
1772
|
+
let firstPrefixIndex = -1;
|
|
1773
|
+
for (let i = 0; i < items.length; i++) {
|
|
1774
|
+
const value = items[i].value;
|
|
1775
|
+
if (value === prefix) {
|
|
1776
|
+
return i; // Exact match always wins
|
|
1777
|
+
}
|
|
1778
|
+
if (firstPrefixIndex === -1 && value.startsWith(prefix)) {
|
|
1779
|
+
firstPrefixIndex = i;
|
|
1676
1780
|
}
|
|
1677
1781
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
}
|
|
1782
|
+
return firstPrefixIndex;
|
|
1783
|
+
}
|
|
1784
|
+
createAutocompleteList(prefix, items) {
|
|
1785
|
+
const layout = prefix.startsWith("/") ? SLASH_COMMAND_SELECT_LIST_LAYOUT : undefined;
|
|
1786
|
+
return new SelectList(items, this.autocompleteMaxVisible, this.theme.selectList, layout);
|
|
1787
|
+
}
|
|
1788
|
+
tryTriggerAutocomplete(explicitTab = false) {
|
|
1789
|
+
this.requestAutocomplete({ force: false, explicitTab });
|
|
1687
1790
|
}
|
|
1688
1791
|
handleTabCompletion() {
|
|
1689
1792
|
if (!this.autocompleteProvider)
|
|
1690
1793
|
return;
|
|
1691
1794
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1692
1795
|
const beforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1693
|
-
// Check if we're in a slash command context
|
|
1694
1796
|
if (this.isInSlashCommandContext(beforeCursor) && !beforeCursor.trimStart().includes(" ")) {
|
|
1695
1797
|
this.handleSlashCommandCompletion();
|
|
1696
1798
|
}
|
|
@@ -1699,68 +1801,130 @@ export class Editor {
|
|
|
1699
1801
|
}
|
|
1700
1802
|
}
|
|
1701
1803
|
handleSlashCommandCompletion() {
|
|
1702
|
-
this.
|
|
1804
|
+
this.requestAutocomplete({ force: false, explicitTab: true });
|
|
1703
1805
|
}
|
|
1704
|
-
/*
|
|
1705
|
-
https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/559322883
|
|
1706
|
-
17 this job fails with https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19
|
|
1707
|
-
536643416/job/55932288317 havea look at .gi
|
|
1708
|
-
*/
|
|
1709
1806
|
forceFileAutocomplete(explicitTab = false) {
|
|
1807
|
+
this.requestAutocomplete({ force: true, explicitTab });
|
|
1808
|
+
}
|
|
1809
|
+
requestAutocomplete(options) {
|
|
1710
1810
|
if (!this.autocompleteProvider)
|
|
1711
1811
|
return;
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1812
|
+
if (options.force) {
|
|
1813
|
+
const provider = this.autocompleteProvider;
|
|
1814
|
+
const shouldTrigger = !provider.shouldTriggerFileCompletion ||
|
|
1815
|
+
provider.shouldTriggerFileCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
1816
|
+
if (!shouldTrigger) {
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
this.cancelAutocompleteRequest();
|
|
1821
|
+
const startToken = ++this.autocompleteStartToken;
|
|
1822
|
+
const debounceMs = this.getAutocompleteDebounceMs(options);
|
|
1823
|
+
if (debounceMs > 0) {
|
|
1824
|
+
this.autocompleteDebounceTimer = setTimeout(() => {
|
|
1825
|
+
this.autocompleteDebounceTimer = undefined;
|
|
1826
|
+
void this.startAutocompleteRequest(startToken, options);
|
|
1827
|
+
}, debounceMs);
|
|
1716
1828
|
return;
|
|
1717
1829
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, item, suggestions.prefix);
|
|
1726
|
-
this.state.lines = result.lines;
|
|
1727
|
-
this.state.cursorLine = result.cursorLine;
|
|
1728
|
-
this.setCursorCol(result.cursorCol);
|
|
1729
|
-
this.notifyChange();
|
|
1830
|
+
void this.startAutocompleteRequest(startToken, options);
|
|
1831
|
+
}
|
|
1832
|
+
async startAutocompleteRequest(startToken, options) {
|
|
1833
|
+
const previousTask = this.autocompleteRequestTask;
|
|
1834
|
+
this.autocompleteRequestTask = (async () => {
|
|
1835
|
+
await previousTask;
|
|
1836
|
+
if (startToken !== this.autocompleteStartToken || !this.autocompleteProvider) {
|
|
1730
1837
|
return;
|
|
1731
1838
|
}
|
|
1732
|
-
|
|
1733
|
-
this.
|
|
1734
|
-
|
|
1839
|
+
const controller = new AbortController();
|
|
1840
|
+
this.autocompleteAbort = controller;
|
|
1841
|
+
const requestId = ++this.autocompleteRequestId;
|
|
1842
|
+
const snapshotText = this.getText();
|
|
1843
|
+
const snapshotLine = this.state.cursorLine;
|
|
1844
|
+
const snapshotCol = this.state.cursorCol;
|
|
1845
|
+
await this.runAutocompleteRequest(requestId, controller, snapshotText, snapshotLine, snapshotCol, options);
|
|
1846
|
+
})();
|
|
1847
|
+
await this.autocompleteRequestTask;
|
|
1848
|
+
}
|
|
1849
|
+
getAutocompleteDebounceMs(options) {
|
|
1850
|
+
if (options.explicitTab || options.force) {
|
|
1851
|
+
return 0;
|
|
1735
1852
|
}
|
|
1736
|
-
|
|
1853
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1854
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1855
|
+
const isAttachmentContext = /(?:^|[ \t])@(?:"[^"]*|[^\s]*)$/.test(textBeforeCursor);
|
|
1856
|
+
return isAttachmentContext ? ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS : 0;
|
|
1857
|
+
}
|
|
1858
|
+
async runAutocompleteRequest(requestId, controller, snapshotText, snapshotLine, snapshotCol, options) {
|
|
1859
|
+
if (!this.autocompleteProvider)
|
|
1860
|
+
return;
|
|
1861
|
+
const suggestions = await this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol, { signal: controller.signal, force: options.force });
|
|
1862
|
+
if (!this.isAutocompleteRequestCurrent(requestId, controller, snapshotText, snapshotLine, snapshotCol)) {
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
this.autocompleteAbort = undefined;
|
|
1866
|
+
if (!suggestions || !Array.isArray(suggestions.items) || suggestions.items.length === 0) {
|
|
1737
1867
|
this.cancelAutocomplete();
|
|
1868
|
+
this.tui.requestRender();
|
|
1869
|
+
return;
|
|
1738
1870
|
}
|
|
1871
|
+
if (options.force && options.explicitTab && suggestions.items.length === 1) {
|
|
1872
|
+
const item = suggestions.items[0];
|
|
1873
|
+
this.pushUndoSnapshot();
|
|
1874
|
+
this.lastAction = null;
|
|
1875
|
+
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, item, suggestions.prefix);
|
|
1876
|
+
this.state.lines = result.lines;
|
|
1877
|
+
this.state.cursorLine = result.cursorLine;
|
|
1878
|
+
this.setCursorCol(result.cursorCol);
|
|
1879
|
+
if (this.onChange)
|
|
1880
|
+
this.onChange(this.getText());
|
|
1881
|
+
this.tui.requestRender();
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
this.applyAutocompleteSuggestions(suggestions, options.force ? "force" : "regular");
|
|
1885
|
+
this.tui.requestRender();
|
|
1739
1886
|
}
|
|
1740
|
-
|
|
1887
|
+
isAutocompleteRequestCurrent(requestId, controller, snapshotText, snapshotLine, snapshotCol) {
|
|
1888
|
+
return (!controller.signal.aborted &&
|
|
1889
|
+
requestId === this.autocompleteRequestId &&
|
|
1890
|
+
this.getText() === snapshotText &&
|
|
1891
|
+
this.state.cursorLine === snapshotLine &&
|
|
1892
|
+
this.state.cursorCol === snapshotCol);
|
|
1893
|
+
}
|
|
1894
|
+
applyAutocompleteSuggestions(suggestions, state) {
|
|
1895
|
+
this.autocompletePrefix = suggestions.prefix;
|
|
1896
|
+
this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
1897
|
+
const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
|
|
1898
|
+
if (bestMatchIndex >= 0) {
|
|
1899
|
+
this.autocompleteList.setSelectedIndex(bestMatchIndex);
|
|
1900
|
+
}
|
|
1901
|
+
this.autocompleteState = state;
|
|
1902
|
+
}
|
|
1903
|
+
cancelAutocompleteRequest() {
|
|
1904
|
+
this.autocompleteStartToken += 1;
|
|
1905
|
+
if (this.autocompleteDebounceTimer) {
|
|
1906
|
+
clearTimeout(this.autocompleteDebounceTimer);
|
|
1907
|
+
this.autocompleteDebounceTimer = undefined;
|
|
1908
|
+
}
|
|
1909
|
+
this.autocompleteAbort?.abort();
|
|
1910
|
+
this.autocompleteAbort = undefined;
|
|
1911
|
+
}
|
|
1912
|
+
clearAutocompleteUi() {
|
|
1741
1913
|
this.autocompleteState = null;
|
|
1742
1914
|
this.autocompleteList = undefined;
|
|
1743
1915
|
this.autocompletePrefix = "";
|
|
1744
1916
|
}
|
|
1917
|
+
cancelAutocomplete() {
|
|
1918
|
+
this.cancelAutocompleteRequest();
|
|
1919
|
+
this.clearAutocompleteUi();
|
|
1920
|
+
}
|
|
1745
1921
|
isShowingAutocomplete() {
|
|
1746
1922
|
return this.autocompleteState !== null;
|
|
1747
1923
|
}
|
|
1748
1924
|
updateAutocomplete() {
|
|
1749
1925
|
if (!this.autocompleteState || !this.autocompleteProvider)
|
|
1750
1926
|
return;
|
|
1751
|
-
|
|
1752
|
-
this.forceFileAutocomplete();
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
const suggestions = this.autocompleteProvider.getSuggestions(this.state.lines, this.state.cursorLine, this.state.cursorCol);
|
|
1756
|
-
if (suggestions && suggestions.items.length > 0) {
|
|
1757
|
-
this.autocompletePrefix = suggestions.prefix;
|
|
1758
|
-
// Always create new SelectList to ensure update
|
|
1759
|
-
this.autocompleteList = new SelectList(suggestions.items, this.autocompleteMaxVisible, this.theme.selectList);
|
|
1760
|
-
}
|
|
1761
|
-
else {
|
|
1762
|
-
this.cancelAutocomplete();
|
|
1763
|
-
}
|
|
1927
|
+
this.requestAutocomplete({ force: this.autocompleteState === "force", explicitTab: false });
|
|
1764
1928
|
}
|
|
1765
1929
|
}
|
|
1766
1930
|
//# sourceMappingURL=editor.js.map
|