@jrichman/ink 6.6.9 → 7.0.0-beta

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 (78) hide show
  1. package/build/components/Box.js +7 -4
  2. package/build/components/Box.js.map +1 -1
  3. package/build/components/StaticRender.d.ts +1 -1
  4. package/build/components/StaticRender.js +19 -7
  5. package/build/components/StaticRender.js.map +1 -1
  6. package/build/dom.d.ts +8 -3
  7. package/build/dom.js +42 -12
  8. package/build/dom.js.map +1 -1
  9. package/build/get-max-width.js +1 -1
  10. package/build/get-max-width.js.map +1 -1
  11. package/build/ink.d.ts +1 -0
  12. package/build/ink.js +61 -5
  13. package/build/ink.js.map +1 -1
  14. package/build/measure-element.js +25 -16
  15. package/build/measure-element.js.map +1 -1
  16. package/build/measure-text.js +8 -3
  17. package/build/measure-text.js.map +1 -1
  18. package/build/output.d.ts +60 -2
  19. package/build/output.js +259 -66
  20. package/build/output.js.map +1 -1
  21. package/build/reconciler.js +36 -6
  22. package/build/reconciler.js.map +1 -1
  23. package/build/render-background.js +2 -2
  24. package/build/render-background.js.map +1 -1
  25. package/build/render-border.js +2 -2
  26. package/build/render-border.js.map +1 -1
  27. package/build/render-container.js +2 -4
  28. package/build/render-container.js.map +1 -1
  29. package/build/render-node-to-output.js +163 -40
  30. package/build/render-node-to-output.js.map +1 -1
  31. package/build/render-sticky.d.ts +3 -3
  32. package/build/render-sticky.js +18 -18
  33. package/build/render-sticky.js.map +1 -1
  34. package/build/renderer.d.ts +1 -0
  35. package/build/renderer.js +58 -14
  36. package/build/renderer.js.map +1 -1
  37. package/build/resize-observer.js +4 -4
  38. package/build/resize-observer.js.map +1 -1
  39. package/build/scroll.js +3 -3
  40. package/build/scroll.js.map +1 -1
  41. package/build/serialization.js +1 -1
  42. package/build/serialization.js.map +1 -1
  43. package/build/squash-text-nodes.js +15 -6
  44. package/build/squash-text-nodes.js.map +1 -1
  45. package/build/styled-line.d.ts +8 -0
  46. package/build/styled-line.js +438 -141
  47. package/build/styled-line.js.map +1 -1
  48. package/build/terminal-buffer.d.ts +1 -0
  49. package/build/terminal-buffer.js +198 -51
  50. package/build/terminal-buffer.js.map +1 -1
  51. package/build/tokenize.d.ts +1 -0
  52. package/build/tokenize.js +146 -0
  53. package/build/tokenize.js.map +1 -1
  54. package/build/worker/animation-controller.js +1 -1
  55. package/build/worker/animation-controller.js.map +1 -1
  56. package/build/worker/canvas.js +15 -3
  57. package/build/worker/canvas.js.map +1 -1
  58. package/build/worker/compositor.js +2 -1
  59. package/build/worker/compositor.js.map +1 -1
  60. package/build/worker/render-worker.d.ts +15 -5
  61. package/build/worker/render-worker.js +34 -28
  62. package/build/worker/render-worker.js.map +1 -1
  63. package/build/worker/scene-manager.d.ts +1 -1
  64. package/build/worker/scene-manager.js +28 -23
  65. package/build/worker/scene-manager.js.map +1 -1
  66. package/build/worker/scroll-optimizer.d.ts +1 -1
  67. package/build/worker/scroll-optimizer.js +3 -3
  68. package/build/worker/scroll-optimizer.js.map +1 -1
  69. package/build/worker/terminal-writer.d.ts +0 -1
  70. package/build/worker/terminal-writer.js +13 -8
  71. package/build/worker/terminal-writer.js.map +1 -1
  72. package/build/worker/worker-entry.js +1 -2
  73. package/build/worker/worker-entry.js.map +1 -1
  74. package/package.json +3 -3
  75. package/readme.md +27 -8
  76. package/build/wrap-text.d.ts +0 -6
  77. package/build/wrap-text.js +0 -120
  78. package/build/wrap-text.js.map +0 -1
@@ -6,6 +6,33 @@
6
6
  import { FULL_WIDTH_MASK, INVERSE_MASK } from './tokenize.js';
7
7
  const OFFSET_MASK = 0x3f_ff_ff_ff;
8
8
  const FULL_WIDTH_FLAG = 0x40_00_00_00;
