@flowtty/core 1.0.0-alpha.6 → 1.0.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{backend-ode0Vc7y.d.ts → backend-BYzQoXfC.d.ts} +14 -2
- package/dist/chunk-AQ33T2P5.js +34 -0
- package/dist/host/index.d.ts +2 -2
- package/dist/host/index.js +32 -6
- package/dist/{host-zpuR6C1c.d.ts → host-6NVC0_E3.d.ts} +23 -1
- package/dist/index.d.ts +35 -5
- package/dist/index.js +90 -22
- package/dist/testing/index.d.ts +6 -2
- package/dist/testing/index.js +27 -1
- package/package.json +1 -1
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { B as Buffer } from './cells-CaXEx4lH.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Every multi-character key name a backend produces. A printable key's name is
|
|
5
|
+
* the character itself (`' '`, `':'`, `'a'`) — there is no `'space'`, `'enter'`
|
|
6
|
+
* or `'colon'`. Sequences the key parser does not recognize surface under a
|
|
7
|
+
* `csi-…` name, which is deliberately not listed here.
|
|
8
|
+
*/
|
|
9
|
+
declare const NAMED_KEYS: readonly ["return", "escape", "tab", "backspace", "delete", "insert", "up", "down", "left", "right", "home", "end", "pageup", "pagedown", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "paste", "wheelup", "wheeldown"];
|
|
10
|
+
type NamedKey = typeof NAMED_KEYS[number];
|
|
11
|
+
/** A key name: a named key, or the character itself for a printable key. The
|
|
12
|
+
* `string & {}` arm keeps any character valid while editors still complete the
|
|
13
|
+
* named ones — it cannot reject a typo like `'space'`; `TestBackend.press` does. */
|
|
14
|
+
type KeyName = NamedKey | (string & {});
|
|
3
15
|
interface Key {
|
|
4
16
|
/**
|
|
5
17
|
* Canonical name of the key. For printable ASCII characters this is the
|
|
@@ -9,7 +21,7 @@ interface Key {
|
|
|
9
21
|
* whose content is in `text`. Mouse wheel steps are 'wheelup' / 'wheeldown',
|
|
10
22
|
* positioned by `x` / `y`.
|
|
11
23
|
*/
|
|
12
|
-
name:
|
|
24
|
+
name: KeyName;
|
|
13
25
|
/**
|
|
14
26
|
* Payload of a 'paste' key: the pasted text, line endings normalized to `\n`.
|
|
15
27
|
* A paste is delivered whole — its newlines and letters are text, never
|
|
@@ -87,4 +99,4 @@ interface Backend {
|
|
|
87
99
|
hyperlinks?: boolean;
|
|
88
100
|
}
|
|
89
101
|
|
|
90
|
-
export type
|
|
102
|
+
export { type Backend as B, type Key as K, NAMED_KEYS as N, type KeyName as a, type NamedKey as b };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/keys.ts
|
|
2
|
+
var NAMED_KEYS = [
|
|
3
|
+
"return",
|
|
4
|
+
"escape",
|
|
5
|
+
"tab",
|
|
6
|
+
"backspace",
|
|
7
|
+
"delete",
|
|
8
|
+
"insert",
|
|
9
|
+
"up",
|
|
10
|
+
"down",
|
|
11
|
+
"left",
|
|
12
|
+
"right",
|
|
13
|
+
"home",
|
|
14
|
+
"end",
|
|
15
|
+
"pageup",
|
|
16
|
+
"pagedown",
|
|
17
|
+
"f1",
|
|
18
|
+
"f2",
|
|
19
|
+
"f3",
|
|
20
|
+
"f4",
|
|
21
|
+
"f5",
|
|
22
|
+
"f6",
|
|
23
|
+
"f7",
|
|
24
|
+
"f8",
|
|
25
|
+
"f9",
|
|
26
|
+
"f10",
|
|
27
|
+
"f11",
|
|
28
|
+
"f12",
|
|
29
|
+
"paste",
|
|
30
|
+
"wheelup",
|
|
31
|
+
"wheeldown"
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export { NAMED_KEYS };
|
package/dist/host/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as Container } from '../host-
|
|
2
|
-
export { B as BORDER_CHARS, H as HostType, I as Instance, R as Rect, T as TextInstance, Y as Yoga, a as YogaNode, b as appendChild, c as applyProps, d as computeLayout, e as createInstance, f as createTextInstance, g as getYoga, i as insertBefore, l as layoutOf, m as measureText, o as ownText, r as refreshMeasure, h as removeChild } from '../host-
|
|
1
|
+
import { C as Container } from '../host-6NVC0_E3.js';
|
|
2
|
+
export { B as BORDER_CHARS, H as HostType, I as Instance, R as Rect, T as TextInstance, Y as Yoga, a as YogaNode, b as appendChild, c as applyProps, d as computeLayout, e as createInstance, f as createTextInstance, g as getYoga, i as insertBefore, l as layoutOf, m as measureText, o as ownText, r as refreshMeasure, h as removeChild } from '../host-6NVC0_E3.js';
|
|
3
3
|
import { B as Buffer } from '../cells-CaXEx4lH.js';
|
|
4
4
|
import 'yoga-layout/load';
|
|
5
5
|
|
package/dist/host/index.js
CHANGED
|
@@ -315,13 +315,22 @@ function contentRectOf(inst, box) {
|
|
|
315
315
|
height: Math.max(0, box.height - padT - padB)
|
|
316
316
|
};
|
|
317
317
|
}
|
|
318
|
+
function paddingRectOf(inst, box) {
|
|
319
|
+
const n = inst.yogaNode;
|
|
320
|
+
const t = n.getComputedBorder(Edge.Top);
|
|
321
|
+
const r = n.getComputedBorder(Edge.Right);
|
|
322
|
+
const b = n.getComputedBorder(Edge.Bottom);
|
|
323
|
+
const l = n.getComputedBorder(Edge.Left);
|
|
324
|
+
return { left: box.left + l, top: box.top + t, width: Math.max(0, box.width - l - r), height: Math.max(0, box.height - t - b) };
|
|
325
|
+
}
|
|
318
326
|
function paintInstance(inst, buffer, offsetX, offsetY, inheritedBg = void 0, clip = null) {
|
|
319
327
|
if (inst.props.display === "none") return;
|
|
320
328
|
const box = layoutOf(inst, offsetX, offsetY);
|
|
321
329
|
inst.props.onLayout?.(box);
|
|
322
330
|
const ownBg = inst.props.backgroundColor;
|
|
323
331
|
const effectiveBg = ownBg ?? inheritedBg;
|
|
324
|
-
|
|
332
|
+
const offscreen = clip !== null && (box.left >= clip.left + clip.width || box.left + box.width <= clip.left || box.top >= clip.top + clip.height || box.top + box.height <= clip.top);
|
|
333
|
+
if (!offscreen && ownBg !== void 0) {
|
|
325
334
|
const fillStyle = ownBg === "default" ? {} : { bg: ownBg };
|
|
326
335
|
for (let y = box.top; y < box.top + box.height; y++) {
|
|
327
336
|
for (let x = box.left; x < box.left + box.width; x++) {
|
|
@@ -329,8 +338,8 @@ function paintInstance(inst, buffer, offsetX, offsetY, inheritedBg = void 0, cli
|
|
|
329
338
|
}
|
|
330
339
|
}
|
|
331
340
|
}
|
|
332
|
-
paintBorder(inst, buffer, box, clip, effectiveBg);
|
|
333
|
-
const text = ownText(inst);
|
|
341
|
+
if (!offscreen) paintBorder(inst, buffer, box, clip, effectiveBg);
|
|
342
|
+
const text = offscreen ? "" : ownText(inst);
|
|
334
343
|
if (text) {
|
|
335
344
|
const content = contentRectOf(inst, box);
|
|
336
345
|
const mode = inst.props.wrap ?? "none";
|
|
@@ -350,7 +359,23 @@ function paintInstance(inst, buffer, offsetX, offsetY, inheritedBg = void 0, cli
|
|
|
350
359
|
}
|
|
351
360
|
}
|
|
352
361
|
}
|
|
353
|
-
const
|
|
362
|
+
const scrolls = inst.props.scrollTop !== void 0 || inst.props.scrollBottom !== void 0;
|
|
363
|
+
let scrollTop = 0;
|
|
364
|
+
if (scrolls || inst.props.onScrollMetrics) {
|
|
365
|
+
const viewport = contentRectOf(inst, box);
|
|
366
|
+
let contentBottom = 0;
|
|
367
|
+
for (const child of inst.children) {
|
|
368
|
+
if (child.type !== "box" || child.props.position === "absolute" || child.props.display === "none") continue;
|
|
369
|
+
const n = child.yogaNode;
|
|
370
|
+
contentBottom = Math.max(contentBottom, n.getComputedTop() + n.getComputedHeight() + n.getComputedMargin(Edge.Bottom));
|
|
371
|
+
}
|
|
372
|
+
const contentHeight = Math.max(0, contentBottom - (viewport.top - box.top));
|
|
373
|
+
const maxScrollTop = Math.max(0, contentHeight - viewport.height);
|
|
374
|
+
const wanted = inst.props.scrollBottom !== void 0 ? maxScrollTop - inst.props.scrollBottom : inst.props.scrollTop ?? 0;
|
|
375
|
+
scrollTop = Math.max(0, Math.min(maxScrollTop, Math.round(wanted)));
|
|
376
|
+
inst.props.onScrollMetrics?.({ contentHeight, viewportHeight: viewport.height, scrollTop, maxScrollTop });
|
|
377
|
+
}
|
|
378
|
+
const childClip = inst.props.overflow === "hidden" || scrolls ? intersectRects(clip, contentRectOf(inst, box)) : clip;
|
|
354
379
|
const stackFlow = [];
|
|
355
380
|
const absolutes = [];
|
|
356
381
|
for (const child of inst.children) {
|
|
@@ -360,8 +385,9 @@ function paintInstance(inst, buffer, offsetX, offsetY, inheritedBg = void 0, cli
|
|
|
360
385
|
const byZ = (a, b) => (a.props.zIndex ?? 0) - (b.props.zIndex ?? 0);
|
|
361
386
|
stackFlow.sort(byZ);
|
|
362
387
|
absolutes.sort(byZ);
|
|
363
|
-
for (const child of stackFlow) paintInstance(child, buffer, box.left, box.top, effectiveBg, childClip);
|
|
364
|
-
|
|
388
|
+
for (const child of stackFlow) paintInstance(child, buffer, box.left, box.top - scrollTop, effectiveBg, childClip);
|
|
389
|
+
const overlayClip = scrolls ? intersectRects(clip, paddingRectOf(inst, box)) : childClip;
|
|
390
|
+
for (const child of absolutes) paintInstance(child, buffer, box.left, box.top, effectiveBg, overlayClip);
|
|
365
391
|
}
|
|
366
392
|
|
|
367
393
|
export { appendChild, applyProps, computeLayout, createInstance, createTextInstance, getYoga, insertBefore, layoutOf, measureText, ownText, paint, refreshMeasure, removeChild };
|
|
@@ -114,6 +114,28 @@ interface BoxProps {
|
|
|
114
114
|
* 'hidden' clips ALL descendant writes including their backgrounds and borders.
|
|
115
115
|
* Does NOT clip this box's own background or border (those are this box's own area). */
|
|
116
116
|
overflow?: 'visible' | 'hidden';
|
|
117
|
+
/** Scroll the content up by this many rows (0 = top). Setting either scroll
|
|
118
|
+
* prop makes the box a scroll viewport: it clips like `overflow: 'hidden'`
|
|
119
|
+
* and paints its flow children shifted. The value is clamped to the scrollable
|
|
120
|
+
* range at paint time, against the content's CURRENT height — so it never
|
|
121
|
+
* needs a second frame to catch up. `position: 'absolute'` children are
|
|
122
|
+
* overlays: they neither scroll nor count as content (a scrollbar, a
|
|
123
|
+
* "new messages" badge). */
|
|
124
|
+
scrollTop?: number;
|
|
125
|
+
/** Like `scrollTop`, but counted from the END of the content: 0 pins the last
|
|
126
|
+
* rows into view and keeps them there as content grows. Wins over `scrollTop`
|
|
127
|
+
* when both are set. */
|
|
128
|
+
scrollBottom?: number;
|
|
129
|
+
/** Fires every paint (diff before setState, like onLayout) with the numbers a
|
|
130
|
+
* scrolling component needs: content and viewport heights, the effective
|
|
131
|
+
* (clamped) `scrollTop`, and its maximum. */
|
|
132
|
+
onScrollMetrics?: (metrics: ScrollMetrics) => void;
|
|
133
|
+
}
|
|
134
|
+
interface ScrollMetrics {
|
|
135
|
+
contentHeight: number;
|
|
136
|
+
viewportHeight: number;
|
|
137
|
+
scrollTop: number;
|
|
138
|
+
maxScrollTop: number;
|
|
117
139
|
}
|
|
118
140
|
interface Instance {
|
|
119
141
|
type: 'box';
|
|
@@ -149,4 +171,4 @@ declare function appendChild(parent: Instance, child: Instance | TextInstance, Y
|
|
|
149
171
|
declare function removeChild(parent: Instance, child: Instance | TextInstance, Yoga: Yoga): void;
|
|
150
172
|
declare function insertBefore(parent: Instance, child: Instance | TextInstance, before: Instance | TextInstance, Yoga: Yoga): void;
|
|
151
173
|
|
|
152
|
-
export { BORDER_CHARS as B, type Container as C, DEFAULT_BORDER_STYLE as D, GRID_CHARS as G, type HostType as H, type Instance as I, type Rect as R, type TextInstance as T, type Yoga as Y, type YogaNode as a, appendChild as b, applyProps as c, computeLayout as d, createInstance as e, createTextInstance as f, getYoga as g, removeChild as h, insertBefore as i, type BorderChars as j, type BorderStyle as k, layoutOf as l, measureText as m, type BoxProps as n, ownText as o, type GridChars as p, refreshMeasure as r };
|
|
174
|
+
export { BORDER_CHARS as B, type Container as C, DEFAULT_BORDER_STYLE as D, GRID_CHARS as G, type HostType as H, type Instance as I, type Rect as R, type ScrollMetrics as S, type TextInstance as T, type Yoga as Y, type YogaNode as a, appendChild as b, applyProps as c, computeLayout as d, createInstance as e, createTextInstance as f, getYoga as g, removeChild as h, insertBefore as i, type BorderChars as j, type BorderStyle as k, layoutOf as l, measureText as m, type BoxProps as n, ownText as o, type GridChars as p, refreshMeasure as r };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { B as Buffer, C as Cell, S as Style } from './cells-CaXEx4lH.js';
|
|
2
|
-
import { K as Key } from './backend-
|
|
3
|
-
export { B as Backend } from './backend-
|
|
4
|
-
export { j as BorderChars, k as BorderStyle, n as BoxProps, D as DEFAULT_BORDER_STYLE, G as GRID_CHARS, p as GridChars } from './host-
|
|
2
|
+
import { K as Key } from './backend-BYzQoXfC.js';
|
|
3
|
+
export { B as Backend, a as KeyName, N as NAMED_KEYS, b as NamedKey } from './backend-BYzQoXfC.js';
|
|
4
|
+
export { j as BorderChars, k as BorderStyle, n as BoxProps, D as DEFAULT_BORDER_STYLE, G as GRID_CHARS, p as GridChars, S as ScrollMetrics } from './host-6NVC0_E3.js';
|
|
5
5
|
import 'yoga-layout/load';
|
|
6
6
|
|
|
7
7
|
type WrapMode = 'wrap' | 'truncate' | 'none';
|
|
@@ -82,6 +82,10 @@ declare function stringWidth(str: string): number;
|
|
|
82
82
|
|
|
83
83
|
interface EditorState {
|
|
84
84
|
value: string;
|
|
85
|
+
/** UTF-16 index into `value` (so `value.slice(0, cursor)` is the text before
|
|
86
|
+
* the caret). It only ever rests on a character boundary: movement and
|
|
87
|
+
* deletion step over a whole code point, and a cursor handed in between the
|
|
88
|
+
* two halves of a surrogate pair is snapped back before anything else. */
|
|
85
89
|
cursor: number;
|
|
86
90
|
}
|
|
87
91
|
type EditorAction = {
|
|
@@ -94,7 +98,33 @@ type EditorAction = {
|
|
|
94
98
|
} | {
|
|
95
99
|
kind: 'noop';
|
|
96
100
|
};
|
|
97
|
-
|
|
101
|
+
interface EditorOptions {
|
|
102
|
+
/** Multi-line editing: line breaks are part of the value. Shift+Enter,
|
|
103
|
+
* Alt+Enter and backslash-then-Enter insert one (plain Enter still submits),
|
|
104
|
+
* up/down move between visual rows, Home/End and the kill bindings work
|
|
105
|
+
* within the current line, and a paste keeps its line breaks. */
|
|
106
|
+
multiline?: boolean;
|
|
107
|
+
/** Wrap width in cells — what up/down use to walk soft-wrapped rows. Only
|
|
108
|
+
* read when `multiline`; without it each source line counts as one row. */
|
|
109
|
+
width?: number;
|
|
110
|
+
}
|
|
111
|
+
declare function reduce$2(state: EditorState, key: Key, opts?: EditorOptions): EditorAction;
|
|
112
|
+
|
|
113
|
+
interface InputRow {
|
|
114
|
+
/** Text of this visual row (no line break). */
|
|
115
|
+
text: string;
|
|
116
|
+
/** Index into the value where this row starts. */
|
|
117
|
+
start: number;
|
|
118
|
+
/** True for a soft-wrap continuation of the previous row's source line. */
|
|
119
|
+
continuation: boolean;
|
|
120
|
+
}
|
|
121
|
+
declare function inputRows(value: string, width: number, cursor?: number): InputRow[];
|
|
122
|
+
/** UTF-16 index of character column `col` within `row` (clamped to its end). */
|
|
123
|
+
declare function rowIndexAt(row: InputRow, col: number): number;
|
|
124
|
+
declare function caretPosition(value: string, cursor: number, width: number): {
|
|
125
|
+
row: number;
|
|
126
|
+
col: number;
|
|
127
|
+
};
|
|
98
128
|
|
|
99
129
|
interface SelectItem<T> {
|
|
100
130
|
label: string;
|
|
@@ -140,4 +170,4 @@ type MultiSelectAction = {
|
|
|
140
170
|
};
|
|
141
171
|
declare function reduce<T>(items: SelectItem<T>[], state: MultiSelectState, key: Key): MultiSelectAction;
|
|
142
172
|
|
|
143
|
-
export { type EditorAction, type EditorState, Key, type MultiSelectAction, type MultiSelectState, type SelectAction, type SelectItem, type SelectState, type VisualLine, type WrapMode, charWidth, reduce$2 as editorReducer, reduce as multiSelectReducer, reduce$1 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround, wrapText };
|
|
173
|
+
export { type EditorAction, type EditorOptions, type EditorState, type InputRow, Key, type MultiSelectAction, type MultiSelectState, type SelectAction, type SelectItem, type SelectState, type VisualLine, type WrapMode, caretPosition, charWidth, reduce$2 as editorReducer, inputRows, reduce as multiSelectReducer, rowIndexAt, reduce$1 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround, wrapText };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Buffer, DEFAULT_BORDER_STYLE, GRID_CHARS, wrapText } from './chunk-D6HW2BNM.js';
|
|
2
|
+
export { NAMED_KEYS } from './chunk-AQ33T2P5.js';
|
|
2
3
|
|
|
3
4
|
// src/visualLines.ts
|
|
4
5
|
function splitVisualLines(text, mode, width) {
|
|
@@ -280,7 +281,50 @@ function stringWidth(str) {
|
|
|
280
281
|
return w;
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
// src/inputRows.ts
|
|
285
|
+
function inputRows(value, width, cursor) {
|
|
286
|
+
const w = Math.max(1, Math.floor(width));
|
|
287
|
+
const rows = [];
|
|
288
|
+
let lineStart = 0;
|
|
289
|
+
for (const line of value.split("\n")) {
|
|
290
|
+
const chars = [...line];
|
|
291
|
+
if (chars.length === 0) {
|
|
292
|
+
rows.push({ text: "", start: lineStart, continuation: false });
|
|
293
|
+
} else {
|
|
294
|
+
let start = lineStart;
|
|
295
|
+
for (let at = 0; at < chars.length; at += w) {
|
|
296
|
+
const text = chars.slice(at, at + w).join("");
|
|
297
|
+
rows.push({ text, start, continuation: at > 0 });
|
|
298
|
+
start += text.length;
|
|
299
|
+
}
|
|
300
|
+
if (cursor === lineStart + line.length && chars.length % w === 0) {
|
|
301
|
+
rows.push({ text: "", start: cursor, continuation: true });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
lineStart += line.length + 1;
|
|
305
|
+
}
|
|
306
|
+
return rows;
|
|
307
|
+
}
|
|
308
|
+
function rowIndexAt(row, col) {
|
|
309
|
+
return row.start + [...row.text].slice(0, Math.max(0, col)).join("").length;
|
|
310
|
+
}
|
|
311
|
+
function caretPosition(value, cursor, width) {
|
|
312
|
+
const c = Math.max(0, Math.min(value.length, cursor));
|
|
313
|
+
const rows = inputRows(value, width, c);
|
|
314
|
+
for (let r = rows.length - 1; r >= 0; r--) {
|
|
315
|
+
const row = rows[r];
|
|
316
|
+
if (row.start <= c && c <= row.start + row.text.length) {
|
|
317
|
+
return { row: r, col: [...value.slice(row.start, c)].length };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return { row: 0, col: 0 };
|
|
321
|
+
}
|
|
322
|
+
|
|
283
323
|
// src/editor.ts
|
|
324
|
+
var isHigh = (u) => u !== void 0 && u >= "\uD800" && u <= "\uDBFF";
|
|
325
|
+
var isLow = (u) => u !== void 0 && u >= "\uDC00" && u <= "\uDFFF";
|
|
326
|
+
var prevIndex = (v, i) => i >= 2 && isLow(v[i - 1]) && isHigh(v[i - 2]) ? i - 2 : Math.max(0, i - 1);
|
|
327
|
+
var nextIndex = (v, i) => isHigh(v[i]) && isLow(v[i + 1]) ? i + 2 : Math.min(v.length, i + 1);
|
|
284
328
|
var OPT_MAP = {
|
|
285
329
|
" ": ["\xA0"],
|
|
286
330
|
// U+00A0 NBSP
|
|
@@ -304,24 +348,50 @@ function wordRight(value, cursor) {
|
|
|
304
348
|
while (c < value.length && isWord(value[c])) c++;
|
|
305
349
|
return c;
|
|
306
350
|
}
|
|
307
|
-
function reduce(state, key) {
|
|
308
|
-
const { value
|
|
309
|
-
|
|
351
|
+
function reduce(state, key, opts = {}) {
|
|
352
|
+
const { value } = state;
|
|
353
|
+
let cursor = Math.max(0, Math.min(value.length, state.cursor));
|
|
354
|
+
if (isLow(value[cursor]) && isHigh(value[cursor - 1])) cursor--;
|
|
355
|
+
const multiline = opts.multiline === true;
|
|
356
|
+
const lineStart = multiline ? value.lastIndexOf("\n", cursor - 1) + 1 : 0;
|
|
357
|
+
const nextBreak = multiline ? value.indexOf("\n", cursor) : -1;
|
|
358
|
+
const lineEnd = nextBreak < 0 ? value.length : nextBreak;
|
|
359
|
+
const insert = (text) => ({
|
|
360
|
+
kind: "edit",
|
|
361
|
+
state: { value: value.slice(0, cursor) + text + value.slice(cursor), cursor: cursor + text.length }
|
|
362
|
+
});
|
|
310
363
|
if (key.name === "paste") {
|
|
311
|
-
const
|
|
364
|
+
const pasted = (key.text ?? "").replace(/\r\n?/g, "\n");
|
|
365
|
+
const text = multiline ? pasted : pasted.replace(/\n/g, " ");
|
|
312
366
|
if (text === "") return { kind: "noop" };
|
|
313
|
-
return
|
|
367
|
+
return insert(text);
|
|
368
|
+
}
|
|
369
|
+
if (multiline) {
|
|
370
|
+
if (key.name === "return" && (key.shift || key.meta)) return insert("\n");
|
|
371
|
+
if (key.name === "return" && cursor > 0 && value[cursor - 1] === "\\") {
|
|
372
|
+
return { kind: "edit", state: { value: value.slice(0, cursor - 1) + "\n" + value.slice(cursor), cursor } };
|
|
373
|
+
}
|
|
374
|
+
if (key.name === "up" || key.name === "down") {
|
|
375
|
+
const width = opts.width ?? Number.MAX_SAFE_INTEGER;
|
|
376
|
+
const rows = inputRows(value, width, cursor);
|
|
377
|
+
const at = caretPosition(value, cursor, width);
|
|
378
|
+
const target = at.row + (key.name === "up" ? -1 : 1);
|
|
379
|
+
if (target < 0) return { kind: "edit", state: { value, cursor: 0 } };
|
|
380
|
+
if (target >= rows.length) return { kind: "edit", state: { value, cursor: value.length } };
|
|
381
|
+
const row = rows[target];
|
|
382
|
+
return { kind: "edit", state: { value, cursor: rowIndexAt(row, at.col) } };
|
|
383
|
+
}
|
|
314
384
|
}
|
|
315
385
|
if (key.name === "left" && key.meta) return { kind: "edit", state: { value, cursor: wordLeft(value, cursor) } };
|
|
316
386
|
if (key.name === "b" && key.meta) return { kind: "edit", state: { value, cursor: wordLeft(value, cursor) } };
|
|
317
387
|
if (key.name === "right" && key.meta) return { kind: "edit", state: { value, cursor: wordRight(value, cursor) } };
|
|
318
388
|
if (key.name === "f" && key.meta) return { kind: "edit", state: { value, cursor: wordRight(value, cursor) } };
|
|
319
|
-
if (key.name === "left") return { kind: "edit", state: { value, cursor:
|
|
320
|
-
if (key.name === "right") return { kind: "edit", state: { value, cursor:
|
|
321
|
-
if (key.name === "home") return { kind: "edit", state: { value, cursor:
|
|
322
|
-
if (key.name === "end") return { kind: "edit", state: { value, cursor:
|
|
323
|
-
if (key.name === "a" && key.ctrl) return { kind: "edit", state: { value, cursor:
|
|
324
|
-
if (key.name === "e" && key.ctrl) return { kind: "edit", state: { value, cursor:
|
|
389
|
+
if (key.name === "left") return { kind: "edit", state: { value, cursor: prevIndex(value, cursor) } };
|
|
390
|
+
if (key.name === "right") return { kind: "edit", state: { value, cursor: nextIndex(value, cursor) } };
|
|
391
|
+
if (key.name === "home") return { kind: "edit", state: { value, cursor: lineStart } };
|
|
392
|
+
if (key.name === "end") return { kind: "edit", state: { value, cursor: lineEnd } };
|
|
393
|
+
if (key.name === "a" && key.ctrl) return { kind: "edit", state: { value, cursor: lineStart } };
|
|
394
|
+
if (key.name === "e" && key.ctrl) return { kind: "edit", state: { value, cursor: lineEnd } };
|
|
325
395
|
if (key.name === "backspace" && key.meta) {
|
|
326
396
|
const start = wordLeft(value, cursor);
|
|
327
397
|
return { kind: "edit", state: { value: value.slice(0, start) + value.slice(cursor), cursor: start } };
|
|
@@ -339,10 +409,10 @@ function reduce(state, key) {
|
|
|
339
409
|
return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(end), cursor } };
|
|
340
410
|
}
|
|
341
411
|
if (key.name === "k" && key.ctrl) {
|
|
342
|
-
return { kind: "edit", state: { value: value.slice(0, cursor), cursor } };
|
|
412
|
+
return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(lineEnd), cursor } };
|
|
343
413
|
}
|
|
344
414
|
if (key.name === "u" && key.ctrl) {
|
|
345
|
-
return { kind: "edit", state: { value: value.slice(cursor), cursor:
|
|
415
|
+
return { kind: "edit", state: { value: value.slice(0, lineStart) + value.slice(cursor), cursor: lineStart } };
|
|
346
416
|
}
|
|
347
417
|
if (key.meta && OPT_MAP[key.name]) {
|
|
348
418
|
const entry = OPT_MAP[key.name];
|
|
@@ -350,19 +420,17 @@ function reduce(state, key) {
|
|
|
350
420
|
return { kind: "edit", state: { value: value.slice(0, cursor) + ch + value.slice(cursor), cursor: cursor + 1 } };
|
|
351
421
|
}
|
|
352
422
|
if (key.name === "backspace") {
|
|
353
|
-
if (cursor === 0) return { kind: "edit", state };
|
|
354
|
-
|
|
423
|
+
if (cursor === 0) return { kind: "edit", state: { value, cursor } };
|
|
424
|
+
const from = prevIndex(value, cursor);
|
|
425
|
+
return { kind: "edit", state: { value: value.slice(0, from) + value.slice(cursor), cursor: from } };
|
|
355
426
|
}
|
|
356
427
|
if (key.name === "delete" || key.name === "d" && key.ctrl) {
|
|
357
|
-
if (cursor === value.length) return { kind: "edit", state };
|
|
358
|
-
return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(cursor
|
|
428
|
+
if (cursor === value.length) return { kind: "edit", state: { value, cursor } };
|
|
429
|
+
return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(nextIndex(value, cursor)), cursor } };
|
|
359
430
|
}
|
|
360
431
|
if (key.name === "return") return { kind: "submit" };
|
|
361
432
|
if (key.name === "escape") return { kind: "cancel" };
|
|
362
|
-
if (!key.ctrl && !key.meta && key.name.length === 1)
|
|
363
|
-
const ch = key.name;
|
|
364
|
-
return { kind: "edit", state: { value: value.slice(0, cursor) + ch + value.slice(cursor), cursor: cursor + 1 } };
|
|
365
|
-
}
|
|
433
|
+
if (!key.ctrl && !key.meta && [...key.name].length === 1) return insert(key.name);
|
|
366
434
|
return { kind: "noop" };
|
|
367
435
|
}
|
|
368
436
|
|
|
@@ -422,4 +490,4 @@ function reduce3(items, state, key) {
|
|
|
422
490
|
return { kind: "noop" };
|
|
423
491
|
}
|
|
424
492
|
|
|
425
|
-
export { charWidth, reduce as editorReducer, reduce3 as multiSelectReducer, reduce2 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround };
|
|
493
|
+
export { caretPosition, charWidth, reduce as editorReducer, inputRows, reduce3 as multiSelectReducer, rowIndexAt, reduce2 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround };
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { B as Buffer } from '../cells-CaXEx4lH.js';
|
|
2
|
-
import { B as Backend, K as Key } from '../backend-
|
|
2
|
+
import { B as Backend, K as Key } from '../backend-BYzQoXfC.js';
|
|
3
3
|
|
|
4
4
|
declare class TestBackend implements Backend {
|
|
5
5
|
private readonly cols;
|
|
@@ -16,7 +16,11 @@ declare class TestBackend implements Backend {
|
|
|
16
16
|
get lastFrame(): string;
|
|
17
17
|
get lastBuffer(): Buffer | null;
|
|
18
18
|
onKey(handler: (key: Key) => void): () => void;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Synchronously deliver one Key to every subscriber. Throws on a name no
|
|
21
|
+
* terminal can produce (`'space'`, `'enter'`): a test that presses such a key
|
|
22
|
+
* exercises a branch real input never reaches, and would pass anyway.
|
|
23
|
+
*/
|
|
20
24
|
press(key: Partial<Key> & {
|
|
21
25
|
name: string;
|
|
22
26
|
}): void;
|
package/dist/testing/index.js
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
+
import { NAMED_KEYS } from '../chunk-AQ33T2P5.js';
|
|
2
|
+
|
|
1
3
|
// src/testing/test-backend.ts
|
|
4
|
+
var KEY_NAME_HINTS = {
|
|
5
|
+
space: " ",
|
|
6
|
+
enter: "return",
|
|
7
|
+
esc: "escape",
|
|
8
|
+
del: "delete",
|
|
9
|
+
ins: "insert",
|
|
10
|
+
pgup: "pageup",
|
|
11
|
+
pgdn: "pagedown",
|
|
12
|
+
pgdown: "pagedown",
|
|
13
|
+
bs: "backspace"
|
|
14
|
+
};
|
|
15
|
+
function assertRealKeyName(name) {
|
|
16
|
+
if ([...name].length === 1 || name.startsWith("csi-")) return;
|
|
17
|
+
if (NAMED_KEYS.includes(name)) return;
|
|
18
|
+
const hint = KEY_NAME_HINTS[name.toLowerCase()];
|
|
19
|
+
throw new Error(
|
|
20
|
+
`TestBackend.press: '${name}' is not a key name any terminal produces` + (hint !== void 0 ? ` \u2014 use '${hint}'.` : `. A printable key is named by its character (':' not 'colon'); named keys: ${NAMED_KEYS.join(", ")}.`)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
2
23
|
var TestBackend = class {
|
|
3
24
|
constructor(cols = 40, rows = 10) {
|
|
4
25
|
this.cols = cols;
|
|
@@ -28,8 +49,13 @@ var TestBackend = class {
|
|
|
28
49
|
this.subscribers.delete(handler);
|
|
29
50
|
};
|
|
30
51
|
}
|
|
31
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Synchronously deliver one Key to every subscriber. Throws on a name no
|
|
54
|
+
* terminal can produce (`'space'`, `'enter'`): a test that presses such a key
|
|
55
|
+
* exercises a branch real input never reaches, and would pass anyway.
|
|
56
|
+
*/
|
|
32
57
|
press(key) {
|
|
58
|
+
assertRealKeyName(key.name);
|
|
33
59
|
const k = {
|
|
34
60
|
...key.text !== void 0 ? { text: key.text } : {},
|
|
35
61
|
...key.x !== void 0 ? { x: key.x } : {},
|