laohuang 0.3.2 → 0.4.1
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 +142 -16
- package/dist/agent.d.ts +166 -0
- package/dist/agent.js +858 -0
- package/dist/agent.js.map +1 -0
- package/dist/bash-runner.d.ts +79 -0
- package/dist/bash-runner.js +464 -0
- package/dist/bash-runner.js.map +1 -0
- package/dist/cancellation.d.ts +36 -0
- package/dist/cancellation.js +123 -0
- package/dist/cancellation.js.map +1 -0
- package/dist/cli.d.ts +117 -0
- package/dist/cli.js +1307 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.js +17 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +123 -0
- package/dist/commands.js +660 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +183 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +12 -0
- package/dist/credentials.js +102 -0
- package/dist/credentials.js.map +1 -0
- package/dist/events.d.ts +288 -0
- package/dist/events.js +838 -0
- package/dist/events.js.map +1 -0
- package/dist/model-adapter.d.ts +138 -0
- package/dist/model-adapter.js +244 -0
- package/dist/model-adapter.js.map +1 -0
- package/dist/model-selection.d.ts +67 -0
- package/dist/model-selection.js +148 -0
- package/dist/model-selection.js.map +1 -0
- package/dist/model-stream.d.ts +128 -0
- package/dist/model-stream.js +582 -0
- package/dist/model-stream.js.map +1 -0
- package/dist/project-instructions.d.ts +99 -0
- package/dist/project-instructions.js +348 -0
- package/dist/project-instructions.js.map +1 -0
- package/dist/providers.d.ts +9 -0
- package/dist/providers.js +26 -0
- package/dist/providers.js.map +1 -0
- package/dist/routing.d.ts +170 -0
- package/dist/routing.js +669 -0
- package/dist/routing.js.map +1 -0
- package/dist/semantic-classifier.d.ts +66 -0
- package/dist/semantic-classifier.js +86 -0
- package/dist/semantic-classifier.js.map +1 -0
- package/dist/session.d.ts +162 -0
- package/dist/session.js +871 -0
- package/dist/session.js.map +1 -0
- package/dist/system-prompt.d.ts +16 -0
- package/dist/system-prompt.js +42 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/terminal/editor.d.ts +161 -0
- package/dist/terminal/editor.js +1060 -0
- package/dist/terminal/editor.js.map +1 -0
- package/dist/terminal/input.d.ts +61 -0
- package/dist/terminal/input.js +276 -0
- package/dist/terminal/input.js.map +1 -0
- package/dist/terminal/markdown.d.ts +20 -0
- package/dist/terminal/markdown.js +621 -0
- package/dist/terminal/markdown.js.map +1 -0
- package/dist/terminal/screen.d.ts +66 -0
- package/dist/terminal/screen.js +624 -0
- package/dist/terminal/screen.js.map +1 -0
- package/dist/terminal/theme.d.ts +23 -0
- package/dist/terminal/theme.js +101 -0
- package/dist/terminal/theme.js.map +1 -0
- package/dist/terminal/ui.d.ts +285 -0
- package/dist/terminal/ui.js +1815 -0
- package/dist/terminal/ui.js.map +1 -0
- package/dist/tools.d.ts +95 -0
- package/dist/tools.js +444 -0
- package/dist/tools.js.map +1 -0
- package/dist/ui-state.d.ts +51 -0
- package/dist/ui-state.js +194 -0
- package/dist/ui-state.js.map +1 -0
- package/dist/web.d.ts +56 -0
- package/dist/web.js +247 -0
- package/dist/web.js.map +1 -0
- package/package.json +24 -21
- package/bin/laohuang.js +0 -10
- package/lib/launcher.js +0 -133
- package/vendor/laohuangcode-0.3.2-py3-none-any.whl +0 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw terminal input decoding and prompt-toolkit-independent editor state.
|
|
3
|
+
*
|
|
4
|
+
* Port of terminal_editor.py: a byte-level stdin buffer (bracketed paste,
|
|
5
|
+
* split escape sequences, kitty keyboard protocol), a terminal-negotiation
|
|
6
|
+
* filter, an incremental raw-byte decoder producing editor actions, and the
|
|
7
|
+
* text/history/completion state machine those actions drive.
|
|
8
|
+
*/
|
|
9
|
+
import { charCellWidth } from "./screen.js";
|
|
10
|
+
// The wcwidth tables and charCellWidth are owned by terminal/screen.ts;
|
|
11
|
+
// re-exported here for existing consumers of this module.
|
|
12
|
+
export { charCellWidth } from "./screen.js";
|
|
13
|
+
/** Editor actions produced by {@link RawInputDecoder}. */
|
|
14
|
+
export const InputActionKind = {
|
|
15
|
+
Insert: "insert",
|
|
16
|
+
Submit: "submit",
|
|
17
|
+
Newline: "newline",
|
|
18
|
+
Complete: "complete",
|
|
19
|
+
HistoryUp: "history_up",
|
|
20
|
+
HistoryDown: "history_down",
|
|
21
|
+
CursorLeft: "cursor_left",
|
|
22
|
+
CursorRight: "cursor_right",
|
|
23
|
+
Backspace: "backspace",
|
|
24
|
+
Dismiss: "dismiss",
|
|
25
|
+
Cancel: "cancel",
|
|
26
|
+
Eof: "eof",
|
|
27
|
+
};
|
|
28
|
+
/** Events emitted by {@link StdinBuffer}. */
|
|
29
|
+
export const BufferedInputKind = {
|
|
30
|
+
Sequence: "sequence",
|
|
31
|
+
Paste: "paste",
|
|
32
|
+
};
|
|
33
|
+
export function inputAction(kind, text = "") {
|
|
34
|
+
return { kind, text };
|
|
35
|
+
}
|
|
36
|
+
export function editorEffect(partial = {}) {
|
|
37
|
+
return {
|
|
38
|
+
submit: null,
|
|
39
|
+
cancelRequested: false,
|
|
40
|
+
exitRequested: false,
|
|
41
|
+
notice: null,
|
|
42
|
+
...partial,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const ESC = 0x1b;
|
|
46
|
+
const ESCAPE = Buffer.from([ESC]);
|
|
47
|
+
const BRACKETED_PASTE_START = Buffer.from("\x1b[200~", "latin1");
|
|
48
|
+
const BRACKETED_PASTE_END = Buffer.from("\x1b[201~", "latin1");
|
|
49
|
+
const APPLE_TERMINAL_SHIFT_ENTER_SEQUENCE = "\x1b[13;2u";
|
|
50
|
+
const CSI_PREFIX = Buffer.from("\x1b[", "latin1");
|
|
51
|
+
const NEGOTIATION_QUERY_PREFIX = Buffer.from("\x1b[?", "latin1");
|
|
52
|
+
function startsWith(buffer, prefix, offset = 0) {
|
|
53
|
+
if (buffer.length - offset < prefix.length) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
for (let index = 0; index < prefix.length; index += 1) {
|
|
57
|
+
if (buffer[offset + index] !== prefix[index]) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Buffer stdin bytes and emit complete Pi-compatible input events.
|
|
65
|
+
*
|
|
66
|
+
* Splits the byte stream into whole escape sequences / UTF-8 characters,
|
|
67
|
+
* coalesces bracketed pastes, and suppresses the raw duplicate byte that
|
|
68
|
+
* kitty terminals send after an unmodified printable key event.
|
|
69
|
+
*/
|
|
70
|
+
export class StdinBuffer {
|
|
71
|
+
buffer = Buffer.alloc(0);
|
|
72
|
+
pasteMode = false;
|
|
73
|
+
pasteBuffer = Buffer.alloc(0);
|
|
74
|
+
pendingKittyPrintableCodepoint = null;
|
|
75
|
+
feed(data) {
|
|
76
|
+
if (data.length === 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
let chunk = Buffer.from(data);
|
|
80
|
+
// High-bit meta bytes (e.g. 0xE1) arrive as ESC + (byte - 128).
|
|
81
|
+
if (chunk.length === 1 && chunk[0] > 127) {
|
|
82
|
+
chunk = Buffer.concat([ESCAPE, Buffer.from([chunk[0] - 128])]);
|
|
83
|
+
}
|
|
84
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
85
|
+
const events = [];
|
|
86
|
+
this.processBuffer(events);
|
|
87
|
+
return events;
|
|
88
|
+
}
|
|
89
|
+
flush() {
|
|
90
|
+
if (this.pasteMode || this.buffer.length === 0) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const sequence = this.buffer;
|
|
94
|
+
this.buffer = Buffer.alloc(0);
|
|
95
|
+
this.pendingKittyPrintableCodepoint = null;
|
|
96
|
+
return [{ kind: BufferedInputKind.Sequence, data: sequence }];
|
|
97
|
+
}
|
|
98
|
+
clear() {
|
|
99
|
+
this.buffer = Buffer.alloc(0);
|
|
100
|
+
this.pasteBuffer = Buffer.alloc(0);
|
|
101
|
+
this.pasteMode = false;
|
|
102
|
+
this.pendingKittyPrintableCodepoint = null;
|
|
103
|
+
}
|
|
104
|
+
processBuffer(events) {
|
|
105
|
+
if (this.pasteMode) {
|
|
106
|
+
this.pasteBuffer = Buffer.concat([this.pasteBuffer, this.buffer]);
|
|
107
|
+
this.buffer = Buffer.alloc(0);
|
|
108
|
+
this.emitFinishedPaste(events);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const startIndex = this.buffer.indexOf(BRACKETED_PASTE_START);
|
|
112
|
+
if (startIndex !== -1) {
|
|
113
|
+
const beforePaste = this.buffer.subarray(0, startIndex);
|
|
114
|
+
const { sequences } = extractCompleteSequences(beforePaste);
|
|
115
|
+
for (const sequence of sequences) {
|
|
116
|
+
this.emitSequence(events, sequence);
|
|
117
|
+
}
|
|
118
|
+
const afterStart = this.buffer.subarray(startIndex + BRACKETED_PASTE_START.length);
|
|
119
|
+
this.buffer = Buffer.alloc(0);
|
|
120
|
+
this.pasteMode = true;
|
|
121
|
+
this.pasteBuffer = Buffer.concat([this.pasteBuffer, afterStart]);
|
|
122
|
+
this.pendingKittyPrintableCodepoint = null;
|
|
123
|
+
this.emitFinishedPaste(events);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const { sequences, remainder } = extractCompleteSequences(this.buffer);
|
|
127
|
+
this.buffer = remainder;
|
|
128
|
+
for (const sequence of sequences) {
|
|
129
|
+
this.emitSequence(events, sequence);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
emitFinishedPaste(events) {
|
|
133
|
+
const endIndex = this.pasteBuffer.indexOf(BRACKETED_PASTE_END);
|
|
134
|
+
if (endIndex === -1) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const pasted = this.pasteBuffer.subarray(0, endIndex);
|
|
138
|
+
const remaining = this.pasteBuffer.subarray(endIndex + BRACKETED_PASTE_END.length);
|
|
139
|
+
this.pasteBuffer = Buffer.alloc(0);
|
|
140
|
+
this.pasteMode = false;
|
|
141
|
+
this.pendingKittyPrintableCodepoint = null;
|
|
142
|
+
events.push({ kind: BufferedInputKind.Paste, data: Buffer.from(pasted) });
|
|
143
|
+
if (remaining.length > 0) {
|
|
144
|
+
this.buffer = Buffer.concat([this.buffer, remaining]);
|
|
145
|
+
this.processBuffer(events);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
emitSequence(events, sequence) {
|
|
149
|
+
const rawCodepoint = singleCodepoint(sequence);
|
|
150
|
+
if (rawCodepoint !== null &&
|
|
151
|
+
rawCodepoint === this.pendingKittyPrintableCodepoint) {
|
|
152
|
+
this.pendingKittyPrintableCodepoint = null;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.pendingKittyPrintableCodepoint =
|
|
156
|
+
parseUnmodifiedKittyPrintableCodepoint(sequence);
|
|
157
|
+
events.push({
|
|
158
|
+
kind: BufferedInputKind.Sequence,
|
|
159
|
+
data: Buffer.from(sequence),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const KITTY_FLAGS_RE = /^\x1b\[\?(\d+)u$/;
|
|
164
|
+
const DEVICE_ATTRIBUTES_RE = /^\x1b\[\?[\d;]*c$/;
|
|
165
|
+
const NEGOTIATION_PREFIX_RE = /^\x1b\[\?[\d;]*$/;
|
|
166
|
+
const ABANDONED_NEGOTIATION_TAIL_RE = /^\x1b\[\?[\d;]*([A-Za-z])$/;
|
|
167
|
+
/**
|
|
168
|
+
* Filter terminal negotiation responses before editor decoding.
|
|
169
|
+
*
|
|
170
|
+
* Kitty keyboard flags and device-attribute answers are consumed here; an
|
|
171
|
+
* abandoned negotiation prefix is dropped once real input follows. Byte
|
|
172
|
+
* patterns are matched on latin1-decoded strings (a 1:1 byte mapping).
|
|
173
|
+
*/
|
|
174
|
+
export class TerminalInputFilter {
|
|
175
|
+
pendingNegotiationPrefix = "";
|
|
176
|
+
isAppleTerminal;
|
|
177
|
+
shiftPressed;
|
|
178
|
+
enableModifyOtherKeys;
|
|
179
|
+
disableModifyOtherKeys;
|
|
180
|
+
kittyProtocolActive = false;
|
|
181
|
+
constructor(options = {}) {
|
|
182
|
+
this.isAppleTerminal = options.isAppleTerminal ?? isAppleTerminalSession;
|
|
183
|
+
this.shiftPressed = options.shiftPressed ?? (() => false);
|
|
184
|
+
this.enableModifyOtherKeys = options.enableModifyOtherKeys ?? (() => { });
|
|
185
|
+
this.disableModifyOtherKeys = options.disableModifyOtherKeys ?? (() => { });
|
|
186
|
+
}
|
|
187
|
+
feed(sequence) {
|
|
188
|
+
const text = Buffer.from(sequence).toString("latin1");
|
|
189
|
+
return this.feedText(text).map((item) => Buffer.from(item, "latin1"));
|
|
190
|
+
}
|
|
191
|
+
flush() {
|
|
192
|
+
if (!this.pendingNegotiationPrefix) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const pending = this.pendingNegotiationPrefix;
|
|
196
|
+
this.pendingNegotiationPrefix = "";
|
|
197
|
+
return [Buffer.from(this.normalizePlatformInput(pending), "latin1")];
|
|
198
|
+
}
|
|
199
|
+
clear() {
|
|
200
|
+
this.pendingNegotiationPrefix = "";
|
|
201
|
+
}
|
|
202
|
+
feedText(sequence) {
|
|
203
|
+
if (this.pendingNegotiationPrefix) {
|
|
204
|
+
const combined = this.pendingNegotiationPrefix + sequence;
|
|
205
|
+
if (this.handleNegotiation(combined)) {
|
|
206
|
+
this.pendingNegotiationPrefix = "";
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
if (this.isNegotiationPrefix(combined)) {
|
|
210
|
+
this.pendingNegotiationPrefix = combined;
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
this.pendingNegotiationPrefix = "";
|
|
214
|
+
return this.feedText(sequence);
|
|
215
|
+
}
|
|
216
|
+
const abandonedTail = this.abandonedNegotiationTail(sequence);
|
|
217
|
+
if (abandonedTail !== null) {
|
|
218
|
+
return this.feedText(abandonedTail);
|
|
219
|
+
}
|
|
220
|
+
if (this.handleNegotiation(sequence)) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
if (this.isNegotiationPrefix(sequence)) {
|
|
224
|
+
this.pendingNegotiationPrefix = sequence;
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
return [this.normalizePlatformInput(sequence)];
|
|
228
|
+
}
|
|
229
|
+
handleNegotiation(sequence) {
|
|
230
|
+
const flags = KITTY_FLAGS_RE.exec(sequence);
|
|
231
|
+
if (flags !== null) {
|
|
232
|
+
const value = Number.parseInt(flags[1], 10);
|
|
233
|
+
if (value) {
|
|
234
|
+
this.disableModifyOtherKeys();
|
|
235
|
+
this.kittyProtocolActive = true;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
this.enableModifyOtherKeys();
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (DEVICE_ATTRIBUTES_RE.test(sequence)) {
|
|
243
|
+
if (!this.kittyProtocolActive) {
|
|
244
|
+
this.enableModifyOtherKeys();
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
isNegotiationPrefix(sequence) {
|
|
251
|
+
return sequence === "\x1b[" || NEGOTIATION_PREFIX_RE.test(sequence);
|
|
252
|
+
}
|
|
253
|
+
abandonedNegotiationTail(sequence) {
|
|
254
|
+
const match = ABANDONED_NEGOTIATION_TAIL_RE.exec(sequence);
|
|
255
|
+
if (match === null || match[1] === "c" || match[1] === "u") {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
return match[1];
|
|
259
|
+
}
|
|
260
|
+
normalizePlatformInput(sequence) {
|
|
261
|
+
if (sequence === "\r" &&
|
|
262
|
+
this.isAppleTerminal() &&
|
|
263
|
+
this.shiftPressed()) {
|
|
264
|
+
return APPLE_TERMINAL_SHIFT_ENTER_SEQUENCE;
|
|
265
|
+
}
|
|
266
|
+
return sequence;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function isAppleTerminalSession() {
|
|
270
|
+
return (process.platform === "darwin" &&
|
|
271
|
+
process.env["TERM_PROGRAM"] === "Apple_Terminal");
|
|
272
|
+
}
|
|
273
|
+
const ESCAPE_ACTIONS = new Map([
|
|
274
|
+
["\x1b[A", InputActionKind.HistoryUp],
|
|
275
|
+
["\x1b[B", InputActionKind.HistoryDown],
|
|
276
|
+
["\x1b[C", InputActionKind.CursorRight],
|
|
277
|
+
["\x1b[D", InputActionKind.CursorLeft],
|
|
278
|
+
["\x1bOA", InputActionKind.HistoryUp],
|
|
279
|
+
["\x1bOB", InputActionKind.HistoryDown],
|
|
280
|
+
["\x1bOC", InputActionKind.CursorRight],
|
|
281
|
+
["\x1bOD", InputActionKind.CursorLeft],
|
|
282
|
+
]);
|
|
283
|
+
const CONTROL_ACTIONS = new Map([
|
|
284
|
+
[3, InputActionKind.Cancel],
|
|
285
|
+
[4, InputActionKind.Eof],
|
|
286
|
+
[9, InputActionKind.Complete],
|
|
287
|
+
[8, InputActionKind.Backspace],
|
|
288
|
+
[127, InputActionKind.Backspace],
|
|
289
|
+
]);
|
|
290
|
+
const ESCAPE_INCOMPLETE = { status: "incomplete" };
|
|
291
|
+
const ESCAPE_CONSUMED = { status: "consumed" };
|
|
292
|
+
/** Incrementally convert raw bytes into editor actions. */
|
|
293
|
+
export class RawInputDecoder {
|
|
294
|
+
buffer = Buffer.alloc(0);
|
|
295
|
+
feed(data) {
|
|
296
|
+
this.buffer = Buffer.concat([this.buffer, Buffer.from(data)]);
|
|
297
|
+
const actions = [];
|
|
298
|
+
while (this.buffer.length > 0) {
|
|
299
|
+
if (this.buffer[0] === ESC) {
|
|
300
|
+
const consumption = this.consumeEscape();
|
|
301
|
+
if (consumption.status === "incomplete") {
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
if (consumption.status === "action") {
|
|
305
|
+
actions.push(consumption.action);
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const byte = this.buffer[0];
|
|
310
|
+
if (byte === 10 || byte === 13) {
|
|
311
|
+
this.buffer = this.buffer.subarray(1);
|
|
312
|
+
actions.push(inputAction(InputActionKind.Submit));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const kind = CONTROL_ACTIONS.get(byte);
|
|
316
|
+
if (kind !== undefined) {
|
|
317
|
+
this.buffer = this.buffer.subarray(1);
|
|
318
|
+
actions.push(inputAction(kind));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (byte < 32) {
|
|
322
|
+
// Ignore every remaining C0 control rather than leaving a
|
|
323
|
+
// zero-length printable chunk at the front of the buffer.
|
|
324
|
+
this.buffer = this.buffer.subarray(1);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
let end = 0;
|
|
328
|
+
while (end < this.buffer.length &&
|
|
329
|
+
this.buffer[end] >= 32 &&
|
|
330
|
+
this.buffer[end] !== 127) {
|
|
331
|
+
end += 1;
|
|
332
|
+
}
|
|
333
|
+
const chunk = this.buffer.subarray(0, end);
|
|
334
|
+
const decoded = decodeUtf8Strict(chunk);
|
|
335
|
+
if (!decoded.ok) {
|
|
336
|
+
if (decoded.reason === "truncated") {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
this.buffer = this.buffer.subarray(Math.max(1, decoded.start + 1));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
this.buffer = this.buffer.subarray(end);
|
|
343
|
+
if (decoded.text) {
|
|
344
|
+
actions.push(inputAction(InputActionKind.Insert, decoded.text));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return actions;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Resolve bytes which remain after the raw-input escape timeout.
|
|
351
|
+
*
|
|
352
|
+
* The event loop calls this after a short no-input interval. This keeps
|
|
353
|
+
* split CSI sequences buffered, while a genuine standalone Escape can
|
|
354
|
+
* dismiss a completion overlay.
|
|
355
|
+
*/
|
|
356
|
+
flush() {
|
|
357
|
+
if (this.buffer.length === 1 && this.buffer[0] === ESC) {
|
|
358
|
+
this.buffer = Buffer.alloc(0);
|
|
359
|
+
return [inputAction(InputActionKind.Dismiss)];
|
|
360
|
+
}
|
|
361
|
+
if (this.buffer.length > 1 && this.buffer[0] === ESC && this.buffer[1] === 0x5b) {
|
|
362
|
+
this.buffer = Buffer.alloc(0);
|
|
363
|
+
}
|
|
364
|
+
return this.feed(Buffer.alloc(0));
|
|
365
|
+
}
|
|
366
|
+
consumeEscape() {
|
|
367
|
+
if (this.buffer.length === 1) {
|
|
368
|
+
return ESCAPE_INCOMPLETE;
|
|
369
|
+
}
|
|
370
|
+
const second = this.buffer[1];
|
|
371
|
+
if (second === 10 || second === 13) {
|
|
372
|
+
this.buffer = this.buffer.subarray(2);
|
|
373
|
+
return { status: "action", action: inputAction(InputActionKind.Newline) };
|
|
374
|
+
}
|
|
375
|
+
if (second === 0x5b /* [ */) {
|
|
376
|
+
let final = null;
|
|
377
|
+
for (let index = 2; index < this.buffer.length; index += 1) {
|
|
378
|
+
const byte = this.buffer[index];
|
|
379
|
+
if (byte >= 0x40 && byte <= 0x7e) {
|
|
380
|
+
final = index;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (final === null) {
|
|
385
|
+
return ESCAPE_INCOMPLETE;
|
|
386
|
+
}
|
|
387
|
+
const sequence = this.buffer.subarray(0, final + 1).toString("latin1");
|
|
388
|
+
if (isKittyRelease(sequence)) {
|
|
389
|
+
this.buffer = this.buffer.subarray(final + 1);
|
|
390
|
+
return ESCAPE_CONSUMED;
|
|
391
|
+
}
|
|
392
|
+
const arrow = decodeKittyArrowAction(sequence);
|
|
393
|
+
if (arrow !== null) {
|
|
394
|
+
this.buffer = this.buffer.subarray(final + 1);
|
|
395
|
+
return { status: "action", action: arrow };
|
|
396
|
+
}
|
|
397
|
+
const special = decodeSpecialEscapeAction(sequence);
|
|
398
|
+
if (special !== null) {
|
|
399
|
+
this.buffer = this.buffer.subarray(final + 1);
|
|
400
|
+
return { status: "action", action: special };
|
|
401
|
+
}
|
|
402
|
+
const printable = decodePrintableKey(sequence);
|
|
403
|
+
if (printable !== null) {
|
|
404
|
+
this.buffer = this.buffer.subarray(final + 1);
|
|
405
|
+
return {
|
|
406
|
+
status: "action",
|
|
407
|
+
action: inputAction(InputActionKind.Insert, printable),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const kind = ESCAPE_ACTIONS.get(sequence);
|
|
411
|
+
this.buffer = this.buffer.subarray(final + 1);
|
|
412
|
+
return kind !== undefined
|
|
413
|
+
? { status: "action", action: inputAction(kind) }
|
|
414
|
+
: ESCAPE_CONSUMED;
|
|
415
|
+
}
|
|
416
|
+
if (second === 0x4f /* O */ && this.buffer.length >= 3) {
|
|
417
|
+
const sequence = this.buffer.subarray(0, 3).toString("latin1");
|
|
418
|
+
const kind = ESCAPE_ACTIONS.get(sequence);
|
|
419
|
+
this.buffer = this.buffer.subarray(3);
|
|
420
|
+
return kind !== undefined
|
|
421
|
+
? { status: "action", action: inputAction(kind) }
|
|
422
|
+
: ESCAPE_CONSUMED;
|
|
423
|
+
}
|
|
424
|
+
// An unsupported Alt sequence has no editor meaning; retain its character.
|
|
425
|
+
this.buffer = this.buffer.subarray(1);
|
|
426
|
+
return ESCAPE_CONSUMED;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function extractCompleteSequences(buffer) {
|
|
430
|
+
const sequences = [];
|
|
431
|
+
let position = 0;
|
|
432
|
+
while (position < buffer.length) {
|
|
433
|
+
const remaining = buffer.subarray(position);
|
|
434
|
+
if (remaining[0] === ESC) {
|
|
435
|
+
if (startsWith(remaining, NEGOTIATION_QUERY_PREFIX)) {
|
|
436
|
+
const nestedEscape = remaining.indexOf(ESCAPE, 2);
|
|
437
|
+
if (nestedEscape !== -1) {
|
|
438
|
+
position += nestedEscape;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
let sequenceEnd = 1;
|
|
443
|
+
let broke = false;
|
|
444
|
+
while (sequenceEnd <= remaining.length) {
|
|
445
|
+
const candidate = remaining.subarray(0, sequenceEnd);
|
|
446
|
+
const status = completeSequenceStatus(candidate);
|
|
447
|
+
if (status === "complete") {
|
|
448
|
+
if (candidate.length === 2 && candidate[0] === ESC && candidate[1] === ESC) {
|
|
449
|
+
const nextByte = remaining[sequenceEnd];
|
|
450
|
+
if (nextByte !== undefined &&
|
|
451
|
+
(nextByte === 0x5b /* [ */ ||
|
|
452
|
+
nextByte === 0x5d /* ] */ ||
|
|
453
|
+
nextByte === 0x4f /* O */ ||
|
|
454
|
+
nextByte === 0x50 /* P */ ||
|
|
455
|
+
nextByte === 0x5f /* _ */)) {
|
|
456
|
+
sequences.push(Buffer.from(ESCAPE));
|
|
457
|
+
position += 1;
|
|
458
|
+
broke = true;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
sequences.push(Buffer.from(candidate));
|
|
463
|
+
position += sequenceEnd;
|
|
464
|
+
broke = true;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
if (status === "incomplete") {
|
|
468
|
+
sequenceEnd += 1;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
sequences.push(Buffer.from(candidate));
|
|
472
|
+
position += sequenceEnd;
|
|
473
|
+
broke = true;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
if (!broke && sequenceEnd > remaining.length) {
|
|
477
|
+
return { sequences, remainder: Buffer.from(remaining) };
|
|
478
|
+
}
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const nextEscape = remaining.indexOf(ESCAPE);
|
|
482
|
+
if (nextEscape === -1) {
|
|
483
|
+
const { sequences: plain, remainder } = extractPlainSequences(remaining);
|
|
484
|
+
sequences.push(...plain);
|
|
485
|
+
position = buffer.length;
|
|
486
|
+
if (remainder.length > 0) {
|
|
487
|
+
return { sequences, remainder };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (nextEscape === 0) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
const { sequences: plain, remainder } = extractPlainSequences(remaining.subarray(0, nextEscape));
|
|
495
|
+
sequences.push(...plain);
|
|
496
|
+
if (remainder.length > 0) {
|
|
497
|
+
return {
|
|
498
|
+
sequences,
|
|
499
|
+
remainder: Buffer.concat([remainder, remaining.subarray(nextEscape)]),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
position += nextEscape;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return { sequences, remainder: Buffer.alloc(0) };
|
|
506
|
+
}
|
|
507
|
+
function extractPlainSequences(data) {
|
|
508
|
+
const sequences = [];
|
|
509
|
+
let position = 0;
|
|
510
|
+
while (position < data.length) {
|
|
511
|
+
const length = utf8SequenceLength(data[position]);
|
|
512
|
+
const chunk = data.subarray(position, position + length);
|
|
513
|
+
if (chunk.length < length) {
|
|
514
|
+
return { sequences, remainder: Buffer.from(data.subarray(position)) };
|
|
515
|
+
}
|
|
516
|
+
if (!isValidUtf8(chunk)) {
|
|
517
|
+
sequences.push(Buffer.from(data.subarray(position, position + 1)));
|
|
518
|
+
position += 1;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
sequences.push(Buffer.from(chunk));
|
|
522
|
+
position += length;
|
|
523
|
+
}
|
|
524
|
+
return { sequences, remainder: Buffer.alloc(0) };
|
|
525
|
+
}
|
|
526
|
+
function utf8SequenceLength(firstByte) {
|
|
527
|
+
if (firstByte < 0x80) {
|
|
528
|
+
return 1;
|
|
529
|
+
}
|
|
530
|
+
if (firstByte >= 0xc0 && firstByte <= 0xdf) {
|
|
531
|
+
return 2;
|
|
532
|
+
}
|
|
533
|
+
if (firstByte >= 0xe0 && firstByte <= 0xef) {
|
|
534
|
+
return 3;
|
|
535
|
+
}
|
|
536
|
+
if (firstByte >= 0xf0 && firstByte <= 0xf7) {
|
|
537
|
+
return 4;
|
|
538
|
+
}
|
|
539
|
+
return 1;
|
|
540
|
+
}
|
|
541
|
+
function completeSequenceStatus(data) {
|
|
542
|
+
if (data[0] !== ESC) {
|
|
543
|
+
return "not-escape";
|
|
544
|
+
}
|
|
545
|
+
if (data.length === 1) {
|
|
546
|
+
return "incomplete";
|
|
547
|
+
}
|
|
548
|
+
const second = data[1];
|
|
549
|
+
if (second === 0x5b /* [ */ && data[2] === 0x4d /* M */) {
|
|
550
|
+
return data.length >= 6 ? "complete" : "incomplete";
|
|
551
|
+
}
|
|
552
|
+
if (second === 0x5b /* [ */) {
|
|
553
|
+
return completeCsiStatus(data);
|
|
554
|
+
}
|
|
555
|
+
if (second === 0x5d /* ] */) {
|
|
556
|
+
return endsWithEscapeBackslash(data) || data[data.length - 1] === 0x07
|
|
557
|
+
? "complete"
|
|
558
|
+
: "incomplete";
|
|
559
|
+
}
|
|
560
|
+
if (second === 0x50 /* P */ || second === 0x5f /* _ */) {
|
|
561
|
+
return endsWithEscapeBackslash(data) ? "complete" : "incomplete";
|
|
562
|
+
}
|
|
563
|
+
if (second === 0x4f /* O */) {
|
|
564
|
+
return data.length >= 3 ? "complete" : "incomplete";
|
|
565
|
+
}
|
|
566
|
+
return "complete";
|
|
567
|
+
}
|
|
568
|
+
function endsWithEscapeBackslash(data) {
|
|
569
|
+
return (data.length >= 2 &&
|
|
570
|
+
data[data.length - 2] === ESC &&
|
|
571
|
+
data[data.length - 1] === 0x5c /* \ */);
|
|
572
|
+
}
|
|
573
|
+
const SGR_MOUSE_RE = /^<\d+;\d+;\d+[Mm]$/;
|
|
574
|
+
function completeCsiStatus(data) {
|
|
575
|
+
if (!startsWith(data, CSI_PREFIX)) {
|
|
576
|
+
return "complete";
|
|
577
|
+
}
|
|
578
|
+
if (data.length < 3) {
|
|
579
|
+
return "incomplete";
|
|
580
|
+
}
|
|
581
|
+
const payload = data.subarray(2).toString("latin1");
|
|
582
|
+
const finalByte = payload.charCodeAt(payload.length - 1);
|
|
583
|
+
if (!(finalByte >= 0x40 && finalByte <= 0x7e)) {
|
|
584
|
+
return "incomplete";
|
|
585
|
+
}
|
|
586
|
+
if (payload.startsWith("<")) {
|
|
587
|
+
if (SGR_MOUSE_RE.test(payload)) {
|
|
588
|
+
return "complete";
|
|
589
|
+
}
|
|
590
|
+
if (finalByte === 0x4d /* M */ || finalByte === 0x6d /* m */) {
|
|
591
|
+
const parts = payload.slice(1, -1).split(";");
|
|
592
|
+
if (parts.length === 3 && parts.every((part) => /^\d+$/.test(part))) {
|
|
593
|
+
return "complete";
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return "incomplete";
|
|
597
|
+
}
|
|
598
|
+
return "complete";
|
|
599
|
+
}
|
|
600
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
601
|
+
function isValidUtf8(chunk) {
|
|
602
|
+
try {
|
|
603
|
+
utf8Decoder.decode(chunk);
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Strict UTF-8 decode mirroring CPython's error semantics: "truncated" for
|
|
612
|
+
* an incomplete sequence at the end of the chunk ("unexpected end of data"),
|
|
613
|
+
* otherwise "invalid" with `start` at the offending sequence's first byte.
|
|
614
|
+
*/
|
|
615
|
+
function decodeUtf8Strict(chunk) {
|
|
616
|
+
let index = 0;
|
|
617
|
+
while (index < chunk.length) {
|
|
618
|
+
const byte = chunk[index];
|
|
619
|
+
let length;
|
|
620
|
+
if (byte < 0x80) {
|
|
621
|
+
index += 1;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
else if (byte >= 0xc2 && byte <= 0xdf) {
|
|
625
|
+
length = 2;
|
|
626
|
+
}
|
|
627
|
+
else if (byte >= 0xe0 && byte <= 0xef) {
|
|
628
|
+
length = 3;
|
|
629
|
+
}
|
|
630
|
+
else if (byte >= 0xf0 && byte <= 0xf4) {
|
|
631
|
+
length = 4;
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
return { ok: false, reason: "invalid", start: index };
|
|
635
|
+
}
|
|
636
|
+
let truncated = false;
|
|
637
|
+
for (let offset = 1; offset < length; offset += 1) {
|
|
638
|
+
if (index + offset >= chunk.length) {
|
|
639
|
+
truncated = true;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
const continuation = chunk[index + offset];
|
|
643
|
+
if (continuation < 0x80 || continuation > 0xbf) {
|
|
644
|
+
return { ok: false, reason: "invalid", start: index };
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (truncated) {
|
|
648
|
+
return { ok: false, reason: "truncated" };
|
|
649
|
+
}
|
|
650
|
+
const secondByte = chunk[index + 1];
|
|
651
|
+
// Reject overlong encodings, UTF-16 surrogates and out-of-range points.
|
|
652
|
+
if ((byte === 0xe0 && secondByte < 0xa0) ||
|
|
653
|
+
(byte === 0xed && secondByte > 0x9f) ||
|
|
654
|
+
(byte === 0xf0 && secondByte < 0x90) ||
|
|
655
|
+
(byte === 0xf4 && secondByte > 0x8f)) {
|
|
656
|
+
return { ok: false, reason: "invalid", start: index };
|
|
657
|
+
}
|
|
658
|
+
index += length;
|
|
659
|
+
}
|
|
660
|
+
return { ok: true, text: chunk.toString("utf8") };
|
|
661
|
+
}
|
|
662
|
+
function singleCodepoint(sequence) {
|
|
663
|
+
let text;
|
|
664
|
+
try {
|
|
665
|
+
text = utf8Decoder.decode(sequence);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
const chars = [...text];
|
|
671
|
+
if (chars.length !== 1) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
return chars[0].codePointAt(0);
|
|
675
|
+
}
|
|
676
|
+
const UNMODIFIED_KITTY_PRINTABLE_RE = /^\x1b\[(\d+)(?::\d*)?(?::\d+)?u$/;
|
|
677
|
+
function parseUnmodifiedKittyPrintableCodepoint(sequence) {
|
|
678
|
+
const match = UNMODIFIED_KITTY_PRINTABLE_RE.exec(sequence.toString("latin1"));
|
|
679
|
+
if (match === null) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
const codepoint = Number.parseInt(match[1], 10);
|
|
683
|
+
return codepoint >= 32 ? codepoint : null;
|
|
684
|
+
}
|
|
685
|
+
const KITTY_RELEASE_MARKERS = [":3u", ":3~", ":3A", ":3B", ":3C", ":3D", ":3H", ":3F"];
|
|
686
|
+
function isKittyRelease(sequence) {
|
|
687
|
+
if (sequence.includes("\x1b[200~")) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
return KITTY_RELEASE_MARKERS.some((marker) => sequence.includes(marker));
|
|
691
|
+
}
|
|
692
|
+
const KITTY_ARROW_RE = /^\x1b\[1;(\d+)(?::(\d+))?([ABCD])$/;
|
|
693
|
+
const KITTY_ARROW_ACTIONS = new Map([
|
|
694
|
+
["A", InputActionKind.HistoryUp],
|
|
695
|
+
["B", InputActionKind.HistoryDown],
|
|
696
|
+
["C", InputActionKind.CursorRight],
|
|
697
|
+
["D", InputActionKind.CursorLeft],
|
|
698
|
+
]);
|
|
699
|
+
function decodeKittyArrowAction(sequence) {
|
|
700
|
+
const match = KITTY_ARROW_RE.exec(sequence);
|
|
701
|
+
if (match === null) {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
const modifier = Number.parseInt(match[1], 10) - 1;
|
|
705
|
+
if (modifier !== 0) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const eventType = Number.parseInt(match[2] ?? "1", 10);
|
|
709
|
+
if (eventType === 3) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
const kind = KITTY_ARROW_ACTIONS.get(match[3]);
|
|
713
|
+
return kind !== undefined ? inputAction(kind) : null;
|
|
714
|
+
}
|
|
715
|
+
const SPECIAL_ESCAPE_ACTIONS = new Map([
|
|
716
|
+
["\x1b[13;2u", InputActionKind.Newline],
|
|
717
|
+
["\x1b[57414;2u", InputActionKind.Newline],
|
|
718
|
+
["\x1b[13u", InputActionKind.Submit],
|
|
719
|
+
["\x1b[13;1u", InputActionKind.Submit],
|
|
720
|
+
["\x1b[57414u", InputActionKind.Submit],
|
|
721
|
+
["\x1b[57414;1u", InputActionKind.Submit],
|
|
722
|
+
["\x1b[9u", InputActionKind.Complete],
|
|
723
|
+
["\x1b[9;1u", InputActionKind.Complete],
|
|
724
|
+
["\x1b[127u", InputActionKind.Backspace],
|
|
725
|
+
["\x1b[127;1u", InputActionKind.Backspace],
|
|
726
|
+
["\x1b[27u", InputActionKind.Dismiss],
|
|
727
|
+
["\x1b[27;1u", InputActionKind.Dismiss],
|
|
728
|
+
]);
|
|
729
|
+
function decodeSpecialEscapeAction(sequence) {
|
|
730
|
+
const kind = SPECIAL_ESCAPE_ACTIONS.get(sequence);
|
|
731
|
+
return kind !== undefined ? inputAction(kind) : null;
|
|
732
|
+
}
|
|
733
|
+
function decodePrintableKey(sequence) {
|
|
734
|
+
const kitty = decodeKittyPrintable(sequence);
|
|
735
|
+
if (kitty !== null) {
|
|
736
|
+
return kitty;
|
|
737
|
+
}
|
|
738
|
+
return decodeModifyOtherKeysPrintable(sequence);
|
|
739
|
+
}
|
|
740
|
+
const KITTY_PRINTABLE_RE = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?u$/;
|
|
741
|
+
function decodeKittyPrintable(sequence) {
|
|
742
|
+
const match = KITTY_PRINTABLE_RE.exec(sequence);
|
|
743
|
+
if (match === null) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
const codepoint = Number.parseInt(match[1], 10);
|
|
747
|
+
const shifted = match[2];
|
|
748
|
+
const shiftedCodepoint = shifted ? Number.parseInt(shifted, 10) : null;
|
|
749
|
+
const modifier = Number.parseInt(match[4] ?? "1", 10) - 1;
|
|
750
|
+
const lockMask = 64 + 128;
|
|
751
|
+
if (modifier & ~(1 | lockMask)) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
if (modifier & (2 | 4)) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
const effective = (modifier & 1) !== 0 && shiftedCodepoint !== null
|
|
758
|
+
? shiftedCodepoint
|
|
759
|
+
: codepoint;
|
|
760
|
+
if (effective < 32) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
try {
|
|
764
|
+
return String.fromCodePoint(effective);
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const MODIFY_OTHER_KEYS_PRINTABLE_RE = /^\x1b\[27;(\d+);(\d+)~$/;
|
|
771
|
+
function decodeModifyOtherKeysPrintable(sequence) {
|
|
772
|
+
const match = MODIFY_OTHER_KEYS_PRINTABLE_RE.exec(sequence);
|
|
773
|
+
if (match === null) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
const modifier = Number.parseInt(match[1], 10) - 1;
|
|
777
|
+
if (modifier & ~1) {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
const codepoint = Number.parseInt(match[2], 10);
|
|
781
|
+
if (codepoint < 32) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
return String.fromCodePoint(codepoint);
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function displayWidth(text) {
|
|
792
|
+
let width = 0;
|
|
793
|
+
for (const char of text) {
|
|
794
|
+
width += charCellWidth(char);
|
|
795
|
+
}
|
|
796
|
+
return width;
|
|
797
|
+
}
|
|
798
|
+
/** Text, history, and command-completion state for the active editor. */
|
|
799
|
+
export class EditorState {
|
|
800
|
+
/** Window for the Pi-style double-Ctrl+C-to-exit gesture, in milliseconds. */
|
|
801
|
+
static DOUBLE_CANCEL_EXIT_MS = 500;
|
|
802
|
+
text = "";
|
|
803
|
+
cursor = 0;
|
|
804
|
+
history = [];
|
|
805
|
+
historyIndex = null;
|
|
806
|
+
historyDraft = "";
|
|
807
|
+
completions = [];
|
|
808
|
+
selectedCompletion = null;
|
|
809
|
+
lastCancelAt = null;
|
|
810
|
+
get completionVisible() {
|
|
811
|
+
return this.completions.length > 0;
|
|
812
|
+
}
|
|
813
|
+
setCompletions(values) {
|
|
814
|
+
this.completions = this.text.startsWith("/") ? [...values] : [];
|
|
815
|
+
this.selectedCompletion = this.completions.length > 0 ? 0 : null;
|
|
816
|
+
}
|
|
817
|
+
apply(action, { runtimeActive }) {
|
|
818
|
+
if (action.kind === InputActionKind.Submit) {
|
|
819
|
+
if (this.completionVisible) {
|
|
820
|
+
this.acceptCompletion();
|
|
821
|
+
if (!this.text.startsWith("/")) {
|
|
822
|
+
return editorEffect();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return this.submit();
|
|
826
|
+
}
|
|
827
|
+
if (action.kind === InputActionKind.Newline) {
|
|
828
|
+
this.insert("\n");
|
|
829
|
+
return editorEffect();
|
|
830
|
+
}
|
|
831
|
+
if (action.kind === InputActionKind.Cancel) {
|
|
832
|
+
return this.cancelOrClear(runtimeActive);
|
|
833
|
+
}
|
|
834
|
+
if (action.kind === InputActionKind.Eof) {
|
|
835
|
+
return this.exitOrNotice(runtimeActive);
|
|
836
|
+
}
|
|
837
|
+
if (action.kind === InputActionKind.Dismiss) {
|
|
838
|
+
this.clearCompletions();
|
|
839
|
+
return editorEffect();
|
|
840
|
+
}
|
|
841
|
+
return this.applyEditAction(action);
|
|
842
|
+
}
|
|
843
|
+
renderLines(width, options = {}) {
|
|
844
|
+
const prompt = options.prompt ?? "❯ ";
|
|
845
|
+
const mask = options.mask ?? false;
|
|
846
|
+
const boundedWidth = Math.max(3, width);
|
|
847
|
+
const promptWidth = Math.max(1, displayWidth(prompt));
|
|
848
|
+
const contentWidth = Math.max(1, boundedWidth - promptWidth);
|
|
849
|
+
const displayText = mask ? "*".repeat(this.text.length) : this.text;
|
|
850
|
+
const sourceLines = displayText.split("\n");
|
|
851
|
+
const rows = [];
|
|
852
|
+
for (const source of sourceLines) {
|
|
853
|
+
rows.push(...wrapLine(source, contentWidth));
|
|
854
|
+
}
|
|
855
|
+
const lastLine = sourceLines[sourceLines.length - 1];
|
|
856
|
+
if (displayText &&
|
|
857
|
+
!displayText.endsWith("\n") &&
|
|
858
|
+
displayWidth(lastLine) % contentWidth === 0) {
|
|
859
|
+
rows.push("");
|
|
860
|
+
}
|
|
861
|
+
const lines = rows.map((row, index) => (index === 0 ? prompt : " ".repeat(promptWidth)) + row);
|
|
862
|
+
const before = displayText.slice(0, this.cursor);
|
|
863
|
+
const beforeLines = before.split("\n");
|
|
864
|
+
let priorRows = 0;
|
|
865
|
+
for (const line of beforeLines.slice(0, -1)) {
|
|
866
|
+
priorRows += wrapLine(line, contentWidth).length;
|
|
867
|
+
}
|
|
868
|
+
const current = beforeLines[beforeLines.length - 1];
|
|
869
|
+
const [currentRow, currentColumn] = cursorPosition(current, contentWidth);
|
|
870
|
+
const cursorRow = priorRows + currentRow;
|
|
871
|
+
const cursorColumn = promptWidth + currentColumn;
|
|
872
|
+
return {
|
|
873
|
+
lines,
|
|
874
|
+
cursorRow: Math.min(cursorRow, lines.length - 1),
|
|
875
|
+
cursorColumn: Math.min(cursorColumn, boundedWidth - 1),
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
submit() {
|
|
879
|
+
const submitted = this.text;
|
|
880
|
+
this.clearCompletions();
|
|
881
|
+
this.text = "";
|
|
882
|
+
this.cursor = 0;
|
|
883
|
+
this.historyIndex = null;
|
|
884
|
+
this.historyDraft = "";
|
|
885
|
+
if (!submitted) {
|
|
886
|
+
return editorEffect();
|
|
887
|
+
}
|
|
888
|
+
this.history = [...this.history, submitted];
|
|
889
|
+
return editorEffect({ submit: submitted });
|
|
890
|
+
}
|
|
891
|
+
cancelOrClear(runtimeActive) {
|
|
892
|
+
if (runtimeActive) {
|
|
893
|
+
// Cancelling a task must not arm (or consume) the exit window.
|
|
894
|
+
this.lastCancelAt = null;
|
|
895
|
+
return editorEffect({ cancelRequested: true });
|
|
896
|
+
}
|
|
897
|
+
// Pi semantics (pi-coding-agent handleCtrlC): a second idle Ctrl+C within
|
|
898
|
+
// the window exits; the first one just clears the editor.
|
|
899
|
+
const now = performance.now();
|
|
900
|
+
if (this.lastCancelAt !== null &&
|
|
901
|
+
now - this.lastCancelAt <= EditorState.DOUBLE_CANCEL_EXIT_MS) {
|
|
902
|
+
this.lastCancelAt = null;
|
|
903
|
+
return editorEffect({ exitRequested: true });
|
|
904
|
+
}
|
|
905
|
+
this.lastCancelAt = now;
|
|
906
|
+
this.text = "";
|
|
907
|
+
this.cursor = 0;
|
|
908
|
+
this.historyIndex = null;
|
|
909
|
+
this.clearCompletions();
|
|
910
|
+
return editorEffect();
|
|
911
|
+
}
|
|
912
|
+
exitOrNotice(runtimeActive) {
|
|
913
|
+
if (runtimeActive) {
|
|
914
|
+
return editorEffect({
|
|
915
|
+
notice: "A task is still running. Press Ctrl+C to cancel it.",
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
if (this.text) {
|
|
919
|
+
return editorEffect({ notice: "Clear the editor before exiting." });
|
|
920
|
+
}
|
|
921
|
+
return editorEffect({ exitRequested: true });
|
|
922
|
+
}
|
|
923
|
+
applyEditAction(action) {
|
|
924
|
+
if (action.kind === InputActionKind.HistoryUp && this.completionVisible) {
|
|
925
|
+
this.moveCompletion(-1);
|
|
926
|
+
}
|
|
927
|
+
else if (action.kind === InputActionKind.HistoryDown &&
|
|
928
|
+
this.completionVisible) {
|
|
929
|
+
this.moveCompletion(1);
|
|
930
|
+
}
|
|
931
|
+
else if (action.kind === InputActionKind.Insert) {
|
|
932
|
+
this.insert(action.text);
|
|
933
|
+
}
|
|
934
|
+
else if (action.kind === InputActionKind.Backspace && this.cursor > 0) {
|
|
935
|
+
this.clearCompletions();
|
|
936
|
+
this.text = this.text.slice(0, this.cursor - 1) + this.text.slice(this.cursor);
|
|
937
|
+
this.cursor -= 1;
|
|
938
|
+
}
|
|
939
|
+
else if (action.kind === InputActionKind.CursorLeft) {
|
|
940
|
+
this.clearCompletions();
|
|
941
|
+
this.cursor = Math.max(0, this.cursor - 1);
|
|
942
|
+
}
|
|
943
|
+
else if (action.kind === InputActionKind.CursorRight) {
|
|
944
|
+
this.clearCompletions();
|
|
945
|
+
this.cursor = Math.min(this.text.length, this.cursor + 1);
|
|
946
|
+
}
|
|
947
|
+
else if (action.kind === InputActionKind.HistoryUp) {
|
|
948
|
+
this.historyUp();
|
|
949
|
+
}
|
|
950
|
+
else if (action.kind === InputActionKind.HistoryDown) {
|
|
951
|
+
this.historyDown();
|
|
952
|
+
}
|
|
953
|
+
else if (action.kind === InputActionKind.Complete) {
|
|
954
|
+
this.acceptCompletion();
|
|
955
|
+
}
|
|
956
|
+
this.closeCompletionIfContextLost();
|
|
957
|
+
return editorEffect();
|
|
958
|
+
}
|
|
959
|
+
insert(text) {
|
|
960
|
+
this.clearCompletions();
|
|
961
|
+
this.text = this.text.slice(0, this.cursor) + text + this.text.slice(this.cursor);
|
|
962
|
+
this.cursor += text.length;
|
|
963
|
+
this.historyIndex = null;
|
|
964
|
+
}
|
|
965
|
+
acceptCompletion() {
|
|
966
|
+
if (this.selectedCompletion === null) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const item = this.completions[this.selectedCompletion];
|
|
970
|
+
if (item === undefined) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const start = Math.max(0, this.cursor + item.start);
|
|
974
|
+
this.text = this.text.slice(0, start) + item.value + this.text.slice(this.cursor);
|
|
975
|
+
this.cursor = start + item.value.length;
|
|
976
|
+
this.clearCompletions();
|
|
977
|
+
}
|
|
978
|
+
moveCompletion(offset) {
|
|
979
|
+
if (this.selectedCompletion === null) {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
this.selectedCompletion =
|
|
983
|
+
(this.selectedCompletion + offset + this.completions.length) %
|
|
984
|
+
this.completions.length;
|
|
985
|
+
}
|
|
986
|
+
historyUp() {
|
|
987
|
+
if (this.history.length === 0) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (this.historyIndex === null) {
|
|
991
|
+
this.historyDraft = this.text;
|
|
992
|
+
this.historyIndex = this.history.length - 1;
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
this.historyIndex = Math.max(0, this.historyIndex - 1);
|
|
996
|
+
}
|
|
997
|
+
this.text = this.history[this.historyIndex];
|
|
998
|
+
this.cursor = this.text.length;
|
|
999
|
+
}
|
|
1000
|
+
historyDown() {
|
|
1001
|
+
if (this.historyIndex === null) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (this.historyIndex === this.history.length - 1) {
|
|
1005
|
+
this.text = this.historyDraft;
|
|
1006
|
+
this.historyIndex = null;
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
this.historyIndex += 1;
|
|
1010
|
+
this.text = this.history[this.historyIndex];
|
|
1011
|
+
}
|
|
1012
|
+
this.cursor = this.text.length;
|
|
1013
|
+
}
|
|
1014
|
+
closeCompletionIfContextLost() {
|
|
1015
|
+
if (!this.text.startsWith("/")) {
|
|
1016
|
+
this.clearCompletions();
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
clearCompletions() {
|
|
1020
|
+
this.completions = [];
|
|
1021
|
+
this.selectedCompletion = null;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function wrapLine(text, width) {
|
|
1025
|
+
const rows = [];
|
|
1026
|
+
let row = "";
|
|
1027
|
+
let rowWidth = 0;
|
|
1028
|
+
for (const char of text) {
|
|
1029
|
+
const charWidth = Math.max(1, charCellWidth(char));
|
|
1030
|
+
if (row && rowWidth + charWidth > width) {
|
|
1031
|
+
rows.push(row);
|
|
1032
|
+
row = "";
|
|
1033
|
+
rowWidth = 0;
|
|
1034
|
+
}
|
|
1035
|
+
row += char;
|
|
1036
|
+
rowWidth += charWidth;
|
|
1037
|
+
}
|
|
1038
|
+
if (row || rows.length === 0) {
|
|
1039
|
+
rows.push(row);
|
|
1040
|
+
}
|
|
1041
|
+
return rows;
|
|
1042
|
+
}
|
|
1043
|
+
function cursorPosition(text, width) {
|
|
1044
|
+
let row = 0;
|
|
1045
|
+
let column = 0;
|
|
1046
|
+
for (const char of text) {
|
|
1047
|
+
const charWidth = Math.max(1, charCellWidth(char));
|
|
1048
|
+
if (column && column + charWidth > width) {
|
|
1049
|
+
row += 1;
|
|
1050
|
+
column = 0;
|
|
1051
|
+
}
|
|
1052
|
+
column += charWidth;
|
|
1053
|
+
if (column === width) {
|
|
1054
|
+
row += 1;
|
|
1055
|
+
column = 0;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return [row, column];
|
|
1059
|
+
}
|
|
1060
|
+
//# sourceMappingURL=editor.js.map
|