@code-yeongyu/senpi-tui 2026.8.18 → 2026.8.20

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.
@@ -1,7 +1,10 @@
1
1
  import type { AutocompleteProvider } from "../autocomplete.ts";
2
+ import { type EditorImageState } from "../image-markers.ts";
2
3
  import { type EditorPasteState } from "../paste-markers.ts";
3
4
  import { type Component, type Focusable, type TUI } from "../tui.ts";
4
5
  import { type SelectListTheme } from "./select-list.ts";
6
+ /** Which atomic marker family a segment belongs to, if any. */
7
+ export type MarkerKind = "paste" | "image";
5
8
  /**
6
9
  * Represents a chunk of text for word-wrap layout.
7
10
  * Tracks both the text content and its position in the original line.
@@ -61,6 +64,7 @@ export declare class Editor implements Component, Focusable {
61
64
  private autocompleteStartToken;
62
65
  private autocompleteRequestId;
63
66
  private pasteMarkers;
67
+ private imageMarkers;
64
68
  private pasteBuffer;
65
69
  private isInPaste;
66
70
  private history;
@@ -75,11 +79,44 @@ export declare class Editor implements Component, Focusable {
75
79
  private wrappedLineCache;
76
80
  onSubmit?: (text: string) => void;
77
81
  onChange?: (text: string) => void;
82
+ /**
83
+ * Fired whenever image markers are added, removed, pruned or renumbered.
84
+ * Carries the marker ids in text reading order so the owner can keep its
85
+ * payload map aligned with the visible numbers.
86
+ *
87
+ * The reported ids are the ids the OWNER CURRENTLY KEYS ITS PAYLOADS BY -
88
+ * i.e. the PRE-renumber ids - and the visible numbers are always canonical
89
+ * 1..k in reading order after the change, so the owner re-keys payload
90
+ * `order[i]` onto slot `i + 1`.
91
+ */
92
+ onImageMarkersChanged?: (order: number[]) => void;
93
+ /**
94
+ * Owner hook: capture the attachment payloads keyed by marker id so an undo
95
+ * can restore them alongside the editor snapshot. The value is stored
96
+ * opaquely - image bytes never cross into the editor - and is handed back
97
+ * through {@link restoreAttachmentState} when the matching snapshot is
98
+ * popped. Return `undefined` to store nothing (restores are skipped).
99
+ */
100
+ snapshotAttachmentState?: () => unknown;
101
+ /**
102
+ * Owner hook: restore the attachment payloads captured by
103
+ * {@link snapshotAttachmentState} for the snapshot an undo just popped.
104
+ * Called BEFORE {@link onImageMarkersChanged} fires for that undo so the
105
+ * reconcile pass maps the restored payloads, not the post-delete ones.
106
+ */
107
+ restoreAttachmentState?: (state: unknown) => void;
78
108
  disableSubmit: boolean;
79
109
  constructor(tui: TUI, theme: EditorTheme, options?: EditorOptions);
80
- /** Segment text with paste-marker awareness, only merging exact canonical markers. */
110
+ /** Segment text with marker awareness, only merging exact canonical markers. */
81
111
  private segment;
112
+ /** Union of every marker family that must segment as one atomic grapheme. */
113
+ private authorizedMarkers;
82
114
  private removePasteMarker;
115
+ /** Removes an image marker whole, applying the registry's renumbered text. */
116
+ private removeImageMarker;
117
+ /** Removes whichever marker family `segment` belongs to; returns false when it is not a marker. */
118
+ private removeMarkerSegment;
119
+ private notifyImageMarkersChanged;
83
120
  getPaddingX(): number;
84
121
  setPaddingX(padding: number): void;
85
122
  getAutocompleteMaxVisible(): number;
