@optique/core 1.3.0-dev.2540 → 1.3.0-dev.2554
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/dist/displaywidth.cjs +12 -2
- package/dist/displaywidth.d.cts +8 -1
- package/dist/displaywidth.d.ts +8 -1
- package/dist/displaywidth.js +11 -2
- package/dist/doc.cjs +63 -83
- package/dist/doc.js +63 -83
- package/dist/facade.cjs +12 -12
- package/dist/facade.js +12 -12
- package/dist/internal/terminal.cjs +5 -1
- package/dist/internal/terminal.d.cts +2 -1
- package/dist/internal/terminal.d.ts +2 -1
- package/dist/internal/terminal.js +2 -1
- package/dist/terminal-internal.cjs +14 -6
- package/dist/terminal-internal.d.cts +2 -0
- package/dist/terminal-internal.d.ts +2 -0
- package/dist/terminal-internal.js +14 -6
- package/dist/terminal.d.cts +2 -1
- package/dist/terminal.d.ts +2 -1
- package/dist/text-layout.cjs +87 -0
- package/dist/text-layout.d.cts +54 -0
- package/dist/text-layout.d.ts +54 -0
- package/dist/text-layout.js +84 -0
- package/dist/usage.cjs +39 -8
- package/dist/usage.js +39 -8
- package/package.json +2 -2
package/dist/facade.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const require_internal_annotations = require('./internal/annotations.cjs');
|
|
2
|
-
const require_displaywidth = require('./displaywidth.cjs');
|
|
3
2
|
const require_terminal_internal = require('./terminal-internal.cjs');
|
|
4
3
|
const require_message = require('./message.cjs');
|
|
5
4
|
const require_message_renderer = require('./message-renderer.cjs');
|
|
5
|
+
const require_text_layout = require('./text-layout.cjs');
|
|
6
6
|
const require_validate = require('./validate.cjs');
|
|
7
7
|
const require_usage = require('./usage.cjs');
|
|
8
8
|
const require_doc = require('./doc.cjs');
|
|
@@ -521,10 +521,10 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
|
|
|
521
521
|
messageFormatter,
|
|
522
522
|
theme
|
|
523
523
|
});
|
|
524
|
-
const errorLabel = () => require_terminal_internal.renderTerminalTerm({
|
|
524
|
+
const errorLabel = () => require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
|
|
525
525
|
type: "errorLabel",
|
|
526
526
|
label: "Error:"
|
|
527
|
-
}, theme, colors)
|
|
527
|
+
}, theme, colors));
|
|
528
528
|
const shellName = completionArgs[0] || "";
|
|
529
529
|
const args = completionArgs.slice(1);
|
|
530
530
|
const callOnError = (code, error) => onError(code, error);
|
|
@@ -536,7 +536,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
|
|
|
536
536
|
colors,
|
|
537
537
|
quotes: !colors,
|
|
538
538
|
maxWidth,
|
|
539
|
-
initialWidth:
|
|
539
|
+
initialWidth: require_text_layout.measureText(prefix).lastLineWidth
|
|
540
540
|
})}\n`);
|
|
541
541
|
if (completionParser) {
|
|
542
542
|
const displayName = isOptionMode ? completionOptionDisplayName ?? "--completion" : completionCommandDisplayName ?? "completion";
|
|
@@ -570,7 +570,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
|
|
|
570
570
|
colors,
|
|
571
571
|
quotes: !colors,
|
|
572
572
|
maxWidth,
|
|
573
|
-
initialWidth:
|
|
573
|
+
initialWidth: require_text_layout.measureText(prefix).lastLineWidth
|
|
574
574
|
})}`);
|
|
575
575
|
return require_mode_dispatch.dispatchByMode(parser.mode, () => {
|
|
576
576
|
const result = callOnError(1, error);
|
|
@@ -886,17 +886,17 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
886
886
|
messageFormatter,
|
|
887
887
|
theme
|
|
888
888
|
});
|
|
889
|
-
const errorLabel = () => require_terminal_internal.renderTerminalTerm({
|
|
889
|
+
const errorLabel = () => require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
|
|
890
890
|
type: "errorLabel",
|
|
891
891
|
label: "Error:"
|
|
892
|
-
}, theme, colors)
|
|
892
|
+
}, theme, colors));
|
|
893
893
|
let usagePrefix;
|
|
894
|
-
const usageLabel = () => usagePrefix ??= require_terminal_internal.renderTerminalTerm({
|
|
894
|
+
const usageLabel = () => usagePrefix ??= require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
|
|
895
895
|
type: "label",
|
|
896
896
|
label: "Usage:",
|
|
897
897
|
kind: "usageSummary"
|
|
898
|
-
}, theme, colors)
|
|
899
|
-
const usageLabelWidth = () =>
|
|
898
|
+
}, theme, colors));
|
|
899
|
+
const usageLabelWidth = () => require_text_layout.measureText(usageLabel()).lastLineWidth;
|
|
900
900
|
const norm = (c) => c === true ? {} : c;
|
|
901
901
|
const helpCommandConfig = norm(options.help?.command);
|
|
902
902
|
const helpOptionConfig = norm(options.help?.option);
|
|
@@ -1059,7 +1059,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
1059
1059
|
}), usageLabelWidth())}`);
|
|
1060
1060
|
const prefix = errorLabel();
|
|
1061
1061
|
const errorMessage = formatMessage(validationError, {
|
|
1062
|
-
initialWidth:
|
|
1062
|
+
initialWidth: require_text_layout.measureText(prefix).lastLineWidth,
|
|
1063
1063
|
maxWidth,
|
|
1064
1064
|
colors,
|
|
1065
1065
|
quotes: !colors
|
|
@@ -1179,7 +1179,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
1179
1179
|
}), usageLabelWidth())}`);
|
|
1180
1180
|
const prefix = errorLabel();
|
|
1181
1181
|
const errorMessage = formatMessage(classified.error, {
|
|
1182
|
-
initialWidth:
|
|
1182
|
+
initialWidth: require_text_layout.measureText(prefix).lastLineWidth,
|
|
1183
1183
|
maxWidth,
|
|
1184
1184
|
colors,
|
|
1185
1185
|
quotes: !colors
|
package/dist/facade.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { injectAnnotations } from "./internal/annotations.js";
|
|
2
|
-
import { getDisplayWidth } from "./displaywidth.js";
|
|
3
2
|
import { renderTerminalTerm } from "./terminal-internal.js";
|
|
4
3
|
import { commandLine, lineBreak, message, optionName, text, value } from "./message.js";
|
|
5
4
|
import { resolveMessageFormatter } from "./message-renderer.js";
|
|
5
|
+
import { measureText, spaceAfterLabel } from "./text-layout.js";
|
|
6
6
|
import { validateCommandNames, validateContextIds, validateMetaNameCollisions, validateOptionNames, validateProgramName } from "./validate.js";
|
|
7
7
|
import { cloneUsage, formatUsage, isSuggestionHidden, normalizeUsage } from "./usage.js";
|
|
8
8
|
import { formatDocPage } from "./doc.js";
|
|
@@ -521,10 +521,10 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
|
|
|
521
521
|
messageFormatter,
|
|
522
522
|
theme
|
|
523
523
|
});
|
|
524
|
-
const errorLabel = () => renderTerminalTerm({
|
|
524
|
+
const errorLabel = () => spaceAfterLabel(renderTerminalTerm({
|
|
525
525
|
type: "errorLabel",
|
|
526
526
|
label: "Error:"
|
|
527
|
-
}, theme, colors)
|
|
527
|
+
}, theme, colors));
|
|
528
528
|
const shellName = completionArgs[0] || "";
|
|
529
529
|
const args = completionArgs.slice(1);
|
|
530
530
|
const callOnError = (code, error) => onError(code, error);
|
|
@@ -536,7 +536,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
|
|
|
536
536
|
colors,
|
|
537
537
|
quotes: !colors,
|
|
538
538
|
maxWidth,
|
|
539
|
-
initialWidth:
|
|
539
|
+
initialWidth: measureText(prefix).lastLineWidth
|
|
540
540
|
})}\n`);
|
|
541
541
|
if (completionParser) {
|
|
542
542
|
const displayName = isOptionMode ? completionOptionDisplayName ?? "--completion" : completionCommandDisplayName ?? "completion";
|
|
@@ -570,7 +570,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
|
|
|
570
570
|
colors,
|
|
571
571
|
quotes: !colors,
|
|
572
572
|
maxWidth,
|
|
573
|
-
initialWidth:
|
|
573
|
+
initialWidth: measureText(prefix).lastLineWidth
|
|
574
574
|
})}`);
|
|
575
575
|
return dispatchByMode(parser.mode, () => {
|
|
576
576
|
const result = callOnError(1, error);
|
|
@@ -886,17 +886,17 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
886
886
|
messageFormatter,
|
|
887
887
|
theme
|
|
888
888
|
});
|
|
889
|
-
const errorLabel = () => renderTerminalTerm({
|
|
889
|
+
const errorLabel = () => spaceAfterLabel(renderTerminalTerm({
|
|
890
890
|
type: "errorLabel",
|
|
891
891
|
label: "Error:"
|
|
892
|
-
}, theme, colors)
|
|
892
|
+
}, theme, colors));
|
|
893
893
|
let usagePrefix;
|
|
894
|
-
const usageLabel = () => usagePrefix ??= renderTerminalTerm({
|
|
894
|
+
const usageLabel = () => usagePrefix ??= spaceAfterLabel(renderTerminalTerm({
|
|
895
895
|
type: "label",
|
|
896
896
|
label: "Usage:",
|
|
897
897
|
kind: "usageSummary"
|
|
898
|
-
}, theme, colors)
|
|
899
|
-
const usageLabelWidth = () =>
|
|
898
|
+
}, theme, colors));
|
|
899
|
+
const usageLabelWidth = () => measureText(usageLabel()).lastLineWidth;
|
|
900
900
|
const norm = (c) => c === true ? {} : c;
|
|
901
901
|
const helpCommandConfig = norm(options.help?.command);
|
|
902
902
|
const helpOptionConfig = norm(options.help?.option);
|
|
@@ -1059,7 +1059,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
1059
1059
|
}), usageLabelWidth())}`);
|
|
1060
1060
|
const prefix = errorLabel();
|
|
1061
1061
|
const errorMessage = formatMessage(validationError, {
|
|
1062
|
-
initialWidth:
|
|
1062
|
+
initialWidth: measureText(prefix).lastLineWidth,
|
|
1063
1063
|
maxWidth,
|
|
1064
1064
|
colors,
|
|
1065
1065
|
quotes: !colors
|
|
@@ -1179,7 +1179,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
|
|
|
1179
1179
|
}), usageLabelWidth())}`);
|
|
1180
1180
|
const prefix = errorLabel();
|
|
1181
1181
|
const errorMessage = formatMessage(classified.error, {
|
|
1182
|
-
initialWidth:
|
|
1182
|
+
initialWidth: measureText(prefix).lastLineWidth,
|
|
1183
1183
|
maxWidth,
|
|
1184
1184
|
colors,
|
|
1185
1185
|
quotes: !colors
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
const require_displaywidth = require('../displaywidth.cjs');
|
|
2
2
|
const require_terminal_internal = require('../terminal-internal.cjs');
|
|
3
3
|
const require_message_renderer = require('../message-renderer.cjs');
|
|
4
|
+
const require_text_layout = require('../text-layout.cjs');
|
|
4
5
|
|
|
5
6
|
exports.getDisplayWidth = require_displaywidth.getDisplayWidth;
|
|
7
|
+
exports.measureText = require_text_layout.measureText;
|
|
8
|
+
exports.placeText = require_text_layout.placeText;
|
|
6
9
|
exports.renderTerminalTerm = require_terminal_internal.renderTerminalTerm;
|
|
7
|
-
exports.resolveMessageFormatter = require_message_renderer.resolveMessageFormatter;
|
|
10
|
+
exports.resolveMessageFormatter = require_message_renderer.resolveMessageFormatter;
|
|
11
|
+
exports.spaceAfterLabel = require_text_layout.spaceAfterLabel;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getDisplayWidth } from "../displaywidth.cjs";
|
|
2
2
|
import { resolveMessageFormatter } from "../message-renderer.cjs";
|
|
3
3
|
import { renderTerminalTerm } from "../terminal-internal.cjs";
|
|
4
|
-
|
|
4
|
+
import { measureText, placeText, spaceAfterLabel } from "../text-layout.cjs";
|
|
5
|
+
export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getDisplayWidth } from "../displaywidth.js";
|
|
2
2
|
import { resolveMessageFormatter } from "../message-renderer.js";
|
|
3
3
|
import { renderTerminalTerm } from "../terminal-internal.js";
|
|
4
|
-
|
|
4
|
+
import { measureText, placeText, spaceAfterLabel } from "../text-layout.js";
|
|
5
|
+
export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getDisplayWidth } from "../displaywidth.js";
|
|
2
2
|
import { renderTerminalTerm } from "../terminal-internal.js";
|
|
3
3
|
import { resolveMessageFormatter } from "../message-renderer.js";
|
|
4
|
+
import { measureText, placeText, spaceAfterLabel } from "../text-layout.js";
|
|
4
5
|
|
|
5
|
-
export { getDisplayWidth, renderTerminalTerm, resolveMessageFormatter };
|
|
6
|
+
export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
|
|
@@ -127,11 +127,19 @@ function sameStyle(a, b) {
|
|
|
127
127
|
}
|
|
128
128
|
function* fragmentTokens(fragment, scopes = [], inherited = {}) {
|
|
129
129
|
if (fragment.type === "text") {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
const lines = fragment.text.split("\n");
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
if (i > 0) yield {
|
|
133
|
+
text: "\n",
|
|
134
|
+
width: -1,
|
|
135
|
+
scopes
|
|
136
|
+
};
|
|
137
|
+
if (lines[i].length > 0 || lines.length === 1) yield {
|
|
138
|
+
text: lines[i],
|
|
139
|
+
width: require_displaywidth.getDisplayWidth(lines[i]),
|
|
140
|
+
scopes
|
|
141
|
+
};
|
|
142
|
+
}
|
|
135
143
|
return;
|
|
136
144
|
}
|
|
137
145
|
let next = scopes;
|
|
@@ -193,7 +201,7 @@ function serializeTokens(tokens, colors = false, resetSuffix = "", ambient = {})
|
|
|
193
201
|
const items = [...tokens];
|
|
194
202
|
for (let i = 0; i < items.length; i++) {
|
|
195
203
|
const token = items[i];
|
|
196
|
-
if (colors && token.width === -1) {
|
|
204
|
+
if (colors && token.width === -1 && token.scopes.length === 0) {
|
|
197
205
|
const next = items.slice(i + 1).find((t) => t.width !== -1)?.scopes ?? [];
|
|
198
206
|
let common = 0;
|
|
199
207
|
while (common < active.length && common < next.length && active[common] === next[common]) common++;
|
|
@@ -10,6 +10,8 @@ interface TerminalToken {
|
|
|
10
10
|
readonly text: string;
|
|
11
11
|
readonly width: number;
|
|
12
12
|
readonly scopes: readonly Scope[];
|
|
13
|
+
/** Layout-owned space, removable beside an explicit hard break. */
|
|
14
|
+
readonly separator?: boolean;
|
|
13
15
|
}
|
|
14
16
|
declare function terminalText(text: string): TerminalFragment;
|
|
15
17
|
declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
|
|
@@ -10,6 +10,8 @@ interface TerminalToken {
|
|
|
10
10
|
readonly text: string;
|
|
11
11
|
readonly width: number;
|
|
12
12
|
readonly scopes: readonly Scope[];
|
|
13
|
+
/** Layout-owned space, removable beside an explicit hard break. */
|
|
14
|
+
readonly separator?: boolean;
|
|
13
15
|
}
|
|
14
16
|
declare function terminalText(text: string): TerminalFragment;
|
|
15
17
|
declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
|
|
@@ -127,11 +127,19 @@ function sameStyle(a, b) {
|
|
|
127
127
|
}
|
|
128
128
|
function* fragmentTokens(fragment, scopes = [], inherited = {}) {
|
|
129
129
|
if (fragment.type === "text") {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
const lines = fragment.text.split("\n");
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
if (i > 0) yield {
|
|
133
|
+
text: "\n",
|
|
134
|
+
width: -1,
|
|
135
|
+
scopes
|
|
136
|
+
};
|
|
137
|
+
if (lines[i].length > 0 || lines.length === 1) yield {
|
|
138
|
+
text: lines[i],
|
|
139
|
+
width: getDisplayWidth(lines[i]),
|
|
140
|
+
scopes
|
|
141
|
+
};
|
|
142
|
+
}
|
|
135
143
|
return;
|
|
136
144
|
}
|
|
137
145
|
let next = scopes;
|
|
@@ -193,7 +201,7 @@ function serializeTokens(tokens, colors = false, resetSuffix = "", ambient = {})
|
|
|
193
201
|
const items = [...tokens];
|
|
194
202
|
for (let i = 0; i < items.length; i++) {
|
|
195
203
|
const token = items[i];
|
|
196
|
-
if (colors && token.width === -1) {
|
|
204
|
+
if (colors && token.width === -1 && token.scopes.length === 0) {
|
|
197
205
|
const next = items.slice(i + 1).find((t) => t.width !== -1)?.scopes ?? [];
|
|
198
206
|
let common = 0;
|
|
199
207
|
while (common < active.length && common < next.length && active[common] === next[common]) common++;
|
package/dist/terminal.d.cts
CHANGED
|
@@ -61,7 +61,8 @@ interface TerminalStyle {
|
|
|
61
61
|
readonly underline?: boolean;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
* Structured output.
|
|
64
|
+
* Structured output. Newlines in text leaves force line breaks; each line
|
|
65
|
+
* within a leaf is an indivisible wrapping unit.
|
|
65
66
|
* @since 1.3.0
|
|
66
67
|
*/
|
|
67
68
|
type TerminalFragment = {
|
package/dist/terminal.d.ts
CHANGED
|
@@ -61,7 +61,8 @@ interface TerminalStyle {
|
|
|
61
61
|
readonly underline?: boolean;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
* Structured output.
|
|
64
|
+
* Structured output. Newlines in text leaves force line breaks; each line
|
|
65
|
+
* within a leaf is an indivisible wrapping unit.
|
|
65
66
|
* @since 1.3.0
|
|
66
67
|
*/
|
|
67
68
|
type TerminalFragment = {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const require_displaywidth = require('./displaywidth.cjs');
|
|
2
|
+
|
|
3
|
+
//#region src/text-layout.ts
|
|
4
|
+
/**
|
|
5
|
+
* Measures physical lines after removing terminal escape sequences.
|
|
6
|
+
* Empty text occupies one empty line; a trailing LF adds an empty final line.
|
|
7
|
+
* @param text Rendered text, possibly containing ANSI styles or OSC links.
|
|
8
|
+
* @returns First, last and widest line widths, and the number of lines.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
function measureText(text) {
|
|
12
|
+
const lines = require_displaywidth.stripAnsi(text).split("\n");
|
|
13
|
+
const widths = lines.map(require_displaywidth.getDisplayWidth);
|
|
14
|
+
return {
|
|
15
|
+
firstLineWidth: widths[0],
|
|
16
|
+
lastLineWidth: widths[widths.length - 1],
|
|
17
|
+
maxLineWidth: widths.reduce((widest, width) => Math.max(widest, width), 0),
|
|
18
|
+
lineCount: lines.length
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Appends rendered text to a cursor, optionally moving it to a fresh line.
|
|
23
|
+
* Measures the actual join so combining sequences crossing the boundary are
|
|
24
|
+
* handled correctly. Hard breaks reset both the line and any external offset.
|
|
25
|
+
* This never reflows the text itself or changes its escape sequences.
|
|
26
|
+
* @param text Text to append.
|
|
27
|
+
* @param cursor The current line and column.
|
|
28
|
+
* @param maxWidth Optional column budget.
|
|
29
|
+
* @param trailingRoom Space to leave after a single-line block. An annotation
|
|
30
|
+
* prefix reserves one column for content; a multiline prefix already has a
|
|
31
|
+
* hard break, so its first line needs no reservation.
|
|
32
|
+
* @returns The placed text (possibly with a leading LF) and resulting cursor.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
function placeText(text, cursor, maxWidth, trailingRoom = 0) {
|
|
36
|
+
const plain = require_displaywidth.stripAnsi(text);
|
|
37
|
+
const lines = plain.split("\n");
|
|
38
|
+
const offset = cursor.column - require_displaywidth.getDisplayWidth(cursor.line);
|
|
39
|
+
const joinedWidth = offset + require_displaywidth.getDisplayWidth(cursor.line + lines[0]);
|
|
40
|
+
const room = lines.length === 1 ? trailingRoom : 0;
|
|
41
|
+
const breaks = maxWidth != null && (cursor.column > 0 || room > 0) && (joinedWidth > cursor.column || room > 0) && joinedWidth + room > maxWidth;
|
|
42
|
+
const lastLine = lines[lines.length - 1];
|
|
43
|
+
return {
|
|
44
|
+
text: breaks ? "\n" + text : text,
|
|
45
|
+
cursor: lines.length > 1 || breaks ? {
|
|
46
|
+
line: lastLine,
|
|
47
|
+
column: require_displaywidth.getDisplayWidth(lastLine)
|
|
48
|
+
} : {
|
|
49
|
+
line: cursor.line + plain,
|
|
50
|
+
column: joinedWidth
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Measures an annotation's fixed text without invoking theme callbacks.
|
|
56
|
+
* @param prefix The already joined prefix and optional label.
|
|
57
|
+
* @param suffix The closing text.
|
|
58
|
+
* @returns Geometry reserving at least one content column after the suffix's
|
|
59
|
+
* first-line width has been deducted from the formatter budget.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
function measureAnnotation(prefix, suffix) {
|
|
63
|
+
const prefixMetrics = measureText(prefix);
|
|
64
|
+
const suffixMetrics = measureText(suffix);
|
|
65
|
+
return {
|
|
66
|
+
prefix,
|
|
67
|
+
suffix,
|
|
68
|
+
prefixMetrics,
|
|
69
|
+
suffixMetrics,
|
|
70
|
+
minWidth: Math.max(prefixMetrics.maxLineWidth, suffixMetrics.maxLineWidth, suffixMetrics.firstLineWidth + 1)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Adds a separating space only when a label occupies its final physical line.
|
|
75
|
+
* @param label The rendered label, including any styles and hard breaks.
|
|
76
|
+
* @returns The label with spacing for the text that follows it.
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
function spaceAfterLabel(label) {
|
|
80
|
+
return measureText(label).lastLineWidth > 0 ? label + " " : label;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
exports.measureAnnotation = measureAnnotation;
|
|
85
|
+
exports.measureText = measureText;
|
|
86
|
+
exports.placeText = placeText;
|
|
87
|
+
exports.spaceAfterLabel = spaceAfterLabel;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/text-layout.d.ts
|
|
2
|
+
/** Physical-line geometry at a rendered-text assembly boundary. @internal */
|
|
3
|
+
interface TextMetrics {
|
|
4
|
+
readonly firstLineWidth: number;
|
|
5
|
+
readonly lastLineWidth: number;
|
|
6
|
+
readonly maxLineWidth: number;
|
|
7
|
+
readonly lineCount: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Measures physical lines after removing terminal escape sequences.
|
|
11
|
+
* Empty text occupies one empty line; a trailing LF adds an empty final line.
|
|
12
|
+
* @param text Rendered text, possibly containing ANSI styles or OSC links.
|
|
13
|
+
* @returns First, last and widest line widths, and the number of lines.
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
declare function measureText(text: string): TextMetrics;
|
|
17
|
+
/**
|
|
18
|
+
* The current visible line and its occupied column. The column may include
|
|
19
|
+
* an external offset for text unavailable to this renderer (initialWidth).
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
interface TextCursor {
|
|
23
|
+
readonly line: string;
|
|
24
|
+
readonly column: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Appends rendered text to a cursor, optionally moving it to a fresh line.
|
|
28
|
+
* Measures the actual join so combining sequences crossing the boundary are
|
|
29
|
+
* handled correctly. Hard breaks reset both the line and any external offset.
|
|
30
|
+
* This never reflows the text itself or changes its escape sequences.
|
|
31
|
+
* @param text Text to append.
|
|
32
|
+
* @param cursor The current line and column.
|
|
33
|
+
* @param maxWidth Optional column budget.
|
|
34
|
+
* @param trailingRoom Space to leave after a single-line block. An annotation
|
|
35
|
+
* prefix reserves one column for content; a multiline prefix already has a
|
|
36
|
+
* hard break, so its first line needs no reservation.
|
|
37
|
+
* @returns The placed text (possibly with a leading LF) and resulting cursor.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
declare function placeText(text: string, cursor: TextCursor, maxWidth?: number, trailingRoom?: number): {
|
|
41
|
+
readonly text: string;
|
|
42
|
+
readonly cursor: TextCursor;
|
|
43
|
+
};
|
|
44
|
+
/** Fixed annotation geometry shared by preflight and placement. @internal */
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Adds a separating space only when a label occupies its final physical line.
|
|
48
|
+
* @param label The rendered label, including any styles and hard breaks.
|
|
49
|
+
* @returns The label with spacing for the text that follows it.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
declare function spaceAfterLabel(label: string): string;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { measureText, placeText, spaceAfterLabel };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/text-layout.d.ts
|
|
2
|
+
/** Physical-line geometry at a rendered-text assembly boundary. @internal */
|
|
3
|
+
interface TextMetrics {
|
|
4
|
+
readonly firstLineWidth: number;
|
|
5
|
+
readonly lastLineWidth: number;
|
|
6
|
+
readonly maxLineWidth: number;
|
|
7
|
+
readonly lineCount: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Measures physical lines after removing terminal escape sequences.
|
|
11
|
+
* Empty text occupies one empty line; a trailing LF adds an empty final line.
|
|
12
|
+
* @param text Rendered text, possibly containing ANSI styles or OSC links.
|
|
13
|
+
* @returns First, last and widest line widths, and the number of lines.
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
declare function measureText(text: string): TextMetrics;
|
|
17
|
+
/**
|
|
18
|
+
* The current visible line and its occupied column. The column may include
|
|
19
|
+
* an external offset for text unavailable to this renderer (initialWidth).
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
interface TextCursor {
|
|
23
|
+
readonly line: string;
|
|
24
|
+
readonly column: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Appends rendered text to a cursor, optionally moving it to a fresh line.
|
|
28
|
+
* Measures the actual join so combining sequences crossing the boundary are
|
|
29
|
+
* handled correctly. Hard breaks reset both the line and any external offset.
|
|
30
|
+
* This never reflows the text itself or changes its escape sequences.
|
|
31
|
+
* @param text Text to append.
|
|
32
|
+
* @param cursor The current line and column.
|
|
33
|
+
* @param maxWidth Optional column budget.
|
|
34
|
+
* @param trailingRoom Space to leave after a single-line block. An annotation
|
|
35
|
+
* prefix reserves one column for content; a multiline prefix already has a
|
|
36
|
+
* hard break, so its first line needs no reservation.
|
|
37
|
+
* @returns The placed text (possibly with a leading LF) and resulting cursor.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
declare function placeText(text: string, cursor: TextCursor, maxWidth?: number, trailingRoom?: number): {
|
|
41
|
+
readonly text: string;
|
|
42
|
+
readonly cursor: TextCursor;
|
|
43
|
+
};
|
|
44
|
+
/** Fixed annotation geometry shared by preflight and placement. @internal */
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Adds a separating space only when a label occupies its final physical line.
|
|
48
|
+
* @param label The rendered label, including any styles and hard breaks.
|
|
49
|
+
* @returns The label with spacing for the text that follows it.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
declare function spaceAfterLabel(label: string): string;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { measureText, placeText, spaceAfterLabel };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getDisplayWidth, stripAnsi } from "./displaywidth.js";
|
|
2
|
+
|
|
3
|
+
//#region src/text-layout.ts
|
|
4
|
+
/**
|
|
5
|
+
* Measures physical lines after removing terminal escape sequences.
|
|
6
|
+
* Empty text occupies one empty line; a trailing LF adds an empty final line.
|
|
7
|
+
* @param text Rendered text, possibly containing ANSI styles or OSC links.
|
|
8
|
+
* @returns First, last and widest line widths, and the number of lines.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
function measureText(text) {
|
|
12
|
+
const lines = stripAnsi(text).split("\n");
|
|
13
|
+
const widths = lines.map(getDisplayWidth);
|
|
14
|
+
return {
|
|
15
|
+
firstLineWidth: widths[0],
|
|
16
|
+
lastLineWidth: widths[widths.length - 1],
|
|
17
|
+
maxLineWidth: widths.reduce((widest, width) => Math.max(widest, width), 0),
|
|
18
|
+
lineCount: lines.length
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Appends rendered text to a cursor, optionally moving it to a fresh line.
|
|
23
|
+
* Measures the actual join so combining sequences crossing the boundary are
|
|
24
|
+
* handled correctly. Hard breaks reset both the line and any external offset.
|
|
25
|
+
* This never reflows the text itself or changes its escape sequences.
|
|
26
|
+
* @param text Text to append.
|
|
27
|
+
* @param cursor The current line and column.
|
|
28
|
+
* @param maxWidth Optional column budget.
|
|
29
|
+
* @param trailingRoom Space to leave after a single-line block. An annotation
|
|
30
|
+
* prefix reserves one column for content; a multiline prefix already has a
|
|
31
|
+
* hard break, so its first line needs no reservation.
|
|
32
|
+
* @returns The placed text (possibly with a leading LF) and resulting cursor.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
function placeText(text, cursor, maxWidth, trailingRoom = 0) {
|
|
36
|
+
const plain = stripAnsi(text);
|
|
37
|
+
const lines = plain.split("\n");
|
|
38
|
+
const offset = cursor.column - getDisplayWidth(cursor.line);
|
|
39
|
+
const joinedWidth = offset + getDisplayWidth(cursor.line + lines[0]);
|
|
40
|
+
const room = lines.length === 1 ? trailingRoom : 0;
|
|
41
|
+
const breaks = maxWidth != null && (cursor.column > 0 || room > 0) && (joinedWidth > cursor.column || room > 0) && joinedWidth + room > maxWidth;
|
|
42
|
+
const lastLine = lines[lines.length - 1];
|
|
43
|
+
return {
|
|
44
|
+
text: breaks ? "\n" + text : text,
|
|
45
|
+
cursor: lines.length > 1 || breaks ? {
|
|
46
|
+
line: lastLine,
|
|
47
|
+
column: getDisplayWidth(lastLine)
|
|
48
|
+
} : {
|
|
49
|
+
line: cursor.line + plain,
|
|
50
|
+
column: joinedWidth
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Measures an annotation's fixed text without invoking theme callbacks.
|
|
56
|
+
* @param prefix The already joined prefix and optional label.
|
|
57
|
+
* @param suffix The closing text.
|
|
58
|
+
* @returns Geometry reserving at least one content column after the suffix's
|
|
59
|
+
* first-line width has been deducted from the formatter budget.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
function measureAnnotation(prefix, suffix) {
|
|
63
|
+
const prefixMetrics = measureText(prefix);
|
|
64
|
+
const suffixMetrics = measureText(suffix);
|
|
65
|
+
return {
|
|
66
|
+
prefix,
|
|
67
|
+
suffix,
|
|
68
|
+
prefixMetrics,
|
|
69
|
+
suffixMetrics,
|
|
70
|
+
minWidth: Math.max(prefixMetrics.maxLineWidth, suffixMetrics.maxLineWidth, suffixMetrics.firstLineWidth + 1)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Adds a separating space only when a label occupies its final physical line.
|
|
75
|
+
* @param label The rendered label, including any styles and hard breaks.
|
|
76
|
+
* @returns The label with spacing for the text that follows it.
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
function spaceAfterLabel(label) {
|
|
80
|
+
return measureText(label).lastLineWidth > 0 ? label + " " : label;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { measureAnnotation, measureText, placeText, spaceAfterLabel };
|