@oh-my-pi/pi-tui 17.4.1 → 17.4.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.4.2] - 2026-08-21
6
+
7
+ ### Added
8
+
9
+ - Editor atom table: `insertAtom`/`registerAtom` stage compact atomic tokens whose registered expansion is emitted on submit (`getExpandedText`), alongside the existing paste-marker store.
10
+
5
11
  ## [17.4.1] - 2026-08-21
6
12
 
7
13
  ### Added
@@ -120,6 +120,15 @@ export declare class Editor implements Component, Focusable {
120
120
  /** Whether the buffer text equals `value`, without `getText()`'s full join —
121
121
  * O(1) for the hot per-keystroke probes against short single-line values. */
122
122
  textEquals(value: string): boolean;
123
+ /** Register `label` as a collapsed atom expanding to `expansion` on submit, without inserting
124
+ * it — for hosts that re-collapse restored draft text via {@link setText}. */
125
+ registerAtom(label: string, expansion: string): void;
126
+ /** Insert `label` (plus a trailing space) at the cursor and register it as an atom expanding
127
+ * to `expansion` on submit. Pair with {@link atomicTokenPattern} so the label deletes as a
128
+ * unit. */
129
+ insertAtom(label: string, expansion: string): void;
130
+ /** Drop every registered atom expansion (draft cleared or replaced by the host). */
131
+ clearAtoms(): void;
123
132
  /**
124
133
  * Get text with paste markers expanded to their actual content.
125
134
  * Use this when you need the full content (e.g., for external editor).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "17.4.1",
4
+ "version": "17.4.2",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "17.4.1",
41
- "@oh-my-pi/pi-utils": "17.4.1"
40
+ "@oh-my-pi/pi-natives": "17.4.2",
41
+ "@oh-my-pi/pi-utils": "17.4.2"
42
42
  },
43
43
  "devDependencies": {
44
44
  "ghostty-web": "^0.4.0"
@@ -484,6 +484,9 @@ export class Editor implements Component, Focusable {
484
484
  #pastes: Map<number, string> = new Map();
485
485
  #pasteCounter: number = 0;
486
486
 
487
+ // Host-registered atomic chip tokens: exact buffer label → expansion emitted on submit.
488
+ #atoms: Map<string, string> = new Map();
489
+
487
490
  /** Optional pattern matching atomic placeholder tokens (e.g. `[Image #1, 800x600]` or
488
491
  * `[Paste #2, +30 lines]`) that the editor treats as indivisible: a backspace or forward-delete
489
492
  * landing on any character of a token removes the whole token instead of corrupting it into
@@ -1752,13 +1755,48 @@ export class Editor implements Component, Focusable {
1752
1755
  return this.getText() === value;
1753
1756
  }
1754
1757
 
1758
+ /** Expand collapsed markers — `[Paste #N]` tokens and registered atom labels — into their
1759
+ * stored content. Single pass so replaced content is never rescanned (a pasted body that
1760
+ * happens to contain another token's label must survive verbatim). Longer atom labels are
1761
+ * tried first so `#1` never shadows `#10`. */
1755
1762
  #expandPasteMarkers(text: string): string {
1756
- let result = text;
1757
- for (const [pasteId, pasteContent] of this.#pastes) {
1758
- const markerRegex = new RegExp(`\\[Paste #${pasteId}(?:, (?:\\+\\d+ lines|\\d+ chars))?\\]`, "g");
1759
- result = result.replace(markerRegex, () => pasteContent);
1760
- }
1761
- return result;
1763
+ const sources: string[] = [];
1764
+ for (const pasteId of this.#pastes.keys()) {
1765
+ sources.push(`\\[Paste #${pasteId}(?:, (?:\\+\\d+ lines|\\d+ chars))?\\]`);
1766
+ }
1767
+ const labels = [...this.#atoms.keys()].sort((a, b) => b.length - a.length);
1768
+ for (const label of labels) sources.push(RegExp.escape(label));
1769
+ if (sources.length === 0) return text;
1770
+ const markerRegex = new RegExp(sources.join("|"), "g");
1771
+ return text.replace(markerRegex, match => {
1772
+ const paste = /^\[Paste #(\d+)/.exec(match);
1773
+ if (paste) return this.#pastes.get(Number(paste[1])) ?? match;
1774
+ return this.#atoms.get(match) ?? match;
1775
+ });
1776
+ }
1777
+
1778
+ /** Register `label` as a collapsed atom expanding to `expansion` on submit, without inserting
1779
+ * it — for hosts that re-collapse restored draft text via {@link setText}. */
1780
+ registerAtom(label: string, expansion: string): void {
1781
+ this.#atoms.set(label, expansion);
1782
+ }
1783
+
1784
+ /** Insert `label` (plus a trailing space) at the cursor and register it as an atom expanding
1785
+ * to `expansion` on submit. Pair with {@link atomicTokenPattern} so the label deletes as a
1786
+ * unit. */
1787
+ insertAtom(label: string, expansion: string): void {
1788
+ this.#historyIndex = -1;
1789
+ this.#resetKillSequence();
1790
+ this.#recordUndoState();
1791
+ this.registerAtom(label, expansion);
1792
+ this.#withUndoSuspended(() => {
1793
+ this.#insertTextAtCursor(`${label} `);
1794
+ });
1795
+ }
1796
+
1797
+ /** Drop every registered atom expansion (draft cleared or replaced by the host). */
1798
+ clearAtoms(): void {
1799
+ this.#atoms.clear();
1762
1800
  }
1763
1801
 
1764
1802
  /**
@@ -2213,6 +2251,7 @@ export class Editor implements Component, Focusable {
2213
2251
  this.#state = { lines: [""], cursorLine: 0, cursorCol: 0 };
2214
2252
  this.#pastes.clear();
2215
2253
  this.#pasteCounter = 0;
2254
+ this.#atoms.clear();
2216
2255
  this.#historyIndex = -1;
2217
2256
  this.#scrollOffset = 0;
2218
2257
  this.#undoStack.length = 0;