@@ -126,6 +163,41 @@ export declare class Editor implements Component, Focusable {
126
163
  * counter is raised so future paste ids cannot collide.
127
164
  */
128
165
  setPasteState(state: EditorPasteState): void;
166
+ /**
167
+ * Snapshot the image-marker registry (ids only, never image payloads) so
168
+ * markers can be transferred to another editor instance alongside getText().
169
+ */
170
+ getImageMarkerState(): EditorImageState;
171
+ /**
172
+ * Install an image-marker registry snapshot taken from another editor
173
+ * instance. Call after setText() with the source editor's raw text: ids whose
174
+ * markers are absent from the current text are dropped, and the id counter is
175
+ * raised so future marker ids cannot collide.
176
+ */
177
+ setImageMarkerState(state: EditorImageState): void;
178
+ /**
179
+ * Insert the next canonical `[Image #N]` marker at the cursor and return its id.
180
+ * This is atomic for undo - a single undo restores the entire pre-insert state,
181
+ * registry included - matching the insertTextAtCursor() contract.
182
+ *
183
+ * The text is canonicalized before returning: the insertion counter only
184
+ * produces reading-order numbers when the cursor sat after every existing
185
+ * marker, so a paste in front of an earlier marker renumbers the visible
186
+ * markers to stay 1..k in reading order. The returned id is the marker's
187
+ * FINAL canonical number (its 1-based reading position), which is the slot
188
+ * the owner must key its payload under; the notification fires with the
189
+ * PRE-renumber ids, so that slot is guaranteed vacant by the time this
190
+ * returns and a caller registering its payload right after cannot orphan
191
+ * or overwrite a surviving payload.
192
+ */
193
+ insertImageMarker(): number;
194
+ /**
195
+ * Rewrite the buffer with a canonicalized marker numbering, preserving the
196
+ * cursor by folding the length delta of every marker rewrite that ended at
197
+ * or before the cursor on the cursor's line (markers never span lines, and
198
+ * rewrites never change the line count).
199
+ */
200
+ private applyCanonicalizedText;
129
201
  /**
130
202
  * Insert text at the current cursor position.
131
203
  * Used for programmatic insertion (e.g., clipboard image markers).
@@ -1 +1 @@
1
- {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../src/components/editor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAA2B,MAAM,oBAAoB,CAAC;AAIxF,OAAO,EACN,KAAK,gBAAgB,EAKrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,SAAS,EAAiB,KAAK,SAAS,EAAE,KAAK,GAAG,EAAE,MAAM,WAAW,CAAC;AAWpF,OAAO,EAA4C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAKlG;;;GAGG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAyDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,GAAG,SAAS,EAAE,CA+F3G;AAqBD,MAAM,WAAW,WAAW;IAC3B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC;;;;;OAKG;IACH,UAAU,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;AAkCD,qBAAa,MAAO,YAAW,SAAS,EAAE,SAAS;IAClD,OAAO,CAAC,KAAK,CAIX;IAEF,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAS;IAEzB,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;IACnB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,QAAQ,CAAa;IAG7B,OAAO,CAAC,SAAS,CAAc;IAG/B,OAAO,CAAC,YAAY,CAAa;IAG1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAG5C,OAAO,CAAC,oBAAoB,CAAC,CAAuB;IACpD,OAAO,CAAC,6BAA6B,CAAgD;IACrF,OAAO,CAAC,0BAA0B,CAA2D;IAC7F,OAAO,CAAC,2BAA2B,CAA4D;IAC/F,OAAO,CAAC,gBAAgB,CAAC,CAAa;IACtC,OAAO,CAAC,iBAAiB,CAAoC;IAC7D,OAAO,CAAC,kBAAkB,CAAc;IACxC,OAAO,CAAC,sBAAsB,CAAa;IAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAkB;IAC5C,OAAO,CAAC,yBAAyB,CAAC,CAAgC;IAClE,OAAO,CAAC,uBAAuB,CAAoC;IACnE,OAAO,CAAC,sBAAsB,CAAa;IAC3C,OAAO,CAAC,qBAAqB,CAAa;IAG1C,OAAO,CAAC,YAAY,CAA6B;IAGjD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,SAAS,CAAkB;IAGnC,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,YAAY,CAA4B;IAGhD,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,UAAU,CAA8C;IAGhE,OAAO,CAAC,QAAQ,CAAuC;IAGvD,OAAO,CAAC,kBAAkB,CAAuB;IAOjD,OAAO,CAAC,oBAAoB,CAAuB;IAGnD,OAAO,CAAC,SAAS,CAAmC;IACpD,OAAO,CAAC,gBAAgB,CAA2B;IAE5C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,aAAa,EAAE,OAAO,CAAS;gBAE1B,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,aAAkB;IAUrE,sFAAsF;IACtF,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,iBAAiB;IAOzB,WAAW,IAAI,MAAM;IAIrB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQlC,yBAAyB,IAAI,MAAM;IAInC,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAQnD,uBAAuB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAM7D;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAYhC,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,eAAe;IAgCvB,OAAO,CAAC,mBAAmB;IAK3B,kFAAkF;IAClF,OAAO,CAAC,eAAe;IAavB,UAAU,IAAI,IAAI;IAIlB,OAAO,CAAC,cAAc;IA+BtB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IA4H/B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IA8S/B,OAAO,CAAC,UAAU;IA0FlB,OAAO,IAAI,MAAM;IAIjB;;;OAGG;IACH,eAAe,IAAI,MAAM;IAIzB,QAAQ,IAAI,MAAM,EAAE;IAIpB,SAAS,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAI1C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAc3B;;;OAGG;IACH,aAAa,IAAI,gBAAgB;IAIjC;;;;;OAKG;IACH,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAI5C;;;;OAIG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAStC;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAIrB;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;IA4ClC,OAAO,CAAC,eAAe;IA4DvB,OAAO,CAAC,WAAW;IA2DnB,OAAO,CAAC,UAAU;IAyBlB,OAAO,CAAC,4BAA4B;IAWpC,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,eAAe;IA8DvB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAMpB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAkFxB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,yBAAyB;IAiCjC,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,mBAAmB;IAmC3B,OAAO,CAAC,iBAAiB;IAgCzB,OAAO,CAAC,mBAAmB;IA6C3B,OAAO,CAAC,iBAAiB;IA0CzB,OAAO,CAAC,mBAAmB;IAyD3B;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAmBxB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,UAAU;IA+DlB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,iBAAiB;IAsBzB;;OAEG;IACH,OAAO,CAAC,IAAI;IAWZ;;;OAGG;IACH,OAAO,CAAC,OAAO;IAmBf;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAuCxB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAsCxB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,IAAI;IAaZ;;;OAGG;IACH,OAAO,CAAC,UAAU;IA8BlB,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,kBAAkB;IAK1B,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,uBAAuB;IAK/B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,6BAA6B;IAkBrC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,sBAAsB;IAI9B,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,4BAA4B;IAIpC,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,mBAAmB;YA+Bb,wBAAwB;IAuBtC,OAAO,CAAC,gCAAgC;IAaxC,OAAO,CAAC,yBAAyB;YAUnB,sBAAsB;IAoDpC,OAAO,CAAC,4BAA4B;IAgBpC,OAAO,CAAC,4BAA4B;IAYpC,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,kBAAkB;IAKnB,qBAAqB,IAAI,OAAO;IAIvC,OAAO,CAAC,kBAAkB;CAI1B"}
1
+ {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../src/components/editor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAA2B,MAAM,oBAAoB,CAAC;AACxF,OAAO,EACN,KAAK,gBAAgB,EAOrB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EACN,KAAK,gBAAgB,EAKrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,SAAS,EAAiB,KAAK,SAAS,EAAE,KAAK,GAAG,EAAE,MAAM,WAAW,CAAC;AAWpF,OAAO,EAA4C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAKlG,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;AAiB3C;;;GAGG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAyDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,GAAG,SAAS,EAAE,CA+F3G;AAwBD,MAAM,WAAW,WAAW;IAC3B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC;;;;;OAKG;IACH,UAAU,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAChC;AAkCD,qBAAa,MAAO,YAAW,SAAS,EAAE,SAAS;IAClD,OAAO,CAAC,KAAK,CAIX;IAEF,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAS;IAEzB,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;IACnB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,QAAQ,CAAa;IAG7B,OAAO,CAAC,SAAS,CAAc;IAG/B,OAAO,CAAC,YAAY,CAAa;IAG1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAG5C,OAAO,CAAC,oBAAoB,CAAC,CAAuB;IACpD,OAAO,CAAC,6BAA6B,CAAgD;IACrF,OAAO,CAAC,0BAA0B,CAA2D;IAC7F,OAAO,CAAC,2BAA2B,CAA4D;IAC/F,OAAO,CAAC,gBAAgB,CAAC,CAAa;IACtC,OAAO,CAAC,iBAAiB,CAAoC;IAC7D,OAAO,CAAC,kBAAkB,CAAc;IACxC,OAAO,CAAC,sBAAsB,CAAa;IAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAkB;IAC5C,OAAO,CAAC,yBAAyB,CAAC,CAAgC;IAClE,OAAO,CAAC,uBAAuB,CAAoC;IACnE,OAAO,CAAC,sBAAsB,CAAa;IAC3C,OAAO,CAAC,qBAAqB,CAAa;IAG1C,OAAO,CAAC,YAAY,CAA6B;IAGjD,OAAO,CAAC,YAAY,CAA6B;IAGjD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,SAAS,CAAkB;IAGnC,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,YAAY,CAA4B;IAGhD,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,UAAU,CAA8C;IAGhE,OAAO,CAAC,QAAQ,CAAuC;IAGvD,OAAO,CAAC,kBAAkB,CAAuB;IAOjD,OAAO,CAAC,oBAAoB,CAAuB;IAGnD,OAAO,CAAC,SAAS,CAAmC;IACpD,OAAO,CAAC,gBAAgB,CAA2B;IAE5C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC;;;;;;;;;OASG;IACI,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACzD;;;;;;OAMG;IACI,uBAAuB,CAAC,EAAE,MAAM,OAAO,CAAC;IAC/C;;;;;OAKG;IACI,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,aAAa,EAAE,OAAO,CAAS;gBAE1B,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,aAAkB;IAUrE,gFAAgF;IAChF,OAAO,CAAC,OAAO;IAQf,6EAA6E;IAC7E,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,iBAAiB;IAOzB,8EAA8E;IAC9E,OAAO,CAAC,iBAAiB;IAYzB,mGAAmG;IACnG,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,yBAAyB;IAIjC,WAAW,IAAI,MAAM;IAIrB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQlC,yBAAyB,IAAI,MAAM;IAInC,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAQnD,uBAAuB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAM7D;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAYhC,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,eAAe;IAgCvB,OAAO,CAAC,mBAAmB;IAK3B,kFAAkF;IAClF,OAAO,CAAC,eAAe;IAavB,UAAU,IAAI,IAAI;IAIlB,OAAO,CAAC,cAAc;IA+BtB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IA4H/B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IA8S/B,OAAO,CAAC,UAAU;IA0FlB,OAAO,IAAI,MAAM;IAIjB;;;OAGG;IACH,eAAe,IAAI,MAAM;IAIzB,QAAQ,IAAI,MAAM,EAAE;IAIpB,SAAS,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAI1C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAwB3B;;;OAGG;IACH,aAAa,IAAI,gBAAgB;IAIjC;;;;;OAKG;IACH,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAI5C;;;OAGG;IACH,mBAAmB,IAAI,gBAAgB;IAIvC;;;;;OAKG;IACH,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAIlD;;;;;;;;;;;;;;OAcG;IACH,iBAAiB,IAAI,MAAM;IAqB3B;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAoB9B;;;;OAIG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAStC;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAIrB;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;IA4ClC,OAAO,CAAC,eAAe;IA4DvB,OAAO,CAAC,WAAW;IA2DnB,OAAO,CAAC,UAAU;IAyBlB,OAAO,CAAC,4BAA4B;IAWpC,OAAO,CAAC,WAAW;IAiBnB,OAAO,CAAC,eAAe;IA4DvB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAMpB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAkFxB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,yBAAyB;IAiCjC,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,mBAAmB;IAmC3B,OAAO,CAAC,iBAAiB;IAgCzB,OAAO,CAAC,mBAAmB;IA6C3B,OAAO,CAAC,iBAAiB;IA0CzB,OAAO,CAAC,mBAAmB;IAsD3B;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAmBxB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,UAAU;IA+DlB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,iBAAiB;IAsBzB;;OAEG;IACH,OAAO,CAAC,IAAI;IAWZ;;;OAGG;IACH,OAAO,CAAC,OAAO;IAmBf;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAuCxB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAsCxB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,IAAI;IAsBZ;;;OAGG;IACH,OAAO,CAAC,UAAU;IA8BlB,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,kBAAkB;IAK1B,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,uBAAuB;IAK/B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,6BAA6B;IAkBrC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,sBAAsB;IAI9B,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,4BAA4B;IAIpC,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,mBAAmB;YA+Bb,wBAAwB;IAuBtC,OAAO,CAAC,gCAAgC;IAaxC,OAAO,CAAC,yBAAyB;YAUnB,sBAAsB;IAoDpC,OAAO,CAAC,4BAA4B;IAgBpC,OAAO,CAAC,4BAA4B;IAYpC,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,kBAAkB;IAKnB,qBAAqB,IAAI,OAAO;IAIvC,OAAO,CAAC,kBAAkB;CAI1B"}
@@ -1,7 +1,8 @@
1
+ import { formatImageMarker, IMAGE_MARKER_REGEX, ImageMarkerRegistry, imageMarkerId, isImageMarker, } from "../image-markers.js";
1
2
  import { getKeybindings } from "../keybindings.js";
2
3
  import { decodePrintableKey, matchesKey } from "../keys.js";
3
4
  import { KillRing } from "../kill-ring.js";
4
- import { isPasteMarker, PasteMarkerRegistry, pasteMarkerId, segmentWithPasteMarkers, } from "../paste-markers.js";
5
+ import { isPasteMarker, PasteMarkerRegistry, pasteMarkerId, segmentWithMarkers, } from "../paste-markers.js";
5
6
  import { CURSOR_MARKER } from "../tui.js";
6
7
  import { UndoStack } from "../undo-stack.js";
7
8
  import { cjkBreakRegex, getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth, } from "../utils.js";
@@ -9,6 +10,20 @@ import { findWordBackward, findWordForward } from "../word-navigation.js";
9
10
  import { SelectList } from "./select-list.js";
10
11
  const graphemeSegmenter = getGraphemeSegmenter();
11
12
  const wordSegmenter = getWordSegmenter();
13
+ /** Atomic-marker predicate covering every marker family the editor treats as one grapheme. */
14
+ function isAtomicMarker(segment) {
15
+ return isPasteMarker(segment) || isImageMarker(segment);
16
+ }
17
+ function markerKind(segment) {
18
+ if (isPasteMarker(segment))
19
+ return "paste";
20
+ if (isImageMarker(segment))
21
+ return "image";
22
+ return undefined;
23
+ }
24
+ function containsMarkerPrefix(text) {
25
+ return text.includes("[paste #") || text.includes("[Image #");
26
+ }
12
27
  function isPrintableAsciiText(text) {
13
28
  for (let i = 0; i < text.length; i++) {
14
29
  const code = text.charCodeAt(i);
@@ -67,7 +82,7 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
67
82
  if (!line || maxWidth <= 0) {
68
83
  return [{ text: "", startIndex: 0, endIndex: 0 }];
69
84
  }
70
- if (preSegmented === undefined && !/\s/.test(line) && !line.includes("[paste #") && isPrintableAsciiText(line)) {
85
+ if (preSegmented === undefined && !/\s/.test(line) && !containsMarkerPrefix(line) && isPrintableAsciiText(line)) {
71
86
  return wordWrapAsciiLine(line, maxWidth);
72
87
  }
73
88
  const lineWidth = visibleWidth(line);
@@ -87,7 +102,7 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
87
102
  const grapheme = seg.segment;
88
103
  const gWidth = visibleWidth(grapheme);
89
104
  const charIndex = seg.index;
90
- const isWs = !isPasteMarker(grapheme) && isWhitespaceChar(grapheme);
105
+ const isWs = !isAtomicMarker(grapheme) && isWhitespaceChar(grapheme);
91
106
  // Overflow check before advancing.
92
107
  if (currentWidth + gWidth > maxWidth) {
93
108
  if (wrapOppIndex >= 0 && currentWidth - wrapOppWidth + gWidth <= maxWidth) {
@@ -132,13 +147,13 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
132
147
  // or at a boundary where either side is CJK (CJK allows breaking
133
148
  // between any adjacent characters).
134
149
  const next = segments[i + 1];
135
- if (isWs && next && (isPasteMarker(next.segment) || !isWhitespaceChar(next.segment))) {
150
+ if (isWs && next && (isAtomicMarker(next.segment) || !isWhitespaceChar(next.segment))) {
136
151
  wrapOppIndex = next.index;
137
152
  wrapOppWidth = currentWidth;
138
153
  }
139
154
  else if (!isWs && next && !isWhitespaceChar(next.segment)) {
140
- const isCjk = !isPasteMarker(grapheme) && cjkBreakRegex.test(grapheme);
141
- const nextIsCjk = !isPasteMarker(next.segment) && cjkBreakRegex.test(next.segment);
155
+ const isCjk = !isAtomicMarker(grapheme) && cjkBreakRegex.test(grapheme);
156
+ const nextIsCjk = !isAtomicMarker(next.segment) && cjkBreakRegex.test(next.segment);
142
157
  if (isCjk || nextIsCjk) {
143
158
  wrapOppIndex = next.index;
144
159
  wrapOppWidth = currentWidth;
@@ -200,6 +215,8 @@ export class Editor {
200
215
  this.autocompleteRequestId = 0;
201
216
  // Paste tracking for large pastes
202
217
  this.pasteMarkers = new PasteMarkerRegistry();
218
+ // Atomic `[Image #N]` markers; ids only, the owner keeps the image payload
219
+ this.imageMarkers = new ImageMarkerRegistry();
203
220
  // Bracketed paste mode buffering
204
221
  this.pasteBuffer = "";
205
222
  this.isInPaste = false;
@@ -232,9 +249,19 @@ export class Editor {
232
249
  const maxVisible = options.autocompleteMaxVisible ?? 5;
233
250
  this.autocompleteMaxVisible = Number.isFinite(maxVisible) ? Math.max(3, Math.min(20, Math.floor(maxVisible))) : 5;
234
251
  }
235
- /** Segment text with paste-marker awareness, only merging exact canonical markers. */
252
+ /** Segment text with marker awareness, only merging exact canonical markers. */
236
253
  segment(text, mode) {
237
- return segmentWithPasteMarkers(text, mode === "word" ? wordSegmenter : graphemeSegmenter, this.pasteMarkers.authorizedMarkers(text));
254
+ return segmentWithMarkers(text, mode === "word" ? wordSegmenter : graphemeSegmenter, this.authorizedMarkers(text));
255
+ }
256
+ /** Union of every marker family that must segment as one atomic grapheme. */
257
+ authorizedMarkers(text) {
258
+ const imageMarkers = this.imageMarkers.authorizedMarkers(text);
259
+ if (imageMarkers.size === 0)
260
+ return this.pasteMarkers.authorizedMarkers(text);
261
+ const merged = new Set(this.pasteMarkers.authorizedMarkers(text));
262
+ for (const marker of imageMarkers)
263
+ merged.add(marker);
264
+ return merged;
238
265
  }
239
266
  removePasteMarker(id) {
240
267
  const removal = this.pasteMarkers.remove(id, this.getText());
@@ -243,6 +270,37 @@ export class Editor {
243
270
  this.state.lines = removal.text.split("\n");
244
271
  return true;
245
272
  }
273
+ /** Removes an image marker whole, applying the registry's renumbered text. */
274
+ removeImageMarker(id) {
275
+ // The reported order is in PRE-renumber ids: the owner keys its payloads by the
276
+ // numbers it was handed, so it needs the surviving originals - `[2]` after
277
+ // deleting `[Image #1]` - to remap them onto the new 1..k display numbers.
278
+ const survivors = this.imageMarkers.ids(this.getText()).filter((entryId) => entryId !== id);
279
+ const removal = this.imageMarkers.remove(id, this.getText());
280
+ if (!removal.removed)
281
+ return false;
282
+ this.state.lines = removal.text.split("\n");
283
+ this.onImageMarkersChanged?.(survivors);
284
+ return true;
285
+ }
286
+ /** Removes whichever marker family `segment` belongs to; returns false when it is not a marker. */
287
+ removeMarkerSegment(segment) {
288
+ switch (markerKind(segment)) {
289
+ case "paste": {
290
+ const id = pasteMarkerId(segment);
291
+ return id !== undefined && this.removePasteMarker(id);
292
+ }
293
+ case "image": {
294
+ const id = imageMarkerId(segment);
295
+ return id !== undefined && this.removeImageMarker(id);
296
+ }
297
+ default:
298
+ return false;
299
+ }
300
+ }
301
+ notifyImageMarkersChanged() {
302
+ this.onImageMarkersChanged?.(this.imageMarkers.ids(this.getText()));
303
+ }
246
304
  getPaddingX() {
247
305
  return this.paddingX;
248
306
  }
@@ -356,7 +414,7 @@ export class Editor {
356
414
  return cached;
357
415
  }
358
416
  const width = isPrintableAsciiText(line) ? line.length : visibleWidth(line);
359
- if (line.includes("[paste #")) {
417
+ if (containsMarkerPrefix(line)) {
360
418
  return {
361
419
  width,
362
420
  chunks: width <= contentWidth
@@ -861,7 +919,17 @@ export class Editor {
861
919
  this.pushUndoSnapshot();
862
920
  }
863
921
  this.pasteMarkers.prune(normalized, previousText);
922
+ const previousImageOrder = this.imageMarkers.ids(previousText);
923
+ this.imageMarkers.prune(normalized, previousText);
864
924
  this.setTextInternal(normalized);
925
+ // A prune can leave a gap (e.g. only id 2 surviving); renumber so the
926
+ // visible numbers stay 1..k in reading order.
927
+ const canonicalization = this.imageMarkers.canonicalize(normalized);
928
+ this.applyCanonicalizedText(canonicalization);
929
+ const imageOrder = canonicalization.order;
930
+ if (previousImageOrder.length !== imageOrder.length || previousImageOrder.some((id, i) => id !== imageOrder[i])) {
931
+ this.onImageMarkersChanged?.(imageOrder);
932
+ }
865
933
  }
866
934
  /**
867
935
  * Snapshot the large-paste registry (marker id -> stored body) so it can be
@@ -879,6 +947,85 @@ export class Editor {
879
947
  setPasteState(state) {
880
948
  this.pasteMarkers.install(state, this.getText());
881
949
  }
950
+ /**
951
+ * Snapshot the image-marker registry (ids only, never image payloads) so
952
+ * markers can be transferred to another editor instance alongside getText().
953
+ */
954
+ getImageMarkerState() {
955
+ return this.imageMarkers.snapshot();
956
+ }
957
+ /**
958
+ * Install an image-marker registry snapshot taken from another editor
959
+ * instance. Call after setText() with the source editor's raw text: ids whose
960
+ * markers are absent from the current text are dropped, and the id counter is
961
+ * raised so future marker ids cannot collide.
962
+ */
963
+ setImageMarkerState(state) {
964
+ this.imageMarkers.install(state, this.getText());
965
+ }
966
+ /**
967
+ * Insert the next canonical `[Image #N]` marker at the cursor and return its id.
968
+ * This is atomic for undo - a single undo restores the entire pre-insert state,
969
+ * registry included - matching the insertTextAtCursor() contract.
970
+ *
971
+ * The text is canonicalized before returning: the insertion counter only
972
+ * produces reading-order numbers when the cursor sat after every existing
973
+ * marker, so a paste in front of an earlier marker renumbers the visible
974
+ * markers to stay 1..k in reading order. The returned id is the marker's
975
+ * FINAL canonical number (its 1-based reading position), which is the slot
976
+ * the owner must key its payload under; the notification fires with the
977
+ * PRE-renumber ids, so that slot is guaranteed vacant by the time this
978
+ * returns and a caller registering its payload right after cannot orphan
979
+ * or overwrite a surviving payload.
980
+ */
981
+ insertImageMarker() {
982
+ this.cancelAutocomplete();
983
+ this.pushUndoSnapshot();
984
+ this.lastAction = null;
985
+ this.exitHistoryBrowsing();
986
+ const marker = this.imageMarkers.add();
987
+ this.insertTextAtCursorInternal(marker);
988
+ const canonicalization = this.imageMarkers.canonicalize(this.getText());
989
+ this.applyCanonicalizedText(canonicalization);
990
+ const insertionId = imageMarkerId(marker);
991
+ // An id missing from the order means the inserted marker duplicates a
992
+ // hand-typed literal, so the registry treats both occurrences as
993
+ // unauthorized; keep the insertion id so first-occurrence-wins still
994
+ // attaches the payload at submit.
995
+ const finalId = canonicalization.order.includes(insertionId)
996
+ ? canonicalization.order.indexOf(insertionId) + 1
997
+ : insertionId;
998
+ this.onImageMarkersChanged?.(canonicalization.order);
999
+ return finalId;
1000
+ }
1001
+ /**
1002
+ * Rewrite the buffer with a canonicalized marker numbering, preserving the
1003
+ * cursor by folding the length delta of every marker rewrite that ended at
1004
+ * or before the cursor on the cursor's line (markers never span lines, and
1005
+ * rewrites never change the line count).
1006
+ */
1007
+ applyCanonicalizedText(canonicalization) {
1008
+ if (canonicalization.text === this.getText())
1009
+ return;
1010
+ const oldLine = this.state.lines[this.state.cursorLine] ?? "";
1011
+ const cursorCol = this.state.cursorCol;
1012
+ const renumbered = new Map();
1013
+ for (const [index, id] of canonicalization.order.entries()) {
1014
+ renumbered.set(id, index + 1);
1015
+ }
1016
+ let delta = 0;
1017
+ for (const match of oldLine.matchAll(IMAGE_MARKER_REGEX)) {
1018
+ const end = (match.index ?? 0) + match[0].length;
1019
+ if (end > cursorCol)
1020
+ break;
1021
+ const newId = renumbered.get(Number.parseInt(match[1] ?? "0", 10));
1022
+ if (newId === undefined)
1023
+ continue;
1024
+ delta += formatImageMarker(newId).length - match[0].length;
1025
+ }
1026
+ this.state.lines = canonicalization.text.split("\n");
1027
+ this.setCursorCol(Math.max(0, cursorCol + delta));
1028
+ }
882
1029
  /**
883
1030
  * Insert text at the current cursor position.
884
1031
  * Used for programmatic insertion (e.g., clipboard image markers).
@@ -1082,6 +1229,8 @@ export class Editor {
1082
1229
  const result = this.pasteMarkers.expand(this.state.lines.join("\n")).trim();
1083
1230
  this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
1084
1231
  this.pasteMarkers.clear();
1232
+ // Marker numbering is turn-local: ids restart at 1 for the next submission.
1233
+ this.imageMarkers.clear();
1085
1234
  this.exitHistoryBrowsing();
1086
1235
  this.scrollOffset = 0;
1087
1236
  this.undoStack.clear();
@@ -1103,9 +1252,7 @@ export class Editor {
1103
1252
  const graphemes = [...this.segment(beforeCursor, "grapheme")];
1104
1253
  const lastGrapheme = graphemes[graphemes.length - 1];
1105
1254
  const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
1106
- const targetId = pasteMarkerId(lastGrapheme?.segment ?? "");
1107
- if (targetId !== undefined) {
1108
- this.removePasteMarker(targetId);
1255
+ if (this.removeMarkerSegment(lastGrapheme?.segment ?? "")) {
1109
1256
  this.setCursorCol(Math.max(0, this.state.cursorCol - graphemeLength));
1110
1257
  }
1111
1258
  else {
@@ -1414,9 +1561,7 @@ export class Editor {
1414
1561
  // Find the first grapheme at cursor
1415
1562
  const graphemes = [...this.segment(afterCursor, "grapheme")];
1416
1563
  const firstGrapheme = graphemes[0];
1417
- const markerId = firstGrapheme && isPasteMarker(firstGrapheme.segment) ? pasteMarkerId(firstGrapheme.segment) : undefined;
1418
- if (markerId !== undefined) {
1419
- this.removePasteMarker(markerId);
1564
+ if (this.removeMarkerSegment(firstGrapheme?.segment ?? "")) {
1420
1565
  this.onChange?.(this.getText());
1421
1566
  return;
1422
1567
  }
@@ -1597,7 +1742,7 @@ export class Editor {
1597
1742
  }
1598
1743
  this.setCursorCol(findWordBackward(currentLine, this.state.cursorCol, {
1599
1744
  segment: (text) => this.segment(text, "word"),
1600
- isAtomicSegment: isPasteMarker,
1745
+ isAtomicSegment: isAtomicMarker,
1601
1746
  }));
1602
1747
  }
1603
1748
  /**
@@ -1705,6 +1850,8 @@ export class Editor {
1705
1850
  this.undoStack.push({
1706
1851
  state: this.state,
1707
1852
  pasteState: this.pasteMarkers.snapshot(),
1853
+ imageState: this.imageMarkers.snapshot(),
1854
+ attachmentState: this.snapshotAttachmentState?.(),
1708
1855
  });
1709
1856
  }
1710
1857
  undo() {
@@ -1714,11 +1861,20 @@ export class Editor {
1714
1861
  return;
1715
1862
  Object.assign(this.state, snapshot.state);
1716
1863
  this.pasteMarkers.restore(snapshot.pasteState);
1864
+ this.imageMarkers.restore(snapshot.imageState);
1865
+ // Restore the owner's payload snapshot BEFORE the marker-order
1866
+ // notification: the reconcile pass maps payloads by the restored ids, so
1867
+ // restoring after it would re-key the post-delete survivors onto the
1868
+ // restored marker set and permanently drop the deleted marker's image.
1869
+ if (snapshot.attachmentState !== undefined) {
1870
+ this.restoreAttachmentState?.(snapshot.attachmentState);
1871
+ }
1717
1872
  this.lastAction = null;
1718
1873
  this.preferredVisualCol = null;
1719
1874
  if (this.onChange) {
1720
1875
  this.onChange(this.getText());
1721
1876
  }
1877
+ this.notifyImageMarkersChanged();
1722
1878
  }
1723
1879
  /**
1724
1880
  * Jump to the first occurrence of a character in the specified direction.
@@ -1761,7 +1917,7 @@ export class Editor {
1761
1917
  }
1762
1918
  this.setCursorCol(findWordForward(currentLine, this.state.cursorCol, {
1763
1919
  segment: (text) => this.segment(text, "word"),
1764
- isAtomicSegment: isPasteMarker,
1920
+ isAtomicSegment: isAtomicMarker,
1765
1921
  }));
1766
1922
  }
1767
1923
  // Slash menu only allowed on the first line of the editor