@teaui/core 1.9.8 → 1.11.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/.dist/Screen.d.ts +11 -2
  2. package/.dist/Screen.js +31 -19
  3. package/.dist/Screen.js.map +1 -1
  4. package/.dist/Style.js +2 -1
  5. package/.dist/Style.js.map +1 -1
  6. package/.dist/TestTerminal.js +4 -22
  7. package/.dist/TestTerminal.js.map +1 -1
  8. package/.dist/Theme.d.ts +4 -3
  9. package/.dist/Theme.js +28 -13
  10. package/.dist/Theme.js.map +1 -1
  11. package/.dist/UI.js +3 -3
  12. package/.dist/UI.js.map +1 -1
  13. package/.dist/View.d.ts +11 -6
  14. package/.dist/View.js +10 -21
  15. package/.dist/View.js.map +1 -1
  16. package/.dist/components/Align.d.ts +83 -0
  17. package/.dist/components/Align.js +384 -0
  18. package/.dist/components/Align.js.map +1 -0
  19. package/.dist/components/AutoLegend.d.ts +19 -0
  20. package/.dist/components/AutoLegend.js +51 -0
  21. package/.dist/components/AutoLegend.js.map +1 -0
  22. package/.dist/components/Box.js +4 -2
  23. package/.dist/components/Box.js.map +1 -1
  24. package/.dist/components/Button.d.ts +3 -1
  25. package/.dist/components/Button.js +7 -1
  26. package/.dist/components/Button.js.map +1 -1
  27. package/.dist/components/Calendar.d.ts +2 -0
  28. package/.dist/components/Calendar.js +10 -0
  29. package/.dist/components/Calendar.js.map +1 -1
  30. package/.dist/components/Callout.js +2 -2
  31. package/.dist/components/Callout.js.map +1 -1
  32. package/.dist/components/Checkbox.d.ts +2 -0
  33. package/.dist/components/Checkbox.js +7 -1
  34. package/.dist/components/Checkbox.js.map +1 -1
  35. package/.dist/components/Drawer.d.ts +2 -0
  36. package/.dist/components/Drawer.js +7 -1
  37. package/.dist/components/Drawer.js.map +1 -1
  38. package/.dist/components/Dropdown.js +14 -10
  39. package/.dist/components/Dropdown.js.map +1 -1
  40. package/.dist/components/Header.d.ts +1 -1
  41. package/.dist/components/HotKey.d.ts +3 -0
  42. package/.dist/components/HotKey.js +9 -1
  43. package/.dist/components/HotKey.js.map +1 -1
  44. package/.dist/components/Input.d.ts +4 -1
  45. package/.dist/components/Input.js +366 -42
  46. package/.dist/components/Input.js.map +1 -1
  47. package/.dist/components/Legend.js +32 -8
  48. package/.dist/components/Legend.js.map +1 -1
  49. package/.dist/components/Notification.d.ts +1 -1
  50. package/.dist/components/Progress.d.ts +1 -1
  51. package/.dist/components/Scrollable.d.ts +1 -1
  52. package/.dist/components/Separator.d.ts +1 -1
  53. package/.dist/components/Slider.d.ts +3 -1
  54. package/.dist/components/Slider.js +7 -0
  55. package/.dist/components/Slider.js.map +1 -1
  56. package/.dist/components/Stack.d.ts +1 -1
  57. package/.dist/components/Text.d.ts +1 -1
  58. package/.dist/components/ToggleGroup.d.ts +1 -1
  59. package/.dist/components/fonts.d.ts +1 -1
  60. package/.dist/components/index.d.ts +2 -1
  61. package/.dist/components/index.js +2 -1
  62. package/.dist/components/index.js.map +1 -1
  63. package/.dist/components/utility/TrackMouse.d.ts +3 -5
  64. package/.dist/components/utility/TrackMouse.js +1 -2
  65. package/.dist/components/utility/TrackMouse.js.map +1 -1
  66. package/.dist/events/key.d.ts +10 -6
  67. package/.dist/events/key.js +29 -15
  68. package/.dist/events/key.js.map +1 -1
  69. package/.dist/events/mouse.d.ts +1 -1
  70. package/.dist/events/translate.js +6 -6
  71. package/.dist/events/translate.js.map +1 -1
  72. package/.dist/index.d.ts +1 -0
  73. package/.dist/index.js +1 -0
  74. package/.dist/index.js.map +1 -1
  75. package/.dist/log.js +7 -1
  76. package/.dist/log.js.map +1 -1
  77. package/.dist/managers/FocusManager.d.ts +2 -0
  78. package/.dist/managers/FocusManager.js +9 -1
  79. package/.dist/managers/FocusManager.js.map +1 -1
  80. package/.dist/managers/MouseManager.js +1 -1
  81. package/.dist/managers/MouseManager.js.map +1 -1
  82. package/.dist/renderToAnsi.js +9 -0
  83. package/.dist/renderToAnsi.js.map +1 -1
  84. package/.dist/testing.d.ts +2 -2
  85. package/.dist/testing.js +27 -9
  86. package/.dist/testing.js.map +1 -1
  87. package/.dist/{components/types.d.ts → types.d.ts} +6 -0
  88. package/.dist/types.js.map +1 -0
  89. package/.dist/util.d.ts +2 -0
  90. package/.dist/util.js +25 -0
  91. package/.dist/util.js.map +1 -1
  92. package/package.json +3 -3
  93. package/.dist/components/types.js.map +0 -1
  94. /package/.dist/{components/types.js → types.js} +0 -0
