@timurproko/a1 0.1.8-dev.139 → 0.1.8-dev.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -9
- package/bin/cli.js +1 -0
- package/dist/cli/dispatch.d.ts +6 -0
- package/dist/cli/dispatch.js +107 -81
- package/dist/cli/version-stats.js +7 -1
- package/dist/composition/owned-ui.js +10 -7
- package/dist/composition/settings-route-host.d.ts +1 -1
- package/dist/composition/settings-route-host.js +1 -1
- package/dist/contracts/owned-ui/model.d.ts +8 -1
- package/dist/contracts/owned-ui/validation.js +11 -0
- package/dist/features/owned-ui/settings-app.js +2 -1
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
- package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
- package/dist/integrations/pi/engine/adapter.js +5 -1
- package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
- package/dist/integrations/pi/engine/session-integration.js +14 -3
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
- package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +17 -3
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +100 -260
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +52 -12
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
- package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
- package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -6
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +3 -3
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/manifest.json +3 -3
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/apps/contracts.d.ts +23 -1
- package/dist/ui/apps/host.js +9 -1
- package/dist/ui/components/scrollbar.d.ts +2 -2
- package/dist/ui/components/scrollbar.js +3 -3
- package/dist/ui/components/spans.d.ts +16 -1
- package/dist/ui/components/spans.js +144 -2
- package/dist/ui/components/text.js +24 -2
- package/dist/ui/components/transcript-viewport.d.ts +6 -0
- package/dist/ui/components/transcript-viewport.js +36 -8
- package/dist/ui/settings/declarations.d.ts +1 -1
- package/dist/ui/settings/declarations.js +3 -3
- package/dist/ui/settings/migrations.js +9 -0
- package/docs/architecture/boundaries.md +1 -1
- package/docs/architecture/project-structure.md +1 -1
- package/docs/ci-release-runbook.md +6 -5
- package/docs/features/launch-profiles.md +3 -3
- package/package.json +4 -1
- package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
- /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
- /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
- /package/dist/integrations/pi/{owned-ui → session-ui}/route-host.js +0 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import { CURSOR_MARKER, decodeKittyPrintable, visibleWidth, } from "#pi-tui";
|
|
2
|
+
export class OwnedEditorUxInterception {
|
|
3
|
+
interceptors;
|
|
4
|
+
fallback;
|
|
5
|
+
constructor(interceptors, fallback) {
|
|
6
|
+
this.interceptors = interceptors;
|
|
7
|
+
this.fallback = fallback;
|
|
8
|
+
}
|
|
9
|
+
handleInput(data) {
|
|
10
|
+
const invoke = (index) => {
|
|
11
|
+
const interceptor = this.interceptors[index];
|
|
12
|
+
if (interceptor === undefined) {
|
|
13
|
+
this.fallback.handleInput(data);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
interceptor.handleInput(data, () => invoke(index + 1));
|
|
17
|
+
};
|
|
18
|
+
invoke(0);
|
|
19
|
+
}
|
|
20
|
+
render(width) {
|
|
21
|
+
const invoke = (index) => {
|
|
22
|
+
const interceptor = this.interceptors[index];
|
|
23
|
+
return interceptor === undefined
|
|
24
|
+
? this.fallback.render(width)
|
|
25
|
+
: interceptor.render(width, () => invoke(index + 1));
|
|
26
|
+
};
|
|
27
|
+
return invoke(0);
|
|
28
|
+
}
|
|
29
|
+
reset() {
|
|
30
|
+
for (const interceptor of this.interceptors)
|
|
31
|
+
interceptor.reset();
|
|
32
|
+
}
|
|
33
|
+
handlePointer(event) {
|
|
34
|
+
return this.interceptors.some(interceptor => interceptor.handlePointer?.(event) === true);
|
|
35
|
+
}
|
|
36
|
+
hasSelection() {
|
|
37
|
+
return this.interceptors.some(interceptor => interceptor.hasSelection?.() === true);
|
|
38
|
+
}
|
|
39
|
+
ownsPointer() {
|
|
40
|
+
return this.interceptors.some(interceptor => interceptor.ownsPointer?.() === true);
|
|
41
|
+
}
|
|
42
|
+
pasteClipboard() {
|
|
43
|
+
return this.interceptors.some(interceptor => interceptor.pasteClipboard?.() === true);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const ATOMIC_SEGMENTATION = Symbol("a1.editor.atomicSegmentation");
|
|
47
|
+
const ATOMIC_SPACE_SENTINEL = "\uE000";
|
|
48
|
+
const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
49
|
+
const MULTI_CLICK_MS = 500;
|
|
50
|
+
export function createPromptSelectionInterceptor(editor, keybindings, options) {
|
|
51
|
+
return new PromptSelectionInterceptor(editor, keybindings, options);
|
|
52
|
+
}
|
|
53
|
+
class PromptSelectionInterceptor {
|
|
54
|
+
editor;
|
|
55
|
+
keybindings;
|
|
56
|
+
options;
|
|
57
|
+
#selection;
|
|
58
|
+
#pointerSelecting = false;
|
|
59
|
+
#lastClick;
|
|
60
|
+
#redoStack = [];
|
|
61
|
+
#selectionRevision = 0;
|
|
62
|
+
#geometry;
|
|
63
|
+
constructor(editor, keybindings, options) {
|
|
64
|
+
this.editor = editor;
|
|
65
|
+
this.keybindings = keybindings;
|
|
66
|
+
this.options = options;
|
|
67
|
+
installAtomicSegmentation(editor, options.atomicRanges);
|
|
68
|
+
}
|
|
69
|
+
handleInput(data, next) {
|
|
70
|
+
if (this.keybindings.matches(data, "owned.editor.selectAll")) {
|
|
71
|
+
this.#selectAll();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (this.keybindings.matches(data, "owned.editor.extendLeft")) {
|
|
75
|
+
this.#extend(-1);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (this.keybindings.matches(data, "owned.editor.extendRight")) {
|
|
79
|
+
this.#extend(1);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (this.keybindings.matches(data, "tui.input.copy") && this.hasSelection()) {
|
|
83
|
+
this.options.copyText(this.options.expandCopiedText(this.#selectedText()));
|
|
84
|
+
this.#collapseCopiedSelection();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (this.keybindings.matches(data, "owned.editor.cut") && this.hasSelection()) {
|
|
88
|
+
this.options.copyText(this.options.expandCopiedText(this.#selectedText()));
|
|
89
|
+
if (!this.#deleteSelection())
|
|
90
|
+
this.#deleteAtomicFocus();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (this.keybindings.matches(data, "owned.editor.paste")) {
|
|
94
|
+
this.pasteClipboard();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (this.keybindings.matches(data, "owned.editor.redo")) {
|
|
98
|
+
this.#redo();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const terminalPaste = bracketedPasteContent(data);
|
|
102
|
+
if (terminalPaste !== undefined) {
|
|
103
|
+
if (terminalPaste.length === 0) {
|
|
104
|
+
this.pasteClipboard();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const transformed = this.options.transformPastedContent({ kind: "text", text: terminalPaste });
|
|
108
|
+
if (transformed !== terminalPaste) {
|
|
109
|
+
if (this.#orderedSelection() !== undefined)
|
|
110
|
+
this.#replaceSelection(transformed);
|
|
111
|
+
else
|
|
112
|
+
this.editor.insertTextAtCursor(transformed);
|
|
113
|
+
this.#redoStack = [];
|
|
114
|
+
this.#requestRender();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (this.keybindings.matches(data, "tui.editor.undo")) {
|
|
119
|
+
const before = this.#snapshot();
|
|
120
|
+
this.#clearSelection(false);
|
|
121
|
+
next();
|
|
122
|
+
if (!sameSnapshot(before, this.#snapshot()))
|
|
123
|
+
this.#redoStack.push(before);
|
|
124
|
+
this.#requestRender();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const selection = this.#orderedSelection();
|
|
128
|
+
if (selection !== undefined) {
|
|
129
|
+
if (this.keybindings.matches(data, "tui.editor.deleteCharBackward")
|
|
130
|
+
|| this.keybindings.matches(data, "tui.editor.deleteCharForward")
|
|
131
|
+
|| this.keybindings.matches(data, "tui.editor.deleteWordBackward")
|
|
132
|
+
|| this.keybindings.matches(data, "tui.editor.deleteWordForward")) {
|
|
133
|
+
this.#deleteSelection();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const inserted = insertedText(data);
|
|
137
|
+
if (inserted !== undefined) {
|
|
138
|
+
this.#replaceSelection(inserted);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (data.includes("\u001b[200~")) {
|
|
142
|
+
this.#deleteSelection();
|
|
143
|
+
next();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (this.keybindings.matches(data, "tui.editor.cursorLeft")
|
|
147
|
+
|| this.keybindings.matches(data, "tui.editor.cursorWordLeft")) {
|
|
148
|
+
this.#setCursor(selection.start);
|
|
149
|
+
this.#clearSelection();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (this.keybindings.matches(data, "tui.editor.cursorRight")
|
|
153
|
+
|| this.keybindings.matches(data, "tui.editor.cursorWordRight")) {
|
|
154
|
+
this.#setCursor(selection.end);
|
|
155
|
+
this.#clearSelection();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.#clearSelection(false);
|
|
159
|
+
}
|
|
160
|
+
else if ((this.keybindings.matches(data, "tui.editor.deleteCharBackward")
|
|
161
|
+
|| this.keybindings.matches(data, "tui.editor.deleteCharForward")
|
|
162
|
+
|| this.keybindings.matches(data, "tui.editor.deleteWordBackward")
|
|
163
|
+
|| this.keybindings.matches(data, "tui.editor.deleteWordForward"))
|
|
164
|
+
&& this.#deleteAtomicFocus()) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (this.keybindings.matches(data, "tui.editor.cursorWordLeft")) {
|
|
168
|
+
const before = this.#cursor();
|
|
169
|
+
const startedWithAtomicFocus = this.#atomicFocus() !== undefined;
|
|
170
|
+
next();
|
|
171
|
+
let after = this.#cursor();
|
|
172
|
+
const landedAtomic = this.#atomicRangeAt(after);
|
|
173
|
+
if (!startedWithAtomicFocus && landedAtomic?.start === after.col && after.col > 0) {
|
|
174
|
+
const line = editorState(this.editor).lines[after.line] ?? "";
|
|
175
|
+
const previous = [...GRAPHEMES.segment(line.slice(0, after.col))].at(-1);
|
|
176
|
+
if (previous !== undefined && !/^\s+$/u.test(previous.segment)) {
|
|
177
|
+
next();
|
|
178
|
+
after = this.#cursor();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!samePosition(before, after))
|
|
182
|
+
this.#moveOntoPreviousSeparator();
|
|
183
|
+
this.#requestRender();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const beforeText = this.editor.getText();
|
|
187
|
+
next();
|
|
188
|
+
if (this.editor.getText() !== beforeText)
|
|
189
|
+
this.#redoStack = [];
|
|
190
|
+
}
|
|
191
|
+
render(width, next) {
|
|
192
|
+
const rows = next().map(row => row.replaceAll(ATOMIC_SPACE_SENTINEL, " "));
|
|
193
|
+
const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
|
|
194
|
+
const padding = Math.min(this.editor.getPaddingX(), maxPadding);
|
|
195
|
+
const contentWidth = Math.max(1, width - padding * 2);
|
|
196
|
+
const layoutWidth = Math.max(1, contentWidth - (padding ? 0 : 1));
|
|
197
|
+
const visualLines = editorVisualLineMap(this.editor, layoutWidth)
|
|
198
|
+
?? buildVisualLineMap(editorState(this.editor).lines, layoutWidth);
|
|
199
|
+
const scrollOffset = numericProperty(this.editor, "scrollOffset");
|
|
200
|
+
const maxVisibleLines = Math.max(5, Math.floor(this.options.getRows() * 0.3));
|
|
201
|
+
const textRows = Math.max(0, Math.min(visualLines.length - scrollOffset, maxVisibleLines, rows.length - 2));
|
|
202
|
+
this.#geometry = { width, padding, layoutWidth, visualLines, scrollOffset, textRows };
|
|
203
|
+
if (this.#atomicFocus() !== undefined) {
|
|
204
|
+
for (let row = 0; row < rows.length; row += 1) {
|
|
205
|
+
rows[row] = (rows[row] ?? "").replaceAll(CURSOR_MARKER, "");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const selection = this.#orderedSelection();
|
|
209
|
+
if (selection !== undefined) {
|
|
210
|
+
for (let row = 0; row < textRows; row += 1) {
|
|
211
|
+
const visual = visualLines[scrollOffset + row];
|
|
212
|
+
if (visual === undefined || visual.logicalLine < selection.start.line || visual.logicalLine > selection.end.line)
|
|
213
|
+
continue;
|
|
214
|
+
const segmentStart = visual.startCol;
|
|
215
|
+
const segmentEnd = visual.startCol + visual.length;
|
|
216
|
+
const from = visual.logicalLine === selection.start.line ? Math.max(selection.start.col, segmentStart) : segmentStart;
|
|
217
|
+
const to = visual.logicalLine === selection.end.line ? Math.min(selection.end.col, segmentEnd) : segmentEnd;
|
|
218
|
+
if (to <= from)
|
|
219
|
+
continue;
|
|
220
|
+
const line = editorState(this.editor).lines[visual.logicalLine] ?? "";
|
|
221
|
+
const fromColumn = padding + visibleWidth(line.slice(segmentStart, from));
|
|
222
|
+
const toColumn = fromColumn + visibleWidth(line.slice(from, to));
|
|
223
|
+
const rendered = rows[row + 1];
|
|
224
|
+
if (rendered !== undefined && toColumn > fromColumn) {
|
|
225
|
+
rows[row + 1] = this.options.paintSelection(rendered, fromColumn, toColumn, false);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return rows.map(row => this.options.decorateRow(row, width));
|
|
230
|
+
}
|
|
231
|
+
reset() {
|
|
232
|
+
this.#selection = undefined;
|
|
233
|
+
this.#pointerSelecting = false;
|
|
234
|
+
this.#lastClick = undefined;
|
|
235
|
+
this.#redoStack = [];
|
|
236
|
+
this.#selectionRevision += 1;
|
|
237
|
+
}
|
|
238
|
+
hasSelection() {
|
|
239
|
+
return this.#activeRange() !== undefined;
|
|
240
|
+
}
|
|
241
|
+
pasteClipboard() {
|
|
242
|
+
this.#pasteFromClipboard();
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
ownsPointer() {
|
|
246
|
+
return this.#pointerSelecting;
|
|
247
|
+
}
|
|
248
|
+
handlePointer(event) {
|
|
249
|
+
if (event.button !== 0)
|
|
250
|
+
return false;
|
|
251
|
+
if (event.kind === "press")
|
|
252
|
+
return this.#pointerPress(event.column, event.row);
|
|
253
|
+
if (event.kind === "motion") {
|
|
254
|
+
if (!this.#pointerSelecting)
|
|
255
|
+
return false;
|
|
256
|
+
let position = this.#positionAt(event.column, event.row, true);
|
|
257
|
+
if (position !== undefined && this.#selection !== undefined) {
|
|
258
|
+
const atomic = this.#atomicRangeAt(position);
|
|
259
|
+
if (atomic !== undefined) {
|
|
260
|
+
const beforeAnchor = position.line < this.#selection.anchor.line
|
|
261
|
+
|| (position.line === this.#selection.anchor.line && atomic.start < this.#selection.anchor.col);
|
|
262
|
+
position = { line: position.line, col: beforeAnchor ? atomic.start : atomic.end };
|
|
263
|
+
}
|
|
264
|
+
this.#selection = { ...this.#selection, head: position };
|
|
265
|
+
this.#selectionRevision += 1;
|
|
266
|
+
this.#requestRender();
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (!this.#pointerSelecting)
|
|
271
|
+
return false;
|
|
272
|
+
this.#pointerSelecting = false;
|
|
273
|
+
const ordered = this.#orderedSelection();
|
|
274
|
+
if (ordered === undefined && this.#selection !== undefined)
|
|
275
|
+
this.#setCursor(this.#selection.head);
|
|
276
|
+
if (ordered === undefined)
|
|
277
|
+
this.#clearSelection(false);
|
|
278
|
+
this.#requestRender();
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
#pointerPress(column, row) {
|
|
282
|
+
const position = this.#positionAt(column, row, false);
|
|
283
|
+
if (position === undefined)
|
|
284
|
+
return false;
|
|
285
|
+
const atomic = this.#atomicRangeAt(position);
|
|
286
|
+
if (atomic !== undefined) {
|
|
287
|
+
this.#selection = {
|
|
288
|
+
anchor: { line: position.line, col: atomic.start },
|
|
289
|
+
head: { line: position.line, col: atomic.start },
|
|
290
|
+
};
|
|
291
|
+
this.#setCursor({ line: position.line, col: atomic.start });
|
|
292
|
+
this.#pointerSelecting = true;
|
|
293
|
+
this.#lastClick = undefined;
|
|
294
|
+
this.#selectionRevision += 1;
|
|
295
|
+
this.#requestRender();
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
const previous = this.#lastClick;
|
|
300
|
+
const count = previous !== undefined
|
|
301
|
+
&& now - previous.time <= MULTI_CLICK_MS
|
|
302
|
+
&& previous.line === position.line
|
|
303
|
+
&& Math.abs(previous.col - position.col) <= 1
|
|
304
|
+
? previous.count + 1
|
|
305
|
+
: 1;
|
|
306
|
+
this.#lastClick = { time: now, ...position, count };
|
|
307
|
+
const kind = ((count - 1) % 3) + 1;
|
|
308
|
+
if (kind === 2)
|
|
309
|
+
this.#selectWord(position);
|
|
310
|
+
else if (kind === 3)
|
|
311
|
+
this.#selectLine(position.line);
|
|
312
|
+
else
|
|
313
|
+
this.#selection = { anchor: position, head: position };
|
|
314
|
+
this.#pointerSelecting = true;
|
|
315
|
+
this.#selectionRevision += 1;
|
|
316
|
+
this.#requestRender();
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
#positionAt(column, row, clampRow) {
|
|
320
|
+
const geometry = this.#geometry;
|
|
321
|
+
if (geometry === undefined || geometry.textRows === 0)
|
|
322
|
+
return undefined;
|
|
323
|
+
let textRow = row - 2;
|
|
324
|
+
if (clampRow)
|
|
325
|
+
textRow = clamp(textRow, 0, geometry.textRows - 1);
|
|
326
|
+
if (textRow < 0 || textRow >= geometry.textRows)
|
|
327
|
+
return undefined;
|
|
328
|
+
const visual = geometry.visualLines[geometry.scrollOffset + textRow];
|
|
329
|
+
if (visual === undefined)
|
|
330
|
+
return undefined;
|
|
331
|
+
const line = editorState(this.editor).lines[visual.logicalLine] ?? "";
|
|
332
|
+
const segment = line.slice(visual.startCol, visual.startCol + visual.length);
|
|
333
|
+
const displayColumn = Math.max(0, column - 1 - geometry.padding);
|
|
334
|
+
return {
|
|
335
|
+
line: visual.logicalLine,
|
|
336
|
+
col: visual.startCol + indexAtDisplayWidth(segment, displayColumn),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
#selectWord(position) {
|
|
340
|
+
const atomic = this.#atomicRangeAt(position);
|
|
341
|
+
if (atomic !== undefined) {
|
|
342
|
+
this.#selection = {
|
|
343
|
+
anchor: { line: position.line, col: atomic.start },
|
|
344
|
+
head: { line: position.line, col: atomic.end },
|
|
345
|
+
};
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const line = editorState(this.editor).lines[position.line] ?? "";
|
|
349
|
+
const segments = [...GRAPHEMES.segment(line)].map(({ segment, index }) => ({
|
|
350
|
+
class: charClass(segment), start: index, end: index + segment.length,
|
|
351
|
+
}));
|
|
352
|
+
let selected = segments.findIndex(segment => position.col >= segment.start && position.col < segment.end);
|
|
353
|
+
if (selected < 0 && segments.length > 0 && position.col === line.length)
|
|
354
|
+
selected = segments.length - 1;
|
|
355
|
+
if (selected < 0)
|
|
356
|
+
return;
|
|
357
|
+
let start = selected;
|
|
358
|
+
let end = selected;
|
|
359
|
+
while (start > 0 && segments[start - 1]?.class === segments[selected]?.class)
|
|
360
|
+
start -= 1;
|
|
361
|
+
while (end + 1 < segments.length && segments[end + 1]?.class === segments[selected]?.class)
|
|
362
|
+
end += 1;
|
|
363
|
+
this.#selection = {
|
|
364
|
+
anchor: { line: position.line, col: segments[start]?.start ?? position.col },
|
|
365
|
+
head: { line: position.line, col: segments[end]?.end ?? position.col },
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
#selectLine(line) {
|
|
369
|
+
const text = editorState(this.editor).lines[line] ?? "";
|
|
370
|
+
this.#selection = { anchor: { line, col: 0 }, head: { line, col: text.length } };
|
|
371
|
+
}
|
|
372
|
+
#selectAll() {
|
|
373
|
+
const lines = editorState(this.editor).lines;
|
|
374
|
+
const finalLine = Math.max(0, lines.length - 1);
|
|
375
|
+
this.#selection = {
|
|
376
|
+
anchor: { line: 0, col: 0 },
|
|
377
|
+
head: { line: finalLine, col: (lines[finalLine] ?? "").length },
|
|
378
|
+
};
|
|
379
|
+
this.#pointerSelecting = false;
|
|
380
|
+
this.#selectionRevision += 1;
|
|
381
|
+
this.#requestRender();
|
|
382
|
+
}
|
|
383
|
+
#extend(delta) {
|
|
384
|
+
const cursor = this.#cursor();
|
|
385
|
+
if (this.#selection === undefined)
|
|
386
|
+
this.#selection = { anchor: cursor, head: cursor };
|
|
387
|
+
else if (!samePosition(cursor, this.#selection.head))
|
|
388
|
+
this.#setCursor(this.#selection.head);
|
|
389
|
+
this.#moveCursor(delta);
|
|
390
|
+
this.#selection = { anchor: this.#selection.anchor, head: this.#cursor() };
|
|
391
|
+
this.#pointerSelecting = false;
|
|
392
|
+
this.#lastClick = undefined;
|
|
393
|
+
this.#selectionRevision += 1;
|
|
394
|
+
this.#requestRender();
|
|
395
|
+
}
|
|
396
|
+
#pasteFromClipboard() {
|
|
397
|
+
const revision = this.#selectionRevision;
|
|
398
|
+
const selection = this.#orderedSelection();
|
|
399
|
+
const atomicFocus = this.#atomicFocus();
|
|
400
|
+
void this.options.readClipboardContent().then(content => {
|
|
401
|
+
if (content === null)
|
|
402
|
+
return;
|
|
403
|
+
const text = this.options.transformPastedContent(content);
|
|
404
|
+
if (text.length === 0)
|
|
405
|
+
return;
|
|
406
|
+
if (selection !== undefined && revision === this.#selectionRevision && this.#orderedSelection() !== undefined) {
|
|
407
|
+
this.#replaceSelection(text);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (atomicFocus !== undefined && samePosition(this.#cursor(), atomicFocus.start)) {
|
|
411
|
+
this.editor.insertTextAtCursor(text);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
this.#clearSelection(false);
|
|
415
|
+
this.editor.insertTextAtCursor(text);
|
|
416
|
+
}
|
|
417
|
+
this.#redoStack = [];
|
|
418
|
+
this.#requestRender();
|
|
419
|
+
}).catch(() => { });
|
|
420
|
+
}
|
|
421
|
+
#replaceSelection(text) {
|
|
422
|
+
const selection = this.#orderedSelection();
|
|
423
|
+
if (selection === undefined) {
|
|
424
|
+
this.editor.insertTextAtCursor(text);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const normalized = normalizeInsertedText(text);
|
|
428
|
+
const current = this.editor.getText();
|
|
429
|
+
const lines = editorState(this.editor).lines;
|
|
430
|
+
const from = positionOffset(lines, selection.start);
|
|
431
|
+
const to = positionOffset(lines, selection.end);
|
|
432
|
+
const next = current.slice(0, from) + normalized + current.slice(to);
|
|
433
|
+
this.editor.setText(next);
|
|
434
|
+
this.#setCursor(positionAtOffset(next, from + normalized.length));
|
|
435
|
+
this.#redoStack = [];
|
|
436
|
+
this.#clearSelection(false);
|
|
437
|
+
this.#requestRender();
|
|
438
|
+
}
|
|
439
|
+
#deleteSelection() {
|
|
440
|
+
const selection = this.#orderedSelection();
|
|
441
|
+
if (selection === undefined)
|
|
442
|
+
return false;
|
|
443
|
+
const current = this.editor.getText();
|
|
444
|
+
const lines = editorState(this.editor).lines;
|
|
445
|
+
const from = positionOffset(lines, selection.start);
|
|
446
|
+
const to = positionOffset(lines, selection.end);
|
|
447
|
+
this.editor.setText(current.slice(0, from) + current.slice(to));
|
|
448
|
+
this.#setCursor(positionAtOffset(this.editor.getText(), from));
|
|
449
|
+
this.#redoStack = [];
|
|
450
|
+
this.#clearSelection(false);
|
|
451
|
+
this.#requestRender();
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
#deleteAtomicFocus() {
|
|
455
|
+
const focus = this.#atomicFocus();
|
|
456
|
+
if (focus === undefined)
|
|
457
|
+
return false;
|
|
458
|
+
const current = this.editor.getText();
|
|
459
|
+
const lines = editorState(this.editor).lines;
|
|
460
|
+
const from = positionOffset(lines, focus.start);
|
|
461
|
+
const to = positionOffset(lines, focus.end);
|
|
462
|
+
this.editor.setText(current.slice(0, from) + current.slice(to));
|
|
463
|
+
this.#setCursor(positionAtOffset(this.editor.getText(), from));
|
|
464
|
+
this.#redoStack = [];
|
|
465
|
+
this.#clearSelection(false);
|
|
466
|
+
this.#requestRender();
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
#redo() {
|
|
470
|
+
const snapshot = this.#redoStack.pop();
|
|
471
|
+
if (snapshot === undefined)
|
|
472
|
+
return;
|
|
473
|
+
this.editor.setText(snapshot.text);
|
|
474
|
+
this.#setCursor(snapshot.cursor);
|
|
475
|
+
this.#clearSelection(false);
|
|
476
|
+
this.#requestRender();
|
|
477
|
+
}
|
|
478
|
+
#collapseCopiedSelection() {
|
|
479
|
+
const selection = this.#activeRange();
|
|
480
|
+
if (selection === undefined)
|
|
481
|
+
return;
|
|
482
|
+
const line = editorState(this.editor).lines[selection.end.line] ?? "";
|
|
483
|
+
let column = selection.end.col;
|
|
484
|
+
while (true) {
|
|
485
|
+
const adjacent = this.options.atomicRanges(line).find(range => range.start === column);
|
|
486
|
+
if (adjacent === undefined)
|
|
487
|
+
break;
|
|
488
|
+
column = adjacent.end;
|
|
489
|
+
}
|
|
490
|
+
this.#setCursor({ line: selection.end.line, col: column });
|
|
491
|
+
this.#clearSelection(false);
|
|
492
|
+
this.#requestRender();
|
|
493
|
+
}
|
|
494
|
+
#selectedText() {
|
|
495
|
+
const selection = this.#activeRange();
|
|
496
|
+
if (selection === undefined)
|
|
497
|
+
return "";
|
|
498
|
+
const current = this.editor.getText();
|
|
499
|
+
const lines = editorState(this.editor).lines;
|
|
500
|
+
return current.slice(positionOffset(lines, selection.start), positionOffset(lines, selection.end));
|
|
501
|
+
}
|
|
502
|
+
#activeRange() {
|
|
503
|
+
return this.#orderedSelection() ?? this.#atomicFocus();
|
|
504
|
+
}
|
|
505
|
+
#atomicFocus() {
|
|
506
|
+
if (this.#orderedSelection() !== undefined)
|
|
507
|
+
return undefined;
|
|
508
|
+
const state = editorState(this.editor);
|
|
509
|
+
const line = state.lines[state.cursorLine] ?? "";
|
|
510
|
+
const range = this.options.atomicRanges(line).find(candidate => candidate.start === state.cursorCol);
|
|
511
|
+
return range === undefined ? undefined : {
|
|
512
|
+
start: { line: state.cursorLine, col: range.start },
|
|
513
|
+
end: { line: state.cursorLine, col: range.end },
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
#orderedSelection() {
|
|
517
|
+
const selection = this.#selection;
|
|
518
|
+
if (selection === undefined || samePosition(selection.anchor, selection.head))
|
|
519
|
+
return undefined;
|
|
520
|
+
return positionBefore(selection.anchor, selection.head)
|
|
521
|
+
? { start: selection.anchor, end: selection.head }
|
|
522
|
+
: { start: selection.head, end: selection.anchor };
|
|
523
|
+
}
|
|
524
|
+
#clearSelection(render = true) {
|
|
525
|
+
if (this.#selection === undefined && !this.#pointerSelecting)
|
|
526
|
+
return;
|
|
527
|
+
this.#selection = undefined;
|
|
528
|
+
this.#pointerSelecting = false;
|
|
529
|
+
this.#selectionRevision += 1;
|
|
530
|
+
if (render)
|
|
531
|
+
this.#requestRender();
|
|
532
|
+
}
|
|
533
|
+
#cursor() {
|
|
534
|
+
return this.editor.getCursor();
|
|
535
|
+
}
|
|
536
|
+
#setCursor(position) {
|
|
537
|
+
const state = editorState(this.editor);
|
|
538
|
+
const line = clamp(position.line, 0, Math.max(0, state.lines.length - 1));
|
|
539
|
+
state.cursorLine = line;
|
|
540
|
+
state.cursorCol = clamp(position.col, 0, (state.lines[line] ?? "").length);
|
|
541
|
+
}
|
|
542
|
+
#moveCursor(delta) {
|
|
543
|
+
const state = editorState(this.editor);
|
|
544
|
+
const line = state.lines[state.cursorLine] ?? "";
|
|
545
|
+
const atomic = this.options.atomicRanges(line).find(range => delta < 0
|
|
546
|
+
? state.cursorCol > range.start && state.cursorCol <= range.end
|
|
547
|
+
: state.cursorCol >= range.start && state.cursorCol < range.end);
|
|
548
|
+
if (atomic !== undefined) {
|
|
549
|
+
state.cursorCol = delta < 0 ? atomic.start : atomic.end;
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (delta < 0) {
|
|
553
|
+
if (state.cursorCol > 0) {
|
|
554
|
+
const segments = [...GRAPHEMES.segment(line.slice(0, state.cursorCol))];
|
|
555
|
+
state.cursorCol -= segments.at(-1)?.segment.length ?? 1;
|
|
556
|
+
}
|
|
557
|
+
else if (state.cursorLine > 0) {
|
|
558
|
+
state.cursorLine -= 1;
|
|
559
|
+
state.cursorCol = (state.lines[state.cursorLine] ?? "").length;
|
|
560
|
+
}
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (state.cursorCol < line.length) {
|
|
564
|
+
state.cursorCol += [...GRAPHEMES.segment(line.slice(state.cursorCol))][0]?.segment.length ?? 1;
|
|
565
|
+
}
|
|
566
|
+
else if (state.cursorLine < state.lines.length - 1) {
|
|
567
|
+
state.cursorLine += 1;
|
|
568
|
+
state.cursorCol = 0;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
#atomicRangeAt(position) {
|
|
572
|
+
const line = editorState(this.editor).lines[position.line] ?? "";
|
|
573
|
+
return this.options.atomicRanges(line).find(range => position.col >= range.start && position.col < range.end);
|
|
574
|
+
}
|
|
575
|
+
#moveOntoPreviousSeparator() {
|
|
576
|
+
const cursor = this.#cursor();
|
|
577
|
+
if (cursor.col === 0)
|
|
578
|
+
return;
|
|
579
|
+
const line = editorState(this.editor).lines[cursor.line] ?? "";
|
|
580
|
+
const previous = [...GRAPHEMES.segment(line.slice(0, cursor.col))].at(-1);
|
|
581
|
+
if (previous !== undefined && /^\s+$/u.test(previous.segment))
|
|
582
|
+
this.#moveCursor(-1);
|
|
583
|
+
}
|
|
584
|
+
#snapshot() {
|
|
585
|
+
return { text: this.editor.getText(), cursor: this.#cursor() };
|
|
586
|
+
}
|
|
587
|
+
#requestRender() {
|
|
588
|
+
this.options.requestRender();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function installAtomicSegmentation(editor, rangesForText) {
|
|
592
|
+
if (Reflect.get(editor, ATOMIC_SEGMENTATION) === true)
|
|
593
|
+
return;
|
|
594
|
+
const originalValue = Reflect.get(editor, "segment");
|
|
595
|
+
if (typeof originalValue !== "function")
|
|
596
|
+
return;
|
|
597
|
+
const original = originalValue.bind(editor);
|
|
598
|
+
Reflect.set(editor, "segment", (text, mode) => {
|
|
599
|
+
const segments = [...original(text, mode)].filter(isEditorSegment);
|
|
600
|
+
const ranges = rangesForText(text);
|
|
601
|
+
if (ranges.length === 0)
|
|
602
|
+
return segments;
|
|
603
|
+
const merged = [];
|
|
604
|
+
let rangeIndex = 0;
|
|
605
|
+
for (const segment of segments) {
|
|
606
|
+
while ((ranges[rangeIndex]?.end ?? Number.POSITIVE_INFINITY) <= segment.index)
|
|
607
|
+
rangeIndex += 1;
|
|
608
|
+
const range = ranges[rangeIndex];
|
|
609
|
+
if (range !== undefined && segment.index >= range.start && segment.index < range.end) {
|
|
610
|
+
if (segment.index === range.start) {
|
|
611
|
+
merged.push({
|
|
612
|
+
segment: text.slice(range.start, range.end).replaceAll(" ", ATOMIC_SPACE_SENTINEL),
|
|
613
|
+
index: range.start,
|
|
614
|
+
input: text,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
merged.push(segment);
|
|
620
|
+
}
|
|
621
|
+
return merged;
|
|
622
|
+
});
|
|
623
|
+
Reflect.set(editor, ATOMIC_SEGMENTATION, true);
|
|
624
|
+
}
|
|
625
|
+
function isEditorSegment(value) {
|
|
626
|
+
if (typeof value !== "object" || value === null)
|
|
627
|
+
return false;
|
|
628
|
+
const segment = Reflect.get(value, "segment");
|
|
629
|
+
const index = Reflect.get(value, "index");
|
|
630
|
+
const input = Reflect.get(value, "input");
|
|
631
|
+
return typeof segment === "string" && typeof index === "number" && typeof input === "string";
|
|
632
|
+
}
|
|
633
|
+
function bracketedPasteContent(data) {
|
|
634
|
+
const start = data.indexOf("\u001b[200~");
|
|
635
|
+
const end = data.indexOf("\u001b[201~", start + 6);
|
|
636
|
+
return start < 0 || end < 0 ? undefined : data.slice(start + 6, end);
|
|
637
|
+
}
|
|
638
|
+
function insertedText(data) {
|
|
639
|
+
if (!data || data.includes("\u001b[200~"))
|
|
640
|
+
return undefined;
|
|
641
|
+
const kitty = decodeKittyPrintable(data);
|
|
642
|
+
if (kitty !== undefined)
|
|
643
|
+
return kitty;
|
|
644
|
+
return data.charCodeAt(0) >= 32 ? data : undefined;
|
|
645
|
+
}
|
|
646
|
+
function indexAtDisplayWidth(text, target) {
|
|
647
|
+
if (target <= 0)
|
|
648
|
+
return 0;
|
|
649
|
+
let width = 0;
|
|
650
|
+
let index = 0;
|
|
651
|
+
for (const { segment } of GRAPHEMES.segment(text)) {
|
|
652
|
+
const next = width + visibleWidth(segment);
|
|
653
|
+
if (next > target)
|
|
654
|
+
break;
|
|
655
|
+
width = next;
|
|
656
|
+
index += segment.length;
|
|
657
|
+
}
|
|
658
|
+
return index;
|
|
659
|
+
}
|
|
660
|
+
function charClass(value) {
|
|
661
|
+
if (/\s/u.test(value))
|
|
662
|
+
return 0;
|
|
663
|
+
if (/[\p{L}\p{N}_]/u.test(value))
|
|
664
|
+
return 1;
|
|
665
|
+
return 2;
|
|
666
|
+
}
|
|
667
|
+
function positionBefore(left, right) {
|
|
668
|
+
return left.line < right.line || (left.line === right.line && left.col <= right.col);
|
|
669
|
+
}
|
|
670
|
+
function samePosition(left, right) {
|
|
671
|
+
return left.line === right.line && left.col === right.col;
|
|
672
|
+
}
|
|
673
|
+
function sameSnapshot(left, right) {
|
|
674
|
+
return left.text === right.text && samePosition(left.cursor, right.cursor);
|
|
675
|
+
}
|
|
676
|
+
function editorState(editor) {
|
|
677
|
+
const value = Object.getOwnPropertyDescriptor(editor, "state")?.value;
|
|
678
|
+
if (!isEditorState(value))
|
|
679
|
+
throw new Error("Pi editor state is unavailable");
|
|
680
|
+
return value;
|
|
681
|
+
}
|
|
682
|
+
function isEditorState(value) {
|
|
683
|
+
if (typeof value !== "object" || value === null)
|
|
684
|
+
return false;
|
|
685
|
+
const lines = Object.getOwnPropertyDescriptor(value, "lines")?.value;
|
|
686
|
+
const cursorLine = Object.getOwnPropertyDescriptor(value, "cursorLine")?.value;
|
|
687
|
+
const cursorCol = Object.getOwnPropertyDescriptor(value, "cursorCol")?.value;
|
|
688
|
+
return Array.isArray(lines) && lines.every(line => typeof line === "string")
|
|
689
|
+
&& typeof cursorLine === "number" && typeof cursorCol === "number";
|
|
690
|
+
}
|
|
691
|
+
function numericProperty(target, key) {
|
|
692
|
+
const value = Object.getOwnPropertyDescriptor(target, key)?.value;
|
|
693
|
+
return typeof value === "number" ? value : 0;
|
|
694
|
+
}
|
|
695
|
+
function editorVisualLineMap(editor, width) {
|
|
696
|
+
const builder = Reflect.get(editor, "buildVisualLineMap");
|
|
697
|
+
if (typeof builder !== "function")
|
|
698
|
+
return undefined;
|
|
699
|
+
const value = builder.call(editor, width);
|
|
700
|
+
if (!Array.isArray(value) || !value.every(isVisualLine))
|
|
701
|
+
return undefined;
|
|
702
|
+
return value;
|
|
703
|
+
}
|
|
704
|
+
function isVisualLine(value) {
|
|
705
|
+
if (typeof value !== "object" || value === null)
|
|
706
|
+
return false;
|
|
707
|
+
return typeof Reflect.get(value, "logicalLine") === "number"
|
|
708
|
+
&& typeof Reflect.get(value, "startCol") === "number"
|
|
709
|
+
&& typeof Reflect.get(value, "length") === "number";
|
|
710
|
+
}
|
|
711
|
+
function buildVisualLineMap(lines, width) {
|
|
712
|
+
const visualLines = [];
|
|
713
|
+
for (let logicalLine = 0; logicalLine < lines.length; logicalLine += 1) {
|
|
714
|
+
const line = lines[logicalLine] ?? "";
|
|
715
|
+
if (line.length === 0) {
|
|
716
|
+
visualLines.push({ logicalLine, startCol: 0, length: 0 });
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
for (const chunk of wrapLine(line, width)) {
|
|
720
|
+
visualLines.push({ logicalLine, startCol: chunk.start, length: chunk.end - chunk.start });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return visualLines;
|
|
724
|
+
}
|
|
725
|
+
function wrapLine(line, width) {
|
|
726
|
+
if (visibleWidth(line) <= width)
|
|
727
|
+
return [{ start: 0, end: line.length }];
|
|
728
|
+
const segments = [...GRAPHEMES.segment(line)];
|
|
729
|
+
const chunks = [];
|
|
730
|
+
let start = 0;
|
|
731
|
+
let currentWidth = 0;
|
|
732
|
+
let wrapAt = -1;
|
|
733
|
+
let widthAtWrap = 0;
|
|
734
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
735
|
+
const segment = segments[index];
|
|
736
|
+
if (segment === undefined)
|
|
737
|
+
continue;
|
|
738
|
+
const segmentWidth = visibleWidth(segment.segment);
|
|
739
|
+
if (currentWidth + segmentWidth > width) {
|
|
740
|
+
if (wrapAt > start && currentWidth - widthAtWrap + segmentWidth <= width) {
|
|
741
|
+
chunks.push({ start, end: wrapAt });
|
|
742
|
+
start = wrapAt;
|
|
743
|
+
currentWidth -= widthAtWrap;
|
|
744
|
+
}
|
|
745
|
+
else if (start < segment.index) {
|
|
746
|
+
chunks.push({ start, end: segment.index });
|
|
747
|
+
start = segment.index;
|
|
748
|
+
currentWidth = 0;
|
|
749
|
+
}
|
|
750
|
+
wrapAt = -1;
|
|
751
|
+
}
|
|
752
|
+
currentWidth += segmentWidth;
|
|
753
|
+
const next = segments[index + 1];
|
|
754
|
+
if (/\s/u.test(segment.segment) && next !== undefined && !/\s/u.test(next.segment)) {
|
|
755
|
+
wrapAt = next.index;
|
|
756
|
+
widthAtWrap = currentWidth;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
chunks.push({ start, end: line.length });
|
|
760
|
+
return chunks;
|
|
761
|
+
}
|
|
762
|
+
function positionOffset(lines, position) {
|
|
763
|
+
let offset = 0;
|
|
764
|
+
for (let line = 0; line < position.line; line += 1)
|
|
765
|
+
offset += (lines[line] ?? "").length + 1;
|
|
766
|
+
return offset + position.col;
|
|
767
|
+
}
|
|
768
|
+
function positionAtOffset(text, requestedOffset) {
|
|
769
|
+
const offset = clamp(requestedOffset, 0, text.length);
|
|
770
|
+
const before = text.slice(0, offset).split("\n");
|
|
771
|
+
return { line: before.length - 1, col: (before.at(-1) ?? "").length };
|
|
772
|
+
}
|
|
773
|
+
function normalizeInsertedText(text) {
|
|
774
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ");
|
|
775
|
+
}
|
|
776
|
+
function clamp(value, minimum, maximum) {
|
|
777
|
+
return Math.max(minimum, Math.min(maximum, value));
|
|
778
|
+
}
|