@jrichman/ink 6.6.9 → 7.0.0-beta.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.
Files changed (75) 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 +161 -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 +418 -137
  47. package/build/styled-line.js.map +1 -1
  48. package/build/terminal-buffer.d.ts +1 -0
  49. package/build/terminal-buffer.js +173 -48
  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 +14 -4
  61. package/build/worker/render-worker.js +32 -26
  62. package/build/worker/render-worker.js.map +1 -1
  63. package/build/worker/scene-manager.js +23 -8
  64. package/build/worker/scene-manager.js.map +1 -1
  65. package/build/worker/scroll-optimizer.d.ts +1 -1
  66. package/build/worker/scroll-optimizer.js +3 -3
  67. package/build/worker/scroll-optimizer.js.map +1 -1
  68. package/build/worker/terminal-writer.d.ts +0 -1
  69. package/build/worker/terminal-writer.js +25 -11
  70. package/build/worker/terminal-writer.js.map +1 -1
  71. package/package.json +2 -2
  72. package/readme.md +27 -8
  73. package/build/wrap-text.d.ts +0 -6
  74. package/build/wrap-text.js +0 -120
  75. 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,49 +487,72 @@ 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
523
  if (this.length === 0)
330
524
  return 0;
331
- if (this.text === undefined || this.charData === undefined)
525
+ if (this.text === undefined)
332
526
  return 0;
527
+ this.ensureSpansMerged();
333
528
  let currentIdx = this.length - 1;
