@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,1174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextInput widget — editable text field with cursor.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - single-line (default): Enter submits; newlines in value are rendered
|
|
6
|
+
* as a literal `↵` glyph if they ever appear.
|
|
7
|
+
* - multi-line (`multiline: true`): Enter inserts a `\n`; `value` may
|
|
8
|
+
* contain logical line breaks; Up/Down navigate
|
|
9
|
+
* between logical lines preserving column intent.
|
|
10
|
+
*
|
|
11
|
+
* [LAW:dataflow-not-control-flow] cursor and value are observable data; the
|
|
12
|
+
* keymap dispatches to small `@action` mutators that all flow through the
|
|
13
|
+
* same value/cursor update path. There is no per-key bespoke branch in the
|
|
14
|
+
* render loop or the change-emit path.
|
|
15
|
+
*
|
|
16
|
+
* [LAW:one-source-of-truth] The cursor's logical line — and therefore "what
|
|
17
|
+
* Home/End/Up/Down mean" — is derived from `value` and `cursorPosition` via
|
|
18
|
+
* `_lineStart()` / `_lineEnd()`. Multi-line behavior is the same code path
|
|
19
|
+
* as single-line; with no `\n` in `value`, line bounds collapse to value
|
|
20
|
+
* bounds, so single-line semantics are recovered without a special case.
|
|
21
|
+
*
|
|
22
|
+
* [LAW:types-are-the-program] Three integer spaces coexist: `CellCol` (terminal
|
|
23
|
+
* cell columns), `CodeUnit` (JS UTF-16 indices), and `CodePoint` (code-unit
|
|
24
|
+
* offsets that are additionally on a Unicode code-point boundary — a subtype of
|
|
25
|
+
* `CodeUnit`). `cursorPosition` is `CodePoint`, and stronger: its setter
|
|
26
|
+
* parses any offset to a grapheme-cluster boundary. Visual positions are `CellCol`. The three are never
|
|
27
|
+
* interchangeable — the type system enforces this at every crossing point.
|
|
28
|
+
*
|
|
29
|
+
* [LAW:one-source-of-truth] The cursor moves, deletes and draws one grapheme
|
|
30
|
+
* cluster at a time (`nextGrapheme` / `prevGrapheme`) — the unit `cellLen`
|
|
31
|
+
* measures — so `👍🏽` is one step and one cursor cell, never a glyph and a
|
|
32
|
+
* stray skin-tone swatch.
|
|
33
|
+
*
|
|
34
|
+
* Keymap (readline / emacs compatible):
|
|
35
|
+
*
|
|
36
|
+
* ─── motion ───
|
|
37
|
+
* left | Ctrl+B char left
|
|
38
|
+
* right | Ctrl+F char right
|
|
39
|
+
* up | Ctrl+P line up (preserves preferred column)
|
|
40
|
+
* down | Ctrl+N line down (preserves preferred column)
|
|
41
|
+
* home | Ctrl+A line start
|
|
42
|
+
* end | Ctrl+E line end
|
|
43
|
+
* Ctrl+Home document start
|
|
44
|
+
* Ctrl+End document end
|
|
45
|
+
* Ctrl+Left | Alt+Left | Alt+B word left
|
|
46
|
+
* Ctrl+Right | Alt+Right | Alt+F word right
|
|
47
|
+
*
|
|
48
|
+
* ─── editing ───
|
|
49
|
+
* backspace | Ctrl+H delete char back
|
|
50
|
+
* delete | Ctrl+D delete char forward
|
|
51
|
+
* Ctrl+W | Alt+Backspace delete word back (whitespace-bounded; readline parity)
|
|
52
|
+
* Alt+D delete word forward
|
|
53
|
+
* Ctrl+U kill to line start (stores in kill buffer)
|
|
54
|
+
* Ctrl+K kill to line end (stores in kill buffer)
|
|
55
|
+
* Ctrl+Y yank kill buffer at cursor
|
|
56
|
+
* Ctrl+T transpose chars (swap pre-cursor/at-cursor, advance)
|
|
57
|
+
* enter submit (single-line) or insert `\n` (multiline)
|
|
58
|
+
*
|
|
59
|
+
* Motion and editing primitives are also exposed as public methods
|
|
60
|
+
* (`moveCharLeft`, `killLineForward`, etc.) so a host can bind custom keys
|
|
61
|
+
* or invoke them programmatically.
|
|
62
|
+
*/
|
|
63
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
64
|
+
var useValue = arguments.length > 2;
|
|
65
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
66
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
67
|
+
}
|
|
68
|
+
return useValue ? value : void 0;
|
|
69
|
+
};
|
|
70
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
71
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
72
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
73
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
74
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
75
|
+
var _, done = false;
|
|
76
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
77
|
+
var context = {};
|
|
78
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
79
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
80
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
81
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
82
|
+
if (kind === "accessor") {
|
|
83
|
+
if (result === void 0) continue;
|
|
84
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
85
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
86
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
87
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
88
|
+
}
|
|
89
|
+
else if (_ = accept(result)) {
|
|
90
|
+
if (kind === "field") initializers.unshift(_);
|
|
91
|
+
else descriptor[key] = _;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
95
|
+
done = true;
|
|
96
|
+
};
|
|
97
|
+
import { observable, action, observableRef, runInAction } from "mobx";
|
|
98
|
+
import { Segment } from "../core/segment.js";
|
|
99
|
+
import { Style } from "../core/style.js";
|
|
100
|
+
import { cellLen, setCellSize, splitText, cellStepFrom, cellColToCodeUnitOffset, asCellCol, asCodePoint, asCodeUnit, graphemeBoundary, graphemes, nextGrapheme, prevGrapheme, } from "../core/cells.js";
|
|
101
|
+
import { drawable } from "../core/protocol.js";
|
|
102
|
+
import { ThemedWidget } from "./themed-widget.js";
|
|
103
|
+
import { ink } from "./ink.js";
|
|
104
|
+
/**
|
|
105
|
+
* Character-greedy soft wrap. Breaks at any character once `firstWidth`
|
|
106
|
+
* (or `continuationWidth` for continuation rows) cells are exhausted. The
|
|
107
|
+
* textarea default when a consumer says "wrap, I don't care how" — no syntax
|
|
108
|
+
* awareness, just fits the line to the width.
|
|
109
|
+
*
|
|
110
|
+
* Wide characters (CJK, emoji) are treated as atomic: a char that would
|
|
111
|
+
* straddle the budget is moved to the next row rather than split, and one too
|
|
112
|
+
* wide for the whole budget is force-taken — see `cellStepFrom`.
|
|
113
|
+
*/
|
|
114
|
+
export const charGreedyWrap = (line, { firstWidth, continuationWidth }) => {
|
|
115
|
+
if (line.length === 0)
|
|
116
|
+
return [{ content: "", start: asCodePoint(0) }];
|
|
117
|
+
const rows = [];
|
|
118
|
+
let pos = asCodePoint(0);
|
|
119
|
+
let isFirst = true;
|
|
120
|
+
while (pos < line.length) {
|
|
121
|
+
const cap = isFirst ? firstWidth : continuationWidth;
|
|
122
|
+
if (cap <= 0)
|
|
123
|
+
break;
|
|
124
|
+
const end = cellStepFrom(line, pos, cap);
|
|
125
|
+
rows.push({ content: line.slice(pos, end), start: pos });
|
|
126
|
+
pos = end;
|
|
127
|
+
isFirst = false;
|
|
128
|
+
}
|
|
129
|
+
return rows;
|
|
130
|
+
};
|
|
131
|
+
const MIN_CONTENT_WIDTH = 8;
|
|
132
|
+
// Each glyph and its ASCII stand-in are one code unit and one cell, so the
|
|
133
|
+
// display string maps 1:1 onto `value` and hit-testing needs no render options.
|
|
134
|
+
const NEWLINE_GLYPH = "↵";
|
|
135
|
+
const ASCII_NEWLINE_GLYPH = "~";
|
|
136
|
+
const MASK_GLYPH = "•";
|
|
137
|
+
const ASCII_MASK_GLYPH = "*";
|
|
138
|
+
// Text a key may insert: anything but the C0 controls, DEL and the C1
|
|
139
|
+
// controls, which a terminal reads as escape sequences (U+009B is CSI).
|
|
140
|
+
const PRINTABLE = /^[^\x00-\x1f\x7f-\x9f]+$/u;
|
|
141
|
+
// Word-character regex used by Alt+B/F and Ctrl+Left/Right. Matches the
|
|
142
|
+
// readline default (alphanumeric + underscore), which is what users typing
|
|
143
|
+
// shell commands or code identifiers expect.
|
|
144
|
+
const WORD_CHAR_RE = /[A-Za-z0-9_]/;
|
|
145
|
+
const WHITESPACE_RE = /\s/;
|
|
146
|
+
/** CRLF and lone CR as LF, the line break `value` holds. */
|
|
147
|
+
function toLf(text) {
|
|
148
|
+
return text.replace(/\r\n?/g, "\n");
|
|
149
|
+
}
|
|
150
|
+
function isWordChar(c) {
|
|
151
|
+
return c !== undefined && WORD_CHAR_RE.test(c);
|
|
152
|
+
}
|
|
153
|
+
function isWhitespace(c) {
|
|
154
|
+
return c !== undefined && WHITESPACE_RE.test(c);
|
|
155
|
+
}
|
|
156
|
+
let TextInput = (() => {
|
|
157
|
+
let _classSuper = ThemedWidget;
|
|
158
|
+
let _instanceExtraInitializers = [];
|
|
159
|
+
let __value_decorators;
|
|
160
|
+
let __value_initializers = [];
|
|
161
|
+
let __value_extraInitializers = [];
|
|
162
|
+
let __cursor_decorators;
|
|
163
|
+
let __cursor_initializers = [];
|
|
164
|
+
let __cursor_extraInitializers = [];
|
|
165
|
+
let _placeholder_decorators;
|
|
166
|
+
let _placeholder_initializers = [];
|
|
167
|
+
let _placeholder_extraInitializers = [];
|
|
168
|
+
let _indicatorStyleOverride_decorators;
|
|
169
|
+
let _indicatorStyleOverride_initializers = [];
|
|
170
|
+
let _indicatorStyleOverride_extraInitializers = [];
|
|
171
|
+
let _cursorStyleOverride_decorators;
|
|
172
|
+
let _cursorStyleOverride_initializers = [];
|
|
173
|
+
let _cursorStyleOverride_extraInitializers = [];
|
|
174
|
+
let _contentStyleOverride_decorators;
|
|
175
|
+
let _contentStyleOverride_initializers = [];
|
|
176
|
+
let _contentStyleOverride_extraInitializers = [];
|
|
177
|
+
let _handleKey_decorators;
|
|
178
|
+
let _handleMouse_decorators;
|
|
179
|
+
let _setHovered_decorators;
|
|
180
|
+
let _moveCharLeft_decorators;
|
|
181
|
+
let _moveCharRight_decorators;
|
|
182
|
+
let _moveLineUp_decorators;
|
|
183
|
+
let _moveLineDown_decorators;
|
|
184
|
+
let _moveLineStart_decorators;
|
|
185
|
+
let _moveLineEnd_decorators;
|
|
186
|
+
let _moveDocStart_decorators;
|
|
187
|
+
let _moveDocEnd_decorators;
|
|
188
|
+
let _moveWordLeft_decorators;
|
|
189
|
+
let _moveWordRight_decorators;
|
|
190
|
+
let _deleteCharBack_decorators;
|
|
191
|
+
let _deleteCharForward_decorators;
|
|
192
|
+
let _deleteWordBack_decorators;
|
|
193
|
+
let _deleteWordForward_decorators;
|
|
194
|
+
let _killLineBack_decorators;
|
|
195
|
+
let _killLineForward_decorators;
|
|
196
|
+
let _yank_decorators;
|
|
197
|
+
let _transposeChars_decorators;
|
|
198
|
+
let __insertText_decorators;
|
|
199
|
+
return class TextInput extends _classSuper {
|
|
200
|
+
static {
|
|
201
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
202
|
+
__value_decorators = [observable];
|
|
203
|
+
__cursor_decorators = [observable];
|
|
204
|
+
_placeholder_decorators = [observableRef];
|
|
205
|
+
_indicatorStyleOverride_decorators = [observableRef];
|
|
206
|
+
_cursorStyleOverride_decorators = [observableRef];
|
|
207
|
+
_contentStyleOverride_decorators = [observableRef];
|
|
208
|
+
_handleKey_decorators = [action];
|
|
209
|
+
_handleMouse_decorators = [action];
|
|
210
|
+
_setHovered_decorators = [action];
|
|
211
|
+
_moveCharLeft_decorators = [action];
|
|
212
|
+
_moveCharRight_decorators = [action];
|
|
213
|
+
_moveLineUp_decorators = [action];
|
|
214
|
+
_moveLineDown_decorators = [action];
|
|
215
|
+
_moveLineStart_decorators = [action];
|
|
216
|
+
_moveLineEnd_decorators = [action];
|
|
217
|
+
_moveDocStart_decorators = [action];
|
|
218
|
+
_moveDocEnd_decorators = [action];
|
|
219
|
+
_moveWordLeft_decorators = [action];
|
|
220
|
+
_moveWordRight_decorators = [action];
|
|
221
|
+
_deleteCharBack_decorators = [action];
|
|
222
|
+
_deleteCharForward_decorators = [action];
|
|
223
|
+
_deleteWordBack_decorators = [action];
|
|
224
|
+
_deleteWordForward_decorators = [action];
|
|
225
|
+
_killLineBack_decorators = [action];
|
|
226
|
+
_killLineForward_decorators = [action];
|
|
227
|
+
_yank_decorators = [action];
|
|
228
|
+
_transposeChars_decorators = [action];
|
|
229
|
+
__insertText_decorators = [action];
|
|
230
|
+
__esDecorate(this, null, __value_decorators, { kind: "accessor", name: "_value", static: false, private: false, access: { has: obj => "_value" in obj, get: obj => obj._value, set: (obj, value) => { obj._value = value; } }, metadata: _metadata }, __value_initializers, __value_extraInitializers);
|
|
231
|
+
__esDecorate(this, null, __cursor_decorators, { kind: "accessor", name: "_cursor", static: false, private: false, access: { has: obj => "_cursor" in obj, get: obj => obj._cursor, set: (obj, value) => { obj._cursor = value; } }, metadata: _metadata }, __cursor_initializers, __cursor_extraInitializers);
|
|
232
|
+
__esDecorate(this, null, _placeholder_decorators, { kind: "accessor", name: "placeholder", static: false, private: false, access: { has: obj => "placeholder" in obj, get: obj => obj.placeholder, set: (obj, value) => { obj.placeholder = value; } }, metadata: _metadata }, _placeholder_initializers, _placeholder_extraInitializers);
|
|
233
|
+
__esDecorate(this, null, _indicatorStyleOverride_decorators, { kind: "accessor", name: "indicatorStyleOverride", static: false, private: false, access: { has: obj => "indicatorStyleOverride" in obj, get: obj => obj.indicatorStyleOverride, set: (obj, value) => { obj.indicatorStyleOverride = value; } }, metadata: _metadata }, _indicatorStyleOverride_initializers, _indicatorStyleOverride_extraInitializers);
|
|
234
|
+
__esDecorate(this, null, _cursorStyleOverride_decorators, { kind: "accessor", name: "cursorStyleOverride", static: false, private: false, access: { has: obj => "cursorStyleOverride" in obj, get: obj => obj.cursorStyleOverride, set: (obj, value) => { obj.cursorStyleOverride = value; } }, metadata: _metadata }, _cursorStyleOverride_initializers, _cursorStyleOverride_extraInitializers);
|
|
235
|
+
__esDecorate(this, null, _contentStyleOverride_decorators, { kind: "accessor", name: "contentStyleOverride", static: false, private: false, access: { has: obj => "contentStyleOverride" in obj, get: obj => obj.contentStyleOverride, set: (obj, value) => { obj.contentStyleOverride = value; } }, metadata: _metadata }, _contentStyleOverride_initializers, _contentStyleOverride_extraInitializers);
|
|
236
|
+
__esDecorate(this, null, _handleKey_decorators, { kind: "method", name: "handleKey", static: false, private: false, access: { has: obj => "handleKey" in obj, get: obj => obj.handleKey }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
237
|
+
__esDecorate(this, null, _handleMouse_decorators, { kind: "method", name: "handleMouse", static: false, private: false, access: { has: obj => "handleMouse" in obj, get: obj => obj.handleMouse }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
238
|
+
__esDecorate(this, null, _setHovered_decorators, { kind: "method", name: "setHovered", static: false, private: false, access: { has: obj => "setHovered" in obj, get: obj => obj.setHovered }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
239
|
+
__esDecorate(this, null, _moveCharLeft_decorators, { kind: "method", name: "moveCharLeft", static: false, private: false, access: { has: obj => "moveCharLeft" in obj, get: obj => obj.moveCharLeft }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
240
|
+
__esDecorate(this, null, _moveCharRight_decorators, { kind: "method", name: "moveCharRight", static: false, private: false, access: { has: obj => "moveCharRight" in obj, get: obj => obj.moveCharRight }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
241
|
+
__esDecorate(this, null, _moveLineUp_decorators, { kind: "method", name: "moveLineUp", static: false, private: false, access: { has: obj => "moveLineUp" in obj, get: obj => obj.moveLineUp }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
242
|
+
__esDecorate(this, null, _moveLineDown_decorators, { kind: "method", name: "moveLineDown", static: false, private: false, access: { has: obj => "moveLineDown" in obj, get: obj => obj.moveLineDown }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
243
|
+
__esDecorate(this, null, _moveLineStart_decorators, { kind: "method", name: "moveLineStart", static: false, private: false, access: { has: obj => "moveLineStart" in obj, get: obj => obj.moveLineStart }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
244
|
+
__esDecorate(this, null, _moveLineEnd_decorators, { kind: "method", name: "moveLineEnd", static: false, private: false, access: { has: obj => "moveLineEnd" in obj, get: obj => obj.moveLineEnd }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
245
|
+
__esDecorate(this, null, _moveDocStart_decorators, { kind: "method", name: "moveDocStart", static: false, private: false, access: { has: obj => "moveDocStart" in obj, get: obj => obj.moveDocStart }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
246
|
+
__esDecorate(this, null, _moveDocEnd_decorators, { kind: "method", name: "moveDocEnd", static: false, private: false, access: { has: obj => "moveDocEnd" in obj, get: obj => obj.moveDocEnd }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
247
|
+
__esDecorate(this, null, _moveWordLeft_decorators, { kind: "method", name: "moveWordLeft", static: false, private: false, access: { has: obj => "moveWordLeft" in obj, get: obj => obj.moveWordLeft }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
248
|
+
__esDecorate(this, null, _moveWordRight_decorators, { kind: "method", name: "moveWordRight", static: false, private: false, access: { has: obj => "moveWordRight" in obj, get: obj => obj.moveWordRight }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
249
|
+
__esDecorate(this, null, _deleteCharBack_decorators, { kind: "method", name: "deleteCharBack", static: false, private: false, access: { has: obj => "deleteCharBack" in obj, get: obj => obj.deleteCharBack }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
250
|
+
__esDecorate(this, null, _deleteCharForward_decorators, { kind: "method", name: "deleteCharForward", static: false, private: false, access: { has: obj => "deleteCharForward" in obj, get: obj => obj.deleteCharForward }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
251
|
+
__esDecorate(this, null, _deleteWordBack_decorators, { kind: "method", name: "deleteWordBack", static: false, private: false, access: { has: obj => "deleteWordBack" in obj, get: obj => obj.deleteWordBack }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
252
|
+
__esDecorate(this, null, _deleteWordForward_decorators, { kind: "method", name: "deleteWordForward", static: false, private: false, access: { has: obj => "deleteWordForward" in obj, get: obj => obj.deleteWordForward }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
253
|
+
__esDecorate(this, null, _killLineBack_decorators, { kind: "method", name: "killLineBack", static: false, private: false, access: { has: obj => "killLineBack" in obj, get: obj => obj.killLineBack }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
254
|
+
__esDecorate(this, null, _killLineForward_decorators, { kind: "method", name: "killLineForward", static: false, private: false, access: { has: obj => "killLineForward" in obj, get: obj => obj.killLineForward }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
255
|
+
__esDecorate(this, null, _yank_decorators, { kind: "method", name: "yank", static: false, private: false, access: { has: obj => "yank" in obj, get: obj => obj.yank }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
256
|
+
__esDecorate(this, null, _transposeChars_decorators, { kind: "method", name: "transposeChars", static: false, private: false, access: { has: obj => "transposeChars" in obj, get: obj => obj.transposeChars }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
257
|
+
__esDecorate(this, null, __insertText_decorators, { kind: "method", name: "_insertText", static: false, private: false, access: { has: obj => "_insertText" in obj, get: obj => obj._insertText }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
258
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
259
|
+
}
|
|
260
|
+
id = __runInitializers(this, _instanceExtraInitializers);
|
|
261
|
+
focusable = true;
|
|
262
|
+
#_value_accessor_storage = __runInitializers(this, __value_initializers, void 0);
|
|
263
|
+
get _value() { return this.#_value_accessor_storage; }
|
|
264
|
+
set _value(value) { this.#_value_accessor_storage = value; }
|
|
265
|
+
#_cursor_accessor_storage = (__runInitializers(this, __value_extraInitializers), __runInitializers(this, __cursor_initializers, void 0));
|
|
266
|
+
get _cursor() { return this.#_cursor_accessor_storage; }
|
|
267
|
+
set _cursor(value) { this.#_cursor_accessor_storage = value; }
|
|
268
|
+
// [LAW:single-enforcer] Every write to either half of the pair passes
|
|
269
|
+
// `graphemeBoundary`, so the cursor is a cluster boundary inside `value`
|
|
270
|
+
// whoever set it last — a caller replacing `value`, a click, an insert that
|
|
271
|
+
// grew the cluster before it. A line break is `\n`, as in an HTML
|
|
272
|
+
// textarea's value: `\r\n` is one cluster, and a cursor could not rest at
|
|
273
|
+
// its line's end.
|
|
274
|
+
get value() { return this._value; }
|
|
275
|
+
set value(v) {
|
|
276
|
+
runInAction(() => {
|
|
277
|
+
this._value = toLf(v);
|
|
278
|
+
this._cursor = graphemeBoundary(this._value, this._cursor);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
get cursorPosition() { return this._cursor; }
|
|
282
|
+
set cursorPosition(p) { this._cursor = graphemeBoundary(this._value, p); }
|
|
283
|
+
#placeholder_accessor_storage = (__runInitializers(this, __cursor_extraInitializers), __runInitializers(this, _placeholder_initializers, void 0));
|
|
284
|
+
get placeholder() { return this.#placeholder_accessor_storage; }
|
|
285
|
+
set placeholder(value) { this.#placeholder_accessor_storage = value; }
|
|
286
|
+
_maxLength = __runInitializers(this, _placeholder_extraInitializers);
|
|
287
|
+
_password;
|
|
288
|
+
_multiline;
|
|
289
|
+
_wrap;
|
|
290
|
+
_continuationMarker;
|
|
291
|
+
_markerWidth;
|
|
292
|
+
_maxRows;
|
|
293
|
+
_minRows;
|
|
294
|
+
_scrollIndicator;
|
|
295
|
+
#indicatorStyleOverride_accessor_storage = __runInitializers(this, _indicatorStyleOverride_initializers, void 0);
|
|
296
|
+
get indicatorStyleOverride() { return this.#indicatorStyleOverride_accessor_storage; }
|
|
297
|
+
set indicatorStyleOverride(value) { this.#indicatorStyleOverride_accessor_storage = value; }
|
|
298
|
+
#cursorStyleOverride_accessor_storage = (__runInitializers(this, _indicatorStyleOverride_extraInitializers), __runInitializers(this, _cursorStyleOverride_initializers, void 0));
|
|
299
|
+
get cursorStyleOverride() { return this.#cursorStyleOverride_accessor_storage; }
|
|
300
|
+
set cursorStyleOverride(value) { this.#cursorStyleOverride_accessor_storage = value; }
|
|
301
|
+
#contentStyleOverride_accessor_storage = (__runInitializers(this, _cursorStyleOverride_extraInitializers), __runInitializers(this, _contentStyleOverride_initializers, void 0));
|
|
302
|
+
get contentStyleOverride() { return this.#contentStyleOverride_accessor_storage; }
|
|
303
|
+
set contentStyleOverride(value) { this.#contentStyleOverride_accessor_storage = value; }
|
|
304
|
+
/**
|
|
305
|
+
* Last computed visual-row decomposition. Cached at the end of `render()`
|
|
306
|
+
* so vertical motion (Up/Down) can step row-by-row without re-running the
|
|
307
|
+
* wrap strategy. Null before the first render — vertical motion falls
|
|
308
|
+
* back to logical-line motion in that case.
|
|
309
|
+
*
|
|
310
|
+
* [LAW:dataflow-not-control-flow] One source of truth for "what does Up
|
|
311
|
+
* mean right now": the row table the renderer just produced. No parallel
|
|
312
|
+
* "where would the cursor go" math; both the renderer and the keymap
|
|
313
|
+
* read from the same array.
|
|
314
|
+
*/
|
|
315
|
+
_visualRows = (__runInitializers(this, _contentStyleOverride_extraInitializers), null);
|
|
316
|
+
// [LAW:types-are-the-program] `_preferredColumn` is a cell column (not a
|
|
317
|
+
// code-unit index) — vertical motion navigates the visual surface, not the
|
|
318
|
+
// string's byte layout. Reset to null on every non-vertical motion or edit
|
|
319
|
+
// so the next Up/Down captures a fresh intent. Without this, Down through a
|
|
320
|
+
// short line then back Up would land at the short-line column instead of the
|
|
321
|
+
// original.
|
|
322
|
+
_preferredColumn = null;
|
|
323
|
+
// Single-slot kill buffer used by Ctrl+U / Ctrl+K → Ctrl+Y. Not a full
|
|
324
|
+
// kill ring (no Alt+Y rotation) — sufficient for the common cut/paste
|
|
325
|
+
// pattern that's the readline 90% case.
|
|
326
|
+
_killBuffer = "";
|
|
327
|
+
// [LAW:one-source-of-truth] Top visual row of the scroll viewport.
|
|
328
|
+
// Persisted across renders so the cursor can move *within* the viewport
|
|
329
|
+
// without dragging it along — the viewport scrolls only when the cursor
|
|
330
|
+
// would leave it. Without this, recomputing scroll from cursor position
|
|
331
|
+
// alone pins the cursor to the viewport's bottom edge whenever
|
|
332
|
+
// `cursorRow >= maxRows`, making every subsequent Up/Down scroll in
|
|
333
|
+
// lockstep with the cursor.
|
|
334
|
+
_scrollStart = 0;
|
|
335
|
+
// [LAW:one-source-of-truth] Viewport scroll offset in cells for the
|
|
336
|
+
// single-line renderer. Captured by _renderSingleLine and read by
|
|
337
|
+
// handleMouse so the hit-test uses the same visual origin the user sees.
|
|
338
|
+
_singleLineViewportStart = asCellCol(0);
|
|
339
|
+
_singleLineAsciiOnly = false;
|
|
340
|
+
multiline;
|
|
341
|
+
constructor(options = {}) {
|
|
342
|
+
super(options.theme);
|
|
343
|
+
this.id = options.id ?? `text-input-${Math.random().toString(36).slice(2, 8)}`;
|
|
344
|
+
this._value = toLf(options.value ?? "");
|
|
345
|
+
this.placeholder = options.placeholder ?? "";
|
|
346
|
+
// [LAW:dataflow-not-control-flow] Initial cursor position is data, not
|
|
347
|
+
// a render-time branch. Single-line inputs follow the pre-filled-form
|
|
348
|
+
// convention (cursor at end — typing continues the value). Multiline
|
|
349
|
+
// inputs follow the textarea convention (cursor at start — pre-loaded
|
|
350
|
+
// content is read top-to-bottom, and the viewport's "scroll into view"
|
|
351
|
+
// logic does nothing because row 0 is already inside the initial window).
|
|
352
|
+
this._cursor = graphemeBoundary(this._value, asCodeUnit((options.multiline ?? false) ? 0 : this._value.length));
|
|
353
|
+
this.disabled = options.disabled ?? false;
|
|
354
|
+
this._maxLength = options.maxLength;
|
|
355
|
+
this._password = options.password ?? false;
|
|
356
|
+
this._multiline = options.multiline ?? false;
|
|
357
|
+
this._wrap = options.wrap;
|
|
358
|
+
this._continuationMarker = options.continuationMarker ?? "↳ ";
|
|
359
|
+
this._markerWidth = asCellCol(cellLen(this._continuationMarker));
|
|
360
|
+
this._maxRows = options.maxRows;
|
|
361
|
+
this._minRows = options.minRows;
|
|
362
|
+
this._scrollIndicator = options.scrollIndicator ?? "arrows";
|
|
363
|
+
this.indicatorStyleOverride = options.indicatorStyle;
|
|
364
|
+
this.cursorStyleOverride = options.cursorStyle;
|
|
365
|
+
this.contentStyleOverride = options.contentStyle;
|
|
366
|
+
this.multiline = this._multiline;
|
|
367
|
+
}
|
|
368
|
+
// --- Event handlers ---
|
|
369
|
+
handleKey(event) {
|
|
370
|
+
if (this.disabled)
|
|
371
|
+
return;
|
|
372
|
+
// ─── editing keys (modifier-conscious) ───
|
|
373
|
+
if (event.key === "backspace") {
|
|
374
|
+
if (event.meta || event.ctrl)
|
|
375
|
+
this.deleteWordBack();
|
|
376
|
+
else
|
|
377
|
+
this.deleteCharBack();
|
|
378
|
+
event.stop();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (event.key === "delete") {
|
|
382
|
+
this.deleteCharForward();
|
|
383
|
+
event.stop();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (event.key === "enter") {
|
|
387
|
+
if (this._multiline && !event.ctrl)
|
|
388
|
+
this._insertText("\n");
|
|
389
|
+
else
|
|
390
|
+
this.emitSubmit();
|
|
391
|
+
event.stop();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// Escape is intentionally ignored — text-input does not stop it so
|
|
395
|
+
// higher-level UI (dialogs, modals) can react to it.
|
|
396
|
+
if (event.key === "escape")
|
|
397
|
+
return;
|
|
398
|
+
// ─── unmodified motion ───
|
|
399
|
+
if (!event.ctrl && !event.meta) {
|
|
400
|
+
switch (event.key) {
|
|
401
|
+
case "left":
|
|
402
|
+
this.moveCharLeft();
|
|
403
|
+
event.stop();
|
|
404
|
+
return;
|
|
405
|
+
case "right":
|
|
406
|
+
this.moveCharRight();
|
|
407
|
+
event.stop();
|
|
408
|
+
return;
|
|
409
|
+
case "up":
|
|
410
|
+
this.moveLineUp();
|
|
411
|
+
event.stop();
|
|
412
|
+
return;
|
|
413
|
+
case "down":
|
|
414
|
+
this.moveLineDown();
|
|
415
|
+
event.stop();
|
|
416
|
+
return;
|
|
417
|
+
case "home":
|
|
418
|
+
this.moveLineStart();
|
|
419
|
+
event.stop();
|
|
420
|
+
return;
|
|
421
|
+
case "end":
|
|
422
|
+
this.moveLineEnd();
|
|
423
|
+
event.stop();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// ─── Ctrl-modified motion + readline editing ───
|
|
428
|
+
if (event.ctrl && !event.meta) {
|
|
429
|
+
switch (event.key) {
|
|
430
|
+
case "left":
|
|
431
|
+
this.moveWordLeft();
|
|
432
|
+
event.stop();
|
|
433
|
+
return;
|
|
434
|
+
case "right":
|
|
435
|
+
this.moveWordRight();
|
|
436
|
+
event.stop();
|
|
437
|
+
return;
|
|
438
|
+
case "home":
|
|
439
|
+
this.moveDocStart();
|
|
440
|
+
event.stop();
|
|
441
|
+
return;
|
|
442
|
+
case "end":
|
|
443
|
+
this.moveDocEnd();
|
|
444
|
+
event.stop();
|
|
445
|
+
return;
|
|
446
|
+
case "a":
|
|
447
|
+
this.moveLineStart();
|
|
448
|
+
event.stop();
|
|
449
|
+
return;
|
|
450
|
+
case "e":
|
|
451
|
+
this.moveLineEnd();
|
|
452
|
+
event.stop();
|
|
453
|
+
return;
|
|
454
|
+
case "b":
|
|
455
|
+
this.moveCharLeft();
|
|
456
|
+
event.stop();
|
|
457
|
+
return;
|
|
458
|
+
case "f":
|
|
459
|
+
this.moveCharRight();
|
|
460
|
+
event.stop();
|
|
461
|
+
return;
|
|
462
|
+
case "p":
|
|
463
|
+
this.moveLineUp();
|
|
464
|
+
event.stop();
|
|
465
|
+
return;
|
|
466
|
+
case "n":
|
|
467
|
+
this.moveLineDown();
|
|
468
|
+
event.stop();
|
|
469
|
+
return;
|
|
470
|
+
case "d":
|
|
471
|
+
this.deleteCharForward();
|
|
472
|
+
event.stop();
|
|
473
|
+
return;
|
|
474
|
+
case "h":
|
|
475
|
+
this.deleteCharBack();
|
|
476
|
+
event.stop();
|
|
477
|
+
return;
|
|
478
|
+
case "w":
|
|
479
|
+
this.deleteWordBack();
|
|
480
|
+
event.stop();
|
|
481
|
+
return;
|
|
482
|
+
case "u":
|
|
483
|
+
this.killLineBack();
|
|
484
|
+
event.stop();
|
|
485
|
+
return;
|
|
486
|
+
case "k":
|
|
487
|
+
this.killLineForward();
|
|
488
|
+
event.stop();
|
|
489
|
+
return;
|
|
490
|
+
case "y":
|
|
491
|
+
this.yank();
|
|
492
|
+
event.stop();
|
|
493
|
+
return;
|
|
494
|
+
case "t":
|
|
495
|
+
this.transposeChars();
|
|
496
|
+
event.stop();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// ─── Alt-modified (meta) motion + editing ───
|
|
501
|
+
if (event.meta && !event.ctrl) {
|
|
502
|
+
switch (event.key) {
|
|
503
|
+
case "left":
|
|
504
|
+
this.moveWordLeft();
|
|
505
|
+
event.stop();
|
|
506
|
+
return;
|
|
507
|
+
case "right":
|
|
508
|
+
this.moveWordRight();
|
|
509
|
+
event.stop();
|
|
510
|
+
return;
|
|
511
|
+
case "b":
|
|
512
|
+
this.moveWordLeft();
|
|
513
|
+
event.stop();
|
|
514
|
+
return;
|
|
515
|
+
case "f":
|
|
516
|
+
this.moveWordRight();
|
|
517
|
+
event.stop();
|
|
518
|
+
return;
|
|
519
|
+
case "d":
|
|
520
|
+
this.deleteWordForward();
|
|
521
|
+
event.stop();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// ─── printable insertion ───
|
|
526
|
+
// A non-control `event.character` with no command modifiers is text to
|
|
527
|
+
// insert. Ctrl/meta-modified keys produce empty `character` from the
|
|
528
|
+
// router (see event-router.ts) so they never reach this branch. The router
|
|
529
|
+
// sends one code point per key, so `👍🏽` arrives as `👍` then `🏽`, and an
|
|
530
|
+
// astral code point is two UTF-16 units — a `.length === 1` test drops it.
|
|
531
|
+
if (PRINTABLE.test(event.character) && !event.ctrl && !event.meta) {
|
|
532
|
+
this._insertText(event.character);
|
|
533
|
+
event.stop();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
handleMouse(event) {
|
|
537
|
+
if (this.disabled)
|
|
538
|
+
return;
|
|
539
|
+
if (event.type !== "mouse_down")
|
|
540
|
+
return;
|
|
541
|
+
if (this._multiline) {
|
|
542
|
+
// Multiline has no brackets; rows may have a leading continuation marker.
|
|
543
|
+
// Use event.y to pick the visual row, then project event.x into its content.
|
|
544
|
+
const rows = this._visualRows;
|
|
545
|
+
if (!rows || rows.length === 0)
|
|
546
|
+
return;
|
|
547
|
+
const rawRowIdx = this._scrollStart + event.y;
|
|
548
|
+
// Clicks on minRows-padded empty rows (beyond the last real row) snap to end.
|
|
549
|
+
if (rawRowIdx >= rows.length) {
|
|
550
|
+
this.cursorPosition = asCodeUnit(this.value.length);
|
|
551
|
+
this._preferredColumn = null;
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const rowIdx = Math.max(0, rawRowIdx);
|
|
555
|
+
const row = rows[rowIdx];
|
|
556
|
+
// Continuation rows start after the marker; non-continuation rows start at column 0.
|
|
557
|
+
const contentXOff = asCellCol(row.isContinuation ? this._markerWidth : 0);
|
|
558
|
+
const relX = asCellCol(Math.max(0, event.x - contentXOff));
|
|
559
|
+
this.cursorPosition = asCodeUnit(row.valueStart + cellColToCodeUnitOffset(row.content, relX));
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
// [LAW:types-are-the-program] relX is a cell-column offset into the full
|
|
563
|
+
// display string. Add _singleLineViewportStart (the scroll offset captured
|
|
564
|
+
// by _renderSingleLine) so a click on a scrolled viewport maps to the
|
|
565
|
+
// correct code-unit in the underlying value.
|
|
566
|
+
const relX = asCellCol(Math.max(0, event.x - 1) + this._singleLineViewportStart);
|
|
567
|
+
// The glyphs of `value` whose shown cells lie wholly left of the click,
|
|
568
|
+
// shown as the last render showed them.
|
|
569
|
+
const drawn = { asciiOnly: this._singleLineAsciiOnly };
|
|
570
|
+
let at = 0;
|
|
571
|
+
let col = 0;
|
|
572
|
+
for (const g of graphemes(this.value)) {
|
|
573
|
+
const w = cellLen(this._shown(g, drawn));
|
|
574
|
+
if (col + w > relX)
|
|
575
|
+
break;
|
|
576
|
+
col += w;
|
|
577
|
+
at += g.length;
|
|
578
|
+
}
|
|
579
|
+
this.cursorPosition = asCodeUnit(at);
|
|
580
|
+
}
|
|
581
|
+
this._preferredColumn = null;
|
|
582
|
+
}
|
|
583
|
+
// --- Hover mutator (router fast-path) ---
|
|
584
|
+
setHovered(value) { this.hovered = value; }
|
|
585
|
+
// ─── Public motion primitives ───────────────────────────────────────────
|
|
586
|
+
moveCharLeft() {
|
|
587
|
+
this.cursorPosition = prevGrapheme(this.value, this.cursorPosition);
|
|
588
|
+
this._preferredColumn = null;
|
|
589
|
+
}
|
|
590
|
+
moveCharRight() {
|
|
591
|
+
this.cursorPosition = nextGrapheme(this.value, this.cursorPosition);
|
|
592
|
+
this._preferredColumn = null;
|
|
593
|
+
}
|
|
594
|
+
moveLineUp() {
|
|
595
|
+
// Prefer the on-screen visual decomposition when it exists — wraps,
|
|
596
|
+
// marker offsets, and scroll alignment are all already accounted for.
|
|
597
|
+
// Fall back to logical-line motion only when no render has occurred yet
|
|
598
|
+
// (e.g. cursor motion before mount).
|
|
599
|
+
if (this._visualRows !== null && this._visualRows.length > 1) {
|
|
600
|
+
const rowIdx = this._cursorVisualRow();
|
|
601
|
+
if (rowIdx === 0)
|
|
602
|
+
return;
|
|
603
|
+
const col = this._preferredColumn ?? this._cursorVisualCol();
|
|
604
|
+
const target = this._visualRows[rowIdx - 1];
|
|
605
|
+
this.cursorPosition = asCodeUnit(target.valueStart + this._clampColForRow(col, rowIdx - 1));
|
|
606
|
+
this._preferredColumn = col;
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const lineStart = this._lineStart();
|
|
610
|
+
if (lineStart === 0)
|
|
611
|
+
return;
|
|
612
|
+
// Fallback: no render cache yet; use cell-space arithmetic for wide-char correctness.
|
|
613
|
+
const col = this._preferredColumn ?? asCellCol(cellLen(this.value.slice(lineStart, this.cursorPosition)));
|
|
614
|
+
const prevLineEnd = lineStart - 1;
|
|
615
|
+
let prevLineStart = prevLineEnd;
|
|
616
|
+
while (prevLineStart > 0 && this.value[prevLineStart - 1] !== "\n")
|
|
617
|
+
prevLineStart--;
|
|
618
|
+
const prevLineContent = this.value.slice(prevLineStart, prevLineEnd);
|
|
619
|
+
this.cursorPosition = asCodeUnit(prevLineStart + cellColToCodeUnitOffset(prevLineContent, asCellCol(Math.min(col, cellLen(prevLineContent)))));
|
|
620
|
+
this._preferredColumn = col;
|
|
621
|
+
}
|
|
622
|
+
moveLineDown() {
|
|
623
|
+
if (this._visualRows !== null && this._visualRows.length > 1) {
|
|
624
|
+
const rowIdx = this._cursorVisualRow();
|
|
625
|
+
if (rowIdx === this._visualRows.length - 1)
|
|
626
|
+
return;
|
|
627
|
+
const col = this._preferredColumn ?? this._cursorVisualCol();
|
|
628
|
+
this.cursorPosition = asCodeUnit(this._visualRows[rowIdx + 1].valueStart + this._clampColForRow(col, rowIdx + 1));
|
|
629
|
+
this._preferredColumn = col;
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const lineEnd = this._lineEnd();
|
|
633
|
+
if (lineEnd === this.value.length)
|
|
634
|
+
return;
|
|
635
|
+
const lineStart = this._lineStart();
|
|
636
|
+
// Fallback: no render cache yet; use cell-space arithmetic for wide-char correctness.
|
|
637
|
+
const col = this._preferredColumn ?? asCellCol(cellLen(this.value.slice(lineStart, this.cursorPosition)));
|
|
638
|
+
const nextLineStart = lineEnd + 1;
|
|
639
|
+
let nextLineEnd = nextLineStart;
|
|
640
|
+
while (nextLineEnd < this.value.length && this.value[nextLineEnd] !== "\n")
|
|
641
|
+
nextLineEnd++;
|
|
642
|
+
const nextLineContent = this.value.slice(nextLineStart, nextLineEnd);
|
|
643
|
+
this.cursorPosition = asCodeUnit(nextLineStart + cellColToCodeUnitOffset(nextLineContent, asCellCol(Math.min(col, cellLen(nextLineContent)))));
|
|
644
|
+
this._preferredColumn = col;
|
|
645
|
+
}
|
|
646
|
+
// Clamp `col` (a cell column) to the target visual row's content, returning
|
|
647
|
+
// the code-unit offset within that row. When the row IS followed by a
|
|
648
|
+
// continuation of the same logical line, we clamp to the last valid
|
|
649
|
+
// cluster boundary strictly inside the row (which may be more than
|
|
650
|
+
// one cell before the visual end when the last glyph is wide) so the
|
|
651
|
+
// cursor stays strictly inside the target row — landing at the boundary
|
|
652
|
+
// would cause `_cursorVisualRow` to resolve to the *later* row, making
|
|
653
|
+
// every subsequent Up/Down stick at the boundary.
|
|
654
|
+
//
|
|
655
|
+
// For rows NOT followed by a continuation (last wrap row, or any unwrapped
|
|
656
|
+
// logical line), the end-of-line position is valid — there is no adjacent
|
|
657
|
+
// continuation row to collide with.
|
|
658
|
+
_clampColForRow(col, targetIdx) {
|
|
659
|
+
const rows = this._visualRows;
|
|
660
|
+
const target = rows[targetIdx];
|
|
661
|
+
const nextIsContinuation = targetIdx + 1 < rows.length && rows[targetIdx + 1].isContinuation;
|
|
662
|
+
const contentCellWidth = asCellCol(cellLen(target.content));
|
|
663
|
+
const capCells = nextIsContinuation
|
|
664
|
+
? asCellCol(Math.max(0, contentCellWidth - 1))
|
|
665
|
+
: contentCellWidth;
|
|
666
|
+
return cellColToCodeUnitOffset(target.content, asCellCol(Math.min(col, capCells)));
|
|
667
|
+
}
|
|
668
|
+
// Locate which cached visual row the cursor sits on. Returns the row index
|
|
669
|
+
// for use in `moveLineUp`/`moveLineDown`; caller must guard `_visualRows`.
|
|
670
|
+
_cursorVisualRow() {
|
|
671
|
+
const rows = this._visualRows;
|
|
672
|
+
let idx = 0;
|
|
673
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
674
|
+
if (rows[i].valueStart <= this.cursorPosition) {
|
|
675
|
+
idx = i;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return idx;
|
|
680
|
+
}
|
|
681
|
+
// Returns the cursor's cell column within its current visual row.
|
|
682
|
+
_cursorVisualCol() {
|
|
683
|
+
const rows = this._visualRows;
|
|
684
|
+
const idx = this._cursorVisualRow();
|
|
685
|
+
const row = rows[idx];
|
|
686
|
+
return asCellCol(cellLen(row.content.slice(0, this.cursorPosition - row.valueStart)));
|
|
687
|
+
}
|
|
688
|
+
moveLineStart() {
|
|
689
|
+
this.cursorPosition = this._lineStart();
|
|
690
|
+
this._preferredColumn = null;
|
|
691
|
+
}
|
|
692
|
+
moveLineEnd() {
|
|
693
|
+
this.cursorPosition = this._lineEnd();
|
|
694
|
+
this._preferredColumn = null;
|
|
695
|
+
}
|
|
696
|
+
moveDocStart() {
|
|
697
|
+
this.cursorPosition = asCodeUnit(0);
|
|
698
|
+
this._preferredColumn = null;
|
|
699
|
+
}
|
|
700
|
+
moveDocEnd() {
|
|
701
|
+
this.cursorPosition = asCodeUnit(this.value.length);
|
|
702
|
+
this._preferredColumn = null;
|
|
703
|
+
}
|
|
704
|
+
moveWordLeft() {
|
|
705
|
+
let p = this.cursorPosition;
|
|
706
|
+
while (p > 0) {
|
|
707
|
+
const prev = prevGrapheme(this.value, p);
|
|
708
|
+
if (isWordChar(this.value.slice(prev, p)))
|
|
709
|
+
break;
|
|
710
|
+
p = prev;
|
|
711
|
+
}
|
|
712
|
+
while (p > 0) {
|
|
713
|
+
const prev = prevGrapheme(this.value, p);
|
|
714
|
+
if (!isWordChar(this.value.slice(prev, p)))
|
|
715
|
+
break;
|
|
716
|
+
p = prev;
|
|
717
|
+
}
|
|
718
|
+
this.cursorPosition = p;
|
|
719
|
+
this._preferredColumn = null;
|
|
720
|
+
}
|
|
721
|
+
moveWordRight() {
|
|
722
|
+
let p = this.cursorPosition;
|
|
723
|
+
while (p < this.value.length) {
|
|
724
|
+
const next = nextGrapheme(this.value, p);
|
|
725
|
+
if (isWordChar(this.value.slice(p, next)))
|
|
726
|
+
break;
|
|
727
|
+
p = next;
|
|
728
|
+
}
|
|
729
|
+
while (p < this.value.length) {
|
|
730
|
+
const next = nextGrapheme(this.value, p);
|
|
731
|
+
if (!isWordChar(this.value.slice(p, next)))
|
|
732
|
+
break;
|
|
733
|
+
p = next;
|
|
734
|
+
}
|
|
735
|
+
this.cursorPosition = p;
|
|
736
|
+
this._preferredColumn = null;
|
|
737
|
+
}
|
|
738
|
+
// ─── Public editing primitives ──────────────────────────────────────────
|
|
739
|
+
deleteCharBack() {
|
|
740
|
+
if (this.cursorPosition === 0)
|
|
741
|
+
return;
|
|
742
|
+
const newPos = prevGrapheme(this.value, this.cursorPosition);
|
|
743
|
+
this.value = this.value.slice(0, newPos) + this.value.slice(this.cursorPosition);
|
|
744
|
+
this.cursorPosition = newPos;
|
|
745
|
+
this._preferredColumn = null;
|
|
746
|
+
this.emitChange();
|
|
747
|
+
}
|
|
748
|
+
deleteCharForward() {
|
|
749
|
+
if (this.cursorPosition >= this.value.length)
|
|
750
|
+
return;
|
|
751
|
+
const nextPos = nextGrapheme(this.value, this.cursorPosition);
|
|
752
|
+
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(nextPos);
|
|
753
|
+
this._preferredColumn = null;
|
|
754
|
+
this.emitChange();
|
|
755
|
+
}
|
|
756
|
+
deleteWordBack() {
|
|
757
|
+
// Readline `unix-word-rubout` semantics: delete back to nearest whitespace,
|
|
758
|
+
// skipping trailing whitespace first so successive Ctrl+W at "foo bar |"
|
|
759
|
+
// → "foo |" → "|" rather than getting stuck on the trailing space.
|
|
760
|
+
let p = this.cursorPosition;
|
|
761
|
+
while (p > 0) {
|
|
762
|
+
const prev = prevGrapheme(this.value, p);
|
|
763
|
+
if (!isWhitespace(this.value.slice(prev, p)))
|
|
764
|
+
break;
|
|
765
|
+
p = prev;
|
|
766
|
+
}
|
|
767
|
+
while (p > 0) {
|
|
768
|
+
const prev = prevGrapheme(this.value, p);
|
|
769
|
+
if (isWhitespace(this.value.slice(prev, p)))
|
|
770
|
+
break;
|
|
771
|
+
p = prev;
|
|
772
|
+
}
|
|
773
|
+
if (p === this.cursorPosition)
|
|
774
|
+
return;
|
|
775
|
+
this._killBuffer = this.value.slice(p, this.cursorPosition);
|
|
776
|
+
this.value = this.value.slice(0, p) + this.value.slice(this.cursorPosition);
|
|
777
|
+
this.cursorPosition = p;
|
|
778
|
+
this._preferredColumn = null;
|
|
779
|
+
this.emitChange();
|
|
780
|
+
}
|
|
781
|
+
deleteWordForward() {
|
|
782
|
+
let p = this.cursorPosition;
|
|
783
|
+
while (p < this.value.length) {
|
|
784
|
+
const next = nextGrapheme(this.value, p);
|
|
785
|
+
if (isWordChar(this.value.slice(p, next)))
|
|
786
|
+
break;
|
|
787
|
+
p = next;
|
|
788
|
+
}
|
|
789
|
+
while (p < this.value.length) {
|
|
790
|
+
const next = nextGrapheme(this.value, p);
|
|
791
|
+
if (!isWordChar(this.value.slice(p, next)))
|
|
792
|
+
break;
|
|
793
|
+
p = next;
|
|
794
|
+
}
|
|
795
|
+
if (p === this.cursorPosition)
|
|
796
|
+
return;
|
|
797
|
+
this._killBuffer = this.value.slice(this.cursorPosition, p);
|
|
798
|
+
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(p);
|
|
799
|
+
this._preferredColumn = null;
|
|
800
|
+
this.emitChange();
|
|
801
|
+
}
|
|
802
|
+
killLineBack() {
|
|
803
|
+
const start = this._lineStart();
|
|
804
|
+
if (start === this.cursorPosition)
|
|
805
|
+
return;
|
|
806
|
+
this._killBuffer = this.value.slice(start, this.cursorPosition);
|
|
807
|
+
this.value = this.value.slice(0, start) + this.value.slice(this.cursorPosition);
|
|
808
|
+
this.cursorPosition = start;
|
|
809
|
+
this._preferredColumn = null;
|
|
810
|
+
this.emitChange();
|
|
811
|
+
}
|
|
812
|
+
killLineForward() {
|
|
813
|
+
const end = this._lineEnd();
|
|
814
|
+
if (end > this.cursorPosition) {
|
|
815
|
+
this._killBuffer = this.value.slice(this.cursorPosition, end);
|
|
816
|
+
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(end);
|
|
817
|
+
this._preferredColumn = null;
|
|
818
|
+
this.emitChange();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
// Already at line end → kill the trailing `\n` (join next line).
|
|
822
|
+
if (this.cursorPosition < this.value.length) {
|
|
823
|
+
this._killBuffer = "\n";
|
|
824
|
+
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(this.cursorPosition + 1);
|
|
825
|
+
this._preferredColumn = null;
|
|
826
|
+
this.emitChange();
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
yank() {
|
|
830
|
+
if (this._killBuffer.length === 0)
|
|
831
|
+
return;
|
|
832
|
+
this._insertText(this._killBuffer);
|
|
833
|
+
}
|
|
834
|
+
transposeChars() {
|
|
835
|
+
// Readline `transpose-chars`: swap the char before the cursor with the
|
|
836
|
+
// char at the cursor and advance. At end-of-value, swap the trailing
|
|
837
|
+
// two chars without advancing. At position 0, no-op.
|
|
838
|
+
if (this.cursorPosition === 0)
|
|
839
|
+
return;
|
|
840
|
+
const p = this.cursorPosition;
|
|
841
|
+
if (p === this.value.length) {
|
|
842
|
+
// End-of-value: swap the trailing two glyphs; cursor stays. One glyph
|
|
843
|
+
// has nothing to swap with.
|
|
844
|
+
const cp2 = prevGrapheme(this.value, p);
|
|
845
|
+
if (cp2 === 0)
|
|
846
|
+
return;
|
|
847
|
+
const cp1 = prevGrapheme(this.value, cp2);
|
|
848
|
+
this.value = this.value.slice(0, cp1) + this.value.slice(cp2) + this.value.slice(cp1, cp2);
|
|
849
|
+
this._preferredColumn = null;
|
|
850
|
+
this.emitChange();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
// Normal case: the whole glyphs before and at the cursor.
|
|
854
|
+
const cpBefore = prevGrapheme(this.value, p);
|
|
855
|
+
const cpAfter = nextGrapheme(this.value, p);
|
|
856
|
+
const charBefore = this.value.slice(cpBefore, p);
|
|
857
|
+
const charAt = this.value.slice(p, cpAfter);
|
|
858
|
+
this.value = this.value.slice(0, cpBefore) + charAt + charBefore + this.value.slice(cpAfter);
|
|
859
|
+
this.cursorPosition = cpAfter;
|
|
860
|
+
this._preferredColumn = null;
|
|
861
|
+
this.emitChange();
|
|
862
|
+
}
|
|
863
|
+
// ─── Internal helpers ───────────────────────────────────────────────────
|
|
864
|
+
_lineStart() {
|
|
865
|
+
let p = this.cursorPosition;
|
|
866
|
+
while (p > 0 && this.value[p - 1] !== "\n")
|
|
867
|
+
p--;
|
|
868
|
+
return asCodePoint(p);
|
|
869
|
+
}
|
|
870
|
+
_lineEnd() {
|
|
871
|
+
let p = this.cursorPosition;
|
|
872
|
+
while (p < this.value.length && this.value[p] !== "\n")
|
|
873
|
+
p++;
|
|
874
|
+
return asCodePoint(p);
|
|
875
|
+
}
|
|
876
|
+
_insertText(text) {
|
|
877
|
+
let toInsert = text;
|
|
878
|
+
if (this._maxLength !== undefined) {
|
|
879
|
+
const room = this._maxLength - this.value.length;
|
|
880
|
+
// Cut between glyphs: the longest run of whole clusters within `room`.
|
|
881
|
+
let fit = 0;
|
|
882
|
+
for (const g of graphemes(toInsert)) {
|
|
883
|
+
if (fit + g.length > room)
|
|
884
|
+
break;
|
|
885
|
+
fit += g.length;
|
|
886
|
+
}
|
|
887
|
+
toInsert = toInsert.slice(0, fit);
|
|
888
|
+
}
|
|
889
|
+
if (toInsert.length === 0)
|
|
890
|
+
return;
|
|
891
|
+
// The end is taken before the write: the value setter has already moved
|
|
892
|
+
// the cursor past a cluster the insert grew.
|
|
893
|
+
const end = asCodeUnit(this.cursorPosition + toInsert.length);
|
|
894
|
+
this.value =
|
|
895
|
+
this.value.slice(0, this.cursorPosition) +
|
|
896
|
+
toInsert +
|
|
897
|
+
this.value.slice(this.cursorPosition);
|
|
898
|
+
this.cursorPosition = end;
|
|
899
|
+
this._preferredColumn = null;
|
|
900
|
+
this.emitChange();
|
|
901
|
+
}
|
|
902
|
+
// --- Rendering ---
|
|
903
|
+
// [LAW:one-source-of-truth] The style a value is drawn in is also what a
|
|
904
|
+
// short row is padded with, so a caller's `contentStyle` fills the row.
|
|
905
|
+
get ground() {
|
|
906
|
+
return this.disabled
|
|
907
|
+
? new Style(this.disabledInk)
|
|
908
|
+
: this.contentStyleOverride ?? super.ground;
|
|
909
|
+
}
|
|
910
|
+
draw(options) {
|
|
911
|
+
if (this._multiline)
|
|
912
|
+
return this._renderMultiline(options);
|
|
913
|
+
return this._renderSingleLine(options);
|
|
914
|
+
}
|
|
915
|
+
// [LAW:dataflow-not-control-flow] How a stretch of `value` is shown in one
|
|
916
|
+
// line derives from mode: one mask cell per glyph, or `\n` as a visible
|
|
917
|
+
// glyph. The render and the click hit-test both read it.
|
|
918
|
+
_shown(text, options) {
|
|
919
|
+
return this._password
|
|
920
|
+
? drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH).repeat(graphemes(text).length)
|
|
921
|
+
: text.replace(/\n/g, drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH));
|
|
922
|
+
}
|
|
923
|
+
_renderSingleLine(options) {
|
|
924
|
+
const showPlaceholder = this.focused && this.value.length === 0 && this.placeholder.length > 0;
|
|
925
|
+
// The cursor's column is the width of the shown text before it, whatever
|
|
926
|
+
// the mode — a placeholder is shown only over an empty value.
|
|
927
|
+
this._singleLineAsciiOnly = options.asciiOnly ?? false;
|
|
928
|
+
const shown = (text) => this._shown(text, options);
|
|
929
|
+
const rawDisplay = showPlaceholder ? this.placeholder : shown(this.value);
|
|
930
|
+
const rawCellWidth = asCellCol(cellLen(rawDisplay));
|
|
931
|
+
const cursorCellCol = asCellCol(cellLen(shown(this.value.slice(0, this.cursorPosition))));
|
|
932
|
+
const maxAvailable = asCellCol(Math.max(MIN_CONTENT_WIDTH, options.maxWidth - 2));
|
|
933
|
+
const desiredWidth = asCellCol(Math.max(MIN_CONTENT_WIDTH, rawCellWidth, cursorCellCol + 1));
|
|
934
|
+
const contentWidth = asCellCol(Math.min(maxAvailable, desiredWidth));
|
|
935
|
+
// Slide viewport (in cells) to keep cursor visible when content overflows.
|
|
936
|
+
const startCell = asCellCol(Math.max(0, Math.min(rawCellWidth - contentWidth, cursorCellCol - contentWidth + 1)));
|
|
937
|
+
const [, afterStart] = splitText(rawDisplay, startCell);
|
|
938
|
+
// splitText snaps backward when startCell falls mid-wide-char, so afterStart
|
|
939
|
+
// may start 1 cell earlier than requested. actualStartCell is the real offset.
|
|
940
|
+
const actualStartCell = asCellCol(rawCellWidth - cellLen(afterStart));
|
|
941
|
+
this._singleLineViewportStart = actualStartCell;
|
|
942
|
+
const [visible] = splitText(afterStart, contentWidth);
|
|
943
|
+
const display = setCellSize(visible, contentWidth);
|
|
944
|
+
const cursorDisplayCellCol = asCellCol(cursorCellCol - actualStartCell);
|
|
945
|
+
const bracketStyle = this.disabled
|
|
946
|
+
? new Style(this.disabledInk)
|
|
947
|
+
: new Style(ink(this.theme, "foreground", "background"));
|
|
948
|
+
const contentStyle = showPlaceholder && !this.disabled
|
|
949
|
+
? new Style({ ...ink(this.theme, "foreground", "background"), dim: true })
|
|
950
|
+
: this.ground;
|
|
951
|
+
const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
|
|
952
|
+
const segments = [new Segment("[", bracketStyle)];
|
|
953
|
+
if (this.focused && !this.disabled && cursorDisplayCellCol >= 0 && cursorDisplayCellCol < contentWidth) {
|
|
954
|
+
const [before, rest] = splitText(display, cursorDisplayCellCol);
|
|
955
|
+
const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
|
|
956
|
+
const at = firstCh || " ";
|
|
957
|
+
const after = rest.slice(firstCh.length);
|
|
958
|
+
if (before.length > 0)
|
|
959
|
+
segments.push(new Segment(before, contentStyle));
|
|
960
|
+
segments.push(new Segment(at, cursorStyle));
|
|
961
|
+
if (after.length > 0)
|
|
962
|
+
segments.push(new Segment(after, contentStyle));
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
segments.push(new Segment(display, contentStyle));
|
|
966
|
+
}
|
|
967
|
+
segments.push(new Segment("]", bracketStyle));
|
|
968
|
+
return segments;
|
|
969
|
+
}
|
|
970
|
+
_renderMultiline(options) {
|
|
971
|
+
// [LAW:dataflow-not-control-flow] Decompose `value` into visual rows
|
|
972
|
+
// exactly once per render, cache the result, then drive both display
|
|
973
|
+
// *and* cursor projection from the same array. Vertical motion reads
|
|
974
|
+
// from the cache so Up/Down step through whatever the user actually
|
|
975
|
+
// sees, including soft-wrap continuations.
|
|
976
|
+
const visualRows = this._computeVisualRows(options.maxWidth);
|
|
977
|
+
this._visualRows = visualRows;
|
|
978
|
+
// Scroll window: keep `_scrollStart` (the viewport's top row) stable
|
|
979
|
+
// across renders, adjusting only when the cursor would leave the
|
|
980
|
+
// viewport. This is the textarea-conventional "viewport follows cursor
|
|
981
|
+
// when it has to" behavior — cursor moves freely *within* the visible
|
|
982
|
+
// rows; the window scrolls only at the edges. When `maxRows` is unset,
|
|
983
|
+
// render every row.
|
|
984
|
+
const total = visualRows.length;
|
|
985
|
+
const cursorRow = this._cursorVisualRow();
|
|
986
|
+
let scrollStart = 0;
|
|
987
|
+
let visibleCount = total;
|
|
988
|
+
if (this._maxRows !== undefined && total > this._maxRows) {
|
|
989
|
+
const maxStart = total - this._maxRows;
|
|
990
|
+
if (cursorRow < this._scrollStart) {
|
|
991
|
+
this._scrollStart = cursorRow;
|
|
992
|
+
}
|
|
993
|
+
else if (cursorRow >= this._scrollStart + this._maxRows) {
|
|
994
|
+
this._scrollStart = cursorRow - this._maxRows + 1;
|
|
995
|
+
}
|
|
996
|
+
this._scrollStart = Math.max(0, Math.min(maxStart, this._scrollStart));
|
|
997
|
+
scrollStart = this._scrollStart;
|
|
998
|
+
visibleCount = this._maxRows;
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
this._scrollStart = 0;
|
|
1002
|
+
}
|
|
1003
|
+
// Pad with empty rows when the value is shorter than `minRows`.
|
|
1004
|
+
let padRows = 0;
|
|
1005
|
+
if (this._minRows !== undefined && total < this._minRows && this._maxRows === undefined) {
|
|
1006
|
+
padRows = this._minRows - total;
|
|
1007
|
+
}
|
|
1008
|
+
const contentStyle = this.ground;
|
|
1009
|
+
const markerStyle = new Style({ ...ink(this.theme, "foreground", "background"), dim: true });
|
|
1010
|
+
const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
|
|
1011
|
+
// Scroll-direction arrows appear in the rightmost cell of the first/last
|
|
1012
|
+
// visible row, *only* when scroll is actually possible in that direction.
|
|
1013
|
+
// _emitRowContent reserves the indicator's cells so content never
|
|
1014
|
+
// collides with the arrow. Only emitted in `"arrows"` mode; other modes
|
|
1015
|
+
// leave the content area untouched and rely on the host to surface scroll
|
|
1016
|
+
// state externally (e.g. Panel border accessory).
|
|
1017
|
+
const scrollable = this._maxRows !== undefined && total > this._maxRows;
|
|
1018
|
+
const arrowsMode = this._scrollIndicator === "arrows";
|
|
1019
|
+
const canScrollUp = arrowsMode && scrollable && this._scrollStart > 0;
|
|
1020
|
+
const canScrollDown = arrowsMode && scrollable && this._scrollStart + this._maxRows < total;
|
|
1021
|
+
const indicatorStyle = this.indicatorStyleOverride ?? new Style(ink(this.theme, "text-primary", "background"));
|
|
1022
|
+
const segments = [];
|
|
1023
|
+
const showCursor = this.focused && !this.disabled;
|
|
1024
|
+
for (let i = 0; i < visibleCount; i++) {
|
|
1025
|
+
const rowIdx = scrollStart + i;
|
|
1026
|
+
const row = visualRows[rowIdx];
|
|
1027
|
+
if (i > 0)
|
|
1028
|
+
segments.push(new Segment("\n"));
|
|
1029
|
+
if (row.isContinuation) {
|
|
1030
|
+
// An ASCII stand-in as wide as the marker, so the rows keep their geometry.
|
|
1031
|
+
segments.push(new Segment(drawable(options, this._continuationMarker, ">".padEnd(this._markerWidth)), markerStyle));
|
|
1032
|
+
}
|
|
1033
|
+
let indicator;
|
|
1034
|
+
if (i === 0 && canScrollUp) {
|
|
1035
|
+
indicator = { ch: drawable(options, "▲", "^"), style: indicatorStyle };
|
|
1036
|
+
}
|
|
1037
|
+
else if (i === visibleCount - 1 && canScrollDown) {
|
|
1038
|
+
indicator = { ch: drawable(options, "▼", "v"), style: indicatorStyle };
|
|
1039
|
+
}
|
|
1040
|
+
// When a continuation marker was emitted, the row's printable width is
|
|
1041
|
+
// reduced by the marker width — the indicator column is still at
|
|
1042
|
+
// `options.maxWidth - 1` in the panel's view, which is
|
|
1043
|
+
// `options.maxWidth - markerWidth - 1` columns past the marker.
|
|
1044
|
+
const rowPrintWidth = row.isContinuation
|
|
1045
|
+
? asCellCol(Math.max(0, options.maxWidth - this._markerWidth))
|
|
1046
|
+
: asCellCol(options.maxWidth);
|
|
1047
|
+
this._emitRowContent(segments, row, rowIdx === cursorRow && showCursor, contentStyle, cursorStyle, indicator, rowPrintWidth);
|
|
1048
|
+
}
|
|
1049
|
+
// Trailing empty rows for minRows padding (no cursor, no marker).
|
|
1050
|
+
// Emit a whitespace segment of the full content width so Segment.splitLines
|
|
1051
|
+
// does not drop the row — an empty segment before a newline would produce
|
|
1052
|
+
// an invisible row that the renderer discards.
|
|
1053
|
+
for (let i = 0; i < padRows; i++) {
|
|
1054
|
+
segments.push(new Segment("\n"));
|
|
1055
|
+
segments.push(new Segment(setCellSize("", asCellCol(options.maxWidth)), contentStyle));
|
|
1056
|
+
}
|
|
1057
|
+
return segments;
|
|
1058
|
+
}
|
|
1059
|
+
_emitRowContent(out, row, cursorOnRow, contentStyle, cursorStyle, indicator, rowPrintWidth) {
|
|
1060
|
+
const content = row.content;
|
|
1061
|
+
// cursorCol is a code-unit offset into row.content; both cursorPosition
|
|
1062
|
+
// and valueStart are CodePoint, so their difference lands on a code-point
|
|
1063
|
+
// boundary within content and is safe for string slicing.
|
|
1064
|
+
const cursorCol = cursorOnRow ? this.cursorPosition - row.valueStart : -1;
|
|
1065
|
+
// Fast path: no indicator on this row. Let the wrapper pad to width.
|
|
1066
|
+
if (indicator === undefined) {
|
|
1067
|
+
if (!cursorOnRow) {
|
|
1068
|
+
if (content.length > 0)
|
|
1069
|
+
out.push(new Segment(content, contentStyle));
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const before = content.slice(0, cursorCol);
|
|
1073
|
+
const nextCp = nextGrapheme(content, asCodePoint(cursorCol));
|
|
1074
|
+
const at = content.slice(cursorCol, nextCp) || " ";
|
|
1075
|
+
const after = content.slice(nextCp);
|
|
1076
|
+
if (before.length > 0)
|
|
1077
|
+
out.push(new Segment(before, contentStyle));
|
|
1078
|
+
out.push(new Segment(at, cursorStyle));
|
|
1079
|
+
if (after.length > 0)
|
|
1080
|
+
out.push(new Segment(after, contentStyle));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
// Indicator path: reserve the indicator's cells at the end of the row so
|
|
1084
|
+
// wide characters never collide with it. Content fills [0, contentCellWidth)
|
|
1085
|
+
// cells; the indicator occupies the remaining cells.
|
|
1086
|
+
const indicatorWidth = asCellCol(cellLen(indicator.ch));
|
|
1087
|
+
const contentCellWidth = asCellCol(Math.max(0, rowPrintWidth - indicatorWidth));
|
|
1088
|
+
// Clip row content to the non-indicator region and pad to exact cell width.
|
|
1089
|
+
const [visibleContent] = splitText(content, contentCellWidth);
|
|
1090
|
+
const paddedContent = setCellSize(visibleContent, contentCellWidth);
|
|
1091
|
+
// Map the code-unit cursor offset to a cell column within this row.
|
|
1092
|
+
const cursorCellColInRow = cursorOnRow && cursorCol >= 0
|
|
1093
|
+
? cellLen(content.slice(0, cursorCol))
|
|
1094
|
+
: -1;
|
|
1095
|
+
if (cursorCellColInRow >= 0 && cursorCellColInRow < contentCellWidth) {
|
|
1096
|
+
const [before, rest] = splitText(paddedContent, asCellCol(cursorCellColInRow));
|
|
1097
|
+
const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
|
|
1098
|
+
const at = firstCh || " ";
|
|
1099
|
+
const after = rest.slice(firstCh.length);
|
|
1100
|
+
if (before.length > 0)
|
|
1101
|
+
out.push(new Segment(before, contentStyle));
|
|
1102
|
+
out.push(new Segment(at, cursorStyle));
|
|
1103
|
+
if (after.length > 0)
|
|
1104
|
+
out.push(new Segment(after, contentStyle));
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
if (paddedContent.length > 0)
|
|
1108
|
+
out.push(new Segment(paddedContent, contentStyle));
|
|
1109
|
+
}
|
|
1110
|
+
out.push(new Segment(indicator.ch, indicator.style));
|
|
1111
|
+
}
|
|
1112
|
+
_computeVisualRows(maxWidth) {
|
|
1113
|
+
const firstWidth = asCellCol(Math.max(1, maxWidth));
|
|
1114
|
+
const continuationWidth = asCellCol(Math.max(1, firstWidth - this._markerWidth));
|
|
1115
|
+
const rows = [];
|
|
1116
|
+
const lines = this.value.split("\n");
|
|
1117
|
+
let pos = 0;
|
|
1118
|
+
for (let li = 0; li < lines.length; li++) {
|
|
1119
|
+
const line = lines[li];
|
|
1120
|
+
if (this._wrap !== undefined) {
|
|
1121
|
+
const wrapRows = this._wrap(line, { firstWidth, continuationWidth });
|
|
1122
|
+
if (wrapRows.length === 0) {
|
|
1123
|
+
rows.push({ content: "", valueStart: asCodePoint(pos), isContinuation: false });
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
for (let ri = 0; ri < wrapRows.length; ri++) {
|
|
1127
|
+
const wr = wrapRows[ri];
|
|
1128
|
+
rows.push({
|
|
1129
|
+
content: wr.content,
|
|
1130
|
+
valueStart: asCodePoint(pos + wr.start),
|
|
1131
|
+
isContinuation: ri > 0,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
rows.push({ content: line, valueStart: asCodePoint(pos), isContinuation: false });
|
|
1138
|
+
}
|
|
1139
|
+
pos += line.length + 1; // +1 for the \n separator
|
|
1140
|
+
}
|
|
1141
|
+
return rows;
|
|
1142
|
+
}
|
|
1143
|
+
measure(_options) {
|
|
1144
|
+
const minimum = MIN_CONTENT_WIDTH + 2;
|
|
1145
|
+
const maximum = Math.max(minimum, Math.max(cellLen(this.value), cellLen(this.placeholder)) + 2);
|
|
1146
|
+
return { minimum, maximum };
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* `[X/Y]` scroll-position string when there's actually something to
|
|
1150
|
+
* scroll, else `undefined`. X is the cursor's 1-indexed visual row; Y is
|
|
1151
|
+
* the total visual row count. Reads cached state populated by the most
|
|
1152
|
+
* recent `render()` — call from a `Panel.bottomRightAccessory` thunk so
|
|
1153
|
+
* it evaluates after content has been rendered for the current frame.
|
|
1154
|
+
*
|
|
1155
|
+
* Returns `undefined` when:
|
|
1156
|
+
* - `scrollIndicator` is not `"indices"`, or
|
|
1157
|
+
* - `maxRows` is unset, or total visual rows ≤ maxRows (nothing to
|
|
1158
|
+
* scroll), or
|
|
1159
|
+
* - no render has happened yet (cache is empty).
|
|
1160
|
+
*/
|
|
1161
|
+
get scrollIndicatorText() {
|
|
1162
|
+
if (this._scrollIndicator !== "indices")
|
|
1163
|
+
return undefined;
|
|
1164
|
+
if (this._maxRows === undefined)
|
|
1165
|
+
return undefined;
|
|
1166
|
+
const rows = this._visualRows;
|
|
1167
|
+
if (rows === null || rows.length <= this._maxRows)
|
|
1168
|
+
return undefined;
|
|
1169
|
+
const cursorRow1 = this._cursorVisualRow() + 1;
|
|
1170
|
+
return `[${cursorRow1}/${rows.length}]`;
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
})();
|
|
1174
|
+
export { TextInput };
|