@jano-editor/editor 1.0.0-alpha.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.
@@ -0,0 +1,2979 @@
1
+ import { i as loadConfig, n as getPluginsDir, r as isPluginEnabled, t as getPaths } from "./cli.js";
2
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { basename, extname, join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ //#region packages/ui/src/screen.ts
6
+ var ESC$1 = "\x1B";
7
+ function createScreen(stream = process.stdout) {
8
+ const write = (data) => stream.write(data);
9
+ return {
10
+ get width() {
11
+ return stream.columns;
12
+ },
13
+ get height() {
14
+ return stream.rows;
15
+ },
16
+ enter() {
17
+ write(`${ESC$1}[?1049h`);
18
+ write(`${ESC$1}[?25l`);
19
+ },
20
+ leave() {
21
+ write(`${ESC$1}[?25h`);
22
+ write(`${ESC$1}[?1049l`);
23
+ },
24
+ clear() {
25
+ write(`${ESC$1}[2J`);
26
+ },
27
+ moveTo(x, y) {
28
+ write(`${ESC$1}[${y + 1};${x + 1}H`);
29
+ },
30
+ showCursor() {
31
+ write(`${ESC$1}[?25h`);
32
+ },
33
+ hideCursor() {
34
+ write(`${ESC$1}[?25l`);
35
+ },
36
+ write(data) {
37
+ write(data);
38
+ }
39
+ };
40
+ }
41
+ //#endregion
42
+ //#region packages/ui/src/color.ts
43
+ var ESC = "\x1B";
44
+ function fg(r, g, b) {
45
+ return `${ESC}[38;2;${r};${g};${b}m`;
46
+ }
47
+ function bg(r, g, b) {
48
+ return `${ESC}[48;2;${r};${g};${b}m`;
49
+ }
50
+ var reset = `${ESC}[0m`;
51
+ `${ESC}`;
52
+ `${ESC}`;
53
+ `${ESC}`;
54
+ `${ESC}`;
55
+ //#endregion
56
+ //#region packages/ui/src/draw.ts
57
+ var borders = {
58
+ single: {
59
+ tl: "┌",
60
+ tr: "┐",
61
+ bl: "└",
62
+ br: "┘",
63
+ h: "─",
64
+ v: "│"
65
+ },
66
+ double: {
67
+ tl: "╔",
68
+ tr: "╗",
69
+ bl: "╚",
70
+ br: "╝",
71
+ h: "═",
72
+ v: "║"
73
+ },
74
+ round: {
75
+ tl: "╭",
76
+ tr: "╮",
77
+ bl: "╰",
78
+ br: "╯",
79
+ h: "─",
80
+ v: "│"
81
+ }
82
+ };
83
+ function createDraw(screen) {
84
+ let buffer = [];
85
+ let bufW = 0;
86
+ let bufH = 0;
87
+ function ensureBuffer() {
88
+ const w = screen.width;
89
+ const h = screen.height;
90
+ if (w !== bufW || h !== bufH) {
91
+ bufW = w;
92
+ bufH = h;
93
+ buffer = [];
94
+ for (let y = 0; y < h; y++) {
95
+ buffer[y] = [];
96
+ for (let x = 0; x < w; x++) buffer[y][x] = {
97
+ char: " ",
98
+ style: ""
99
+ };
100
+ }
101
+ }
102
+ }
103
+ function set(x, y, char, style = "") {
104
+ if (x < 0 || y < 0 || x >= bufW || y >= bufH) return;
105
+ buffer[y][x] = {
106
+ char,
107
+ style
108
+ };
109
+ }
110
+ function buildStyle(opts) {
111
+ let s = "";
112
+ if (opts.fg) s += fg(opts.fg[0], opts.fg[1], opts.fg[2]);
113
+ if (opts.bg) s += bg(opts.bg[0], opts.bg[1], opts.bg[2]);
114
+ return s;
115
+ }
116
+ return {
117
+ clear() {
118
+ ensureBuffer();
119
+ for (let y = 0; y < bufH; y++) for (let x = 0; x < bufW; x++) buffer[y][x] = {
120
+ char: " ",
121
+ style: ""
122
+ };
123
+ },
124
+ char(x, y, char, opts = {}) {
125
+ ensureBuffer();
126
+ set(x, y, char, buildStyle(opts));
127
+ },
128
+ text(x, y, text, opts = {}) {
129
+ ensureBuffer();
130
+ const style = buildStyle(opts);
131
+ for (let i = 0; i < text.length; i++) set(x + i, y, text[i], style);
132
+ },
133
+ line(x1, y1, x2, y2, opts = {}) {
134
+ ensureBuffer();
135
+ const style = buildStyle(opts);
136
+ const char = opts.char || (y1 === y2 ? "─" : "│");
137
+ if (y1 === y2) {
138
+ const start = Math.min(x1, x2);
139
+ const end = Math.max(x1, x2);
140
+ for (let x = start; x <= end; x++) set(x, y1, char, style);
141
+ } else if (x1 === x2) {
142
+ const start = Math.min(y1, y2);
143
+ const end = Math.max(y1, y2);
144
+ for (let y = start; y <= end; y++) set(x1, y, char, style);
145
+ }
146
+ },
147
+ rect(x, y, w, h, opts = {}) {
148
+ ensureBuffer();
149
+ const style = buildStyle(opts);
150
+ const b = borders[opts.border || "single"];
151
+ set(x, y, b.tl, style);
152
+ set(x + w - 1, y, b.tr, style);
153
+ set(x, y + h - 1, b.bl, style);
154
+ set(x + w - 1, y + h - 1, b.br, style);
155
+ for (let i = 1; i < w - 1; i++) {
156
+ set(x + i, y, b.h, style);
157
+ set(x + i, y + h - 1, b.h, style);
158
+ }
159
+ for (let i = 1; i < h - 1; i++) {
160
+ set(x, y + i, b.v, style);
161
+ set(x + w - 1, y + i, b.v, style);
162
+ }
163
+ if (opts.fill) {
164
+ const fillStyle = buildStyle({ bg: opts.fill });
165
+ for (let iy = 1; iy < h - 1; iy++) for (let ix = 1; ix < w - 1; ix++) set(x + ix, y + iy, " ", fillStyle);
166
+ }
167
+ },
168
+ flush() {
169
+ ensureBuffer();
170
+ let out = "";
171
+ for (let y = 0; y < bufH; y++) {
172
+ out += `\x1b[${y + 1};1H`;
173
+ let lastStyle = "";
174
+ for (let x = 0; x < bufW; x++) {
175
+ const cell = buffer[y][x];
176
+ if (cell.style !== lastStyle) {
177
+ out += reset + cell.style;
178
+ lastStyle = cell.style;
179
+ }
180
+ out += cell.char;
181
+ }
182
+ }
183
+ out += reset;
184
+ screen.write(out);
185
+ }
186
+ };
187
+ }
188
+ //#endregion
189
+ //#region packages/ui/src/dialog.ts
190
+ var theme$1 = {
191
+ border: [
192
+ 120,
193
+ 120,
194
+ 120
195
+ ],
196
+ title: [
197
+ 255,
198
+ 255,
199
+ 100
200
+ ],
201
+ message: [
202
+ 200,
203
+ 200,
204
+ 200
205
+ ],
206
+ buttonFg: [
207
+ 0,
208
+ 0,
209
+ 0
210
+ ],
211
+ buttonBg: [
212
+ 180,
213
+ 180,
214
+ 180
215
+ ],
216
+ buttonActiveFg: [
217
+ 0,
218
+ 0,
219
+ 0
220
+ ],
221
+ buttonActiveBg: [
222
+ 100,
223
+ 200,
224
+ 255
225
+ ],
226
+ inputFg: [
227
+ 255,
228
+ 255,
229
+ 255
230
+ ],
231
+ inputBg: [
232
+ 50,
233
+ 50,
234
+ 50
235
+ ],
236
+ fill: [
237
+ 30,
238
+ 30,
239
+ 30
240
+ ]
241
+ };
242
+ function showDialog(screen, draw, opts, renderBackground) {
243
+ return new Promise((resolve) => {
244
+ const buttons = opts.buttons ?? [];
245
+ const hasInput = opts.input ?? false;
246
+ let selectedButton = 0;
247
+ let inputText = opts.inputValue ?? "";
248
+ let inputCursorX = inputText.length;
249
+ let buttonsActive = !hasInput;
250
+ const dialogW = opts.width ?? Math.min(50, screen.width - 4);
251
+ const messageLines = wrapText(opts.message, dialogW - 4);
252
+ function calcHeight() {
253
+ let h = 2;
254
+ h += messageLines.length;
255
+ if (hasInput) h += 2;
256
+ if (buttons.length > 0) h += 2;
257
+ return h;
258
+ }
259
+ let backgroundDrawn = false;
260
+ function renderDialog() {
261
+ if (!backgroundDrawn) {
262
+ renderBackground();
263
+ backgroundDrawn = true;
264
+ }
265
+ const w = dialogW;
266
+ const h = calcHeight();
267
+ const x = Math.floor((screen.width - w) / 2);
268
+ const y = Math.floor((screen.height - h) / 2);
269
+ draw.rect(x, y, w, h, {
270
+ fg: theme$1.border,
271
+ border: opts.border ?? "round",
272
+ fill: theme$1.fill
273
+ });
274
+ if (opts.title) {
275
+ const titleText = ` ${opts.title} `;
276
+ const titleX = x + Math.floor((w - titleText.length) / 2);
277
+ draw.text(titleX, y, titleText, { fg: theme$1.title });
278
+ }
279
+ let row = y + 1;
280
+ for (const line of messageLines) {
281
+ draw.text(x + 2, row, line, {
282
+ fg: theme$1.message,
283
+ bg: theme$1.fill
284
+ });
285
+ row++;
286
+ }
287
+ if (hasInput) {
288
+ row++;
289
+ const inputW = w - 4;
290
+ for (let i = 0; i < inputW; i++) draw.char(x + 2 + i, row, " ", { bg: theme$1.inputBg });
291
+ const visibleText = inputText.substring(0, inputW);
292
+ draw.text(x + 2, row, visibleText, {
293
+ fg: theme$1.inputFg,
294
+ bg: theme$1.inputBg
295
+ });
296
+ if (inputText.length === 0 && opts.inputPlaceholder) draw.text(x + 2, row, opts.inputPlaceholder.substring(0, inputW), {
297
+ fg: [
298
+ 100,
299
+ 100,
300
+ 100
301
+ ],
302
+ bg: theme$1.inputBg
303
+ });
304
+ row++;
305
+ }
306
+ if (buttons.length > 0) {
307
+ row++;
308
+ const btnWidths = buttons.map((b) => b.label.length + 2);
309
+ const totalLen = btnWidths.reduce((a, b) => a + b, 0) + (buttons.length - 1) * 2;
310
+ let btnX = x + Math.floor((w - totalLen) / 2);
311
+ for (let i = 0; i < buttons.length; i++) {
312
+ const label = ` ${buttons[i].label} `;
313
+ const isActive = buttonsActive && i === selectedButton;
314
+ draw.text(btnX, row, label, {
315
+ fg: isActive ? theme$1.buttonActiveFg : theme$1.buttonFg,
316
+ bg: isActive ? theme$1.buttonActiveBg : theme$1.buttonBg
317
+ });
318
+ btnX += btnWidths[i] + 2;
319
+ }
320
+ }
321
+ draw.flush();
322
+ if (hasInput && !buttonsActive) {
323
+ const inputY = y + 1 + messageLines.length + 1;
324
+ screen.moveTo(x + 2 + inputCursorX, inputY);
325
+ screen.showCursor();
326
+ } else screen.hideCursor();
327
+ }
328
+ function onData(data) {
329
+ if (data[0] === 27 && data.length === 1) {
330
+ cleanup();
331
+ resolve({ type: "cancel" });
332
+ return;
333
+ }
334
+ if (data[0] === 13) {
335
+ if (hasInput && !buttonsActive && buttons.length > 0) {
336
+ buttonsActive = true;
337
+ renderDialog();
338
+ return;
339
+ }
340
+ cleanup();
341
+ if (hasInput && !buttonsActive && buttons.length === 0) resolve({
342
+ type: "input",
343
+ value: inputText
344
+ });
345
+ else if (buttons.length > 0) resolve({
346
+ type: "button",
347
+ value: buttons[selectedButton].value,
348
+ inputValue: inputText
349
+ });
350
+ return;
351
+ }
352
+ if (data[0] === 9 && hasInput && buttons.length > 0) {
353
+ buttonsActive = !buttonsActive;
354
+ renderDialog();
355
+ return;
356
+ }
357
+ if (data[0] === 27 && data[1] === 91) {
358
+ const seq = data.toString("utf8", 2);
359
+ if (buttonsActive) {
360
+ if (seq === "C" || seq === "D") {
361
+ if (seq === "C") selectedButton = Math.min(selectedButton + 1, buttons.length - 1);
362
+ if (seq === "D") selectedButton = Math.max(selectedButton - 1, 0);
363
+ renderDialog();
364
+ }
365
+ } else if (hasInput) {
366
+ if (seq === "C") inputCursorX = Math.min(inputCursorX + 1, inputText.length);
367
+ if (seq === "D") inputCursorX = Math.max(inputCursorX - 1, 0);
368
+ if (seq === "H") inputCursorX = 0;
369
+ if (seq === "F") inputCursorX = inputText.length;
370
+ renderDialog();
371
+ }
372
+ return;
373
+ }
374
+ if (data[0] === 127 && hasInput && !buttonsActive) {
375
+ if (inputCursorX > 0) {
376
+ inputText = inputText.substring(0, inputCursorX - 1) + inputText.substring(inputCursorX);
377
+ inputCursorX--;
378
+ renderDialog();
379
+ }
380
+ return;
381
+ }
382
+ if (hasInput && !buttonsActive) {
383
+ const ch = data.toString("utf8");
384
+ if ((ch.codePointAt(0) ?? 0) >= 32) {
385
+ inputText = inputText.substring(0, inputCursorX) + ch + inputText.substring(inputCursorX);
386
+ inputCursorX += ch.length;
387
+ renderDialog();
388
+ }
389
+ }
390
+ }
391
+ function cleanup() {
392
+ process.stdin.removeListener("data", onData);
393
+ }
394
+ process.stdin.on("data", onData);
395
+ renderDialog();
396
+ });
397
+ }
398
+ function wrapText(text, maxWidth) {
399
+ const result = [];
400
+ for (const line of text.split("\n")) if (line.length <= maxWidth) result.push(line);
401
+ else {
402
+ let remaining = line;
403
+ while (remaining.length > maxWidth) {
404
+ let breakAt = remaining.lastIndexOf(" ", maxWidth);
405
+ if (breakAt <= 0) breakAt = maxWidth;
406
+ result.push(remaining.substring(0, breakAt));
407
+ remaining = remaining.substring(breakAt).trimStart();
408
+ }
409
+ if (remaining.length > 0) result.push(remaining);
410
+ }
411
+ return result;
412
+ }
413
+ //#endregion
414
+ //#region packages/ui/src/list.ts
415
+ function drawList(draw, opts) {
416
+ const { x, y, width, height, items, selectedIndex, scrollOffset, fg = [
417
+ 171,
418
+ 178,
419
+ 191
420
+ ], bg = [
421
+ 30,
422
+ 30,
423
+ 30
424
+ ], selectedFg = [
425
+ 255,
426
+ 255,
427
+ 255
428
+ ], selectedBg = [
429
+ 60,
430
+ 100,
431
+ 180
432
+ ], descriptionFg = [
433
+ 100,
434
+ 105,
435
+ 115
436
+ ] } = opts;
437
+ for (let i = 0; i < height; i++) {
438
+ const itemIdx = i + scrollOffset;
439
+ const screenY = y + i;
440
+ if (itemIdx >= items.length) {
441
+ for (let col = 0; col < width; col++) draw.char(x + col, screenY, " ", { bg });
442
+ continue;
443
+ }
444
+ const item = items[itemIdx];
445
+ const isSelected = itemIdx === selectedIndex;
446
+ const rowFg = isSelected ? selectedFg : fg;
447
+ const rowBg = isSelected ? selectedBg : bg;
448
+ for (let col = 0; col < width; col++) draw.char(x + col, screenY, " ", { bg: rowBg });
449
+ const label = item.label.substring(0, width);
450
+ draw.text(x, screenY, label, {
451
+ fg: rowFg,
452
+ bg: rowBg
453
+ });
454
+ if (item.description) {
455
+ const descMaxW = width - label.length - 2;
456
+ if (descMaxW > 3) {
457
+ const desc = item.description.substring(0, descMaxW);
458
+ draw.text(x + width - desc.length, screenY, desc, {
459
+ fg: isSelected ? selectedFg : descriptionFg,
460
+ bg: rowBg
461
+ });
462
+ }
463
+ }
464
+ }
465
+ if (items.length > height) {
466
+ const thumbSize = Math.max(1, Math.round(height * (height / items.length)));
467
+ const scrollRatio = scrollOffset / Math.max(1, items.length - height);
468
+ const thumbPos = Math.round(scrollRatio * (height - thumbSize));
469
+ for (let i = 0; i < height; i++) if (i >= thumbPos && i < thumbPos + thumbSize) draw.char(x + width - 1, y + i, "┃", { fg: [
470
+ 100,
471
+ 100,
472
+ 100
473
+ ] });
474
+ }
475
+ }
476
+ function listMoveUp(state, _itemCount) {
477
+ const selectedIndex = Math.max(0, state.selectedIndex - 1);
478
+ let scrollOffset = state.scrollOffset;
479
+ if (selectedIndex < scrollOffset) scrollOffset = selectedIndex;
480
+ return {
481
+ selectedIndex,
482
+ scrollOffset
483
+ };
484
+ }
485
+ function listMoveDown(state, itemCount, visibleHeight) {
486
+ const selectedIndex = Math.min(itemCount - 1, state.selectedIndex + 1);
487
+ let scrollOffset = state.scrollOffset;
488
+ if (selectedIndex >= scrollOffset + visibleHeight) scrollOffset = selectedIndex - visibleHeight + 1;
489
+ return {
490
+ selectedIndex,
491
+ scrollOffset
492
+ };
493
+ }
494
+ //#endregion
495
+ //#region packages/ui/src/search.ts
496
+ var theme = {
497
+ border: [
498
+ 80,
499
+ 90,
500
+ 105
501
+ ],
502
+ fill: [
503
+ 30,
504
+ 33,
505
+ 40
506
+ ],
507
+ title: [
508
+ 230,
509
+ 200,
510
+ 100
511
+ ],
512
+ inputFg: [
513
+ 255,
514
+ 255,
515
+ 255
516
+ ],
517
+ inputBg: [
518
+ 45,
519
+ 48,
520
+ 55
521
+ ],
522
+ inputActiveBorder: [
523
+ 100,
524
+ 200,
525
+ 255
526
+ ],
527
+ placeholder: [
528
+ 80,
529
+ 85,
530
+ 95
531
+ ],
532
+ matchCount: [
533
+ 100,
534
+ 200,
535
+ 255
536
+ ],
537
+ labelFg: [
538
+ 130,
539
+ 135,
540
+ 145
541
+ ],
542
+ replaceFg: [
543
+ 152,
544
+ 195,
545
+ 121
546
+ ]
547
+ };
548
+ function findMatches(lines, query, maxResults) {
549
+ if (!query) return [];
550
+ const matches = [];
551
+ const lowerQuery = query.toLowerCase();
552
+ for (let i = 0; i < lines.length && matches.length < maxResults; i++) {
553
+ const line = lines[i];
554
+ const lowerLine = line.toLowerCase();
555
+ let searchFrom = 0;
556
+ while (searchFrom < lowerLine.length && matches.length < maxResults) {
557
+ const col = lowerLine.indexOf(lowerQuery, searchFrom);
558
+ if (col === -1) break;
559
+ const contextStart = Math.max(0, col - 15);
560
+ const contextEnd = Math.min(line.length, col + query.length + 30);
561
+ const prefix = contextStart > 0 ? "…" : "";
562
+ const suffix = contextEnd < line.length ? "…" : "";
563
+ const context = prefix + line.substring(contextStart, contextEnd) + suffix;
564
+ matches.push({
565
+ line: i,
566
+ col,
567
+ length: query.length,
568
+ context
569
+ });
570
+ searchFrom = col + 1;
571
+ }
572
+ }
573
+ return matches;
574
+ }
575
+ function showSearch(screen, draw, lines, opts, renderBackground) {
576
+ return new Promise((resolve) => {
577
+ let query = opts.initialQuery ?? "";
578
+ let replace = opts.initialReplace ?? "";
579
+ let queryCursorX = query.length;
580
+ let replaceCursorX = replace.length;
581
+ let activeField = 0;
582
+ let matches = [];
583
+ let listState = {
584
+ selectedIndex: 0,
585
+ scrollOffset: 0
586
+ };
587
+ const searchW = opts.width ?? Math.min(60, screen.width - 4);
588
+ const listH = Math.min(15, screen.height - 10);
589
+ updateMatches();
590
+ if (opts.lastSelectedIndex != null && opts.lastSelectedIndex < matches.length) listState = {
591
+ selectedIndex: opts.lastSelectedIndex,
592
+ scrollOffset: Math.max(0, opts.lastSelectedIndex - Math.floor(listH / 2))
593
+ };
594
+ let backgroundDrawn = false;
595
+ function renderSearch() {
596
+ if (!backgroundDrawn) {
597
+ renderBackground();
598
+ backgroundDrawn = true;
599
+ }
600
+ const totalH = 5 + listH + 1;
601
+ const x = Math.floor((screen.width - searchW) / 2);
602
+ const y = 1;
603
+ const inputW = searchW - 7;
604
+ draw.rect(x, y, searchW, totalH, {
605
+ fg: theme.border,
606
+ border: opts.border ?? "round",
607
+ fill: theme.fill
608
+ });
609
+ draw.text(x + Math.floor((searchW - 18) / 2), y, " Search & Replace ", { fg: theme.title });
610
+ if (query.length > 0) {
611
+ const countText = ` ${matches.length} `;
612
+ draw.text(x + searchW - countText.length - 1, y, countText, { fg: theme.matchCount });
613
+ }
614
+ const searchY = y + 1;
615
+ draw.text(x + 1, searchY, " ⌕", {
616
+ fg: theme.labelFg,
617
+ bg: theme.fill
618
+ });
619
+ for (let i = 0; i < inputW; i++) draw.char(x + 5 + i, searchY, " ", { bg: theme.inputBg });
620
+ if (query.length > 0) draw.text(x + 5, searchY, query.substring(0, inputW), {
621
+ fg: theme.inputFg,
622
+ bg: theme.inputBg
623
+ });
624
+ else draw.text(x + 5, searchY, "Search...", {
625
+ fg: theme.placeholder,
626
+ bg: theme.inputBg
627
+ });
628
+ if (activeField === 0) draw.char(x + 4, searchY, "▎", { fg: theme.inputActiveBorder });
629
+ const replaceY = y + 2;
630
+ draw.text(x + 1, replaceY, " →", {
631
+ fg: theme.labelFg,
632
+ bg: theme.fill
633
+ });
634
+ for (let i = 0; i < inputW; i++) draw.char(x + 5 + i, replaceY, " ", { bg: theme.inputBg });
635
+ if (replace.length > 0) draw.text(x + 5, replaceY, replace.substring(0, inputW), {
636
+ fg: theme.replaceFg,
637
+ bg: theme.inputBg
638
+ });
639
+ else draw.text(x + 5, replaceY, "Replace...", {
640
+ fg: theme.placeholder,
641
+ bg: theme.inputBg
642
+ });
643
+ if (activeField === 1) draw.char(x + 4, replaceY, "▎", { fg: theme.inputActiveBorder });
644
+ const hintY = y + 3;
645
+ const hint = replace.length > 0 ? " Enter=Replace ^A=All ↑↓=Nav Esc=Close" : " Enter=Jump ↑↓=Navigate Tab=Replace Esc=Close";
646
+ draw.text(x + 1, hintY, hint.substring(0, searchW - 2), {
647
+ fg: [
648
+ 70,
649
+ 75,
650
+ 85
651
+ ],
652
+ bg: theme.fill
653
+ });
654
+ const sepY = y + 4;
655
+ for (let i = 1; i < searchW - 1; i++) draw.char(x + i, sepY, "─", { fg: theme.border });
656
+ const listItems = matches.map((m) => ({
657
+ label: ` ${String(m.line + 1).padStart(4)} │ ${m.context}`,
658
+ value: `${m.line}:${m.col}`
659
+ }));
660
+ drawList(draw, {
661
+ x: x + 1,
662
+ y: sepY + 1,
663
+ width: searchW - 2,
664
+ height: listH,
665
+ items: listItems,
666
+ selectedIndex: listState.selectedIndex,
667
+ scrollOffset: listState.scrollOffset,
668
+ bg: theme.fill
669
+ });
670
+ screen.hideCursor();
671
+ draw.flush();
672
+ if (activeField === 0) screen.moveTo(x + 5 + queryCursorX, searchY);
673
+ else screen.moveTo(x + 5 + replaceCursorX, replaceY);
674
+ screen.showCursor();
675
+ }
676
+ function updateMatches(preserveSelection = false) {
677
+ matches = findMatches(lines, query, opts.maxResults ?? 500);
678
+ if (preserveSelection && listState.selectedIndex < matches.length) return;
679
+ const cl = opts.cursorLine ?? 0;
680
+ const cc = opts.cursorCol ?? 0;
681
+ let best = 0;
682
+ for (let i = 0; i < matches.length; i++) {
683
+ const m = matches[i];
684
+ if (m.line > cl || m.line === cl && m.col >= cc) {
685
+ best = i;
686
+ break;
687
+ }
688
+ best = i;
689
+ }
690
+ listState = {
691
+ selectedIndex: best,
692
+ scrollOffset: Math.max(0, best - Math.floor(listH / 2))
693
+ };
694
+ }
695
+ function getActiveText() {
696
+ return activeField === 0 ? query : replace;
697
+ }
698
+ function getActiveCursorX() {
699
+ return activeField === 0 ? queryCursorX : replaceCursorX;
700
+ }
701
+ function setActiveText(text) {
702
+ if (activeField === 0) {
703
+ query = text;
704
+ updateMatches();
705
+ } else replace = text;
706
+ }
707
+ function setActiveCursorX(x) {
708
+ if (activeField === 0) queryCursorX = x;
709
+ else replaceCursorX = x;
710
+ }
711
+ function onData(data) {
712
+ if (data[0] === 27 && data.length === 1) {
713
+ cleanup();
714
+ resolve({
715
+ type: "cancel",
716
+ query,
717
+ replacement: replace,
718
+ selectedIndex: listState.selectedIndex
719
+ });
720
+ return;
721
+ }
722
+ if (data[0] === 9) {
723
+ activeField = activeField === 0 ? 1 : 0;
724
+ renderSearch();
725
+ return;
726
+ }
727
+ if (data[0] === 13) {
728
+ if (matches.length === 0) return;
729
+ cleanup();
730
+ if (replace.length > 0) resolve({
731
+ type: "replace",
732
+ match: matches[listState.selectedIndex],
733
+ query,
734
+ replacement: replace,
735
+ selectedIndex: listState.selectedIndex
736
+ });
737
+ else resolve({
738
+ type: "jump",
739
+ match: matches[listState.selectedIndex],
740
+ query,
741
+ selectedIndex: listState.selectedIndex
742
+ });
743
+ return;
744
+ }
745
+ if (data.length === 1 && data[0] === 1 && replace.length > 0 && matches.length > 0) {
746
+ cleanup();
747
+ resolve({
748
+ type: "replaceAll",
749
+ matches: [...matches],
750
+ query,
751
+ replacement: replace,
752
+ selectedIndex: listState.selectedIndex
753
+ });
754
+ return;
755
+ }
756
+ if (data[0] === 27 && data[1] === 91) {
757
+ const seq = data.toString("utf8", 2);
758
+ if (seq === "Z") {
759
+ activeField = activeField === 0 ? 1 : 0;
760
+ renderSearch();
761
+ return;
762
+ }
763
+ if (seq === "A" && matches.length > 0) {
764
+ listState = listMoveUp(listState, matches.length);
765
+ renderSearch();
766
+ return;
767
+ }
768
+ if (seq === "B" && matches.length > 0) {
769
+ listState = listMoveDown(listState, matches.length, listH);
770
+ renderSearch();
771
+ return;
772
+ }
773
+ if (seq === "C") {
774
+ setActiveCursorX(Math.min(getActiveCursorX() + 1, getActiveText().length));
775
+ renderSearch();
776
+ return;
777
+ }
778
+ if (seq === "D") {
779
+ setActiveCursorX(Math.max(getActiveCursorX() - 1, 0));
780
+ renderSearch();
781
+ return;
782
+ }
783
+ if (seq === "H") {
784
+ setActiveCursorX(0);
785
+ renderSearch();
786
+ return;
787
+ }
788
+ if (seq === "F") {
789
+ setActiveCursorX(getActiveText().length);
790
+ renderSearch();
791
+ return;
792
+ }
793
+ return;
794
+ }
795
+ if (data[0] === 127) {
796
+ const cx = getActiveCursorX();
797
+ if (cx > 0) {
798
+ const text = getActiveText();
799
+ setActiveText(text.substring(0, cx - 1) + text.substring(cx));
800
+ setActiveCursorX(cx - 1);
801
+ renderSearch();
802
+ }
803
+ return;
804
+ }
805
+ if (data[0] === 8) {
806
+ const text = getActiveText();
807
+ const cx = getActiveCursorX();
808
+ setActiveText(text.substring(cx));
809
+ setActiveCursorX(0);
810
+ renderSearch();
811
+ return;
812
+ }
813
+ const ch = data.toString("utf8");
814
+ if ((ch.codePointAt(0) ?? 0) >= 32) {
815
+ const text = getActiveText();
816
+ const cx = getActiveCursorX();
817
+ setActiveText(text.substring(0, cx) + ch + text.substring(cx));
818
+ setActiveCursorX(cx + ch.length);
819
+ renderSearch();
820
+ }
821
+ }
822
+ function cleanup() {
823
+ process.stdin.removeListener("data", onData);
824
+ }
825
+ process.stdin.on("data", onData);
826
+ renderSearch();
827
+ });
828
+ }
829
+ //#endregion
830
+ //#region packages/editor/src/editor.ts
831
+ function createEditor(filePath) {
832
+ if (!filePath) return {
833
+ lines: [""],
834
+ filePath: "",
835
+ dirty: false,
836
+ clipboardParts: [],
837
+ isNewFile: true
838
+ };
839
+ if (!existsSync(filePath)) return {
840
+ lines: [""],
841
+ filePath,
842
+ dirty: false,
843
+ clipboardParts: [],
844
+ isNewFile: true
845
+ };
846
+ return {
847
+ lines: readFileSync(filePath, "utf8").split("\n"),
848
+ filePath,
849
+ dirty: false,
850
+ clipboardParts: [],
851
+ isNewFile: false
852
+ };
853
+ }
854
+ function saveAs(state, filePath) {
855
+ state.filePath = filePath;
856
+ writeFileSync(filePath, state.lines.join("\n"), "utf8");
857
+ state.dirty = false;
858
+ state.isNewFile = false;
859
+ }
860
+ function insertChar(state, x, y, ch) {
861
+ const line = state.lines[y];
862
+ state.lines[y] = line.substring(0, x) + ch + line.substring(x);
863
+ state.dirty = true;
864
+ return x + ch.length;
865
+ }
866
+ function insertNewline(state, x, y) {
867
+ const before = state.lines[y].substring(0, x);
868
+ const after = state.lines[y].substring(x);
869
+ state.lines[y] = before;
870
+ state.lines.splice(y + 1, 0, after);
871
+ state.dirty = true;
872
+ return {
873
+ x: 0,
874
+ y: y + 1
875
+ };
876
+ }
877
+ function deleteCharBack(state, x, y) {
878
+ if (x > 0) {
879
+ const line = state.lines[y];
880
+ state.lines[y] = line.substring(0, x - 1) + line.substring(x);
881
+ state.dirty = true;
882
+ return {
883
+ x: x - 1,
884
+ y
885
+ };
886
+ }
887
+ if (y > 0) {
888
+ const newX = state.lines[y - 1].length;
889
+ state.lines[y - 1] += state.lines[y];
890
+ state.lines.splice(y, 1);
891
+ state.dirty = true;
892
+ return {
893
+ x: newX,
894
+ y: y - 1
895
+ };
896
+ }
897
+ return {
898
+ x,
899
+ y
900
+ };
901
+ }
902
+ function deleteWordBack(state, x, y, boundaryX) {
903
+ if (x > 0) {
904
+ const line = state.lines[y];
905
+ state.lines[y] = line.substring(0, boundaryX) + line.substring(x);
906
+ state.dirty = true;
907
+ return {
908
+ x: boundaryX,
909
+ y
910
+ };
911
+ }
912
+ if (y > 0) {
913
+ const newX = state.lines[y - 1].length;
914
+ state.lines[y - 1] += state.lines[y];
915
+ state.lines.splice(y, 1);
916
+ state.dirty = true;
917
+ return {
918
+ x: newX,
919
+ y: y - 1
920
+ };
921
+ }
922
+ return {
923
+ x,
924
+ y
925
+ };
926
+ }
927
+ function deleteWordForward(state, x, y, boundaryX) {
928
+ if (x < state.lines[y].length) {
929
+ const line = state.lines[y];
930
+ state.lines[y] = line.substring(0, x) + line.substring(boundaryX);
931
+ } else if (y < state.lines.length - 1) {
932
+ state.lines[y] += state.lines[y + 1];
933
+ state.lines.splice(y + 1, 1);
934
+ }
935
+ state.dirty = true;
936
+ }
937
+ function deleteCharForward(state, x, y) {
938
+ if (x < state.lines[y].length) {
939
+ const line = state.lines[y];
940
+ state.lines[y] = line.substring(0, x) + line.substring(x + 1);
941
+ } else if (y < state.lines.length - 1) {
942
+ state.lines[y] += state.lines[y + 1];
943
+ state.lines.splice(y + 1, 1);
944
+ }
945
+ state.dirty = true;
946
+ }
947
+ function insertTab(state, x, y) {
948
+ const line = state.lines[y];
949
+ state.lines[y] = line.substring(0, x) + " " + line.substring(x);
950
+ state.dirty = true;
951
+ return x + 2;
952
+ }
953
+ function moveLinesUp(state, startLine, endLine) {
954
+ if (startLine <= 0) return false;
955
+ const moved = state.lines.splice(startLine, endLine - startLine + 1);
956
+ state.lines.splice(startLine - 1, 0, ...moved);
957
+ state.dirty = true;
958
+ return true;
959
+ }
960
+ function moveLinesDown(state, startLine, endLine) {
961
+ if (endLine >= state.lines.length - 1) return false;
962
+ const moved = state.lines.splice(startLine, endLine - startLine + 1);
963
+ state.lines.splice(startLine + 1, 0, ...moved);
964
+ state.dirty = true;
965
+ return true;
966
+ }
967
+ function pasteText(state, x, y, text) {
968
+ const pasteLines = text.split("\n");
969
+ if (pasteLines.length === 1) {
970
+ const line = state.lines[y];
971
+ state.lines[y] = line.substring(0, x) + pasteLines[0] + line.substring(x);
972
+ state.dirty = true;
973
+ return {
974
+ x: x + pasteLines[0].length,
975
+ y
976
+ };
977
+ }
978
+ const before = state.lines[y].substring(0, x);
979
+ const after = state.lines[y].substring(x);
980
+ state.lines[y] = before + pasteLines[0];
981
+ for (let i = 1; i < pasteLines.length - 1; i++) state.lines.splice(y + i, 0, pasteLines[i]);
982
+ const lastLine = pasteLines[pasteLines.length - 1];
983
+ state.lines.splice(y + pasteLines.length - 1, 0, lastLine + after);
984
+ state.dirty = true;
985
+ return {
986
+ x: lastLine.length,
987
+ y: y + pasteLines.length - 1
988
+ };
989
+ }
990
+ //#endregion
991
+ //#region packages/editor/src/cursor-manager.ts
992
+ function wordBoundaryLeft(line, col) {
993
+ if (col <= 0) return 0;
994
+ let i = col - 1;
995
+ while (i > 0 && /\s/.test(line[i])) i--;
996
+ if (i >= 0 && /[^\w\s]/.test(line[i])) {
997
+ while (i > 0 && /[^\w\s]/.test(line[i - 1])) i--;
998
+ return i;
999
+ }
1000
+ while (i > 0 && /\w/.test(line[i - 1])) i--;
1001
+ return i;
1002
+ }
1003
+ function wordBoundaryRight(line, col) {
1004
+ const len = line.length;
1005
+ if (col >= len) return len;
1006
+ let i = col;
1007
+ if (/\w/.test(line[i])) while (i < len && /\w/.test(line[i])) i++;
1008
+ else if (/[^\w\s]/.test(line[i])) while (i < len && /[^\w\s]/.test(line[i])) i++;
1009
+ while (i < len && /\s/.test(line[i])) i++;
1010
+ return i;
1011
+ }
1012
+ function getSelRange(c) {
1013
+ if (!c.anchor) return null;
1014
+ const a = c.anchor;
1015
+ const b = {
1016
+ x: c.x,
1017
+ y: c.y
1018
+ };
1019
+ if (a.y < b.y || a.y === b.y && a.x <= b.x) return {
1020
+ start: a,
1021
+ end: b
1022
+ };
1023
+ return {
1024
+ start: b,
1025
+ end: a
1026
+ };
1027
+ }
1028
+ function isInRange(range, lineIdx, colIdx) {
1029
+ if (!range) return false;
1030
+ if (lineIdx < range.start.y || lineIdx > range.end.y) return false;
1031
+ if (lineIdx === range.start.y && colIdx < range.start.x) return false;
1032
+ if (lineIdx === range.end.y && colIdx >= range.end.x) return false;
1033
+ return true;
1034
+ }
1035
+ function clampCursor(c, lines) {
1036
+ if (c.y < 0) c.y = 0;
1037
+ if (c.y >= lines.length) c.y = lines.length - 1;
1038
+ const lineLen = lines[c.y].length;
1039
+ if (c.x < 0) c.x = 0;
1040
+ if (c.x > lineLen) c.x = lineLen;
1041
+ }
1042
+ function moveLeftOne(c, lines) {
1043
+ if (c.x > 0) c.x--;
1044
+ else if (c.y > 0) {
1045
+ c.y--;
1046
+ c.x = lines[c.y].length;
1047
+ }
1048
+ }
1049
+ function moveRightOne(c, lines) {
1050
+ if (c.x < lines[c.y].length) c.x++;
1051
+ else if (c.y < lines.length - 1) {
1052
+ c.y++;
1053
+ c.x = 0;
1054
+ }
1055
+ }
1056
+ function createCursorManager() {
1057
+ const cursors = [{
1058
+ x: 0,
1059
+ y: 0,
1060
+ anchor: null
1061
+ }];
1062
+ let scrollX = 0;
1063
+ let scrollY = 0;
1064
+ return {
1065
+ get primary() {
1066
+ return cursors[0];
1067
+ },
1068
+ get all() {
1069
+ return cursors;
1070
+ },
1071
+ get count() {
1072
+ return cursors.length;
1073
+ },
1074
+ get isMulti() {
1075
+ return cursors.length > 1;
1076
+ },
1077
+ get scrollX() {
1078
+ return scrollX;
1079
+ },
1080
+ set scrollX(v) {
1081
+ scrollX = v;
1082
+ },
1083
+ get scrollY() {
1084
+ return scrollY;
1085
+ },
1086
+ set scrollY(v) {
1087
+ scrollY = v;
1088
+ },
1089
+ saveState() {
1090
+ return {
1091
+ cursors: cursors.map((c) => ({
1092
+ x: c.x,
1093
+ y: c.y,
1094
+ anchor: c.anchor ? { ...c.anchor } : null
1095
+ })),
1096
+ scrollX,
1097
+ scrollY
1098
+ };
1099
+ },
1100
+ restoreState(snapshot) {
1101
+ cursors.length = 0;
1102
+ for (const c of snapshot.cursors) cursors.push({
1103
+ x: c.x,
1104
+ y: c.y,
1105
+ anchor: c.anchor ? { ...c.anchor } : null
1106
+ });
1107
+ scrollX = snapshot.scrollX;
1108
+ scrollY = snapshot.scrollY;
1109
+ },
1110
+ addAbove(x, y) {
1111
+ cursors.push({
1112
+ x,
1113
+ y,
1114
+ anchor: null
1115
+ });
1116
+ },
1117
+ addBelow() {
1118
+ const allY = cursors.map((c) => c.y);
1119
+ const newY = Math.max(...allY) + 1;
1120
+ const newX = cursors[0].x;
1121
+ cursors.push({
1122
+ x: newX,
1123
+ y: newY,
1124
+ anchor: null
1125
+ });
1126
+ },
1127
+ clearExtras() {
1128
+ cursors.splice(1);
1129
+ },
1130
+ getSelection(c) {
1131
+ return getSelRange(c);
1132
+ },
1133
+ getSelectedText(c, lines) {
1134
+ const range = getSelRange(c);
1135
+ if (!range) return "";
1136
+ if (range.start.y === range.end.y) return lines[range.start.y].substring(range.start.x, range.end.x);
1137
+ const parts = [];
1138
+ parts.push(lines[range.start.y].substring(range.start.x));
1139
+ for (let y = range.start.y + 1; y < range.end.y; y++) parts.push(lines[y]);
1140
+ parts.push(lines[range.end.y].substring(0, range.end.x));
1141
+ return parts.join("\n");
1142
+ },
1143
+ isCellSelected(lineIdx, colIdx) {
1144
+ return cursors.some((c) => isInRange(getSelRange(c), lineIdx, colIdx));
1145
+ },
1146
+ isCellExtraCursor(lineIdx, colIdx) {
1147
+ return cursors.slice(1).some((c) => c.y === lineIdx && c.x === colIdx);
1148
+ },
1149
+ startSelectionAll() {
1150
+ for (const c of cursors) if (!c.anchor) c.anchor = {
1151
+ x: c.x,
1152
+ y: c.y
1153
+ };
1154
+ },
1155
+ clearSelectionAll() {
1156
+ for (const c of cursors) c.anchor = null;
1157
+ },
1158
+ collapseEmptySelections() {
1159
+ for (const c of cursors) if (c.anchor && c.x === c.anchor.x && c.y === c.anchor.y) c.anchor = null;
1160
+ },
1161
+ forEachBottomUp(fn) {
1162
+ const indexed = cursors.map((c, i) => ({
1163
+ c,
1164
+ i
1165
+ }));
1166
+ indexed.sort((a, b) => b.c.y - a.c.y || b.c.x - a.c.x);
1167
+ for (const { c, i } of indexed) fn(c, i === 0);
1168
+ },
1169
+ forEachAll(fn) {
1170
+ cursors.forEach((c, i) => fn(c, i === 0));
1171
+ },
1172
+ moveAll(direction, lines, pageSize) {
1173
+ for (const c of cursors) switch (direction) {
1174
+ case "up":
1175
+ c.y--;
1176
+ break;
1177
+ case "down":
1178
+ c.y++;
1179
+ break;
1180
+ case "left":
1181
+ moveLeftOne(c, lines);
1182
+ break;
1183
+ case "right":
1184
+ moveRightOne(c, lines);
1185
+ break;
1186
+ case "home":
1187
+ c.x = 0;
1188
+ break;
1189
+ case "end":
1190
+ c.x = lines[c.y]?.length ?? 0;
1191
+ break;
1192
+ case "pageup":
1193
+ c.y -= pageSize;
1194
+ break;
1195
+ case "pagedown":
1196
+ c.y += pageSize;
1197
+ break;
1198
+ }
1199
+ },
1200
+ moveWordAll(direction, lines) {
1201
+ for (const c of cursors) if (direction === "left") {
1202
+ if (c.x > 0) c.x = wordBoundaryLeft(lines[c.y], c.x);
1203
+ else if (c.y > 0) {
1204
+ c.y--;
1205
+ c.x = lines[c.y].length;
1206
+ }
1207
+ } else if (c.x < lines[c.y].length) c.x = wordBoundaryRight(lines[c.y], c.x);
1208
+ else if (c.y < lines.length - 1) {
1209
+ c.y++;
1210
+ c.x = 0;
1211
+ }
1212
+ },
1213
+ mergeIntoSelection(direction) {
1214
+ if (cursors.length <= 1) return;
1215
+ const all = [];
1216
+ for (const c of cursors) {
1217
+ all.push({
1218
+ x: c.x,
1219
+ y: c.y
1220
+ });
1221
+ if (c.anchor) all.push(c.anchor);
1222
+ }
1223
+ let min = all[0], max = all[0];
1224
+ for (const p of all) {
1225
+ if (p.y < min.y || p.y === min.y && p.x < min.x) min = p;
1226
+ if (p.y > max.y || p.y === max.y && p.x > max.x) max = p;
1227
+ }
1228
+ cursors.splice(1);
1229
+ if (direction === "down") {
1230
+ cursors[0].anchor = { ...min };
1231
+ cursors[0].x = max.x;
1232
+ cursors[0].y = max.y;
1233
+ } else {
1234
+ cursors[0].anchor = { ...max };
1235
+ cursors[0].x = min.x;
1236
+ cursors[0].y = min.y;
1237
+ }
1238
+ },
1239
+ clampAll(lines) {
1240
+ for (const c of cursors) clampCursor(c, lines);
1241
+ },
1242
+ dedup() {
1243
+ const seen = /* @__PURE__ */ new Set();
1244
+ seen.add(`${cursors[0].x},${cursors[0].y}`);
1245
+ for (let i = cursors.length - 1; i >= 1; i--) {
1246
+ const key = `${cursors[i].x},${cursors[i].y}`;
1247
+ if (seen.has(key)) cursors.splice(i, 1);
1248
+ else seen.add(key);
1249
+ }
1250
+ },
1251
+ ensureVisible(viewW, viewH) {
1252
+ const p = cursors[0];
1253
+ if (p.y < scrollY) scrollY = p.y;
1254
+ if (p.y >= scrollY + viewH) scrollY = p.y - viewH + 1;
1255
+ if (p.x < scrollX) scrollX = p.x;
1256
+ if (p.x >= scrollX + viewW) scrollX = p.x - viewW + 1;
1257
+ },
1258
+ deleteSelection(c, lines) {
1259
+ const range = getSelRange(c);
1260
+ if (!range) return;
1261
+ if (range.start.y === range.end.y) {
1262
+ const line = lines[range.start.y];
1263
+ lines[range.start.y] = line.substring(0, range.start.x) + line.substring(range.end.x);
1264
+ } else {
1265
+ const before = lines[range.start.y].substring(0, range.start.x);
1266
+ const after = lines[range.end.y].substring(range.end.x);
1267
+ lines[range.start.y] = before + after;
1268
+ lines.splice(range.start.y + 1, range.end.y - range.start.y);
1269
+ }
1270
+ c.x = range.start.x;
1271
+ c.y = range.start.y;
1272
+ c.anchor = null;
1273
+ }
1274
+ };
1275
+ }
1276
+ //#endregion
1277
+ //#region packages/editor/src/undo.ts
1278
+ var GROUP_THRESHOLD = 800;
1279
+ function truncate(s, max) {
1280
+ return s.length > max ? s.substring(0, max - 1) + "…" : s;
1281
+ }
1282
+ function createUndoManager() {
1283
+ const undoStack = [];
1284
+ const redoStack = [];
1285
+ let pendingLabel = "";
1286
+ let pendingCursorBefore = {
1287
+ x: 0,
1288
+ y: 0
1289
+ };
1290
+ let pendingLinesBefore = [];
1291
+ let pendingCursorState = null;
1292
+ let hasPending = false;
1293
+ function buildEntry(label, cursorBefore, cursorAfter, linesBefore, linesAfter) {
1294
+ const diffs = [];
1295
+ const removed = [];
1296
+ const inserted = [];
1297
+ const minLen = Math.min(linesBefore.length, linesAfter.length);
1298
+ for (let i = 0; i < minLen; i++) if (linesBefore[i] !== linesAfter[i]) diffs.push({
1299
+ index: i,
1300
+ old: linesBefore[i],
1301
+ new: linesAfter[i]
1302
+ });
1303
+ for (let i = minLen; i < linesBefore.length; i++) removed.push({
1304
+ index: i,
1305
+ content: linesBefore[i]
1306
+ });
1307
+ for (let i = minLen; i < linesAfter.length; i++) inserted.push({
1308
+ index: i,
1309
+ content: linesAfter[i]
1310
+ });
1311
+ return {
1312
+ label,
1313
+ timestamp: Date.now(),
1314
+ cursorBefore,
1315
+ cursorAfter,
1316
+ cursorStateBefore: null,
1317
+ cursorStateAfter: null,
1318
+ diffs,
1319
+ removed,
1320
+ inserted
1321
+ };
1322
+ }
1323
+ function applyUndo(entry, lines, cursor) {
1324
+ const result = [...lines];
1325
+ for (let i = entry.inserted.length - 1; i >= 0; i--) {
1326
+ const ins = entry.inserted[i];
1327
+ if (ins.index < result.length) result.splice(ins.index, 1);
1328
+ }
1329
+ for (const rem of entry.removed) result.splice(rem.index, 0, rem.content);
1330
+ for (const diff of entry.diffs) if (diff.index < result.length) result[diff.index] = diff.old;
1331
+ cursor.x = entry.cursorBefore.x;
1332
+ cursor.y = entry.cursorBefore.y;
1333
+ return result;
1334
+ }
1335
+ function applyRedo(entry, lines, cursor) {
1336
+ const result = [...lines];
1337
+ for (const diff of entry.diffs) if (diff.index < result.length) result[diff.index] = diff.new;
1338
+ for (let i = entry.removed.length - 1; i >= 0; i--) {
1339
+ const rem = entry.removed[i];
1340
+ if (rem.index < result.length) result.splice(rem.index, 1);
1341
+ }
1342
+ for (const ins of entry.inserted) result.splice(ins.index, 0, ins.content);
1343
+ cursor.x = entry.cursorAfter.x;
1344
+ cursor.y = entry.cursorAfter.y;
1345
+ return result;
1346
+ }
1347
+ function canGroup(label) {
1348
+ if (undoStack.length === 0) return false;
1349
+ const last = undoStack[undoStack.length - 1];
1350
+ if (last.label !== label) return false;
1351
+ if (Date.now() - last.timestamp > GROUP_THRESHOLD) return false;
1352
+ if (label !== "type") return false;
1353
+ return true;
1354
+ }
1355
+ return {
1356
+ snapshot(label, cursorBefore, linesBefore, cursorState) {
1357
+ pendingLabel = label;
1358
+ pendingCursorBefore = { ...cursorBefore };
1359
+ pendingLinesBefore = [...linesBefore];
1360
+ pendingCursorState = cursorState ?? null;
1361
+ hasPending = true;
1362
+ },
1363
+ commit(cursorAfter, linesAfter, cursorState) {
1364
+ if (!hasPending) return;
1365
+ hasPending = false;
1366
+ if (pendingLinesBefore.length === linesAfter.length && pendingLinesBefore.every((l, i) => l === linesAfter[i])) return;
1367
+ if (canGroup(pendingLabel)) {
1368
+ const last = undoStack[undoStack.length - 1];
1369
+ const merged = buildEntry(pendingLabel, last.cursorBefore, { ...cursorAfter }, applyUndo(last, linesAfter, { ...cursorAfter }), [...linesAfter]);
1370
+ merged.cursorBefore = last.cursorBefore;
1371
+ merged.cursorStateBefore = last.cursorStateBefore;
1372
+ merged.cursorStateAfter = cursorState ?? null;
1373
+ undoStack[undoStack.length - 1] = merged;
1374
+ } else {
1375
+ const entry = buildEntry(pendingLabel, pendingCursorBefore, { ...cursorAfter }, pendingLinesBefore, [...linesAfter]);
1376
+ entry.cursorStateBefore = pendingCursorState;
1377
+ entry.cursorStateAfter = cursorState ?? null;
1378
+ undoStack.push(entry);
1379
+ }
1380
+ redoStack.length = 0;
1381
+ },
1382
+ undo(lines, cursor) {
1383
+ const entry = undoStack.pop();
1384
+ if (!entry) return null;
1385
+ redoStack.push(entry);
1386
+ return {
1387
+ lines: applyUndo(entry, lines, cursor),
1388
+ cursorState: entry.cursorStateBefore
1389
+ };
1390
+ },
1391
+ redo(lines, cursor) {
1392
+ const entry = redoStack.pop();
1393
+ if (!entry) return null;
1394
+ undoStack.push(entry);
1395
+ return {
1396
+ lines: applyRedo(entry, lines, cursor),
1397
+ cursorState: entry.cursorStateAfter
1398
+ };
1399
+ },
1400
+ getHistory() {
1401
+ return undoStack;
1402
+ },
1403
+ describeEntry(entry) {
1404
+ const line = entry.cursorAfter.y + 1;
1405
+ if (entry.diffs.length > 0) {
1406
+ const diff = entry.diffs[0];
1407
+ const added = diff.new.length > diff.old.length ? diff.new.substring(diff.old.length === 0 ? 0 : diff.old.length) : "";
1408
+ const removed = diff.old.length > diff.new.length ? diff.old.substring(diff.new.length === 0 ? 0 : diff.new.length) : "";
1409
+ if (entry.label === "type" && added) return `Ln ${line}: +"${truncate(added, 25)}"`;
1410
+ if (entry.label === "backspace" && removed) return `Ln ${line}: -"${truncate(removed, 25)}"`;
1411
+ if (entry.label === "delete" && removed) return `Ln ${line}: -"${truncate(removed, 25)}"`;
1412
+ if (added && removed) return `Ln ${line}: "${truncate(removed, 12)}" → "${truncate(added, 12)}"`;
1413
+ if (added) return `Ln ${line}: +"${truncate(added, 25)}"`;
1414
+ if (removed) return `Ln ${line}: -"${truncate(removed, 25)}"`;
1415
+ }
1416
+ if (entry.inserted.length > 0) {
1417
+ const text = entry.inserted[0].content;
1418
+ return `Ln ${line}: +line "${truncate(text || "(empty)", 20)}"`;
1419
+ }
1420
+ if (entry.removed.length > 0) {
1421
+ const text = entry.removed[0].content;
1422
+ return `Ln ${line}: -line "${truncate(text || "(empty)", 20)}"`;
1423
+ }
1424
+ if (entry.label === "enter") return `Ln ${line}: new line`;
1425
+ if (entry.label === "tab") return `Ln ${line}: indent`;
1426
+ if (entry.label === "cut") return `Ln ${line}: cut`;
1427
+ if (entry.label === "paste") return `Ln ${line}: paste`;
1428
+ return `Ln ${line}: ${entry.label}`;
1429
+ },
1430
+ jumpTo(index, lines, cursor) {
1431
+ let current = lines;
1432
+ while (undoStack.length > index + 1) {
1433
+ const entry = undoStack.pop();
1434
+ redoStack.push(entry);
1435
+ current = applyUndo(entry, current, cursor);
1436
+ }
1437
+ while (undoStack.length < index + 1 && redoStack.length > 0) {
1438
+ const entry = redoStack.pop();
1439
+ undoStack.push(entry);
1440
+ current = applyRedo(entry, current, cursor);
1441
+ }
1442
+ return current;
1443
+ }
1444
+ };
1445
+ }
1446
+ //#endregion
1447
+ //#region packages/editor/src/keypress.ts
1448
+ function parseKey(data) {
1449
+ const key = {
1450
+ name: "",
1451
+ ctrl: false,
1452
+ shift: false,
1453
+ alt: false,
1454
+ raw: data
1455
+ };
1456
+ if (data.length === 1 && data[0] === 13) {
1457
+ key.name = "enter";
1458
+ return key;
1459
+ }
1460
+ if (data.length === 1 && data[0] === 9) {
1461
+ key.name = "tab";
1462
+ return key;
1463
+ }
1464
+ if (data.length === 1 && data[0] === 8) {
1465
+ key.name = "backspace";
1466
+ key.ctrl = true;
1467
+ return key;
1468
+ }
1469
+ if (data.length === 1 && data[0] < 27) {
1470
+ key.ctrl = true;
1471
+ key.name = String.fromCharCode(data[0] + 96);
1472
+ return key;
1473
+ }
1474
+ if (data.length === 2 && data[0] === 27 && data[1] >= 32) {
1475
+ key.alt = true;
1476
+ key.name = String.fromCharCode(data[1]);
1477
+ return key;
1478
+ }
1479
+ if (data[0] === 27 && data[1] === 79) {
1480
+ switch (data[2]) {
1481
+ case 80:
1482
+ key.name = "f1";
1483
+ break;
1484
+ case 81:
1485
+ key.name = "f2";
1486
+ break;
1487
+ case 82:
1488
+ key.name = "f3";
1489
+ break;
1490
+ case 83:
1491
+ key.name = "f4";
1492
+ break;
1493
+ default:
1494
+ key.name = "unknown";
1495
+ break;
1496
+ }
1497
+ return key;
1498
+ }
1499
+ if (data[0] === 27 && data[1] === 91) {
1500
+ switch (data.toString("utf8", 2)) {
1501
+ case "A":
1502
+ key.name = "up";
1503
+ break;
1504
+ case "B":
1505
+ key.name = "down";
1506
+ break;
1507
+ case "C":
1508
+ key.name = "right";
1509
+ break;
1510
+ case "D":
1511
+ key.name = "left";
1512
+ break;
1513
+ case "H":
1514
+ key.name = "home";
1515
+ break;
1516
+ case "F":
1517
+ key.name = "end";
1518
+ break;
1519
+ case "5~":
1520
+ key.name = "pageup";
1521
+ break;
1522
+ case "6~":
1523
+ key.name = "pagedown";
1524
+ break;
1525
+ case "3~":
1526
+ key.name = "delete";
1527
+ break;
1528
+ case "3;5~":
1529
+ key.name = "delete";
1530
+ key.ctrl = true;
1531
+ break;
1532
+ case "1;2A":
1533
+ key.name = "up";
1534
+ key.shift = true;
1535
+ break;
1536
+ case "1;2B":
1537
+ key.name = "down";
1538
+ key.shift = true;
1539
+ break;
1540
+ case "1;2C":
1541
+ key.name = "right";
1542
+ key.shift = true;
1543
+ break;
1544
+ case "1;2D":
1545
+ key.name = "left";
1546
+ key.shift = true;
1547
+ break;
1548
+ case "1;2H":
1549
+ key.name = "home";
1550
+ key.shift = true;
1551
+ break;
1552
+ case "1;2F":
1553
+ key.name = "end";
1554
+ key.shift = true;
1555
+ break;
1556
+ case "1;3A":
1557
+ key.name = "up";
1558
+ key.alt = true;
1559
+ break;
1560
+ case "1;3B":
1561
+ key.name = "down";
1562
+ key.alt = true;
1563
+ break;
1564
+ case "1;5A":
1565
+ key.name = "up";
1566
+ key.ctrl = true;
1567
+ break;
1568
+ case "1;5B":
1569
+ key.name = "down";
1570
+ key.ctrl = true;
1571
+ break;
1572
+ case "1;5C":
1573
+ key.name = "right";
1574
+ key.ctrl = true;
1575
+ break;
1576
+ case "1;5D":
1577
+ key.name = "left";
1578
+ key.ctrl = true;
1579
+ break;
1580
+ case "1;6A":
1581
+ key.name = "up";
1582
+ key.ctrl = true;
1583
+ key.shift = true;
1584
+ break;
1585
+ case "1;6B":
1586
+ key.name = "down";
1587
+ key.ctrl = true;
1588
+ key.shift = true;
1589
+ break;
1590
+ case "1;6C":
1591
+ key.name = "right";
1592
+ key.ctrl = true;
1593
+ key.shift = true;
1594
+ break;
1595
+ case "1;6D":
1596
+ key.name = "left";
1597
+ key.ctrl = true;
1598
+ key.shift = true;
1599
+ break;
1600
+ case "1;7A":
1601
+ key.name = "up";
1602
+ key.ctrl = true;
1603
+ key.alt = true;
1604
+ break;
1605
+ case "1;7B":
1606
+ key.name = "down";
1607
+ key.ctrl = true;
1608
+ key.alt = true;
1609
+ break;
1610
+ case "1;7C":
1611
+ key.name = "right";
1612
+ key.ctrl = true;
1613
+ key.alt = true;
1614
+ break;
1615
+ case "1;7D":
1616
+ key.name = "left";
1617
+ key.ctrl = true;
1618
+ key.alt = true;
1619
+ break;
1620
+ default:
1621
+ key.name = "unknown";
1622
+ break;
1623
+ }
1624
+ return key;
1625
+ }
1626
+ if (data[0] === 127) {
1627
+ key.name = "backspace";
1628
+ return key;
1629
+ }
1630
+ key.name = data.toString("utf8");
1631
+ return key;
1632
+ }
1633
+ //#endregion
1634
+ //#region packages/editor/src/plugins/context.ts
1635
+ function buildContext(editor, cm, viewport, action) {
1636
+ const allCursors = cm.all.map((c) => ({
1637
+ position: {
1638
+ line: c.y,
1639
+ col: c.x
1640
+ },
1641
+ anchor: c.anchor ? {
1642
+ line: c.anchor.y,
1643
+ col: c.anchor.x
1644
+ } : null
1645
+ }));
1646
+ return {
1647
+ filePath: editor.filePath,
1648
+ fileName: basename(editor.filePath),
1649
+ extension: extname(editor.filePath),
1650
+ lines: editor.lines,
1651
+ lineCount: editor.lines.length,
1652
+ cursors: allCursors,
1653
+ action,
1654
+ viewport: {
1655
+ firstVisibleLine: viewport.firstLine,
1656
+ lastVisibleLine: viewport.lastLine,
1657
+ width: viewport.width,
1658
+ height: viewport.height
1659
+ },
1660
+ dirty: editor.dirty,
1661
+ language: ""
1662
+ };
1663
+ }
1664
+ function buildAction(type, c, previousPosition, extra) {
1665
+ return {
1666
+ type,
1667
+ cursor: {
1668
+ position: {
1669
+ line: c.y,
1670
+ col: c.x
1671
+ },
1672
+ anchor: c.anchor ? {
1673
+ line: c.anchor.y,
1674
+ col: c.anchor.x
1675
+ } : null
1676
+ },
1677
+ previousPosition,
1678
+ char: extra?.char,
1679
+ pastedText: extra?.pastedText
1680
+ };
1681
+ }
1682
+ //#endregion
1683
+ //#region packages/editor/src/plugins/apply.ts
1684
+ function applyEditResult(result, editor, cm, targetCursor) {
1685
+ if (result.replaceAll) {
1686
+ if (result.replaceAll.length !== editor.lines.length || result.replaceAll.some((l, i) => l !== editor.lines[i])) {
1687
+ editor.lines = result.replaceAll;
1688
+ editor.dirty = true;
1689
+ }
1690
+ }
1691
+ if (result.edits) {
1692
+ const sorted = [...result.edits].sort((a, b) => {
1693
+ if (a.range.start.line !== b.range.start.line) return b.range.start.line - a.range.start.line;
1694
+ return b.range.start.col - a.range.start.col;
1695
+ });
1696
+ for (const edit of sorted) {
1697
+ const { start, end } = edit.range;
1698
+ if (start.line === end.line) {
1699
+ const line = editor.lines[start.line];
1700
+ editor.lines[start.line] = line.substring(0, start.col) + edit.text + line.substring(end.col);
1701
+ } else {
1702
+ const firstPart = editor.lines[start.line].substring(0, start.col);
1703
+ const lastPart = editor.lines[end.line].substring(end.col);
1704
+ const newLines = edit.text.split("\n");
1705
+ newLines[0] = firstPart + newLines[0];
1706
+ newLines[newLines.length - 1] = newLines[newLines.length - 1] + lastPart;
1707
+ editor.lines.splice(start.line, end.line - start.line + 1, ...newLines);
1708
+ }
1709
+ editor.dirty = true;
1710
+ }
1711
+ }
1712
+ if (result.cursors && result.cursors.length > 0) {
1713
+ const resultCursor = result.cursors[0];
1714
+ const target = targetCursor ?? cm.primary;
1715
+ target.x = resultCursor.position.col;
1716
+ target.y = resultCursor.position.line;
1717
+ target.anchor = resultCursor.anchor ? {
1718
+ x: resultCursor.anchor.col,
1719
+ y: resultCursor.anchor.line
1720
+ } : null;
1721
+ }
1722
+ }
1723
+ //#endregion
1724
+ //#region packages/editor/src/input.ts
1725
+ function notifyPlugin(plugin, actionType, c, prevPos, editor, cm, screen, extra) {
1726
+ if (!plugin?.onCursorAction) return;
1727
+ const action = buildAction(actionType, c, {
1728
+ line: prevPos.y,
1729
+ col: prevPos.x
1730
+ }, extra);
1731
+ const ctx = buildContext(editor, cm, getViewport(cm, screen), action);
1732
+ const result = plugin.onCursorAction(ctx);
1733
+ if (result) applyEditResult(result, editor, cm, c);
1734
+ }
1735
+ function snap(undo, label, cm, editor) {
1736
+ undo.snapshot(label, {
1737
+ x: cm.primary.x,
1738
+ y: cm.primary.y
1739
+ }, editor.lines, cm.saveState());
1740
+ }
1741
+ function commit(undo, cm, editor) {
1742
+ undo.commit({
1743
+ x: cm.primary.x,
1744
+ y: cm.primary.y
1745
+ }, editor.lines, cm.saveState());
1746
+ }
1747
+ function getViewport(cm, screen) {
1748
+ return {
1749
+ firstLine: cm.scrollY,
1750
+ lastLine: cm.scrollY + screen.height - 5,
1751
+ width: screen.width,
1752
+ height: screen.height
1753
+ };
1754
+ }
1755
+ function handleKey(key, editor, cm, screen, undo, plugin) {
1756
+ const isWindows = process.platform === "win32" || !!process.env["WSL_DISTRO_NAME"];
1757
+ if ((key.name === "up" || key.name === "down") && key.ctrl && (isWindows ? key.alt : key.shift)) {
1758
+ const allCursors = cm.all;
1759
+ if (key.name === "up") {
1760
+ const newY = Math.min(...allCursors.map((c) => c.y)) - 1;
1761
+ if (newY >= 0) {
1762
+ const newX = Math.min(cm.primary.x, editor.lines[newY].length);
1763
+ cm.addAbove(newX, newY);
1764
+ cm.dedup();
1765
+ }
1766
+ } else {
1767
+ const newY = Math.max(...allCursors.map((c) => c.y)) + 1;
1768
+ if (newY < editor.lines.length) {
1769
+ const newX = Math.min(cm.primary.x, editor.lines[newY].length);
1770
+ cm.addBelow();
1771
+ const last = cm.all[cm.all.length - 1];
1772
+ last.x = newX;
1773
+ last.y = newY;
1774
+ cm.dedup();
1775
+ }
1776
+ }
1777
+ return "continue";
1778
+ }
1779
+ if (key.shift && key.ctrl && (key.name === "left" || key.name === "right")) {
1780
+ cm.startSelectionAll();
1781
+ cm.moveWordAll(key.name, editor.lines);
1782
+ cm.clampAll(editor.lines);
1783
+ cm.collapseEmptySelections();
1784
+ return "continue";
1785
+ }
1786
+ if (key.shift && [
1787
+ "up",
1788
+ "down",
1789
+ "left",
1790
+ "right",
1791
+ "home",
1792
+ "end"
1793
+ ].includes(key.name)) {
1794
+ cm.startSelectionAll();
1795
+ cm.moveAll(key.name, editor.lines, screen.height - 2);
1796
+ cm.clampAll(editor.lines);
1797
+ if (cm.isMulti && (key.name === "up" || key.name === "down")) {
1798
+ cm.mergeIntoSelection(key.name);
1799
+ return "continue";
1800
+ }
1801
+ cm.collapseEmptySelections();
1802
+ return "continue";
1803
+ }
1804
+ if (key.raw.length === 1 && key.raw[0] === 27) {
1805
+ if (cm.isMulti) {
1806
+ cm.clearExtras();
1807
+ return "continue";
1808
+ }
1809
+ }
1810
+ if (key.alt && (key.name === "up" || key.name === "down")) {
1811
+ const p = cm.primary;
1812
+ const range = cm.getSelection(p);
1813
+ const startLine = range ? range.start.y : p.y;
1814
+ const endLine = range ? range.end.y : p.y;
1815
+ snap(undo, "move-line", cm, editor);
1816
+ if (key.name === "up") {
1817
+ if (moveLinesUp(editor, startLine, endLine)) {
1818
+ p.y--;
1819
+ if (p.anchor) p.anchor = {
1820
+ x: p.anchor.x,
1821
+ y: p.anchor.y - 1
1822
+ };
1823
+ }
1824
+ } else if (moveLinesDown(editor, startLine, endLine)) {
1825
+ p.y++;
1826
+ if (p.anchor) p.anchor = {
1827
+ x: p.anchor.x,
1828
+ y: p.anchor.y + 1
1829
+ };
1830
+ }
1831
+ commit(undo, cm, editor);
1832
+ cm.clampAll(editor.lines);
1833
+ return "continue";
1834
+ }
1835
+ if (key.name === "f1" || key.name === "f2" || key.name === "f3" || key.name === "f4") {
1836
+ if (key.name === "f2") return "history";
1837
+ if (key.name === "f3" && plugin?.onFormat) {
1838
+ const linesBefore = [...editor.lines];
1839
+ snap(undo, "format", cm, editor);
1840
+ const ctx = buildContext(editor, cm, getViewport(cm, screen));
1841
+ const result = plugin.onFormat(ctx);
1842
+ if (result) applyEditResult(result, editor, cm);
1843
+ if (editor.lines.length !== linesBefore.length || editor.lines.some((l, i) => l !== linesBefore[i])) commit(undo, cm, editor);
1844
+ cm.clampAll(editor.lines);
1845
+ }
1846
+ return "continue";
1847
+ }
1848
+ if (key.ctrl) {
1849
+ const p = cm.primary;
1850
+ switch (key.name) {
1851
+ case "q": return "exit";
1852
+ case "s": return "save";
1853
+ case "f": return "search";
1854
+ case "g": return "goto";
1855
+ case "left":
1856
+ cm.moveWordAll("left", editor.lines);
1857
+ break;
1858
+ case "right":
1859
+ cm.moveWordAll("right", editor.lines);
1860
+ break;
1861
+ case "backspace":
1862
+ snap(undo, "delete-word", cm, editor);
1863
+ cm.forEachBottomUp((c) => {
1864
+ if (c.anchor) {
1865
+ cm.deleteSelection(c, editor.lines);
1866
+ editor.dirty = true;
1867
+ } else {
1868
+ const b = wordBoundaryLeft(editor.lines[c.y], c.x);
1869
+ const pos = deleteWordBack(editor, c.x, c.y, b);
1870
+ c.x = pos.x;
1871
+ c.y = pos.y;
1872
+ }
1873
+ });
1874
+ commit(undo, cm, editor);
1875
+ break;
1876
+ case "delete":
1877
+ snap(undo, "delete-word", cm, editor);
1878
+ cm.forEachBottomUp((c) => {
1879
+ if (c.anchor) {
1880
+ cm.deleteSelection(c, editor.lines);
1881
+ editor.dirty = true;
1882
+ } else {
1883
+ const b = wordBoundaryRight(editor.lines[c.y], c.x);
1884
+ deleteWordForward(editor, c.x, c.y, b);
1885
+ }
1886
+ });
1887
+ commit(undo, cm, editor);
1888
+ break;
1889
+ case "z": {
1890
+ const result = undo.undo(editor.lines, p);
1891
+ if (result) {
1892
+ editor.lines = result.lines;
1893
+ editor.dirty = true;
1894
+ if (result.cursorState) cm.restoreState(result.cursorState);
1895
+ }
1896
+ break;
1897
+ }
1898
+ case "y": {
1899
+ const result = undo.redo(editor.lines, p);
1900
+ if (result) {
1901
+ editor.lines = result.lines;
1902
+ editor.dirty = true;
1903
+ if (result.cursorState) cm.restoreState(result.cursorState);
1904
+ }
1905
+ break;
1906
+ }
1907
+ case "c": {
1908
+ const parts = [];
1909
+ cm.forEachAll((c) => {
1910
+ const text = cm.getSelectedText(c, editor.lines);
1911
+ if (text) parts.push(text);
1912
+ });
1913
+ if (parts.length > 0) {
1914
+ editor.clipboardParts = parts;
1915
+ cm.clearSelectionAll();
1916
+ }
1917
+ break;
1918
+ }
1919
+ case "x": {
1920
+ snap(undo, "cut", cm, editor);
1921
+ const parts = [];
1922
+ cm.forEachBottomUp((c) => {
1923
+ if (c.anchor) {
1924
+ parts.unshift(cm.getSelectedText(c, editor.lines));
1925
+ cm.deleteSelection(c, editor.lines);
1926
+ editor.dirty = true;
1927
+ } else {
1928
+ parts.unshift(editor.lines[c.y]);
1929
+ editor.lines.splice(c.y, 1);
1930
+ if (editor.lines.length === 0) editor.lines = [""];
1931
+ if (c.y >= editor.lines.length) c.y = editor.lines.length - 1;
1932
+ c.x = Math.min(c.x, editor.lines[c.y].length);
1933
+ editor.dirty = true;
1934
+ }
1935
+ });
1936
+ editor.clipboardParts = parts;
1937
+ commit(undo, cm, editor);
1938
+ break;
1939
+ }
1940
+ case "v": {
1941
+ if (editor.clipboardParts.length === 0) break;
1942
+ snap(undo, "paste", cm, editor);
1943
+ const parts = editor.clipboardParts;
1944
+ const cursorCount = cm.count;
1945
+ if (parts.length === cursorCount) cm.forEachBottomUp((c) => {
1946
+ const partIdx = cm.all.indexOf(c);
1947
+ if (c.anchor) {
1948
+ cm.deleteSelection(c, editor.lines);
1949
+ editor.dirty = true;
1950
+ }
1951
+ const pos = pasteText(editor, c.x, c.y, parts[partIdx]);
1952
+ c.x = pos.x;
1953
+ c.y = pos.y;
1954
+ });
1955
+ else if (cursorCount === 1) {
1956
+ const c = cm.primary;
1957
+ if (c.anchor) {
1958
+ cm.deleteSelection(c, editor.lines);
1959
+ editor.dirty = true;
1960
+ }
1961
+ for (let i = 0; i < parts.length - 1; i++) editor.lines.splice(c.y + i, 0, parts[i]);
1962
+ c.y += parts.length - 1;
1963
+ const lastPart = parts[parts.length - 1];
1964
+ const line = editor.lines[c.y];
1965
+ editor.lines[c.y] = line.substring(0, c.x) + lastPart + line.substring(c.x);
1966
+ c.x += lastPart.length;
1967
+ editor.dirty = true;
1968
+ } else {
1969
+ const joined = parts.join("\n");
1970
+ cm.forEachBottomUp((c) => {
1971
+ if (c.anchor) {
1972
+ cm.deleteSelection(c, editor.lines);
1973
+ editor.dirty = true;
1974
+ }
1975
+ const pos = pasteText(editor, c.x, c.y, joined);
1976
+ c.x = pos.x;
1977
+ c.y = pos.y;
1978
+ });
1979
+ }
1980
+ commit(undo, cm, editor);
1981
+ break;
1982
+ }
1983
+ }
1984
+ cm.clampAll(editor.lines);
1985
+ return "continue";
1986
+ }
1987
+ switch (key.name) {
1988
+ case "up":
1989
+ case "down":
1990
+ case "left":
1991
+ case "right":
1992
+ case "home":
1993
+ case "end":
1994
+ cm.clearSelectionAll();
1995
+ cm.moveAll(key.name, editor.lines, 0);
1996
+ break;
1997
+ case "pageup":
1998
+ case "pagedown":
1999
+ cm.clearSelectionAll();
2000
+ cm.moveAll(key.name, editor.lines, screen.height - 2);
2001
+ break;
2002
+ case "enter": {
2003
+ snap(undo, "enter", cm, editor);
2004
+ const sorted = [...cm.all].sort((a, b) => a.y - b.y || a.x - b.x);
2005
+ let lineShift = 0;
2006
+ for (const c of sorted) {
2007
+ c.y += lineShift;
2008
+ const prev = {
2009
+ x: c.x,
2010
+ y: c.y
2011
+ };
2012
+ if (c.anchor) {
2013
+ cm.deleteSelection(c, editor.lines);
2014
+ editor.dirty = true;
2015
+ }
2016
+ const pos = insertNewline(editor, c.x, c.y);
2017
+ c.x = pos.x;
2018
+ c.y = pos.y;
2019
+ notifyPlugin(plugin, "newline", c, prev, editor, cm, screen);
2020
+ lineShift++;
2021
+ }
2022
+ commit(undo, cm, editor);
2023
+ break;
2024
+ }
2025
+ case "backspace":
2026
+ snap(undo, "backspace", cm, editor);
2027
+ cm.forEachBottomUp((c) => {
2028
+ const prev = {
2029
+ x: c.x,
2030
+ y: c.y
2031
+ };
2032
+ if (c.anchor) {
2033
+ cm.deleteSelection(c, editor.lines);
2034
+ editor.dirty = true;
2035
+ } else {
2036
+ const pos = deleteCharBack(editor, c.x, c.y);
2037
+ c.x = pos.x;
2038
+ c.y = pos.y;
2039
+ }
2040
+ notifyPlugin(plugin, "backspace", c, prev, editor, cm, screen);
2041
+ });
2042
+ commit(undo, cm, editor);
2043
+ break;
2044
+ case "delete":
2045
+ snap(undo, "delete", cm, editor);
2046
+ cm.forEachBottomUp((c) => {
2047
+ const prev = {
2048
+ x: c.x,
2049
+ y: c.y
2050
+ };
2051
+ if (c.anchor) {
2052
+ cm.deleteSelection(c, editor.lines);
2053
+ editor.dirty = true;
2054
+ } else deleteCharForward(editor, c.x, c.y);
2055
+ notifyPlugin(plugin, "delete", c, prev, editor, cm, screen);
2056
+ });
2057
+ commit(undo, cm, editor);
2058
+ break;
2059
+ case "tab":
2060
+ snap(undo, "tab", cm, editor);
2061
+ cm.forEachBottomUp((c) => {
2062
+ const prev = {
2063
+ x: c.x,
2064
+ y: c.y
2065
+ };
2066
+ c.x = insertTab(editor, c.x, c.y);
2067
+ notifyPlugin(plugin, "tab", c, prev, editor, cm, screen);
2068
+ });
2069
+ commit(undo, cm, editor);
2070
+ break;
2071
+ default: {
2072
+ const ch = key.name;
2073
+ if (ch === "unknown") break;
2074
+ if ((ch.codePointAt(0) ?? 0) >= 32) {
2075
+ snap(undo, "type", cm, editor);
2076
+ cm.forEachBottomUp((c) => {
2077
+ const prev = {
2078
+ x: c.x,
2079
+ y: c.y
2080
+ };
2081
+ if (c.anchor) {
2082
+ cm.deleteSelection(c, editor.lines);
2083
+ editor.dirty = true;
2084
+ }
2085
+ c.x = insertChar(editor, c.x, c.y, ch);
2086
+ notifyPlugin(plugin, "char", c, prev, editor, cm, screen, { char: ch });
2087
+ });
2088
+ commit(undo, cm, editor);
2089
+ }
2090
+ break;
2091
+ }
2092
+ }
2093
+ cm.clampAll(editor.lines);
2094
+ cm.dedup();
2095
+ return "continue";
2096
+ }
2097
+ //#endregion
2098
+ //#region packages/editor/src/highlight.ts
2099
+ function tokenizeLine(line, plugin) {
2100
+ if (!plugin?.highlight) return [];
2101
+ const tokens = [];
2102
+ const { keywords, patterns } = plugin.highlight;
2103
+ const claimed = /* @__PURE__ */ new Set();
2104
+ function addToken(start, end, type) {
2105
+ for (let i = start; i < end; i++) if (claimed.has(i)) return;
2106
+ tokens.push({
2107
+ start,
2108
+ end,
2109
+ type
2110
+ });
2111
+ for (let i = start; i < end; i++) claimed.add(i);
2112
+ }
2113
+ const order = [
2114
+ "comment",
2115
+ "string",
2116
+ "number",
2117
+ "keyword",
2118
+ "type",
2119
+ "function",
2120
+ "operator",
2121
+ "variable",
2122
+ "property",
2123
+ "tag",
2124
+ "attribute",
2125
+ "constant",
2126
+ "builtin",
2127
+ "punctuation"
2128
+ ];
2129
+ if (patterns) for (const type of order) {
2130
+ const pattern = patterns[type];
2131
+ if (!pattern) continue;
2132
+ const re = new RegExp(pattern.source, pattern.flags.replace("g", "") + "g");
2133
+ let match;
2134
+ while ((match = re.exec(line)) !== null) {
2135
+ addToken(match.index, match.index + match[0].length, type);
2136
+ if (match[0].length === 0) re.lastIndex++;
2137
+ }
2138
+ }
2139
+ if (keywords && keywords.length > 0) {
2140
+ const kwPattern = new RegExp(`\\b(${keywords.join("|")})\\b`, "g");
2141
+ let match;
2142
+ while ((match = kwPattern.exec(line)) !== null) addToken(match.index, match.index + match[0].length, "keyword");
2143
+ }
2144
+ return tokens.sort((a, b) => a.start - b.start);
2145
+ }
2146
+ //#endregion
2147
+ //#region packages/editor/src/plugins/types.ts
2148
+ var tokenColors = {
2149
+ keyword: [
2150
+ 198,
2151
+ 120,
2152
+ 221
2153
+ ],
2154
+ string: [
2155
+ 152,
2156
+ 195,
2157
+ 121
2158
+ ],
2159
+ comment: [
2160
+ 92,
2161
+ 99,
2162
+ 112
2163
+ ],
2164
+ number: [
2165
+ 209,
2166
+ 154,
2167
+ 102
2168
+ ],
2169
+ type: [
2170
+ 229,
2171
+ 192,
2172
+ 123
2173
+ ],
2174
+ function: [
2175
+ 97,
2176
+ 175,
2177
+ 239
2178
+ ],
2179
+ operator: [
2180
+ 86,
2181
+ 182,
2182
+ 194
2183
+ ],
2184
+ variable: [
2185
+ 224,
2186
+ 108,
2187
+ 117
2188
+ ],
2189
+ property: [
2190
+ 224,
2191
+ 108,
2192
+ 117
2193
+ ],
2194
+ tag: [
2195
+ 224,
2196
+ 108,
2197
+ 117
2198
+ ],
2199
+ attribute: [
2200
+ 209,
2201
+ 154,
2202
+ 102
2203
+ ],
2204
+ constant: [
2205
+ 209,
2206
+ 154,
2207
+ 102
2208
+ ],
2209
+ builtin: [
2210
+ 198,
2211
+ 120,
2212
+ 221
2213
+ ],
2214
+ punctuation: [
2215
+ 171,
2216
+ 178,
2217
+ 191
2218
+ ]
2219
+ };
2220
+ //#endregion
2221
+ //#region packages/editor/src/render.ts
2222
+ function getShortcuts(plugin) {
2223
+ const list = [
2224
+ ["^Q", "Exit"],
2225
+ ["^S", "Save"],
2226
+ ["^Z", "Undo"],
2227
+ ["^Y", "Redo"],
2228
+ ["^X", "Cut"],
2229
+ ["^V", "Paste"]
2230
+ ];
2231
+ list.push(["^F", "Search"]);
2232
+ list.push(["^G", "Go to"]);
2233
+ const isWindows = process.platform === "win32" || !!process.env["WSL_DISTRO_NAME"];
2234
+ list.push([isWindows ? "^⌥↕" : "^⇧↕", "Multi"]);
2235
+ if (plugin?.onFormat) list.push(["F3", "Format"]);
2236
+ return list;
2237
+ }
2238
+ function gutterWidth(lineCount) {
2239
+ return String(lineCount).length + 1;
2240
+ }
2241
+ function getViewDimensions(screen, lineCount) {
2242
+ const gw = gutterWidth(lineCount);
2243
+ const contentTop = 1;
2244
+ return {
2245
+ gw,
2246
+ contentTop,
2247
+ viewH: screen.height - 4 - contentTop + 1,
2248
+ viewW: screen.width - 2 - gw
2249
+ };
2250
+ }
2251
+ function render(screen, draw, editor, cm, plugin, pluginVersion) {
2252
+ draw.clear();
2253
+ const w = screen.width;
2254
+ const h = screen.height;
2255
+ const { gw, contentTop, viewH, viewW } = getViewDimensions(screen, editor.lines.length);
2256
+ draw.rect(0, 0, w, h - 1, {
2257
+ fg: [
2258
+ 55,
2259
+ 60,
2260
+ 70
2261
+ ],
2262
+ border: "round"
2263
+ });
2264
+ for (let x = 1; x < w - 1; x++) draw.char(x, 0, "─", { fg: [
2265
+ 55,
2266
+ 60,
2267
+ 70
2268
+ ] });
2269
+ const langName = plugin ? ` [${plugin.name}]` : "";
2270
+ const title = ` jano — ${editor.filePath || "untitled"}${langName} `;
2271
+ const titleX = Math.floor((w - title.length) / 2);
2272
+ draw.text(titleX, 0, title, { fg: [
2273
+ 230,
2274
+ 200,
2275
+ 100
2276
+ ] });
2277
+ if (plugin && pluginVersion) {
2278
+ const vText = ` v${pluginVersion} `;
2279
+ draw.text(w - vText.length - 1, 0, vText, { fg: [
2280
+ 80,
2281
+ 85,
2282
+ 95
2283
+ ] });
2284
+ }
2285
+ for (let y = 0; y < viewH; y++) {
2286
+ const lineIdx = y + cm.scrollY;
2287
+ if (lineIdx >= editor.lines.length) break;
2288
+ const lineNum = String(lineIdx + 1).padStart(gw - 1, " ") + " ";
2289
+ const isCurrentLine = lineIdx === cm.primary.y;
2290
+ draw.text(1, contentTop + y, lineNum, { fg: isCurrentLine ? [
2291
+ 180,
2292
+ 185,
2293
+ 195
2294
+ ] : [
2295
+ 70,
2296
+ 75,
2297
+ 85
2298
+ ] });
2299
+ const line = editor.lines[lineIdx];
2300
+ const tokens = tokenizeLine(line, plugin);
2301
+ const colorMap = Array.from({ length: line.length }).fill(null);
2302
+ for (const token of tokens) {
2303
+ const color = tokenColors[token.type];
2304
+ if (color) for (let i = token.start; i < token.end && i < line.length; i++) colorMap[i] = color;
2305
+ }
2306
+ for (let col = 0; col < viewW; col++) {
2307
+ const charIdx = col + cm.scrollX;
2308
+ const screenX = 1 + gw + col;
2309
+ const screenY = contentTop + y;
2310
+ const ch = charIdx < line.length ? line[charIdx] : " ";
2311
+ if (cm.isCellSelected(lineIdx, charIdx)) draw.char(screenX, screenY, ch, {
2312
+ fg: [
2313
+ 255,
2314
+ 255,
2315
+ 255
2316
+ ],
2317
+ bg: [
2318
+ 60,
2319
+ 100,
2320
+ 180
2321
+ ]
2322
+ });
2323
+ else if (cm.isCellExtraCursor(lineIdx, charIdx)) draw.char(screenX, screenY, ch, {
2324
+ fg: [
2325
+ 0,
2326
+ 0,
2327
+ 0
2328
+ ],
2329
+ bg: [
2330
+ 200,
2331
+ 200,
2332
+ 200
2333
+ ]
2334
+ });
2335
+ else if (charIdx < line.length) {
2336
+ const fg = colorMap[charIdx] ?? [
2337
+ 171,
2338
+ 178,
2339
+ 191
2340
+ ];
2341
+ draw.char(screenX, screenY, ch, { fg });
2342
+ }
2343
+ }
2344
+ }
2345
+ if (editor.lines.length > viewH) {
2346
+ const scrollRatio = cm.scrollY / (editor.lines.length - viewH);
2347
+ const thumbSize = Math.max(1, Math.round(viewH * (viewH / editor.lines.length)));
2348
+ const thumbPos = Math.round(scrollRatio * (viewH - thumbSize));
2349
+ for (let y = 0; y < viewH; y++) {
2350
+ const screenY = contentTop + y;
2351
+ if (y >= thumbPos && y < thumbPos + thumbSize) draw.char(w - 1, screenY, "┃", { fg: [
2352
+ 140,
2353
+ 140,
2354
+ 140
2355
+ ] });
2356
+ }
2357
+ }
2358
+ const maxLineLen = Math.max(...editor.lines.slice(cm.scrollY, cm.scrollY + viewH).map((l) => l.length), 0);
2359
+ if (maxLineLen > viewW) {
2360
+ const scrollRatio = cm.scrollX / (maxLineLen - viewW);
2361
+ const thumbSize = Math.max(2, Math.round(viewW * (viewW / maxLineLen)));
2362
+ const thumbPos = Math.round(scrollRatio * (viewW - thumbSize));
2363
+ const barY = h - 2;
2364
+ for (let x = 0; x < viewW; x++) if (x >= thumbPos && x < thumbPos + thumbSize) draw.char(1 + gw + x, barY, "━", { fg: [
2365
+ 140,
2366
+ 140,
2367
+ 140
2368
+ ] });
2369
+ }
2370
+ const p = cm.primary;
2371
+ const statusY = h - 3;
2372
+ for (let x = 1; x < w - 1; x++) draw.char(x, statusY, " ", { bg: [
2373
+ 45,
2374
+ 50,
2375
+ 60
2376
+ ] });
2377
+ const posInfo = ` Ln ${p.y + 1}, Col ${p.x + 1}`;
2378
+ draw.text(2, statusY, posInfo, {
2379
+ fg: [
2380
+ 180,
2381
+ 185,
2382
+ 195
2383
+ ],
2384
+ bg: [
2385
+ 45,
2386
+ 50,
2387
+ 60
2388
+ ]
2389
+ });
2390
+ const modified = editor.dirty ? " ●" : "";
2391
+ const fileInfo = `${editor.lines.length} lines${modified}`;
2392
+ const fileInfoX = Math.floor((w - fileInfo.length) / 2);
2393
+ draw.text(fileInfoX, statusY, fileInfo, {
2394
+ fg: editor.dirty ? [
2395
+ 229,
2396
+ 192,
2397
+ 123
2398
+ ] : [
2399
+ 130,
2400
+ 135,
2401
+ 145
2402
+ ],
2403
+ bg: [
2404
+ 45,
2405
+ 50,
2406
+ 60
2407
+ ]
2408
+ });
2409
+ if (cm.isMulti) {
2410
+ const multiInfo = `${cm.count} cursors `;
2411
+ draw.text(w - multiInfo.length - 1, statusY, multiInfo, {
2412
+ fg: [
2413
+ 100,
2414
+ 200,
2415
+ 255
2416
+ ],
2417
+ bg: [
2418
+ 45,
2419
+ 50,
2420
+ 60
2421
+ ]
2422
+ });
2423
+ }
2424
+ const helpY = h - 1;
2425
+ const sc = getShortcuts(plugin);
2426
+ for (let x = 0; x < w; x++) draw.char(x, helpY, " ", { bg: [
2427
+ 35,
2428
+ 38,
2429
+ 45
2430
+ ] });
2431
+ let helpX = 1;
2432
+ for (let i = 0; i < sc.length; i++) {
2433
+ const [key, label] = sc[i];
2434
+ draw.text(helpX, helpY, key, {
2435
+ fg: [
2436
+ 220,
2437
+ 220,
2438
+ 220
2439
+ ],
2440
+ bg: [
2441
+ 60,
2442
+ 65,
2443
+ 75
2444
+ ]
2445
+ });
2446
+ draw.text(helpX + key.length, helpY, ` ${label}`, {
2447
+ fg: [
2448
+ 120,
2449
+ 125,
2450
+ 135
2451
+ ],
2452
+ bg: [
2453
+ 35,
2454
+ 38,
2455
+ 45
2456
+ ]
2457
+ });
2458
+ helpX += key.length + label.length + 3;
2459
+ if (i < sc.length - 1) draw.text(helpX - 1, helpY, "│", {
2460
+ fg: [
2461
+ 55,
2462
+ 58,
2463
+ 65
2464
+ ],
2465
+ bg: [
2466
+ 35,
2467
+ 38,
2468
+ 45
2469
+ ]
2470
+ });
2471
+ }
2472
+ screen.hideCursor();
2473
+ draw.flush();
2474
+ const screenCursorX = 1 + gw + (p.x - cm.scrollX);
2475
+ const screenCursorY = contentTop + (p.y - cm.scrollY);
2476
+ screen.moveTo(screenCursorX, screenCursorY);
2477
+ screen.showCursor();
2478
+ }
2479
+ //#endregion
2480
+ //#region packages/editor/src/plugins/manifest.ts
2481
+ function validateManifest(data) {
2482
+ if (!data || typeof data !== "object") return null;
2483
+ const obj = data;
2484
+ if (typeof obj.name !== "string" || !obj.name) return null;
2485
+ if (typeof obj.version !== "string" || !obj.version) return null;
2486
+ if (typeof obj.description !== "string") return null;
2487
+ if (!Array.isArray(obj.extensions) || obj.extensions.length === 0) return null;
2488
+ if (!obj.extensions.every((e) => typeof e === "string")) return null;
2489
+ if (typeof obj.entry !== "string" || !obj.entry) return null;
2490
+ const api = typeof obj.api === "number" ? obj.api : 1;
2491
+ return {
2492
+ name: obj.name,
2493
+ version: obj.version,
2494
+ api,
2495
+ description: obj.description,
2496
+ extensions: obj.extensions,
2497
+ entry: obj.entry,
2498
+ author: typeof obj.author === "string" ? obj.author : void 0,
2499
+ homepage: typeof obj.homepage === "string" ? obj.homepage : void 0,
2500
+ license: typeof obj.license === "string" ? obj.license : void 0
2501
+ };
2502
+ }
2503
+ //#endregion
2504
+ //#region packages/editor/src/plugins/loader.ts
2505
+ async function loadPlugins() {
2506
+ const pluginsDir = getPluginsDir();
2507
+ const config = loadConfig();
2508
+ const result = {
2509
+ plugins: [],
2510
+ errors: [],
2511
+ conflicts: []
2512
+ };
2513
+ if (!existsSync(pluginsDir)) return result;
2514
+ let dirs;
2515
+ try {
2516
+ dirs = readdirSync(pluginsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2517
+ } catch {
2518
+ return result;
2519
+ }
2520
+ const extensionMap = /* @__PURE__ */ new Map();
2521
+ for (const dirName of dirs) {
2522
+ const dir = join(pluginsDir, dirName);
2523
+ const manifestPath = join(dir, "plugin.json");
2524
+ if (!existsSync(manifestPath)) {
2525
+ result.errors.push({
2526
+ dir,
2527
+ error: "Missing plugin.json"
2528
+ });
2529
+ continue;
2530
+ }
2531
+ let manifest;
2532
+ try {
2533
+ const raw = readFileSync(manifestPath, "utf8");
2534
+ manifest = validateManifest(JSON.parse(raw));
2535
+ } catch (err) {
2536
+ result.errors.push({
2537
+ dir,
2538
+ error: `Invalid plugin.json: ${String(err)}`
2539
+ });
2540
+ continue;
2541
+ }
2542
+ if (!manifest) {
2543
+ result.errors.push({
2544
+ dir,
2545
+ error: "plugin.json missing required fields"
2546
+ });
2547
+ continue;
2548
+ }
2549
+ if (manifest.api > 1) {
2550
+ result.errors.push({
2551
+ dir,
2552
+ error: `"${manifest.name}" requires API v${manifest.api}, but jano supports v1. Update jano to use this plugin.`
2553
+ });
2554
+ continue;
2555
+ }
2556
+ if (!isPluginEnabled(config, manifest.name)) continue;
2557
+ let hasConflict = false;
2558
+ for (const ext of manifest.extensions) {
2559
+ const existing = extensionMap.get(ext);
2560
+ if (existing) {
2561
+ result.conflicts.push(`Extension "${ext}" claimed by both "${existing}" and "${manifest.name}". Skipping "${manifest.name}".`);
2562
+ hasConflict = true;
2563
+ break;
2564
+ }
2565
+ }
2566
+ if (hasConflict) continue;
2567
+ const entryPath = join(dir, manifest.entry);
2568
+ if (!existsSync(entryPath)) {
2569
+ result.errors.push({
2570
+ dir,
2571
+ error: `Entry file not found: ${manifest.entry}`
2572
+ });
2573
+ continue;
2574
+ }
2575
+ try {
2576
+ const mod = await import(pathToFileURL(entryPath).href);
2577
+ const plugin = mod.default?.default ?? mod.default ?? mod.plugin ?? mod;
2578
+ if (!plugin.name || !plugin.extensions) {
2579
+ result.errors.push({
2580
+ dir,
2581
+ error: "Plugin does not export a valid LanguagePlugin"
2582
+ });
2583
+ continue;
2584
+ }
2585
+ for (const ext of manifest.extensions) extensionMap.set(ext, manifest.name);
2586
+ result.plugins.push({
2587
+ manifest,
2588
+ plugin,
2589
+ dir
2590
+ });
2591
+ } catch (err) {
2592
+ result.errors.push({
2593
+ dir,
2594
+ error: `Failed to load: ${String(err)}`
2595
+ });
2596
+ }
2597
+ }
2598
+ return result;
2599
+ }
2600
+ //#endregion
2601
+ //#region packages/editor/src/plugins/index.ts
2602
+ var loadedPlugins = [];
2603
+ async function initPlugins() {
2604
+ const result = await loadPlugins();
2605
+ loadedPlugins = result.plugins;
2606
+ return result;
2607
+ }
2608
+ function getLoadedPlugins() {
2609
+ return loadedPlugins;
2610
+ }
2611
+ function detectLanguage(filePath) {
2612
+ const ext = extname(filePath);
2613
+ const base = basename(filePath);
2614
+ for (const { plugin } of loadedPlugins) for (const pattern of plugin.extensions) if (pattern === ext || pattern === base) return plugin;
2615
+ return null;
2616
+ }
2617
+ //#endregion
2618
+ //#region packages/editor/src/index.ts
2619
+ var filePath = process.argv[2] || void 0;
2620
+ var screen = createScreen();
2621
+ var draw = createDraw(screen);
2622
+ var editor = createEditor(filePath);
2623
+ var cm = createCursorManager();
2624
+ var undo = createUndoManager();
2625
+ var plugin = null;
2626
+ var pluginVersion;
2627
+ var dialogOpen = false;
2628
+ function update() {
2629
+ const { viewW, viewH } = getViewDimensions(screen, editor.lines.length);
2630
+ cm.ensureVisible(viewW, viewH);
2631
+ render(screen, draw, editor, cm, plugin, pluginVersion);
2632
+ }
2633
+ function reloadPlugin() {
2634
+ plugin = detectLanguage(editor.filePath);
2635
+ if (plugin) pluginVersion = getLoadedPlugins().find((p) => p.plugin === plugin)?.manifest.version;
2636
+ else pluginVersion = void 0;
2637
+ }
2638
+ async function trySave(filePath) {
2639
+ const { existsSync } = await import("node:fs");
2640
+ if (existsSync(filePath) && filePath !== editor.filePath) {
2641
+ dialogOpen = true;
2642
+ const confirm = await showDialog(screen, draw, {
2643
+ title: "Overwrite?",
2644
+ message: `"${filePath}" already exists. Overwrite?`,
2645
+ buttons: [{
2646
+ label: "Overwrite",
2647
+ value: "yes"
2648
+ }, {
2649
+ label: "Cancel",
2650
+ value: "no"
2651
+ }],
2652
+ border: "round"
2653
+ }, update);
2654
+ dialogOpen = false;
2655
+ if (confirm.type !== "button" || confirm.value !== "yes") return false;
2656
+ }
2657
+ try {
2658
+ saveAs(editor, filePath);
2659
+ reloadPlugin();
2660
+ return true;
2661
+ } catch (err) {
2662
+ dialogOpen = true;
2663
+ await showDialog(screen, draw, {
2664
+ title: "Error",
2665
+ message: `Could not save: ${err instanceof Error ? err.message : String(err)}`,
2666
+ buttons: [{
2667
+ label: "OK",
2668
+ value: "ok"
2669
+ }],
2670
+ border: "round"
2671
+ }, update);
2672
+ dialogOpen = false;
2673
+ return false;
2674
+ }
2675
+ }
2676
+ async function saveWithDialog() {
2677
+ dialogOpen = true;
2678
+ const result = await showDialog(screen, draw, {
2679
+ title: "Save As",
2680
+ message: "Enter file name:",
2681
+ input: true,
2682
+ inputPlaceholder: "filename.ext",
2683
+ buttons: [{
2684
+ label: "Save",
2685
+ value: "save"
2686
+ }, {
2687
+ label: "Cancel",
2688
+ value: "cancel"
2689
+ }],
2690
+ border: "round",
2691
+ width: 50
2692
+ }, update);
2693
+ dialogOpen = false;
2694
+ let targetPath = "";
2695
+ if (result.type === "button" && result.value === "save" && result.inputValue) targetPath = result.inputValue;
2696
+ else if (result.type === "input" && result.value) targetPath = result.value;
2697
+ if (targetPath) await trySave(targetPath);
2698
+ update();
2699
+ }
2700
+ async function confirmExit() {
2701
+ if (!editor.dirty) {
2702
+ screen.leave();
2703
+ process.exit(0);
2704
+ }
2705
+ dialogOpen = true;
2706
+ const result = await showDialog(screen, draw, {
2707
+ title: "Unsaved Changes",
2708
+ message: `Save changes to "${editor.filePath || "untitled"}" before closing?`,
2709
+ buttons: [
2710
+ {
2711
+ label: "Save",
2712
+ value: "save"
2713
+ },
2714
+ {
2715
+ label: "Discard",
2716
+ value: "discard"
2717
+ },
2718
+ {
2719
+ label: "Cancel",
2720
+ value: "cancel"
2721
+ }
2722
+ ],
2723
+ border: "round"
2724
+ }, update);
2725
+ dialogOpen = false;
2726
+ if (result.type === "button") {
2727
+ if (result.value === "save") {
2728
+ if (!editor.filePath) {
2729
+ await saveWithDialog();
2730
+ if (!editor.filePath) {
2731
+ update();
2732
+ return;
2733
+ }
2734
+ } else if (!await trySave(editor.filePath)) {
2735
+ update();
2736
+ return;
2737
+ }
2738
+ screen.leave();
2739
+ process.exit(0);
2740
+ }
2741
+ if (result.value === "discard") {
2742
+ screen.leave();
2743
+ process.exit(0);
2744
+ }
2745
+ }
2746
+ update();
2747
+ }
2748
+ async function showHistory() {
2749
+ const history = undo.getHistory();
2750
+ if (history.length === 0) {
2751
+ dialogOpen = true;
2752
+ await showDialog(screen, draw, {
2753
+ title: "History",
2754
+ message: "No changes recorded yet.",
2755
+ buttons: [{
2756
+ label: "OK",
2757
+ value: "ok"
2758
+ }],
2759
+ border: "round"
2760
+ }, update);
2761
+ dialogOpen = false;
2762
+ update();
2763
+ return;
2764
+ }
2765
+ dialogOpen = true;
2766
+ const items = [" 0. Original file"];
2767
+ for (let i = 0; i < history.length; i++) {
2768
+ const entry = history[i];
2769
+ const time = new Date(entry.timestamp).toLocaleTimeString();
2770
+ const desc = undo.describeEntry(entry);
2771
+ const marker = i === history.length - 1 ? "▸" : " ";
2772
+ items.push(`${marker} ${i + 1}. [${time}] ${desc}`);
2773
+ }
2774
+ const result = await showDialog(screen, draw, {
2775
+ title: `History (${history.length} changes)`,
2776
+ message: items.slice(-15).join("\n"),
2777
+ input: true,
2778
+ inputPlaceholder: "Number (0 = original)...",
2779
+ buttons: [{
2780
+ label: "Jump",
2781
+ value: "jump"
2782
+ }, {
2783
+ label: "Cancel",
2784
+ value: "cancel"
2785
+ }],
2786
+ border: "round",
2787
+ width: 60
2788
+ }, update);
2789
+ dialogOpen = false;
2790
+ if (result.type === "input" || result.type === "button" && result.value === "jump") {
2791
+ const inputVal = result.type === "input" ? result.value : result.inputValue ?? "";
2792
+ const idx = parseInt(inputVal, 10);
2793
+ if (idx === 0) {
2794
+ while (true) {
2795
+ const undone = undo.undo(editor.lines, cm.primary);
2796
+ if (!undone) break;
2797
+ editor.lines = undone.lines;
2798
+ if (undone.cursorState) cm.restoreState(undone.cursorState);
2799
+ }
2800
+ editor.dirty = false;
2801
+ } else if (idx >= 1 && idx <= history.length) {
2802
+ editor.lines = undo.jumpTo(idx - 1, editor.lines, cm.primary);
2803
+ editor.dirty = true;
2804
+ }
2805
+ }
2806
+ update();
2807
+ }
2808
+ var lastSearchQuery = "";
2809
+ var lastReplaceText = "";
2810
+ var lastSelectedIndex;
2811
+ async function openSearch() {
2812
+ dialogOpen = true;
2813
+ const result = await showSearch(screen, draw, editor.lines, {
2814
+ initialQuery: lastSearchQuery,
2815
+ initialReplace: lastReplaceText,
2816
+ cursorLine: cm.primary.y,
2817
+ cursorCol: cm.primary.x,
2818
+ lastSelectedIndex,
2819
+ border: "round"
2820
+ }, update);
2821
+ dialogOpen = false;
2822
+ lastSearchQuery = result.query;
2823
+ lastSelectedIndex = result.selectedIndex;
2824
+ if (result.type === "replace" || result.type === "replaceAll") lastReplaceText = result.replacement;
2825
+ else lastReplaceText = "";
2826
+ const p = cm.primary;
2827
+ cm.clearExtras();
2828
+ if (result.type === "jump") {
2829
+ p.y = result.match.line;
2830
+ p.x = result.match.col;
2831
+ p.anchor = null;
2832
+ }
2833
+ if (result.type === "replace") {
2834
+ undo.snapshot("replace", {
2835
+ x: p.x,
2836
+ y: p.y
2837
+ }, editor.lines, cm.saveState());
2838
+ const m = result.match;
2839
+ const line = editor.lines[m.line];
2840
+ editor.lines[m.line] = line.substring(0, m.col) + result.replacement + line.substring(m.col + m.length);
2841
+ editor.dirty = true;
2842
+ p.y = m.line;
2843
+ p.x = m.col + result.replacement.length;
2844
+ p.anchor = null;
2845
+ undo.commit({
2846
+ x: p.x,
2847
+ y: p.y
2848
+ }, editor.lines, cm.saveState());
2849
+ update();
2850
+ openSearch();
2851
+ return;
2852
+ }
2853
+ if (result.type === "replaceAll") {
2854
+ undo.snapshot("replace-all", {
2855
+ x: p.x,
2856
+ y: p.y
2857
+ }, editor.lines, cm.saveState());
2858
+ const sorted = [...result.matches].sort((a, b) => b.line - a.line || b.col - a.col);
2859
+ for (const m of sorted) {
2860
+ const line = editor.lines[m.line];
2861
+ editor.lines[m.line] = line.substring(0, m.col) + result.replacement + line.substring(m.col + m.length);
2862
+ }
2863
+ editor.dirty = true;
2864
+ undo.commit({
2865
+ x: p.x,
2866
+ y: p.y
2867
+ }, editor.lines, cm.saveState());
2868
+ }
2869
+ update();
2870
+ }
2871
+ async function openGoto() {
2872
+ dialogOpen = true;
2873
+ const total = editor.lines.length;
2874
+ const current = cm.primary.y + 1;
2875
+ const result = await showDialog(screen, draw, {
2876
+ title: `Go to Line (1-${total})`,
2877
+ message: `Current: line ${current}`,
2878
+ input: true,
2879
+ inputPlaceholder: "Line number, 'start' or 'end'...",
2880
+ buttons: [
2881
+ {
2882
+ label: "Start",
2883
+ value: "start"
2884
+ },
2885
+ {
2886
+ label: "Go",
2887
+ value: "go"
2888
+ },
2889
+ {
2890
+ label: "End",
2891
+ value: "end"
2892
+ }
2893
+ ],
2894
+ border: "round",
2895
+ width: 45
2896
+ }, update);
2897
+ dialogOpen = false;
2898
+ const p = cm.primary;
2899
+ cm.clearExtras();
2900
+ p.anchor = null;
2901
+ if (result.type === "button") {
2902
+ if (result.value === "start") {
2903
+ p.y = 0;
2904
+ p.x = 0;
2905
+ } else if (result.value === "end") {
2906
+ p.y = editor.lines.length - 1;
2907
+ p.x = 0;
2908
+ } else if (result.value === "go") {
2909
+ const inputVal = result.inputValue ?? "";
2910
+ const line = parseInt(inputVal, 10);
2911
+ if (line >= 1 && line <= editor.lines.length) {
2912
+ p.y = line - 1;
2913
+ p.x = 0;
2914
+ }
2915
+ }
2916
+ } else if (result.type === "input") {
2917
+ const val = result.value.trim().toLowerCase();
2918
+ if (val === "start" || val === "s") {
2919
+ p.y = 0;
2920
+ p.x = 0;
2921
+ } else if (val === "end" || val === "e") {
2922
+ p.y = editor.lines.length - 1;
2923
+ p.x = 0;
2924
+ } else {
2925
+ const line = parseInt(val, 10);
2926
+ if (line >= 1 && line <= editor.lines.length) {
2927
+ p.y = line - 1;
2928
+ p.x = 0;
2929
+ }
2930
+ }
2931
+ }
2932
+ update();
2933
+ }
2934
+ async function start() {
2935
+ const paths = getPaths();
2936
+ console.log(`[jano] config: ${paths.config}`);
2937
+ console.log(`[jano] plugins: ${paths.plugins}`);
2938
+ const loadResult = await initPlugins();
2939
+ console.log(`[jano] loaded ${loadResult.plugins.length} plugin(s)`);
2940
+ for (const p of loadResult.plugins) console.log(`[jano] ✓ ${p.manifest.name} v${p.manifest.version} (${p.manifest.extensions.join(", ")})`);
2941
+ for (const err of loadResult.errors) console.log(`[jano] ✗ ${err.dir}: ${err.error}`);
2942
+ for (const conflict of loadResult.conflicts) console.log(`[jano] ⚠ ${conflict}`);
2943
+ if (filePath) {
2944
+ plugin = detectLanguage(filePath);
2945
+ if (plugin) {
2946
+ pluginVersion = getLoadedPlugins().find((p) => p.plugin === plugin)?.manifest.version;
2947
+ console.log(`[jano] language: ${plugin.name}`);
2948
+ }
2949
+ }
2950
+ screen.enter();
2951
+ process.stdin.setRawMode(true);
2952
+ update();
2953
+ }
2954
+ start();
2955
+ process.stdin.on("data", (data) => {
2956
+ if (dialogOpen) return;
2957
+ switch (handleKey(parseKey(data), editor, cm, screen, undo, plugin)) {
2958
+ case "exit":
2959
+ confirmExit();
2960
+ return;
2961
+ case "history":
2962
+ showHistory();
2963
+ return;
2964
+ case "search":
2965
+ openSearch();
2966
+ return;
2967
+ case "goto":
2968
+ openGoto();
2969
+ return;
2970
+ case "save":
2971
+ if (editor.filePath) trySave(editor.filePath).then(() => update());
2972
+ else saveWithDialog();
2973
+ return;
2974
+ }
2975
+ update();
2976
+ });
2977
+ process.stdout.on("resize", update);
2978
+ //#endregion
2979
+ export {};