@chances-ai/ui-core 24.2.0 → 24.3.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/dist/editor/buffer.d.ts +57 -0
- package/dist/editor/buffer.d.ts.map +1 -0
- package/dist/editor/buffer.js +189 -0
- package/dist/editor/buffer.js.map +1 -0
- package/dist/editor/completion.d.ts +25 -0
- package/dist/editor/completion.d.ts.map +1 -0
- package/dist/editor/completion.js +27 -0
- package/dist/editor/completion.js.map +1 -0
- package/dist/editor/component.d.ts +58 -0
- package/dist/editor/component.d.ts.map +1 -0
- package/dist/editor/component.js +125 -0
- package/dist/editor/component.js.map +1 -0
- package/dist/editor/index.d.ts +15 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +15 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/editor/input-mode.d.ts +26 -0
- package/dist/editor/input-mode.d.ts.map +1 -0
- package/dist/editor/input-mode.js +17 -0
- package/dist/editor/input-mode.js.map +1 -0
- package/dist/editor/kill-ring.d.ts +36 -0
- package/dist/editor/kill-ring.d.ts.map +1 -0
- package/dist/editor/kill-ring.js +58 -0
- package/dist/editor/kill-ring.js.map +1 -0
- package/dist/editor/layout.d.ts +60 -0
- package/dist/editor/layout.d.ts.map +1 -0
- package/dist/editor/layout.js +113 -0
- package/dist/editor/layout.js.map +1 -0
- package/dist/editor/reducer.d.ts +93 -0
- package/dist/editor/reducer.d.ts.map +1 -0
- package/dist/editor/reducer.js +196 -0
- package/dist/editor/reducer.js.map +1 -0
- package/dist/editor/segments.d.ts +46 -0
- package/dist/editor/segments.d.ts.map +1 -0
- package/dist/editor/segments.js +104 -0
- package/dist/editor/segments.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/view-model.d.ts +4 -0
- package/dist/view-model.d.ts.map +1 -1
- package/dist/view-model.js +10 -0
- package/dist/view-model.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (7.4 / task 04) Emacs kill-ring. **Immutable** (every op returns a new
|
|
3
|
+
* `KillRing`) so the editor reducer (docs/7.4 §2.5) stays pure and testable.
|
|
4
|
+
*
|
|
5
|
+
* Ports pi's `KillRing` behaviour (`push({prepend,accumulate})` / `peek` /
|
|
6
|
+
* `rotate` / `length`); chances adds a **cap of 10** (claude-code — fixes pi's
|
|
7
|
+
* unbounded growth) + `snapshot()` for tests (docs/7.4 §2.4, R1-N2).
|
|
8
|
+
*/
|
|
9
|
+
export declare class KillRing {
|
|
10
|
+
/** CAP — most-recent at the end; older entries drop when exceeded. */
|
|
11
|
+
static readonly CAP = 10;
|
|
12
|
+
private readonly ring;
|
|
13
|
+
constructor(ring?: readonly string[]);
|
|
14
|
+
/**
|
|
15
|
+
* Add killed `text`.
|
|
16
|
+
* - `accumulate` (consecutive kills) merges into the most-recent entry rather
|
|
17
|
+
* than adding one: `prepend` ⇒ `text + last` (backward kill), else
|
|
18
|
+
* `last + text` (forward kill) — so a run of Ctrl-K reads back in order.
|
|
19
|
+
* - otherwise pushes a new entry, dropping the oldest past {@link CAP}.
|
|
20
|
+
*
|
|
21
|
+
* Empty `text` is a no-op (returns `this`).
|
|
22
|
+
*/
|
|
23
|
+
push(text: string, opts: {
|
|
24
|
+
prepend: boolean;
|
|
25
|
+
accumulate?: boolean;
|
|
26
|
+
}): KillRing;
|
|
27
|
+
/** Most-recent entry (yank target), or undefined if empty. */
|
|
28
|
+
peek(): string | undefined;
|
|
29
|
+
/** Yank-pop: rotate the most-recent entry to the front so `peek()` returns the
|
|
30
|
+
* next-older one. No-op when <2 entries. */
|
|
31
|
+
rotate(): KillRing;
|
|
32
|
+
get length(): number;
|
|
33
|
+
/** Current entries (oldest→newest) — tests only. */
|
|
34
|
+
snapshot(): readonly string[];
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=kill-ring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kill-ring.d.ts","sourceRoot":"","sources":["../../src/editor/kill-ring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,qBAAa,QAAQ;IACnB,sEAAsE;IACtE,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM;IAEzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;gBAE7B,IAAI,GAAE,SAAS,MAAM,EAAO;IAIxC;;;;;;;;OAQG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,QAAQ;IAY9E,8DAA8D;IAC9D,IAAI,IAAI,MAAM,GAAG,SAAS;IAI1B;iDAC6C;IAC7C,MAAM,IAAI,QAAQ;IAMlB,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,oDAAoD;IACpD,QAAQ,IAAI,SAAS,MAAM,EAAE;CAG9B"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (7.4 / task 04) Emacs kill-ring. **Immutable** (every op returns a new
|
|
3
|
+
* `KillRing`) so the editor reducer (docs/7.4 §2.5) stays pure and testable.
|
|
4
|
+
*
|
|
5
|
+
* Ports pi's `KillRing` behaviour (`push({prepend,accumulate})` / `peek` /
|
|
6
|
+
* `rotate` / `length`); chances adds a **cap of 10** (claude-code — fixes pi's
|
|
7
|
+
* unbounded growth) + `snapshot()` for tests (docs/7.4 §2.4, R1-N2).
|
|
8
|
+
*/
|
|
9
|
+
export class KillRing {
|
|
10
|
+
/** CAP — most-recent at the end; older entries drop when exceeded. */
|
|
11
|
+
static CAP = 10;
|
|
12
|
+
ring;
|
|
13
|
+
constructor(ring = []) {
|
|
14
|
+
this.ring = ring;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Add killed `text`.
|
|
18
|
+
* - `accumulate` (consecutive kills) merges into the most-recent entry rather
|
|
19
|
+
* than adding one: `prepend` ⇒ `text + last` (backward kill), else
|
|
20
|
+
* `last + text` (forward kill) — so a run of Ctrl-K reads back in order.
|
|
21
|
+
* - otherwise pushes a new entry, dropping the oldest past {@link CAP}.
|
|
22
|
+
*
|
|
23
|
+
* Empty `text` is a no-op (returns `this`).
|
|
24
|
+
*/
|
|
25
|
+
push(text, opts) {
|
|
26
|
+
if (!text)
|
|
27
|
+
return this;
|
|
28
|
+
if (opts.accumulate && this.ring.length > 0) {
|
|
29
|
+
const last = this.ring[this.ring.length - 1];
|
|
30
|
+
const merged = opts.prepend ? text + last : last + text;
|
|
31
|
+
return new KillRing([...this.ring.slice(0, -1), merged]);
|
|
32
|
+
}
|
|
33
|
+
let next = [...this.ring, text];
|
|
34
|
+
if (next.length > KillRing.CAP)
|
|
35
|
+
next = next.slice(next.length - KillRing.CAP);
|
|
36
|
+
return new KillRing(next);
|
|
37
|
+
}
|
|
38
|
+
/** Most-recent entry (yank target), or undefined if empty. */
|
|
39
|
+
peek() {
|
|
40
|
+
return this.ring.length > 0 ? this.ring[this.ring.length - 1] : undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Yank-pop: rotate the most-recent entry to the front so `peek()` returns the
|
|
43
|
+
* next-older one. No-op when <2 entries. */
|
|
44
|
+
rotate() {
|
|
45
|
+
if (this.ring.length <= 1)
|
|
46
|
+
return this;
|
|
47
|
+
const last = this.ring[this.ring.length - 1];
|
|
48
|
+
return new KillRing([last, ...this.ring.slice(0, -1)]);
|
|
49
|
+
}
|
|
50
|
+
get length() {
|
|
51
|
+
return this.ring.length;
|
|
52
|
+
}
|
|
53
|
+
/** Current entries (oldest→newest) — tests only. */
|
|
54
|
+
snapshot() {
|
|
55
|
+
return this.ring;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=kill-ring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kill-ring.js","sourceRoot":"","sources":["../../src/editor/kill-ring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,OAAO,QAAQ;IACnB,sEAAsE;IACtE,MAAM,CAAU,GAAG,GAAG,EAAE,CAAC;IAER,IAAI,CAAoB;IAEzC,YAAY,OAA0B,EAAE;QACtC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED;;;;;;;;OAQG;IACH,IAAI,CAAC,IAAY,EAAE,IAAgD;QACjE,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;YAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;YACxD,OAAO,IAAI,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,CAAC;QACD,IAAI,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAChC,IAAI,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,GAAG;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC9E,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,8DAA8D;IAC9D,IAAI;QACF,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5E,CAAC;IAED;iDAC6C;IAC7C,MAAM;QACJ,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;QAC9C,OAAO,IAAI,QAAQ,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;IAC1B,CAAC;IAED,oDAAoD;IACpD,QAAQ;QACN,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (7.4 / task 04) Pure visual-line layout for the multi-line editor: wrap to a
|
|
3
|
+
* width, locate the cursor's visual row/col, and compute vertical (↑/↓) moves
|
|
4
|
+
* with a sticky preferred column (docs/7.4 §2.6). Width is INJECTED as a
|
|
5
|
+
* `MeasureFn` — the TUI passes the native `measure` so layout and render share
|
|
6
|
+
* ONE measure (R1-M1, no drift); ui-core/tests use the default `graphemeWidth`.
|
|
7
|
+
*/
|
|
8
|
+
import type { TextBuffer } from "./buffer.js";
|
|
9
|
+
import { type MeasureFn } from "./segments.js";
|
|
10
|
+
export interface VisualRow {
|
|
11
|
+
/** Row text (no trailing `\n`). */
|
|
12
|
+
text: string;
|
|
13
|
+
/** UTF-16 offset of the row start in the buffer. */
|
|
14
|
+
start: number;
|
|
15
|
+
/** UTF-16 offset just past the row content. */
|
|
16
|
+
end: number;
|
|
17
|
+
}
|
|
18
|
+
export interface Layout {
|
|
19
|
+
rows: VisualRow[];
|
|
20
|
+
/** Visual row the cursor sits on. */
|
|
21
|
+
cursorRow: number;
|
|
22
|
+
/** Visual column (terminal cells) of the cursor within its row. */
|
|
23
|
+
cursorCol: number;
|
|
24
|
+
}
|
|
25
|
+
/** Wrap + locate the cursor. Greedy word-agnostic wrap by cluster width (a
|
|
26
|
+
* cluster wider than `width` still occupies its own row). */
|
|
27
|
+
export declare function layout(text: string, cursor: number, width: number, measure?: MeasureFn): Layout;
|
|
28
|
+
export interface Viewport {
|
|
29
|
+
/** The visible slice of rows (≤ `maxRows`). */
|
|
30
|
+
rows: VisualRow[];
|
|
31
|
+
/** Cursor row index WITHIN `rows`. */
|
|
32
|
+
cursorRow: number;
|
|
33
|
+
/** Count of rows hidden above the window (0 = none; caller may show `↑N`). */
|
|
34
|
+
above: number;
|
|
35
|
+
/** Count of rows hidden below the window (0 = none; caller may show `↓N`). */
|
|
36
|
+
below: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* (7.4 §G2 / claude-code viewport scroll) Window a wrapped draft to at most
|
|
40
|
+
* `maxRows` content rows centered on the cursor, so a long multi-line draft
|
|
41
|
+
* never pushes the transcript off-screen. Pure: the caller renders `rows` and,
|
|
42
|
+
* when `above`/`below` > 0, a clipped-line indicator. Returns all rows unchanged
|
|
43
|
+
* when the draft already fits.
|
|
44
|
+
*/
|
|
45
|
+
export declare function viewportRows(rows: VisualRow[], cursorRow: number, maxRows: number): Viewport;
|
|
46
|
+
export interface VisualMove {
|
|
47
|
+
cursor: number;
|
|
48
|
+
/** Sticky column to remember for the NEXT vertical move (null = none). */
|
|
49
|
+
preferredCol: number | null;
|
|
50
|
+
/** False when already at the top/bottom row (caller may fall back to history). */
|
|
51
|
+
moved: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Move the cursor up (`dir=-1`) or down (`dir=+1`) by one visual row, honouring
|
|
55
|
+
* a sticky `preferredCol` (oh-my-pi): if the target row is shorter than the goal
|
|
56
|
+
* column the cursor lands at row end but the goal is remembered so a further move
|
|
57
|
+
* restores it. `moved=false` when there is no row in that direction.
|
|
58
|
+
*/
|
|
59
|
+
export declare function moveVisual(buf: TextBuffer, width: number, dir: -1 | 1, preferredCol: number | null, measure?: MeasureFn): VisualMove;
|
|
60
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/editor/layout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAmC,KAAK,SAAS,EAAW,MAAM,eAAe,CAAC;AAEzF,MAAM,WAAW,SAAS;IACxB,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;CACnB;AAgBD;8DAC8D;AAC9D,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,SAAyB,GAAG,MAAM,CAwC9G;AAED,MAAM,WAAW,QAAQ;IACvB,+CAA+C;IAC/C,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,CAS5F;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kFAAkF;IAClF,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,EACX,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,GAAE,SAAyB,GACjC,UAAU,CAqBZ"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (7.4 / task 04) Pure visual-line layout for the multi-line editor: wrap to a
|
|
3
|
+
* width, locate the cursor's visual row/col, and compute vertical (↑/↓) moves
|
|
4
|
+
* with a sticky preferred column (docs/7.4 §2.6). Width is INJECTED as a
|
|
5
|
+
* `MeasureFn` — the TUI passes the native `measure` so layout and render share
|
|
6
|
+
* ONE measure (R1-M1, no drift); ui-core/tests use the default `graphemeWidth`.
|
|
7
|
+
*/
|
|
8
|
+
import { graphemeSegments, graphemeWidth, widthOf } from "./segments.js";
|
|
9
|
+
/** Split `text` into logical lines (`\n`-delimited), returning offset ranges
|
|
10
|
+
* (the `\n` itself excluded; a trailing `\n` yields a final empty range). */
|
|
11
|
+
function logicalLines(text) {
|
|
12
|
+
const out = [];
|
|
13
|
+
let start = 0;
|
|
14
|
+
for (let i = 0; i <= text.length; i++) {
|
|
15
|
+
if (i === text.length || text[i] === "\n") {
|
|
16
|
+
out.push({ start, end: i });
|
|
17
|
+
start = i + 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/** Wrap + locate the cursor. Greedy word-agnostic wrap by cluster width (a
|
|
23
|
+
* cluster wider than `width` still occupies its own row). */
|
|
24
|
+
export function layout(text, cursor, width, measure = graphemeWidth) {
|
|
25
|
+
const w = Math.max(1, width);
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (const ln of logicalLines(text)) {
|
|
28
|
+
const seg = text.slice(ln.start, ln.end);
|
|
29
|
+
if (seg === "") {
|
|
30
|
+
rows.push({ text: "", start: ln.start, end: ln.start });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let rowStart = ln.start;
|
|
34
|
+
let colW = 0;
|
|
35
|
+
for (const { segment, index } of graphemeSegments(seg)) {
|
|
36
|
+
const off = ln.start + index;
|
|
37
|
+
const cw = measure(segment);
|
|
38
|
+
if (colW + cw > w && off > rowStart) {
|
|
39
|
+
rows.push({ text: text.slice(rowStart, off), start: rowStart, end: off });
|
|
40
|
+
rowStart = off;
|
|
41
|
+
colW = 0;
|
|
42
|
+
}
|
|
43
|
+
colW += cw;
|
|
44
|
+
}
|
|
45
|
+
rows.push({ text: text.slice(rowStart, ln.end), start: rowStart, end: ln.end });
|
|
46
|
+
}
|
|
47
|
+
// Locate the cursor. At a soft-wrap boundary (cursor === row.end === next
|
|
48
|
+
// row.start, same logical line) defer to the next row's column 0.
|
|
49
|
+
let cursorRow = rows.length - 1;
|
|
50
|
+
let cursorCol = rows.length > 0 ? widthOf(rows[cursorRow].text, measure) : 0;
|
|
51
|
+
for (let r = 0; r < rows.length; r++) {
|
|
52
|
+
const row = rows[r];
|
|
53
|
+
if (cursor < row.start)
|
|
54
|
+
break;
|
|
55
|
+
if (cursor <= row.end) {
|
|
56
|
+
if (cursor === row.end && r + 1 < rows.length && rows[r + 1].start === cursor)
|
|
57
|
+
continue;
|
|
58
|
+
cursorRow = r;
|
|
59
|
+
cursorCol = widthOf(text.slice(row.start, cursor), measure);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { rows, cursorRow, cursorCol };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* (7.4 §G2 / claude-code viewport scroll) Window a wrapped draft to at most
|
|
67
|
+
* `maxRows` content rows centered on the cursor, so a long multi-line draft
|
|
68
|
+
* never pushes the transcript off-screen. Pure: the caller renders `rows` and,
|
|
69
|
+
* when `above`/`below` > 0, a clipped-line indicator. Returns all rows unchanged
|
|
70
|
+
* when the draft already fits.
|
|
71
|
+
*/
|
|
72
|
+
export function viewportRows(rows, cursorRow, maxRows) {
|
|
73
|
+
const n = rows.length;
|
|
74
|
+
const max = Math.max(1, Math.floor(maxRows));
|
|
75
|
+
if (n <= max)
|
|
76
|
+
return { rows, cursorRow, above: 0, below: 0 };
|
|
77
|
+
let start = cursorRow - Math.floor(max / 2);
|
|
78
|
+
if (start < 0)
|
|
79
|
+
start = 0;
|
|
80
|
+
if (start + max > n)
|
|
81
|
+
start = n - max;
|
|
82
|
+
const end = start + max;
|
|
83
|
+
return { rows: rows.slice(start, end), cursorRow: cursorRow - start, above: start, below: n - end };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Move the cursor up (`dir=-1`) or down (`dir=+1`) by one visual row, honouring
|
|
87
|
+
* a sticky `preferredCol` (oh-my-pi): if the target row is shorter than the goal
|
|
88
|
+
* column the cursor lands at row end but the goal is remembered so a further move
|
|
89
|
+
* restores it. `moved=false` when there is no row in that direction.
|
|
90
|
+
*/
|
|
91
|
+
export function moveVisual(buf, width, dir, preferredCol, measure = graphemeWidth) {
|
|
92
|
+
const lay = layout(buf.text, buf.cursor, width, measure);
|
|
93
|
+
const targetRow = lay.cursorRow + dir;
|
|
94
|
+
if (targetRow < 0 || targetRow >= lay.rows.length) {
|
|
95
|
+
return { cursor: buf.cursor, preferredCol, moved: false };
|
|
96
|
+
}
|
|
97
|
+
const goalCol = preferredCol ?? lay.cursorCol;
|
|
98
|
+
const row = lay.rows[targetRow];
|
|
99
|
+
let off = row.start;
|
|
100
|
+
let col = 0;
|
|
101
|
+
for (const { segment, index } of graphemeSegments(row.text)) {
|
|
102
|
+
const cw = measure(segment);
|
|
103
|
+
if (col + cw > goalCol)
|
|
104
|
+
break;
|
|
105
|
+
col += cw;
|
|
106
|
+
off = row.start + index + segment.length;
|
|
107
|
+
}
|
|
108
|
+
const rowWidth = widthOf(row.text, measure);
|
|
109
|
+
// Keep the goal sticky while the target row can't reach it.
|
|
110
|
+
const nextPreferred = rowWidth < goalCol ? goalCol : null;
|
|
111
|
+
return { cursor: off, preferredCol: nextPreferred, moved: true };
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/editor/layout.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (7.4 / task 04) The PURE editor reducer (docs/7.4 §2.5): folds buffer +
|
|
3
|
+
* kill-ring + undo + last-action into `editorReducer(state, action) => state`,
|
|
4
|
+
* so "consecutive kills accumulate", "yank-pop only right after a yank", and
|
|
5
|
+
* "undo groups runs of typing" are all unit-testable as data. NO Ink/React/node.
|
|
6
|
+
*
|
|
7
|
+
* Vertical (↑/↓) visual moves are NOT here — they need an injected width/measure,
|
|
8
|
+
* so `EmacsEditor` computes them via `layout.moveVisual` and dispatches
|
|
9
|
+
* `setCursor` (keeps this reducer measure-free + pure). See docs/7.4 §2.8.
|
|
10
|
+
*/
|
|
11
|
+
import { type TextBuffer } from "./buffer.js";
|
|
12
|
+
import { KillRing } from "./kill-ring.js";
|
|
13
|
+
export interface Snapshot {
|
|
14
|
+
text: string;
|
|
15
|
+
cursor: number;
|
|
16
|
+
}
|
|
17
|
+
export type LastAction = "kill" | "yank" | "insert" | "edit" | "move" | "none";
|
|
18
|
+
export interface EditorState {
|
|
19
|
+
buffer: TextBuffer;
|
|
20
|
+
killRing: KillRing;
|
|
21
|
+
lastAction: LastAction;
|
|
22
|
+
/** The region a yank inserted, so yank-pop can replace it. */
|
|
23
|
+
yankAnchor: {
|
|
24
|
+
start: number;
|
|
25
|
+
len: number;
|
|
26
|
+
} | null;
|
|
27
|
+
undo: {
|
|
28
|
+
stack: Snapshot[];
|
|
29
|
+
redo: Snapshot[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export type EditorAction = {
|
|
33
|
+
t: "insert";
|
|
34
|
+
text: string;
|
|
35
|
+
} | {
|
|
36
|
+
t: "newline";
|
|
37
|
+
} | {
|
|
38
|
+
t: "backspace";
|
|
39
|
+
} | {
|
|
40
|
+
t: "deleteForward";
|
|
41
|
+
} | {
|
|
42
|
+
t: "moveLeft";
|
|
43
|
+
} | {
|
|
44
|
+
t: "moveRight";
|
|
45
|
+
} | {
|
|
46
|
+
t: "moveWordLeft";
|
|
47
|
+
} | {
|
|
48
|
+
t: "moveWordRight";
|
|
49
|
+
} | {
|
|
50
|
+
t: "moveLineStart";
|
|
51
|
+
} | {
|
|
52
|
+
t: "moveLineEnd";
|
|
53
|
+
} | {
|
|
54
|
+
t: "moveBufferStart";
|
|
55
|
+
} | {
|
|
56
|
+
t: "moveBufferEnd";
|
|
57
|
+
} | {
|
|
58
|
+
t: "setCursor";
|
|
59
|
+
cursor: number;
|
|
60
|
+
} | {
|
|
61
|
+
t: "deleteWordBackward";
|
|
62
|
+
} | {
|
|
63
|
+
t: "deleteWordForward";
|
|
64
|
+
} | {
|
|
65
|
+
t: "killToLineEnd";
|
|
66
|
+
} | {
|
|
67
|
+
t: "killToLineStart";
|
|
68
|
+
} | {
|
|
69
|
+
t: "yank";
|
|
70
|
+
} | {
|
|
71
|
+
t: "yankPop";
|
|
72
|
+
} | {
|
|
73
|
+
t: "undo";
|
|
74
|
+
} | {
|
|
75
|
+
t: "redo";
|
|
76
|
+
} | {
|
|
77
|
+
t: "setText";
|
|
78
|
+
text: string;
|
|
79
|
+
} | {
|
|
80
|
+
t: "clear";
|
|
81
|
+
};
|
|
82
|
+
export declare function initialEditorState(text?: string): EditorState;
|
|
83
|
+
export declare function editorReducer(state: EditorState, action: EditorAction): EditorState;
|
|
84
|
+
/** All `EditorAction["t"]` values — used to validate `Editor`-context keybindings
|
|
85
|
+
* (docs/7.4 §4.3). Keymap-driven keys map to a subset; the rest are dispatched
|
|
86
|
+
* by `EmacsEditor` directly. */
|
|
87
|
+
export declare const EDITOR_ACTION_TYPES: ReadonlySet<EditorAction["t"]>;
|
|
88
|
+
/** Editor actions a `keybindings.json` `Editor` binding may target (7.4 R2-M1):
|
|
89
|
+
* EVERY entry is payload-free, so `EmacsEditor` can dispatch `{ t: action }`
|
|
90
|
+
* safely. EXCLUDES `insert`/`setText`/`setCursor` (they need a payload — binding
|
|
91
|
+
* one would dispatch `{t:"insert"}` with no text and crash on `toNFC(undefined)`). */
|
|
92
|
+
export declare const BINDABLE_EDITOR_ACTIONS: ReadonlySet<EditorAction["t"]>;
|
|
93
|
+
//# sourceMappingURL=reducer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../../src/editor/reducer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAmBL,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE/E,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,UAAU,CAAC;IACvB,8DAA8D;IAC9D,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClD,IAAI,EAAE;QAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;QAAC,IAAI,EAAE,QAAQ,EAAE,CAAA;KAAE,CAAC;CAC/C;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,CAAC,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,CAAC,EAAE,SAAS,CAAA;CAAE,GAChB;IAAE,CAAC,EAAE,WAAW,CAAA;CAAE,GAClB;IAAE,CAAC,EAAE,eAAe,CAAA;CAAE,GACtB;IAAE,CAAC,EAAE,UAAU,CAAA;CAAE,GACjB;IAAE,CAAC,EAAE,WAAW,CAAA;CAAE,GAClB;IAAE,CAAC,EAAE,cAAc,CAAA;CAAE,GACrB;IAAE,CAAC,EAAE,eAAe,CAAA;CAAE,GACtB;IAAE,CAAC,EAAE,eAAe,CAAA;CAAE,GACtB;IAAE,CAAC,EAAE,aAAa,CAAA;CAAE,GACpB;IAAE,CAAC,EAAE,iBAAiB,CAAA;CAAE,GACxB;IAAE,CAAC,EAAE,eAAe,CAAA;CAAE,GACtB;IAAE,CAAC,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,CAAC,EAAE,oBAAoB,CAAA;CAAE,GAC3B;IAAE,CAAC,EAAE,mBAAmB,CAAA;CAAE,GAC1B;IAAE,CAAC,EAAE,eAAe,CAAA;CAAE,GACtB;IAAE,CAAC,EAAE,iBAAiB,CAAA;CAAE,GACxB;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GACb;IAAE,CAAC,EAAE,SAAS,CAAA;CAAE,GAChB;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GACb;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GACb;IAAE,CAAC,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAInB,wBAAgB,kBAAkB,CAAC,IAAI,SAAK,GAAG,WAAW,CAQzD;AAyCD,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,WAAW,CAuGnF;AAED;;iCAEiC;AACjC,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,CAwB7D,CAAC;AAEH;;;uFAGuF;AACvF,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,CAElE,CAAC"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (7.4 / task 04) The PURE editor reducer (docs/7.4 §2.5): folds buffer +
|
|
3
|
+
* kill-ring + undo + last-action into `editorReducer(state, action) => state`,
|
|
4
|
+
* so "consecutive kills accumulate", "yank-pop only right after a yank", and
|
|
5
|
+
* "undo groups runs of typing" are all unit-testable as data. NO Ink/React/node.
|
|
6
|
+
*
|
|
7
|
+
* Vertical (↑/↓) visual moves are NOT here — they need an injected width/measure,
|
|
8
|
+
* so `EmacsEditor` computes them via `layout.moveVisual` and dispatches
|
|
9
|
+
* `setCursor` (keeps this reducer measure-free + pure). See docs/7.4 §2.8.
|
|
10
|
+
*/
|
|
11
|
+
import { backspace, deleteForward, deleteWordBackward, deleteWordForward, insert, killToLineEnd, killToLineStart, makeBuffer, moveBufferEnd, moveBufferStart, moveLeft, moveLineEnd, moveLineStart, moveRight, moveWordLeft, moveWordRight, setCursor, } from "./buffer.js";
|
|
12
|
+
import { KillRing } from "./kill-ring.js";
|
|
13
|
+
const MAX_UNDO = 200;
|
|
14
|
+
export function initialEditorState(text = "") {
|
|
15
|
+
return {
|
|
16
|
+
buffer: makeBuffer(text),
|
|
17
|
+
killRing: new KillRing(),
|
|
18
|
+
lastAction: "none",
|
|
19
|
+
yankAnchor: null,
|
|
20
|
+
undo: { stack: [], redo: [] },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function snapshot(buf) {
|
|
24
|
+
return { text: buf.text, cursor: buf.cursor };
|
|
25
|
+
}
|
|
26
|
+
/** Push an undo entry, grouping a contiguous run of typing into one. `redo` is
|
|
27
|
+
* cleared on any fresh edit. */
|
|
28
|
+
function pushUndo(state, isInsert) {
|
|
29
|
+
const continuing = isInsert && state.lastAction === "insert";
|
|
30
|
+
if (continuing)
|
|
31
|
+
return { stack: state.undo.stack, redo: [] };
|
|
32
|
+
const stack = [...state.undo.stack, snapshot(state.buffer)];
|
|
33
|
+
if (stack.length > MAX_UNDO)
|
|
34
|
+
stack.splice(0, stack.length - MAX_UNDO);
|
|
35
|
+
return { stack, redo: [] };
|
|
36
|
+
}
|
|
37
|
+
/** Apply a kill result: buffer + accumulate-aware kill-ring + last-action=kill. */
|
|
38
|
+
function applyKill(state, kr) {
|
|
39
|
+
// (7.4 R2-S3) An empty kill (e.g. Ctrl-K at end of buffer) removed nothing —
|
|
40
|
+
// don't pollute the kill-ring, undo stack, or the accumulate flag.
|
|
41
|
+
if (kr.killed === "")
|
|
42
|
+
return state;
|
|
43
|
+
const accumulate = state.lastAction === "kill";
|
|
44
|
+
return {
|
|
45
|
+
buffer: kr.buffer,
|
|
46
|
+
killRing: kr.killed ? state.killRing.push(kr.killed, { prepend: kr.dir === "backward", accumulate }) : state.killRing,
|
|
47
|
+
lastAction: "kill",
|
|
48
|
+
yankAnchor: null,
|
|
49
|
+
undo: pushUndo(state, false),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** A non-kill edit: new buffer + undo + last-action. */
|
|
53
|
+
function applyEdit(state, buffer, lastAction, isInsert = false) {
|
|
54
|
+
return { ...state, buffer, lastAction, yankAnchor: null, undo: pushUndo(state, isInsert) };
|
|
55
|
+
}
|
|
56
|
+
/** A pure cursor move: no undo, no kill-ring change; resets accumulation. */
|
|
57
|
+
function applyMove(state, buffer) {
|
|
58
|
+
return { ...state, buffer, lastAction: "move", yankAnchor: null };
|
|
59
|
+
}
|
|
60
|
+
export function editorReducer(state, action) {
|
|
61
|
+
switch (action.t) {
|
|
62
|
+
case "insert":
|
|
63
|
+
return action.text === "" ? state : applyEdit(state, insert(state.buffer, action.text), "insert", true);
|
|
64
|
+
case "newline":
|
|
65
|
+
return applyEdit(state, insert(state.buffer, "\n"), "insert", true);
|
|
66
|
+
case "backspace":
|
|
67
|
+
return applyEdit(state, backspace(state.buffer), "edit");
|
|
68
|
+
case "deleteForward":
|
|
69
|
+
return applyEdit(state, deleteForward(state.buffer), "edit");
|
|
70
|
+
case "moveLeft":
|
|
71
|
+
return applyMove(state, moveLeft(state.buffer));
|
|
72
|
+
case "moveRight":
|
|
73
|
+
return applyMove(state, moveRight(state.buffer));
|
|
74
|
+
case "moveWordLeft":
|
|
75
|
+
return applyMove(state, moveWordLeft(state.buffer));
|
|
76
|
+
case "moveWordRight":
|
|
77
|
+
return applyMove(state, moveWordRight(state.buffer));
|
|
78
|
+
case "moveLineStart":
|
|
79
|
+
return applyMove(state, moveLineStart(state.buffer));
|
|
80
|
+
case "moveLineEnd":
|
|
81
|
+
return applyMove(state, moveLineEnd(state.buffer));
|
|
82
|
+
case "moveBufferStart":
|
|
83
|
+
return applyMove(state, moveBufferStart(state.buffer));
|
|
84
|
+
case "moveBufferEnd":
|
|
85
|
+
return applyMove(state, moveBufferEnd(state.buffer));
|
|
86
|
+
case "setCursor":
|
|
87
|
+
return applyMove(state, setCursor(state.buffer, action.cursor));
|
|
88
|
+
case "deleteWordBackward":
|
|
89
|
+
return applyKill(state, deleteWordBackward(state.buffer));
|
|
90
|
+
case "deleteWordForward":
|
|
91
|
+
return applyKill(state, deleteWordForward(state.buffer));
|
|
92
|
+
case "killToLineEnd":
|
|
93
|
+
return applyKill(state, killToLineEnd(state.buffer));
|
|
94
|
+
case "killToLineStart":
|
|
95
|
+
return applyKill(state, killToLineStart(state.buffer));
|
|
96
|
+
case "yank": {
|
|
97
|
+
const text = state.killRing.peek();
|
|
98
|
+
if (!text)
|
|
99
|
+
return { ...state, lastAction: "yank", yankAnchor: null };
|
|
100
|
+
return {
|
|
101
|
+
...state,
|
|
102
|
+
buffer: insert(state.buffer, text),
|
|
103
|
+
lastAction: "yank",
|
|
104
|
+
yankAnchor: { start: state.buffer.cursor, len: text.length },
|
|
105
|
+
undo: pushUndo(state, false),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
case "yankPop": {
|
|
109
|
+
if (state.lastAction !== "yank" || state.yankAnchor === null || state.killRing.length <= 1)
|
|
110
|
+
return state;
|
|
111
|
+
const ring = state.killRing.rotate();
|
|
112
|
+
const text = ring.peek() ?? "";
|
|
113
|
+
const { start, len } = state.yankAnchor;
|
|
114
|
+
const newText = state.buffer.text.slice(0, start) + text + state.buffer.text.slice(start + len);
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
buffer: { text: newText, cursor: start + text.length },
|
|
118
|
+
killRing: ring,
|
|
119
|
+
lastAction: "yank",
|
|
120
|
+
yankAnchor: { start, len: text.length },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case "undo": {
|
|
124
|
+
const prev = state.undo.stack[state.undo.stack.length - 1];
|
|
125
|
+
if (!prev)
|
|
126
|
+
return state;
|
|
127
|
+
return {
|
|
128
|
+
...state,
|
|
129
|
+
buffer: makeBuffer(prev.text, prev.cursor),
|
|
130
|
+
lastAction: "none",
|
|
131
|
+
yankAnchor: null,
|
|
132
|
+
undo: { stack: state.undo.stack.slice(0, -1), redo: [...state.undo.redo, snapshot(state.buffer)] },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
case "redo": {
|
|
136
|
+
const next = state.undo.redo[state.undo.redo.length - 1];
|
|
137
|
+
if (!next)
|
|
138
|
+
return state;
|
|
139
|
+
return {
|
|
140
|
+
...state,
|
|
141
|
+
buffer: makeBuffer(next.text, next.cursor),
|
|
142
|
+
lastAction: "none",
|
|
143
|
+
yankAnchor: null,
|
|
144
|
+
undo: { stack: [...state.undo.stack, snapshot(state.buffer)], redo: state.undo.redo.slice(0, -1) },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
case "setText":
|
|
148
|
+
return {
|
|
149
|
+
...state,
|
|
150
|
+
buffer: makeBuffer(action.text),
|
|
151
|
+
lastAction: "none",
|
|
152
|
+
yankAnchor: null,
|
|
153
|
+
undo: { stack: [...state.undo.stack, snapshot(state.buffer)].slice(-MAX_UNDO), redo: [] },
|
|
154
|
+
};
|
|
155
|
+
case "clear":
|
|
156
|
+
return { ...state, buffer: makeBuffer(""), lastAction: "none", yankAnchor: null };
|
|
157
|
+
default:
|
|
158
|
+
// Defensive: an unknown action.t (e.g. a typo'd keybindings.json target
|
|
159
|
+
// cast through) is a no-op rather than collapsing state to undefined.
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** All `EditorAction["t"]` values — used to validate `Editor`-context keybindings
|
|
164
|
+
* (docs/7.4 §4.3). Keymap-driven keys map to a subset; the rest are dispatched
|
|
165
|
+
* by `EmacsEditor` directly. */
|
|
166
|
+
export const EDITOR_ACTION_TYPES = new Set([
|
|
167
|
+
"insert",
|
|
168
|
+
"newline",
|
|
169
|
+
"backspace",
|
|
170
|
+
"deleteForward",
|
|
171
|
+
"moveLeft",
|
|
172
|
+
"moveRight",
|
|
173
|
+
"moveWordLeft",
|
|
174
|
+
"moveWordRight",
|
|
175
|
+
"moveLineStart",
|
|
176
|
+
"moveLineEnd",
|
|
177
|
+
"moveBufferStart",
|
|
178
|
+
"moveBufferEnd",
|
|
179
|
+
"setCursor",
|
|
180
|
+
"deleteWordBackward",
|
|
181
|
+
"deleteWordForward",
|
|
182
|
+
"killToLineEnd",
|
|
183
|
+
"killToLineStart",
|
|
184
|
+
"yank",
|
|
185
|
+
"yankPop",
|
|
186
|
+
"undo",
|
|
187
|
+
"redo",
|
|
188
|
+
"setText",
|
|
189
|
+
"clear",
|
|
190
|
+
]);
|
|
191
|
+
/** Editor actions a `keybindings.json` `Editor` binding may target (7.4 R2-M1):
|
|
192
|
+
* EVERY entry is payload-free, so `EmacsEditor` can dispatch `{ t: action }`
|
|
193
|
+
* safely. EXCLUDES `insert`/`setText`/`setCursor` (they need a payload — binding
|
|
194
|
+
* one would dispatch `{t:"insert"}` with no text and crash on `toNFC(undefined)`). */
|
|
195
|
+
export const BINDABLE_EDITOR_ACTIONS = new Set([...EDITOR_ACTION_TYPES].filter((a) => a !== "insert" && a !== "setText" && a !== "setCursor"));
|
|
196
|
+
//# sourceMappingURL=reducer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reducer.js","sourceRoot":"","sources":["../../src/editor/reducer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (7.4 / task 04) Grapheme + word segmentation for the editor core. PURE +
|
|
3
|
+
* browser-safe: built-in `Intl.Segmenter` (Node 16+/Bun/browsers) + `string-width`
|
|
4
|
+
* for cluster display width — no `node:*`, no Ink/React.
|
|
5
|
+
*
|
|
6
|
+
* Two concerns:
|
|
7
|
+
* - **grapheme-cluster boundaries** — the cursor moves one *cluster* at a time,
|
|
8
|
+
* so an emoji ZWJ family 👨👩👧👦 or a CJK char is one move unit, never split.
|
|
9
|
+
* - **word-nav classification** (`getWordNavKind`, ported from oh-my-pi,
|
|
10
|
+
* CJK-aware) for word-wise move/delete.
|
|
11
|
+
*
|
|
12
|
+
* Display WIDTH (the `MeasureFn` layout + render share, docs/7.4 §2.6 R1-M1)
|
|
13
|
+
* defaults here to a grapheme-cluster-aware width via `string-width`; the TUI
|
|
14
|
+
* injects the native `measure` instead so layout and render can never drift.
|
|
15
|
+
*/
|
|
16
|
+
/** NFC-normalize at the buffer boundary so combining-mark variants can't desync
|
|
17
|
+
* the cursor (claude-code `MeasuredText` lesson). */
|
|
18
|
+
export declare function toNFC(text: string): string;
|
|
19
|
+
/** Iterate grapheme clusters of `text` as `{ segment, index }` (index = UTF-16
|
|
20
|
+
* offset of the cluster start). Thin wrapper so the rest of the module — and
|
|
21
|
+
* `layout.ts` — share one segmenter instance. */
|
|
22
|
+
export declare function graphemeSegments(text: string): Intl.Segments;
|
|
23
|
+
/** UTF-16 offsets of every grapheme-cluster start in `text`, plus `text.length`
|
|
24
|
+
* as the final sentinel. `""` → `[0]`. */
|
|
25
|
+
export declare function graphemeBoundaries(text: string): number[];
|
|
26
|
+
/** The grapheme-cluster boundary strictly after `offset` (clamped to length). */
|
|
27
|
+
export declare function nextGraphemeOffset(text: string, offset: number): number;
|
|
28
|
+
/** The grapheme-cluster boundary strictly before `offset` (clamped to 0). */
|
|
29
|
+
export declare function prevGraphemeOffset(text: string, offset: number): number;
|
|
30
|
+
/** Terminal-cell width of ONE grapheme cluster — cluster-aware (docs/7.4 §2.6
|
|
31
|
+
* R1-M1): emoji presentation / ZWJ / VS16 / skin-tone + CJK wide → 2,
|
|
32
|
+
* combining / control → 0, else 1. Delegates to `string-width`, which already
|
|
33
|
+
* handles East-Asian-Width + emoji. */
|
|
34
|
+
export declare function graphemeWidth(cluster: string): number;
|
|
35
|
+
/** Measures one grapheme cluster's terminal-cell width. The TUI injects the
|
|
36
|
+
* native `measure`; ui-core + tests use {@link graphemeWidth}. */
|
|
37
|
+
export type MeasureFn = (cluster: string) => number;
|
|
38
|
+
/** Sum of cluster widths across `text` under `measure`. */
|
|
39
|
+
export declare function widthOf(text: string, measure?: MeasureFn): number;
|
|
40
|
+
export type WordNavKind = "whitespace" | "delimiter" | "cjk" | "word" | "other";
|
|
41
|
+
/** Classify a grapheme for word navigation. Order: whitespace → cjk → word →
|
|
42
|
+
* delimiter → other (cjk/word before delimiter because of `\p{L}` / `_`). */
|
|
43
|
+
export declare function getWordNavKind(grapheme: string): WordNavKind;
|
|
44
|
+
/** Whether `grapheme` is a word-internal joiner (kept inside a word run). */
|
|
45
|
+
export declare function isWordJoiner(grapheme: string): boolean;
|
|
46
|
+
//# sourceMappingURL=segments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segments.d.ts","sourceRoot":"","sources":["../../src/editor/segments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;sDACsD;AACtD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AAID;;kDAEkD;AAClD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,QAAQ,CAE5D;AAED;2CAC2C;AAC3C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKzD;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAOvE;AAED;;;wCAGwC;AACxC,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;mEACmE;AACnE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;AAEpD,2DAA2D;AAC3D,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,SAAyB,GAAG,MAAM,CAIhF;AAID,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAiBhF;8EAC8E;AAC9E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAQ5D;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEtD"}
|