@@ -5,6 +5,7 @@ import { Style } from '../Style.js';
5
5
  import { Point, Size } from '../geometry.js';
6
6
  import { FONTS } from './fonts.js';
7
7
  const NL_SIGIL = '⤦';
8
+ const TAB_SIGIL = '⭾ ';
8
9
  /**
9
10
  * Text input. Supports selection, word movement via alt+←→, single and multiline
10
11
  * input, and wrapped lines.
@@ -28,12 +29,21 @@ export class Input extends View {
28
29
  #wrap = false;
29
30
  #multiline = false;
30
31
  #font = 'default';
32
+ #format;
33
+ #formatStyles = [];
31
34
  #onChange;
32
35
  #onSubmit;
33
36
  // Printable width
34
37
  #maxLineWidth = 0;
35
38
  #cursor = { start: 0, end: 0 };
36
39
  #visibleWidth = 0;
40
+ // Undo/redo
41
+ #undoStack = [];
42
+ #redoStack = [];
43
+ #pendingUndoKind;
44
+ #preEditChars = [];
45
+ #preEditCursor = { start: 0, end: 0 };
46
+ #insertCoalesceEnabled = true;
37
47
  constructor(props = {}) {
38
48
  super(props);
39
49
  this.#update(props);
@@ -43,11 +53,12 @@ export class Input extends View {
43
53
  this.#update(props);
44
54
  super.update(props);
45
55
  }
46
- #update({ value, wrap, multiline, font, placeholder, onChange, onSubmit, }) {
56
+ #update({ value, wrap, multiline, font, format, placeholder, onChange, onSubmit, }) {
47
57
  this.#onChange = onChange;
48
58
  this.#onSubmit = onSubmit;
49
59
  this.#wrap = wrap ?? false;
50
60
  this.#multiline = multiline ?? false;
61
+ this.#format = format;
51
62
  this.#updatePlaceholderLines(placeholder ?? '');
52
63
  this.#updateLines(unicode.printableChars(value ?? ''), font ?? 'default');
53
64
  }
@@ -92,10 +103,11 @@ export class Input extends View {
92
103
  return [lines, line];
93
104
  }, [[], []]);
94
105
  this.#printableLines = charLines.map((printableLine, index, all) => {
106
+ const displayLine = printableLine;
95
107
  // every line needs a ' ' or NL_SIGIL at the end, for the EOL cursor
96
108
  return [
97
- printableLine.concat(index === all.length - 1 ? ' ' : NL_SIGIL),
98
- printableLine.reduce((width, char) => width + unicode.charWidth(char), 0) + 1,
109
+ displayLine.concat(index === all.length - 1 ? ' ' : NL_SIGIL),
110
+ displayLine.reduce((width, char) => width + unicode.charWidth(char), 0) + 1,
99
111
  ];
100
112
  });
101
113
  }
@@ -122,8 +134,48 @@ export class Input extends View {
122
134
  // the _printable_ width, not the number of characters
123
135
  return Math.max(maxWidth, width);
124
136
  }, 0);
137
+ this.#updateFormatStyles();
125
138
  this.invalidateSize();
126
139
  }
140
+ /**
141
+ * Run the format function on the current value and parse the ANSI output into
142
+ * a flat Style[] array with one entry per character in #chars.
143
+ */
144
+ #updateFormatStyles() {
145
+ if (!this.#format || !this.#chars.length) {
146
+ this.#formatStyles = [];
147
+ return;
148
+ }
149
+ let formatted;
150
+ try {
151
+ formatted = this.#format(this.#value);
152
+ }
153
+ catch {
154
+ this.#formatStyles = [];
155
+ return;
156
+ }
157
+ // Parse the ANSI output to extract a style per character.
158
+ // Walk through the formatted string, tracking ANSI state.
159
+ const styles = [];
160
+ let currentStyle = Style.NONE;
161
+ for (const token of unicode.printableChars(formatted)) {
162
+ const charWidth = unicode.charWidth(token);
163
+ if (charWidth === 0) {
164
+ // ANSI escape sequence — update current style.
165
+ // Full resets (ESC[0m) clear back to no styling.
166
+ if (RESET_RE.test(token)) {
167
+ currentStyle = Style.NONE;
168
+ }
169
+ else {
170
+ currentStyle = currentStyle.merge(Style.fromSGR(token, Style.NONE));
171
+ }
172
+ }
173
+ else {
174
+ styles.push(currentStyle);
175
+ }
176
+ }
177
+ this.#formatStyles = styles;
178
+ }
127
179
  get value() {
128
180
  return this.#value;
129
181
  }