9
+ function hasAnyStyles(formatFlags, fgColor, bgColor, link) {
10
+ return ((formatFlags & ~FULL_WIDTH_MASK) !== 0 ||
11
+ fgColor !== undefined ||
12
+ bgColor !== undefined ||
13
+ link !== undefined);
14
+ }
15
+ function spanHasStyles(span) {
16
+ return hasAnyStyles(span.formatFlags, span.fgColor, span.bgColor, span.link);
17
+ }
18
+ function spansHaveStyles(spans) {
19
+ if (!spans)
20
+ return false;
21
+ for (const s of spans) {
22
+ if (spanHasStyles(s))
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+ function isDefaultCharData(charData, length) {
28
+ if (!charData)
29
+ return true;
30
+ for (let i = 0; i < length; i++) {
31
+ if (charData[i] !== i)
32
+ return false;
33
+ }
34
+ return true;
35
+ }
9
36
  export class StyledLine {
10
37
  static empty(length) {
11
38
  if (length <= 0) {
@@ -18,17 +45,10 @@ export class StyledLine {
18
45
  const line = new StyledLine();
19
46
  line.length = length;
20
47
  line.text = ' '.repeat(length);
21
- line.charData = Array.from({ length });
22
- for (let i = 0; i < length; i++) {
23
- line.charData[i] = i;
24
- }
25
- line.spans = [{ length, formatFlags: 0 }];
26
48
  line._cachedTrimmedLength = 0;
27
49
  if (StyledLine.emptyCache.size > 100) {
28
50
  StyledLine.emptyCache.clear();
29
51
  }
30
- Object.freeze(line.spans[0]);
31
- Object.freeze(line.spans);
32
52
  Object.freeze(line);
33
53
  StyledLine.emptyCache.set(length, line);
34
54
  return line.clone();
@@ -44,12 +64,50 @@ export class StyledLine {
44
64
  charData;
45
65
  spans;
46
66
  _cachedTrimmedLength;
67
+ _spansDirty = false;
47
68
  constructor() {
48
69
  this.length = 0;
49
70
  }
71
+ padTo(targetLength) {
72
+ if (targetLength <= this.length)
73
+ return;
74
+ this._cachedTrimmedLength = undefined;
75
+ this.ensureInitialized();
76
+ const diff = targetLength - this.length;
77
+ if (this.charData === undefined) {
78
+ this.text += ' '.repeat(diff);
79
+ }
80
+ else {
81
+ const offset = this.text.length;
82
+ this.text += ' '.repeat(diff);
83
+ for (let i = 0; i < diff; i++) {
84
+ this.charData.push(offset + i);
85
+ }
86
+ }
87
+ if (this.spans !== undefined) {
88
+ const lastSpan = this.spans.at(-1);
89
+ if (lastSpan &&
90
+ lastSpan.formatFlags === 0 &&
91
+ lastSpan.fgColor === undefined &&
92
+ lastSpan.bgColor === undefined &&
93
+ lastSpan.link === undefined) {
94
+ lastSpan.length += diff;
95
+ }
96
+ else {
97
+ this.spans.push({
98
+ length: diff,
99
+ formatFlags: 0,
100
+ });
101
+ }
102
+ }
103
+ this.length = targetLength;
104
+ }
50
105
  getValue(index) {
51
106
  if (this.text === undefined || index < 0 || index >= this.length)
52
107
  return '';
108
+ if (this.charData === undefined) {
109
+ return this.text[index];
110
+ }
53
111
  const start = this.charData[index] & OFFSET_MASK;
54
112
  const end = index + 1 < this.length
55
113
  ? this.charData[index + 1] & OFFSET_MASK
@@ -73,13 +131,12 @@ export class StyledLine {
73
131
  return (this.charData[index] & FULL_WIDTH_FLAG) !== 0;
74
132
  }
75
133
  hasStyles(index) {
134
+ if (this.spans === undefined)
135
+ return false;
76
136
  const span = this.getSpan(index);
77
137
  if (!span)
78
138
  return false;
79
- return ((span.formatFlags & ~FULL_WIDTH_MASK) !== 0 ||
80
- span.fgColor !== undefined ||
81
- span.bgColor !== undefined ||
82
- span.link !== undefined);
139
+ return spanHasStyles(span);
83
140
  }
84
141
  getFormatFlags(index) {
85
142
  let flags = this.getSpan(index)?.formatFlags ?? 0;
@@ -102,6 +159,7 @@ export class StyledLine {
102
159
  return;
103
160
  this._cachedTrimmedLength = undefined;
104
161
  this.ensureInitialized();
162
+ this.ensureSpans();
105
163
  this.splitSpansAt(index);
106
164
  this.splitSpansAt(index + 1);
107
165
  let current = 0;
@@ -117,13 +175,14 @@ export class StyledLine {
117
175
  }
118
176
  current += span.length;
119
177
  }
120
- this.mergeSpans();
178
+ this._spansDirty = true;
121
179
  }
122
180
  setBackgroundColor(index, color) {
123
181
  if (index < 0 || index >= this.length)
124
182
  return;
125
183
  this._cachedTrimmedLength = undefined;
126
184
  this.ensureInitialized();
185
+ this.ensureSpans();
127
186
  this.splitSpansAt(index);
128
187
  this.splitSpansAt(index + 1);
129
188
  let current = 0;
@@ -134,13 +193,14 @@ export class StyledLine {
134
193
  }
135
194
  current += span.length;
136
195
  }
137
- this.mergeSpans();
196
+ this._spansDirty = true;
138
197
  }
139
198
  setForegroundColor(index, color) {
140
199
  if (index < 0 || index >= this.length)
141
200
  return;
142
201
  this._cachedTrimmedLength = undefined;
143
202
  this.ensureInitialized();
203
+ this.ensureSpans();
144
204
  this.splitSpansAt(index);
145
205
  this.splitSpansAt(index + 1);
146
206
  let current = 0;
@@ -151,7 +211,26 @@ export class StyledLine {
151
211
  }
152
212
  current += span.length;
153
213
  }
154
- this.mergeSpans();
214
+ this._spansDirty = true;
215
+ }
216
+ replaceAt(index, chars) {
217
+ if (chars.length === 0 || index >= this.length)
218
+ return;
219
+ this.ensureInitialized();
220
+ chars.ensureInitialized();
221
+ const start = Math.max(0, index);
222
+ const end = Math.min(this.length, start + chars.length);
223
+ const replacementLen = end - start;
224
+ const slicedChars = chars.length > replacementLen ? chars.slice(0, replacementLen) : chars;
225
+ const left = this.slice(0, start);
226
+ const right = this.slice(end);
227
+ const combined = left.combine(slicedChars, right);
228
+ this.length = combined.length;
229
+ this.text = combined.text;
230
+ this.charData = combined.charData;
231
+ this.spans = combined.spans;
232
+ this._spansDirty = combined._spansDirty;
233
+ this._cachedTrimmedLength = undefined;
155
234
  }
156
235
  // eslint-disable-next-line max-params
157
236
  setChar(index, value, formatFlags, fgColor, bgColor, link) {
@@ -161,26 +240,105 @@ export class StyledLine {
161
240
  this.ensureInitialized();
162
241
  const isFullWidth = (formatFlags & FULL_WIDTH_MASK) !== 0;
163
242
  const cleanFormatFlags = formatFlags & ~FULL_WIDTH_MASK;
164
- const start = this.charData[index] & OFFSET_MASK;
165
- const end = index + 1 < this.length
166
- ? this.charData[index + 1] & OFFSET_MASK
167
- : this.text.length;
168
- const oldLen = end - start;
243
+ const hasStyles = hasAnyStyles(formatFlags, fgColor, bgColor, link);
169
244
  const newLen = value.length;
170
- if (oldLen === newLen) {
171
- this.text = this.text.slice(0, start) + value + this.text.slice(end);
245
+ if (this.charData === undefined) {
246
+ if (newLen === 1 && !isFullWidth) {
247
+ this.text =
248
+ this.text.slice(0, index) + value + this.text.slice(index + 1);
249
+ }
250
+ else {
251
+ this.ensureCharData();
252
+ }
172
253
  }
173
- else {
174
- this.text = this.text.slice(0, start) + value + this.text.slice(end);
175
- const diff = newLen - oldLen;
176
- for (let i = index + 1; i < this.length; i++) {
177
- const data = this.charData[i];
178
- const oldOffset = data & OFFSET_MASK;
179
- const fw = data & FULL_WIDTH_FLAG;
180
- this.charData[i] = (oldOffset + diff) | fw;
254
+ if (this.charData !== undefined) {
255
+ const start = this.charData[index] & OFFSET_MASK;
256
+ const end = index + 1 < this.length
257
+ ? this.charData[index + 1] & OFFSET_MASK
258
+ : this.text.length;
259
+ const oldLen = end - start;
260
+ if (oldLen === newLen) {
261
+ this.text = this.text.slice(0, start) + value + this.text.slice(end);
262
+ }
263
+ else {
264
+ this.text = this.text.slice(0, start) + value + this.text.slice(end);
265
+ const diff = newLen - oldLen;
266
+ for (let i = index + 1; i < this.length; i++) {
267
+ const data = this.charData[i];
268
+ const oldOffset = data & OFFSET_MASK;
269
+ const fw = data & FULL_WIDTH_FLAG;
270
+ this.charData[i] = (oldOffset + diff) | fw;
271
+ }
272
+ }
273
+ this.charData[index] = start | (isFullWidth ? FULL_WIDTH_FLAG : 0);
274
+ }
275
+ if (this.spans === undefined) {
276
+ if (!hasStyles) {
277
+ return;
278
+ }
279
+ this.ensureSpans();
280
+ }
281
+ // Fast path: Find the span containing this index
282
+ let currentOffset = 0;
283
+ let spanIndex = -1;
284
+ let span;
285
+ for (let i = 0; i < this.spans.length; i++) {
286
+ const s = this.spans[i];
287
+ if (currentOffset <= index && currentOffset + s.length > index) {
288
+ spanIndex = i;
289
+ span = s;
290
+ break;
291
+ }
292
+ currentOffset += s.length;
293
+ }
294
+ if (span) {
295
+ const isMatch = span.formatFlags === cleanFormatFlags &&
296
+ span.fgColor === fgColor &&
297
+ span.bgColor === bgColor &&
298
+ span.link === link;
299
+ if (isMatch) {
300
+ return; // Style already matches, no need to touch spans
301
+ }
302
+ // If it's the very first character of the span, check if we can merge with the previous span
303
+ if (index === currentOffset && spanIndex > 0) {
304
+ const prevSpan = this.spans[spanIndex - 1];
305
+ const prevMatch = prevSpan.formatFlags === cleanFormatFlags &&
306
+ prevSpan.fgColor === fgColor &&
307
+ prevSpan.bgColor === bgColor &&
308
+ prevSpan.link === link;
309
+ if (prevMatch) {
310
+ prevSpan.length += 1;
311
+ if (span.length === 1) {
312
+ this.spans.splice(spanIndex, 1);
313
+ }
314
+ else {
315
+ span.length -= 1;
316
+ }
317
+ this._spansDirty = true;
318
+ return;
319
+ }
320
+ }
321
+ // If it's the very last character of the span, check if we can merge with the next span
322
+ if (index === currentOffset + span.length - 1 &&
323
+ spanIndex < this.spans.length - 1) {
324
+ const nextSpan = this.spans[spanIndex + 1];
325
+ const nextMatch = nextSpan.formatFlags === cleanFormatFlags &&
326
+ nextSpan.fgColor === fgColor &&
327
+ nextSpan.bgColor === bgColor &&
328
+ nextSpan.link === link;
329
+ if (nextMatch) {
330
+ nextSpan.length += 1;
331
+ if (span.length === 1) {
332
+ this.spans.splice(spanIndex, 1);
333
+ }
334
+ else {
335
+ span.length -= 1;
336
+ }
337
+ this._spansDirty = true;
338
+ return;
339
+ }
181
340
  }
182
341
  }
183
- this.charData[index] = start | (isFullWidth ? FULL_WIDTH_FLAG : 0);
184
342
  this.splitSpansAt(index);
185
343
  this.splitSpansAt(index + 1);
186
344
  let current = 0;
@@ -198,7 +356,7 @@ export class StyledLine {
198
356
  }
199
357
  current += span.length;
200
358
  }
201
- this.mergeSpans();
359
+ this._spansDirty = true;
202
360
  }
203
361
  // eslint-disable-next-line max-params
204
362
  pushChar(value, formatFlags, fgColor, bgColor, link) {
@@ -206,9 +364,28 @@ export class StyledLine {
206
364
  this.ensureInitialized();
207
365
  const isFullWidth = (formatFlags & FULL_WIDTH_MASK) !== 0;
208
366
  const cleanFormatFlags = formatFlags & ~FULL_WIDTH_MASK;
209
- const offset = this.text.length;
210
- this.text += value;
211
- this.charData.push(offset | (isFullWidth ? FULL_WIDTH_FLAG : 0));
367
+ const hasStyles = hasAnyStyles(formatFlags, fgColor, bgColor, link);
368
+ const newLen = value.length;
369
+ if (this.charData === undefined) {
370
+ if (newLen === 1 && !isFullWidth) {
371
+ this.text += value;
372
+ }
373
+ else {
374
+ this.ensureCharData();
375
+ }
376
+ }
377
+ if (this.charData !== undefined) {
378
+ const offset = this.text.length;
379
+ this.text += value;
380
+ this.charData.push(offset | (isFullWidth ? FULL_WIDTH_FLAG : 0));
381
+ }
382
+ if (this.spans === undefined) {
383
+ if (!hasStyles) {
384
+ this.length++;
385
+ return;
386
+ }
387
+ this.ensureSpans();
388
+ }
212
389
  const lastSpan = this.spans.at(-1);
213
390
  if (lastSpan &&
214
391
  lastSpan.formatFlags === cleanFormatFlags &&
@@ -229,19 +406,17 @@ export class StyledLine {
229
406
  this.length++;
230
407
  }
231
408
  clone() {
232
- if (this.charData === undefined)
233
- return new StyledLine();
409
+ this.ensureSpansMerged();
234
410
  const result = new StyledLine();
235
411
  result.length = this.length;
236
412
  result.text = this.text;
237
- result.charData = [...this.charData];
238
- result.spans = this.spans.map(span => ({ ...span }));
413
+ result.charData = this.charData ? [...this.charData] : undefined;
414
+ result.spans = this.spans ? this.spans.map(span => ({ ...span })) : undefined;
239
415
  result._cachedTrimmedLength = this._cachedTrimmedLength;
240
416
  return result;
241
417
  }
242
418
  slice(start, end) {
243
- if (this.charData === undefined)
244
- return new StyledLine();
419
+ this.ensureSpansMerged();
245
420
  const actualStart = Math.max(0, start);
246
421
  const actualEnd = end === undefined ? this.length : Math.min(this.length, end);
247
422
  if (actualStart >= actualEnd)
@@ -251,40 +426,53 @@ export class StyledLine {
251
426
  }
252
427
  const result = new StyledLine();
253
428
  result.length = actualEnd - actualStart;
254
- result.charData = Array.from({ length: result.length });
255
- const textStart = this.charData[actualStart] & OFFSET_MASK;
256
- const textEnd = actualEnd < this.length
257
- ? this.charData[actualEnd] & OFFSET_MASK
258
- : this.text.length;
259
- result.text = this.text.slice(textStart, textEnd);
260
- for (let i = 0; i < result.length; i++) {
261
- const oldData = this.charData[actualStart + i];
262
- const oldOffset = oldData & OFFSET_MASK;
263
- const fw = oldData & FULL_WIDTH_FLAG;
264
- result.charData[i] = (oldOffset - textStart) | fw;
429
+ if (this.charData === undefined) {
430
+ if (this.text !== undefined) {
431
+ result.text = this.text.slice(actualStart, actualEnd);
432
+ }
265
433
  }
266
- const newSpans = [];
267
- let current = 0;
268
- for (const span of this.spans) {
269
- const spanStart = current;
270
- const spanEnd = current + span.length;
271
- const intersectStart = Math.max(actualStart, spanStart);
272
- const intersectEnd = Math.min(actualEnd, spanEnd);
273
- if (intersectStart < intersectEnd) {
274
- newSpans.push({
275
- ...span,
276
- length: intersectEnd - intersectStart,
277
- });
434
+ else {
435
+ result.charData = Array.from({ length: result.length });
436
+ const textStart = this.charData[actualStart] & OFFSET_MASK;
437
+ const textEnd = actualEnd < this.length
438
+ ? this.charData[actualEnd] & OFFSET_MASK
439
+ : this.text.length;
440
+ result.text = this.text.slice(textStart, textEnd);
441
+ for (let i = 0; i < result.length; i++) {
442
+ const oldData = this.charData[actualStart + i];
443
+ const oldOffset = oldData & OFFSET_MASK;
444
+ const fw = oldData & FULL_WIDTH_FLAG;
445
+ result.charData[i] = (oldOffset - textStart) | fw;
278
446
  }
279
- current += span.length;
280
- if (current >= actualEnd)
281
- break;
282
447
  }
283
- result.spans = newSpans;
284
- result.mergeSpans();
448
+ if (this.spans !== undefined) {
449
+ const newSpans = [];
450
+ let current = 0;
451
+ for (const span of this.spans) {
452
+ const spanStart = current;
453
+ const spanEnd = current + span.length;
454
+ const intersectStart = Math.max(actualStart, spanStart);
455
+ const intersectEnd = Math.min(actualEnd, spanEnd);
456
+ if (intersectStart < intersectEnd) {
457
+ newSpans.push({
458
+ ...span,
459
+ length: intersectEnd - intersectStart,
460
+ });
461
+ }
462
+ current += span.length;
463
+ if (current >= actualEnd)
464
+ break;
465
+ }
466
+ result.spans = newSpans;
467
+ result.mergeSpans();
468
+ }
285
469
  return result;
286
470
  }
287
471
  combine(...others) {
472
+ this.ensureSpansMerged();
473
+ for (const other of others) {
474
+ other.ensureSpansMerged();
475
+ }
288
476
  if (others.length === 0)
289
477
  return this.clone();
290
478
  const allLines = [this, ...others].filter(l => l.length > 0);
@@ -299,61 +487,103 @@ export class StyledLine {
299
487
  const result = new StyledLine();
300
488
  result.length = totalChars;
301
489
  result.text = allLines.map(l => l.getText()).join('');
302
- result.charData = Array.from({ length: totalChars });
303
- let currentChar = 0;
304
- let currentOffset = 0;
305
- for (const line of allLines) {
306
- const lineCharData = line.charData;
307
- const lineText = line.getText();
308
- if (lineCharData) {
309
- for (let i = 0; i < line.length; i++) {
310
- const data = lineCharData[i];
311
- const offset = data & OFFSET_MASK;
312
- const fw = data & FULL_WIDTH_FLAG;
313
- result.charData[currentChar + i] = (currentOffset + offset) | fw;
490
+ const anyHasCharData = allLines.some(l => l.charData !== undefined);
491
+ if (anyHasCharData) {
492
+ result.charData = Array.from({ length: totalChars });
493
+ let currentChar = 0;
494
+ let currentOffset = 0;
495
+ for (const line of allLines) {
496
+ const lineCharData = line.charData;
497
+ const lineText = line.getText();
498
+ if (lineCharData) {
499
+ for (let i = 0; i < line.length; i++) {
500
+ const data = lineCharData[i];
501
+ const offset = data & OFFSET_MASK;
502
+ const fw = data & FULL_WIDTH_FLAG;
503
+ result.charData[currentChar + i] = (currentOffset + offset) | fw;
504
+ }
314
505
  }
315
- }
316
- else {
317
- for (let i = 0; i < line.length; i++) {
318
- result.charData[currentChar + i] = currentOffset + i;
506
+ else {
507
+ for (let i = 0; i < line.length; i++) {
508
+ result.charData[currentChar + i] = currentOffset + i;
509
+ }
319
510
  }
511
+ currentChar += line.length;
512
+ currentOffset += lineText.length;
320
513
  }
321
- currentChar += line.length;
322
- currentOffset += lineText.length;
323
514
  }
324
- result.spans = allLines.flatMap(l => l.getSpans().map(s => ({ ...s })));
325
- result.mergeSpans();
515
+ const anyHasSpans = allLines.some(l => l.spans !== undefined);
516
+ if (anyHasSpans) {
517
+ result.spans = allLines.flatMap(l => l.getSpans().map(s => ({ ...s })));
518
+ result.mergeSpans();
519
+ }
326
520
  return result;
327
521
  }
328
522
  getTrimmedLength() {
329
- if (this.length === 0)
523
+ if (this._cachedTrimmedLength !== undefined) {
524
+ return this._cachedTrimmedLength;
525
+ }
526
+ if (this.length === 0) {
527
+ this._cachedTrimmedLength = 0;
330
528
  return 0;
331
- if (this.text === undefined || this.charData === undefined)
529
+ }
530
+ if (this.text === undefined) {
531
+ this._cachedTrimmedLength = 0;
332
532
  return 0;
533
+ }
534
+ this.ensureSpansMerged();
333
535
  let currentIdx = this.length - 1;
536
+ let trimmedLength = 0;
334
537
  if (this.spans) {
335
538
  for (let s = this.spans.length - 1; s >= 0; s--) {
336
539
  const span = this.spans[s];
337
- const hasStyles = (span.formatFlags & ~FULL_WIDTH_MASK) !== 0 ||
338
- span.fgColor !== undefined ||
339
- span.bgColor !== undefined ||
340
- span.link !== undefined;
540
+ const hasStyles = spanHasStyles(span);
341
541
  if (hasStyles) {
342
- return currentIdx + 1;
542
+ trimmedLength = currentIdx + 1;
543
+ break;
343
544
  }
344
545
  for (let i = 0; i < span.length; i++) {
546
+ if (this.charData) {
547
+ const start = this.charData[currentIdx] & OFFSET_MASK;
548
+ const end = currentIdx + 1 < this.length
549
+ ? this.charData[currentIdx + 1] & OFFSET_MASK
550
+ : this.text.length;
551
+ if (end - start !== 1 || this.text[start] !== ' ') {
552
+ trimmedLength = currentIdx + 1;
553
+ break;
554
+ }
555
+ }
556
+ else if (this.text[currentIdx] !== ' ') {
557
+ trimmedLength = currentIdx + 1;
558
+ break;
559
+ }
560
+ currentIdx--;
561
+ }
562
+ if (trimmedLength > 0)
563
+ break;
564
+ }
565
+ }
566
+ else {
567
+ while (currentIdx >= 0) {
568
+ if (this.charData) {
345
569
  const start = this.charData[currentIdx] & OFFSET_MASK;
346
570
  const end = currentIdx + 1 < this.length
347
571
  ? this.charData[currentIdx + 1] & OFFSET_MASK
348
572
  : this.text.length;
349
573
  if (end - start !== 1 || this.text[start] !== ' ') {
350
- return currentIdx + 1;
574
+ trimmedLength = currentIdx + 1;
575
+ break;
351
576
  }
352
- currentIdx--;
353
577
  }
578
+ else if (this.text[currentIdx] !== ' ') {
579
+ trimmedLength = currentIdx + 1;
580
+ break;
581
+ }
582
+ currentIdx--;
354
583
  }
355
584
  }
356
- return 0;
585
+ this._cachedTrimmedLength = trimmedLength;
586
+ return trimmedLength;
357
587
  }
358
588
  trimEnd() {
359
589
  const trimmedLength = this.getTrimmedLength();
@@ -370,24 +600,44 @@ export class StyledLine {
370
600
  return true;
371
601
  if (this.getText() !== other.getText())
372
602
  return false;
373
- const s1 = this.getSpans();
374
- const s2 = other.getSpans();
375
- if (s1.length !== s2.length)
376
- return false;
377
- for (let i = 0; i < s1.length; i++) {
378
- const sp1 = s1[i];
379
- const sp2 = s2[i];
380
- if (sp1.length !== sp2.length ||
381
- sp1.formatFlags !== sp2.formatFlags ||
382
- sp1.fgColor !== sp2.fgColor ||
383
- sp1.bgColor !== sp2.bgColor ||
384
- sp1.link !== sp2.link) {
603
+ this.ensureSpansMerged();
604
+ other.ensureSpansMerged();
605
+ const thisSpans = this.internalGetSpans();
606
+ const otherSpans = other.internalGetSpans();
607
+ if (thisSpans === undefined && otherSpans !== undefined) {
608
+ if (spansHaveStyles(otherSpans))
609
+ return false;
610
+ }
611
+ else if (thisSpans !== undefined && otherSpans === undefined) {
612
+ if (spansHaveStyles(thisSpans))
613
+ return false;
614
+ }
615
+ else if (thisSpans !== undefined && otherSpans !== undefined) {
616
+ if (thisSpans.length !== otherSpans.length)
385
617
  return false;
618
+ for (let i = 0; i < thisSpans.length; i++) {
619
+ const sp1 = thisSpans[i];
620
+ const sp2 = otherSpans[i];
621
+ if (sp1.length !== sp2.length ||
622
+ sp1.formatFlags !== sp2.formatFlags ||
623
+ sp1.fgColor !== sp2.fgColor ||
624
+ sp1.bgColor !== sp2.bgColor ||
625
+ sp1.link !== sp2.link) {
626
+ return false;
627
+ }
386
628
  }
387
629
  }
388
- const thisCharData = this.charData;
389
- const otherCharData = other.charData;
390
- if (thisCharData && otherCharData) {
630
+ const thisCharData = this.internalGetCharData();
631
+ const otherCharData = other.internalGetCharData();
632
+ if (thisCharData === undefined && otherCharData !== undefined) {
633
+ if (!isDefaultCharData(otherCharData, this.length))
634
+ return false;
635
+ }
636
+ else if (thisCharData !== undefined && otherCharData === undefined) {
637
+ if (!isDefaultCharData(thisCharData, this.length))
638
+ return false;
639
+ }
640
+ else if (thisCharData !== undefined && otherCharData !== undefined) {
391
641
  for (let i = 0; i < this.length; i++) {
392
642
  if (thisCharData[i] !== otherCharData[i])
393
643
  return false;
@@ -399,7 +649,12 @@ export class StyledLine {
399
649
  return this.text ?? '';
400
650
  }
401
651
  getSpans() {
402
- return this.spans ?? [];
652
+ this.ensureSpansMerged();
653
+ if (this.spans !== undefined)
654
+ return this.spans;
655
+ if (this.length > 0)
656
+ return [{ length: this.length, formatFlags: 0 }];
657
+ return [];
403
658
  }
404
659
  getValues() {
405
660
  return Array.from({ length: this.length }, (_, i) => this.getValue(i));
@@ -435,18 +690,36 @@ export class StyledLine {
435
690
  }
436
691
  }
437
692
  }
693
+ internalGetCharData() {
694
+ return this.charData;
695
+ }
696
+ internalGetSpans() {
697
+ return this.spans;
698
+ }
699
+ ensureSpansMerged() {
700
+ if (this._spansDirty) {
701
+ this.mergeSpans();
702
+ this._spansDirty = false;
703
+ }
704
+ }
438
705
  ensureInitialized() {
706
+ if (this.text === undefined) {
707
+ this.text = this.length > 0 ? ' '.repeat(this.length) : '';
708
+ }
709
+ }
710
+ ensureCharData() {
711
+ this.ensureInitialized();
439
712
  if (this.charData === undefined) {
440
- this.text = '';
441
713
  this.charData = Array.from({ length: this.length });
714
+ for (let i = 0; i < this.length; i++) {
715
+ this.charData[i] = i;
716
+ }
717
+ }
718
+ }
719
+ ensureSpans() {
720
+ if (this.spans === undefined) {
442
721
  this.spans =
443
722
  this.length > 0 ? [{ length: this.length, formatFlags: 0 }] : [];
444
- if (this.length > 0 && this.text.length === 0) {
445
- this.text = ' '.repeat(this.length);
446
- for (let i = 0; i < this.length; i++) {
447
- this.charData[i] = i;
448
- }
449
- }
450
723
  }
451
724
  }
452
725
  applyValuesAndSpans(values, spans) {
@@ -454,36 +727,60 @@ export class StyledLine {
454
727
  const visibleChars = values.length;
455
728
  this.length = visibleChars;
456
729
  this.text = values.join('');
457
- this.charData = Array.from({ length: this.length });
458
- let currentOffset = 0;
459
- let spanIdx = 0;
460
- let spanPos = 0;
730
+ let needsCharData = false;
461
731
  for (let i = 0; i < visibleChars; i++) {
462
- const val = values[i];
463
- let isFullWidth = false;
464
- if (spans.length > 0 && spanIdx < spans.length) {
465
- const span = spans[spanIdx];
732
+ if (values[i].length !== 1) {
733
+ needsCharData = true;
734
+ break;
735
+ }
736
+ }
737
+ if (!needsCharData && spans.length > 0) {
738
+ for (const span of spans) {
466
739
  if ((span.formatFlags & FULL_WIDTH_MASK) !== 0) {
467
- isFullWidth = true;
740
+ needsCharData = true;
741
+ break;
468
742
  }
469
- spanPos++;
470
- if (spanPos >= span.length) {
471
- spanIdx++;
472
- spanPos = 0;
743
+ }
744
+ }
745
+ if (needsCharData) {
746
+ this.charData = Array.from({ length: this.length });
747
+ let currentOffset = 0;
748
+ let spanIdx = 0;
749
+ let spanPos = 0;
750
+ for (let i = 0; i < visibleChars; i++) {
751
+ const val = values[i];
752
+ let isFullWidth = false;
753
+ if (spans.length > 0 && spanIdx < spans.length) {
754
+ const span = spans[spanIdx];
755
+ if ((span.formatFlags & FULL_WIDTH_MASK) !== 0) {
756
+ isFullWidth = true;
757
+ }
758
+ spanPos++;
759
+ if (spanPos >= span.length) {
760
+ spanIdx++;
761
+ spanPos = 0;
762
+ }
473
763
  }
764
+ this.charData[i] = currentOffset | (isFullWidth ? FULL_WIDTH_FLAG : 0);
765
+ currentOffset += val.length;
474
766
  }
475
- this.charData[i] = currentOffset | (isFullWidth ? FULL_WIDTH_FLAG : 0);
476
- currentOffset += val.length;
477
767
  }
478
- this.spans = spans.map(s => ({
479
- ...s,
480
- formatFlags: s.formatFlags & ~FULL_WIDTH_MASK,
481
- }));
482
- this.mergeSpans();
768
+ const hasStyles = spansHaveStyles(spans);
769
+ if (hasStyles) {
770
+ this.spans = spans.map(s => ({
771
+ ...s,
772
+ formatFlags: s.formatFlags & ~FULL_WIDTH_MASK,
773
+ }));
774
+ this._spansDirty = true;
775
+ }
776
+ else {
777
+ this.spans = undefined;
778
+ }
483
779
  }
484
780
  splitSpansAt(index) {
485
781
  if (this.spans === undefined || index <= 0 || index >= this.length)
486
782
  return;
783
+ this.ensureSpansMerged();
487
784
  let current = 0;
488
785
  for (let i = 0; i < this.spans.length; i++) {
489
786
  const span = this.spans[i];