334
529
  if (this.spans) {
335
530
  for (let s = this.spans.length - 1; s >= 0; s--) {
336
531
  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;
532
+ const hasStyles = spanHasStyles(span);
341
533
  if (hasStyles) {
342
534
  return currentIdx + 1;
343
535
  }
344
536
  for (let i = 0; i < span.length; i++) {
537
+ if (this.charData) {
538
+ const start = this.charData[currentIdx] & OFFSET_MASK;
539
+ const end = currentIdx + 1 < this.length
540
+ ? this.charData[currentIdx + 1] & OFFSET_MASK
541
+ : this.text.length;
542
+ if (end - start !== 1 || this.text[start] !== ' ') {
543
+ return currentIdx + 1;
544
+ }
545
+ }
546
+ else if (this.text[currentIdx] !== ' ') {
547
+ return currentIdx + 1;
548
+ }
549
+ currentIdx--;
550
+ }
551
+ }
552
+ }
553
+ else {
554
+ while (currentIdx >= 0) {
555
+ if (this.charData) {
345
556
  const start = this.charData[currentIdx] & OFFSET_MASK;
346
557
  const end = currentIdx + 1 < this.length
347
558
  ? this.charData[currentIdx + 1] & OFFSET_MASK
@@ -349,8 +560,11 @@ export class StyledLine {
349
560
  if (end - start !== 1 || this.text[start] !== ' ') {
350
561
  return currentIdx + 1;
351
562
  }
352
- currentIdx--;
353
563
  }
564
+ else if (this.text[currentIdx] !== ' ') {
565
+ return currentIdx + 1;
566
+ }
567
+ currentIdx--;
354
568
  }
355
569
  }
356
570
  return 0;
@@ -370,24 +584,44 @@ export class StyledLine {
370
584
  return true;
371
585
  if (this.getText() !== other.getText())
372
586
  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) {
587
+ this.ensureSpansMerged();
588
+ other.ensureSpansMerged();
589
+ const thisSpans = this.internalGetSpans();
590
+ const otherSpans = other.internalGetSpans();
591
+ if (thisSpans === undefined && otherSpans !== undefined) {
592
+ if (spansHaveStyles(otherSpans))
593
+ return false;
594
+ }
595
+ else if (thisSpans !== undefined && otherSpans === undefined) {
596
+ if (spansHaveStyles(thisSpans))
597
+ return false;
598
+ }
599
+ else if (thisSpans !== undefined && otherSpans !== undefined) {
600
+ if (thisSpans.length !== otherSpans.length)
385
601
  return false;
602
+ for (let i = 0; i < thisSpans.length; i++) {
603
+ const sp1 = thisSpans[i];
604
+ const sp2 = otherSpans[i];
605
+ if (sp1.length !== sp2.length ||
606
+ sp1.formatFlags !== sp2.formatFlags ||
607
+ sp1.fgColor !== sp2.fgColor ||
608
+ sp1.bgColor !== sp2.bgColor ||
609
+ sp1.link !== sp2.link) {
610
+ return false;
611
+ }
386
612
  }
387
613
  }
388
- const thisCharData = this.charData;
389
- const otherCharData = other.charData;
390
- if (thisCharData && otherCharData) {
614
+ const thisCharData = this.internalGetCharData();
615
+ const otherCharData = other.internalGetCharData();
616
+ if (thisCharData === undefined && otherCharData !== undefined) {
617
+ if (!isDefaultCharData(otherCharData, this.length))
618
+ return false;
619
+ }
620
+ else if (thisCharData !== undefined && otherCharData === undefined) {
621
+ if (!isDefaultCharData(thisCharData, this.length))
622
+ return false;
623
+ }
624
+ else if (thisCharData !== undefined && otherCharData !== undefined) {
391
625
  for (let i = 0; i < this.length; i++) {
392
626
  if (thisCharData[i] !== otherCharData[i])
393
627
  return false;
@@ -399,7 +633,12 @@ export class StyledLine {
399
633
  return this.text ?? '';
400
634
  }
401
635
  getSpans() {
402
- return this.spans ?? [];
636
+ this.ensureSpansMerged();
637
+ if (this.spans !== undefined)
638
+ return this.spans;
639
+ if (this.length > 0)
640
+ return [{ length: this.length, formatFlags: 0 }];
641
+ return [];
403
642
  }
404
643
  getValues() {
405
644
  return Array.from({ length: this.length }, (_, i) => this.getValue(i));
@@ -435,18 +674,36 @@ export class StyledLine {
435
674
  }
436
675
  }
437
676
  }
677
+ internalGetCharData() {
678
+ return this.charData;
679
+ }
680
+ internalGetSpans() {
681
+ return this.spans;
682
+ }
683
+ ensureSpansMerged() {
684
+ if (this._spansDirty) {
685
+ this.mergeSpans();
686
+ this._spansDirty = false;
687
+ }
688
+ }
438
689
  ensureInitialized() {
690
+ if (this.text === undefined) {
691
+ this.text = this.length > 0 ? ' '.repeat(this.length) : '';
692
+ }
693
+ }
694
+ ensureCharData() {
695
+ this.ensureInitialized();
439
696
  if (this.charData === undefined) {
440
- this.text = '';
441
697
  this.charData = Array.from({ length: this.length });
698
+ for (let i = 0; i < this.length; i++) {
699
+ this.charData[i] = i;
700
+ }
701
+ }
702
+ }
703
+ ensureSpans() {
704
+ if (this.spans === undefined) {
442
705
  this.spans =
443
706
  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
707
  }
451
708
  }
452
709
  applyValuesAndSpans(values, spans) {
@@ -454,36 +711,60 @@ export class StyledLine {
454
711
  const visibleChars = values.length;
455
712
  this.length = visibleChars;
456
713
  this.text = values.join('');
457
- this.charData = Array.from({ length: this.length });
458
- let currentOffset = 0;
459
- let spanIdx = 0;
460
- let spanPos = 0;
714
+ let needsCharData = false;
461
715
  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];
716
+ if (values[i].length !== 1) {
717
+ needsCharData = true;
718
+ break;
719
+ }
720
+ }
721
+ if (!needsCharData && spans.length > 0) {
722
+ for (const span of spans) {
466
723
  if ((span.formatFlags & FULL_WIDTH_MASK) !== 0) {
467
- isFullWidth = true;
724
+ needsCharData = true;
725
+ break;
468
726
  }
469
- spanPos++;
470
- if (spanPos >= span.length) {
471
- spanIdx++;
472
- spanPos = 0;
727
+ }
728
+ }
729
+ if (needsCharData) {
730
+ this.charData = Array.from({ length: this.length });
731
+ let currentOffset = 0;
732
+ let spanIdx = 0;
733
+ let spanPos = 0;
734
+ for (let i = 0; i < visibleChars; i++) {
735
+ const val = values[i];
736
+ let isFullWidth = false;
737
+ if (spans.length > 0 && spanIdx < spans.length) {
738
+ const span = spans[spanIdx];
739
+ if ((span.formatFlags & FULL_WIDTH_MASK) !== 0) {
740
+ isFullWidth = true;
741
+ }
742
+ spanPos++;
743
+ if (spanPos >= span.length) {
744
+ spanIdx++;
745
+ spanPos = 0;
746
+ }
473
747
  }
748
+ this.charData[i] = currentOffset | (isFullWidth ? FULL_WIDTH_FLAG : 0);
749
+ currentOffset += val.length;
474
750
  }
475
- this.charData[i] = currentOffset | (isFullWidth ? FULL_WIDTH_FLAG : 0);
476
- currentOffset += val.length;
477
751
  }
478
- this.spans = spans.map(s => ({
479
- ...s,
480
- formatFlags: s.formatFlags & ~FULL_WIDTH_MASK,
481
- }));
482
- this.mergeSpans();
752
+ const hasStyles = spansHaveStyles(spans);
753
+ if (hasStyles) {
754
+ this.spans = spans.map(s => ({
755
+ ...s,
756
+ formatFlags: s.formatFlags & ~FULL_WIDTH_MASK,
757
+ }));
758
+ this._spansDirty = true;
759
+ }
760
+ else {
761
+ this.spans = undefined;
762
+ }
483
763
  }
484
764
  splitSpansAt(index) {
485
765
  if (this.spans === undefined || index <= 0 || index >= this.length)
486
766
  return;
767
+ this.ensureSpansMerged();
487
768
  let current = 0;
488
769
  for (let i = 0; i < this.spans.length; i++) {
489
770
  const span = this.spans[i];