@promptctl/rich-js 0.18.0 → 0.19.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 +9 -7
- package/dist/core/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +145 -0
- package/dist/core/box.js +372 -0
- package/dist/core/cells.d.ts +192 -0
- package/dist/core/cells.js +334 -0
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +326 -0
- package/dist/core/color.js +1225 -0
- package/dist/core/console.d.ts +196 -0
- package/dist/core/console.js +596 -0
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/emoji.d.ts +24 -0
- package/dist/core/emoji.js +391 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.d.ts +59 -0
- package/dist/core/export-html.js +197 -0
- package/dist/core/export-lines.d.ts +130 -0
- package/dist/core/export-lines.js +159 -0
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +33 -0
- package/dist/core/highlighter.js +190 -0
- package/dist/core/json.d.ts +27 -0
- package/dist/core/json.js +53 -0
- package/dist/core/markup.d.ts +140 -0
- package/dist/core/markup.js +654 -0
- package/dist/core/measure.d.ts +27 -0
- package/dist/core/measure.js +67 -0
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/oklch.d.ts +161 -0
- package/dist/core/oklch.js +312 -0
- package/dist/core/osc8.d.ts +68 -0
- package/dist/core/osc8.js +78 -0
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +150 -0
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +49 -0
- package/dist/core/pretty.d.ts +247 -0
- package/dist/core/pretty.js +906 -0
- package/dist/core/protocol.d.ts +305 -0
- package/dist/core/protocol.js +183 -0
- package/dist/core/render.d.ts +87 -0
- package/dist/core/render.js +105 -0
- package/dist/core/segment.d.ts +162 -0
- package/dist/core/segment.js +488 -0
- package/dist/core/spinnerData.d.ts +12 -0
- package/dist/core/spinnerData.js +117 -0
- package/dist/core/strip.d.ts +152 -0
- package/dist/core/strip.js +326 -0
- package/dist/core/style.d.ts +189 -0
- package/dist/core/style.js +758 -0
- package/dist/core/subscription.d.ts +12 -0
- package/dist/core/subscription.js +12 -0
- package/dist/core/text.d.ts +283 -0
- package/dist/core/text.js +1097 -0
- package/dist/core/wrap.d.ts +46 -0
- package/dist/core/wrap.js +140 -0
- package/dist/host/app.d.ts +133 -0
- package/dist/host/app.js +285 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.js +3 -0
- package/dist/host/terminal-host.d.ts +180 -0
- package/dist/host/terminal-host.js +186 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +114 -0
- package/dist/node/prompt.d.ts +34 -0
- package/dist/node/prompt.js +51 -0
- package/dist/node/save.d.ts +18 -0
- package/dist/node/save.js +24 -0
- package/dist/node/terminal-host.d.ts +114 -0
- package/dist/node/terminal-host.js +227 -0
- package/dist/node/traceback.d.ts +38 -0
- package/dist/node/traceback.js +98 -0
- package/dist/renderables/align.d.ts +16 -0
- package/dist/renderables/align.js +26 -0
- package/dist/renderables/columns.d.ts +77 -0
- package/dist/renderables/columns.js +191 -0
- package/dist/renderables/constrain.d.ts +15 -0
- package/dist/renderables/constrain.js +31 -0
- package/dist/renderables/effect.d.ts +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +107 -0
- package/dist/renderables/embed.js +160 -0
- package/dist/renderables/flexStrip.d.ts +41 -0
- package/dist/renderables/flexStrip.js +200 -0
- package/dist/renderables/group.d.ts +14 -0
- package/dist/renderables/group.js +22 -0
- package/dist/renderables/layout.d.ts +75 -0
- package/dist/renderables/layout.js +223 -0
- package/dist/renderables/live.d.ts +74 -0
- package/dist/renderables/live.js +188 -0
- package/dist/renderables/markdown.d.ts +38 -0
- package/dist/renderables/markdown.js +587 -0
- package/dist/renderables/padding.d.ts +59 -0
- package/dist/renderables/padding.js +147 -0
- package/dist/renderables/panel.d.ts +114 -0
- package/dist/renderables/panel.js +303 -0
- package/dist/renderables/progress.d.ts +178 -0
- package/dist/renderables/progress.js +362 -0
- package/dist/renderables/progressBar.d.ts +31 -0
- package/dist/renderables/progressBar.js +64 -0
- package/dist/renderables/prompt.d.ts +71 -0
- package/dist/renderables/prompt.js +165 -0
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +26 -0
- package/dist/renderables/rule.js +86 -0
- package/dist/renderables/spinner.d.ts +57 -0
- package/dist/renderables/spinner.js +103 -0
- package/dist/renderables/status.d.ts +29 -0
- package/dist/renderables/status.js +46 -0
- package/dist/renderables/syntax.d.ts +65 -0
- package/dist/renderables/syntax.js +166 -0
- package/dist/renderables/table.d.ts +232 -0
- package/dist/renderables/table.js +973 -0
- package/dist/renderables/traceback.d.ts +32 -0
- package/dist/renderables/traceback.js +279 -0
- package/dist/renderables/tree.d.ts +76 -0
- package/dist/renderables/tree.js +177 -0
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +196 -0
- package/dist/template-bindings/color-funcs.d.ts +109 -0
- package/dist/template-bindings/color-funcs.js +243 -0
- package/dist/template-bindings/helpers.d.ts +26 -0
- package/dist/template-bindings/helpers.js +33 -0
- package/dist/template-bindings/index.d.ts +119 -0
- package/dist/template-bindings/index.js +162 -0
- package/dist/template-bindings/palette-funcs.d.ts +96 -0
- package/dist/template-bindings/palette-funcs.js +145 -0
- package/dist/template-bindings/style-funcs.d.ts +28 -0
- package/dist/template-bindings/style-funcs.js +157 -0
- package/dist/themes/buildPalette.d.ts +57 -0
- package/dist/themes/buildPalette.js +100 -0
- package/dist/themes/colorMath.d.ts +94 -0
- package/dist/themes/colorMath.js +280 -0
- package/dist/themes/colorRef.d.ts +36 -0
- package/dist/themes/colorRef.js +78 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.d.ts +3 -0
- package/dist/themes/data/atom-one-dark.js +177 -0
- package/dist/themes/data/atom-one-light.d.ts +3 -0
- package/dist/themes/data/atom-one-light.js +177 -0
- package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
- package/dist/themes/data/catppuccin-frappe.js +177 -0
- package/dist/themes/data/catppuccin-latte.d.ts +3 -0
- package/dist/themes/data/catppuccin-latte.js +177 -0
- package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
- package/dist/themes/data/catppuccin-macchiato.js +177 -0
- package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
- package/dist/themes/data/catppuccin-mocha.js +177 -0
- package/dist/themes/data/cyberpunk.d.ts +17 -0
- package/dist/themes/data/cyberpunk.js +193 -0
- package/dist/themes/data/default.d.ts +3 -0
- package/dist/themes/data/default.js +160 -0
- package/dist/themes/data/dracula.d.ts +3 -0
- package/dist/themes/data/dracula.js +177 -0
- package/dist/themes/data/flexoki.d.ts +3 -0
- package/dist/themes/data/flexoki.js +177 -0
- package/dist/themes/data/gruvbox.d.ts +3 -0
- package/dist/themes/data/gruvbox.js +177 -0
- package/dist/themes/data/index.d.ts +35 -0
- package/dist/themes/data/index.js +64 -0
- package/dist/themes/data/monokai.d.ts +3 -0
- package/dist/themes/data/monokai.js +177 -0
- package/dist/themes/data/nord.d.ts +3 -0
- package/dist/themes/data/nord.js +177 -0
- package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
- package/dist/themes/data/rose-pine-dawn.js +177 -0
- package/dist/themes/data/rose-pine-moon.d.ts +3 -0
- package/dist/themes/data/rose-pine-moon.js +177 -0
- package/dist/themes/data/rose-pine.d.ts +3 -0
- package/dist/themes/data/rose-pine.js +177 -0
- package/dist/themes/data/solarized-dark.d.ts +3 -0
- package/dist/themes/data/solarized-dark.js +177 -0
- package/dist/themes/data/solarized-light.d.ts +3 -0
- package/dist/themes/data/solarized-light.js +177 -0
- package/dist/themes/data/svg-export.d.ts +3 -0
- package/dist/themes/data/svg-export.js +177 -0
- package/dist/themes/data/textual-ansi.d.ts +3 -0
- package/dist/themes/data/textual-ansi.js +160 -0
- package/dist/themes/data/textual-dark.d.ts +3 -0
- package/dist/themes/data/textual-dark.js +160 -0
- package/dist/themes/data/textual-light.d.ts +3 -0
- package/dist/themes/data/textual-light.js +160 -0
- package/dist/themes/data/tokyo-night.d.ts +3 -0
- package/dist/themes/data/tokyo-night.js +177 -0
- package/dist/themes/data/types.d.ts +31 -0
- package/dist/themes/data/types.js +10 -0
- package/dist/themes/palette.d.ts +45 -0
- package/dist/themes/palette.js +71 -0
- package/dist/themes/ramp.d.ts +70 -0
- package/dist/themes/ramp.js +103 -0
- package/dist/themes/registry.d.ts +44 -0
- package/dist/themes/registry.js +105 -0
- package/dist/themes/terminalThemes.d.ts +49 -0
- package/dist/themes/terminalThemes.js +63 -0
- package/dist/themes/transpose.d.ts +87 -0
- package/dist/themes/transpose.js +138 -0
- package/dist/widgets/button.d.ts +45 -0
- package/dist/widgets/button.js +177 -0
- package/dist/widgets/checkbox.d.ts +38 -0
- package/dist/widgets/checkbox.js +135 -0
- package/dist/widgets/dropdown.d.ts +64 -0
- package/dist/widgets/dropdown.js +416 -0
- package/dist/widgets/event-router.d.ts +71 -0
- package/dist/widgets/event-router.js +629 -0
- package/dist/widgets/focus-manager.d.ts +45 -0
- package/dist/widgets/focus-manager.js +191 -0
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +14 -0
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +54 -0
- package/dist/widgets/slider.js +247 -0
- package/dist/widgets/static-item.d.ts +34 -0
- package/dist/widgets/static-item.js +54 -0
- package/dist/widgets/text-input.d.ts +283 -0
- package/dist/widgets/text-input.js +1174 -0
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +41 -0
- package/dist/widgets/toggle.js +157 -0
- package/dist/widgets/types.d.ts +87 -0
- package/dist/widgets/types.js +30 -0
- package/dist/widgets/widget-app.d.ts +65 -0
- package/dist/widgets/widget-app.js +206 -0
- package/dist/widgets/widget-base.d.ts +42 -0
- package/dist/widgets/widget-base.js +172 -0
- package/package.json +16 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traceback — renders error tracebacks with formatting.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:effects-at-boundaries] Pure rendering: a caught value in, `Segment`s out.
|
|
5
|
+
* Installing this as the process-wide crash handler touches `process.on` and
|
|
6
|
+
* `process.exit`, so that lives behind the node seam as `installTraceback` in
|
|
7
|
+
* `src/node/traceback.ts` — which is what keeps this module, and therefore the
|
|
8
|
+
* main barrel, importable in a browser.
|
|
9
|
+
*
|
|
10
|
+
* [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
|
|
11
|
+
* reads. There is no `showLocals`: an `Error` carries a stack of locations and
|
|
12
|
+
* nothing of the values in scope at them. The one way to get those in node —
|
|
13
|
+
* an inspector session pausing on exceptions — would have to pause on every
|
|
14
|
+
* throw, caught ones included, to serve `new Traceback(caughtError)`, and it
|
|
15
|
+
* cannot exist in a browser at all. Nor is there a `width` or `theme`: those
|
|
16
|
+
* sized and coloured a source-code excerpt this renderer does not produce.
|
|
17
|
+
*/
|
|
18
|
+
import { Segment } from "../core/segment.js";
|
|
19
|
+
import type { Renderable, RenderOptions } from "../core/protocol.js";
|
|
20
|
+
export interface TracebackOptions {
|
|
21
|
+
suppress?: string[];
|
|
22
|
+
maxFrames?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class Traceback implements Renderable {
|
|
25
|
+
/** What was caught, as it was handed in. */
|
|
26
|
+
readonly error: unknown;
|
|
27
|
+
readonly maxFrames: number;
|
|
28
|
+
readonly suppress: string[];
|
|
29
|
+
private readonly caught;
|
|
30
|
+
constructor(error: unknown, options?: TracebackOptions);
|
|
31
|
+
render(options: RenderOptions): Iterable<Segment>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traceback — renders error tracebacks with formatting.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:effects-at-boundaries] Pure rendering: a caught value in, `Segment`s out.
|
|
5
|
+
* Installing this as the process-wide crash handler touches `process.on` and
|
|
6
|
+
* `process.exit`, so that lives behind the node seam as `installTraceback` in
|
|
7
|
+
* `src/node/traceback.ts` — which is what keeps this module, and therefore the
|
|
8
|
+
* main barrel, importable in a browser.
|
|
9
|
+
*
|
|
10
|
+
* [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
|
|
11
|
+
* reads. There is no `showLocals`: an `Error` carries a stack of locations and
|
|
12
|
+
* nothing of the values in scope at them. The one way to get those in node —
|
|
13
|
+
* an inspector session pausing on exceptions — would have to pause on every
|
|
14
|
+
* throw, caught ones included, to serve `new Traceback(caughtError)`, and it
|
|
15
|
+
* cannot exist in a browser at all. Nor is there a `width` or `theme`: those
|
|
16
|
+
* sized and coloured a source-code excerpt this renderer does not produce.
|
|
17
|
+
*/
|
|
18
|
+
import { Segment } from "../core/segment.js";
|
|
19
|
+
import { RichText } from "../core/text.js";
|
|
20
|
+
import { Pretty, UNSEEN_DATA_BOUNDS } from "../core/pretty.js";
|
|
21
|
+
import { layoutPadding } from "./padding.js";
|
|
22
|
+
/**
|
|
23
|
+
* [LAW:parse-dont-validate] The checkpoint between whatever a `catch` caught —
|
|
24
|
+
* JavaScript lets a program throw any value — and the report. Anything with a
|
|
25
|
+
* string `name` and a `message` is reported as the error it presents itself
|
|
26
|
+
* as: that is every native error from any realm, which `instanceof Error`
|
|
27
|
+
* would refuse for one from a `node:vm` context or an iframe, and any thrower
|
|
28
|
+
* shaped like one. A message that is not a string — a native error's, assigned
|
|
29
|
+
* after construction — is formatted by `Pretty` under the error's own name.
|
|
30
|
+
* Anything else is reported as the value it is, under `NonError`, formatted by
|
|
31
|
+
* `Pretty` so it reads the same in a browser as in node — with the frames of a
|
|
32
|
+
* stack it carries, when it carries one.
|
|
33
|
+
*/
|
|
34
|
+
function readCaught(value) {
|
|
35
|
+
const fields = readFields(value);
|
|
36
|
+
if (fields !== undefined && typeof fields.name === "string" && fields.hasMessage) {
|
|
37
|
+
const { name, message } = fields;
|
|
38
|
+
const text = typeof message === "string" ? message : undefined;
|
|
39
|
+
return {
|
|
40
|
+
name: name || "Error",
|
|
41
|
+
message: text === undefined
|
|
42
|
+
? prettyMessage(message)
|
|
43
|
+
: () => new RichText(text, { style: "traceback.text", end: "" }),
|
|
44
|
+
frames: parseStack(fields.stack, name, text),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { name: "NonError", message: prettyMessage(value), frames: parseStack(fields?.stack ?? "", undefined, undefined) };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The fields the report reads off a caught object, or `undefined` when it is
|
|
51
|
+
* not an object or reading them throws — a getter that throws, a `Proxy` whose
|
|
52
|
+
* trap does. Such a value is reported under `NonError`, where `Pretty` reads
|
|
53
|
+
* it again and marks the read that threw as `[Threw: …]`, so the fault stays
|
|
54
|
+
* in the report instead of taking the crash handler down with it.
|
|
55
|
+
*/
|
|
56
|
+
function readFields(value) {
|
|
57
|
+
if (typeof value !== "object" || value === null)
|
|
58
|
+
return undefined;
|
|
59
|
+
try {
|
|
60
|
+
const record = value;
|
|
61
|
+
const stack = record.stack;
|
|
62
|
+
return {
|
|
63
|
+
name: record.name,
|
|
64
|
+
hasMessage: "message" in record,
|
|
65
|
+
message: record.message,
|
|
66
|
+
stack: typeof stack === "string" ? stack : "",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function prettyMessage(value) {
|
|
74
|
+
const pretty = new Pretty(value, UNSEEN_DATA_BOUNDS);
|
|
75
|
+
return (options) => pretty.toText(options);
|
|
76
|
+
}
|
|
77
|
+
/** The first line V8 writes into an error's stack: `String(error)` at the throw. */
|
|
78
|
+
function header(name, message) {
|
|
79
|
+
if (!name)
|
|
80
|
+
return message;
|
|
81
|
+
if (!message)
|
|
82
|
+
return name;
|
|
83
|
+
return `${name}: ${message}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Where the header that opens an error's stack ends, or `undefined` when the
|
|
87
|
+
* stack does not open with the header the error now has.
|
|
88
|
+
*
|
|
89
|
+
* V8 writes `String(error)`; node writes its own errors' code into the name
|
|
90
|
+
* there (`RangeError [ERR_X]: message`), so after the name may come one line of
|
|
91
|
+
* anything before `: ` and the message.
|
|
92
|
+
*/
|
|
93
|
+
function headerEnd(stack, name, message) {
|
|
94
|
+
const endsLine = (at) => at === stack.length || stack[at] === "\n";
|
|
95
|
+
const plain = header(name, message);
|
|
96
|
+
if (stack.startsWith(plain) && endsLine(plain.length))
|
|
97
|
+
return plain.length;
|
|
98
|
+
if (!name || !stack.startsWith(name))
|
|
99
|
+
return undefined;
|
|
100
|
+
const tail = `: ${message}`;
|
|
101
|
+
const at = stack.indexOf(tail, name.length);
|
|
102
|
+
if (at < 0 || stack.slice(name.length, at).includes("\n"))
|
|
103
|
+
return undefined;
|
|
104
|
+
return endsLine(at + tail.length) ? at + tail.length : undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether a stack line is shaped as V8's header — a name, then `:` or nothing —
|
|
108
|
+
* rather than as a SpiderMonkey or JavaScriptCore frame, whose `@` comes before
|
|
109
|
+
* the first `:` of its location. Read by shape, not by the error's name,
|
|
110
|
+
* because the name may have been reassigned after V8 wrote the header too.
|
|
111
|
+
*/
|
|
112
|
+
function isHeaderShaped(line) {
|
|
113
|
+
const colon = line.indexOf(":");
|
|
114
|
+
return !(colon < 0 ? line : line.slice(0, colon)).includes("@");
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The frames of a stack, in order.
|
|
118
|
+
*
|
|
119
|
+
* V8 opens a stack with the error's header, and a message can span lines that
|
|
120
|
+
* look like frames, so the header comes off first, as the exact text it is.
|
|
121
|
+
* When it does not open the stack, the message was reassigned after the throw
|
|
122
|
+
* and what precedes the first V8 frame is that stale header — all of it, when
|
|
123
|
+
* V8 recorded no frame, as under `Error.stackTraceLimit = 0`. SpiderMonkey and
|
|
124
|
+
* JavaScriptCore write no header, so a stack that opens with one of their
|
|
125
|
+
* frames is all frames. A value that is not an error has no name, so its stack is
|
|
126
|
+
* read from its first V8 frame, or whole when it has none.
|
|
127
|
+
*/
|
|
128
|
+
function parseStack(stack, name, message) {
|
|
129
|
+
const end = name !== undefined && message !== undefined ? headerEnd(stack, name, message) : undefined;
|
|
130
|
+
const lines = stack.slice(end ?? 0).split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
131
|
+
if (end !== undefined)
|
|
132
|
+
return lines.map(parseFrame);
|
|
133
|
+
const firstV8 = lines.findIndex((line) => V8_FRAME.test(line));
|
|
134
|
+
if (firstV8 >= 0)
|
|
135
|
+
return lines.slice(firstV8).map(parseFrame);
|
|
136
|
+
return name !== undefined && lines[0] !== undefined && isHeaderShaped(lines[0]) ? [] : lines.map(parseFrame);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* V8 writes `at [async] [fn (]location[)]`. The `async` marker is not part of
|
|
140
|
+
* any name, so it is read and dropped; the lookahead keeps `at async (…)` — a
|
|
141
|
+
* function named `async` — a name.
|
|
142
|
+
*/
|
|
143
|
+
const V8_FRAME = /^at\s+(?:async\s+(?!\())?(.*)$/;
|
|
144
|
+
/**
|
|
145
|
+
* SpiderMonkey and JavaScriptCore write `[fn]@location`, Firefox prefixing an
|
|
146
|
+
* awaited call's name with a cause up to a `*` (`async*`, `promise callback*`),
|
|
147
|
+
* which is dropped as V8's `async` is.
|
|
148
|
+
*/
|
|
149
|
+
const AT_SIGN_FRAME = /^(?:[^@]*\*)?([^@]*)@(.*)$/;
|
|
150
|
+
function parseFrame(text) {
|
|
151
|
+
const v8 = V8_FRAME.exec(text);
|
|
152
|
+
if (v8) {
|
|
153
|
+
const body = v8[1];
|
|
154
|
+
const open = openingParen(body);
|
|
155
|
+
return open > 0
|
|
156
|
+
? locate(body.slice(0, open).trim() || undefined, body.slice(open + 1, -1))
|
|
157
|
+
: locate(undefined, body);
|
|
158
|
+
}
|
|
159
|
+
const atSign = AT_SIGN_FRAME.exec(text);
|
|
160
|
+
if (atSign)
|
|
161
|
+
return locate(atSign[1] || undefined, atSign[2]);
|
|
162
|
+
return { function: undefined, location: text, line: undefined };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Where the parenthesised location that closes a V8 frame opens, or -1 when
|
|
166
|
+
* the frame does not end in one. Matched from the end, because an eval
|
|
167
|
+
* frame's location nests parentheses: `eval (eval at f (file:8:22), …)`.
|
|
168
|
+
*/
|
|
169
|
+
function openingParen(body) {
|
|
170
|
+
if (!body.endsWith(")"))
|
|
171
|
+
return -1;
|
|
172
|
+
let depth = 0;
|
|
173
|
+
for (let i = body.length - 1; i >= 0; i--) {
|
|
174
|
+
if (body[i] === ")")
|
|
175
|
+
depth++;
|
|
176
|
+
else if (body[i] === "(" && --depth === 0)
|
|
177
|
+
return i;
|
|
178
|
+
}
|
|
179
|
+
return -1;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The shapes a location takes, each read as its file and line, first match
|
|
183
|
+
* wins. Code run by `eval` or `new Function` has no file of its own, so its
|
|
184
|
+
* frame is placed at the call that ran it, which is where the source is: V8
|
|
185
|
+
* nests that site inside the location (`eval at f (file:8:22), <anonymous>:1:1`,
|
|
186
|
+
* innermost first-parenthesised for an eval inside an eval), and Firefox appends
|
|
187
|
+
* the code to it (`file line 8 > eval:1:1`).
|
|
188
|
+
*/
|
|
189
|
+
const LOCATIONS = [
|
|
190
|
+
/^eval at [^]*?\(([^()]+):(\d+):\d+\)/,
|
|
191
|
+
/^(.*?) line (\d+) > (?:eval|Function)/,
|
|
192
|
+
/^(.*?):(\d+)(?::\d+)?$/,
|
|
193
|
+
];
|
|
194
|
+
function locate(fn, location) {
|
|
195
|
+
for (const shape of LOCATIONS) {
|
|
196
|
+
const match = shape.exec(location);
|
|
197
|
+
if (match)
|
|
198
|
+
return { function: fn, location: match[1], line: parseInt(match[2], 10) };
|
|
199
|
+
}
|
|
200
|
+
return { function: fn, location, line: undefined };
|
|
201
|
+
}
|
|
202
|
+
export class Traceback {
|
|
203
|
+
/** What was caught, as it was handed in. */
|
|
204
|
+
error;
|
|
205
|
+
maxFrames;
|
|
206
|
+
suppress;
|
|
207
|
+
caught;
|
|
208
|
+
constructor(error, options) {
|
|
209
|
+
this.error = error;
|
|
210
|
+
this.caught = readCaught(error);
|
|
211
|
+
this.maxFrames = options?.maxFrames ?? 100;
|
|
212
|
+
this.suppress = options?.suppress ?? [];
|
|
213
|
+
}
|
|
214
|
+
// [LAW:single-enforcer] The report is `RichText`, so fitting it to the width
|
|
215
|
+
// is the one fitting every text in the library gets: a line wider than the
|
|
216
|
+
// report wraps, and a location wider than the report folds across lines
|
|
217
|
+
// whole, as Rich's does. Written as bare segments, each line was cropped at
|
|
218
|
+
// the edge instead, and a frame lost its file and line to a long path.
|
|
219
|
+
*render(options) {
|
|
220
|
+
const header = new RichText("", { end: "" });
|
|
221
|
+
const { name, message } = this.caught;
|
|
222
|
+
header.append(name, "traceback.exc_type");
|
|
223
|
+
header.append(": ");
|
|
224
|
+
// The message starts after the name, so a value `Pretty` lays out fits the
|
|
225
|
+
// width that is left on that line rather than overrunning it.
|
|
226
|
+
header.append(message({ ...options, maxWidth: Math.max(1, options.maxWidth - header.cellLength) }));
|
|
227
|
+
header.append("\n\n");
|
|
228
|
+
yield* header.render(options);
|
|
229
|
+
// A suppressed frame keeps its place and shows its file and line only: it
|
|
230
|
+
// is the same frame with no function name to show.
|
|
231
|
+
const frames = this.caught.frames.map((frame) => this.suppress.some((s) => frame.location.includes(s)) ? { ...frame, function: undefined } : frame);
|
|
232
|
+
// One entry per row of the frame block, so a report with no frames has no
|
|
233
|
+
// rows: empty text is one line, as in Rich, and a block built as one text
|
|
234
|
+
// drew a blank indented row under every frameless report.
|
|
235
|
+
let entries;
|
|
236
|
+
if (this.maxFrames > 0 && frames.length > this.maxFrames) {
|
|
237
|
+
// Spend the budget exactly: the tail takes `maxFrames - head` rather than
|
|
238
|
+
// a second `head`, so an odd budget shows every frame it counts, and it
|
|
239
|
+
// slices from an index because `slice(-0)` is the whole array.
|
|
240
|
+
const head = Math.floor(this.maxFrames / 2);
|
|
241
|
+
const omitted = frames.length - this.maxFrames;
|
|
242
|
+
entries = [
|
|
243
|
+
...frames.slice(0, head).map(frameEntry),
|
|
244
|
+
new RichText(`... ${omitted} frames omitted ...`, { style: "traceback.text", end: "" }),
|
|
245
|
+
...frames.slice(frames.length - (this.maxFrames - head)).map(frameEntry),
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
entries = frames.map(frameEntry);
|
|
250
|
+
}
|
|
251
|
+
// The frames are one block set in from the report's edge, so a frame that
|
|
252
|
+
// wraps continues under itself instead of at the edge, where it read as a
|
|
253
|
+
// frame of its own. The width is divided as `Padding` divides it; the rows
|
|
254
|
+
// are not padded out on the right, as `Padding`'s are, because trailing
|
|
255
|
+
// blanks on every line of a crash report are bytes nobody asked for.
|
|
256
|
+
const { left, contentWidth } = layoutPadding(options.maxWidth, 2, 0);
|
|
257
|
+
const indent = new Segment(" ".repeat(left));
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
for (const line of Segment.splitLines(entry.render({ ...options, maxWidth: contentWidth }))) {
|
|
260
|
+
yield indent;
|
|
261
|
+
yield* line;
|
|
262
|
+
yield Segment.line();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function frameEntry(frame) {
|
|
268
|
+
const entry = new RichText("", { end: "" });
|
|
269
|
+
if (frame.function) {
|
|
270
|
+
entry.append(frame.function, "bold");
|
|
271
|
+
entry.append(" ");
|
|
272
|
+
}
|
|
273
|
+
entry.append(frame.location, "dim");
|
|
274
|
+
if (frame.line !== undefined) {
|
|
275
|
+
entry.append(":");
|
|
276
|
+
entry.append(String(frame.line), "traceback.offset");
|
|
277
|
+
}
|
|
278
|
+
return entry;
|
|
279
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree — hierarchical view with guide lines.
|
|
3
|
+
*/
|
|
4
|
+
import { Segment } from "../core/segment.js";
|
|
5
|
+
import { Style } from "../core/style.js";
|
|
6
|
+
import { RichText } from "../core/text.js";
|
|
7
|
+
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
8
|
+
export interface TreeOptions {
|
|
9
|
+
expanded?: boolean;
|
|
10
|
+
hideRoot?: boolean;
|
|
11
|
+
guide_style?: string | Style;
|
|
12
|
+
style?: string | Style;
|
|
13
|
+
}
|
|
14
|
+
export declare class Tree implements Renderable, Measurable {
|
|
15
|
+
readonly label: Renderable;
|
|
16
|
+
readonly children: Tree[];
|
|
17
|
+
expanded: boolean;
|
|
18
|
+
readonly hideRoot: boolean;
|
|
19
|
+
readonly guideStyle: string | Style;
|
|
20
|
+
readonly style: string | Style;
|
|
21
|
+
constructor(label: string | RichText | Renderable, options?: TreeOptions);
|
|
22
|
+
add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
|
|
23
|
+
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
24
|
+
/**
|
|
25
|
+
* The rows `render` and `measure` both read, entered in exactly one place so
|
|
26
|
+
* the two cannot start the walk differently. A hidden root takes its own row
|
|
27
|
+
* and the column its children hang from with it, as Rich does — its children
|
|
28
|
+
* stand at the left edge, and its guide style still reaches their guides.
|
|
29
|
+
*/
|
|
30
|
+
private _walk;
|
|
31
|
+
/**
|
|
32
|
+
* The tree walked into the rows it emits, one row per label, guides first.
|
|
33
|
+
*
|
|
34
|
+
* [LAW:one-source-of-truth] The walk that decides which rows exist and what
|
|
35
|
+
* leads them is here and only here. `render` turns each row into segments and
|
|
36
|
+
* `measure` reads each row's width off the same sequence, so the two cannot
|
|
37
|
+
* come to disagree about how many rows there are or how wide the guides on
|
|
38
|
+
* them run — which they would the moment a second walk existed.
|
|
39
|
+
*
|
|
40
|
+
* [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
|
|
41
|
+
* this one recursion: its children get the columns above it, settled, plus a
|
|
42
|
+
* branch of their own, so no depth is drawn by a rule of its own. A node's
|
|
43
|
+
* style and its guide style are handed down the same way, as one more entry
|
|
44
|
+
* on the stack its ancestors built.
|
|
45
|
+
*/
|
|
46
|
+
private _rows;
|
|
47
|
+
/**
|
|
48
|
+
* One row of the tree: its label in whatever width the guides left, each of
|
|
49
|
+
* the label's lines led by the guides.
|
|
50
|
+
*
|
|
51
|
+
* [LAW:single-enforcer] The row's width is divided in exactly one place, here.
|
|
52
|
+
* Before this, each call site emitted its guides and then handed the label
|
|
53
|
+
* `options` unchanged — the label was told it had the whole outer width while
|
|
54
|
+
* the guides had already spent four cells of it, so a tree at maxWidth 5
|
|
55
|
+
* emitted rows of 9. That is the same defect Panel and Table were fixed for,
|
|
56
|
+
* and the same fix: divide the requested width once and let every part read
|
|
57
|
+
* its share off the division. `options` arrives parsed from `render`.
|
|
58
|
+
*
|
|
59
|
+
* [LAW:single-enforcer] Tree also owns its row boundaries here rather than
|
|
60
|
+
* depending on label renderables to invent trailing newlines: the label's
|
|
61
|
+
* output is cut into lines and every line ends where Tree ends it. A label
|
|
62
|
+
* that emits nothing still holds its row, and one that ends in a newline of
|
|
63
|
+
* its own gets the blank row that line is, as in Rich — padded to the label's
|
|
64
|
+
* width when the label is justified.
|
|
65
|
+
*
|
|
66
|
+
* The node's style sits under the label's own, as Rich's `Styled` puts it, so
|
|
67
|
+
* markup in the label still wins; its background alone reaches under the
|
|
68
|
+
* guides, so a row with a background is filled from its first guide to the
|
|
69
|
+
* end of its label.
|
|
70
|
+
*/
|
|
71
|
+
private _renderRow;
|
|
72
|
+
measure(options: RenderOptions): {
|
|
73
|
+
minimum: number;
|
|
74
|
+
maximum: number;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree — hierarchical view with guide lines.
|
|
3
|
+
*/
|
|
4
|
+
import { Segment } from "../core/segment.js";
|
|
5
|
+
import { cellFit, cellLen, asCellCol } from "../core/cells.js";
|
|
6
|
+
import { Style, NULL_STYLE } from "../core/style.js";
|
|
7
|
+
import { embed } from "./embed.js";
|
|
8
|
+
import { drawable, getStyle, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
9
|
+
import { Measurement } from "../core/measure.js";
|
|
10
|
+
/** Every glyph of every set is this wide, so a row's guides are as wide however they are drawn. */
|
|
11
|
+
const GUIDE_CELLS = 4;
|
|
12
|
+
const LIGHT_GUIDES = { fork: "├── ", end: "└── ", continue: "│ ", space: " " };
|
|
13
|
+
const HEAVY_GUIDES = { fork: "┣━━ ", end: "┗━━ ", continue: "┃ ", space: " " };
|
|
14
|
+
const DOUBLE_GUIDES = { fork: "╠══ ", end: "╚══ ", continue: "║ ", space: " " };
|
|
15
|
+
const ASCII_GUIDES = { fork: "+-- ", end: "`-- ", continue: "| ", space: " " };
|
|
16
|
+
/**
|
|
17
|
+
* The guide attributes a glyph set stands for, switched off on the guide itself
|
|
18
|
+
* as Rich's `remove_guide_styles` does: a bold guide is drawn heavy, not bold.
|
|
19
|
+
*/
|
|
20
|
+
const REMOVE_GUIDE_STYLES = new Style({ bold: false, underline2: false });
|
|
21
|
+
const guideGlyphs = (glyphs) => Object.values(glyphs).join("");
|
|
22
|
+
/**
|
|
23
|
+
* The glyphs a column of guide is drawn with, read off its resolved guide style
|
|
24
|
+
* as Rich does: heavy when bold, double when underline2, light otherwise — and
|
|
25
|
+
* ASCII whenever the terminal cannot draw the chosen set.
|
|
26
|
+
*/
|
|
27
|
+
function glyphsFor(options, style) {
|
|
28
|
+
const weight = style.bold === true ? HEAVY_GUIDES : style.underline2 === true ? DOUBLE_GUIDES : LIGHT_GUIDES;
|
|
29
|
+
return drawable(options, weight, ASCII_GUIDES, guideGlyphs);
|
|
30
|
+
}
|
|
31
|
+
/** How wide a label wants to be. */
|
|
32
|
+
function labelWidth(options, label) {
|
|
33
|
+
return Measurement.get(options, label).maximum;
|
|
34
|
+
}
|
|
35
|
+
export class Tree {
|
|
36
|
+
label;
|
|
37
|
+
children;
|
|
38
|
+
expanded;
|
|
39
|
+
hideRoot;
|
|
40
|
+
guideStyle;
|
|
41
|
+
style;
|
|
42
|
+
constructor(label, options) {
|
|
43
|
+
this.label = embed(label);
|
|
44
|
+
this.children = [];
|
|
45
|
+
this.expanded = options?.expanded !== false;
|
|
46
|
+
this.hideRoot = options?.hideRoot ?? false;
|
|
47
|
+
this.guideStyle = options?.guide_style ?? NULL_STYLE;
|
|
48
|
+
this.style = options?.style ?? NULL_STYLE;
|
|
49
|
+
}
|
|
50
|
+
add(label, options) {
|
|
51
|
+
const child = new Tree(label, {
|
|
52
|
+
guide_style: options?.guide_style,
|
|
53
|
+
style: options?.style,
|
|
54
|
+
expanded: options?.expanded,
|
|
55
|
+
});
|
|
56
|
+
this.children.push(child);
|
|
57
|
+
return child;
|
|
58
|
+
}
|
|
59
|
+
*render(rawOptions) {
|
|
60
|
+
const options = withBoundedWidth(rawOptions, this);
|
|
61
|
+
for (const row of this._walk()) {
|
|
62
|
+
yield* this._renderRow(options, row);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The rows `render` and `measure` both read, entered in exactly one place so
|
|
67
|
+
* the two cannot start the walk differently. A hidden root takes its own row
|
|
68
|
+
* and the column its children hang from with it, as Rich does — its children
|
|
69
|
+
* stand at the left edge, and its guide style still reaches their guides.
|
|
70
|
+
*/
|
|
71
|
+
_walk() {
|
|
72
|
+
const rows = [...this._rows([], [], [])];
|
|
73
|
+
return this.hideRoot ? rows.slice(1).map((row) => ({ ...row, guides: row.guides.slice(1) })) : rows;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The tree walked into the rows it emits, one row per label, guides first.
|
|
77
|
+
*
|
|
78
|
+
* [LAW:one-source-of-truth] The walk that decides which rows exist and what
|
|
79
|
+
* leads them is here and only here. `render` turns each row into segments and
|
|
80
|
+
* `measure` reads each row's width off the same sequence, so the two cannot
|
|
81
|
+
* come to disagree about how many rows there are or how wide the guides on
|
|
82
|
+
* them run — which they would the moment a second walk existed.
|
|
83
|
+
*
|
|
84
|
+
* [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
|
|
85
|
+
* this one recursion: its children get the columns above it, settled, plus a
|
|
86
|
+
* branch of their own, so no depth is drawn by a rule of its own. A node's
|
|
87
|
+
* style and its guide style are handed down the same way, as one more entry
|
|
88
|
+
* on the stack its ancestors built.
|
|
89
|
+
*/
|
|
90
|
+
*_rows(guides, inheritedGuideStyles, inheritedStyles) {
|
|
91
|
+
const styles = [...inheritedStyles, this.style];
|
|
92
|
+
yield { guides, label: this.label, styles };
|
|
93
|
+
const children = this.expanded ? this.children : [];
|
|
94
|
+
const guideStyles = [...inheritedGuideStyles, this.guideStyle];
|
|
95
|
+
const above = guides.map((guide) => ({ ...guide, first: guide.rest }));
|
|
96
|
+
for (let i = 0; i < children.length; i++) {
|
|
97
|
+
const branch = i === children.length - 1
|
|
98
|
+
? { first: "end", rest: "space", styles: guideStyles }
|
|
99
|
+
: { first: "fork", rest: "continue", styles: guideStyles };
|
|
100
|
+
yield* children[i]._rows([...above, branch], guideStyles, styles);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* One row of the tree: its label in whatever width the guides left, each of
|
|
105
|
+
* the label's lines led by the guides.
|
|
106
|
+
*
|
|
107
|
+
* [LAW:single-enforcer] The row's width is divided in exactly one place, here.
|
|
108
|
+
* Before this, each call site emitted its guides and then handed the label
|
|
109
|
+
* `options` unchanged — the label was told it had the whole outer width while
|
|
110
|
+
* the guides had already spent four cells of it, so a tree at maxWidth 5
|
|
111
|
+
* emitted rows of 9. That is the same defect Panel and Table were fixed for,
|
|
112
|
+
* and the same fix: divide the requested width once and let every part read
|
|
113
|
+
* its share off the division. `options` arrives parsed from `render`.
|
|
114
|
+
*
|
|
115
|
+
* [LAW:single-enforcer] Tree also owns its row boundaries here rather than
|
|
116
|
+
* depending on label renderables to invent trailing newlines: the label's
|
|
117
|
+
* output is cut into lines and every line ends where Tree ends it. A label
|
|
118
|
+
* that emits nothing still holds its row, and one that ends in a newline of
|
|
119
|
+
* its own gets the blank row that line is, as in Rich — padded to the label's
|
|
120
|
+
* width when the label is justified.
|
|
121
|
+
*
|
|
122
|
+
* The node's style sits under the label's own, as Rich's `Styled` puts it, so
|
|
123
|
+
* markup in the label still wins; its background alone reaches under the
|
|
124
|
+
* guides, so a row with a background is filled from its first guide to the
|
|
125
|
+
* end of its label.
|
|
126
|
+
*/
|
|
127
|
+
*_renderRow(options, row) {
|
|
128
|
+
const resolve = (styles) => Style.combine(styles.map((style) => getStyle(options, style)));
|
|
129
|
+
const style = resolve(row.styles);
|
|
130
|
+
const drawn = row.guides.map((guide) => {
|
|
131
|
+
const guideStyle = resolve(guide.styles);
|
|
132
|
+
return {
|
|
133
|
+
glyphs: glyphsFor(options, guideStyle),
|
|
134
|
+
style: style.backgroundStyle.add(guideStyle).add(REMOVE_GUIDE_STYLES),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
const width = Math.max(0, options.maxWidth - row.guides.length * GUIDE_CELLS);
|
|
138
|
+
// The guides are cropped by `cellFit`; the label is cropped too, so a label
|
|
139
|
+
// that ignores the width it is handed cannot push the row past the offer the
|
|
140
|
+
// guides were fitted into. No highlighter: a label is drawn plain, as
|
|
141
|
+
// Rich's `Tree` hands its labels `highlight=False`.
|
|
142
|
+
const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
|
|
143
|
+
const lines = Segment.splitLines(Segment.applyStyle(Segment.cropLines(row.label.render(labelOptions), width), style));
|
|
144
|
+
if (lines.length === 0)
|
|
145
|
+
lines.push([]);
|
|
146
|
+
for (let i = 0; i < lines.length; i++) {
|
|
147
|
+
let left = options.maxWidth;
|
|
148
|
+
for (let g = 0; g < row.guides.length; g++) {
|
|
149
|
+
const { glyphs, style: guideStyle } = drawn[g];
|
|
150
|
+
const piece = cellFit(glyphs[i === 0 ? row.guides[g].first : row.guides[g].rest], asCellCol(left));
|
|
151
|
+
if (piece.length > 0)
|
|
152
|
+
yield new Segment(piece, guideStyle);
|
|
153
|
+
left -= cellLen(piece);
|
|
154
|
+
}
|
|
155
|
+
yield* lines[i];
|
|
156
|
+
yield Segment.line();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
measure(options) {
|
|
160
|
+
// The ceiling is parsed for the same reason the rows are: an unparsed NaN
|
|
161
|
+
// ceiling yields a range no parent layout can divide.
|
|
162
|
+
const parsed = withCellWidth(options);
|
|
163
|
+
const ceiling = parsed.maxWidth;
|
|
164
|
+
// The widest row the walk emits, not the offer. Reported as the offer, a
|
|
165
|
+
// tree told any parent that asked "I want all of it" — so `Panel` in fit
|
|
166
|
+
// mode drew a 40-cell frame around nine cells of tree, and an unbounded
|
|
167
|
+
// offer came back unbounded.
|
|
168
|
+
let natural = 0;
|
|
169
|
+
for (const row of this._walk()) {
|
|
170
|
+
natural = Math.max(natural, row.guides.length * GUIDE_CELLS + labelWidth(parsed, row.label));
|
|
171
|
+
}
|
|
172
|
+
const maximum = Math.min(natural, ceiling);
|
|
173
|
+
// A row is its guides plus at least one cell of label — but never more than
|
|
174
|
+
// this tree turned out to want, or the floor would sit above its own ceiling.
|
|
175
|
+
return { minimum: Math.min(4, maximum), maximum };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport — a fixed number of rows onto a taller renderable, scrolled by an
|
|
3
|
+
* offset that cannot leave the content.
|
|
4
|
+
*
|
|
5
|
+
* Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
|
|
6
|
+
* when it is given one; otherwise its own configured rows, or the content's
|
|
7
|
+
* full length when it has none, capped by any ceiling. It renders its content
|
|
8
|
+
* with no height, because the content's natural length is the scroll extent,
|
|
9
|
+
* and pads or crops what comes back to its rows — its rows are its own height,
|
|
10
|
+
* not a region someone else will shape.
|
|
11
|
+
*
|
|
12
|
+
* Its width is the width it is given, in both directions: each row is cropped
|
|
13
|
+
* or padded to it, so content that ignores its width cannot spill past the
|
|
14
|
+
* viewport's edge and the scrollbar, drawn in a gutter at that edge, lines up
|
|
15
|
+
* down every row. [LAW:dataflow-not-control-flow] A viewport with no scrollbar
|
|
16
|
+
* has a gutter zero cells wide, and every row takes the same path either way.
|
|
17
|
+
*
|
|
18
|
+
* [LAW:no-ambient-temporal-coupling] Neither half of what an offset is clamped
|
|
19
|
+
* against exists before a render: the rows come from the budget the render is
|
|
20
|
+
* handed, and the content's length from rendering it at the width it is handed.
|
|
21
|
+
* So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
|
|
22
|
+
* are called. Each queues a move, and the next render resolves the queue in
|
|
23
|
+
* call order, clamping after every move, against the rows and length that
|
|
24
|
+
* render found. Calling an operation before the first render, or three of them
|
|
25
|
+
* between two renders, is the same code path as calling one.
|
|
26
|
+
*
|
|
27
|
+
* Every cell of its rows, gutter included, is stamped as its own
|
|
28
|
+
* (`Segment.anchorLines`), around whatever anchors its content drew. So the
|
|
29
|
+
* composed frame names the viewport under any of its cells however deep it is
|
|
30
|
+
* nested, and a cell of a widget inside it names both: that is how the wheel
|
|
31
|
+
* finds the viewport to scroll. `canScrollBy` is how it passes over one that
|
|
32
|
+
* cannot move that way, to the viewport around it.
|
|
33
|
+
*/
|
|
34
|
+
import { Segment } from "../core/segment.js";
|
|
35
|
+
import type { Style } from "../core/style.js";
|
|
36
|
+
import type { Measurable, Renderable, RenderOptions, Scrollable } from "../core/protocol.js";
|
|
37
|
+
/** How one part of a scrollbar draws each row it covers. */
|
|
38
|
+
export interface ScrollbarPart {
|
|
39
|
+
readonly glyph: string;
|
|
40
|
+
/** A style, or a theme name resolved when the viewport renders. */
|
|
41
|
+
readonly style: string | Style;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The look of a scrollbar: the thumb, over the rows of the track that stand for
|
|
45
|
+
* the lines in view, and the track, over the rest. Its gutter is as wide as the
|
|
46
|
+
* wider of the two glyphs.
|
|
47
|
+
*/
|
|
48
|
+
export interface Scrollbar {
|
|
49
|
+
readonly thumb: ScrollbarPart;
|
|
50
|
+
readonly track: ScrollbarPart;
|
|
51
|
+
}
|
|
52
|
+
/** A heavy line for the thumb on a light line for the track. */
|
|
53
|
+
export declare const SCROLLBAR: Scrollbar;
|
|
54
|
+
export interface ViewportOptions {
|
|
55
|
+
/**
|
|
56
|
+
* The rows shown when the viewport is given no region. Absent, it shows the
|
|
57
|
+
* content's full length. A ceiling caps either.
|
|
58
|
+
*/
|
|
59
|
+
rows?: number;
|
|
60
|
+
/** A scrollbar down the right edge. Absent, the content has the full width. */
|
|
61
|
+
scrollbar?: Scrollbar;
|
|
62
|
+
}
|
|
63
|
+
export declare class Viewport implements Renderable, Measurable, Scrollable {
|
|
64
|
+
/**
|
|
65
|
+
* What the viewport shows. Replace it to show new content from the same
|
|
66
|
+
* scroll position — a view rebuilt every frame keeps one `Viewport`.
|
|
67
|
+
*/
|
|
68
|
+
content: Renderable;
|
|
69
|
+
readonly rows: number | undefined;
|
|
70
|
+
readonly scrollbar: Scrollbar;
|
|
71
|
+
private _offset;
|
|
72
|
+
private _extent;
|
|
73
|
+
private _moves;
|
|
74
|
+
constructor(content: Renderable, options?: ViewportOptions);
|
|
75
|
+
/**
|
|
76
|
+
* The first content line the last render showed. A move requested since is
|
|
77
|
+
* not reflected until the next render resolves it.
|
|
78
|
+
*/
|
|
79
|
+
get offset(): number;
|
|
80
|
+
/** Scroll so `line` is the first line shown. */
|
|
81
|
+
scrollTo(line: number): void;
|
|
82
|
+
/** Scroll by `lines`: down when positive, up when negative. */
|
|
83
|
+
scrollBy(lines: number): void;
|
|
84
|
+
/**
|
|
85
|
+
* Whether `scrollBy(lines)` would move the viewport from where the moves
|
|
86
|
+
* queued so far leave it, against the rows and length the last render
|
|
87
|
+
* found.
|
|
88
|
+
*/
|
|
89
|
+
canScrollBy(lines: number): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Scroll the least distance that shows lines `start` up to but not
|
|
92
|
+
* including `end` — lines of the content as it renders at the viewport's
|
|
93
|
+
* width, so an item that wraps spans more than one. A range already in view does not move; one taller than
|
|
94
|
+
* the viewport shows its first line at the top.
|
|
95
|
+
*/
|
|
96
|
+
ensureVisible(start: number, end: number): void;
|
|
97
|
+
/**
|
|
98
|
+
* The width its content renders at, out of `width`: what the scrollbar's
|
|
99
|
+
* gutter leaves. Lines handed to `ensureVisible` are lines at this width.
|
|
100
|
+
*/
|
|
101
|
+
contentWidth(width: number): number;
|
|
102
|
+
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
103
|
+
/**
|
|
104
|
+
* [LAW:one-source-of-truth] Where the queued moves leave the offset against
|
|
105
|
+
* `extent`: what a render commits, and what `canScrollBy` asks from.
|
|
106
|
+
* [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
|
|
107
|
+
* queued moves: content that shrank since the last render pulls it back.
|
|
108
|
+
*/
|
|
109
|
+
private resolve;
|
|
110
|
+
measure(rawOptions: RenderOptions): {
|
|
111
|
+
minimum: number;
|
|
112
|
+
maximum: number;
|
|
113
|
+
};
|
|
114
|
+
}
|