@promptctl/rich-js 0.24.0 → 0.25.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 +7 -0
- package/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +243 -0
- package/dist/core/animation.js +375 -0
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +40 -24
- package/dist/core/cells.js +151 -66
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +21 -29
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +48 -43
- package/dist/core/easing.d.ts +140 -10
- package/dist/core/easing.js +328 -50
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +3 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +3 -3
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/place.js +2 -1
- package/dist/core/pretty.js +11 -12
- package/dist/core/protocol.d.ts +136 -21
- package/dist/core/protocol.js +203 -16
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +27 -6
- package/dist/core/segment.d.ts +31 -6
- package/dist/core/segment.js +68 -53
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +11 -13
- package/dist/core/strip.js +126 -113
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +74 -34
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +133 -133
- package/dist/core/timeline.d.ts +109 -0
- package/dist/core/timeline.js +180 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +71 -0
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +72 -38
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +30 -15
- package/dist/index.js +20 -6
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +1 -1
- package/dist/renderables/columns.js +3 -3
- package/dist/renderables/constrain.js +2 -1
- package/dist/renderables/effect.d.ts +155 -92
- package/dist/renderables/effect.js +203 -136
- package/dist/renderables/effects.d.ts +137 -24
- package/dist/renderables/effects.js +276 -113
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.js +2 -1
- package/dist/renderables/frames.d.ts +45 -43
- package/dist/renderables/frames.js +57 -45
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +1 -1
- package/dist/renderables/group.js +2 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +10 -9
- package/dist/renderables/layout.js +42 -35
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +2 -2
- package/dist/renderables/list-view.js +11 -8
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +89 -42
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1427 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +5 -7
- package/dist/renderables/markdown.js +356 -683
- package/dist/renderables/padding.d.ts +7 -20
- package/dist/renderables/padding.js +29 -50
- package/dist/renderables/panel.d.ts +30 -32
- package/dist/renderables/panel.js +65 -109
- package/dist/renderables/progress.d.ts +83 -32
- package/dist/renderables/progress.js +186 -90
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +33 -10
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +25 -0
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +57 -24
- package/dist/renderables/spinner.js +101 -49
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +5 -3
- package/dist/renderables/table.d.ts +48 -46
- package/dist/renderables/table.js +170 -208
- package/dist/renderables/traceback.d.ts +3 -3
- package/dist/renderables/traceback.js +12 -10
- package/dist/renderables/tree.d.ts +28 -25
- package/dist/renderables/tree.js +108 -58
- package/dist/renderables/viewport.d.ts +77 -38
- package/dist/renderables/viewport.js +156 -72
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +7 -6
- package/dist/themes/ramp.d.ts +62 -4
- package/dist/themes/ramp.js +82 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +23 -14
- package/dist/widgets/focus-manager.js +34 -33
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/index.d.ts +1 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +1 -1
- package/dist/widgets/static-item.js +6 -7
- package/dist/widgets/text-input.d.ts +53 -47
- package/dist/widgets/text-input.js +456 -399
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +32 -6
- package/dist/widgets/widget-app.js +74 -27
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -21
- package/package.json +9 -5
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two modes:
|
|
5
5
|
* - single-line (default): Enter submits; newlines in value are rendered
|
|
6
|
-
* as a literal `↵` glyph if they ever appear
|
|
6
|
+
* as a literal `↵` glyph if they ever appear, and
|
|
7
|
+
* any other glyph of zero cells as `◌`.
|
|
7
8
|
* - multi-line (`multiline: true`): Enter inserts a `\n`; `value` may
|
|
8
9
|
* contain logical line breaks; Up/Down navigate
|
|
9
10
|
* between logical lines preserving column intent.
|
|
11
|
+
* A glyph of zero cells is drawn as `◌` here too.
|
|
10
12
|
*
|
|
11
13
|
* [LAW:dataflow-not-control-flow] cursor and value are observable data; the
|
|
12
14
|
* keymap dispatches to small `@action` mutators that all flow through the
|
|
@@ -94,13 +96,72 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
94
96
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
95
97
|
done = true;
|
|
96
98
|
};
|
|
97
|
-
import {
|
|
99
|
+
import { action, runInAction } from "mobx";
|
|
100
|
+
import { observableRef } from "./observable.js";
|
|
98
101
|
import { Segment } from "../core/segment.js";
|
|
99
102
|
import { Style } from "../core/style.js";
|
|
100
|
-
import { cellLen, setCellSize, splitText,
|
|
101
|
-
import { drawable } from "../core/protocol.js";
|
|
103
|
+
import { cellLen, setCellSize, splitText, cellFitEnd, cellRows, clustersBefore, clustersFrom, asCellCol, cellCount, asCodePoint, asCodeUnit, graphemeBoundary, graphemes, nextGrapheme, prevGrapheme, } from "../core/cells.js";
|
|
104
|
+
import { afterPass, drawable, render } from "../core/protocol.js";
|
|
105
|
+
import { State } from "../core/state.js";
|
|
106
|
+
import { Viewport } from "../renderables/viewport.js";
|
|
107
|
+
import { Animation } from "../core/animation.js";
|
|
108
|
+
import { beat } from "../core/easing.js";
|
|
109
|
+
import { Timeline } from "../core/timeline.js";
|
|
110
|
+
import { holdOf, shownAt, stampFrame } from "../core/animation.js";
|
|
111
|
+
import { levelOf } from "../core/anchor.js";
|
|
102
112
|
import { ThemedWidget } from "./themed-widget.js";
|
|
103
113
|
import { ink } from "./ink.js";
|
|
114
|
+
import { pattern } from "../core/pattern.js";
|
|
115
|
+
/** The row of `rows` the cursor at `cursor` sits on: the last that starts at or before it. */
|
|
116
|
+
function cursorRowIn(rows, cursor) {
|
|
117
|
+
let row = rows.length - 1;
|
|
118
|
+
while (row > 0 && rows[row].valueStart > cursor)
|
|
119
|
+
row--;
|
|
120
|
+
return Math.max(0, row);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The content row `shown`'s first row draws: what a viewport showing content
|
|
124
|
+
* stamped by `owner` scrolled it to. [LAW:no-silent-failure] Every content row
|
|
125
|
+
* is padded and stamped, so a first row without the stamp is a broken frame,
|
|
126
|
+
* not an offset of 0.
|
|
127
|
+
*/
|
|
128
|
+
function shownRow(shown, owner) {
|
|
129
|
+
if (shown.length === 0)
|
|
130
|
+
return 0;
|
|
131
|
+
const anchor = levelOf(Segment.anchorAt(shown, 0, 0), owner);
|
|
132
|
+
if (anchor === undefined)
|
|
133
|
+
throw new Error("TextInput: the viewport's first row carries no row of the field's content");
|
|
134
|
+
return anchor.row;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* [LAW:one-source-of-truth] A frame's layout, as the owner of every cell the
|
|
138
|
+
* field drew in it: a click and the scroll indicator read the layout of the
|
|
139
|
+
* frame they land on off its cells, never one a later frame kept.
|
|
140
|
+
*/
|
|
141
|
+
class Laid {
|
|
142
|
+
layout;
|
|
143
|
+
constructor(layout) {
|
|
144
|
+
this.layout = layout;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** The layout stamped on the cell `anchor` names, at whatever depth. */
|
|
148
|
+
function layoutOn(anchor) {
|
|
149
|
+
let a = anchor;
|
|
150
|
+
while (a !== undefined && !(a.owner instanceof Laid))
|
|
151
|
+
a = a.inner;
|
|
152
|
+
return a === undefined ? undefined : a.owner.layout;
|
|
153
|
+
}
|
|
154
|
+
/** The layout `field` drew in the frame `lines` hold: off the first of its cells. */
|
|
155
|
+
function layoutIn(lines, field) {
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
for (const segment of line) {
|
|
158
|
+
const layout = layoutOn(levelOf(segment.style?.anchor, field));
|
|
159
|
+
if (layout !== undefined)
|
|
160
|
+
return layout;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
104
165
|
/**
|
|
105
166
|
* Character-greedy soft wrap. Breaks at any character once `firstWidth`
|
|
106
167
|
* (or `continuationWidth` for continuation rows) cells are exhausted. The
|
|
@@ -109,40 +170,45 @@ import { ink } from "./ink.js";
|
|
|
109
170
|
*
|
|
110
171
|
* Wide characters (CJK, emoji) are treated as atomic: a char that would
|
|
111
172
|
* 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 `
|
|
173
|
+
* wide for the whole budget is force-taken — see `cellRows`.
|
|
113
174
|
*/
|
|
114
175
|
export const charGreedyWrap = (line, { firstWidth, continuationWidth }) => {
|
|
115
176
|
if (line.length === 0)
|
|
116
|
-
return [{
|
|
177
|
+
return [{ start: asCodePoint(0), end: asCodePoint(0) }];
|
|
178
|
+
const capOf = (row) => (row === 0 ? firstWidth : continuationWidth);
|
|
117
179
|
const rows = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
while (pos < line.length) {
|
|
121
|
-
const cap = isFirst ? firstWidth : continuationWidth;
|
|
122
|
-
if (cap <= 0)
|
|
180
|
+
for (const row of cellRows(line, capOf)) {
|
|
181
|
+
if (capOf(rows.length) <= 0)
|
|
123
182
|
break;
|
|
124
|
-
|
|
125
|
-
rows.push({ content: line.slice(pos, end), start: pos });
|
|
126
|
-
pos = end;
|
|
127
|
-
isFirst = false;
|
|
183
|
+
rows.push(row);
|
|
128
184
|
}
|
|
129
185
|
return rows;
|
|
130
186
|
};
|
|
131
187
|
const MIN_CONTENT_WIDTH = 8;
|
|
132
|
-
//
|
|
133
|
-
//
|
|
188
|
+
// [LAW:one-source-of-truth] The cells single-line content takes: its text and,
|
|
189
|
+
// past the text's end, the cell the cursor stands on there — never fewer than
|
|
190
|
+
// the floor. The render sizes and scrolls the field by it; `measure` asks it
|
|
191
|
+
// with the cursor at the end, the widest a value draws.
|
|
192
|
+
function singleLineExtent(textCells, cursorCell) {
|
|
193
|
+
return asCellCol(Math.max(MIN_CONTENT_WIDTH, textCells, cursorCell + 1));
|
|
194
|
+
}
|
|
195
|
+
// What one line shows in place of a glyph of `value`; each is one cell.
|
|
134
196
|
const NEWLINE_GLYPH = "↵";
|
|
135
197
|
const ASCII_NEWLINE_GLYPH = "~";
|
|
136
198
|
const MASK_GLYPH = "•";
|
|
137
199
|
const ASCII_MASK_GLYPH = "*";
|
|
200
|
+
// The dotted circle Unicode draws for a mark with no base to sit on; `?` is the
|
|
201
|
+
// ASCII replacement character.
|
|
202
|
+
const ZERO_CELL_GLYPH = "◌";
|
|
203
|
+
const ASCII_ZERO_CELL_GLYPH = "?";
|
|
138
204
|
// Text a key may insert: anything but the C0 controls, DEL and the C1
|
|
139
205
|
// controls, which a terminal reads as escape sequences (U+009B is CSI).
|
|
140
|
-
const PRINTABLE = /^[^\x00-\x1f\x7f-\x9f]+$/u;
|
|
206
|
+
const PRINTABLE = pattern(/^[^\x00-\x1f\x7f-\x9f]+$/u);
|
|
141
207
|
// Word-character regex used by Alt+B/F and Ctrl+Left/Right. Matches the
|
|
142
208
|
// readline default (alphanumeric + underscore), which is what users typing
|
|
143
209
|
// shell commands or code identifiers expect.
|
|
144
|
-
const WORD_CHAR_RE = /[A-Za-z0-9_]
|
|
145
|
-
const WHITESPACE_RE = /\s
|
|
210
|
+
const WORD_CHAR_RE = pattern(/[A-Za-z0-9_]/);
|
|
211
|
+
const WHITESPACE_RE = pattern(/\s/);
|
|
146
212
|
/** CRLF and lone CR as LF, the line break `value` holds. */
|
|
147
213
|
function toLf(text) {
|
|
148
214
|
return text.replace(/\r\n?/g, "\n");
|
|
@@ -153,6 +219,33 @@ function isWordChar(c) {
|
|
|
153
219
|
function isWhitespace(c) {
|
|
154
220
|
return c !== undefined && WHITESPACE_RE.test(c);
|
|
155
221
|
}
|
|
222
|
+
const isNotWordChar = (c) => !isWordChar(c);
|
|
223
|
+
const isNotWhitespace = (c) => !isWhitespace(c);
|
|
224
|
+
/**
|
|
225
|
+
* How many code units a word motion crosses: every leading cluster `skipped`
|
|
226
|
+
* accepts, then every one after them `taken` accepts. `clusters` is one walk
|
|
227
|
+
* from the cursor (`clustersFrom` or `clustersBefore`), so a motion across a
|
|
228
|
+
* long word segments the value once, not once a cluster.
|
|
229
|
+
*/
|
|
230
|
+
function wordSpan(clusters, skipped, taken) {
|
|
231
|
+
let span = 0;
|
|
232
|
+
let skipping = true;
|
|
233
|
+
for (const cluster of clusters) {
|
|
234
|
+
skipping &&= skipped(cluster);
|
|
235
|
+
if (!skipping && !taken(cluster))
|
|
236
|
+
break;
|
|
237
|
+
span += cluster.length;
|
|
238
|
+
}
|
|
239
|
+
return span;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The caret's blink, from the moment it restarts: lit for the first half
|
|
243
|
+
* second, so the caret is solid the moment the field is touched, then dark
|
|
244
|
+
* for the next, on a `beat`. A loop, so with its motion taken out it holds lit.
|
|
245
|
+
*/
|
|
246
|
+
const CARET_BLINK = Animation.map(beat([0.5, 0.5]), (hold) => hold === 0);
|
|
247
|
+
/** The caret of a frame drawn for no moment — a `print` — which nothing moves: lit, and never changing. */
|
|
248
|
+
const CARET_UNTIMED = { lit: true, hold: { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY } };
|
|
156
249
|
let TextInput = (() => {
|
|
157
250
|
let _classSuper = ThemedWidget;
|
|
158
251
|
let _instanceExtraInitializers = [];
|
|
@@ -162,6 +255,9 @@ let TextInput = (() => {
|
|
|
162
255
|
let __cursor_decorators;
|
|
163
256
|
let __cursor_initializers = [];
|
|
164
257
|
let __cursor_extraInitializers = [];
|
|
258
|
+
let __blinkFrom_decorators;
|
|
259
|
+
let __blinkFrom_initializers = [];
|
|
260
|
+
let __blinkFrom_extraInitializers = [];
|
|
165
261
|
let _placeholder_decorators;
|
|
166
262
|
let _placeholder_initializers = [];
|
|
167
263
|
let _placeholder_extraInitializers = [];
|
|
@@ -175,6 +271,7 @@ let TextInput = (() => {
|
|
|
175
271
|
let _contentStyleOverride_initializers = [];
|
|
176
272
|
let _contentStyleOverride_extraInitializers = [];
|
|
177
273
|
let _handleKey_decorators;
|
|
274
|
+
let _handleFocus_decorators;
|
|
178
275
|
let _handleMouse_decorators;
|
|
179
276
|
let _setHovered_decorators;
|
|
180
277
|
let _moveCharLeft_decorators;
|
|
@@ -199,13 +296,15 @@ let TextInput = (() => {
|
|
|
199
296
|
return class TextInput extends _classSuper {
|
|
200
297
|
static {
|
|
201
298
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
202
|
-
__value_decorators = [
|
|
203
|
-
__cursor_decorators = [
|
|
299
|
+
__value_decorators = [observableRef];
|
|
300
|
+
__cursor_decorators = [observableRef];
|
|
301
|
+
__blinkFrom_decorators = [observableRef];
|
|
204
302
|
_placeholder_decorators = [observableRef];
|
|
205
303
|
_indicatorStyleOverride_decorators = [observableRef];
|
|
206
304
|
_cursorStyleOverride_decorators = [observableRef];
|
|
207
305
|
_contentStyleOverride_decorators = [observableRef];
|
|
208
306
|
_handleKey_decorators = [action];
|
|
307
|
+
_handleFocus_decorators = [action];
|
|
209
308
|
_handleMouse_decorators = [action];
|
|
210
309
|
_setHovered_decorators = [action];
|
|
211
310
|
_moveCharLeft_decorators = [action];
|
|
@@ -229,11 +328,13 @@ let TextInput = (() => {
|
|
|
229
328
|
__insertText_decorators = [action];
|
|
230
329
|
__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
330
|
__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);
|
|
331
|
+
__esDecorate(this, null, __blinkFrom_decorators, { kind: "accessor", name: "_blinkFrom", static: false, private: false, access: { has: obj => "_blinkFrom" in obj, get: obj => obj._blinkFrom, set: (obj, value) => { obj._blinkFrom = value; } }, metadata: _metadata }, __blinkFrom_initializers, __blinkFrom_extraInitializers);
|
|
232
332
|
__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
333
|
__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
334
|
__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
335
|
__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
336
|
__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);
|
|
337
|
+
__esDecorate(this, null, _handleFocus_decorators, { kind: "method", name: "handleFocus", static: false, private: false, access: { has: obj => "handleFocus" in obj, get: obj => obj.handleFocus }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
237
338
|
__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
339
|
__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
340
|
__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);
|
|
@@ -265,6 +366,13 @@ let TextInput = (() => {
|
|
|
265
366
|
#_cursor_accessor_storage = (__runInitializers(this, __value_extraInitializers), __runInitializers(this, __cursor_initializers, void 0));
|
|
266
367
|
get _cursor() { return this.#_cursor_accessor_storage; }
|
|
267
368
|
set _cursor(value) { this.#_cursor_accessor_storage = value; }
|
|
369
|
+
#_blinkFrom_accessor_storage = (__runInitializers(this, __cursor_extraInitializers), __runInitializers(this, __blinkFrom_initializers, 0));
|
|
370
|
+
// When the field last took focus, a click, or a key it handled, on the
|
|
371
|
+
// frame's clock (each event's `time`): where the caret's blink restarts.
|
|
372
|
+
// The blink itself is a function of this and the time a frame is drawn
|
|
373
|
+
// for, so nothing about it is kept.
|
|
374
|
+
get _blinkFrom() { return this.#_blinkFrom_accessor_storage; }
|
|
375
|
+
set _blinkFrom(value) { this.#_blinkFrom_accessor_storage = value; }
|
|
268
376
|
// [LAW:single-enforcer] Every write to either half of the pair passes
|
|
269
377
|
// `graphemeBoundary`, so the cursor is a cluster boundary inside `value`
|
|
270
378
|
// whoever set it last — a caller replacing `value`, a click, an insert that
|
|
@@ -280,7 +388,7 @@ let TextInput = (() => {
|
|
|
280
388
|
}
|
|
281
389
|
get cursorPosition() { return this._cursor; }
|
|
282
390
|
set cursorPosition(p) { this._cursor = graphemeBoundary(this._value, p); }
|
|
283
|
-
#placeholder_accessor_storage = (__runInitializers(this,
|
|
391
|
+
#placeholder_accessor_storage = (__runInitializers(this, __blinkFrom_extraInitializers), __runInitializers(this, _placeholder_initializers, void 0));
|
|
284
392
|
get placeholder() { return this.#placeholder_accessor_storage; }
|
|
285
393
|
set placeholder(value) { this.#placeholder_accessor_storage = value; }
|
|
286
394
|
_maxLength = __runInitializers(this, _placeholder_extraInitializers);
|
|
@@ -301,42 +409,28 @@ let TextInput = (() => {
|
|
|
301
409
|
#contentStyleOverride_accessor_storage = (__runInitializers(this, _cursorStyleOverride_extraInitializers), __runInitializers(this, _contentStyleOverride_initializers, void 0));
|
|
302
410
|
get contentStyleOverride() { return this.#contentStyleOverride_accessor_storage; }
|
|
303
411
|
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
412
|
// [LAW:types-are-the-program] `_preferredColumn` is a cell column (not a
|
|
317
413
|
// code-unit index) — vertical motion navigates the visual surface, not the
|
|
318
414
|
// string's byte layout. Reset to null on every non-vertical motion or edit
|
|
319
415
|
// so the next Up/Down captures a fresh intent. Without this, Down through a
|
|
320
416
|
// short line then back Up would land at the short-line column instead of the
|
|
321
417
|
// original.
|
|
322
|
-
_preferredColumn = null;
|
|
418
|
+
_preferredColumn = (__runInitializers(this, _contentStyleOverride_extraInitializers), new State(null));
|
|
323
419
|
// Single-slot kill buffer used by Ctrl+U / Ctrl+K → Ctrl+Y. Not a full
|
|
324
420
|
// kill ring (no Alt+Y rotation) — sufficient for the common cut/paste
|
|
325
421
|
// pattern that's the readline 90% case.
|
|
326
|
-
_killBuffer = "";
|
|
327
|
-
// [LAW:one-source-of-truth]
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
_singleLineViewportStart = asCellCol(0);
|
|
339
|
-
_singleLineAsciiOnly = false;
|
|
422
|
+
_killBuffer = new State("");
|
|
423
|
+
// [LAW:one-source-of-truth] What the last frame drew under — the one layout
|
|
424
|
+
// fact only a render learns. The visual rows, the single-line window and
|
|
425
|
+
// the cursor's row are functions of it and of `value`, derived where they
|
|
426
|
+
// are read: a click and Up/Down see the rows of the value as it is now at
|
|
427
|
+
// the width the user last saw. Kept once the frame is drawn (`afterPass`).
|
|
428
|
+
// Before any frame the width bounds nothing, and nothing wraps.
|
|
429
|
+
_drawn = new State({ maxWidth: Infinity, asciiOnly: false });
|
|
430
|
+
// [LAW:single-enforcer] The multiline scroll: the viewport keeps the cursor
|
|
431
|
+
// row in view, clamps when the value shrinks, and keeps its offset once the
|
|
432
|
+
// frame is drawn, as it does for a `ListView`.
|
|
433
|
+
_viewport;
|
|
340
434
|
multiline;
|
|
341
435
|
constructor(options = {}) {
|
|
342
436
|
super(options.theme);
|
|
@@ -354,7 +448,8 @@ let TextInput = (() => {
|
|
|
354
448
|
this._maxLength = options.maxLength;
|
|
355
449
|
this._password = options.password ?? false;
|
|
356
450
|
this._multiline = options.multiline ?? false;
|
|
357
|
-
|
|
451
|
+
// Soft wrap is multiline's: one line wraps to nothing.
|
|
452
|
+
this._wrap = this._multiline ? options.wrap : undefined;
|
|
358
453
|
this._continuationMarker = options.continuationMarker ?? "↳ ";
|
|
359
454
|
this._markerWidth = asCellCol(cellLen(this._continuationMarker));
|
|
360
455
|
this._maxRows = options.maxRows;
|
|
@@ -364,9 +459,29 @@ let TextInput = (() => {
|
|
|
364
459
|
this.cursorStyleOverride = options.cursorStyle;
|
|
365
460
|
this.contentStyleOverride = options.contentStyle;
|
|
366
461
|
this.multiline = this._multiline;
|
|
462
|
+
this._viewport = new Viewport();
|
|
367
463
|
}
|
|
368
464
|
// --- Event handlers ---
|
|
465
|
+
// A key the field handles is one it stops; only that restarts the blink,
|
|
466
|
+
// so a key passing through to the app — Tab, Escape — moves nothing here.
|
|
369
467
|
handleKey(event) {
|
|
468
|
+
this._edit(event);
|
|
469
|
+
if (!event.stopped)
|
|
470
|
+
return;
|
|
471
|
+
this._blinkFrom = event.time;
|
|
472
|
+
// An edit on the cursor's row moves no followed range, so the row is
|
|
473
|
+
// brought into view here too: typing after the wheel scrolled away scrolls
|
|
474
|
+
// back. The rows are the edited value's, at the width last drawn.
|
|
475
|
+
if (this._multiline) {
|
|
476
|
+
const row = cursorRowIn(this._rows(this._drawn.get()), this.cursorPosition);
|
|
477
|
+
this._viewport.ensureVisible(row, row + 1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
handleFocus(event) {
|
|
481
|
+
super.handleFocus(event);
|
|
482
|
+
this._blinkFrom = event.time;
|
|
483
|
+
}
|
|
484
|
+
_edit(event) {
|
|
370
485
|
if (this.disabled)
|
|
371
486
|
return;
|
|
372
487
|
// ─── editing keys (modifier-conscious) ───
|
|
@@ -538,110 +653,69 @@ let TextInput = (() => {
|
|
|
538
653
|
return;
|
|
539
654
|
if (event.type !== "mouse_down")
|
|
540
655
|
return;
|
|
541
|
-
|
|
656
|
+
this._blinkFrom = event.time;
|
|
657
|
+
// [LAW:no-silent-failure] Every cell the field draws carries its frame's
|
|
658
|
+
// layout, so a press without one did not come off a frame it drew.
|
|
659
|
+
const layout = layoutOn(event.cell);
|
|
660
|
+
if (layout === undefined)
|
|
661
|
+
throw new Error("TextInput: a press carries no layout of the frame it landed on");
|
|
662
|
+
if (layout.kind === "rows") {
|
|
542
663
|
// Multiline has no brackets; rows may have a leading continuation marker.
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
return;
|
|
547
|
-
const rawRowIdx = this._scrollStart + event.y;
|
|
664
|
+
// event.y is a row of the window the clicked frame showed; event.x is
|
|
665
|
+
// projected into the row's content.
|
|
666
|
+
const rawRowIdx = layout.offset + event.y;
|
|
548
667
|
// Clicks on minRows-padded empty rows (beyond the last real row) snap to end.
|
|
549
|
-
if (rawRowIdx >= rows.length) {
|
|
668
|
+
if (rawRowIdx >= layout.rows.length) {
|
|
550
669
|
this.cursorPosition = asCodeUnit(this.value.length);
|
|
551
|
-
this._preferredColumn
|
|
670
|
+
this._preferredColumn.set(null);
|
|
552
671
|
return;
|
|
553
672
|
}
|
|
554
|
-
const
|
|
555
|
-
const row = rows[rowIdx];
|
|
673
|
+
const row = layout.rows[rawRowIdx];
|
|
556
674
|
// Continuation rows start after the marker; non-continuation rows start at column 0.
|
|
557
675
|
const contentXOff = asCellCol(row.isContinuation ? this._markerWidth : 0);
|
|
558
676
|
const relX = asCellCol(Math.max(0, event.x - contentXOff));
|
|
559
|
-
this.cursorPosition = asCodeUnit(row.valueStart +
|
|
677
|
+
this.cursorPosition = asCodeUnit(row.valueStart + this._offsetAtShownCell(row.content, relX, layout.drawn));
|
|
560
678
|
}
|
|
561
679
|
else {
|
|
562
680
|
// [LAW:types-are-the-program] relX is a cell-column offset into the full
|
|
563
|
-
// display string
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
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);
|
|
681
|
+
// display string: the click's column past the bracket, from where the
|
|
682
|
+
// window the clicked frame showed begins.
|
|
683
|
+
const relX = asCellCol(Math.max(0, event.x - 1) + layout.startCell);
|
|
684
|
+
this.cursorPosition = this._offsetAtShownCell(layout.value, relX, layout.drawn);
|
|
580
685
|
}
|
|
581
|
-
this._preferredColumn
|
|
686
|
+
this._preferredColumn.set(null);
|
|
582
687
|
}
|
|
583
688
|
// --- Hover mutator (router fast-path) ---
|
|
584
689
|
setHovered(value) { this.hovered = value; }
|
|
585
690
|
// ─── Public motion primitives ───────────────────────────────────────────
|
|
586
691
|
moveCharLeft() {
|
|
587
692
|
this.cursorPosition = prevGrapheme(this.value, this.cursorPosition);
|
|
588
|
-
this._preferredColumn
|
|
693
|
+
this._preferredColumn.set(null);
|
|
589
694
|
}
|
|
590
695
|
moveCharRight() {
|
|
591
696
|
this.cursorPosition = nextGrapheme(this.value, this.cursorPosition);
|
|
592
|
-
this._preferredColumn
|
|
697
|
+
this._preferredColumn.set(null);
|
|
593
698
|
}
|
|
594
699
|
moveLineUp() {
|
|
595
|
-
|
|
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;
|
|
700
|
+
this._moveLine(-1);
|
|
621
701
|
}
|
|
622
702
|
moveLineDown() {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
if (
|
|
703
|
+
this._moveLine(1);
|
|
704
|
+
}
|
|
705
|
+
// [LAW:dataflow-not-control-flow] One motion over the rows as last drawn —
|
|
706
|
+
// wraps and marker offsets accounted for. Before any frame they are the
|
|
707
|
+
// logical lines, so motion before mount is this same path, not a fallback.
|
|
708
|
+
_moveLine(step) {
|
|
709
|
+
const rows = this._rows(this._drawn.get());
|
|
710
|
+
const rowIdx = cursorRowIn(rows, this.cursorPosition);
|
|
711
|
+
const targetIdx = rowIdx + step;
|
|
712
|
+
const target = rows[targetIdx];
|
|
713
|
+
if (target === undefined)
|
|
634
714
|
return;
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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;
|
|
715
|
+
const row = rows[rowIdx];
|
|
716
|
+
const col = this._preferredColumn.get() ?? this._shownCells(row.content.slice(0, this.cursorPosition - row.valueStart));
|
|
717
|
+
this.cursorPosition = asCodeUnit(target.valueStart + this._clampColForRow(rows, col, targetIdx));
|
|
718
|
+
this._preferredColumn.set(col);
|
|
645
719
|
}
|
|
646
720
|
// Clamp `col` (a cell column) to the target visual row's content, returning
|
|
647
721
|
// the code-unit offset within that row. When the row IS followed by a
|
|
@@ -649,91 +723,46 @@ let TextInput = (() => {
|
|
|
649
723
|
// cluster boundary strictly inside the row (which may be more than
|
|
650
724
|
// one cell before the visual end when the last glyph is wide) so the
|
|
651
725
|
// cursor stays strictly inside the target row — landing at the boundary
|
|
652
|
-
// would cause `
|
|
726
|
+
// would cause `cursorRowIn` to resolve to the *later* row, making
|
|
653
727
|
// every subsequent Up/Down stick at the boundary.
|
|
654
728
|
//
|
|
655
729
|
// For rows NOT followed by a continuation (last wrap row, or any unwrapped
|
|
656
730
|
// logical line), the end-of-line position is valid — there is no adjacent
|
|
657
731
|
// continuation row to collide with.
|
|
658
|
-
_clampColForRow(col, targetIdx) {
|
|
659
|
-
const rows = this._visualRows;
|
|
732
|
+
_clampColForRow(rows, col, targetIdx) {
|
|
660
733
|
const target = rows[targetIdx];
|
|
661
734
|
const nextIsContinuation = targetIdx + 1 < rows.length && rows[targetIdx + 1].isContinuation;
|
|
662
|
-
const contentCellWidth =
|
|
735
|
+
const contentCellWidth = this._shownCells(target.content);
|
|
663
736
|
const capCells = nextIsContinuation
|
|
664
737
|
? asCellCol(Math.max(0, contentCellWidth - 1))
|
|
665
738
|
: contentCellWidth;
|
|
666
|
-
return
|
|
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)));
|
|
739
|
+
return this._offsetAtShownCell(target.content, asCellCol(Math.min(col, capCells)), this._drawn.get());
|
|
687
740
|
}
|
|
688
741
|
moveLineStart() {
|
|
689
742
|
this.cursorPosition = this._lineStart();
|
|
690
|
-
this._preferredColumn
|
|
743
|
+
this._preferredColumn.set(null);
|
|
691
744
|
}
|
|
692
745
|
moveLineEnd() {
|
|
693
746
|
this.cursorPosition = this._lineEnd();
|
|
694
|
-
this._preferredColumn
|
|
747
|
+
this._preferredColumn.set(null);
|
|
695
748
|
}
|
|
696
749
|
moveDocStart() {
|
|
697
750
|
this.cursorPosition = asCodeUnit(0);
|
|
698
|
-
this._preferredColumn
|
|
751
|
+
this._preferredColumn.set(null);
|
|
699
752
|
}
|
|
700
753
|
moveDocEnd() {
|
|
701
754
|
this.cursorPosition = asCodeUnit(this.value.length);
|
|
702
|
-
this._preferredColumn
|
|
755
|
+
this._preferredColumn.set(null);
|
|
703
756
|
}
|
|
704
757
|
moveWordLeft() {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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;
|
|
758
|
+
const at = this.cursorPosition;
|
|
759
|
+
this.cursorPosition = asCodePoint(at - wordSpan(clustersBefore(this.value, at), isNotWordChar, isWordChar));
|
|
760
|
+
this._preferredColumn.set(null);
|
|
720
761
|
}
|
|
721
762
|
moveWordRight() {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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;
|
|
763
|
+
const at = this.cursorPosition;
|
|
764
|
+
this.cursorPosition = asCodePoint(at + wordSpan(clustersFrom(this.value, at), isNotWordChar, isWordChar));
|
|
765
|
+
this._preferredColumn.set(null);
|
|
737
766
|
}
|
|
738
767
|
// ─── Public editing primitives ──────────────────────────────────────────
|
|
739
768
|
deleteCharBack() {
|
|
@@ -742,7 +771,7 @@ let TextInput = (() => {
|
|
|
742
771
|
const newPos = prevGrapheme(this.value, this.cursorPosition);
|
|
743
772
|
this.value = this.value.slice(0, newPos) + this.value.slice(this.cursorPosition);
|
|
744
773
|
this.cursorPosition = newPos;
|
|
745
|
-
this._preferredColumn
|
|
774
|
+
this._preferredColumn.set(null);
|
|
746
775
|
this.emitChange();
|
|
747
776
|
}
|
|
748
777
|
deleteCharForward() {
|
|
@@ -750,86 +779,63 @@ let TextInput = (() => {
|
|
|
750
779
|
return;
|
|
751
780
|
const nextPos = nextGrapheme(this.value, this.cursorPosition);
|
|
752
781
|
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(nextPos);
|
|
753
|
-
this._preferredColumn
|
|
782
|
+
this._preferredColumn.set(null);
|
|
754
783
|
this.emitChange();
|
|
755
784
|
}
|
|
756
785
|
deleteWordBack() {
|
|
757
786
|
// Readline `unix-word-rubout` semantics: delete back to nearest whitespace,
|
|
758
787
|
// skipping trailing whitespace first so successive Ctrl+W at "foo bar |"
|
|
759
788
|
// → "foo |" → "|" rather than getting stuck on the trailing space.
|
|
760
|
-
|
|
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
|
-
}
|
|
789
|
+
const p = asCodePoint(this.cursorPosition - wordSpan(clustersBefore(this.value, this.cursorPosition), isWhitespace, isNotWhitespace));
|
|
773
790
|
if (p === this.cursorPosition)
|
|
774
791
|
return;
|
|
775
|
-
this._killBuffer
|
|
792
|
+
this._killBuffer.set(this.value.slice(p, this.cursorPosition));
|
|
776
793
|
this.value = this.value.slice(0, p) + this.value.slice(this.cursorPosition);
|
|
777
794
|
this.cursorPosition = p;
|
|
778
|
-
this._preferredColumn
|
|
795
|
+
this._preferredColumn.set(null);
|
|
779
796
|
this.emitChange();
|
|
780
797
|
}
|
|
781
798
|
deleteWordForward() {
|
|
782
|
-
|
|
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
|
-
}
|
|
799
|
+
const p = this.cursorPosition + wordSpan(clustersFrom(this.value, this.cursorPosition), isNotWordChar, isWordChar);
|
|
795
800
|
if (p === this.cursorPosition)
|
|
796
801
|
return;
|
|
797
|
-
this._killBuffer
|
|
802
|
+
this._killBuffer.set(this.value.slice(this.cursorPosition, p));
|
|
798
803
|
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(p);
|
|
799
|
-
this._preferredColumn
|
|
804
|
+
this._preferredColumn.set(null);
|
|
800
805
|
this.emitChange();
|
|
801
806
|
}
|
|
802
807
|
killLineBack() {
|
|
803
808
|
const start = this._lineStart();
|
|
804
809
|
if (start === this.cursorPosition)
|
|
805
810
|
return;
|
|
806
|
-
this._killBuffer
|
|
811
|
+
this._killBuffer.set(this.value.slice(start, this.cursorPosition));
|
|
807
812
|
this.value = this.value.slice(0, start) + this.value.slice(this.cursorPosition);
|
|
808
813
|
this.cursorPosition = start;
|
|
809
|
-
this._preferredColumn
|
|
814
|
+
this._preferredColumn.set(null);
|
|
810
815
|
this.emitChange();
|
|
811
816
|
}
|
|
812
817
|
killLineForward() {
|
|
813
818
|
const end = this._lineEnd();
|
|
814
819
|
if (end > this.cursorPosition) {
|
|
815
|
-
this._killBuffer
|
|
820
|
+
this._killBuffer.set(this.value.slice(this.cursorPosition, end));
|
|
816
821
|
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(end);
|
|
817
|
-
this._preferredColumn
|
|
822
|
+
this._preferredColumn.set(null);
|
|
818
823
|
this.emitChange();
|
|
819
824
|
return;
|
|
820
825
|
}
|
|
821
826
|
// Already at line end → kill the trailing `\n` (join next line).
|
|
822
827
|
if (this.cursorPosition < this.value.length) {
|
|
823
|
-
this._killBuffer
|
|
828
|
+
this._killBuffer.set("\n");
|
|
824
829
|
this.value = this.value.slice(0, this.cursorPosition) + this.value.slice(this.cursorPosition + 1);
|
|
825
|
-
this._preferredColumn
|
|
830
|
+
this._preferredColumn.set(null);
|
|
826
831
|
this.emitChange();
|
|
827
832
|
}
|
|
828
833
|
}
|
|
829
834
|
yank() {
|
|
830
|
-
|
|
835
|
+
const killed = this._killBuffer.get();
|
|
836
|
+
if (killed.length === 0)
|
|
831
837
|
return;
|
|
832
|
-
this._insertText(
|
|
838
|
+
this._insertText(killed);
|
|
833
839
|
}
|
|
834
840
|
transposeChars() {
|
|
835
841
|
// Readline `transpose-chars`: swap the char before the cursor with the
|
|
@@ -846,7 +852,7 @@ let TextInput = (() => {
|
|
|
846
852
|
return;
|
|
847
853
|
const cp1 = prevGrapheme(this.value, cp2);
|
|
848
854
|
this.value = this.value.slice(0, cp1) + this.value.slice(cp2) + this.value.slice(cp1, cp2);
|
|
849
|
-
this._preferredColumn
|
|
855
|
+
this._preferredColumn.set(null);
|
|
850
856
|
this.emitChange();
|
|
851
857
|
return;
|
|
852
858
|
}
|
|
@@ -857,7 +863,7 @@ let TextInput = (() => {
|
|
|
857
863
|
const charAt = this.value.slice(p, cpAfter);
|
|
858
864
|
this.value = this.value.slice(0, cpBefore) + charAt + charBefore + this.value.slice(cpAfter);
|
|
859
865
|
this.cursorPosition = cpAfter;
|
|
860
|
-
this._preferredColumn
|
|
866
|
+
this._preferredColumn.set(null);
|
|
861
867
|
this.emitChange();
|
|
862
868
|
}
|
|
863
869
|
// ─── Internal helpers ───────────────────────────────────────────────────
|
|
@@ -896,7 +902,7 @@ let TextInput = (() => {
|
|
|
896
902
|
toInsert +
|
|
897
903
|
this.value.slice(this.cursorPosition);
|
|
898
904
|
this.cursorPosition = end;
|
|
899
|
-
this._preferredColumn
|
|
905
|
+
this._preferredColumn.set(null);
|
|
900
906
|
this.emitChange();
|
|
901
907
|
}
|
|
902
908
|
// --- Rendering ---
|
|
@@ -908,56 +914,93 @@ let TextInput = (() => {
|
|
|
908
914
|
: this.contentStyleOverride ?? super.ground;
|
|
909
915
|
}
|
|
910
916
|
draw(options) {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
917
|
+
const drawn = { maxWidth: options.maxWidth, asciiOnly: options.asciiOnly ?? false };
|
|
918
|
+
// [LAW:effects-at-boundaries] Kept once the frame is drawn, never while it is.
|
|
919
|
+
afterPass(() => this._drawn.set(drawn));
|
|
920
|
+
const { lines, layout } = this._multiline ? this._renderMultiline(options, drawn) : this._renderSingleLine(options, drawn);
|
|
921
|
+
return Segment.anchorLines(lines, new Laid(layout)).flatMap((line) => [...line, Segment.line()]);
|
|
914
922
|
}
|
|
915
923
|
// [LAW:dataflow-not-control-flow] How a stretch of `value` is shown in one
|
|
916
|
-
// line derives from mode:
|
|
917
|
-
// glyph
|
|
924
|
+
// line derives glyph by glyph from mode: a mask cell in password mode, else
|
|
925
|
+
// the glyph itself — or, for a glyph of zero cells, a stand-in of one cell.
|
|
926
|
+
// Every glyph then has a column of its own, so each cursor stop draws the
|
|
927
|
+
// cursor somewhere new. The render and the click hit-test both read it.
|
|
928
|
+
_shownAs(options) {
|
|
929
|
+
const mask = drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH);
|
|
930
|
+
const newline = drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH);
|
|
931
|
+
const zeroCell = drawable(options, ZERO_CELL_GLYPH, ASCII_ZERO_CELL_GLYPH);
|
|
932
|
+
return (g) => this._password ? mask
|
|
933
|
+
: cellLen(g) > 0 ? g
|
|
934
|
+
: g === "\n" ? newline : zeroCell;
|
|
935
|
+
}
|
|
918
936
|
_shown(text, options) {
|
|
919
|
-
return this.
|
|
920
|
-
? drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH).repeat(graphemes(text).length)
|
|
921
|
-
: text.replace(/\n/g, drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH));
|
|
937
|
+
return graphemes(text).map(this._shownAs(options)).join("");
|
|
922
938
|
}
|
|
923
|
-
|
|
924
|
-
|
|
939
|
+
// The cells a stretch of `value` took in the last render.
|
|
940
|
+
_shownCells(text) {
|
|
941
|
+
return asCellCol(cellLen(this._shown(text, this._drawn.get())));
|
|
942
|
+
}
|
|
943
|
+
// The end of the glyphs of `text` whose cells, as a frame drawn under `drawn`
|
|
944
|
+
// shows them, lie wholly left of `col`, so a column inside a wide glyph
|
|
945
|
+
// lands before it.
|
|
946
|
+
_offsetAtShownCell(text, col, drawn) {
|
|
947
|
+
const shownAs = this._shownAs(drawn);
|
|
948
|
+
let at = 0;
|
|
949
|
+
let cells = 0;
|
|
950
|
+
for (const g of graphemes(text)) {
|
|
951
|
+
cells += cellLen(shownAs(g));
|
|
952
|
+
if (cells > col)
|
|
953
|
+
break;
|
|
954
|
+
at += g.length;
|
|
955
|
+
}
|
|
956
|
+
return asCodePoint(at);
|
|
957
|
+
}
|
|
958
|
+
// The window single-line content shows at `drawn`: what it displays, how
|
|
959
|
+
// many cells wide, from which cell, and the cursor's cell in it. The render
|
|
960
|
+
// draws it and a click maps through it.
|
|
961
|
+
_singleLine(drawn) {
|
|
925
962
|
// The cursor's column is the width of the shown text before it, whatever
|
|
926
963
|
// the mode — a placeholder is shown only over an empty value.
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
const rawDisplay = showPlaceholder ? this.placeholder : shown(this.value);
|
|
964
|
+
const shown = (text) => this._shown(text, drawn);
|
|
965
|
+
const rawDisplay = this._showsPlaceholder() ? this.placeholder : shown(this.value);
|
|
930
966
|
const rawCellWidth = asCellCol(cellLen(rawDisplay));
|
|
931
|
-
const
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
const
|
|
967
|
+
const beforeCursor = shown(this.value.slice(0, this.cursorPosition));
|
|
968
|
+
const cursorCellCol = asCellCol(cellLen(beforeCursor));
|
|
969
|
+
// The cursor's glyph, or the one cell past the text's end.
|
|
970
|
+
const cursorCells = Math.max(1, cellLen(shown(this.value.slice(this.cursorPosition, nextGrapheme(this.value, this.cursorPosition)))));
|
|
971
|
+
const maxAvailable = asCellCol(Math.max(MIN_CONTENT_WIDTH, drawn.maxWidth - 2));
|
|
972
|
+
const contentWidth = asCellCol(Math.min(maxAvailable, singleLineExtent(rawCellWidth, cursorCellCol)));
|
|
973
|
+
// The window (in cells) starts where the most of the text before the
|
|
974
|
+
// cursor that fits beside the cursor's glyph begins: a glyph boundary, so
|
|
975
|
+
// a wide glyph is never cut, and the cursor's glyph always in view.
|
|
976
|
+
const startCell = asCellCol(cursorCellCol - cellLen(cellFitEnd(beforeCursor, asCellCol(contentWidth - cursorCells))));
|
|
977
|
+
return { rawDisplay, contentWidth, startCell, cursorCellCol };
|
|
978
|
+
}
|
|
979
|
+
_showsPlaceholder() {
|
|
980
|
+
return this.focused && this.value.length === 0 && this.placeholder.length > 0;
|
|
981
|
+
}
|
|
982
|
+
_renderSingleLine(options, drawn) {
|
|
983
|
+
const { rawDisplay, contentWidth, startCell, cursorCellCol } = this._singleLine(drawn);
|
|
937
984
|
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
985
|
const [visible] = splitText(afterStart, contentWidth);
|
|
943
986
|
const display = setCellSize(visible, contentWidth);
|
|
944
|
-
const cursorDisplayCellCol = asCellCol(cursorCellCol -
|
|
987
|
+
const cursorDisplayCellCol = asCellCol(cursorCellCol - startCell);
|
|
945
988
|
const bracketStyle = this.disabled
|
|
946
989
|
? new Style(this.disabledInk)
|
|
947
990
|
: new Style(ink(this.theme, "foreground", "background"));
|
|
948
|
-
const contentStyle =
|
|
991
|
+
const contentStyle = this._showsPlaceholder() && !this.disabled
|
|
949
992
|
? new Style({ ...ink(this.theme, "foreground", "background"), dim: true })
|
|
950
993
|
: this.ground;
|
|
951
994
|
const cursorStyle = this.cursorStyleOverride ?? new Style(ink(this.theme, "on-primary", "primary"));
|
|
952
995
|
const segments = [new Segment("[", bracketStyle)];
|
|
953
|
-
if (this.focused && !this.disabled
|
|
996
|
+
if (this.focused && !this.disabled) {
|
|
954
997
|
const [before, rest] = splitText(display, cursorDisplayCellCol);
|
|
955
998
|
const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
|
|
956
999
|
const at = firstCh || " ";
|
|
957
1000
|
const after = rest.slice(firstCh.length);
|
|
958
1001
|
if (before.length > 0)
|
|
959
1002
|
segments.push(new Segment(before, contentStyle));
|
|
960
|
-
segments.push(
|
|
1003
|
+
segments.push(...this._caret(at, cursorStyle, contentStyle, options));
|
|
961
1004
|
if (after.length > 0)
|
|
962
1005
|
segments.push(new Segment(after, contentStyle));
|
|
963
1006
|
}
|
|
@@ -965,169 +1008,168 @@ let TextInput = (() => {
|
|
|
965
1008
|
segments.push(new Segment(display, contentStyle));
|
|
966
1009
|
}
|
|
967
1010
|
segments.push(new Segment("]", bracketStyle));
|
|
968
|
-
return segments;
|
|
1011
|
+
return { lines: [segments], layout: { drawn, kind: "line", value: this.value, startCell } };
|
|
969
1012
|
}
|
|
970
|
-
_renderMultiline(options) {
|
|
971
|
-
// [LAW:dataflow-not-control-flow] Decompose `value` into visual rows
|
|
972
|
-
//
|
|
973
|
-
|
|
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.
|
|
1013
|
+
_renderMultiline(options, drawn) {
|
|
1014
|
+
// [LAW:dataflow-not-control-flow] Decompose `value` into visual rows once,
|
|
1015
|
+
// then drive both display and cursor projection from the same array.
|
|
1016
|
+
const visualRows = this._rows(drawn);
|
|
984
1017
|
const total = visualRows.length;
|
|
985
|
-
const cursorRow = this.
|
|
986
|
-
|
|
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
|
-
}
|
|
1018
|
+
const cursorRow = cursorRowIn(visualRows, this.cursorPosition);
|
|
1019
|
+
const arrows = this._drawsArrows(total);
|
|
1008
1020
|
const contentStyle = this.ground;
|
|
1009
1021
|
const markerStyle = new Style({ ...ink(this.theme, "foreground", "background"), dim: true });
|
|
1010
1022
|
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
1023
|
const showCursor = this.focused && !this.disabled;
|
|
1024
|
-
|
|
1025
|
-
|
|
1024
|
+
// One visual row as drawn: its continuation marker, then its content, the
|
|
1025
|
+
// caret on the cursor's row.
|
|
1026
|
+
const rowAt = (rowIdx) => {
|
|
1026
1027
|
const row = visualRows[rowIdx];
|
|
1027
|
-
|
|
1028
|
-
|
|
1028
|
+
// A row past the value's last is `minRows` padding: blank.
|
|
1029
|
+
if (row === undefined)
|
|
1030
|
+
return [];
|
|
1031
|
+
const line = [];
|
|
1029
1032
|
if (row.isContinuation) {
|
|
1030
1033
|
// An ASCII stand-in as wide as the marker, so the rows keep their geometry.
|
|
1031
|
-
|
|
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 };
|
|
1034
|
+
line.push(new Segment(drawable(options, this._continuationMarker, ">".padEnd(this._markerWidth)), markerStyle));
|
|
1039
1035
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1036
|
+
this._emitRowContent(line, options, row, rowIdx === cursorRow && showCursor, contentStyle, cursorStyle);
|
|
1037
|
+
return line;
|
|
1038
|
+
};
|
|
1039
|
+
// The window is drawn at the width offered, or wider where the cursor's
|
|
1040
|
+
// row draws past it: a caret past the end of a full row.
|
|
1041
|
+
const offered = cellCount(options.maxWidth);
|
|
1042
|
+
const width = Math.max(offered, Segment.getLineLength(rowAt(cursorRow)));
|
|
1043
|
+
// Pad with empty rows when the value is shorter than `minRows`.
|
|
1044
|
+
const padRows = this._minRows !== undefined && this._maxRows === undefined ? Math.max(0, this._minRows - total) : 0;
|
|
1045
|
+
// [LAW:dataflow-not-control-flow] Only the rows a frame shows are drawn,
|
|
1046
|
+
// so a frame costs the rows in view and not the value's length. Every row
|
|
1047
|
+
// is padded to the width, so each has cells to stamp — a blank row
|
|
1048
|
+
// included — and the stamp names its row of the content.
|
|
1049
|
+
const owner = {};
|
|
1050
|
+
const rowLines = {
|
|
1051
|
+
layout: () => ({
|
|
1052
|
+
length: total + padRows,
|
|
1053
|
+
lines: (start, end) => Segment.anchorLines(Array.from({ length: end - start }, (_, i) => Segment.adjustLineLength(rowAt(start + i), width, contentStyle)), owner, start),
|
|
1054
|
+
}),
|
|
1055
|
+
measure: () => ({ minimum: width, maximum: width }),
|
|
1056
|
+
};
|
|
1057
|
+
// The viewport picks the window, at most `maxRows` tall whatever region the
|
|
1058
|
+
// field is placed in, with the cursor's row followed.
|
|
1059
|
+
const shown = Segment.splitLines(render(this._viewport.showingLines(rowLines, { start: cursorRow, end: cursorRow + 1 }), { ...options, maxWidth: width, height: this._maxRows === undefined ? undefined : { rows: this._maxRows, exact: false } }));
|
|
1060
|
+
// The content row the window's first row shows: the offset this frame
|
|
1061
|
+
// drew, read off what the viewport drew.
|
|
1062
|
+
const offset = shownRow(shown, owner);
|
|
1063
|
+
// The field is as wide as the widest row it shows, and never narrower than
|
|
1064
|
+
// its least measure the offer allows, so a blank field has cells to click;
|
|
1065
|
+
// a field with arrows is the whole offer, the arrows in its last cell. Read
|
|
1066
|
+
// off the rows in view, so a frame never measures the whole value.
|
|
1067
|
+
const widest = shown.reduce((w, _, i) => Math.max(w, Segment.getLineLength(rowAt(offset + i))), 0);
|
|
1068
|
+
const fieldWidth = arrows ? width : Math.min(width, Math.max(widest, Math.min(offered, MIN_CONTENT_WIDTH + 2)));
|
|
1069
|
+
// Scroll-direction arrows appear in the rightmost cell of the first/last
|
|
1070
|
+
// visible row, *only* when scroll is actually possible in that direction.
|
|
1071
|
+
// Only emitted in `"arrows"` mode; other modes leave the content area
|
|
1072
|
+
// untouched and rely on the host to surface scroll state externally (e.g.
|
|
1073
|
+
// Panel border accessory).
|
|
1074
|
+
const indicatorStyle = this.indicatorStyleOverride ?? new Style(ink(this.theme, "text-primary", "background"));
|
|
1075
|
+
const arrowOn = (i) => !arrows ? undefined
|
|
1076
|
+
: i === 0 && offset > 0 ? drawable(options, "▲", "^")
|
|
1077
|
+
: i === shown.length - 1 && offset + shown.length < total ? drawable(options, "▼", "v")
|
|
1078
|
+
: undefined;
|
|
1079
|
+
// The arrow's cells are reserved at the end of the offer, so a wide glyph
|
|
1080
|
+
// never collides with it: the row is cut to the cells before it. The arrow
|
|
1081
|
+
// keeps the place of the cell it covers, the viewport's included, so the
|
|
1082
|
+
// wheel over it scrolls the field.
|
|
1083
|
+
const withArrows = shown.map((line, i) => {
|
|
1084
|
+
const arrow = arrowOn(i);
|
|
1085
|
+
if (arrow === undefined)
|
|
1086
|
+
return Segment.adjustLineLength(line, fieldWidth, contentStyle);
|
|
1087
|
+
const at = offered - cellLen(arrow);
|
|
1088
|
+
const cell = indicatorStyle.withAnchor(Segment.anchorAt([line], at, 0));
|
|
1089
|
+
return Segment.adjustLineLength([...Segment.adjustLineLength(line, at, contentStyle), new Segment(arrow, cell)], fieldWidth, contentStyle);
|
|
1090
|
+
});
|
|
1091
|
+
return { lines: withArrows, layout: { drawn, kind: "rows", rows: visualRows, offset } };
|
|
1058
1092
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
//
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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));
|
|
1093
|
+
// The caret's cell for the frame `options` draws: the blink restarted at the
|
|
1094
|
+
// last touch, sampled at the frame's time (`shownAt`, so under reduced
|
|
1095
|
+
// motion it holds lit). It runs away from the restart in either direction
|
|
1096
|
+
// of time, so a timeline played backwards also finds it lit just after a
|
|
1097
|
+
// key. Lit, the cell is the cursor's; dark, it is drawn as the
|
|
1098
|
+
// text around it, since the cursor is a colour pair and no attribute hides
|
|
1099
|
+
// it. The cell says when it next flips, which is when a host draws again.
|
|
1100
|
+
_caret(at, cursorStyle, contentStyle, options) {
|
|
1101
|
+
const { frame } = options;
|
|
1102
|
+
const blink = (time) => {
|
|
1103
|
+
const shown = shownAt(Timeline.run(Timeline.play(this._blinkFrom, time < this._blinkFrom ? -1 : 1), CARET_BLINK), time, options);
|
|
1104
|
+
return { lit: shown.value, hold: holdOf(shown, time) };
|
|
1105
|
+
};
|
|
1106
|
+
const { lit, hold } = frame === undefined ? CARET_UNTIMED : blink(frame.time);
|
|
1107
|
+
return stampFrame([new Segment(at, lit ? cursorStyle : contentStyle)], hold);
|
|
1108
|
+
}
|
|
1109
|
+
// Whether `rows` visual rows draw a scroll arrow in the last cell of a row.
|
|
1110
|
+
_drawsArrows(rows) {
|
|
1111
|
+
return this._scrollIndicator === "arrows" && this._maxRows !== undefined && rows > this._maxRows;
|
|
1112
|
+
}
|
|
1113
|
+
_emitRowContent(out, options, row, cursorOnRow, contentStyle, cursorStyle) {
|
|
1114
|
+
const display = this._shown(row.content, options);
|
|
1115
|
+
// cursorPosition and valueStart are both cluster boundaries, so the
|
|
1116
|
+
// cursor's offset into the row is one too, and the shown cells before it
|
|
1117
|
+
// are its column.
|
|
1118
|
+
if (cursorOnRow) {
|
|
1119
|
+
const cursorCell = cellLen(this._shown(row.content.slice(0, this.cursorPosition - row.valueStart), options));
|
|
1120
|
+
const [before, rest] = splitText(display, asCellCol(cursorCell));
|
|
1097
1121
|
const firstCh = rest.slice(0, nextGrapheme(rest, asCodePoint(0)));
|
|
1098
1122
|
const at = firstCh || " ";
|
|
1099
1123
|
const after = rest.slice(firstCh.length);
|
|
1100
1124
|
if (before.length > 0)
|
|
1101
1125
|
out.push(new Segment(before, contentStyle));
|
|
1102
|
-
out.push(
|
|
1126
|
+
out.push(...this._caret(at, cursorStyle, contentStyle, options));
|
|
1103
1127
|
if (after.length > 0)
|
|
1104
1128
|
out.push(new Segment(after, contentStyle));
|
|
1105
1129
|
}
|
|
1106
|
-
else {
|
|
1107
|
-
|
|
1108
|
-
out.push(new Segment(paddedContent, contentStyle));
|
|
1130
|
+
else if (display.length > 0) {
|
|
1131
|
+
out.push(new Segment(display, contentStyle));
|
|
1109
1132
|
}
|
|
1110
|
-
out.push(new Segment(indicator.ch, indicator.style));
|
|
1111
1133
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1134
|
+
// The visual rows `value` lays out into at `drawn`. A width that bounds
|
|
1135
|
+
// nothing wraps nothing, so before any frame they are the logical lines.
|
|
1136
|
+
_rows(drawn) {
|
|
1137
|
+
const wrap = Number.isFinite(drawn.maxWidth) ? this._wrap : undefined;
|
|
1138
|
+
const firstWidth = asCellCol(Math.max(1, drawn.maxWidth));
|
|
1114
1139
|
const continuationWidth = asCellCol(Math.max(1, firstWidth - this._markerWidth));
|
|
1140
|
+
const shownAs = this._shownAs(drawn);
|
|
1115
1141
|
const rows = [];
|
|
1116
1142
|
const lines = this.value.split("\n");
|
|
1117
1143
|
let pos = 0;
|
|
1118
1144
|
for (let li = 0; li < lines.length; li++) {
|
|
1119
1145
|
const line = lines[li];
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1146
|
+
if (wrap !== undefined) {
|
|
1147
|
+
// `lineOffset[s]` is where offset `s` of the line as shown falls in
|
|
1148
|
+
// `line`: a glyph shown as itself maps code unit for code unit, and
|
|
1149
|
+
// every offset inside a stand-in falls at the start of its glyph, so
|
|
1150
|
+
// a row the strategy ends there draws the glyph on the next row.
|
|
1151
|
+
let shownLine = "";
|
|
1152
|
+
const lineOffset = [];
|
|
1153
|
+
let at = 0;
|
|
1154
|
+
for (const g of graphemes(line)) {
|
|
1155
|
+
const s = shownAs(g);
|
|
1156
|
+
for (let i = 0; i < s.length; i++)
|
|
1157
|
+
lineOffset.push(s === g ? at + i : at);
|
|
1158
|
+
shownLine += s;
|
|
1159
|
+
at += g.length;
|
|
1160
|
+
}
|
|
1161
|
+
lineOffset.push(at);
|
|
1162
|
+
const wrapRows = wrap(shownLine, { firstWidth, continuationWidth });
|
|
1122
1163
|
if (wrapRows.length === 0) {
|
|
1123
1164
|
rows.push({ content: "", valueStart: asCodePoint(pos), isContinuation: false });
|
|
1124
1165
|
}
|
|
1125
1166
|
else {
|
|
1126
1167
|
for (let ri = 0; ri < wrapRows.length; ri++) {
|
|
1127
1168
|
const wr = wrapRows[ri];
|
|
1169
|
+
const start = lineOffset[wr.start];
|
|
1128
1170
|
rows.push({
|
|
1129
|
-
content: wr.
|
|
1130
|
-
valueStart: asCodePoint(pos +
|
|
1171
|
+
content: line.slice(start, lineOffset[wr.end]),
|
|
1172
|
+
valueStart: asCodePoint(pos + start),
|
|
1131
1173
|
isContinuation: ri > 0,
|
|
1132
1174
|
});
|
|
1133
1175
|
}
|
|
@@ -1140,34 +1182,49 @@ let TextInput = (() => {
|
|
|
1140
1182
|
}
|
|
1141
1183
|
return rows;
|
|
1142
1184
|
}
|
|
1143
|
-
measure(
|
|
1185
|
+
measure(options) {
|
|
1144
1186
|
const minimum = MIN_CONTENT_WIDTH + 2;
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1187
|
+
const shownCells = (line) => cellLen(this._shown(line, options));
|
|
1188
|
+
// The widest the value draws, its cursor at the end of a line included. A
|
|
1189
|
+
// multi-line value draws each logical line on rows of its own, with no
|
|
1190
|
+
// brackets and no placeholder, and a scroll arrow's cell past each line
|
|
1191
|
+
// when its lines outnumber the rows.
|
|
1192
|
+
if (this._multiline) {
|
|
1193
|
+
const lines = this.value.split("\n");
|
|
1194
|
+
const arrowCells = this._drawsArrows(lines.length) ? 1 : 0;
|
|
1195
|
+
return { minimum, maximum: lines.reduce((w, line) => Math.max(w, shownCells(line) + 1 + arrowCells), minimum) };
|
|
1196
|
+
}
|
|
1197
|
+
// A single-line value draws its breaks as glyphs between brackets, and its
|
|
1198
|
+
// placeholder over an empty value.
|
|
1199
|
+
const valueCells = shownCells(this.value);
|
|
1200
|
+
return { minimum, maximum: singleLineExtent(this.value === "" ? cellLen(this.placeholder) : valueCells, valueCells) + 2 };
|
|
1147
1201
|
}
|
|
1148
1202
|
/**
|
|
1149
1203
|
* `[X/Y]` scroll-position string when there's actually something to
|
|
1150
1204
|
* scroll, else `undefined`. X is the cursor's 1-indexed visual row; Y is
|
|
1151
|
-
* the total visual row count.
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1205
|
+
* the total visual row count. Hand it a frame this field is drawn in — a
|
|
1206
|
+
* `Panel`'s `bottomRightAccessory` function is handed its body's:
|
|
1207
|
+
*
|
|
1208
|
+
* bottomRightAccessory: (body) => input.scrollIndicatorText(body)
|
|
1209
|
+
*
|
|
1210
|
+
* It counts the rows that frame laid out, read off the field's own cells, so
|
|
1211
|
+
* it is right on the frame it labels however deep the field sits in it.
|
|
1154
1212
|
*
|
|
1155
1213
|
* Returns `undefined` when:
|
|
1156
1214
|
* - `scrollIndicator` is not `"indices"`, or
|
|
1157
1215
|
* - `maxRows` is unset, or total visual rows ≤ maxRows (nothing to
|
|
1158
1216
|
* scroll), or
|
|
1159
|
-
* -
|
|
1217
|
+
* - `frame` shows none of the field's rows (a width too narrow to draw it).
|
|
1160
1218
|
*/
|
|
1161
|
-
|
|
1219
|
+
scrollIndicatorText(frame) {
|
|
1162
1220
|
if (this._scrollIndicator !== "indices")
|
|
1163
1221
|
return undefined;
|
|
1164
1222
|
if (this._maxRows === undefined)
|
|
1165
1223
|
return undefined;
|
|
1166
|
-
const
|
|
1167
|
-
if (
|
|
1224
|
+
const layout = layoutIn(frame, this);
|
|
1225
|
+
if (layout?.kind !== "rows" || layout.rows.length <= this._maxRows)
|
|
1168
1226
|
return undefined;
|
|
1169
|
-
|
|
1170
|
-
return `[${cursorRow1}/${rows.length}]`;
|
|
1227
|
+
return `[${cursorRowIn(layout.rows, this.cursorPosition) + 1}/${layout.rows.length}]`;
|
|
1171
1228
|
}
|
|
1172
1229
|
};
|
|
1173
1230
|
})();
|