@code-yeongyu/senpi-tui 2026.8.18 → 2026.8.19
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/components/editor.d.ts +73 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +173 -17
- package/dist/components/editor.js.map +1 -1
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +5 -4
- package/dist/components/text.js.map +1 -1
- package/dist/editor-component.d.ts +69 -0
- package/dist/editor-component.d.ts.map +1 -1
- package/dist/editor-component.js.map +1 -1
- package/dist/image-markers.d.ts +46 -0
- package/dist/image-markers.d.ts.map +1 -0
- package/dist/image-markers.js +130 -0
- package/dist/image-markers.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/paste-markers.d.ts +3 -0
- package/dist/paste-markers.d.ts.map +1 -1
- package/dist/paste-markers.js +23 -7
- package/dist/paste-markers.js.map +1 -1
- package/dist/tui-alt-screen.d.ts.map +1 -1
- package/dist/tui-alt-screen.js +5 -1
- package/dist/tui-alt-screen.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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;
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../src/components/editor.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,
|
|
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
|
|
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 = !
|
|
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 && (
|
|
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 = !
|
|
141
|
-
const nextIsCjk = !
|
|
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
|
|
252
|
+
/** Segment text with marker awareness, only merging exact canonical markers. */
|
|
236
253
|
segment(text, mode) {
|
|
237
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1920
|
+
isAtomicSegment: isAtomicMarker,
|
|
1765
1921
|
}));
|
|
1766
1922
|
}
|
|
1767
1923
|
// Slash menu only allowed on the first line of the editor
|