@@ -164,6 +216,15 @@ export class Input extends View {
164
216
  this.#updateLines(undefined, undefined);
165
217
  }
166
218
  }
219
+ legendItems() {
220
+ const items = [];
221
+ items.push({ key: ['C-a', 'home'], label: 'Line Start' }, { key: ['C-e', 'end'], label: 'Line End' }, { key: 'A-,', label: 'Start' }, { key: 'A-.', label: 'End' });
222
+ if (this.#multiline) {
223
+ items.push({ key: 'C-]', label: 'Indent' }, { key: 'C-[', label: 'Dedent' });
224
+ }
225
+ items.push({ key: 'C-z', label: 'Undo' }, { key: 'C-S-z', label: 'Redo' });
226
+ return items;
227
+ }
167
228
  naturalSize(available) {
168
229
  let lines = this.#printableLines;
169
230
  if (!lines.length || !available.width) {
@@ -194,61 +255,111 @@ export class Input extends View {
194
255
  const prevChars = this.#chars;
195
256
  const prevText = this.#value;
196
257
  let removeAccent = true;
197
- if (event.name === 'enter' || event.name === 'return') {
258
+ if (event.full === 'C-z' || event.full === 'C--') {
259
+ this.#undo();
260
+ }
261
+ else if (event.full === 'C-S-z' || event.full === 'C-S--') {
262
+ this.#redo();
263
+ }
264
+ else if (event.name === 'enter' || event.name === 'return') {
198
265
  if (this.#multiline) {
199
- this.#receiveChar('\n', true);
266
+ this.#beginEdit('replace');
267
+ if (event.shift || event.alt) {
268
+ // Shift+Enter or Alt+Enter: plain newline without indentation
269
+ this.#receiveChar('\n', true);
270
+ }
271
+ else {
272
+ // Enter: newline + preserve current line's indentation
273
+ this.#receiveEnterWithIndent();
274
+ }
200
275
  }
201
276
  else {
202
277
  this.#onSubmit?.(this.#value);
203
278
  return;
204
279
  }
205
280
  }
206
- else if (event.full === 'C-a') {
207
- this.#receiveGotoStart();
281
+ else if (event.full === 'C-]') {
282
+ this.#beginEdit('replace');
283
+ this.#receiveIndent();
284
+ }
285
+ else if (event.full === 'C-[') {
286
+ this.#beginEdit('replace');
287
+ this.#receiveDedent();
208
288
  }
209
- else if (event.full === 'C-e') {
210
- this.#receiveGotoEnd();
289
+ else if (event.name === 'tab' && event.alt) {
290
+ this.#beginEdit('insert');
291
+ this.#receiveChar('\t', true);
292
+ }
293
+ else if (event.full === 'C-a' || event.name === 'home') {
294
+ this.#insertCoalesceEnabled = false;
295
+ this.#receiveHome(event);
296
+ }
297
+ else if (event.full === 'C-e' || event.name === 'end') {
298
+ this.#insertCoalesceEnabled = false;
299
+ this.#receiveEnd(event);
300
+ }
301
+ else if (event.full === 'A-,' ||
302
+ event.full === 'A-S-,' ||
303
+ event.full === 'A-S-<') {
304
+ this.#insertCoalesceEnabled = false;
305
+ this.#receiveGotoStart(event);
306
+ }
307
+ else if (event.full === 'A-.' ||
308
+ event.full === 'A-S-.' ||
309
+ event.full === 'A-S->') {
310
+ this.#insertCoalesceEnabled = false;
311
+ this.#receiveGotoEnd(event);
211
312
  }
212
313
  else if (event.name === 'up') {
314
+ this.#insertCoalesceEnabled = false;
213
315
  this.#receiveKeyUpArrow(event);
214
316
  }
215
317
  else if (event.name === 'down') {
318
+ this.#insertCoalesceEnabled = false;
216
319
  this.#receiveKeyDownArrow(event);
217
320
  }
218
- else if (event.name === 'home') {
219
- this.#receiveHome(event);
220
- }
221
- else if (event.name === 'end') {
222
- this.#receiveEnd(event);
223
- }
224
321
  else if (event.name === 'left') {
322
+ this.#insertCoalesceEnabled = false;
225
323
  this.#receiveKeyLeftArrow(event);
226
324
  }
227
325
  else if (event.name === 'right') {
326
+ this.#insertCoalesceEnabled = false;
228
327
  this.#receiveKeyRightArrow(event);
229
328
  }
230
329
  else if (event.full === 'backspace') {
330
+ this.#beginEdit('delete');
231
331
  this.#receiveKeyBackspace();
232
332
  }
233
333
  else if (event.name === 'delete') {
334
+ this.#beginEdit('delete');
234
335
  this.#receiveKeyDelete();
235
336
  }
236
337
  else if (event.full === 'A-backspace' || event.full === 'C-w') {
338
+ this.#beginEdit('delete');
237
339
  this.#receiveKeyDeleteWord();
238
340
  }
239
341
  else if (isKeyAccent(event)) {
342
+ this.#beginEdit('insert');
240
343
  this.#receiveKeyAccent(event);
241
344
  removeAccent = false;
242
345
  }
243
- else if (isKeyPrintable(event)) {
346
+ else if (!event.ctrl &&
347
+ !event.alt &&
348
+ !event.gui &&
349
+ isKeyPrintable(event)) {
350
+ this.#beginEdit('insert');
244
351
  this.#receiveKeyPrintable(event);
245
352
  }
246
353
  if (removeAccent) {
247
354
  this.#chars = this.#chars.filter(char => !isAccentChar(char));
248
355
  }
249
356
  if (prevChars !== this.#chars) {
357
+ this.#commitEdit();
250
358
  this.#updateLines(this.#chars, undefined);
251
359
  }
360
+ else {
361
+ this.#pendingUndoKind = undefined;
362
+ }
252
363
  if (prevText !== this.#value) {
253
364
  this.#onChange?.(this.#value);
254
365
  }
@@ -257,6 +368,7 @@ export class Input extends View {
257
368
  const pasteChars = unicode.printableChars(this.#multiline ? text : text.replaceAll('\n', ''));
258
369
  if (pasteChars.length === 0)
259
370
  return;
371
+ this.#beginEdit('replace');
260
372
  const prevText = this.#value;
261
373
  if (isEmptySelection(this.#cursor)) {
262
374
  this.#chars = this.#chars
@@ -272,6 +384,7 @@ export class Input extends View {
272
384
  this.#cursor.start = this.#cursor.end =
273
385
  this.minSelected() + pasteChars.length;
274
386
  }
387
+ this.#commitEdit();
275
388
  this.#updateLines(this.#chars, undefined);
276
389
  if (prevText !== this.#value) {
277
390
  this.#onChange?.(this.#value);
@@ -348,12 +461,23 @@ export class Input extends View {
348
461
  const cursorStyle = plainStyle.merge({ underline: true });
349
462
  const nlStyle = this.theme.text({ isPlaceholder: true });
350
463
  const fontMap = this.#font && FONTS[this.#font];
464
+ const hasFormatStyles = this.#formatStyles.length > 0;
351
465
  viewport.usingPen(pen => {
352
466
  let style = plainStyle;
353
467
  const visibleLines = lines.slice(cursorVisible.y);
354
468
  if (visibleLines.length === 0) {
355
469
  visibleLines.push([[' '], 0]);
356
470
  }
471
+ // Compute the character offset into the format styles array at the start
472
+ // of visible lines. Each line's chars (excluding the trailing sigil) map
473
+ // 1:1 to format style entries. Newlines do NOT have entries in the styles
474
+ // array (charWidth('\n') === 0, so they're skipped during parsing).
475
+ let formatOffset = 0;
476
+ if (hasFormatStyles) {
477
+ for (let i = 0; i < cursorVisible.y && i < lines.length; i++) {
478
+ formatOffset += lines[i][0].length - 1;
479
+ }
480
+ }
357
481
  // is the viewport tall/wide enough to show ellipses …
358
482
  const isTallEnough = viewport.contentSize.height > 4;
359
483
  const isWideEnough = viewport.contentSize.width > 9;
@@ -372,41 +496,52 @@ export class Input extends View {
372
496
  // set to true if any character is skipped
373
497
  let drawInitialEllipses = false;
374
498
  scanTextPosition.x = 0;
499
+ let charIndex = 0;
375
500
  for (let char of line) {
376
501
  char = fontMap?.get(char) ?? char;
377
502
  const charWidth = unicode.charWidth(char);
503
+ const isSigil = charIndex === line.length - 1;
378
504
  if (scanTextPosition.x >= cursorVisible.x) {
379
505
  const inSelection = isInSelection(cursorMin, cursorMax, scanTextPosition);
380
506
  const inCursor = scanTextPosition.x === cursorEnd.x &&
381
507
  scanTextPosition.y === cursorEnd.y;
382
508
  const inNewline = char === NL_SIGIL && scanTextPosition.x + charWidth === width;
509
+ const inTab = isTabSigil(char);
510
+ const isDimSigil = inNewline || inTab;
511
+ // Look up the format style for this character.
512
+ const formatStyle = hasFormatStyles && !isSigil && !inTab
513
+ ? this.#formatStyles[formatOffset + charIndex]
514
+ : undefined;
515
+ const baseStyle = formatStyle
516
+ ? plainStyle.merge(formatStyle)
517
+ : plainStyle;
383
518
  if (isEmptySelection(this.#cursor)) {
384
519
  if (isAccentChar(char)) {
385
- style = plainStyle.merge({ underline: true, inverse: true });
520
+ style = baseStyle.merge({ underline: true, inverse: true });
386
521
  }
387
522
  else if (hasFocus && inCursor) {
388
- style = inNewline
523
+ style = isDimSigil
389
524
  ? nlStyle.merge({ underline: true })
390
- : cursorStyle;
525
+ : baseStyle.merge({ underline: true });
391
526
  }
392
- else if (inNewline) {
527
+ else if (isDimSigil) {
393
528
  style = nlStyle;
394
529
  }
395
530
  else {
396
- style = plainStyle;
531
+ style = baseStyle;
397
532
  }
398
533
  }
399
534
  else {
400
535
  if (inSelection) {
401
- style = inNewline
402
- ? nlStyle.merge({ background: selectedStyle.foreground })
536
+ style = isDimSigil
537
+ ? selectedStyle.merge({ foreground: nlStyle.foreground })
403
538
  : selectedStyle.merge({ underline: inCursor });
404
539
  }
405
- else if (inNewline) {
540
+ else if (isDimSigil) {
406
541
  style = nlStyle;
407
542
  }
408
543
  else {
409
- style = plainStyle;
544
+ style = baseStyle;
410
545
  }
411
546
  }
412
547
  if (!currentStyle.isEqual(style)) {
@@ -430,18 +565,31 @@ export class Input extends View {
430
565
  drawEllipses = true;
431
566
  }
432
567
  }
433
- viewport.write(drawEllipses ? '…' : char, scanTextPosition.offset(-cursorVisible.x, -cursorVisible.y));
568
+ if (!drawEllipses && char === '\t') {
569
+ // Render tab as two-char sigil
570
+ const offset = scanTextPosition.offset(-cursorVisible.x, -cursorVisible.y);
571
+ viewport.write(TAB_SIGIL[0], offset);
572
+ viewport.write(TAB_SIGIL[1], offset.offset(1, 0));
573
+ }
574
+ else {
575
+ viewport.write(drawEllipses ? '…' : char, scanTextPosition.offset(-cursorVisible.x, -cursorVisible.y));
576
+ }
434
577
  drawInitialEllipses = false;
435
578
  }
436
579
  else {
437
580
  drawInitialEllipses = true;
438
581
  }
439
582
  scanTextPosition.x += charWidth;
583
+ charIndex++;
440
584
  if (scanTextPosition.x - cursorVisible.x >=
441
585
  viewport.contentSize.width) {
442
586
  break;
443
587
  }
444
588
  }
589
+ // Advance formatOffset past this line's chars (excluding the trailing sigil).
590
+ if (hasFormatStyles) {
591
+ formatOffset += line.length - 1;
592
+ }
445
593
  scanTextPosition.y += 1;
446
594
  if (scanTextPosition.y - cursorVisible.y >=
447
595
  viewport.contentSize.height) {
@@ -680,11 +828,21 @@ export class Input extends View {
680
828
  this.minSelected() + (advance ? 1 : 0);
681
829
  }
682
830
  }
683
- #receiveGotoStart() {
684
- this.#cursor = { start: 0, end: 0 };
831
+ #receiveGotoStart({ shift }) {
832
+ if (shift) {
833
+ this.#cursor.end = 0;
834
+ }
835
+ else {
836
+ this.#cursor = { start: 0, end: 0 };
837
+ }
685
838
  }
686
- #receiveGotoEnd() {
687
- this.#cursor = { start: this.#chars.length, end: this.#chars.length };
839
+ #receiveGotoEnd({ shift }) {
840
+ if (shift) {
841
+ this.#cursor.end = this.#chars.length;
842
+ }
843
+ else {
844
+ this.#cursor = { start: this.#chars.length, end: this.#chars.length };
845
+ }
688
846
  }
689
847
  #receiveHome({ shift }) {
690
848
  let dest = 0;
@@ -698,8 +856,8 @@ export class Input extends View {
698
856
  const [targetChars, targetWidth] = this.#wrappedLines[cursorPosition.y];
699
857
  dest = this.#wrappedLines
700
858
  .slice(0, cursorPosition.y)
701
- .reduce((dest, [, width]) => {
702
- return dest + width;
859
+ .reduce((dest, [chars]) => {
860
+ return dest + chars.length;
703
861
  }, 0);
704
862
  }
705
863
  if (shift) {
@@ -722,8 +880,8 @@ export class Input extends View {
722
880
  dest =
723
881
  this.#wrappedLines
724
882
  .slice(0, cursorPosition.y + 1)
725
- .reduce((dest, [, width]) => {
726
- return dest + width;
883
+ .reduce((dest, [chars]) => {
884
+ return dest + chars.length;
727
885
  }, 0) - 1;
728
886
  }
729
887
  if (shift) {
@@ -745,19 +903,21 @@ export class Input extends View {
745
903
  const [targetChars, targetWidth] = this.#wrappedLines[cursorPosition.y - 1];
746
904
  dest = this.#wrappedLines
747
905
  .slice(0, cursorPosition.y - 1)
748
- .reduce((dest, [, width]) => {
749
- return dest + width;
906
+ .reduce((dest, [chars]) => {
907
+ return dest + chars.length;
750
908
  }, 0);
751
909
  if (targetWidth <= cursorPosition.x) {
752
- dest += targetWidth - 1;
910
+ dest += targetChars.length - 1;
753
911
  }
754
912
  else {
913
+ let displayX = 0;
755
914
  let destOffset = 0;
756
915
  for (const char of targetChars) {
757
916
  const charWidth = unicode.charWidth(char);
758
- if (destOffset + charWidth > cursorPosition.x) {
917
+ if (displayX + charWidth > cursorPosition.x) {
759
918
  break;
760
919
  }
920
+ displayX += charWidth;
761
921
  destOffset += 1;
762
922
  }
763
923
  dest += destOffset;
@@ -783,19 +943,21 @@ export class Input extends View {
783
943
  const [targetChars, targetWidth] = this.#wrappedLines[cursorPosition.y + 1];
784
944
  dest = this.#wrappedLines
785
945
  .slice(0, cursorPosition.y + 1)
786
- .reduce((dest, [, width]) => {
787
- return dest + width;
946
+ .reduce((dest, [chars]) => {
947
+ return dest + chars.length;
788
948
  }, 0);
789
949
  if (targetWidth <= cursorPosition.x) {
790
- dest += targetWidth - 1;
950
+ dest += targetChars.length - 1;
791
951
  }
792
952
  else {
953
+ let displayX = 0;
793
954
  let destOffset = 0;
794
955
  for (const char of targetChars) {
795
956
  const charWidth = unicode.charWidth(char);
796
- if (destOffset + charWidth > cursorPosition.x) {
957
+ if (displayX + charWidth > cursorPosition.x) {
797
958
  break;
798
959
  }
960
+ displayX += charWidth;
799
961
  destOffset += 1;
800
962
  }
801
963
  dest += destOffset;
@@ -942,6 +1104,162 @@ export class Input extends View {
942
1104
  this.#cursor.start = this.#cursor.end = offset;
943
1105
  this.#updateWidth();
944
1106
  }
1107
+ /**
1108
+ * Insert a newline followed by the current line's leading whitespace.
1109
+ */
1110
+ #receiveEnterWithIndent() {
1111
+ const indent = this.#currentLineIndent();
1112
+ const chars = ['\n', ...indent];
1113
+ if (isEmptySelection(this.#cursor)) {
1114
+ this.#chars = this.#chars
1115
+ .slice(0, this.#cursor.start)
1116
+ .concat(chars, this.#chars.slice(this.#cursor.start));
1117
+ this.#cursor.start = this.#cursor.end = this.#cursor.start + chars.length;
1118
+ }
1119
+ else {
1120
+ this.#chars = this.#chars
1121
+ .slice(0, this.minSelected())
1122
+ .concat(chars, this.#chars.slice(this.maxSelected()));
1123
+ this.#cursor.start = this.#cursor.end = this.minSelected() + chars.length;
1124
+ }
1125
+ }
1126
+ /**
1127
+ * Indent the current line. Uses tabs if any line starts with a tab,
1128
+ * otherwise uses two spaces.
1129
+ */
1130
+ #receiveIndent() {
1131
+ const indent = this.#useTabs() ? ['\t'] : [' ', ' '];
1132
+ const lineStart = this.#currentLineStart();
1133
+ this.#chars = this.#chars
1134
+ .slice(0, lineStart)
1135
+ .concat(indent, this.#chars.slice(lineStart));
1136
+ this.#cursor.start += indent.length;
1137
+ this.#cursor.end += indent.length;
1138
+ }
1139
+ /**
1140
+ * Remove one level of indentation from the current line.
1141
+ */
1142
+ #receiveDedent() {
1143
+ const lineStart = this.#currentLineStart();
1144
+ if (this.#chars[lineStart] === '\t') {
1145
+ this.#chars = this.#chars
1146
+ .slice(0, lineStart)
1147
+ .concat(this.#chars.slice(lineStart + 1));
1148
+ this.#cursor.start = Math.max(lineStart, this.#cursor.start - 1);
1149
+ this.#cursor.end = Math.max(lineStart, this.#cursor.end - 1);
1150
+ }
1151
+ else if (this.#chars[lineStart] === ' ' &&
1152
+ this.#chars[lineStart + 1] === ' ') {
1153
+ this.#chars = this.#chars
1154
+ .slice(0, lineStart)
1155
+ .concat(this.#chars.slice(lineStart + 2));
1156
+ this.#cursor.start = Math.max(lineStart, this.#cursor.start - 2);
1157
+ this.#cursor.end = Math.max(lineStart, this.#cursor.end - 2);
1158
+ }
1159
+ }
1160
+ /**
1161
+ * Returns the leading whitespace characters of the current line.
1162
+ */
1163
+ #currentLineIndent() {
1164
+ const lineStart = this.#currentLineStart();
1165
+ const indent = [];
1166
+ for (let i = lineStart; i < this.#chars.length; i++) {
1167
+ if (this.#chars[i] === ' ' || this.#chars[i] === '\t') {
1168
+ indent.push(this.#chars[i]);
1169
+ }
1170
+ else {
1171
+ break;
1172
+ }
1173
+ }
1174
+ return indent;
1175
+ }
1176
+ /**
1177
+ * Returns the index in #chars where the current line starts.
1178
+ */
1179
+ #currentLineStart() {
1180
+ const pos = Math.min(this.#cursor.end, this.#chars.length);
1181
+ for (let i = pos - 1; i >= 0; i--) {
1182
+ if (this.#chars[i] === '\n') {
1183
+ return i + 1;
1184
+ }
1185
+ }
1186
+ return 0;
1187
+ }
1188
+ /**
1189
+ * Returns true if any line in the current text starts with a tab character.
1190
+ */
1191
+ #useTabs() {
1192
+ for (let i = 0; i < this.#chars.length; i++) {
1193
+ if (this.#chars[i] === '\t' && (i === 0 || this.#chars[i - 1] === '\n')) {
1194
+ return true;
1195
+ }
1196
+ }
1197
+ return false;
1198
+ }
1199
+ /**
1200
+ * Snapshot the current state before an edit.
1201
+ */
1202
+ #beginEdit(kind) {
1203
+ this.#pendingUndoKind = kind;
1204
+ this.#preEditChars = [...this.#chars];
1205
+ this.#preEditCursor = { ...this.#cursor };
1206
+ }
1207
+ /**
1208
+ * Push the completed edit onto the undo stack. For consecutive inserts at
1209
+ * adjacent positions, coalesce into a single undo entry.
1210
+ */
1211
+ #commitEdit() {
1212
+ if (!this.#pendingUndoKind) {
1213
+ return;
1214
+ }
1215
+ const kind = this.#pendingUndoKind;
1216
+ this.#pendingUndoKind = undefined;
1217
+ const entry = {
1218
+ kind,
1219
+ beforeChars: this.#preEditChars,
1220
+ beforeCursor: this.#preEditCursor,
1221
+ afterChars: [...this.#chars],
1222
+ afterCursor: { ...this.#cursor },
1223
+ insertOffset: kind === 'insert' ? this.#preEditCursor.start : undefined,
1224
+ };
1225
+ // Coalesce consecutive inserts at adjacent positions
1226
+ if (kind === 'insert' &&
1227
+ this.#insertCoalesceEnabled &&
1228
+ this.#undoStack.length > 0) {
1229
+ const last = this.#undoStack[this.#undoStack.length - 1];
1230
+ if (last.kind === 'insert' &&
1231
+ last.afterCursor.start === entry.insertOffset) {
1232
+ // Extend the previous entry
1233
+ last.afterChars = entry.afterChars;
1234
+ last.afterCursor = entry.afterCursor;
1235
+ this.#redoStack = [];
1236
+ return;
1237
+ }
1238
+ }
1239
+ this.#insertCoalesceEnabled = kind === 'insert';
1240
+ this.#undoStack.push(entry);
1241
+ this.#redoStack = [];
1242
+ }
1243
+ #undo() {
1244
+ const entry = this.#undoStack.pop();
1245
+ if (!entry) {
1246
+ return;
1247
+ }
1248
+ this.#redoStack.push(entry);
1249
+ this.#chars = [...entry.beforeChars];
1250
+ this.#cursor = { ...entry.beforeCursor };
1251
+ this.#updateLines(this.#chars, undefined);
1252
+ }
1253
+ #redo() {
1254
+ const entry = this.#redoStack.pop();
1255
+ if (!entry) {
1256
+ return;
1257
+ }
1258
+ this.#undoStack.push(entry);
1259
+ this.#chars = [...entry.afterChars];
1260
+ this.#cursor = { ...entry.afterCursor };
1261
+ this.#updateLines(this.#chars, undefined);
1262
+ }
945
1263
  }
946
1264
  function isEmptySelection(cursor) {
947
1265
  return cursor.start === cursor.end;
@@ -1113,9 +1431,11 @@ const ACCENTS = {
1113
1431
  };
1114
1432
  const ACCENT_KEYS = {
1115
1433
  'A-a': '‵',
1434
+ 'A-`': '‵',
1116
1435
  'A-c': '¸',
1117
1436
  'A-e': '´',
1118
1437
  'A-i': 'ˆ',
1438
+ 'A-6': 'ˆ',
1119
1439
  'A-n': '˜',
1120
1440
  'A-o': '¯',
1121
1441
  'A-s': '¸',
@@ -1130,4 +1450,8 @@ function isKeyAccent(event) {
1130
1450
  }
1131
1451
  return ACCENT_KEYS[event.full] !== undefined;
1132
1452
  }
1453
+ const RESET_RE = /^\x1b\[0?m$/;
1454
+ function isTabSigil(char) {
1455
+ return char === '\t';
1456
+ }
1133
1457
  //# sourceMappingURL=Input.js.map