@vincemakes/kiso-tui 0.16.1 → 0.16.2

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.
Files changed (2) hide show
  1. package/dist/editor.js +93 -0
  2. package/package.json +2 -2
package/dist/editor.js CHANGED
@@ -270,6 +270,19 @@ export class Editor {
270
270
  #history = [];
271
271
  #historyIdx = null;
272
272
  #preBrowse = [];
273
+ // UD-1: minimal draft undo. Two stacks of FROZEN snapshots beside
274
+ // the one mutable buffer (the KC1 flat-buffer discipline holds —
275
+ // the stacks are history, never a second projection). A checkpoint
276
+ // is pushed only by a gesture about to discard ≥1 code point (the
277
+ // kills, the menu-esc clear, each queue-pop replacement, the
278
+ // @-apply splice); typing and single backspace push nothing — v1
279
+ // is loss-recovery, not char-granular edit history. ctrl+z
280
+ // restores text+cursor exactly; ctrl+y mirrors; undo never
281
+ // discards (what it replaces always lands on the redo stack).
282
+ // Both stacks clear on submit/clearLine — a sent turn is in the
283
+ // durable log and the ↑ history, not a loss.
284
+ #undoStack = [];
285
+ #redoStack = [];
273
286
  // W22: the pending-turn queue's bound state — the CLI's live slots
274
287
  // (chat.ts). ↑ pops the LAST queued message into the buffer and
275
288
  // enters the pop-mode (the walk: repeated ↑ pop older ones, each
@@ -355,6 +368,8 @@ export class Editor {
355
368
  return this.#sheetOpen;
356
369
  }
357
370
  clearLine() {
371
+ this.#undoStack.length = 0; // UD-1
372
+ this.#redoStack.length = 0;
358
373
  this.#chars = [];
359
374
  this.#cursor = 0;
360
375
  this.#scroll = 0;
@@ -577,6 +592,8 @@ export class Editor {
577
592
  if (view === null)
578
593
  return;
579
594
  const insert = [...`@${view.matches[view.selected].path} `].map((ch) => ch.codePointAt(0));
595
+ if (this.#cursor - view.start >= 1)
596
+ this.#checkpoint(); // UD-1
580
597
  this.#chars.splice(view.start, this.#cursor - view.start, ...insert);
581
598
  this.#cursor = view.start + insert.length;
582
599
  this.#atClose();
@@ -1102,6 +1119,8 @@ export class Editor {
1102
1119
  // v3 §04: Esc closes the menu and clears the buffer.
1103
1120
  // CA-4: the closing esc consumes its burst (the `i += 1`
1104
1121
  // convention) — a double-esc can never abort the turn.
1122
+ if (this.#chars.length > 0)
1123
+ this.#checkpoint(); // UD-1
1105
1124
  this.#chars = [];
1106
1125
  this.#cursor = 0;
1107
1126
  this.#scroll = 0;
@@ -1201,6 +1220,14 @@ export class Editor {
1201
1220
  this.#killWord();
1202
1221
  i += 1;
1203
1222
  }
1223
+ else if (c === "\x1a" || c === "\x1f") {
1224
+ this.#undoOp(); // UD-1: ctrl+z (and the readline ctrl+_)
1225
+ i += 1;
1226
+ }
1227
+ else if (c === "\x19") {
1228
+ this.#redoOp(); // UD-1: ctrl+y
1229
+ i += 1;
1230
+ }
1204
1231
  else if (c === "\x01") {
1205
1232
  this.#cursor = this.#cursorBounds().start; // A3: line-local (a single line starts at 0 — unchanged)
1206
1233
  this.#reflow();
@@ -1891,8 +1918,66 @@ export class Editor {
1891
1918
  if (!this.#pasting)
1892
1919
  this.#onRender();
1893
1920
  }
1921
+ // ---- UD-1: the undo machinery ----
1922
+ /** The caps: entries and total code points (a D13-class 260KB paste
1923
+ * fits with room). Evict oldest; both stacks share the shape. */
1924
+ static #UNDO_CAP = 64;
1925
+ static #UNDO_CP_CAP = 2 * 1024 * 1024;
1926
+ #snap() {
1927
+ return { chars: [...this.#chars], cursor: this.#cursor };
1928
+ }
1929
+ #sameSnap(a, b) {
1930
+ return a.cursor === b.cursor && a.chars.length === b.chars.length && a.chars.every((c, i) => c === b.chars[i]);
1931
+ }
1932
+ #capStack(stack) {
1933
+ while (stack.length > _a.#UNDO_CAP)
1934
+ stack.shift();
1935
+ let total = stack.reduce((n, s) => n + s.chars.length, 0);
1936
+ while (stack.length > 1 && total > _a.#UNDO_CP_CAP)
1937
+ total -= stack.shift().chars.length;
1938
+ }
1939
+ /** Push the CURRENT state before a destructive gesture. Always
1940
+ * clears the redo stack (a new destruction forks history); the
1941
+ * push itself is deduped against the top. */
1942
+ #checkpoint() {
1943
+ this.#redoStack.length = 0;
1944
+ const cur = this.#snap();
1945
+ const top = this.#undoStack.at(-1);
1946
+ if (top !== undefined && this.#sameSnap(top, cur))
1947
+ return;
1948
+ this.#undoStack.push(cur);
1949
+ this.#capStack(this.#undoStack);
1950
+ }
1951
+ #restoreSnap(s) {
1952
+ this.#chars = [...s.chars];
1953
+ this.#cursor = s.cursor;
1954
+ this.#scroll = 0;
1955
+ this.#verticalGoalCol = null;
1956
+ this.#reflow();
1957
+ this.#refreshMenu();
1958
+ if (!this.#pasting)
1959
+ this.#onRender();
1960
+ }
1961
+ #undoOp() {
1962
+ const prev = this.#undoStack.pop();
1963
+ if (prev === undefined)
1964
+ return;
1965
+ this.#redoStack.push(this.#snap());
1966
+ this.#capStack(this.#redoStack);
1967
+ this.#restoreSnap(prev);
1968
+ }
1969
+ #redoOp() {
1970
+ const next = this.#redoStack.pop();
1971
+ if (next === undefined)
1972
+ return;
1973
+ this.#undoStack.push(this.#snap());
1974
+ this.#capStack(this.#undoStack);
1975
+ this.#restoreSnap(next);
1976
+ }
1894
1977
  #killToStart() {
1895
1978
  const { start } = this.#cursorBounds(); // A3: line-local (0 on a single line — unchanged)
1979
+ if (this.#cursor > start)
1980
+ this.#checkpoint(); // UD-1
1896
1981
  this.#chars.splice(start, this.#cursor - start);
1897
1982
  this.#cursor = start;
1898
1983
  this.#reflow();
@@ -1901,6 +1986,8 @@ export class Editor {
1901
1986
  }
1902
1987
  #killToEnd() {
1903
1988
  const { end } = this.#cursorBounds(); // A3: line-local (the buffer's end on a single line — unchanged)
1989
+ if (end > this.#cursor)
1990
+ this.#checkpoint(); // UD-1
1904
1991
  this.#chars.splice(this.#cursor, end - this.#cursor);
1905
1992
  this.#reflow();
1906
1993
  if (!this.#pasting)
@@ -1915,6 +2002,8 @@ export class Editor {
1915
2002
  i -= 1; // trailing spaces
1916
2003
  while (i > 0 && this.#chars[i - 1] !== 0x20)
1917
2004
  i -= 1; // the word
2005
+ if (i < this.#cursor)
2006
+ this.#checkpoint(); // UD-1
1918
2007
  this.#chars.splice(i, this.#cursor - i);
1919
2008
  this.#cursor = i;
1920
2009
  this.#reflow();
@@ -2029,6 +2118,8 @@ export class Editor {
2029
2118
  }
2030
2119
  #takeLine() {
2031
2120
  const line = String.fromCodePoint(...this.#chars);
2121
+ this.#undoStack.length = 0; // UD-1: a sent turn is not a loss
2122
+ this.#redoStack.length = 0;
2032
2123
  this.#chars = [];
2033
2124
  this.#cursor = 0;
2034
2125
  this.#scroll = 0;
@@ -2196,6 +2287,8 @@ export class Editor {
2196
2287
  const line = this.#queuePop();
2197
2288
  if (line === null)
2198
2289
  return;
2290
+ if (this.#chars.length > 0)
2291
+ this.#checkpoint(); // UD-1: a mid-walk edit is recoverable
2199
2292
  this.#chars = [...line].map((ch) => ch.codePointAt(0));
2200
2293
  this.#cursor = this.#chars.length;
2201
2294
  this.#scroll = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "kiso tui \u2014 the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,6 +35,6 @@
35
35
  },
36
36
  "homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
37
37
  "dependencies": {
38
- "@vincemakes/kiso-tui-cells": "0.16.1"
38
+ "@vincemakes/kiso-tui-cells": "0.16.2"
39
39
  }
40
40
  }