@termaxjs/editor-core 0.1.1
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/LICENSE +201 -0
- package/README.md +274 -0
- package/dist/buffer/PieceTable.d.ts +31 -0
- package/dist/buffer/PieceTable.d.ts.map +1 -0
- package/dist/buffer/PieceTable.js +365 -0
- package/dist/buffer/PieceTable.js.map +1 -0
- package/dist/buffer/TerminalBuffer.d.ts +45 -0
- package/dist/buffer/TerminalBuffer.d.ts.map +1 -0
- package/dist/buffer/TerminalBuffer.js +166 -0
- package/dist/buffer/TerminalBuffer.js.map +1 -0
- package/dist/collab/AwarenessState.d.ts +30 -0
- package/dist/collab/AwarenessState.d.ts.map +1 -0
- package/dist/collab/AwarenessState.js +76 -0
- package/dist/collab/AwarenessState.js.map +1 -0
- package/dist/collab/CollabProvider.d.ts +45 -0
- package/dist/collab/CollabProvider.d.ts.map +1 -0
- package/dist/collab/CollabProvider.js +153 -0
- package/dist/collab/CollabProvider.js.map +1 -0
- package/dist/coordinates.d.ts +132 -0
- package/dist/coordinates.d.ts.map +1 -0
- package/dist/coordinates.js +651 -0
- package/dist/coordinates.js.map +1 -0
- package/dist/decoration/Decoration.d.ts +36 -0
- package/dist/decoration/Decoration.d.ts.map +1 -0
- package/dist/decoration/Decoration.js +34 -0
- package/dist/decoration/Decoration.js.map +1 -0
- package/dist/decoration/DecorationSet.d.ts +16 -0
- package/dist/decoration/DecorationSet.d.ts.map +1 -0
- package/dist/decoration/DecorationSet.js +54 -0
- package/dist/decoration/DecorationSet.js.map +1 -0
- package/dist/diff/diff.d.ts +27 -0
- package/dist/diff/diff.d.ts.map +1 -0
- package/dist/diff/diff.js +219 -0
- package/dist/diff/diff.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/HeightMap.d.ts +35 -0
- package/dist/layout/HeightMap.d.ts.map +1 -0
- package/dist/layout/HeightMap.js +95 -0
- package/dist/layout/HeightMap.js.map +1 -0
- package/dist/layout/softWrap.d.ts +23 -0
- package/dist/layout/softWrap.d.ts.map +1 -0
- package/dist/layout/softWrap.js +67 -0
- package/dist/layout/softWrap.js.map +1 -0
- package/dist/state/Annotation.d.ts +22 -0
- package/dist/state/Annotation.d.ts.map +1 -0
- package/dist/state/Annotation.js +38 -0
- package/dist/state/Annotation.js.map +1 -0
- package/dist/state/ChangeSet.d.ts +27 -0
- package/dist/state/ChangeSet.d.ts.map +1 -0
- package/dist/state/ChangeSet.js +162 -0
- package/dist/state/ChangeSet.js.map +1 -0
- package/dist/state/EditorState.d.ts +25 -0
- package/dist/state/EditorState.d.ts.map +1 -0
- package/dist/state/EditorState.js +110 -0
- package/dist/state/EditorState.js.map +1 -0
- package/dist/state/Extension.d.ts +16 -0
- package/dist/state/Extension.d.ts.map +1 -0
- package/dist/state/Extension.js +12 -0
- package/dist/state/Extension.js.map +1 -0
- package/dist/state/Facet.d.ts +13 -0
- package/dist/state/Facet.d.ts.map +1 -0
- package/dist/state/Facet.js +22 -0
- package/dist/state/Facet.js.map +1 -0
- package/dist/state/Keymap.d.ts +22 -0
- package/dist/state/Keymap.d.ts.map +1 -0
- package/dist/state/Keymap.js +10 -0
- package/dist/state/Keymap.js.map +1 -0
- package/dist/state/MultiCursorPaste.d.ts +14 -0
- package/dist/state/MultiCursorPaste.d.ts.map +1 -0
- package/dist/state/MultiCursorPaste.js +18 -0
- package/dist/state/MultiCursorPaste.js.map +1 -0
- package/dist/state/RectangularSelection.d.ts +12 -0
- package/dist/state/RectangularSelection.d.ts.map +1 -0
- package/dist/state/RectangularSelection.js +27 -0
- package/dist/state/RectangularSelection.js.map +1 -0
- package/dist/state/StateField.d.ts +18 -0
- package/dist/state/StateField.d.ts.map +1 -0
- package/dist/state/StateField.js +29 -0
- package/dist/state/StateField.js.map +1 -0
- package/dist/state/Transaction.d.ts +22 -0
- package/dist/state/Transaction.d.ts.map +1 -0
- package/dist/state/Transaction.js +23 -0
- package/dist/state/Transaction.js.map +1 -0
- package/dist/types.d.ts +161 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
const lineStartsCache = new WeakMap();
|
|
2
|
+
/**
|
|
3
|
+
* Compute or retrieve cached line starts table for a given lines array.
|
|
4
|
+
*/
|
|
5
|
+
export function computeLineStarts(lines) {
|
|
6
|
+
const cached = lineStartsCache.get(lines);
|
|
7
|
+
if (cached && cached.length === lines.length) {
|
|
8
|
+
return cached;
|
|
9
|
+
}
|
|
10
|
+
const starts = new Uint32Array(lines.length);
|
|
11
|
+
let acc = 0;
|
|
12
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13
|
+
starts[i] = acc;
|
|
14
|
+
acc += (lines[i]?.length ?? 0) + 1;
|
|
15
|
+
}
|
|
16
|
+
lineStartsCache.set(lines, starts);
|
|
17
|
+
return starts;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Convert 0-indexed (line, column) to character offset in full text.
|
|
21
|
+
*/
|
|
22
|
+
export function positionToOffset(lines, pos) {
|
|
23
|
+
if (lines.length === 0)
|
|
24
|
+
return 0;
|
|
25
|
+
const targetLine = Math.max(0, Math.min(pos.line, lines.length - 1));
|
|
26
|
+
if (lines.length <= 16) {
|
|
27
|
+
let offset = 0;
|
|
28
|
+
for (let i = 0; i < targetLine; i++) {
|
|
29
|
+
offset += (lines[i]?.length ?? 0) + 1; // +1 for newline
|
|
30
|
+
}
|
|
31
|
+
if (targetLine >= 0 && targetLine < lines.length) {
|
|
32
|
+
offset += Math.min(pos.column, lines[targetLine]?.length ?? 0);
|
|
33
|
+
}
|
|
34
|
+
return offset;
|
|
35
|
+
}
|
|
36
|
+
const starts = computeLineStarts(lines);
|
|
37
|
+
const lineStart = starts[targetLine] ?? 0;
|
|
38
|
+
const lineLen = lines[targetLine]?.length ?? 0;
|
|
39
|
+
return lineStart + Math.min(pos.column, lineLen);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Convert character offset to 0-indexed (line, column).
|
|
43
|
+
*/
|
|
44
|
+
export function offsetToPosition(lines, offset) {
|
|
45
|
+
if (lines.length === 0)
|
|
46
|
+
return { line: 0, column: 0 };
|
|
47
|
+
if (offset <= 0)
|
|
48
|
+
return { line: 0, column: 0 };
|
|
49
|
+
if (lines.length <= 16) {
|
|
50
|
+
let current = 0;
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
const lineLen = lines[i]?.length ?? 0;
|
|
53
|
+
if (offset <= current + lineLen) {
|
|
54
|
+
return { line: i, column: Math.max(0, offset - current) };
|
|
55
|
+
}
|
|
56
|
+
current += lineLen + 1; // +1 for newline
|
|
57
|
+
}
|
|
58
|
+
const lastLine = Math.max(0, lines.length - 1);
|
|
59
|
+
return { line: lastLine, column: lines[lastLine]?.length ?? 0 };
|
|
60
|
+
}
|
|
61
|
+
const starts = computeLineStarts(lines);
|
|
62
|
+
let low = 0;
|
|
63
|
+
let high = lines.length - 1;
|
|
64
|
+
let lineIdx = 0;
|
|
65
|
+
while (low <= high) {
|
|
66
|
+
const mid = (low + high) >> 1;
|
|
67
|
+
if (starts[mid] <= offset) {
|
|
68
|
+
lineIdx = mid;
|
|
69
|
+
low = mid + 1;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
high = mid - 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const lineStart = starts[lineIdx] ?? 0;
|
|
76
|
+
const lineLen = lines[lineIdx]?.length ?? 0;
|
|
77
|
+
const column = Math.max(0, Math.min(offset - lineStart, lineLen));
|
|
78
|
+
return { line: lineIdx, column };
|
|
79
|
+
}
|
|
80
|
+
export function isLeadSurrogate(code) {
|
|
81
|
+
return code >= 0xd800 && code <= 0xdbff;
|
|
82
|
+
}
|
|
83
|
+
export function isTrailSurrogate(code) {
|
|
84
|
+
return code >= 0xdc00 && code <= 0xdfff;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Step one code point backward, keeping surrogate pairs (emoji, astral CJK)
|
|
88
|
+
* whole so backspace never leaves a lone surrogate behind.
|
|
89
|
+
*/
|
|
90
|
+
export function stepBackCodePoint(text, offset) {
|
|
91
|
+
if (offset <= 0)
|
|
92
|
+
return 0;
|
|
93
|
+
if (isTrailSurrogate(text.charCodeAt(offset - 1)) &&
|
|
94
|
+
offset >= 2 &&
|
|
95
|
+
isLeadSurrogate(text.charCodeAt(offset - 2))) {
|
|
96
|
+
return offset - 2;
|
|
97
|
+
}
|
|
98
|
+
return offset - 1;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Step one code point forward, keeping surrogate pairs whole so
|
|
102
|
+
* forward-delete never leaves a lone surrogate behind.
|
|
103
|
+
*/
|
|
104
|
+
export function stepForwardCodePoint(text, offset) {
|
|
105
|
+
if (offset >= text.length)
|
|
106
|
+
return text.length;
|
|
107
|
+
if (isLeadSurrogate(text.charCodeAt(offset)) &&
|
|
108
|
+
offset + 1 < text.length &&
|
|
109
|
+
isTrailSurrogate(text.charCodeAt(offset + 1))) {
|
|
110
|
+
return offset + 2;
|
|
111
|
+
}
|
|
112
|
+
return offset + 1;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Convert pixel (x, y) coordinates to (line, column) position.
|
|
116
|
+
*/
|
|
117
|
+
export function pointToPosition(x, y, lineHeight, charWidth) {
|
|
118
|
+
const line = Math.max(0, Math.floor(y / Math.max(1, lineHeight)));
|
|
119
|
+
const column = Math.max(0, Math.round(x / Math.max(1, charWidth)));
|
|
120
|
+
return { line, column };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Convert (line, column) position to pixel (x, y) coordinates.
|
|
124
|
+
*/
|
|
125
|
+
export function positionToPoint(pos, lineHeight, charWidth) {
|
|
126
|
+
return {
|
|
127
|
+
x: pos.column * charWidth,
|
|
128
|
+
y: pos.line * lineHeight,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Clamp a position to valid buffer bounds.
|
|
133
|
+
*/
|
|
134
|
+
export function clampPosition(lines, pos) {
|
|
135
|
+
const total = lines.length;
|
|
136
|
+
if (total === 0)
|
|
137
|
+
return { line: 0, column: 0 };
|
|
138
|
+
const line = Math.max(0, Math.min(pos.line, total - 1));
|
|
139
|
+
const maxCol = Array.isArray(lines) ? (lines[line]?.length ?? 0) : 10000;
|
|
140
|
+
const column = Math.max(0, Math.min(pos.column, maxCol));
|
|
141
|
+
return { line, column };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if two positions are equal.
|
|
145
|
+
*/
|
|
146
|
+
export function arePositionsEqual(a, b) {
|
|
147
|
+
return a.line === b.line && a.column === b.column;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check if position a is strictly before or equal to position b.
|
|
151
|
+
*/
|
|
152
|
+
export function isPositionBefore(a, b) {
|
|
153
|
+
if (a.line < b.line)
|
|
154
|
+
return true;
|
|
155
|
+
if (a.line > b.line)
|
|
156
|
+
return false;
|
|
157
|
+
return a.column <= b.column;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Normalize a Range ensuring start is before or equal to end.
|
|
161
|
+
*/
|
|
162
|
+
export function normalizeRange(range) {
|
|
163
|
+
if (isPositionBefore(range.start, range.end)) {
|
|
164
|
+
return { start: { ...range.start }, end: { ...range.end } };
|
|
165
|
+
}
|
|
166
|
+
return { start: { ...range.end }, end: { ...range.start } };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if a position lies within a Range (inclusive).
|
|
170
|
+
*/
|
|
171
|
+
export function isPositionInRange(pos, range) {
|
|
172
|
+
const norm = normalizeRange(range);
|
|
173
|
+
const afterStart = pos.line > norm.start.line || (pos.line === norm.start.line && pos.column >= norm.start.column);
|
|
174
|
+
const beforeEnd = pos.line < norm.end.line || (pos.line === norm.end.line && pos.column <= norm.end.column);
|
|
175
|
+
return afterStart && beforeEnd;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Find the word boundaries at a given column in a line of text.
|
|
179
|
+
*/
|
|
180
|
+
export function getWordRangeAt(lineText, line, column) {
|
|
181
|
+
if (lineText.length === 0) {
|
|
182
|
+
return { start: { line, column: 0 }, end: { line, column: 0 } };
|
|
183
|
+
}
|
|
184
|
+
const isWordChar = (c) => c !== undefined && /[a-zA-Z0-9_$]/.test(c);
|
|
185
|
+
let startCol = Math.max(0, Math.min(column, lineText.length));
|
|
186
|
+
let endCol = startCol;
|
|
187
|
+
if (startCol < lineText.length && isWordChar(lineText[startCol])) {
|
|
188
|
+
while (startCol > 0 && isWordChar(lineText[startCol - 1])) {
|
|
189
|
+
startCol--;
|
|
190
|
+
}
|
|
191
|
+
while (endCol < lineText.length && isWordChar(lineText[endCol])) {
|
|
192
|
+
endCol++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (startCol > 0 && isWordChar(lineText[startCol - 1])) {
|
|
196
|
+
startCol--;
|
|
197
|
+
while (startCol > 0 && isWordChar(lineText[startCol - 1])) {
|
|
198
|
+
startCol--;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const targetChar = lineText[startCol] ?? " ";
|
|
203
|
+
const isWhitespace = /\s/.test(targetChar);
|
|
204
|
+
if (isWhitespace) {
|
|
205
|
+
while (startCol > 0 && /\s/.test(lineText[startCol - 1] ?? ""))
|
|
206
|
+
startCol--;
|
|
207
|
+
while (endCol < lineText.length && /\s/.test(lineText[endCol] ?? ""))
|
|
208
|
+
endCol++;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
endCol = Math.min(lineText.length, startCol + 1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
start: { line, column: startCol },
|
|
216
|
+
end: { line, column: endCol },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Extract text within a given Range across lines.
|
|
221
|
+
*/
|
|
222
|
+
export function getTextInRange(lines, range) {
|
|
223
|
+
if (!range)
|
|
224
|
+
return "";
|
|
225
|
+
const norm = normalizeRange(range);
|
|
226
|
+
if (norm.start.line === norm.end.line) {
|
|
227
|
+
if (norm.start.column === norm.end.column)
|
|
228
|
+
return "";
|
|
229
|
+
const line = lines[norm.start.line] ?? "";
|
|
230
|
+
return line.slice(norm.start.column, norm.end.column);
|
|
231
|
+
}
|
|
232
|
+
const result = [];
|
|
233
|
+
const firstLine = lines[norm.start.line] ?? "";
|
|
234
|
+
result.push(firstLine.slice(norm.start.column));
|
|
235
|
+
for (let l = norm.start.line + 1; l < norm.end.line; l++) {
|
|
236
|
+
result.push(lines[l] ?? "");
|
|
237
|
+
}
|
|
238
|
+
const lastLine = lines[norm.end.line] ?? "";
|
|
239
|
+
result.push(lastLine.slice(0, norm.end.column));
|
|
240
|
+
return result.join("\n");
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Convert a Selection (anchor + head) to a normalized Range (start <= end).
|
|
244
|
+
*/
|
|
245
|
+
export function normalizeSelection(sel) {
|
|
246
|
+
return normalizeRange({ start: sel.anchor, end: sel.head });
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Check if a selection is collapsed (anchor == head).
|
|
250
|
+
*/
|
|
251
|
+
export function isSelectionCollapsed(sel) {
|
|
252
|
+
return sel.anchor.line === sel.head.line && sel.anchor.column === sel.head.column;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Sort selections descending by maximum position (bottom-to-top, right-to-left).
|
|
256
|
+
* Necessary for multi-cursor mutations to avoid invalidating earlier offsets.
|
|
257
|
+
*/
|
|
258
|
+
export function sortSelectionsDescending(selections) {
|
|
259
|
+
return [...selections].sort((a, b) => {
|
|
260
|
+
const aMax = isPositionBefore(a.anchor, a.head) ? a.head : a.anchor;
|
|
261
|
+
const bMax = isPositionBefore(b.anchor, b.head) ? b.head : b.anchor;
|
|
262
|
+
if (aMax.line !== bMax.line)
|
|
263
|
+
return bMax.line - aMax.line;
|
|
264
|
+
return bMax.column - aMax.column;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Remove duplicate cursors/selections.
|
|
269
|
+
*/
|
|
270
|
+
export function deduplicateSelections(selections) {
|
|
271
|
+
const seen = new Set();
|
|
272
|
+
const result = [];
|
|
273
|
+
for (const sel of selections) {
|
|
274
|
+
const key = `${sel.anchor.line}:${sel.anchor.column}-${sel.head.line}:${sel.head.column}`;
|
|
275
|
+
if (!seen.has(key)) {
|
|
276
|
+
seen.add(key);
|
|
277
|
+
result.push(sel);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Apply text insertion across multiple cursors or selections in a single atomic pass.
|
|
284
|
+
*/
|
|
285
|
+
export function applyMultiCursorInsert(lines, selections, insertText) {
|
|
286
|
+
if (selections.length === 0) {
|
|
287
|
+
return { newLines: lines, newSelections: [] };
|
|
288
|
+
}
|
|
289
|
+
// Fast-path: single cursor/selection typing without line breaks (the 95% common user keystroke)
|
|
290
|
+
if (selections.length === 1 && !insertText.includes("\n") && !insertText.includes("\r")) {
|
|
291
|
+
const s = selections[0];
|
|
292
|
+
const norm = normalizeSelection(s);
|
|
293
|
+
if (norm.start.line === norm.end.line) {
|
|
294
|
+
const line = norm.start.line;
|
|
295
|
+
if (line >= 0 && line < lines.length) {
|
|
296
|
+
const lineStr = lines[line] ?? "";
|
|
297
|
+
const c1 = Math.max(0, Math.min(norm.start.column, lineStr.length));
|
|
298
|
+
const c2 = Math.max(0, Math.min(norm.end.column, lineStr.length));
|
|
299
|
+
const nextLines = lines.slice();
|
|
300
|
+
nextLines[line] = lineStr.slice(0, c1) + insertText + lineStr.slice(c2);
|
|
301
|
+
const newCol = c1 + insertText.length;
|
|
302
|
+
const newPos = { line, column: newCol };
|
|
303
|
+
return {
|
|
304
|
+
newLines: nextLines,
|
|
305
|
+
newSelections: [{ anchor: newPos, head: newPos }],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const fullText = lines.join("\n");
|
|
311
|
+
const ranges = selections.map((s, idx) => {
|
|
312
|
+
const norm = normalizeSelection(s);
|
|
313
|
+
return {
|
|
314
|
+
from: positionToOffset(lines, norm.start),
|
|
315
|
+
to: positionToOffset(lines, norm.end),
|
|
316
|
+
originalIndex: idx,
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
ranges.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
320
|
+
const merged = [];
|
|
321
|
+
for (const r of ranges) {
|
|
322
|
+
if (merged.length > 0 && r.from <= merged[merged.length - 1].to) {
|
|
323
|
+
merged[merged.length - 1].to = Math.max(merged[merged.length - 1].to, r.to);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
merged.push({ ...r });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
let result = "";
|
|
330
|
+
let lastEnd = 0;
|
|
331
|
+
const newOffsets = [];
|
|
332
|
+
for (const r of merged) {
|
|
333
|
+
result += fullText.slice(lastEnd, r.from);
|
|
334
|
+
const cursorOffset = result.length + insertText.length;
|
|
335
|
+
result += insertText;
|
|
336
|
+
newOffsets.push(cursorOffset);
|
|
337
|
+
lastEnd = r.to;
|
|
338
|
+
}
|
|
339
|
+
result += fullText.slice(lastEnd);
|
|
340
|
+
const nextLines = result.split(/\r?\n/);
|
|
341
|
+
const newSelections = newOffsets.map((offset) => {
|
|
342
|
+
const pos = offsetToPosition(nextLines, offset);
|
|
343
|
+
return { anchor: pos, head: pos };
|
|
344
|
+
});
|
|
345
|
+
return {
|
|
346
|
+
newLines: nextLines,
|
|
347
|
+
newSelections: deduplicateSelections(newSelections),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Apply deletion (backspace or forward delete) across multiple cursors or selections.
|
|
352
|
+
*/
|
|
353
|
+
export function applyMultiCursorDelete(lines, selections, isForward) {
|
|
354
|
+
if (selections.length === 0) {
|
|
355
|
+
return { newLines: lines, newSelections: [] };
|
|
356
|
+
}
|
|
357
|
+
const fullText = lines.join("\n");
|
|
358
|
+
const ranges = selections.map((s, idx) => {
|
|
359
|
+
const norm = normalizeSelection(s);
|
|
360
|
+
let from = positionToOffset(lines, norm.start);
|
|
361
|
+
let to = positionToOffset(lines, norm.end);
|
|
362
|
+
if (from === to) {
|
|
363
|
+
if (!isForward) {
|
|
364
|
+
from = stepBackCodePoint(fullText, from);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
to = stepForwardCodePoint(fullText, to);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { from, to, originalIndex: idx };
|
|
371
|
+
});
|
|
372
|
+
ranges.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
373
|
+
const merged = [];
|
|
374
|
+
for (const r of ranges) {
|
|
375
|
+
if (merged.length > 0 && r.from <= merged[merged.length - 1].to) {
|
|
376
|
+
merged[merged.length - 1].to = Math.max(merged[merged.length - 1].to, r.to);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
merged.push({ ...r });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
let result = "";
|
|
383
|
+
let lastEnd = 0;
|
|
384
|
+
const newOffsets = [];
|
|
385
|
+
for (const r of merged) {
|
|
386
|
+
result += fullText.slice(lastEnd, r.from);
|
|
387
|
+
newOffsets.push(result.length);
|
|
388
|
+
lastEnd = r.to;
|
|
389
|
+
}
|
|
390
|
+
result += fullText.slice(lastEnd);
|
|
391
|
+
const nextLines = result.split(/\r?\n/);
|
|
392
|
+
const newSelections = newOffsets.map((offset) => {
|
|
393
|
+
const pos = offsetToPosition(nextLines, offset);
|
|
394
|
+
return { anchor: pos, head: pos };
|
|
395
|
+
});
|
|
396
|
+
return {
|
|
397
|
+
newLines: nextLines,
|
|
398
|
+
newSelections: deduplicateSelections(newSelections),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Find word boundary in a line of text for word-by-word cursor navigation (Ctrl/Alt + Arrow).
|
|
403
|
+
*/
|
|
404
|
+
export function findWordBoundary(lineText, column, direction) {
|
|
405
|
+
if (lineText.length === 0)
|
|
406
|
+
return 0;
|
|
407
|
+
const isWordChar = (c) => c !== undefined && /[a-zA-Z0-9_$]/.test(c);
|
|
408
|
+
const isSpace = (c) => c !== undefined && /\s/.test(c);
|
|
409
|
+
let col = Math.max(0, Math.min(column, lineText.length));
|
|
410
|
+
if (direction === -1) {
|
|
411
|
+
if (col === 0)
|
|
412
|
+
return 0;
|
|
413
|
+
// Step 1: skip whitespace to the left
|
|
414
|
+
while (col > 0 && isSpace(lineText[col - 1])) {
|
|
415
|
+
col--;
|
|
416
|
+
}
|
|
417
|
+
if (col === 0)
|
|
418
|
+
return 0;
|
|
419
|
+
// Step 2: if previous char is word char, jump to word start
|
|
420
|
+
if (isWordChar(lineText[col - 1])) {
|
|
421
|
+
while (col > 0 && isWordChar(lineText[col - 1])) {
|
|
422
|
+
col--;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
// Step 3: if previous char is punctuation/symbol, jump across same symbol group
|
|
427
|
+
while (col > 0 && !isWordChar(lineText[col - 1]) && !isSpace(lineText[col - 1])) {
|
|
428
|
+
col--;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return col;
|
|
432
|
+
}
|
|
433
|
+
// direction === 1 (forward)
|
|
434
|
+
if (col >= lineText.length)
|
|
435
|
+
return lineText.length;
|
|
436
|
+
// Step 1: if currently on whitespace, skip it
|
|
437
|
+
if (isSpace(lineText[col])) {
|
|
438
|
+
while (col < lineText.length && isSpace(lineText[col])) {
|
|
439
|
+
col++;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (col >= lineText.length)
|
|
443
|
+
return lineText.length;
|
|
444
|
+
// Step 2: if on word char, jump to word end
|
|
445
|
+
if (isWordChar(lineText[col])) {
|
|
446
|
+
while (col < lineText.length && isWordChar(lineText[col])) {
|
|
447
|
+
col++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
// Step 3: if on symbol, jump to symbol end
|
|
452
|
+
while (col < lineText.length && !isWordChar(lineText[col]) && !isSpace(lineText[col])) {
|
|
453
|
+
col++;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return col;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Toggle single-line comment on a range of lines.
|
|
460
|
+
*/
|
|
461
|
+
export function toggleLinesComment(lines, startLine, endLine, commentPrefix = "// ") {
|
|
462
|
+
const minLine = Math.max(0, Math.min(startLine, endLine));
|
|
463
|
+
const maxLine = Math.min(lines.length - 1, Math.max(startLine, endLine));
|
|
464
|
+
const prefixTrimmed = commentPrefix.trimEnd();
|
|
465
|
+
// Check if all non-empty lines are already commented
|
|
466
|
+
let allCommented = true;
|
|
467
|
+
let hasNonEmpty = false;
|
|
468
|
+
for (let l = minLine; l <= maxLine; l++) {
|
|
469
|
+
const line = lines[l] ?? "";
|
|
470
|
+
const trimmed = line.trimStart();
|
|
471
|
+
if (trimmed.length > 0) {
|
|
472
|
+
hasNonEmpty = true;
|
|
473
|
+
if (!trimmed.startsWith(prefixTrimmed)) {
|
|
474
|
+
allCommented = false;
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!hasNonEmpty) {
|
|
480
|
+
// If all selected lines are blank, comment the first line
|
|
481
|
+
const newLines = [...lines];
|
|
482
|
+
newLines[minLine] = commentPrefix;
|
|
483
|
+
return { newLines };
|
|
484
|
+
}
|
|
485
|
+
const newLines = [...lines];
|
|
486
|
+
if (allCommented) {
|
|
487
|
+
// Uncomment all lines
|
|
488
|
+
for (let l = minLine; l <= maxLine; l++) {
|
|
489
|
+
const line = lines[l] ?? "";
|
|
490
|
+
const match = line.match(/^(\s*)(.*)$/);
|
|
491
|
+
if (!match)
|
|
492
|
+
continue;
|
|
493
|
+
const indent = match[1] || "";
|
|
494
|
+
const content = match[2] || "";
|
|
495
|
+
if (content.startsWith(commentPrefix)) {
|
|
496
|
+
newLines[l] = indent + content.slice(commentPrefix.length);
|
|
497
|
+
}
|
|
498
|
+
else if (content.startsWith(prefixTrimmed)) {
|
|
499
|
+
newLines[l] = indent + content.slice(prefixTrimmed.length);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
// Find minimum indentation of non-empty lines
|
|
505
|
+
let minIndent = Number.POSITIVE_INFINITY;
|
|
506
|
+
for (let l = minLine; l <= maxLine; l++) {
|
|
507
|
+
const line = lines[l] ?? "";
|
|
508
|
+
if (line.trim().length > 0) {
|
|
509
|
+
const indentMatch = line.match(/^\s*/);
|
|
510
|
+
const indentLen = indentMatch ? indentMatch[0].length : 0;
|
|
511
|
+
if (indentLen < minIndent)
|
|
512
|
+
minIndent = indentLen;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (minIndent === Number.POSITIVE_INFINITY)
|
|
516
|
+
minIndent = 0;
|
|
517
|
+
for (let l = minLine; l <= maxLine; l++) {
|
|
518
|
+
const line = lines[l] ?? "";
|
|
519
|
+
if (line.length >= minIndent) {
|
|
520
|
+
newLines[l] = line.slice(0, minIndent) + commentPrefix + line.slice(minIndent);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
newLines[l] = " ".repeat(minIndent) + commentPrefix + line;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return { newLines };
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Check if the cursor is directly between an open/close bracket pair (e.g. `(|)`),
|
|
531
|
+
* in which case backspace should delete both characters.
|
|
532
|
+
*/
|
|
533
|
+
export function isPairedBracketDelete(lineText, column) {
|
|
534
|
+
if (column <= 0 || column > lineText.length)
|
|
535
|
+
return false;
|
|
536
|
+
const left = lineText[column - 1];
|
|
537
|
+
const right = lineText[column];
|
|
538
|
+
if (!left || !right)
|
|
539
|
+
return false;
|
|
540
|
+
const pairs = {
|
|
541
|
+
"(": ")",
|
|
542
|
+
"[": "]",
|
|
543
|
+
"{": "}",
|
|
544
|
+
'"': '"',
|
|
545
|
+
"'": "'",
|
|
546
|
+
"`": "`",
|
|
547
|
+
};
|
|
548
|
+
return pairs[left] === right;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Compute rectangular / column selection spanning across multiple lines from start to end position.
|
|
552
|
+
* Returns an array of Selections, one for each line in the rectangular bounding box.
|
|
553
|
+
*/
|
|
554
|
+
export function computeColumnSelection(lines, start, end) {
|
|
555
|
+
if (lines.length === 0)
|
|
556
|
+
return [];
|
|
557
|
+
const minLine = Math.max(0, Math.min(start.line, end.line));
|
|
558
|
+
const maxLine = Math.min(lines.length - 1, Math.max(start.line, end.line));
|
|
559
|
+
const minCol = Math.min(start.column, end.column);
|
|
560
|
+
const maxCol = Math.max(start.column, end.column);
|
|
561
|
+
const isForward = start.column <= end.column;
|
|
562
|
+
const selections = [];
|
|
563
|
+
for (let l = minLine; l <= maxLine; l++) {
|
|
564
|
+
const lineLen = lines[l]?.length ?? 0;
|
|
565
|
+
const colA = Math.min(minCol, lineLen);
|
|
566
|
+
const colB = Math.min(maxCol, lineLen);
|
|
567
|
+
if (isForward) {
|
|
568
|
+
selections.push({
|
|
569
|
+
anchor: { line: l, column: colA },
|
|
570
|
+
head: { line: l, column: colB },
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
selections.push({
|
|
575
|
+
anchor: { line: l, column: colB },
|
|
576
|
+
head: { line: l, column: colA },
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return selections;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Compute column positions for vertical indent guides on a line of code.
|
|
584
|
+
*/
|
|
585
|
+
export function computeIndentGuides(lineText, tabSize = 2) {
|
|
586
|
+
if (tabSize <= 0)
|
|
587
|
+
return [];
|
|
588
|
+
const guides = [];
|
|
589
|
+
let leadingSpaces = 0;
|
|
590
|
+
for (let i = 0; i < lineText.length; i++) {
|
|
591
|
+
const ch = lineText[i];
|
|
592
|
+
if (ch === " ") {
|
|
593
|
+
leadingSpaces += 1;
|
|
594
|
+
}
|
|
595
|
+
else if (ch === "\t") {
|
|
596
|
+
leadingSpaces += tabSize - (leadingSpaces % tabSize);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const guideCount = Math.floor(leadingSpaces / tabSize);
|
|
603
|
+
for (let g = 0; g < guideCount; g++) {
|
|
604
|
+
guides.push(g * tabSize);
|
|
605
|
+
}
|
|
606
|
+
return guides;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Calculate rainbow bracket nesting depths and column positions across a line.
|
|
610
|
+
*/
|
|
611
|
+
export function computeRainbowBrackets(lineText, _language = "typescript", startDepth = 0) {
|
|
612
|
+
let depth = Math.max(0, startDepth);
|
|
613
|
+
const brackets = [];
|
|
614
|
+
let inString = null;
|
|
615
|
+
let isEscaped = false;
|
|
616
|
+
for (let i = 0; i < lineText.length; i++) {
|
|
617
|
+
const ch = lineText[i] ?? "";
|
|
618
|
+
if (isEscaped) {
|
|
619
|
+
isEscaped = false;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (ch === "\\") {
|
|
623
|
+
isEscaped = true;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (inString) {
|
|
627
|
+
if (ch === inString) {
|
|
628
|
+
inString = null;
|
|
629
|
+
}
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
633
|
+
inString = ch;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
// Line comments
|
|
637
|
+
if (ch === "/" && lineText[i + 1] === "/") {
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
641
|
+
brackets.push({ col: i, depth, char: ch });
|
|
642
|
+
depth += 1;
|
|
643
|
+
}
|
|
644
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
645
|
+
depth = Math.max(0, depth - 1);
|
|
646
|
+
brackets.push({ col: i, depth, char: ch });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return { nextDepth: depth, brackets };
|
|
650
|
+
}
|
|
651
|
+
//# sourceMappingURL=coordinates.js.map
|