@code-yeongyu/senpi-tui 2026.7.26 → 2026.7.28
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 +16 -6
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +56 -120
- package/dist/components/editor.js.map +1 -1
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +7 -1
- package/dist/components/image.js.map +1 -1
- package/dist/editor-component.d.ts +18 -0
- package/dist/editor-component.d.ts.map +1 -1
- package/dist/editor-component.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/paste-markers.d.ts +29 -0
- package/dist/paste-markers.d.ts.map +1 -0
- package/dist/paste-markers.js +185 -0
- package/dist/paste-markers.js.map +1 -0
- package/dist/terminal-capabilities.d.ts +18 -0
- package/dist/terminal-capabilities.d.ts.map +1 -0
- package/dist/terminal-capabilities.js +62 -0
- package/dist/terminal-capabilities.js.map +1 -0
- package/dist/terminal-image.d.ts +31 -6
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +137 -92
- package/dist/terminal-image.js.map +1 -1
- package/dist/tmux-focus.d.ts +8 -0
- package/dist/tmux-focus.d.ts.map +1 -0
- package/dist/tmux-focus.js +14 -0
- package/dist/tmux-focus.js.map +1 -0
- package/dist/tmux-image-capability.d.ts +17 -0
- package/dist/tmux-image-capability.d.ts.map +1 -0
- package/dist/tmux-image-capability.js +61 -0
- package/dist/tmux-image-capability.js.map +1 -0
- package/dist/tmux-image-probe.d.ts +39 -0
- package/dist/tmux-image-probe.d.ts.map +1 -0
- package/dist/tmux-image-probe.js +121 -0
- package/dist/tmux-image-probe.js.map +1 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +63 -33
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +19 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AutocompleteProvider } from "../autocomplete.ts";
|
|
2
|
+
import { type EditorPasteState } from "../paste-markers.ts";
|
|
2
3
|
import { type Component, type Focusable, type TUI } from "../tui.ts";
|
|
3
4
|
import { type SelectListTheme } from "./select-list.ts";
|
|
4
5
|
/**
|
|
@@ -59,8 +60,7 @@ export declare class Editor implements Component, Focusable {
|
|
|
59
60
|
private autocompleteRequestTask;
|
|
60
61
|
private autocompleteStartToken;
|
|
61
62
|
private autocompleteRequestId;
|
|
62
|
-
private
|
|
63
|
-
private pasteCounter;
|
|
63
|
+
private pasteMarkers;
|
|
64
64
|
private pasteBuffer;
|
|
65
65
|
private isInPaste;
|
|
66
66
|
private history;
|
|
@@ -77,10 +77,9 @@ export declare class Editor implements Component, Focusable {
|
|
|
77
77
|
onChange?: (text: string) => void;
|
|
78
78
|
disableSubmit: boolean;
|
|
79
79
|
constructor(tui: TUI, theme: EditorTheme, options?: EditorOptions);
|
|
80
|
-
/**
|
|
81
|
-
private validPasteIds;
|
|
82
|
-
/** Segment text with paste-marker awareness, only merging markers with valid IDs. */
|
|
80
|
+
/** Segment text with paste-marker awareness, only merging exact canonical markers. */
|
|
83
81
|
private segment;
|
|
82
|
+
private removePasteMarker;
|
|
84
83
|
getPaddingX(): number;
|
|
85
84
|
setPaddingX(padding: number): void;
|
|
86
85
|
getAutocompleteMaxVisible(): number;
|
|
@@ -104,7 +103,6 @@ export declare class Editor implements Component, Focusable {
|
|
|
104
103
|
handleInput(data: string): void;
|
|
105
104
|
private layoutText;
|
|
106
105
|
getText(): string;
|
|
107
|
-
private expandPasteMarkers;
|
|
108
106
|
/**
|
|
109
107
|
* Get text with paste markers expanded to their actual content.
|
|
110
108
|
* Use this when you need the full content (e.g., for external editor).
|
|
@@ -116,6 +114,18 @@ export declare class Editor implements Component, Focusable {
|
|
|
116
114
|
col: number;
|
|
117
115
|
};
|
|
118
116
|
setText(text: string): void;
|
|
117
|
+
/**
|
|
118
|
+
* Snapshot the large-paste registry (marker id -> stored body) so it can be
|
|
119
|
+
* transferred to another editor instance alongside getText().
|
|
120
|
+
*/
|
|
121
|
+
getPasteState(): EditorPasteState;
|
|
122
|
+
/**
|
|
123
|
+
* Install a paste registry snapshot taken from another editor instance.
|
|
124
|
+
* Call after setText() with the source editor's raw text: entries whose
|
|
125
|
+
* markers are not present in the current text are dropped, and the paste
|
|
126
|
+
* counter is raised so future paste ids cannot collide.
|
|
127
|
+
*/
|
|
128
|
+
setPasteState(state: EditorPasteState): void;
|
|
119
129
|
/**
|
|
120
130
|
* Insert text at the current cursor position.
|
|
121
131
|
* 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,EAAE,KAAK,SAAS,EAAiB,KAAK,SAAS,EAAE,KAAK,GAAG,EAAE,MAAM,WAAW,CAAC;AAWpF,OAAO,EAA4C,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../src/components/editor.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getKeybindings } from "../keybindings.js";
|
|
2
2
|
import { decodePrintableKey, matchesKey } from "../keys.js";
|
|
3
3
|
import { KillRing } from "../kill-ring.js";
|
|
4
|
+
import { isPasteMarker, PasteMarkerRegistry, pasteMarkerId, segmentWithPasteMarkers, } from "../paste-markers.js";
|
|
4
5
|
import { CURSOR_MARKER } from "../tui.js";
|
|
5
6
|
import { UndoStack } from "../undo-stack.js";
|
|
6
7
|
import { cjkBreakRegex, getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth, } from "../utils.js";
|
|
@@ -8,66 +9,6 @@ import { findWordBackward, findWordForward } from "../word-navigation.js";
|
|
|
8
9
|
import { SelectList } from "./select-list.js";
|
|
9
10
|
const graphemeSegmenter = getGraphemeSegmenter();
|
|
10
11
|
const wordSegmenter = getWordSegmenter();
|
|
11
|
-
/** Regex matching paste markers like `[paste #1 +123 lines]` or `[paste #2 1234 chars]`. */
|
|
12
|
-
const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]/g;
|
|
13
|
-
/** Non-global version for single-segment testing. */
|
|
14
|
-
const PASTE_MARKER_SINGLE = /^\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]$/;
|
|
15
|
-
/** Check if a segment is a paste marker (i.e. was merged by segmentWithMarkers). */
|
|
16
|
-
function isPasteMarker(segment) {
|
|
17
|
-
return segment.length >= 10 && PASTE_MARKER_SINGLE.test(segment);
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* A segmenter that wraps Intl.Segmenter and merges graphemes that fall
|
|
21
|
-
* within paste markers into single atomic segments. This makes cursor
|
|
22
|
-
* movement, deletion, word-wrap, etc. treat paste markers as single units.
|
|
23
|
-
*
|
|
24
|
-
* Only markers whose numeric ID exists in `validIds` are merged.
|
|
25
|
-
*/
|
|
26
|
-
function segmentWithMarkers(text, baseSegmenter, validIds) {
|
|
27
|
-
// Fast path: no paste markers in the text or no valid IDs.
|
|
28
|
-
if (validIds.size === 0 || !text.includes("[paste #")) {
|
|
29
|
-
return baseSegmenter.segment(text);
|
|
30
|
-
}
|
|
31
|
-
// Find all marker spans with valid IDs.
|
|
32
|
-
const markers = [];
|
|
33
|
-
for (const m of text.matchAll(PASTE_MARKER_REGEX)) {
|
|
34
|
-
const id = Number.parseInt(m[1], 10);
|
|
35
|
-
if (!validIds.has(id))
|
|
36
|
-
continue;
|
|
37
|
-
markers.push({ start: m.index, end: m.index + m[0].length });
|
|
38
|
-
}
|
|
39
|
-
if (markers.length === 0) {
|
|
40
|
-
return baseSegmenter.segment(text);
|
|
41
|
-
}
|
|
42
|
-
// Build merged segment list.
|
|
43
|
-
const baseSegments = baseSegmenter.segment(text);
|
|
44
|
-
const result = [];
|
|
45
|
-
let markerIdx = 0;
|
|
46
|
-
for (const seg of baseSegments) {
|
|
47
|
-
// Skip past markers that are entirely before this segment.
|
|
48
|
-
while (markerIdx < markers.length && markers[markerIdx].end <= seg.index) {
|
|
49
|
-
markerIdx++;
|
|
50
|
-
}
|
|
51
|
-
const marker = markerIdx < markers.length ? markers[markerIdx] : null;
|
|
52
|
-
if (marker && seg.index >= marker.start && seg.index < marker.end) {
|
|
53
|
-
// This segment falls inside a marker.
|
|
54
|
-
// If this is the first segment of the marker, emit a merged segment.
|
|
55
|
-
if (seg.index === marker.start) {
|
|
56
|
-
const markerText = text.slice(marker.start, marker.end);
|
|
57
|
-
result.push({
|
|
58
|
-
segment: markerText,
|
|
59
|
-
index: marker.start,
|
|
60
|
-
input: text,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
// Otherwise skip (already merged into the first segment).
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
result.push(seg);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
12
|
function isPrintableAsciiText(text) {
|
|
72
13
|
for (let i = 0; i < text.length; i++) {
|
|
73
14
|
const code = text.charCodeAt(i);
|
|
@@ -258,8 +199,7 @@ export class Editor {
|
|
|
258
199
|
this.autocompleteStartToken = 0;
|
|
259
200
|
this.autocompleteRequestId = 0;
|
|
260
201
|
// Paste tracking for large pastes
|
|
261
|
-
this.
|
|
262
|
-
this.pasteCounter = 0;
|
|
202
|
+
this.pasteMarkers = new PasteMarkerRegistry();
|
|
263
203
|
// Bracketed paste mode buffering
|
|
264
204
|
this.pasteBuffer = "";
|
|
265
205
|
this.isInPaste = false;
|
|
@@ -292,13 +232,16 @@ export class Editor {
|
|
|
292
232
|
const maxVisible = options.autocompleteMaxVisible ?? 5;
|
|
293
233
|
this.autocompleteMaxVisible = Number.isFinite(maxVisible) ? Math.max(3, Math.min(20, Math.floor(maxVisible))) : 5;
|
|
294
234
|
}
|
|
295
|
-
/**
|
|
296
|
-
validPasteIds() {
|
|
297
|
-
return new Set(this.pastes.keys());
|
|
298
|
-
}
|
|
299
|
-
/** Segment text with paste-marker awareness, only merging markers with valid IDs. */
|
|
235
|
+
/** Segment text with paste-marker awareness, only merging exact canonical markers. */
|
|
300
236
|
segment(text, mode) {
|
|
301
|
-
return
|
|
237
|
+
return segmentWithPasteMarkers(text, mode === "word" ? wordSegmenter : graphemeSegmenter, this.pasteMarkers.authorizedMarkers(text));
|
|
238
|
+
}
|
|
239
|
+
removePasteMarker(id) {
|
|
240
|
+
const removal = this.pasteMarkers.remove(id, this.getText());
|
|
241
|
+
if (!removal.removed)
|
|
242
|
+
return false;
|
|
243
|
+
this.state.lines = removal.text.split("\n");
|
|
244
|
+
return true;
|
|
302
245
|
}
|
|
303
246
|
getPaddingX() {
|
|
304
247
|
return this.paddingX;
|
|
@@ -879,20 +822,12 @@ export class Editor {
|
|
|
879
822
|
getText() {
|
|
880
823
|
return this.state.lines.join("\n");
|
|
881
824
|
}
|
|
882
|
-
expandPasteMarkers(text) {
|
|
883
|
-
let result = text;
|
|
884
|
-
for (const [pasteId, pasteContent] of this.pastes) {
|
|
885
|
-
const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
|
|
886
|
-
result = result.replace(markerRegex, () => pasteContent);
|
|
887
|
-
}
|
|
888
|
-
return result;
|
|
889
|
-
}
|
|
890
825
|
/**
|
|
891
826
|
* Get text with paste markers expanded to their actual content.
|
|
892
827
|
* Use this when you need the full content (e.g., for external editor).
|
|
893
828
|
*/
|
|
894
829
|
getExpandedText() {
|
|
895
|
-
return this.
|
|
830
|
+
return this.pasteMarkers.expand(this.state.lines.join("\n"));
|
|
896
831
|
}
|
|
897
832
|
getLines() {
|
|
898
833
|
return [...this.state.lines];
|
|
@@ -905,14 +840,30 @@ export class Editor {
|
|
|
905
840
|
this.lastAction = null;
|
|
906
841
|
this.exitHistoryBrowsing();
|
|
907
842
|
const normalized = this.normalizeText(text);
|
|
843
|
+
const previousText = this.getText();
|
|
908
844
|
// Push undo snapshot if content differs (makes programmatic changes undoable)
|
|
909
|
-
if (
|
|
845
|
+
if (previousText !== normalized) {
|
|
910
846
|
this.pushUndoSnapshot();
|
|
911
847
|
}
|
|
912
|
-
this.
|
|
913
|
-
this.pasteCounter = 0;
|
|
848
|
+
this.pasteMarkers.prune(normalized, previousText);
|
|
914
849
|
this.setTextInternal(normalized);
|
|
915
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* Snapshot the large-paste registry (marker id -> stored body) so it can be
|
|
853
|
+
* transferred to another editor instance alongside getText().
|
|
854
|
+
*/
|
|
855
|
+
getPasteState() {
|
|
856
|
+
return this.pasteMarkers.snapshot();
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Install a paste registry snapshot taken from another editor instance.
|
|
860
|
+
* Call after setText() with the source editor's raw text: entries whose
|
|
861
|
+
* markers are not present in the current text are dropped, and the paste
|
|
862
|
+
* counter is raised so future paste ids cannot collide.
|
|
863
|
+
*/
|
|
864
|
+
setPasteState(state) {
|
|
865
|
+
this.pasteMarkers.install(state, this.getText());
|
|
866
|
+
}
|
|
916
867
|
/**
|
|
917
868
|
* Insert text at the current cursor position.
|
|
918
869
|
* Used for programmatic insertion (e.g., clipboard image markers).
|
|
@@ -1069,14 +1020,7 @@ export class Editor {
|
|
|
1069
1020
|
// Check if this is a large paste (> 10 lines or > 1000 characters)
|
|
1070
1021
|
const totalChars = filteredText.length;
|
|
1071
1022
|
if (pastedLines.length > 10 || totalChars > 1000) {
|
|
1072
|
-
|
|
1073
|
-
this.pasteCounter++;
|
|
1074
|
-
const pasteId = this.pasteCounter;
|
|
1075
|
-
this.pastes.set(pasteId, filteredText);
|
|
1076
|
-
// Insert marker like "[paste #1 +123 lines]" or "[paste #1 1234 chars]"
|
|
1077
|
-
const marker = pastedLines.length > 10
|
|
1078
|
-
? `[paste #${pasteId} +${pastedLines.length} lines]`
|
|
1079
|
-
: `[paste #${pasteId} ${totalChars} chars]`;
|
|
1023
|
+
const marker = this.pasteMarkers.add(filteredText, pastedLines.length, totalChars);
|
|
1080
1024
|
this.insertTextAtCursorInternal(marker);
|
|
1081
1025
|
return;
|
|
1082
1026
|
}
|
|
@@ -1120,10 +1064,9 @@ export class Editor {
|
|
|
1120
1064
|
}
|
|
1121
1065
|
submitValue() {
|
|
1122
1066
|
this.cancelAutocomplete();
|
|
1123
|
-
const result = this.
|
|
1067
|
+
const result = this.pasteMarkers.expand(this.state.lines.join("\n")).trim();
|
|
1124
1068
|
this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
|
|
1125
|
-
this.
|
|
1126
|
-
this.pasteCounter = 0;
|
|
1069
|
+
this.pasteMarkers.clear();
|
|
1127
1070
|
this.exitHistoryBrowsing();
|
|
1128
1071
|
this.scrollOffset = 0;
|
|
1129
1072
|
this.undoStack.clear();
|
|
@@ -1145,33 +1088,18 @@ export class Editor {
|
|
|
1145
1088
|
const graphemes = [...this.segment(beforeCursor, "grapheme")];
|
|
1146
1089
|
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
1147
1090
|
const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
1148
|
-
const
|
|
1149
|
-
if (
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
this.pastes.set(id - 1, this.pastes.get(id));
|
|
1160
|
-
this.pastes.delete(id);
|
|
1161
|
-
}
|
|
1162
|
-
// Renumber markers with ids greater than the removed one.
|
|
1163
|
-
this.state.lines = this.state.lines.map((line) => line.replace(PASTE_MARKER_REGEX, (fullMatch, idGroup, suffixGroup) => {
|
|
1164
|
-
const x = Number(idGroup);
|
|
1165
|
-
if (x <= targetId)
|
|
1166
|
-
return fullMatch;
|
|
1167
|
-
return `[paste #${x - 1}${suffixGroup}]`;
|
|
1168
|
-
}));
|
|
1091
|
+
const targetId = pasteMarkerId(lastGrapheme?.segment ?? "");
|
|
1092
|
+
if (targetId !== undefined) {
|
|
1093
|
+
this.removePasteMarker(targetId);
|
|
1094
|
+
this.setCursorCol(Math.max(0, this.state.cursorCol - graphemeLength));
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
line = this.state.lines[this.state.cursorLine] || "";
|
|
1098
|
+
const before = line.slice(0, this.state.cursorCol - graphemeLength);
|
|
1099
|
+
const after = line.slice(this.state.cursorCol);
|
|
1100
|
+
this.state.lines[this.state.cursorLine] = before + after;
|
|
1101
|
+
this.setCursorCol(this.state.cursorCol - graphemeLength);
|
|
1169
1102
|
}
|
|
1170
|
-
line = this.state.lines[this.state.cursorLine] || "";
|
|
1171
|
-
const before = line.slice(0, this.state.cursorCol - graphemeLength);
|
|
1172
|
-
const after = line.slice(this.state.cursorCol);
|
|
1173
|
-
this.state.lines[this.state.cursorLine] = before + after;
|
|
1174
|
-
this.setCursorCol(this.state.cursorCol - graphemeLength);
|
|
1175
1103
|
}
|
|
1176
1104
|
else if (this.state.cursorLine > 0) {
|
|
1177
1105
|
this.pushUndoSnapshot();
|
|
@@ -1471,6 +1399,12 @@ export class Editor {
|
|
|
1471
1399
|
// Find the first grapheme at cursor
|
|
1472
1400
|
const graphemes = [...this.segment(afterCursor, "grapheme")];
|
|
1473
1401
|
const firstGrapheme = graphemes[0];
|
|
1402
|
+
const markerId = firstGrapheme && isPasteMarker(firstGrapheme.segment) ? pasteMarkerId(firstGrapheme.segment) : undefined;
|
|
1403
|
+
if (markerId !== undefined) {
|
|
1404
|
+
this.removePasteMarker(markerId);
|
|
1405
|
+
this.onChange?.(this.getText());
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1474
1408
|
const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
|
|
1475
1409
|
const before = currentLine.slice(0, this.state.cursorCol);
|
|
1476
1410
|
const after = currentLine.slice(this.state.cursorCol + graphemeLength);
|
|
@@ -1753,7 +1687,10 @@ export class Editor {
|
|
|
1753
1687
|
}
|
|
1754
1688
|
}
|
|
1755
1689
|
pushUndoSnapshot() {
|
|
1756
|
-
this.undoStack.push({
|
|
1690
|
+
this.undoStack.push({
|
|
1691
|
+
state: this.state,
|
|
1692
|
+
pasteState: this.pasteMarkers.snapshot(),
|
|
1693
|
+
});
|
|
1757
1694
|
}
|
|
1758
1695
|
undo() {
|
|
1759
1696
|
this.exitHistoryBrowsing();
|
|
@@ -1761,8 +1698,7 @@ export class Editor {
|
|
|
1761
1698
|
if (!snapshot)
|
|
1762
1699
|
return;
|
|
1763
1700
|
Object.assign(this.state, snapshot.state);
|
|
1764
|
-
this.
|
|
1765
|
-
this.pasteCounter = snapshot.pasteCounter;
|
|
1701
|
+
this.pasteMarkers.restore(snapshot.pasteState);
|
|
1766
1702
|
this.lastAction = null;
|
|
1767
1703
|
this.preferredVisualCol = null;
|
|
1768
1704
|
if (this.onChange) {
|