@fleetagent/pi-tui 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -7
- package/dist/autocomplete.d.ts +17 -23
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +129 -195
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/alt-screen-flash.d.ts +13 -0
- package/dist/components/alt-screen-flash.d.ts.map +1 -0
- package/dist/components/alt-screen-flash.js +37 -0
- package/dist/components/alt-screen-flash.js.map +1 -0
- package/dist/components/editor.d.ts +47 -4
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +615 -585
- package/dist/components/editor.js.map +1 -1
- package/dist/components/h-stack.d.ts +7 -0
- package/dist/components/h-stack.d.ts.map +1 -0
- package/dist/components/h-stack.js +43 -0
- package/dist/components/h-stack.js.map +1 -0
- package/dist/components/image.d.ts +4 -0
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +38 -51
- package/dist/components/image.js.map +1 -1
- package/dist/components/input.d.ts +9 -0
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +121 -147
- package/dist/components/input.js.map +1 -1
- package/dist/components/markdown.d.ts +34 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +491 -429
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/scroll-view.d.ts +53 -0
- package/dist/components/scroll-view.d.ts.map +1 -0
- package/dist/components/scroll-view.js +173 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts +2 -0
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +31 -29
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/stack.d.ts +32 -0
- package/dist/components/stack.d.ts.map +1 -0
- package/dist/components/stack.js +122 -0
- package/dist/components/stack.js.map +1 -0
- package/dist/components/v-stack.d.ts +8 -0
- package/dist/components/v-stack.d.ts.map +1 -0
- package/dist/components/v-stack.js +27 -0
- package/dist/components/v-stack.js.map +1 -0
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +37 -46
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +54 -4
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +39 -4
- package/dist/keybindings.js.map +1 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +359 -479
- package/dist/keys.js.map +1 -1
- package/dist/kill-ring.d.ts +6 -4
- package/dist/kill-ring.d.ts.map +1 -1
- package/dist/kill-ring.js +0 -7
- package/dist/kill-ring.js.map +1 -1
- package/dist/latex.d.ts +10 -0
- package/dist/latex.d.ts.map +1 -0
- package/dist/latex.js +1406 -0
- package/dist/latex.js.map +1 -0
- package/dist/layout-node.d.ts +43 -0
- package/dist/layout-node.d.ts.map +1 -0
- package/dist/layout-node.js +6 -0
- package/dist/layout-node.js.map +1 -0
- package/dist/layout.d.ts +40 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +386 -0
- package/dist/layout.js.map +1 -0
- package/dist/stdin-buffer.d.ts +8 -0
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +116 -159
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal-image.d.ts +41 -19
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +171 -32
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts +3 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +27 -23
- package/dist/terminal.js.map +1 -1
- package/dist/tui-alt-screen.d.ts +114 -0
- package/dist/tui-alt-screen.d.ts.map +1 -0
- package/dist/tui-alt-screen.js +950 -0
- package/dist/tui-alt-screen.js.map +1 -0
- package/dist/tui-main-screen.d.ts +52 -0
- package/dist/tui-main-screen.d.ts.map +1 -0
- package/dist/tui-main-screen.js +511 -0
- package/dist/tui-main-screen.js.map +1 -0
- package/dist/tui.d.ts +112 -44
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +489 -699
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +29 -17
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +605 -528
- package/dist/utils.js.map +1 -1
- package/dist/word-navigation.d.ts.map +1 -1
- package/dist/word-navigation.js +37 -39
- package/dist/word-navigation.js.map +1 -1
- package/package.json +1 -1
package/dist/utils.js
CHANGED
|
@@ -33,6 +33,11 @@ function couldBeEmoji(segment) {
|
|
|
33
33
|
// Regexes for character classification (same as string-width library)
|
|
34
34
|
const zeroWidthRegex = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Mark}|\p{Surrogate})+$/v;
|
|
35
35
|
const leadingNonPrintingRegex = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v;
|
|
36
|
+
const nonPrintingCharRegex = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Format}|\p{Mark}|\p{Surrogate})$/v;
|
|
37
|
+
const markCharRegex = /^\p{Mark}$/v;
|
|
38
|
+
// Marks that terminals allocate cells for when attached to a base character.
|
|
39
|
+
// This includes Unicode spacing marks and non-spacing exceptions in legacy wcwidth tables.
|
|
40
|
+
const terminalSpacingMarkRegex = /^(?:[\p{Spacing_Mark}--[\u1734\u302E\u302F]]|[\u065F\u0F7F\u102B\u102C\u1031\u1033-\u1035\u1038\u103A-\u103E])+$/v;
|
|
36
41
|
const rgiEmojiRegex = /^\p{RGI_Emoji}$/v;
|
|
37
42
|
// Cache for non-ASCII strings
|
|
38
43
|
const WIDTH_CACHE_SIZE = 512;
|
|
@@ -46,86 +51,76 @@ function isPrintableAscii(str) {
|
|
|
46
51
|
}
|
|
47
52
|
return true;
|
|
48
53
|
}
|
|
49
|
-
function
|
|
50
|
-
if (
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
result += segment;
|
|
68
|
-
width += w;
|
|
69
|
-
}
|
|
70
|
-
return { text: result, width };
|
|
54
|
+
function appendPendingAnsi(state) {
|
|
55
|
+
if (!state.pendingAnsi)
|
|
56
|
+
return;
|
|
57
|
+
state.text += state.pendingAnsi;
|
|
58
|
+
state.pendingAnsi = "";
|
|
59
|
+
}
|
|
60
|
+
function appendVisibleFragment(state, fragment, fragmentWidth, maxWidth) {
|
|
61
|
+
if (state.width + fragmentWidth > maxWidth)
|
|
62
|
+
return false;
|
|
63
|
+
appendPendingAnsi(state);
|
|
64
|
+
state.text += fragment;
|
|
65
|
+
state.width += fragmentWidth;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
function appendGraphemeRun(state, text, maxWidth) {
|
|
69
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
70
|
+
if (!appendVisibleFragment(state, segment, graphemeWidth(segment), maxWidth))
|
|
71
|
+
return false;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
let
|
|
76
|
-
while (
|
|
77
|
-
const ansi = extractAnsiCode(text,
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
function truncateStyledFragment(state, text, maxWidth) {
|
|
76
|
+
let index = 0;
|
|
77
|
+
while (index < text.length) {
|
|
78
|
+
const ansi = extractAnsiCode(text, index);
|
|
78
79
|
if (ansi) {
|
|
79
|
-
pendingAnsi += ansi.code;
|
|
80
|
-
|
|
80
|
+
state.pendingAnsi += ansi.code;
|
|
81
|
+
index += ansi.length;
|
|
81
82
|
continue;
|
|
82
83
|
}
|
|
83
|
-
if (text[
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (pendingAnsi) {
|
|
88
|
-
result += pendingAnsi;
|
|
89
|
-
pendingAnsi = "";
|
|
90
|
-
}
|
|
91
|
-
result += "\t";
|
|
92
|
-
width += 3;
|
|
93
|
-
i++;
|
|
84
|
+
if (text[index] === "\t") {
|
|
85
|
+
if (!appendVisibleFragment(state, "\t", 3, maxWidth))
|
|
86
|
+
return;
|
|
87
|
+
index++;
|
|
94
88
|
continue;
|
|
95
89
|
}
|
|
96
|
-
let end =
|
|
97
|
-
while (end < text.length && text[end] !== "\t")
|
|
98
|
-
const nextAnsi = extractAnsiCode(text, end);
|
|
99
|
-
if (nextAnsi) {
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
90
|
+
let end = index;
|
|
91
|
+
while (end < text.length && text[end] !== "\t" && !extractAnsiCode(text, end))
|
|
102
92
|
end++;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
93
|
+
if (!appendGraphemeRun(state, text.slice(index, end), maxWidth))
|
|
94
|
+
return;
|
|
95
|
+
index = end;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function truncateFragmentToWidth(text, maxWidth) {
|
|
99
|
+
if (maxWidth <= 0 || text.length === 0)
|
|
100
|
+
return { text: "", width: 0 };
|
|
101
|
+
if (isPrintableAscii(text)) {
|
|
102
|
+
const clipped = text.slice(0, maxWidth);
|
|
103
|
+
return { text: clipped, width: clipped.length };
|
|
104
|
+
}
|
|
105
|
+
const state = { text: "", width: 0, pendingAnsi: "" };
|
|
106
|
+
if (!text.includes("\x1b") && !text.includes("\t")) {
|
|
107
|
+
appendGraphemeRun(state, text, maxWidth);
|
|
117
108
|
}
|
|
118
|
-
|
|
109
|
+
else {
|
|
110
|
+
truncateStyledFragment(state, text, maxWidth);
|
|
111
|
+
}
|
|
112
|
+
return { text: state.text, width: state.width };
|
|
119
113
|
}
|
|
120
114
|
function finalizeTruncatedResult(prefix, prefixWidth, ellipsis, ellipsisWidth, maxWidth, pad) {
|
|
121
115
|
const reset = "\x1b[0m";
|
|
116
|
+
const hyperlinkClose = getActiveOsc8Close(prefix);
|
|
122
117
|
const visibleWidth = prefixWidth + ellipsisWidth;
|
|
123
118
|
let result;
|
|
124
119
|
if (ellipsis.length > 0) {
|
|
125
|
-
result = `${prefix}${reset}${ellipsis}${reset}`;
|
|
120
|
+
result = `${prefix}${hyperlinkClose}${reset}${ellipsis}${reset}`;
|
|
126
121
|
}
|
|
127
122
|
else {
|
|
128
|
-
result = `${prefix}${reset}`;
|
|
123
|
+
result = `${prefix}${hyperlinkClose}${reset}`;
|
|
129
124
|
}
|
|
130
125
|
return pad ? result + " ".repeat(Math.max(0, maxWidth - visibleWidth)) : result;
|
|
131
126
|
}
|
|
@@ -134,7 +129,45 @@ function finalizeTruncatedResult(prefix, prefixWidth, ellipsis, ellipsisWidth, m
|
|
|
134
129
|
* Based on code from the string-width library, but includes a possible-emoji
|
|
135
130
|
* check to avoid running the RGI_Emoji regex unnecessarily.
|
|
136
131
|
*/
|
|
132
|
+
function trailingCodePointWidth(codePoint, followsMark) {
|
|
133
|
+
if (followsMark || (codePoint >= 0xff00 && codePoint <= 0xffef)) {
|
|
134
|
+
// Indic consonants after marks and halfwidth/fullwidth forms.
|
|
135
|
+
return eastAsianWidth(codePoint);
|
|
136
|
+
}
|
|
137
|
+
if (codePoint === 0x0e33 || codePoint === 0x0eb3) {
|
|
138
|
+
// Thai/Lao AM vowels.
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
function trailingGraphemeWidth(base) {
|
|
144
|
+
let width = 0;
|
|
145
|
+
let followsMark = false;
|
|
146
|
+
for (const char of [...base].slice(1)) {
|
|
147
|
+
if (terminalSpacingMarkRegex.test(char)) {
|
|
148
|
+
width += 1;
|
|
149
|
+
followsMark = false;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (markCharRegex.test(char)) {
|
|
153
|
+
followsMark = true;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (nonPrintingCharRegex.test(char))
|
|
157
|
+
continue;
|
|
158
|
+
width += trailingCodePointWidth(char.codePointAt(0), followsMark);
|
|
159
|
+
followsMark = false;
|
|
160
|
+
}
|
|
161
|
+
return width;
|
|
162
|
+
}
|
|
137
163
|
function graphemeWidth(segment) {
|
|
164
|
+
if (segment === "\t") {
|
|
165
|
+
return 3;
|
|
166
|
+
}
|
|
167
|
+
// Some marks occupy cells even without a base character.
|
|
168
|
+
if (terminalSpacingMarkRegex.test(segment)) {
|
|
169
|
+
return [...segment].length;
|
|
170
|
+
}
|
|
138
171
|
// Zero-width clusters
|
|
139
172
|
if (zeroWidthRegex.test(segment)) {
|
|
140
173
|
return 0;
|
|
@@ -155,20 +188,10 @@ function graphemeWidth(segment) {
|
|
|
155
188
|
if (cp >= 0x1f1e6 && cp <= 0x1f1ff) {
|
|
156
189
|
return 2;
|
|
157
190
|
}
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const c = char.codePointAt(0);
|
|
163
|
-
if (c >= 0xff00 && c <= 0xffef) {
|
|
164
|
-
width += eastAsianWidth(c);
|
|
165
|
-
}
|
|
166
|
-
else if (c === 0x0e33 || c === 0x0eb3) {
|
|
167
|
-
width += 1;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return width;
|
|
191
|
+
// Intl.Segmenter can group multiple terminal-spacing code points into one
|
|
192
|
+
// grapheme. Count trailing visible code points that terminals may allocate
|
|
193
|
+
// cells for.
|
|
194
|
+
return eastAsianWidth(cp) + trailingGraphemeWidth(base);
|
|
172
195
|
}
|
|
173
196
|
/**
|
|
174
197
|
* Calculate the visible width of a string in terminal columns.
|
|
@@ -223,63 +246,146 @@ export function visibleWidth(str) {
|
|
|
223
246
|
widthCache.set(str, width);
|
|
224
247
|
return width;
|
|
225
248
|
}
|
|
249
|
+
/** Remove ANSI, OSC, and APC control sequences while preserving visible text. */
|
|
250
|
+
export function stripTerminalSequences(str) {
|
|
251
|
+
if (!str.includes("\x1b"))
|
|
252
|
+
return str;
|
|
253
|
+
let result = "";
|
|
254
|
+
let i = 0;
|
|
255
|
+
while (i < str.length) {
|
|
256
|
+
const ansi = extractAnsiCode(str, i);
|
|
257
|
+
if (ansi) {
|
|
258
|
+
i += ansi.length;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
result += str[i];
|
|
262
|
+
i++;
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
function scanTerminalTextColumn(text, column, startColumn) {
|
|
267
|
+
let currentColumn = startColumn;
|
|
268
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
269
|
+
const width = segment === "\t" ? 3 : graphemeWidth(segment);
|
|
270
|
+
if (column >= currentColumn && column < currentColumn + width) {
|
|
271
|
+
return { containsColumn: true, nextColumn: currentColumn };
|
|
272
|
+
}
|
|
273
|
+
currentColumn += width;
|
|
274
|
+
}
|
|
275
|
+
return { containsColumn: false, nextColumn: currentColumn };
|
|
276
|
+
}
|
|
277
|
+
/** Return the terminal-cell range occupied by the grapheme at a visible column. */
|
|
278
|
+
export function getGraphemeCellRange(line, column) {
|
|
279
|
+
let currentCol = 0;
|
|
280
|
+
let i = 0;
|
|
281
|
+
while (i < line.length) {
|
|
282
|
+
const ansi = extractAnsiCode(line, i);
|
|
283
|
+
if (ansi) {
|
|
284
|
+
i += ansi.length;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
let textEnd = i;
|
|
288
|
+
while (textEnd < line.length && !extractAnsiCode(line, textEnd))
|
|
289
|
+
textEnd++;
|
|
290
|
+
for (const { segment } of graphemeSegmenter.segment(line.slice(i, textEnd))) {
|
|
291
|
+
const width = graphemeWidth(segment);
|
|
292
|
+
if (width > 0 && column >= currentCol && column < currentCol + width) {
|
|
293
|
+
return { start: currentCol, end: currentCol + width };
|
|
294
|
+
}
|
|
295
|
+
currentCol += width;
|
|
296
|
+
}
|
|
297
|
+
i = textEnd;
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
/** Return the OSC 8 hyperlink covering a visible terminal column. */
|
|
302
|
+
export function getOsc8LinkAtColumn(line, column) {
|
|
303
|
+
let activeUrl;
|
|
304
|
+
let currentCol = 0;
|
|
305
|
+
let i = 0;
|
|
306
|
+
while (i < line.length) {
|
|
307
|
+
const ansi = extractAnsiCode(line, i);
|
|
308
|
+
if (ansi) {
|
|
309
|
+
const hyperlink = /^\x1b\]8;[^;]*;([^\x07\x1b]*)(?:\x07|\x1b\\)$/.exec(ansi.code);
|
|
310
|
+
if (hyperlink)
|
|
311
|
+
activeUrl = hyperlink[1] || undefined;
|
|
312
|
+
i += ansi.length;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
let textEnd = i;
|
|
316
|
+
while (textEnd < line.length && !extractAnsiCode(line, textEnd))
|
|
317
|
+
textEnd++;
|
|
318
|
+
const scan = scanTerminalTextColumn(line.slice(i, textEnd), column, currentCol);
|
|
319
|
+
if (scan.containsColumn)
|
|
320
|
+
return activeUrl;
|
|
321
|
+
currentCol = scan.nextColumn;
|
|
322
|
+
i = textEnd;
|
|
323
|
+
}
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
226
326
|
/**
|
|
227
327
|
* Normalize text for terminal output without changing logical editor content.
|
|
228
328
|
* Some terminals render precomposed Thai/Lao AM vowels inconsistently during
|
|
229
329
|
* differential repaint. Their compatibility decompositions have the same cell
|
|
230
|
-
* width but avoid stale-cell artifacts in terminal renderers.
|
|
330
|
+
* width but avoid stale-cell artifacts in terminal renderers. Visible tabs are
|
|
331
|
+
* expanded to the fixed width used by layout so terminal tab stops cannot wrap
|
|
332
|
+
* a logical line, while tabs inside terminal string sequences stay untouched.
|
|
231
333
|
*/
|
|
232
334
|
const THAI_LAO_AM_REGEX = /[\u0e33\u0eb3]/;
|
|
233
335
|
const THAI_LAO_AM_GLOBAL_REGEX = /[\u0e33\u0eb3]/g;
|
|
234
336
|
export function normalizeTerminalOutput(str) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Extract ANSI escape sequences from a string at the given position.
|
|
241
|
-
*/
|
|
242
|
-
export function extractAnsiCode(str, pos) {
|
|
243
|
-
if (pos >= str.length || str[pos] !== "\x1b")
|
|
244
|
-
return null;
|
|
245
|
-
const next = str[pos + 1];
|
|
246
|
-
// CSI sequence: ESC [ ... m/G/K/H/J
|
|
247
|
-
if (next === "[") {
|
|
248
|
-
let j = pos + 2;
|
|
249
|
-
while (j < str.length && !/[mGKHJ]/.test(str[j]))
|
|
250
|
-
j++;
|
|
251
|
-
if (j < str.length)
|
|
252
|
-
return { code: str.substring(pos, j + 1), length: j + 1 - pos };
|
|
253
|
-
return null;
|
|
337
|
+
let normalized = str;
|
|
338
|
+
if (THAI_LAO_AM_REGEX.test(normalized)) {
|
|
339
|
+
normalized = normalized.replace(THAI_LAO_AM_GLOBAL_REGEX, (char) => char === "\u0e33" ? "\u0e4d\u0e32" : "\u0ecd\u0eb2");
|
|
254
340
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
341
|
+
if (!normalized.includes("\t"))
|
|
342
|
+
return normalized;
|
|
343
|
+
let result = "";
|
|
344
|
+
let i = 0;
|
|
345
|
+
while (i < normalized.length) {
|
|
346
|
+
const ansi = extractAnsiCode(normalized, i);
|
|
347
|
+
if (ansi) {
|
|
348
|
+
result += ansi.code;
|
|
349
|
+
i += ansi.length;
|
|
350
|
+
continue;
|
|
265
351
|
}
|
|
266
|
-
|
|
352
|
+
result += normalized[i] === "\t" ? " " : normalized[i];
|
|
353
|
+
i++;
|
|
267
354
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (str[j] === "\x1b" && str[j + 1] === "\\")
|
|
276
|
-
return { code: str.substring(pos, j + 2), length: j + 2 - pos };
|
|
277
|
-
j++;
|
|
278
|
-
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
function extractCsiSequence(str, pos) {
|
|
358
|
+
let end = pos + 2;
|
|
359
|
+
while (end < str.length && !/[mGKHJ]/.test(str[end]))
|
|
360
|
+
end++;
|
|
361
|
+
if (end >= str.length)
|
|
279
362
|
return null;
|
|
363
|
+
return { code: str.substring(pos, end + 1), length: end + 1 - pos };
|
|
364
|
+
}
|
|
365
|
+
function extractTerminatedControlString(str, pos) {
|
|
366
|
+
let end = pos + 2;
|
|
367
|
+
while (end < str.length) {
|
|
368
|
+
if (str[end] === "\x07")
|
|
369
|
+
return { code: str.substring(pos, end + 1), length: end + 1 - pos };
|
|
370
|
+
if (str[end] === "\x1b" && str[end + 1] === "\\") {
|
|
371
|
+
return { code: str.substring(pos, end + 2), length: end + 2 - pos };
|
|
372
|
+
}
|
|
373
|
+
end++;
|
|
280
374
|
}
|
|
281
375
|
return null;
|
|
282
376
|
}
|
|
377
|
+
/** Extract an ANSI escape sequence from a string at the given position. */
|
|
378
|
+
export function extractAnsiCode(str, pos) {
|
|
379
|
+
if (pos >= str.length || str[pos] !== "\x1b")
|
|
380
|
+
return null;
|
|
381
|
+
const introducer = str[pos + 1];
|
|
382
|
+
if (introducer === "[")
|
|
383
|
+
return extractCsiSequence(str, pos);
|
|
384
|
+
// OSC and APC sequences share BEL and ST termination rules.
|
|
385
|
+
if (introducer === "]" || introducer === "_")
|
|
386
|
+
return extractTerminatedControlString(str, pos);
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
283
389
|
function parseOsc8Hyperlink(ansiCode) {
|
|
284
390
|
if (!ansiCode.startsWith("\x1b]8;")) {
|
|
285
391
|
return undefined;
|
|
@@ -303,6 +409,27 @@ function formatOsc8Hyperlink(hyperlink) {
|
|
|
303
409
|
function formatOsc8Close(terminator) {
|
|
304
410
|
return `\x1b]8;;${terminator}`;
|
|
305
411
|
}
|
|
412
|
+
function getActiveOsc8Close(prefix) {
|
|
413
|
+
if (!prefix.includes("\x1b]8;")) {
|
|
414
|
+
return "";
|
|
415
|
+
}
|
|
416
|
+
let activeHyperlink = null;
|
|
417
|
+
let i = 0;
|
|
418
|
+
while (i < prefix.length) {
|
|
419
|
+
const ansi = extractAnsiCode(prefix, i);
|
|
420
|
+
if (ansi) {
|
|
421
|
+
const hyperlink = parseOsc8Hyperlink(ansi.code);
|
|
422
|
+
if (hyperlink !== undefined) {
|
|
423
|
+
activeHyperlink = hyperlink;
|
|
424
|
+
}
|
|
425
|
+
i += ansi.length;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
i++;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return activeHyperlink ? formatOsc8Close(activeHyperlink.terminator) : "";
|
|
432
|
+
}
|
|
306
433
|
/**
|
|
307
434
|
* Track active ANSI SGR codes to preserve styling across line breaks.
|
|
308
435
|
*/
|
|
@@ -319,135 +446,119 @@ class AnsiCodeTracker {
|
|
|
319
446
|
fgColor = null; // Stores the full code like "31" or "38;5;240"
|
|
320
447
|
bgColor = null; // Stores the full code like "41" or "48;5;240"
|
|
321
448
|
activeHyperlink = null;
|
|
449
|
+
setColor(code, color) {
|
|
450
|
+
if (code === 38)
|
|
451
|
+
this.fgColor = color;
|
|
452
|
+
else
|
|
453
|
+
this.bgColor = color;
|
|
454
|
+
}
|
|
455
|
+
processExtendedColor(parts, index, code) {
|
|
456
|
+
if (code !== 38 && code !== 48)
|
|
457
|
+
return undefined;
|
|
458
|
+
if (parts[index + 1] === "5" && parts[index + 2] !== undefined) {
|
|
459
|
+
this.setColor(code, `${parts[index]};${parts[index + 1]};${parts[index + 2]}`);
|
|
460
|
+
return index + 3;
|
|
461
|
+
}
|
|
462
|
+
if (parts[index + 1] === "2" && parts[index + 4] !== undefined) {
|
|
463
|
+
this.setColor(code, `${parts[index]};${parts[index + 1]};${parts[index + 2]};${parts[index + 3]};${parts[index + 4]}`);
|
|
464
|
+
return index + 5;
|
|
465
|
+
}
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
processStandardSgrCode(code) {
|
|
469
|
+
switch (code) {
|
|
470
|
+
case 0:
|
|
471
|
+
this.reset();
|
|
472
|
+
break;
|
|
473
|
+
case 1:
|
|
474
|
+
this.bold = true;
|
|
475
|
+
break;
|
|
476
|
+
case 2:
|
|
477
|
+
this.dim = true;
|
|
478
|
+
break;
|
|
479
|
+
case 3:
|
|
480
|
+
this.italic = true;
|
|
481
|
+
break;
|
|
482
|
+
case 4:
|
|
483
|
+
this.underline = true;
|
|
484
|
+
break;
|
|
485
|
+
case 5:
|
|
486
|
+
this.blink = true;
|
|
487
|
+
break;
|
|
488
|
+
case 7:
|
|
489
|
+
this.inverse = true;
|
|
490
|
+
break;
|
|
491
|
+
case 8:
|
|
492
|
+
this.hidden = true;
|
|
493
|
+
break;
|
|
494
|
+
case 9:
|
|
495
|
+
this.strikethrough = true;
|
|
496
|
+
break;
|
|
497
|
+
case 21:
|
|
498
|
+
this.bold = false;
|
|
499
|
+
break;
|
|
500
|
+
case 22:
|
|
501
|
+
this.bold = false;
|
|
502
|
+
this.dim = false;
|
|
503
|
+
break;
|
|
504
|
+
case 23:
|
|
505
|
+
this.italic = false;
|
|
506
|
+
break;
|
|
507
|
+
case 24:
|
|
508
|
+
this.underline = false;
|
|
509
|
+
break;
|
|
510
|
+
case 25:
|
|
511
|
+
this.blink = false;
|
|
512
|
+
break;
|
|
513
|
+
case 27:
|
|
514
|
+
this.inverse = false;
|
|
515
|
+
break;
|
|
516
|
+
case 28:
|
|
517
|
+
this.hidden = false;
|
|
518
|
+
break;
|
|
519
|
+
case 29:
|
|
520
|
+
this.strikethrough = false;
|
|
521
|
+
break;
|
|
522
|
+
case 39:
|
|
523
|
+
this.fgColor = null;
|
|
524
|
+
break;
|
|
525
|
+
case 49:
|
|
526
|
+
this.bgColor = null;
|
|
527
|
+
break;
|
|
528
|
+
default:
|
|
529
|
+
if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97))
|
|
530
|
+
this.fgColor = String(code);
|
|
531
|
+
else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107))
|
|
532
|
+
this.bgColor = String(code);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
322
535
|
process(ansiCode) {
|
|
323
|
-
// OSC 8 hyperlink: \x1b]8;;<url>\x1b\\ (open) or \x1b]8;;\x1b\\ (close).
|
|
324
|
-
// Preserve the original terminator because some terminals only make BEL-terminated
|
|
325
|
-
// links clickable. OAuth login URLs use BEL, so reopening wrapped lines with ST
|
|
326
|
-
// made only the first physical line clickable in those terminals.
|
|
327
536
|
const hyperlink = parseOsc8Hyperlink(ansiCode);
|
|
328
537
|
if (hyperlink !== undefined) {
|
|
329
538
|
this.activeHyperlink = hyperlink;
|
|
330
539
|
return;
|
|
331
540
|
}
|
|
332
|
-
if (!ansiCode.endsWith("m"))
|
|
541
|
+
if (!ansiCode.endsWith("m"))
|
|
333
542
|
return;
|
|
334
|
-
}
|
|
335
|
-
// Extract the parameters between \x1b[ and m
|
|
336
543
|
const match = ansiCode.match(/\x1b\[([\d;]*)m/);
|
|
337
544
|
if (!match)
|
|
338
545
|
return;
|
|
339
546
|
const params = match[1];
|
|
340
547
|
if (params === "" || params === "0") {
|
|
341
|
-
// Full reset
|
|
342
548
|
this.reset();
|
|
343
549
|
return;
|
|
344
550
|
}
|
|
345
|
-
// Parse parameters (can be semicolon-separated)
|
|
346
551
|
const parts = params.split(";");
|
|
347
|
-
let
|
|
348
|
-
while (
|
|
349
|
-
const code = Number.parseInt(parts[
|
|
350
|
-
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (parts[i + 1] === "5" && parts[i + 2] !== undefined) {
|
|
355
|
-
// 256 color: 38;5;N or 48;5;N
|
|
356
|
-
const colorCode = `${parts[i]};${parts[i + 1]};${parts[i + 2]}`;
|
|
357
|
-
if (code === 38) {
|
|
358
|
-
this.fgColor = colorCode;
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
this.bgColor = colorCode;
|
|
362
|
-
}
|
|
363
|
-
i += 3;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
else if (parts[i + 1] === "2" && parts[i + 4] !== undefined) {
|
|
367
|
-
// RGB color: 38;2;R;G;B or 48;2;R;G;B
|
|
368
|
-
const colorCode = `${parts[i]};${parts[i + 1]};${parts[i + 2]};${parts[i + 3]};${parts[i + 4]}`;
|
|
369
|
-
if (code === 38) {
|
|
370
|
-
this.fgColor = colorCode;
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
this.bgColor = colorCode;
|
|
374
|
-
}
|
|
375
|
-
i += 5;
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
// Standard SGR codes
|
|
380
|
-
switch (code) {
|
|
381
|
-
case 0:
|
|
382
|
-
this.reset();
|
|
383
|
-
break;
|
|
384
|
-
case 1:
|
|
385
|
-
this.bold = true;
|
|
386
|
-
break;
|
|
387
|
-
case 2:
|
|
388
|
-
this.dim = true;
|
|
389
|
-
break;
|
|
390
|
-
case 3:
|
|
391
|
-
this.italic = true;
|
|
392
|
-
break;
|
|
393
|
-
case 4:
|
|
394
|
-
this.underline = true;
|
|
395
|
-
break;
|
|
396
|
-
case 5:
|
|
397
|
-
this.blink = true;
|
|
398
|
-
break;
|
|
399
|
-
case 7:
|
|
400
|
-
this.inverse = true;
|
|
401
|
-
break;
|
|
402
|
-
case 8:
|
|
403
|
-
this.hidden = true;
|
|
404
|
-
break;
|
|
405
|
-
case 9:
|
|
406
|
-
this.strikethrough = true;
|
|
407
|
-
break;
|
|
408
|
-
case 21:
|
|
409
|
-
this.bold = false;
|
|
410
|
-
break; // Some terminals
|
|
411
|
-
case 22:
|
|
412
|
-
this.bold = false;
|
|
413
|
-
this.dim = false;
|
|
414
|
-
break;
|
|
415
|
-
case 23:
|
|
416
|
-
this.italic = false;
|
|
417
|
-
break;
|
|
418
|
-
case 24:
|
|
419
|
-
this.underline = false;
|
|
420
|
-
break;
|
|
421
|
-
case 25:
|
|
422
|
-
this.blink = false;
|
|
423
|
-
break;
|
|
424
|
-
case 27:
|
|
425
|
-
this.inverse = false;
|
|
426
|
-
break;
|
|
427
|
-
case 28:
|
|
428
|
-
this.hidden = false;
|
|
429
|
-
break;
|
|
430
|
-
case 29:
|
|
431
|
-
this.strikethrough = false;
|
|
432
|
-
break;
|
|
433
|
-
case 39:
|
|
434
|
-
this.fgColor = null;
|
|
435
|
-
break; // Default fg
|
|
436
|
-
case 49:
|
|
437
|
-
this.bgColor = null;
|
|
438
|
-
break; // Default bg
|
|
439
|
-
default:
|
|
440
|
-
// Standard foreground colors 30-37, 90-97
|
|
441
|
-
if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
|
|
442
|
-
this.fgColor = String(code);
|
|
443
|
-
}
|
|
444
|
-
// Standard background colors 40-47, 100-107
|
|
445
|
-
else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
|
|
446
|
-
this.bgColor = String(code);
|
|
447
|
-
}
|
|
448
|
-
break;
|
|
552
|
+
let index = 0;
|
|
553
|
+
while (index < parts.length) {
|
|
554
|
+
const code = Number.parseInt(parts[index], 10);
|
|
555
|
+
const nextIndex = this.processExtendedColor(parts, index, code);
|
|
556
|
+
if (nextIndex !== undefined) {
|
|
557
|
+
index = nextIndex;
|
|
558
|
+
continue;
|
|
449
559
|
}
|
|
450
|
-
|
|
560
|
+
this.processStandardSgrCode(code);
|
|
561
|
+
index++;
|
|
451
562
|
}
|
|
452
563
|
}
|
|
453
564
|
reset() {
|
|
@@ -598,7 +709,7 @@ export function wrapTextWithAnsi(text, width) {
|
|
|
598
709
|
}
|
|
599
710
|
// Handle newlines by processing each line separately
|
|
600
711
|
// Track ANSI state across lines so styles carry over after literal newlines
|
|
601
|
-
const inputLines = text.split(
|
|
712
|
+
const inputLines = text.split(/\r\n|\r|\n/);
|
|
602
713
|
const result = [];
|
|
603
714
|
const tracker = new AnsiCodeTracker();
|
|
604
715
|
for (const inputLine of inputLines) {
|
|
@@ -613,76 +724,61 @@ export function wrapTextWithAnsi(text, width) {
|
|
|
613
724
|
}
|
|
614
725
|
return result.length > 0 ? result : [""];
|
|
615
726
|
}
|
|
727
|
+
function appendLineEndReset(text, tracker) {
|
|
728
|
+
return text + tracker.getLineEndReset();
|
|
729
|
+
}
|
|
730
|
+
function appendOversizedWrapToken(token, context) {
|
|
731
|
+
if (context.current.text) {
|
|
732
|
+
context.lines.push(appendLineEndReset(context.current.text, context.tracker));
|
|
733
|
+
context.current.text = "";
|
|
734
|
+
context.current.visibleWidth = 0;
|
|
735
|
+
}
|
|
736
|
+
const broken = breakLongWord(token, context.maxWidth, context.tracker);
|
|
737
|
+
for (let index = 0; index < broken.length - 1; index++)
|
|
738
|
+
context.lines.push(broken[index]);
|
|
739
|
+
context.current.text = broken[broken.length - 1];
|
|
740
|
+
context.current.visibleWidth = visibleWidth(context.current.text);
|
|
741
|
+
}
|
|
742
|
+
function appendFittingWrapToken(token, tokenVisibleWidth, isWhitespace, context) {
|
|
743
|
+
const exceedsLine = context.current.visibleWidth + tokenVisibleWidth > context.maxWidth;
|
|
744
|
+
if (!exceedsLine || context.current.visibleWidth === 0) {
|
|
745
|
+
context.current.text += token;
|
|
746
|
+
context.current.visibleWidth += tokenVisibleWidth;
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
context.lines.push(appendLineEndReset(context.current.text.trimEnd(), context.tracker));
|
|
750
|
+
if (isWhitespace) {
|
|
751
|
+
context.current.text = context.tracker.getActiveCodes();
|
|
752
|
+
context.current.visibleWidth = 0;
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
context.current.text = context.tracker.getActiveCodes() + token;
|
|
756
|
+
context.current.visibleWidth = tokenVisibleWidth;
|
|
757
|
+
}
|
|
616
758
|
function wrapSingleLine(line, width) {
|
|
617
|
-
if (!line)
|
|
759
|
+
if (!line)
|
|
618
760
|
return [""];
|
|
619
|
-
|
|
620
|
-
const visibleLength = visibleWidth(line);
|
|
621
|
-
if (visibleLength <= width) {
|
|
761
|
+
if (visibleWidth(line) <= width)
|
|
622
762
|
return [line];
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
for (const token of
|
|
630
|
-
const
|
|
763
|
+
const context = {
|
|
764
|
+
maxWidth: width,
|
|
765
|
+
lines: [],
|
|
766
|
+
tracker: new AnsiCodeTracker(),
|
|
767
|
+
current: { text: "", visibleWidth: 0 },
|
|
768
|
+
};
|
|
769
|
+
for (const token of splitIntoTokensWithAnsi(line)) {
|
|
770
|
+
const tokenVisibleWidth = visibleWidth(token);
|
|
631
771
|
const isWhitespace = token.trim() === "";
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (currentLine) {
|
|
635
|
-
// Add specific reset for underline only (preserves background)
|
|
636
|
-
const lineEndReset = tracker.getLineEndReset();
|
|
637
|
-
if (lineEndReset) {
|
|
638
|
-
currentLine += lineEndReset;
|
|
639
|
-
}
|
|
640
|
-
wrapped.push(currentLine);
|
|
641
|
-
currentLine = "";
|
|
642
|
-
currentVisibleLength = 0;
|
|
643
|
-
}
|
|
644
|
-
// Break long token - breakLongWord handles its own resets
|
|
645
|
-
const broken = breakLongWord(token, width, tracker);
|
|
646
|
-
for (let i = 0; i < broken.length - 1; i++) {
|
|
647
|
-
wrapped.push(broken[i]);
|
|
648
|
-
}
|
|
649
|
-
currentLine = broken[broken.length - 1];
|
|
650
|
-
currentVisibleLength = visibleWidth(currentLine);
|
|
772
|
+
if (tokenVisibleWidth > width && !isWhitespace) {
|
|
773
|
+
appendOversizedWrapToken(token, context);
|
|
651
774
|
continue;
|
|
652
775
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (totalNeeded > width && currentVisibleLength > 0) {
|
|
656
|
-
// Trim trailing whitespace, then add underline reset (not full reset, to preserve background)
|
|
657
|
-
let lineToWrap = currentLine.trimEnd();
|
|
658
|
-
const lineEndReset = tracker.getLineEndReset();
|
|
659
|
-
if (lineEndReset) {
|
|
660
|
-
lineToWrap += lineEndReset;
|
|
661
|
-
}
|
|
662
|
-
wrapped.push(lineToWrap);
|
|
663
|
-
if (isWhitespace) {
|
|
664
|
-
// Don't start new line with whitespace
|
|
665
|
-
currentLine = tracker.getActiveCodes();
|
|
666
|
-
currentVisibleLength = 0;
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
currentLine = tracker.getActiveCodes() + token;
|
|
670
|
-
currentVisibleLength = tokenVisibleLength;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
else {
|
|
674
|
-
// Add to current line
|
|
675
|
-
currentLine += token;
|
|
676
|
-
currentVisibleLength += tokenVisibleLength;
|
|
677
|
-
}
|
|
678
|
-
updateTrackerFromText(token, tracker);
|
|
679
|
-
}
|
|
680
|
-
if (currentLine) {
|
|
681
|
-
// No reset at end of final line - let caller handle it
|
|
682
|
-
wrapped.push(currentLine);
|
|
776
|
+
appendFittingWrapToken(token, tokenVisibleWidth, isWhitespace, context);
|
|
777
|
+
updateTrackerFromText(token, context.tracker);
|
|
683
778
|
}
|
|
684
|
-
|
|
685
|
-
|
|
779
|
+
if (context.current.text)
|
|
780
|
+
context.lines.push(context.current.text);
|
|
781
|
+
return context.lines.length > 0 ? context.lines.map((wrappedLine) => wrappedLine.trimEnd()) : [""];
|
|
686
782
|
}
|
|
687
783
|
export const PUNCTUATION_REGEX = /[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;
|
|
688
784
|
/**
|
|
@@ -697,39 +793,33 @@ export function isWhitespaceChar(char) {
|
|
|
697
793
|
export function isPunctuationChar(char) {
|
|
698
794
|
return PUNCTUATION_REGEX.test(char);
|
|
699
795
|
}
|
|
700
|
-
function
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
// First, separate ANSI codes from visible content
|
|
705
|
-
// We need to handle ANSI codes specially since they're not graphemes
|
|
706
|
-
let i = 0;
|
|
707
|
-
const segments = [];
|
|
708
|
-
while (i < word.length) {
|
|
709
|
-
const ansiResult = extractAnsiCode(word, i);
|
|
796
|
+
function* iterateAnsiTextSegments(text) {
|
|
797
|
+
let index = 0;
|
|
798
|
+
while (index < text.length) {
|
|
799
|
+
const ansiResult = extractAnsiCode(text, index);
|
|
710
800
|
if (ansiResult) {
|
|
711
|
-
|
|
712
|
-
|
|
801
|
+
yield { type: "ansi", value: ansiResult.code };
|
|
802
|
+
index += ansiResult.length;
|
|
803
|
+
continue;
|
|
713
804
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
// Segment this non-ANSI portion into graphemes
|
|
724
|
-
const textPortion = word.slice(i, end);
|
|
725
|
-
for (const seg of graphemeSegmenter.segment(textPortion)) {
|
|
726
|
-
segments.push({ type: "grapheme", value: seg.segment });
|
|
727
|
-
}
|
|
728
|
-
i = end;
|
|
805
|
+
let end = index;
|
|
806
|
+
while (end < text.length) {
|
|
807
|
+
if (extractAnsiCode(text, end))
|
|
808
|
+
break;
|
|
809
|
+
end++;
|
|
810
|
+
}
|
|
811
|
+
for (const segment of graphemeSegmenter.segment(text.slice(index, end))) {
|
|
812
|
+
yield { type: "grapheme", value: segment.segment };
|
|
729
813
|
}
|
|
814
|
+
index = end;
|
|
730
815
|
}
|
|
816
|
+
}
|
|
817
|
+
function breakLongWord(word, width, tracker) {
|
|
818
|
+
const lines = [];
|
|
819
|
+
let currentLine = tracker.getActiveCodes();
|
|
820
|
+
let currentWidth = 0;
|
|
731
821
|
// Now process segments
|
|
732
|
-
for (const seg of
|
|
822
|
+
for (const seg of iterateAnsiTextSegments(word)) {
|
|
733
823
|
if (seg.type === "ansi") {
|
|
734
824
|
currentLine += seg.value;
|
|
735
825
|
tracker.process(seg.value);
|
|
@@ -776,6 +866,100 @@ export function applyBackgroundToLine(line, width, bgFn) {
|
|
|
776
866
|
const withPadding = line + padding;
|
|
777
867
|
return bgFn(withPadding);
|
|
778
868
|
}
|
|
869
|
+
function createWidthTruncationScan() {
|
|
870
|
+
return {
|
|
871
|
+
prefix: "",
|
|
872
|
+
prefixWidth: 0,
|
|
873
|
+
pendingAnsi: "",
|
|
874
|
+
visibleWidth: 0,
|
|
875
|
+
keepingPrefix: true,
|
|
876
|
+
overflowed: false,
|
|
877
|
+
exhaustedInput: false,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function appendWidthTruncationFragment(state, fragment, fragmentWidth, targetWidth, maxWidth) {
|
|
881
|
+
if (state.keepingPrefix && state.prefixWidth + fragmentWidth <= targetWidth) {
|
|
882
|
+
state.prefix += state.pendingAnsi + fragment;
|
|
883
|
+
state.pendingAnsi = "";
|
|
884
|
+
state.prefixWidth += fragmentWidth;
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
state.keepingPrefix = false;
|
|
888
|
+
state.pendingAnsi = "";
|
|
889
|
+
}
|
|
890
|
+
state.visibleWidth += fragmentWidth;
|
|
891
|
+
if (state.visibleWidth <= maxWidth)
|
|
892
|
+
return true;
|
|
893
|
+
state.overflowed = true;
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
function scanTruncationGraphemes(text, state, targetWidth, maxWidth) {
|
|
897
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
898
|
+
if (!appendWidthTruncationFragment(state, segment, graphemeWidth(segment), targetWidth, maxWidth))
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
function scanPlainTextForTruncation(text, targetWidth, maxWidth) {
|
|
904
|
+
const state = createWidthTruncationScan();
|
|
905
|
+
state.exhaustedInput = scanTruncationGraphemes(text, state, targetWidth, maxWidth);
|
|
906
|
+
return state;
|
|
907
|
+
}
|
|
908
|
+
function scanStyledTextForTruncation(text, targetWidth, maxWidth) {
|
|
909
|
+
const state = createWidthTruncationScan();
|
|
910
|
+
let index = 0;
|
|
911
|
+
while (index < text.length) {
|
|
912
|
+
const ansi = extractAnsiCode(text, index);
|
|
913
|
+
if (ansi) {
|
|
914
|
+
state.pendingAnsi += ansi.code;
|
|
915
|
+
index += ansi.length;
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
if (text[index] === "\t") {
|
|
919
|
+
if (!appendWidthTruncationFragment(state, "\t", 3, targetWidth, maxWidth))
|
|
920
|
+
break;
|
|
921
|
+
index++;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
let end = index;
|
|
925
|
+
while (end < text.length && text[end] !== "\t" && !extractAnsiCode(text, end))
|
|
926
|
+
end++;
|
|
927
|
+
if (!scanTruncationGraphemes(text.slice(index, end), state, targetWidth, maxWidth))
|
|
928
|
+
break;
|
|
929
|
+
index = end;
|
|
930
|
+
}
|
|
931
|
+
state.exhaustedInput = index >= text.length;
|
|
932
|
+
return state;
|
|
933
|
+
}
|
|
934
|
+
function truncateWithOversizedEllipsis(text, maxWidth, ellipsis, ellipsisWidth, pad) {
|
|
935
|
+
if (ellipsisWidth < maxWidth)
|
|
936
|
+
return undefined;
|
|
937
|
+
const textWidth = visibleWidth(text);
|
|
938
|
+
if (textWidth <= maxWidth)
|
|
939
|
+
return pad ? text + " ".repeat(maxWidth - textWidth) : text;
|
|
940
|
+
const clippedEllipsis = truncateFragmentToWidth(ellipsis, maxWidth);
|
|
941
|
+
if (clippedEllipsis.width === 0)
|
|
942
|
+
return pad ? " ".repeat(maxWidth) : "";
|
|
943
|
+
return finalizeTruncatedResult("", 0, clippedEllipsis.text, clippedEllipsis.width, maxWidth, pad);
|
|
944
|
+
}
|
|
945
|
+
function truncatePrintableAscii(text, maxWidth, ellipsis, ellipsisWidth, pad) {
|
|
946
|
+
if (!isPrintableAscii(text))
|
|
947
|
+
return undefined;
|
|
948
|
+
if (text.length <= maxWidth)
|
|
949
|
+
return pad ? text + " ".repeat(maxWidth - text.length) : text;
|
|
950
|
+
const targetWidth = maxWidth - ellipsisWidth;
|
|
951
|
+
return finalizeTruncatedResult(text.slice(0, targetWidth), targetWidth, ellipsis, ellipsisWidth, maxWidth, pad);
|
|
952
|
+
}
|
|
953
|
+
function truncateMeasuredText(text, maxWidth, ellipsis, ellipsisWidth, pad) {
|
|
954
|
+
const targetWidth = maxWidth - ellipsisWidth;
|
|
955
|
+
const scan = text.includes("\x1b") || text.includes("\t")
|
|
956
|
+
? scanStyledTextForTruncation(text, targetWidth, maxWidth)
|
|
957
|
+
: scanPlainTextForTruncation(text, targetWidth, maxWidth);
|
|
958
|
+
if (!scan.overflowed && scan.exhaustedInput) {
|
|
959
|
+
return pad ? text + " ".repeat(Math.max(0, maxWidth - scan.visibleWidth)) : text;
|
|
960
|
+
}
|
|
961
|
+
return finalizeTruncatedResult(scan.prefix, scan.prefixWidth, ellipsis, ellipsisWidth, maxWidth, pad);
|
|
962
|
+
}
|
|
779
963
|
/**
|
|
780
964
|
* Truncate text to fit within a maximum visible width, adding ellipsis if needed.
|
|
781
965
|
* Optionally pad with spaces to reach exactly maxWidth.
|
|
@@ -788,128 +972,18 @@ export function applyBackgroundToLine(line, width, bgFn) {
|
|
|
788
972
|
* @returns Truncated text, optionally padded to exactly maxWidth
|
|
789
973
|
*/
|
|
790
974
|
export function truncateToWidth(text, maxWidth, ellipsis = "...", pad = false) {
|
|
791
|
-
if (maxWidth <= 0)
|
|
975
|
+
if (maxWidth <= 0)
|
|
792
976
|
return "";
|
|
793
|
-
|
|
794
|
-
if (text.length === 0) {
|
|
977
|
+
if (text.length === 0)
|
|
795
978
|
return pad ? " ".repeat(maxWidth) : "";
|
|
796
|
-
}
|
|
797
979
|
const ellipsisWidth = visibleWidth(ellipsis);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return pad ? " ".repeat(maxWidth) : "";
|
|
806
|
-
}
|
|
807
|
-
return finalizeTruncatedResult("", 0, clippedEllipsis.text, clippedEllipsis.width, maxWidth, pad);
|
|
808
|
-
}
|
|
809
|
-
if (isPrintableAscii(text)) {
|
|
810
|
-
if (text.length <= maxWidth) {
|
|
811
|
-
return pad ? text + " ".repeat(maxWidth - text.length) : text;
|
|
812
|
-
}
|
|
813
|
-
const targetWidth = maxWidth - ellipsisWidth;
|
|
814
|
-
return finalizeTruncatedResult(text.slice(0, targetWidth), targetWidth, ellipsis, ellipsisWidth, maxWidth, pad);
|
|
815
|
-
}
|
|
816
|
-
const targetWidth = maxWidth - ellipsisWidth;
|
|
817
|
-
let result = "";
|
|
818
|
-
let pendingAnsi = "";
|
|
819
|
-
let visibleSoFar = 0;
|
|
820
|
-
let keptWidth = 0;
|
|
821
|
-
let keepContiguousPrefix = true;
|
|
822
|
-
let overflowed = false;
|
|
823
|
-
let exhaustedInput = false;
|
|
824
|
-
const hasAnsi = text.includes("\x1b");
|
|
825
|
-
const hasTabs = text.includes("\t");
|
|
826
|
-
if (!hasAnsi && !hasTabs) {
|
|
827
|
-
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
828
|
-
const width = graphemeWidth(segment);
|
|
829
|
-
if (keepContiguousPrefix && keptWidth + width <= targetWidth) {
|
|
830
|
-
result += segment;
|
|
831
|
-
keptWidth += width;
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
keepContiguousPrefix = false;
|
|
835
|
-
}
|
|
836
|
-
visibleSoFar += width;
|
|
837
|
-
if (visibleSoFar > maxWidth) {
|
|
838
|
-
overflowed = true;
|
|
839
|
-
break;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
exhaustedInput = !overflowed;
|
|
843
|
-
}
|
|
844
|
-
else {
|
|
845
|
-
let i = 0;
|
|
846
|
-
while (i < text.length) {
|
|
847
|
-
const ansi = extractAnsiCode(text, i);
|
|
848
|
-
if (ansi) {
|
|
849
|
-
pendingAnsi += ansi.code;
|
|
850
|
-
i += ansi.length;
|
|
851
|
-
continue;
|
|
852
|
-
}
|
|
853
|
-
if (text[i] === "\t") {
|
|
854
|
-
if (keepContiguousPrefix && keptWidth + 3 <= targetWidth) {
|
|
855
|
-
if (pendingAnsi) {
|
|
856
|
-
result += pendingAnsi;
|
|
857
|
-
pendingAnsi = "";
|
|
858
|
-
}
|
|
859
|
-
result += "\t";
|
|
860
|
-
keptWidth += 3;
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
keepContiguousPrefix = false;
|
|
864
|
-
pendingAnsi = "";
|
|
865
|
-
}
|
|
866
|
-
visibleSoFar += 3;
|
|
867
|
-
if (visibleSoFar > maxWidth) {
|
|
868
|
-
overflowed = true;
|
|
869
|
-
break;
|
|
870
|
-
}
|
|
871
|
-
i++;
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
874
|
-
let end = i;
|
|
875
|
-
while (end < text.length && text[end] !== "\t") {
|
|
876
|
-
const nextAnsi = extractAnsiCode(text, end);
|
|
877
|
-
if (nextAnsi) {
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
end++;
|
|
881
|
-
}
|
|
882
|
-
for (const { segment } of graphemeSegmenter.segment(text.slice(i, end))) {
|
|
883
|
-
const width = graphemeWidth(segment);
|
|
884
|
-
if (keepContiguousPrefix && keptWidth + width <= targetWidth) {
|
|
885
|
-
if (pendingAnsi) {
|
|
886
|
-
result += pendingAnsi;
|
|
887
|
-
pendingAnsi = "";
|
|
888
|
-
}
|
|
889
|
-
result += segment;
|
|
890
|
-
keptWidth += width;
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
keepContiguousPrefix = false;
|
|
894
|
-
pendingAnsi = "";
|
|
895
|
-
}
|
|
896
|
-
visibleSoFar += width;
|
|
897
|
-
if (visibleSoFar > maxWidth) {
|
|
898
|
-
overflowed = true;
|
|
899
|
-
break;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
if (overflowed) {
|
|
903
|
-
break;
|
|
904
|
-
}
|
|
905
|
-
i = end;
|
|
906
|
-
}
|
|
907
|
-
exhaustedInput = i >= text.length;
|
|
908
|
-
}
|
|
909
|
-
if (!overflowed && exhaustedInput) {
|
|
910
|
-
return pad ? text + " ".repeat(Math.max(0, maxWidth - visibleSoFar)) : text;
|
|
911
|
-
}
|
|
912
|
-
return finalizeTruncatedResult(result, keptWidth, ellipsis, ellipsisWidth, maxWidth, pad);
|
|
980
|
+
const oversizedEllipsis = truncateWithOversizedEllipsis(text, maxWidth, ellipsis, ellipsisWidth, pad);
|
|
981
|
+
if (oversizedEllipsis !== undefined)
|
|
982
|
+
return oversizedEllipsis;
|
|
983
|
+
const printableAscii = truncatePrintableAscii(text, maxWidth, ellipsis, ellipsisWidth, pad);
|
|
984
|
+
if (printableAscii !== undefined)
|
|
985
|
+
return printableAscii;
|
|
986
|
+
return truncateMeasuredText(text, maxWidth, ellipsis, ellipsisWidth, pad);
|
|
913
987
|
}
|
|
914
988
|
/**
|
|
915
989
|
* Extract a range of visible columns from a line. Handles ANSI codes and wide chars.
|
|
@@ -918,112 +992,115 @@ export function truncateToWidth(text, maxWidth, ellipsis = "...", pad = false) {
|
|
|
918
992
|
export function sliceByColumn(line, startCol, length, strict = false) {
|
|
919
993
|
return sliceWithWidth(line, startCol, length, strict).text;
|
|
920
994
|
}
|
|
995
|
+
function appendAnsiToColumnSlice(state, ansi, startColumn, endColumn) {
|
|
996
|
+
if (state.currentColumn >= startColumn && state.currentColumn < endColumn)
|
|
997
|
+
state.text += ansi;
|
|
998
|
+
else if (state.currentColumn < startColumn)
|
|
999
|
+
state.pendingAnsi += ansi;
|
|
1000
|
+
}
|
|
1001
|
+
function appendGraphemeToColumnSlice(state, grapheme, graphemeWidth, startColumn, endColumn, strict) {
|
|
1002
|
+
if (state.currentColumn < startColumn || state.currentColumn >= endColumn)
|
|
1003
|
+
return;
|
|
1004
|
+
if (strict && state.currentColumn + graphemeWidth > endColumn)
|
|
1005
|
+
return;
|
|
1006
|
+
if (state.pendingAnsi) {
|
|
1007
|
+
state.text += state.pendingAnsi;
|
|
1008
|
+
state.pendingAnsi = "";
|
|
1009
|
+
}
|
|
1010
|
+
state.text += grapheme;
|
|
1011
|
+
state.width += graphemeWidth;
|
|
1012
|
+
}
|
|
921
1013
|
/** Like sliceByColumn but also returns the actual visible width of the result. */
|
|
922
1014
|
export function sliceWithWidth(line, startCol, length, strict = false) {
|
|
923
1015
|
if (length <= 0)
|
|
924
1016
|
return { text: "", width: 0 };
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (currentCol >= startCol && currentCol < endCol)
|
|
931
|
-
result += ansi.code;
|
|
932
|
-
else if (currentCol < startCol)
|
|
933
|
-
pendingAnsi += ansi.code;
|
|
934
|
-
i += ansi.length;
|
|
1017
|
+
const endColumn = startCol + length;
|
|
1018
|
+
const state = { text: "", width: 0, currentColumn: 0, pendingAnsi: "" };
|
|
1019
|
+
for (const segment of iterateAnsiTextSegments(line)) {
|
|
1020
|
+
if (segment.type === "ansi") {
|
|
1021
|
+
appendAnsiToColumnSlice(state, segment.value, startCol, endColumn);
|
|
935
1022
|
continue;
|
|
936
1023
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const w = graphemeWidth(segment);
|
|
942
|
-
const inRange = currentCol >= startCol && currentCol < endCol;
|
|
943
|
-
const fits = !strict || currentCol + w <= endCol;
|
|
944
|
-
if (inRange && fits) {
|
|
945
|
-
if (pendingAnsi) {
|
|
946
|
-
result += pendingAnsi;
|
|
947
|
-
pendingAnsi = "";
|
|
948
|
-
}
|
|
949
|
-
result += segment;
|
|
950
|
-
resultWidth += w;
|
|
951
|
-
}
|
|
952
|
-
currentCol += w;
|
|
953
|
-
if (currentCol >= endCol)
|
|
954
|
-
break;
|
|
955
|
-
}
|
|
956
|
-
i = textEnd;
|
|
957
|
-
if (currentCol >= endCol)
|
|
1024
|
+
const width = graphemeWidth(segment.value);
|
|
1025
|
+
appendGraphemeToColumnSlice(state, segment.value, width, startCol, endColumn, strict);
|
|
1026
|
+
state.currentColumn += width;
|
|
1027
|
+
if (state.currentColumn >= endColumn)
|
|
958
1028
|
break;
|
|
959
1029
|
}
|
|
960
|
-
return { text:
|
|
1030
|
+
return { text: state.text, width: state.width };
|
|
961
1031
|
}
|
|
962
1032
|
// Pooled tracker instance for extractSegments (avoids allocation per call)
|
|
963
1033
|
const pooledStyleTracker = new AnsiCodeTracker();
|
|
1034
|
+
function appendAnsiToExtractedSegments(state, ansiCode, context) {
|
|
1035
|
+
pooledStyleTracker.process(ansiCode);
|
|
1036
|
+
if (state.currentColumn < context.beforeEnd) {
|
|
1037
|
+
state.pendingAnsiBefore += ansiCode;
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (state.currentColumn >= context.afterStart && state.currentColumn < context.afterEnd && state.afterStarted) {
|
|
1041
|
+
state.after += ansiCode;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function appendGraphemeToExtractedSegments(state, grapheme, graphemeWidth, context) {
|
|
1045
|
+
if (state.currentColumn < context.beforeEnd && state.currentColumn + graphemeWidth <= context.beforeEnd) {
|
|
1046
|
+
state.before += state.pendingAnsiBefore + grapheme;
|
|
1047
|
+
state.pendingAnsiBefore = "";
|
|
1048
|
+
state.beforeWidth += graphemeWidth;
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (state.currentColumn < context.afterStart || state.currentColumn >= context.afterEnd)
|
|
1052
|
+
return;
|
|
1053
|
+
if (context.strictAfter && state.currentColumn + graphemeWidth > context.afterEnd)
|
|
1054
|
+
return;
|
|
1055
|
+
if (!state.afterStarted) {
|
|
1056
|
+
state.after += pooledStyleTracker.getActiveCodes();
|
|
1057
|
+
state.afterStarted = true;
|
|
1058
|
+
}
|
|
1059
|
+
state.after += grapheme;
|
|
1060
|
+
state.afterWidth += graphemeWidth;
|
|
1061
|
+
}
|
|
1062
|
+
function isLineSegmentExtractionComplete(state, context) {
|
|
1063
|
+
return context.afterLength <= 0 ? state.currentColumn >= context.beforeEnd : state.currentColumn >= context.afterEnd;
|
|
1064
|
+
}
|
|
964
1065
|
/**
|
|
965
1066
|
* Extract "before" and "after" segments from a line in a single pass.
|
|
966
1067
|
* Used for overlay compositing where we need content before and after the overlay region.
|
|
967
1068
|
* Preserves styling from before the overlay that should affect content after it.
|
|
968
1069
|
*/
|
|
969
1070
|
export function extractSegments(line, beforeEnd, afterStart, afterLen, strictAfter = false) {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1071
|
+
const context = {
|
|
1072
|
+
beforeEnd,
|
|
1073
|
+
afterStart,
|
|
1074
|
+
afterEnd: afterStart + afterLen,
|
|
1075
|
+
afterLength: afterLen,
|
|
1076
|
+
strictAfter,
|
|
1077
|
+
};
|
|
1078
|
+
const state = {
|
|
1079
|
+
before: "",
|
|
1080
|
+
beforeWidth: 0,
|
|
1081
|
+
after: "",
|
|
1082
|
+
afterWidth: 0,
|
|
1083
|
+
currentColumn: 0,
|
|
1084
|
+
pendingAnsiBefore: "",
|
|
1085
|
+
afterStarted: false,
|
|
1086
|
+
};
|
|
976
1087
|
pooledStyleTracker.clear();
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
// Track all SGR codes to know styling state at afterStart
|
|
981
|
-
pooledStyleTracker.process(ansi.code);
|
|
982
|
-
// Include ANSI codes in their respective segments
|
|
983
|
-
if (currentCol < beforeEnd) {
|
|
984
|
-
pendingAnsiBefore += ansi.code;
|
|
985
|
-
}
|
|
986
|
-
else if (currentCol >= afterStart && currentCol < afterEnd && afterStarted) {
|
|
987
|
-
// Only include after we've started "after" (styling already prepended)
|
|
988
|
-
after += ansi.code;
|
|
989
|
-
}
|
|
990
|
-
i += ansi.length;
|
|
1088
|
+
for (const segment of iterateAnsiTextSegments(line)) {
|
|
1089
|
+
if (segment.type === "ansi") {
|
|
1090
|
+
appendAnsiToExtractedSegments(state, segment.value, context);
|
|
991
1091
|
continue;
|
|
992
1092
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const w = graphemeWidth(segment);
|
|
998
|
-
if (currentCol < beforeEnd) {
|
|
999
|
-
if (pendingAnsiBefore) {
|
|
1000
|
-
before += pendingAnsiBefore;
|
|
1001
|
-
pendingAnsiBefore = "";
|
|
1002
|
-
}
|
|
1003
|
-
before += segment;
|
|
1004
|
-
beforeWidth += w;
|
|
1005
|
-
}
|
|
1006
|
-
else if (currentCol >= afterStart && currentCol < afterEnd) {
|
|
1007
|
-
const fits = !strictAfter || currentCol + w <= afterEnd;
|
|
1008
|
-
if (fits) {
|
|
1009
|
-
// On first "after" grapheme, prepend inherited styling from before overlay
|
|
1010
|
-
if (!afterStarted) {
|
|
1011
|
-
after += pooledStyleTracker.getActiveCodes();
|
|
1012
|
-
afterStarted = true;
|
|
1013
|
-
}
|
|
1014
|
-
after += segment;
|
|
1015
|
-
afterWidth += w;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
currentCol += w;
|
|
1019
|
-
// Early exit: done with "before" only, or done with both segments
|
|
1020
|
-
if (afterLen <= 0 ? currentCol >= beforeEnd : currentCol >= afterEnd)
|
|
1021
|
-
break;
|
|
1022
|
-
}
|
|
1023
|
-
i = textEnd;
|
|
1024
|
-
if (afterLen <= 0 ? currentCol >= beforeEnd : currentCol >= afterEnd)
|
|
1093
|
+
const width = graphemeWidth(segment.value);
|
|
1094
|
+
appendGraphemeToExtractedSegments(state, segment.value, width, context);
|
|
1095
|
+
state.currentColumn += width;
|
|
1096
|
+
if (isLineSegmentExtractionComplete(state, context))
|
|
1025
1097
|
break;
|
|
1026
1098
|
}
|
|
1027
|
-
return {
|
|
1099
|
+
return {
|
|
1100
|
+
before: state.before,
|
|
1101
|
+
beforeWidth: state.beforeWidth,
|
|
1102
|
+
after: state.after,
|
|
1103
|
+
afterWidth: state.afterWidth,
|
|
1104
|
+
};
|
|
1028
1105
|
}
|
|
1029
1106
|
//# sourceMappingURL=utils.js.map
|