@adecore/editor-core 0.38.0 → 0.40.0
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/block-folds.js +5 -2
- package/dist/bracket-tracker.js +4 -3
- package/dist/brackets.d.ts +2 -12
- package/dist/brackets.js +3 -15
- package/dist/closer-tracker.js +0 -1
- package/dist/doc-comments.js +0 -1
- package/dist/document.d.ts +2 -0
- package/dist/document.js +33 -35
- package/dist/edit-regions.js +3 -4
- package/dist/enter.d.ts +2 -0
- package/dist/enter.js +11 -9
- package/dist/fold-roles.js +2 -2
- package/dist/lexical-folds.d.ts +0 -1
- package/dist/lexical-folds.js +1 -4
- package/dist/lexical.d.ts +11 -5
- package/dist/lexical.js +13 -17
- package/dist/line-commands.js +7 -15
- package/dist/line-text.d.ts +2 -0
- package/dist/line-text.js +4 -0
- package/dist/markup-regions.js +2 -3
- package/dist/paste.js +2 -7
- package/dist/structure.js +2 -1
- package/dist/typing-context.js +5 -15
- package/package.json +2 -2
- package/src/block-folds.ts +39 -2
- package/src/bracket-tracker.ts +55 -12
- package/src/brackets.ts +35 -42
- package/src/closer-tracker.ts +6 -1
- package/src/comments.ts +103 -0
- package/src/doc-comments.ts +19 -1
- package/src/document.ts +505 -36
- package/src/edit-regions.ts +26 -4
- package/src/enter.ts +104 -9
- package/src/fold-roles.ts +34 -2
- package/src/index.ts +2 -0
- package/src/languages.ts +11 -0
- package/src/lexical-folds.ts +21 -5
- package/src/lexical.ts +31 -23
- package/src/line-commands.ts +81 -17
- package/src/line-text.ts +7 -0
- package/src/markup-regions.ts +101 -3
- package/src/offsets.ts +10 -0
- package/src/paste.ts +20 -7
- package/src/php-markup.ts +23 -0
- package/src/preserve-case.ts +12 -0
- package/src/rope.ts +70 -0
- package/src/script-source.ts +10 -0
- package/src/search.ts +38 -0
- package/src/structure.ts +29 -1
- package/src/text-span.ts +53 -0
- package/src/typing-context.ts +65 -41
- package/src/typing-handlers.ts +28 -0
- package/src/words.ts +10 -0
package/src/document.ts
CHANGED
|
@@ -156,6 +156,7 @@ const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
|
156
156
|
/* What can follow `->`: the first letter of a member name, or the `{` of a dynamic one. */
|
|
157
157
|
const arrowContinues = /^[\p{L}_{]/u;
|
|
158
158
|
const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
|
|
159
|
+
|
|
159
160
|
let graphemes: Intl.Segmenter | undefined;
|
|
160
161
|
|
|
161
162
|
function sameSelections(left: readonly Selection[], right: readonly Selection[]): boolean {
|
|
@@ -163,7 +164,7 @@ function sameSelections(left: readonly Selection[], right: readonly Selection[])
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
function trailingBlankLength(text: string): number {
|
|
166
|
-
return
|
|
167
|
+
return /[\t ]*$/.exec(text)![0].length;
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
function changesText(rope: TextRope, edit: TextEdit): boolean {
|
|
@@ -173,51 +174,65 @@ function changesText(rope: TextRope, edit: TextEdit): boolean {
|
|
|
173
174
|
/* Merges the carets' edits into one simultaneous batch and works out where each caret lands in the result. */
|
|
174
175
|
function selectionTransaction(plans: readonly SelectionEdit[]): SelectionTransaction {
|
|
175
176
|
const edits: TextEdit[] = [];
|
|
177
|
+
|
|
176
178
|
for (const plan of plans.filter((candidate) => !candidate.skip).sort((left, right) => left.from - right.from || left.to - right.to)) {
|
|
177
179
|
const previous = edits.at(-1);
|
|
180
|
+
|
|
178
181
|
if (previous?.from !== plan.from || previous.to !== plan.to || previous.text !== plan.text) {
|
|
179
182
|
edits.push(plan);
|
|
180
183
|
}
|
|
181
184
|
}
|
|
185
|
+
|
|
182
186
|
// What the edits before each one add to the text, so a caret finds its shift by a binary search.
|
|
183
187
|
const shifts = [0];
|
|
188
|
+
|
|
184
189
|
for (const edit of edits) {
|
|
185
190
|
shifts.push(shifts.at(-1)! + edit.text.length - (edit.to - edit.from));
|
|
186
191
|
}
|
|
192
|
+
|
|
187
193
|
const selections = plans.map((plan) => {
|
|
188
194
|
let low = 0;
|
|
189
195
|
let high = edits.length;
|
|
196
|
+
|
|
190
197
|
while (low < high) {
|
|
191
198
|
const middle = (low + high) >>> 1;
|
|
199
|
+
|
|
192
200
|
if (edits[middle]!.from < plan.from) {
|
|
193
201
|
low = middle + 1;
|
|
194
202
|
} else {
|
|
195
203
|
high = middle;
|
|
196
204
|
}
|
|
197
205
|
}
|
|
206
|
+
|
|
198
207
|
const delta = shifts[low]!;
|
|
208
|
+
|
|
199
209
|
return { anchor: plan.from + delta + plan.anchor, head: plan.from + delta + plan.head };
|
|
200
210
|
});
|
|
211
|
+
|
|
201
212
|
return { edits, selections };
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
function splitsCluster(rope: TextRope, offset: number): boolean {
|
|
205
216
|
const before = rope.charAt(offset - 1);
|
|
217
|
+
|
|
206
218
|
// One read of the rope settles most offsets, which follow neither a `\r` nor the first half of a surrogate pair.
|
|
207
219
|
if (before === '\r') {
|
|
208
220
|
return rope.charAt(offset) === '\n';
|
|
209
221
|
}
|
|
222
|
+
|
|
210
223
|
return /[\ud800-\udbff]/.test(before) && splitsSurrogate(rope, offset);
|
|
211
224
|
}
|
|
212
225
|
|
|
213
226
|
/* Clamps to the document and moves back off the middle of a surrogate pair or a CRLF. */
|
|
214
227
|
function offsetIn(rope: TextRope, value: number): number {
|
|
215
228
|
const offset = clampInteger(value, rope.length);
|
|
229
|
+
|
|
216
230
|
return splitsCluster(rope, offset) ? offset - 1 : offset;
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
function step(rope: TextRope, offset: number, direction: -1 | 1): number {
|
|
220
234
|
const next = clampInteger(offset + direction, rope.length);
|
|
235
|
+
|
|
221
236
|
return splitsCluster(rope, next) ? next + direction : next;
|
|
222
237
|
}
|
|
223
238
|
|
|
@@ -230,25 +245,33 @@ function deletionStep(rope: TextRope, offset: number, direction: -1 | 1): number
|
|
|
230
245
|
const scalar = step(rope, offset, direction);
|
|
231
246
|
const current = rope.charAt(direction < 0 ? offset - 1 : offset);
|
|
232
247
|
const following = rope.charAt(direction < 0 ? offset : offset + 1);
|
|
248
|
+
|
|
233
249
|
if (current === '\n' || current === '\r') {
|
|
234
250
|
return scalar;
|
|
235
251
|
}
|
|
252
|
+
|
|
236
253
|
if (isAsciiOrEmpty(current) && isAsciiOrEmpty(following)) {
|
|
237
254
|
return scalar;
|
|
238
255
|
}
|
|
256
|
+
|
|
239
257
|
const line = rope.getLine(rope.lineAt(offset));
|
|
240
258
|
let previous = line.start;
|
|
241
259
|
graphemes ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
260
|
+
|
|
242
261
|
for (const segment of graphemes.segment(line.text)) {
|
|
243
262
|
const at = line.start + segment.index;
|
|
263
|
+
|
|
244
264
|
if (direction < 0 && at >= offset) {
|
|
245
265
|
return previous;
|
|
246
266
|
}
|
|
267
|
+
|
|
247
268
|
if (direction > 0 && at > offset) {
|
|
248
269
|
return at;
|
|
249
270
|
}
|
|
271
|
+
|
|
250
272
|
previous = at;
|
|
251
273
|
}
|
|
274
|
+
|
|
252
275
|
return direction < 0 ? previous : line.end;
|
|
253
276
|
}
|
|
254
277
|
|
|
@@ -258,14 +281,19 @@ function deletionStep(rope: TextRope, offset: number, direction: -1 | 1): number
|
|
|
258
281
|
*/
|
|
259
282
|
function normalizeSelections(rope: TextRope, selections: readonly Selection[]): Selection[] {
|
|
260
283
|
const input = selections.length > 0 ? selections : [{ anchor: 0, head: 0 }];
|
|
284
|
+
|
|
261
285
|
const clamped = input.map((raw) => {
|
|
262
286
|
const anchor = offsetIn(rope, raw.anchor);
|
|
287
|
+
|
|
263
288
|
return { anchor, head: raw.head === raw.anchor ? anchor : offsetIn(rope, raw.head) };
|
|
264
289
|
});
|
|
290
|
+
|
|
265
291
|
const order = clamped.map((selection, index) => ({ ...rangeOf(selection), index })).sort((left, right) => left.from - right.from);
|
|
266
292
|
const groups: { from: number; to: number; last: number }[] = [];
|
|
293
|
+
|
|
267
294
|
for (const { from, to, index } of order) {
|
|
268
295
|
const group = groups.at(-1);
|
|
296
|
+
|
|
269
297
|
if (group !== undefined && from <= group.to) {
|
|
270
298
|
group.to = Math.max(group.to, to);
|
|
271
299
|
group.last = Math.max(group.last, index);
|
|
@@ -273,19 +301,23 @@ function normalizeSelections(rope: TextRope, selections: readonly Selection[]):
|
|
|
273
301
|
groups.push({ from, to, last: index });
|
|
274
302
|
}
|
|
275
303
|
}
|
|
304
|
+
|
|
276
305
|
return groups
|
|
277
306
|
.sort((left, right) => left.last - right.last)
|
|
278
307
|
.map((group) => {
|
|
279
308
|
const selection = clamped[group.last]!;
|
|
309
|
+
|
|
280
310
|
return selection.anchor > selection.head ? { anchor: group.to, head: group.from } : { anchor: group.from, head: group.to };
|
|
281
311
|
});
|
|
282
312
|
}
|
|
283
313
|
|
|
284
314
|
function inverseChanges(changes: readonly DocumentChange[]): DocumentChange[] {
|
|
285
315
|
let delta = 0;
|
|
316
|
+
|
|
286
317
|
return changes.map((change) => {
|
|
287
318
|
const from = change.from + delta;
|
|
288
319
|
delta += change.insertedLength - (change.to - change.from);
|
|
320
|
+
|
|
289
321
|
return { from, to: from + change.insertedLength, insertedLength: change.to - change.from };
|
|
290
322
|
});
|
|
291
323
|
}
|
|
@@ -299,14 +331,17 @@ function range(first: number, last: number): number[] {
|
|
|
299
331
|
/* Runs of consecutive line numbers, from a sorted list. */
|
|
300
332
|
function blocks(indices: readonly number[]): LineBlock[] {
|
|
301
333
|
const result: LineBlock[] = [];
|
|
334
|
+
|
|
302
335
|
for (const index of indices) {
|
|
303
336
|
const previous = result.at(-1);
|
|
337
|
+
|
|
304
338
|
if (previous && previous.last + 1 === index) {
|
|
305
339
|
previous.last = index;
|
|
306
340
|
} else {
|
|
307
341
|
result.push({ first: index, last: index });
|
|
308
342
|
}
|
|
309
343
|
}
|
|
344
|
+
|
|
310
345
|
return result;
|
|
311
346
|
}
|
|
312
347
|
|
|
@@ -316,20 +351,25 @@ function indentStopLength(prefix: string, tabSize: number): number {
|
|
|
316
351
|
const target = Math.floor((column - 1) / tabSize) * tabSize;
|
|
317
352
|
let width = 0;
|
|
318
353
|
let length = 0;
|
|
354
|
+
|
|
319
355
|
while (length < prefix.length) {
|
|
320
356
|
const next = width + (prefix[length] === '\t' ? tabSize - (width % tabSize) : 1);
|
|
357
|
+
|
|
321
358
|
if (next > target) {
|
|
322
359
|
break;
|
|
323
360
|
}
|
|
361
|
+
|
|
324
362
|
width = next;
|
|
325
363
|
length++;
|
|
326
364
|
}
|
|
365
|
+
|
|
327
366
|
return length;
|
|
328
367
|
}
|
|
329
368
|
|
|
330
369
|
function positionIn(rope: TextRope, offset: number): Position {
|
|
331
370
|
const line = rope.lineAt(offset);
|
|
332
371
|
const bounds = rope.lineBounds(line);
|
|
372
|
+
|
|
333
373
|
return { line, column: Math.min(offset, bounds.end) - bounds.start };
|
|
334
374
|
}
|
|
335
375
|
|
|
@@ -345,9 +385,11 @@ function editsOf(rope: TextRope, edits: readonly TextEdit[]): ContentEdit[] {
|
|
|
345
385
|
*/
|
|
346
386
|
function stretchEdits(before: TextRope, after: TextRope, transactions: readonly (readonly DocumentChange[])[]): ContentEdit[] {
|
|
347
387
|
const stretch = stretchOf(before, after, transactions);
|
|
388
|
+
|
|
348
389
|
if (stretch === undefined) {
|
|
349
390
|
return [];
|
|
350
391
|
}
|
|
392
|
+
|
|
351
393
|
return [
|
|
352
394
|
{ start: positionIn(before, stretch.from), end: positionIn(before, stretch.to), text: after.slice(stretch.from, stretch.from + stretch.insertedLength) }
|
|
353
395
|
];
|
|
@@ -358,24 +400,31 @@ function stretchOf(before: TextRope, after: TextRope, transactions: readonly (re
|
|
|
358
400
|
let length = before.length;
|
|
359
401
|
let prefix = Number.POSITIVE_INFINITY;
|
|
360
402
|
let suffix = Number.POSITIVE_INFINITY;
|
|
403
|
+
|
|
361
404
|
for (const changes of transactions) {
|
|
362
405
|
let delta = 0;
|
|
406
|
+
|
|
363
407
|
for (const change of changes) {
|
|
364
408
|
prefix = Math.min(prefix, change.from);
|
|
365
409
|
suffix = Math.min(suffix, length - change.to);
|
|
366
410
|
delta += change.insertedLength - (change.to - change.from);
|
|
367
411
|
}
|
|
412
|
+
|
|
368
413
|
length += delta;
|
|
369
414
|
}
|
|
415
|
+
|
|
370
416
|
if (!Number.isFinite(prefix)) {
|
|
371
417
|
return undefined;
|
|
372
418
|
}
|
|
419
|
+
|
|
373
420
|
const kept = Math.min(Math.max(0, suffix), before.length - prefix, after.length - prefix);
|
|
374
421
|
const oldEnd = before.length - kept;
|
|
375
422
|
const newEnd = after.length - kept;
|
|
423
|
+
|
|
376
424
|
if (oldEnd === prefix && newEnd === prefix) {
|
|
377
425
|
return undefined;
|
|
378
426
|
}
|
|
427
|
+
|
|
379
428
|
return { from: prefix, to: oldEnd, insertedLength: newEnd - prefix };
|
|
380
429
|
}
|
|
381
430
|
|
|
@@ -384,7 +433,9 @@ function occurrenceSelection(match: OffsetRange, shift: number): Selection {
|
|
|
384
433
|
if (shift === 0) {
|
|
385
434
|
return { anchor: match.to, head: match.from };
|
|
386
435
|
}
|
|
436
|
+
|
|
387
437
|
const caret = Math.min(match.from + shift, match.to);
|
|
438
|
+
|
|
388
439
|
return { anchor: caret === match.to ? match.from : match.to, head: caret };
|
|
389
440
|
}
|
|
390
441
|
|
|
@@ -477,6 +528,7 @@ export class DocumentModel {
|
|
|
477
528
|
const rope = this.rope;
|
|
478
529
|
const cache = this.textCache;
|
|
479
530
|
let text: string | undefined;
|
|
531
|
+
|
|
480
532
|
return {
|
|
481
533
|
// Bound to this version's rope, so a retained snapshot keeps its own text however the document moves on.
|
|
482
534
|
get text() {
|
|
@@ -501,9 +553,11 @@ export class DocumentModel {
|
|
|
501
553
|
this.selectionHistory = [];
|
|
502
554
|
this.statementClosers.clear();
|
|
503
555
|
this.arrows.clear();
|
|
556
|
+
|
|
504
557
|
if (sameSelections(this.selections, normalized)) {
|
|
505
558
|
return;
|
|
506
559
|
}
|
|
560
|
+
|
|
507
561
|
this.selections = normalized;
|
|
508
562
|
this.closers.prune(this.selections);
|
|
509
563
|
this.emit();
|
|
@@ -511,6 +565,7 @@ export class DocumentModel {
|
|
|
511
565
|
|
|
512
566
|
subscribe(listener: (snapshot: EditorSnapshot) => void): Disposable {
|
|
513
567
|
this.listeners.add(listener);
|
|
568
|
+
|
|
514
569
|
return {
|
|
515
570
|
dispose: () => {
|
|
516
571
|
this.listeners.delete(listener);
|
|
@@ -526,31 +581,40 @@ export class DocumentModel {
|
|
|
526
581
|
if (options.expectedRevision !== undefined && options.expectedRevision !== this.revision) {
|
|
527
582
|
return false;
|
|
528
583
|
}
|
|
584
|
+
|
|
529
585
|
const unique = this.validateEdits(edits);
|
|
530
586
|
const changed = unique.filter((edit) => changesText(this.rope, edit));
|
|
531
587
|
// Sparse edits keep structural sharing; dense edits avoid rescanning a leaf for each replacement.
|
|
532
588
|
const rebuild = changed.length >= 256 && changed.length * ROPE_CHUNK_SIZE >= this.rope.length;
|
|
533
589
|
let next = rebuild ? this.rope.rebuild(changed) : this.rope;
|
|
590
|
+
|
|
534
591
|
if (!rebuild) {
|
|
535
592
|
for (let index = changed.length - 1; index >= 0; index--) {
|
|
536
593
|
const edit = changed[index]!;
|
|
537
594
|
next = next.replace(edit.from, edit.to, edit.text);
|
|
538
595
|
}
|
|
539
596
|
}
|
|
597
|
+
|
|
540
598
|
const map = offsetMapper(unique);
|
|
599
|
+
|
|
541
600
|
const at = (position: Position): number => {
|
|
542
601
|
const line = next.lineBounds(position.line);
|
|
602
|
+
|
|
543
603
|
return offsetIn(next, line.start + clampInteger(position.column, line.end - line.start));
|
|
544
604
|
};
|
|
605
|
+
|
|
545
606
|
// Several edits can cancel each other out, like moving a character and moving it back.
|
|
546
607
|
const unchanged =
|
|
547
608
|
next === this.rope || (changed.length > 1 && next.length === this.rope.length && next.equalsRange(this.rope, changed[0]!.from, changed.at(-1)!.to));
|
|
609
|
+
|
|
548
610
|
if (unchanged) {
|
|
549
611
|
if (options.selections) {
|
|
550
612
|
this.setSelections(options.selections);
|
|
551
613
|
}
|
|
614
|
+
|
|
552
615
|
return false;
|
|
553
616
|
}
|
|
617
|
+
|
|
554
618
|
const nextSelections =
|
|
555
619
|
options.selections ??
|
|
556
620
|
options.selectionPositions?.map((selection) => ({ anchor: at(selection.anchor), head: at(selection.head) })) ??
|
|
@@ -570,30 +634,37 @@ export class DocumentModel {
|
|
|
570
634
|
this.revision++;
|
|
571
635
|
const after = this.state();
|
|
572
636
|
const last = this.undoStack.at(-1);
|
|
637
|
+
|
|
573
638
|
const joinsGroup =
|
|
574
639
|
options.historyGroup &&
|
|
575
640
|
this.groupOpen &&
|
|
576
641
|
last?.group === options.historyGroup &&
|
|
577
642
|
last.after.rope === before.rope &&
|
|
578
643
|
sameSelections(last.after.selections, before.selections);
|
|
644
|
+
|
|
579
645
|
const joinsPrevious = options.joinPrevious === true && last !== undefined && last.after.rope === before.rope;
|
|
646
|
+
|
|
580
647
|
if (last !== undefined && (joinsGroup || joinsPrevious)) {
|
|
581
648
|
last.after = after;
|
|
582
649
|
last.changes.push(changes);
|
|
583
650
|
} else {
|
|
584
651
|
this.undoStack.push({ before, after, group: options.historyGroup, changes: [changes] });
|
|
585
652
|
}
|
|
653
|
+
|
|
586
654
|
if (this.undoStack.length > historyLimit) {
|
|
587
655
|
this.undoStack.shift();
|
|
588
656
|
}
|
|
657
|
+
|
|
589
658
|
this.groupOpen = Boolean(options.historyGroup) && !joinsPrevious;
|
|
590
659
|
this.redoStack = [];
|
|
660
|
+
|
|
591
661
|
this.emit(
|
|
592
662
|
[changes],
|
|
593
663
|
options.source ?? 'external',
|
|
594
664
|
() => (rebuild ? stretchEdits(previous, next, [changes]) : editsOf(previous, changed)),
|
|
595
665
|
options.typed
|
|
596
666
|
);
|
|
667
|
+
|
|
597
668
|
return true;
|
|
598
669
|
}
|
|
599
670
|
|
|
@@ -604,21 +675,27 @@ export class DocumentModel {
|
|
|
604
675
|
|
|
605
676
|
undo(): boolean {
|
|
606
677
|
const entry = this.undoStack.pop();
|
|
678
|
+
|
|
607
679
|
if (!entry) {
|
|
608
680
|
return false;
|
|
609
681
|
}
|
|
682
|
+
|
|
610
683
|
this.redoStack.push(entry);
|
|
611
684
|
this.restore(entry.before, [...entry.changes].reverse().map(inverseChanges));
|
|
685
|
+
|
|
612
686
|
return true;
|
|
613
687
|
}
|
|
614
688
|
|
|
615
689
|
redo(): boolean {
|
|
616
690
|
const entry = this.redoStack.pop();
|
|
691
|
+
|
|
617
692
|
if (!entry) {
|
|
618
693
|
return false;
|
|
619
694
|
}
|
|
695
|
+
|
|
620
696
|
this.undoStack.push(entry);
|
|
621
697
|
this.restore(entry.after, entry.changes);
|
|
698
|
+
|
|
622
699
|
return true;
|
|
623
700
|
}
|
|
624
701
|
|
|
@@ -629,6 +706,7 @@ export class DocumentModel {
|
|
|
629
706
|
/* A column past the end of its line lands on the end of the line, before the line break. */
|
|
630
707
|
offsetAt(position: Position): number {
|
|
631
708
|
const line = this.rope.lineBounds(position.line);
|
|
709
|
+
|
|
632
710
|
return offsetIn(this.rope, line.start + clampInteger(position.column, line.end - line.start));
|
|
633
711
|
}
|
|
634
712
|
|
|
@@ -646,9 +724,11 @@ export class DocumentModel {
|
|
|
646
724
|
if (match.revision !== this.revision || (options.expectedRevision !== undefined && options.expectedRevision !== this.revision)) {
|
|
647
725
|
return false;
|
|
648
726
|
}
|
|
727
|
+
|
|
649
728
|
if (this.slice(match.from, match.to) !== match.text) {
|
|
650
729
|
return false;
|
|
651
730
|
}
|
|
731
|
+
|
|
652
732
|
return this.applyEdits(
|
|
653
733
|
[{ from: match.from, to: match.to, text: replacementText(this.getText(), match, replacement, options.literal, options.preserveCase) }],
|
|
654
734
|
{
|
|
@@ -664,12 +744,14 @@ export class DocumentModel {
|
|
|
664
744
|
if (options.expectedRevision !== undefined && options.expectedRevision !== this.revision) {
|
|
665
745
|
return 0;
|
|
666
746
|
}
|
|
747
|
+
|
|
667
748
|
return this.replaceMatches(this.find(query, options), replacement, options);
|
|
668
749
|
}
|
|
669
750
|
|
|
670
751
|
/* Commits retained search results as one undo step; one stale result refuses the entire batch. */
|
|
671
752
|
replaceMatches(matches: readonly FindMatch[], replacement: string, options: ReplaceOptions = {}): number {
|
|
672
753
|
const revision = this.revision;
|
|
754
|
+
|
|
673
755
|
if (
|
|
674
756
|
matches.length === 0 ||
|
|
675
757
|
(options.expectedRevision !== undefined && options.expectedRevision !== revision) ||
|
|
@@ -677,42 +759,58 @@ export class DocumentModel {
|
|
|
677
759
|
) {
|
|
678
760
|
return 0;
|
|
679
761
|
}
|
|
762
|
+
|
|
680
763
|
const ranges = new Set<string>();
|
|
764
|
+
|
|
681
765
|
for (const match of matches) {
|
|
682
766
|
const key = `${match.from}:${match.to}`;
|
|
767
|
+
|
|
683
768
|
if (ranges.has(key)) {
|
|
684
769
|
throw new RangeError('Search matches must have unique ranges.');
|
|
685
770
|
}
|
|
771
|
+
|
|
686
772
|
ranges.add(key);
|
|
687
773
|
}
|
|
774
|
+
|
|
688
775
|
const text = this.getText();
|
|
776
|
+
|
|
689
777
|
const edits = matches.map((match) => ({
|
|
690
778
|
from: match.from,
|
|
691
779
|
to: match.to,
|
|
692
780
|
text: replacementText(text, match, replacement, options.literal, options.preserveCase)
|
|
693
781
|
}));
|
|
782
|
+
|
|
694
783
|
return this.applyEdits(edits, { source: 'command', ...options, expectedRevision: revision }) ? matches.length : 0;
|
|
695
784
|
}
|
|
696
785
|
|
|
697
786
|
getFoldingRanges(options: FoldingOptions = {}): FoldingRange[] {
|
|
698
787
|
const ranges = options.brackets === false && options.comments === false ? [] : this.structure();
|
|
788
|
+
|
|
699
789
|
const lineAt = (offset: number): number => this.rope.lineAt(offset);
|
|
790
|
+
|
|
700
791
|
const getLine = (line: number): DocumentLine => this.getLine(line);
|
|
792
|
+
|
|
701
793
|
const slice = (from: number, to: number): string => this.slice(from, to);
|
|
794
|
+
|
|
702
795
|
const derived = deriveFoldingRanges(ranges, lineAt, this.getLineCount(), getLine, options, slice);
|
|
796
|
+
|
|
703
797
|
if (options.language === undefined) {
|
|
704
798
|
return derived;
|
|
705
799
|
}
|
|
800
|
+
|
|
706
801
|
const minimum = options.minLines === undefined ? 1 : Math.max(1, Math.trunc(options.minLines) || 1);
|
|
802
|
+
|
|
707
803
|
// Listed first, so a fold that spans the same lines as a bracket pair is the import list and not the pair.
|
|
708
804
|
const merged = [
|
|
709
805
|
...lexicalFolds(this.getLineCount(), getLine, options.language, options),
|
|
710
806
|
...blockFolds(this.getLineCount(), getLine, options.language),
|
|
711
807
|
...derived
|
|
712
808
|
].filter((range) => range.endLine - range.startLine >= minimum);
|
|
809
|
+
|
|
713
810
|
merged.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
714
811
|
const unique = dedupeFoldingRanges(merged);
|
|
715
812
|
markFileHeader(unique, getLine, this.getLineCount(), options.language);
|
|
813
|
+
|
|
716
814
|
return options.hints === undefined ? unique : applyFoldHints(unique, options.hints, lineAt, getLine, slice, minimum);
|
|
717
815
|
}
|
|
718
816
|
|
|
@@ -726,17 +824,22 @@ export class DocumentModel {
|
|
|
726
824
|
if (options.expectedRevision !== undefined && options.expectedRevision !== this.revision) {
|
|
727
825
|
return false;
|
|
728
826
|
}
|
|
827
|
+
|
|
729
828
|
if (!text) {
|
|
730
829
|
return false;
|
|
731
830
|
}
|
|
831
|
+
|
|
732
832
|
const arrows = this.arrows;
|
|
733
833
|
this.arrows = new Set();
|
|
834
|
+
|
|
734
835
|
if (arrows.size > 0 && text !== '>' && !arrowContinues.test(text)) {
|
|
735
836
|
this.takeBackArrows();
|
|
736
837
|
}
|
|
838
|
+
|
|
737
839
|
const language = options.language ?? 'typescript';
|
|
738
840
|
const single = text.length === 1;
|
|
739
841
|
const pairing = options.autoClosingPairs !== false && single && !isPlainText(language);
|
|
842
|
+
|
|
740
843
|
const input: TypingInput = {
|
|
741
844
|
text,
|
|
742
845
|
language,
|
|
@@ -748,21 +851,26 @@ export class DocumentModel {
|
|
|
748
851
|
overArrows: text === '>' ? arrows : new Set(),
|
|
749
852
|
dedent: options.smartEnter !== false
|
|
750
853
|
};
|
|
854
|
+
|
|
751
855
|
const plans = this.selections.map((selection) => this.planTyping(selection, input));
|
|
752
856
|
const transaction = selectionTransaction(plans);
|
|
753
857
|
const textChanges = transaction.edits.some((edit) => changesText(this.rope, edit));
|
|
754
858
|
const appliedRevision = this.revision + Number(textChanges);
|
|
859
|
+
|
|
755
860
|
const changed = this.applySelectionEdits(
|
|
756
861
|
plans,
|
|
757
862
|
{ source: 'input', historyGroup: options.historyGroup ?? 'typing', expectedRevision: options.expectedRevision },
|
|
758
863
|
transaction
|
|
759
864
|
);
|
|
865
|
+
|
|
760
866
|
this.statementClosers.clear();
|
|
867
|
+
|
|
761
868
|
if (this.revision === appliedRevision) {
|
|
762
869
|
this.rememberStatementClosers(plans, transaction, language);
|
|
763
870
|
this.rememberPairs(plans, transaction);
|
|
764
871
|
this.completeArrows(plans, transaction);
|
|
765
872
|
}
|
|
873
|
+
|
|
766
874
|
return changed || plans.some((plan) => plan.consumed);
|
|
767
875
|
}
|
|
768
876
|
|
|
@@ -779,11 +887,15 @@ export class DocumentModel {
|
|
|
779
887
|
/* Adds the `>` after every `-` that became an arrow, as its own undo step, so the first undo leaves the `-` that was typed. */
|
|
780
888
|
private completeArrows(plans: readonly SelectionEdit[], transaction: SelectionTransaction): void {
|
|
781
889
|
const at = transaction.selections.flatMap((selection, index) => (plans[index]?.arrow ? [selection.head] : [])).sort((left, right) => left - right);
|
|
890
|
+
|
|
782
891
|
if (at.length === 0) {
|
|
783
892
|
return;
|
|
784
893
|
}
|
|
894
|
+
|
|
785
895
|
const moved = (offset: number): number => offset + at.filter((inserted) => inserted <= offset).length;
|
|
896
|
+
|
|
786
897
|
const selections = this.selections.map((selection) => ({ anchor: moved(selection.anchor), head: moved(selection.head) }));
|
|
898
|
+
|
|
787
899
|
if (
|
|
788
900
|
this.applyEdits(
|
|
789
901
|
at.map((offset) => ({ from: offset, to: offset, text: '>' })),
|
|
@@ -800,12 +912,14 @@ export class DocumentModel {
|
|
|
800
912
|
*/
|
|
801
913
|
paste(text: string, options: CommandOptions & { wholeLines?: boolean } = {}): boolean {
|
|
802
914
|
const tabSize = tabWidth(options);
|
|
915
|
+
|
|
803
916
|
const plan = planPaste(this.editSource(options.language ?? 'typescript'), this.selections, text.replace(/\r\n|\r|\n/g, this.newlineAt(0)), {
|
|
804
917
|
tabSize,
|
|
805
918
|
insertSpaces: options.insertSpaces !== false,
|
|
806
919
|
wholeLines: options.wholeLines === true,
|
|
807
920
|
reindent: options.indentOnPaste !== false
|
|
808
921
|
});
|
|
922
|
+
|
|
809
923
|
return this.applyEdits(plan.edits, { source: 'input', selections: plan.selections });
|
|
810
924
|
}
|
|
811
925
|
|
|
@@ -814,46 +928,62 @@ export class DocumentModel {
|
|
|
814
928
|
if (command === 'undo') {
|
|
815
929
|
return this.undo();
|
|
816
930
|
}
|
|
931
|
+
|
|
817
932
|
if (command === 'redo') {
|
|
818
933
|
return this.redo();
|
|
819
934
|
}
|
|
935
|
+
|
|
820
936
|
if (command === 'selectAll') {
|
|
821
937
|
return this.changeSelections([{ anchor: 0, head: this.rope.length }]);
|
|
822
938
|
}
|
|
939
|
+
|
|
823
940
|
if (command === 'expandSelection') {
|
|
824
941
|
return this.expandSelection();
|
|
825
942
|
}
|
|
943
|
+
|
|
826
944
|
if (command === 'shrinkSelection') {
|
|
827
945
|
return this.shrinkSelection();
|
|
828
946
|
}
|
|
947
|
+
|
|
829
948
|
if (command === 'smartHome' || command === 'smartEnd' || command === 'selectSmartHome' || command === 'selectSmartEnd') {
|
|
830
949
|
return this.moveToLineEdge(command, tabWidth(options), options.visualLine);
|
|
831
950
|
}
|
|
951
|
+
|
|
832
952
|
const wordMatch = wordCommand.exec(command);
|
|
953
|
+
|
|
833
954
|
if (wordMatch) {
|
|
834
955
|
const camel = wordMatch[2]!.toLowerCase() === 'camel' || options.camelCase === true;
|
|
956
|
+
|
|
835
957
|
return this.moveByWord(wordMatch[1] as 'select' | 'delete' | undefined, wordMatch[3] === 'Left' ? -1 : 1, camel, options.lineSpan);
|
|
836
958
|
}
|
|
959
|
+
|
|
837
960
|
if (command === 'addCaretAbove' || command === 'addCaretBelow') {
|
|
838
961
|
return this.addCaret(command === 'addCaretAbove' ? -1 : 1, options.lineSpan);
|
|
839
962
|
}
|
|
963
|
+
|
|
840
964
|
if (command === 'selectNextOccurrence') {
|
|
841
965
|
return this.selectNextOccurrence();
|
|
842
966
|
}
|
|
967
|
+
|
|
843
968
|
if (command === 'unselectOccurrence') {
|
|
844
969
|
return this.unselectOccurrence();
|
|
845
970
|
}
|
|
971
|
+
|
|
846
972
|
if (command === 'selectAllOccurrences') {
|
|
847
973
|
return this.selectAllOccurrences();
|
|
848
974
|
}
|
|
975
|
+
|
|
849
976
|
if (command === 'addCaretPerSelectedLine') {
|
|
850
977
|
return this.addCaretPerSelectedLine();
|
|
851
978
|
}
|
|
979
|
+
|
|
852
980
|
const tabSize = tabWidth(options);
|
|
853
981
|
const language = options.language ?? 'typescript';
|
|
854
982
|
const unit = options.insertSpaces === false ? '\t' : ' '.repeat(tabSize);
|
|
983
|
+
|
|
855
984
|
if (command === 'smartBackspace' || command === 'deleteForward') {
|
|
856
985
|
const pairing = options.autoClosingPairs !== false && !isPlainText(language);
|
|
986
|
+
|
|
857
987
|
return this.deleteCharacter(
|
|
858
988
|
command === 'smartBackspace',
|
|
859
989
|
{ tabSize, unit },
|
|
@@ -861,34 +991,45 @@ export class DocumentModel {
|
|
|
861
991
|
language
|
|
862
992
|
);
|
|
863
993
|
}
|
|
994
|
+
|
|
864
995
|
const enter = { language, unit, tabSize, smart: options.smartEnter !== false, reach: true };
|
|
996
|
+
|
|
865
997
|
if (command === 'insertNewline' || command === 'startNewLine' || command === 'splitLine') {
|
|
866
998
|
return this.insertNewline(enter, command === 'insertNewline' ? 'enter' : command === 'startNewLine' ? 'below' : 'split');
|
|
867
999
|
}
|
|
1000
|
+
|
|
868
1001
|
if (command === 'startNewLineBefore') {
|
|
869
1002
|
return this.startNewLineBefore(unit);
|
|
870
1003
|
}
|
|
1004
|
+
|
|
871
1005
|
if (command === 'joinLines' || command === 'toggleCase' || command === 'autoIndentLines') {
|
|
872
1006
|
return this.editLines(command, { language, unit });
|
|
873
1007
|
}
|
|
1008
|
+
|
|
874
1009
|
if (command === 'insertTab') {
|
|
875
1010
|
return this.insertTab(tabSize, options);
|
|
876
1011
|
}
|
|
1012
|
+
|
|
877
1013
|
if (command === 'toggleLineComment' || command === 'toggleBlockComment') {
|
|
878
1014
|
return this.toggleComment(command, tabSize, options, language);
|
|
879
1015
|
}
|
|
1016
|
+
|
|
880
1017
|
if (command === 'indent' || command === 'outdent') {
|
|
881
1018
|
return this.changeLineIndentation(command, tabSize, options);
|
|
882
1019
|
}
|
|
1020
|
+
|
|
883
1021
|
if (command === 'duplicateLine') {
|
|
884
1022
|
return this.duplicate(options.lineSpan);
|
|
885
1023
|
}
|
|
1024
|
+
|
|
886
1025
|
if (command === 'deleteLine') {
|
|
887
1026
|
return this.deleteLines(this.selectedLines(options.lineSpan));
|
|
888
1027
|
}
|
|
1028
|
+
|
|
889
1029
|
if (command === 'moveLineUp' || command === 'moveLineDown') {
|
|
890
1030
|
return this.moveLines(this.selectedLines(options.lineSpan), command === 'moveLineUp' ? -1 : 1, options);
|
|
891
1031
|
}
|
|
1032
|
+
|
|
892
1033
|
return false;
|
|
893
1034
|
}
|
|
894
1035
|
|
|
@@ -896,25 +1037,33 @@ export class DocumentModel {
|
|
|
896
1037
|
private validateEdits(edits: readonly TextEdit[]): TextEdit[] {
|
|
897
1038
|
const sorted = [...edits].sort((left, right) => left.from - right.from || left.to - right.to);
|
|
898
1039
|
const unique: TextEdit[] = [];
|
|
1040
|
+
|
|
899
1041
|
for (const edit of sorted) {
|
|
900
1042
|
if (!Number.isInteger(edit.from) || !Number.isInteger(edit.to) || edit.from < 0 || edit.to < edit.from || edit.to > this.rope.length) {
|
|
901
1043
|
throw new RangeError('Edit offsets must be ordered integers inside the document.');
|
|
902
1044
|
}
|
|
1045
|
+
|
|
903
1046
|
if (typeof edit.text !== 'string') {
|
|
904
1047
|
throw new TypeError('Edit text must be a string.');
|
|
905
1048
|
}
|
|
1049
|
+
|
|
906
1050
|
if (splitsSurrogate(this.rope, edit.from) || splitsSurrogate(this.rope, edit.to)) {
|
|
907
1051
|
throw new RangeError('Edit boundaries cannot split a UTF-16 surrogate pair.');
|
|
908
1052
|
}
|
|
1053
|
+
|
|
909
1054
|
const previous = unique.at(-1);
|
|
1055
|
+
|
|
910
1056
|
if (previous && edit.from === previous.from && edit.to === previous.to && edit.text === previous.text) {
|
|
911
1057
|
continue;
|
|
912
1058
|
}
|
|
1059
|
+
|
|
913
1060
|
if (previous && (edit.from < previous.to || (edit.from === previous.from && edit.to === previous.to))) {
|
|
914
1061
|
throw new RangeError('Simultaneous edits must not overlap.');
|
|
915
1062
|
}
|
|
1063
|
+
|
|
916
1064
|
unique.push({ ...edit });
|
|
917
1065
|
}
|
|
1066
|
+
|
|
918
1067
|
return unique;
|
|
919
1068
|
}
|
|
920
1069
|
|
|
@@ -924,10 +1073,13 @@ export class DocumentModel {
|
|
|
924
1073
|
const after = this.rope.charAt(to);
|
|
925
1074
|
const quote = isQuote(text);
|
|
926
1075
|
const pairing = quote ? input.quotes : input.brackets;
|
|
1076
|
+
|
|
927
1077
|
if (text === '>' && from === to && input.overArrows.has(from)) {
|
|
928
1078
|
return { from, to, text: '', anchor: 0, head: 0, skip: true, consumed: true };
|
|
929
1079
|
}
|
|
1080
|
+
|
|
930
1081
|
const pendingClosers = this.statementClosers.get(from);
|
|
1082
|
+
|
|
931
1083
|
if (input.brackets && smartSemicolon && from === to && pendingClosers?.language === language && pendingClosers.text.startsWith(text)) {
|
|
932
1084
|
return { from, to, text: '', anchor: 0, head: 0, skip: true, consumed: true, completedClosers: pendingClosers.text.slice(1) };
|
|
933
1085
|
}
|
|
@@ -935,21 +1087,26 @@ export class DocumentModel {
|
|
|
935
1087
|
const arrow = text === '-' && input.smartArrow && from === to;
|
|
936
1088
|
const needsContext = text.length === 1 && (/[()[\]{}'"`<]/.test(text) || (text === ';' && smartSemicolon) || arrow);
|
|
937
1089
|
const context = needsContext ? this.typingContext(from, language) : undefined;
|
|
1090
|
+
|
|
938
1091
|
if (arrow && context?.mode === 'code' && context.embedded === undefined && context.word !== 'control-close' && this.endsOperand(from)) {
|
|
939
1092
|
return { from, to, text, anchor: 1, head: 1, arrow: true };
|
|
940
1093
|
}
|
|
1094
|
+
|
|
941
1095
|
const insideQuote =
|
|
942
1096
|
context &&
|
|
943
1097
|
(((context.mode === 'quote' || context.mode === 'template') && context.quote === text && !context.escaped) ||
|
|
944
1098
|
(context.mode === 'html' && context.markup === 'value' && context.markupQuote === text));
|
|
1099
|
+
|
|
945
1100
|
if (pairing && from === to && after === text && (quote ? insideQuote : context?.mode === 'code' && context.bracket?.close === text)) {
|
|
946
1101
|
return { from, to, text: '', anchor: 1, head: 1, skip: true };
|
|
947
1102
|
}
|
|
948
1103
|
|
|
949
1104
|
if (text === ';' && from === to && smartSemicolon && context?.mode === 'code') {
|
|
950
1105
|
const end = this.semicolonTarget(from, context);
|
|
1106
|
+
|
|
951
1107
|
if (end !== undefined) {
|
|
952
1108
|
const existing = this.rope.charAt(end) === ';';
|
|
1109
|
+
|
|
953
1110
|
return {
|
|
954
1111
|
from: end,
|
|
955
1112
|
to: end,
|
|
@@ -964,6 +1121,7 @@ export class DocumentModel {
|
|
|
964
1121
|
|
|
965
1122
|
if (from !== to && input.surround && (pairs[text] !== undefined || text === '<')) {
|
|
966
1123
|
const wrapped = this.surroundSelection(selection, input, context);
|
|
1124
|
+
|
|
967
1125
|
if (wrapped) {
|
|
968
1126
|
return wrapped;
|
|
969
1127
|
}
|
|
@@ -975,10 +1133,12 @@ export class DocumentModel {
|
|
|
975
1133
|
|
|
976
1134
|
if (input.dedent && from === to && /[)\]}]/.test(text) && context?.mode === 'code' && context.bracket?.close === text) {
|
|
977
1135
|
const dedented = this.dedentCloser(from, text, context.bracket.at);
|
|
1136
|
+
|
|
978
1137
|
if (dedented) {
|
|
979
1138
|
return dedented;
|
|
980
1139
|
}
|
|
981
1140
|
}
|
|
1141
|
+
|
|
982
1142
|
return { from, to, text, anchor: text.length, head: text.length };
|
|
983
1143
|
}
|
|
984
1144
|
|
|
@@ -987,21 +1147,27 @@ export class DocumentModel {
|
|
|
987
1147
|
const { text, language } = input;
|
|
988
1148
|
const { from, to } = rangeOf(selection);
|
|
989
1149
|
const selected = this.slice(from, to);
|
|
1150
|
+
|
|
990
1151
|
if (input.quotes && isQuote(text) && selected.length === 1) {
|
|
991
1152
|
const swapped = swapQuotes(this.editSource(language), from, text);
|
|
1153
|
+
|
|
992
1154
|
if (swapped) {
|
|
993
1155
|
return { from: swapped.from, to: swapped.to, text: swapped.text, anchor: swapped.caret, head: swapped.caret };
|
|
994
1156
|
}
|
|
995
1157
|
}
|
|
1158
|
+
|
|
996
1159
|
const inCommentOrString = context !== undefined && context.mode !== 'code';
|
|
1160
|
+
|
|
997
1161
|
if (replacesComparison(text, selected, inCommentOrString)) {
|
|
998
1162
|
return null;
|
|
999
1163
|
}
|
|
1164
|
+
|
|
1000
1165
|
const wrapped = surround(selected, text);
|
|
1001
|
-
const reversed = selection.anchor > selection.head;
|
|
1002
|
-
const start = 1;
|
|
1003
1166
|
const end = 1 + wrapped.inner.length;
|
|
1004
|
-
|
|
1167
|
+
|
|
1168
|
+
return selection.anchor > selection.head
|
|
1169
|
+
? { from, to, text: wrapped.text, anchor: end, head: 1 }
|
|
1170
|
+
: { from, to, text: wrapped.text, anchor: 1, head: end };
|
|
1005
1171
|
}
|
|
1006
1172
|
|
|
1007
1173
|
/* A closer alone on its line goes back to the indentation of the line its opener is on. */
|
|
@@ -1009,13 +1175,17 @@ export class DocumentModel {
|
|
|
1009
1175
|
const index = this.rope.lineAt(offset);
|
|
1010
1176
|
const line = this.rope.lineBounds(index);
|
|
1011
1177
|
const before = this.slice(line.start, offset);
|
|
1178
|
+
|
|
1012
1179
|
if (!/^[\t ]*$/.test(before)) {
|
|
1013
1180
|
return null;
|
|
1014
1181
|
}
|
|
1015
|
-
|
|
1182
|
+
|
|
1183
|
+
const indent = whitespaceOf(this.lineOf(opener).text);
|
|
1184
|
+
|
|
1016
1185
|
if (indent === before) {
|
|
1017
1186
|
return null;
|
|
1018
1187
|
}
|
|
1188
|
+
|
|
1019
1189
|
return { from: line.start, to: offset, text: indent + closer, anchor: indent.length + 1, head: indent.length + 1 };
|
|
1020
1190
|
}
|
|
1021
1191
|
|
|
@@ -1023,16 +1193,20 @@ export class DocumentModel {
|
|
|
1023
1193
|
if (context?.mode === 'html') {
|
|
1024
1194
|
return context.markup === 'equals' && (opener === '"' || opener === "'") && (!after || /[\s>/]/u.test(after));
|
|
1025
1195
|
}
|
|
1196
|
+
|
|
1026
1197
|
const interpolation = this.isInterpolation(offset, opener, context);
|
|
1027
1198
|
const quote = isQuote(opener);
|
|
1028
1199
|
const scriptEnd = context?.embedded !== undefined && /^<\/script(?=[\s/>]|$)/i.test(this.slice(offset, offset + 9));
|
|
1029
1200
|
const followedByTerminator = quote ? !identifier.test(after) && after !== opener : !after || scriptEnd || /[\s)\]}>,;:]/u.test(after);
|
|
1201
|
+
|
|
1030
1202
|
if (context?.mode !== 'code' && !interpolation) {
|
|
1031
1203
|
return false;
|
|
1032
1204
|
}
|
|
1205
|
+
|
|
1033
1206
|
if (!followedByTerminator && !(interpolation && after === '`')) {
|
|
1034
1207
|
return false;
|
|
1035
1208
|
}
|
|
1209
|
+
|
|
1036
1210
|
if (quote) {
|
|
1037
1211
|
// A quote after a word is an apostrophe or the end of a string, not the start of one.
|
|
1038
1212
|
return (
|
|
@@ -1041,6 +1215,7 @@ export class DocumentModel {
|
|
|
1041
1215
|
!this.completesTripleQuote(offset, opener, input.language)
|
|
1042
1216
|
);
|
|
1043
1217
|
}
|
|
1218
|
+
|
|
1044
1219
|
return !this.hasMatch(offset, opener, input.language, context);
|
|
1045
1220
|
}
|
|
1046
1221
|
|
|
@@ -1053,18 +1228,23 @@ export class DocumentModel {
|
|
|
1053
1228
|
private hasMatch(offset: number, opener: string, language: string, context?: TypingContext): boolean {
|
|
1054
1229
|
if (context?.embedded === 'javascript' && context.embedStart !== undefined) {
|
|
1055
1230
|
const source = this.scriptSource(context.embedStart, matchScanLimit).text;
|
|
1231
|
+
|
|
1056
1232
|
if (source === undefined) {
|
|
1057
1233
|
return false;
|
|
1058
1234
|
}
|
|
1235
|
+
|
|
1059
1236
|
const at = offset - context.embedStart;
|
|
1237
|
+
|
|
1060
1238
|
return scanBrackets(source.slice(0, at) + opener + source.slice(at), 'javascript').pairs.has(at);
|
|
1061
1239
|
}
|
|
1240
|
+
|
|
1062
1241
|
if (this.rope.length > matchScanLimit) {
|
|
1063
1242
|
return false;
|
|
1064
1243
|
}
|
|
1244
|
+
|
|
1065
1245
|
const text = this.getText();
|
|
1066
|
-
|
|
1067
|
-
return
|
|
1246
|
+
|
|
1247
|
+
return scanBrackets(text.slice(0, offset) + opener + text.slice(offset), language).pairs.has(offset);
|
|
1068
1248
|
}
|
|
1069
1249
|
|
|
1070
1250
|
/*
|
|
@@ -1072,16 +1252,20 @@ export class DocumentModel {
|
|
|
1072
1252
|
* property or method after `->` or `?->`, or a `)` or `]`.
|
|
1073
1253
|
*/
|
|
1074
1254
|
private endsOperand(offset: number): boolean {
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1255
|
+
const before = this.slice(this.boundsOf(offset).start, offset);
|
|
1256
|
+
|
|
1077
1257
|
if (/[)\]]$/.test(before)) {
|
|
1078
1258
|
return true;
|
|
1079
1259
|
}
|
|
1260
|
+
|
|
1080
1261
|
const name = /[\p{L}_][\p{L}\p{N}_]*$/u.exec(before);
|
|
1262
|
+
|
|
1081
1263
|
if (name === null) {
|
|
1082
1264
|
return false;
|
|
1083
1265
|
}
|
|
1266
|
+
|
|
1084
1267
|
const ahead = before.slice(0, name.index);
|
|
1268
|
+
|
|
1085
1269
|
return ahead.endsWith('$') || ahead.endsWith('->');
|
|
1086
1270
|
}
|
|
1087
1271
|
|
|
@@ -1089,10 +1273,13 @@ export class DocumentModel {
|
|
|
1089
1273
|
private rememberStatementClosers(plans: readonly SelectionEdit[], transaction: SelectionTransaction, language: string): void {
|
|
1090
1274
|
for (const [index, selection] of transaction.selections.entries()) {
|
|
1091
1275
|
const closers = plans[index]?.completedClosers;
|
|
1276
|
+
|
|
1092
1277
|
if (!closers) {
|
|
1093
1278
|
continue;
|
|
1094
1279
|
}
|
|
1280
|
+
|
|
1095
1281
|
const caretIsThere = this.selections.some((current) => current.anchor === selection.anchor && current.head === selection.head);
|
|
1282
|
+
|
|
1096
1283
|
if (caretIsThere && closers.length > (this.statementClosers.get(selection.head)?.text.length ?? 0)) {
|
|
1097
1284
|
this.statementClosers.set(selection.head, { text: closers, language });
|
|
1098
1285
|
}
|
|
@@ -1115,36 +1302,42 @@ export class DocumentModel {
|
|
|
1115
1302
|
if (this.selections.some((selection) => selection.anchor !== selection.head)) {
|
|
1116
1303
|
return this.changeLineIndentation('indent', tabSize, options);
|
|
1117
1304
|
}
|
|
1305
|
+
|
|
1118
1306
|
const tabOut = options.tabOutOfClosers !== false;
|
|
1119
1307
|
// A caret further along a line stands where the spaces of the carets before it moved it to.
|
|
1120
1308
|
const moved = new Map<number, number>();
|
|
1121
1309
|
const widths = new Map<Selection, number>();
|
|
1310
|
+
|
|
1122
1311
|
for (const selection of [...this.selections].sort((left, right) => left.head - right.head)) {
|
|
1123
1312
|
if (tabOut && this.closers.isCloserAt(selection.head)) {
|
|
1124
1313
|
continue;
|
|
1125
1314
|
}
|
|
1315
|
+
|
|
1126
1316
|
const line = this.rope.lineAt(selection.head);
|
|
1127
1317
|
const column = this.columnOf(selection.head, tabSize) + (moved.get(line) ?? 0);
|
|
1128
1318
|
const width = tabSize - (column % tabSize);
|
|
1129
1319
|
widths.set(selection, width);
|
|
1130
1320
|
moved.set(line, (moved.get(line) ?? 0) + width);
|
|
1131
1321
|
}
|
|
1322
|
+
|
|
1132
1323
|
return this.applySelectionEdits(
|
|
1133
1324
|
this.selections.map((selection) => {
|
|
1134
1325
|
const offset = selection.head;
|
|
1326
|
+
|
|
1135
1327
|
if (tabOut && this.closers.isCloserAt(offset)) {
|
|
1136
1328
|
return { from: offset, to: offset, text: '', anchor: 1, head: 1, skip: true };
|
|
1137
1329
|
}
|
|
1330
|
+
|
|
1138
1331
|
const text = options.insertSpaces === false ? '\t' : ' '.repeat(widths.get(selection)!);
|
|
1332
|
+
|
|
1139
1333
|
return { from: offset, to: offset, text, anchor: text.length, head: text.length };
|
|
1140
1334
|
}),
|
|
1141
1335
|
{ source: 'command' }
|
|
1142
1336
|
);
|
|
1143
1337
|
}
|
|
1144
1338
|
|
|
1145
|
-
/* The visual column of an offset, with tabs going to the next tab stop. */
|
|
1146
1339
|
private columnOf(offset: number, tabSize: number): number {
|
|
1147
|
-
return indentationColumn(this.slice(this.
|
|
1340
|
+
return indentationColumn(this.slice(this.boundsOf(offset).start, offset), tabSize);
|
|
1148
1341
|
}
|
|
1149
1342
|
|
|
1150
1343
|
private moveToLineEdge(
|
|
@@ -1154,9 +1347,11 @@ export class DocumentModel {
|
|
|
1154
1347
|
): boolean {
|
|
1155
1348
|
const home = command.endsWith('Home');
|
|
1156
1349
|
const select = command.startsWith('select');
|
|
1350
|
+
|
|
1157
1351
|
return this.changeSelections(
|
|
1158
1352
|
this.selections.map((selection) => {
|
|
1159
1353
|
const head = home ? this.homeOffset(selection.head, tabSize, visualLine) : this.endOffset(selection.head, visualLine);
|
|
1354
|
+
|
|
1160
1355
|
return { anchor: select ? selection.anchor : head, head };
|
|
1161
1356
|
})
|
|
1162
1357
|
);
|
|
@@ -1165,6 +1360,7 @@ export class DocumentModel {
|
|
|
1165
1360
|
/* The first character on a line that is not a space or a tab, or -1 when there is none. */
|
|
1166
1361
|
private firstNonSpace(line: DocumentLine): number {
|
|
1167
1362
|
const found = line.text.search(/[^ \t]/);
|
|
1363
|
+
|
|
1168
1364
|
return found === -1 ? -1 : line.start + found;
|
|
1169
1365
|
}
|
|
1170
1366
|
|
|
@@ -1175,15 +1371,18 @@ export class DocumentModel {
|
|
|
1175
1371
|
* text to the first character, and from the first character or inside the indentation to the start.
|
|
1176
1372
|
*/
|
|
1177
1373
|
private homeOffset(offset: number, tabSize: number, visualLine: VisualLineOf | undefined): number {
|
|
1178
|
-
const line = this.
|
|
1374
|
+
const line = this.lineOf(offset);
|
|
1179
1375
|
const row = visualLine?.(offset) ?? line;
|
|
1376
|
+
const indent = this.firstNonSpace(line);
|
|
1377
|
+
|
|
1180
1378
|
if (row.start > line.start) {
|
|
1181
|
-
return offset > row.start ? row.start : Math.max(
|
|
1379
|
+
return offset > row.start ? row.start : Math.max(indent, line.start);
|
|
1182
1380
|
}
|
|
1183
|
-
|
|
1381
|
+
|
|
1184
1382
|
if (offset === line.start) {
|
|
1185
1383
|
return this.offsetAtColumn(line, this.smartIndentColumn(line.start, tabSize), tabSize);
|
|
1186
1384
|
}
|
|
1385
|
+
|
|
1187
1386
|
return indent !== -1 && indent < offset ? indent : line.start;
|
|
1188
1387
|
}
|
|
1189
1388
|
|
|
@@ -1192,10 +1391,12 @@ export class DocumentModel {
|
|
|
1192
1391
|
for (let index = this.rope.lineAt(offset); index >= 0; index--) {
|
|
1193
1392
|
const line = this.getLine(index);
|
|
1194
1393
|
const indent = this.firstNonSpace(line);
|
|
1394
|
+
|
|
1195
1395
|
if (indent !== -1) {
|
|
1196
1396
|
return indentationColumn(line.text.slice(0, indent - line.start), tabSize);
|
|
1197
1397
|
}
|
|
1198
1398
|
}
|
|
1399
|
+
|
|
1199
1400
|
return 0;
|
|
1200
1401
|
}
|
|
1201
1402
|
|
|
@@ -1204,13 +1405,17 @@ export class DocumentModel {
|
|
|
1204
1405
|
if (column <= 0) {
|
|
1205
1406
|
return line.start;
|
|
1206
1407
|
}
|
|
1408
|
+
|
|
1207
1409
|
let width = 0;
|
|
1410
|
+
|
|
1208
1411
|
for (let index = 0; index < line.text.length && width < column; index++) {
|
|
1209
1412
|
width += line.text[index] === '\t' ? tabSize - (width % tabSize) : 1;
|
|
1413
|
+
|
|
1210
1414
|
if (width >= column) {
|
|
1211
1415
|
return line.start + index + 1;
|
|
1212
1416
|
}
|
|
1213
1417
|
}
|
|
1418
|
+
|
|
1214
1419
|
return line.end;
|
|
1215
1420
|
}
|
|
1216
1421
|
|
|
@@ -1220,41 +1425,49 @@ export class DocumentModel {
|
|
|
1220
1425
|
* the way, and a stop at the end of the text sends the next one past them.
|
|
1221
1426
|
*/
|
|
1222
1427
|
private endOffset(offset: number, visualLine: VisualLineOf | undefined): number {
|
|
1223
|
-
const line = this.
|
|
1428
|
+
const line = this.lineOf(offset);
|
|
1224
1429
|
const row = visualLine?.(offset) ?? line;
|
|
1225
|
-
|
|
1226
|
-
if (offset === row.end && offset < this.rope.length) {
|
|
1227
|
-
end = line.end;
|
|
1228
|
-
}
|
|
1430
|
+
const end = offset === row.end && offset < this.rope.length ? line.end : row.end;
|
|
1229
1431
|
let target = end;
|
|
1432
|
+
|
|
1230
1433
|
for (let at = end - 1; at >= line.start; at--) {
|
|
1231
1434
|
if (at > line.start && visualLine !== undefined && visualLine(at).start === at) {
|
|
1232
1435
|
target = end;
|
|
1436
|
+
|
|
1233
1437
|
break;
|
|
1234
1438
|
}
|
|
1439
|
+
|
|
1235
1440
|
const character = this.rope.charAt(at);
|
|
1441
|
+
|
|
1236
1442
|
if (character !== ' ' && character !== '\t') {
|
|
1237
1443
|
break;
|
|
1238
1444
|
}
|
|
1445
|
+
|
|
1239
1446
|
target = at;
|
|
1240
1447
|
}
|
|
1448
|
+
|
|
1241
1449
|
return target === end || target === offset ? end : target;
|
|
1242
1450
|
}
|
|
1243
1451
|
|
|
1244
1452
|
private moveByWord(mode: 'select' | 'delete' | undefined, direction: -1 | 1, camel: boolean, span?: LineSpanOf): boolean {
|
|
1245
1453
|
if (mode === 'delete') {
|
|
1246
1454
|
const stops = direction === 1 ? DELETE_FORWARD : DELETE_BACKWARD;
|
|
1455
|
+
|
|
1247
1456
|
return this.deleteRanges(
|
|
1248
1457
|
this.selections.map((selection) => {
|
|
1249
1458
|
const head = selection.head === selection.anchor ? this.caretStop(selection.head, direction, stops, camel) : selection.anchor;
|
|
1459
|
+
|
|
1250
1460
|
return { from: Math.min(selection.head, head), to: Math.max(selection.head, head), text: '' };
|
|
1251
1461
|
})
|
|
1252
1462
|
);
|
|
1253
1463
|
}
|
|
1464
|
+
|
|
1254
1465
|
const stops = direction === 1 ? MOVE_FORWARD : MOVE_BACKWARD;
|
|
1466
|
+
|
|
1255
1467
|
return this.changeSelections(
|
|
1256
1468
|
this.selections.map((selection) => {
|
|
1257
1469
|
const head = this.foldedStop(selection.head, direction, this.caretStop(selection.head, direction, stops, camel), span);
|
|
1470
|
+
|
|
1258
1471
|
return { anchor: mode === 'select' ? selection.anchor : head, head };
|
|
1259
1472
|
})
|
|
1260
1473
|
);
|
|
@@ -1267,20 +1480,26 @@ export class DocumentModel {
|
|
|
1267
1480
|
private caretStop(offset: number, direction: -1 | 1, stops: CaretStops, camel: boolean): number {
|
|
1268
1481
|
const line = this.rope.lineAt(offset);
|
|
1269
1482
|
const edge = direction === 1 ? this.nextLineStop(line, offset, stops.line) : this.previousLineStop(line, offset, stops.line);
|
|
1483
|
+
|
|
1270
1484
|
if (offset === edge) {
|
|
1271
1485
|
return offset;
|
|
1272
1486
|
}
|
|
1487
|
+
|
|
1273
1488
|
const isStop = (position: number): boolean =>
|
|
1274
1489
|
((stops.word !== 'start' && isWordBoundary(this.rope, position, camel, false)) ||
|
|
1275
1490
|
(stops.word !== 'end' && isWordBoundary(this.rope, position, camel, true))) &&
|
|
1276
1491
|
!splitsCluster(this.rope, position);
|
|
1492
|
+
|
|
1277
1493
|
let position = offset + direction;
|
|
1494
|
+
|
|
1278
1495
|
while (direction === 1 ? position < edge : position > edge) {
|
|
1279
1496
|
if (isStop(position)) {
|
|
1280
1497
|
break;
|
|
1281
1498
|
}
|
|
1499
|
+
|
|
1282
1500
|
position += direction;
|
|
1283
1501
|
}
|
|
1502
|
+
|
|
1284
1503
|
return position;
|
|
1285
1504
|
}
|
|
1286
1505
|
|
|
@@ -1293,21 +1512,29 @@ export class DocumentModel {
|
|
|
1293
1512
|
if (span === undefined) {
|
|
1294
1513
|
return stop;
|
|
1295
1514
|
}
|
|
1515
|
+
|
|
1296
1516
|
const fold = (position: number): { from: number; to: number; next: number } | null => {
|
|
1297
1517
|
const { first, last } = span(this.rope.lineAt(position));
|
|
1518
|
+
|
|
1298
1519
|
if (last === first) {
|
|
1299
1520
|
return null;
|
|
1300
1521
|
}
|
|
1522
|
+
|
|
1301
1523
|
return { from: this.rope.lineBounds(first).end, to: this.rope.lineBounds(last).end, next: this.rope.lineBounds(last).next };
|
|
1302
1524
|
};
|
|
1525
|
+
|
|
1303
1526
|
const current = direction === 1 ? fold(offset) : fold(offset - 1);
|
|
1527
|
+
|
|
1304
1528
|
if (direction === 1 && current !== null && current.from <= offset && offset < current.to) {
|
|
1305
1529
|
return current.next > current.to ? current.next : offset;
|
|
1306
1530
|
}
|
|
1531
|
+
|
|
1307
1532
|
if (direction === -1 && current !== null && current.from <= offset - 1 && offset - 1 < current.to) {
|
|
1308
1533
|
return current.from;
|
|
1309
1534
|
}
|
|
1535
|
+
|
|
1310
1536
|
const landing = fold(stop);
|
|
1537
|
+
|
|
1311
1538
|
return landing !== null && landing.from < stop && stop <= landing.to ? landing.from : stop;
|
|
1312
1539
|
}
|
|
1313
1540
|
|
|
@@ -1315,10 +1542,13 @@ export class DocumentModel {
|
|
|
1315
1542
|
if (line + 1 >= this.rope.lineCount) {
|
|
1316
1543
|
return this.rope.length;
|
|
1317
1544
|
}
|
|
1545
|
+
|
|
1318
1546
|
const atEnd = offset === this.rope.lineBounds(line).end;
|
|
1547
|
+
|
|
1319
1548
|
if (!atEnd && stop !== 'start') {
|
|
1320
1549
|
return this.rope.lineBounds(line).end;
|
|
1321
1550
|
}
|
|
1551
|
+
|
|
1322
1552
|
return stop === 'end' ? this.rope.lineBounds(line + 1).end : this.rope.lineBounds(line + 1).start;
|
|
1323
1553
|
}
|
|
1324
1554
|
|
|
@@ -1326,10 +1556,13 @@ export class DocumentModel {
|
|
|
1326
1556
|
if (line === 0) {
|
|
1327
1557
|
return 0;
|
|
1328
1558
|
}
|
|
1559
|
+
|
|
1329
1560
|
const atStart = offset === this.rope.lineBounds(line).start;
|
|
1561
|
+
|
|
1330
1562
|
if (!atStart && stop !== 'end') {
|
|
1331
1563
|
return this.rope.lineBounds(line).start;
|
|
1332
1564
|
}
|
|
1565
|
+
|
|
1333
1566
|
return stop === 'start' ? this.rope.lineBounds(line - 1).start : this.rope.lineBounds(line - 1).end;
|
|
1334
1567
|
}
|
|
1335
1568
|
|
|
@@ -1342,33 +1575,42 @@ export class DocumentModel {
|
|
|
1342
1575
|
const session = this.cloneSession;
|
|
1343
1576
|
const frames = session !== undefined && session.signature === this.occurrenceSignature() ? session.frames : [];
|
|
1344
1577
|
const top = frames.at(-1);
|
|
1578
|
+
|
|
1345
1579
|
if (top !== undefined && top.direction !== direction) {
|
|
1346
1580
|
const taken = new Set(top.carets.map((clone) => `${clone.selection.anchor}:${clone.selection.head}`));
|
|
1347
1581
|
const kept = this.selections.filter((selection) => !taken.has(`${selection.anchor}:${selection.head}`));
|
|
1348
1582
|
frames.pop();
|
|
1349
1583
|
const changed = this.changeSelections(kept.length > 0 ? kept : this.selections.slice(0, 1));
|
|
1350
1584
|
this.cloneSession = { signature: this.occurrenceSignature(), frames };
|
|
1585
|
+
|
|
1351
1586
|
return changed;
|
|
1352
1587
|
}
|
|
1588
|
+
|
|
1353
1589
|
const sources = top === undefined ? this.selections.map((selection) => this.cloneSource(selection)) : top.carets;
|
|
1354
1590
|
const clones: CaretClone[] = [];
|
|
1591
|
+
|
|
1355
1592
|
for (const source of sources) {
|
|
1356
1593
|
const clone = this.cloneOnto(source, direction, [...this.selections, ...clones.map((added) => added.selection)], span);
|
|
1594
|
+
|
|
1357
1595
|
if (clone !== null) {
|
|
1358
1596
|
clones.push(clone);
|
|
1359
1597
|
}
|
|
1360
1598
|
}
|
|
1599
|
+
|
|
1361
1600
|
if (clones.length === 0) {
|
|
1362
1601
|
return false;
|
|
1363
1602
|
}
|
|
1603
|
+
|
|
1364
1604
|
const changed = this.changeSelections([...this.selections, ...clones.map((clone) => clone.selection)]);
|
|
1365
1605
|
frames.push({ direction, carets: clones });
|
|
1366
1606
|
this.cloneSession = { signature: this.occurrenceSignature(), frames };
|
|
1607
|
+
|
|
1367
1608
|
return changed;
|
|
1368
1609
|
}
|
|
1369
1610
|
|
|
1370
1611
|
private cloneSource(selection: Selection): CaretClone {
|
|
1371
1612
|
const { from, to } = rangeOf(selection);
|
|
1613
|
+
|
|
1372
1614
|
return {
|
|
1373
1615
|
selection,
|
|
1374
1616
|
caretColumn: this.positionAt(selection.head).column,
|
|
@@ -1384,70 +1626,90 @@ export class DocumentModel {
|
|
|
1384
1626
|
const startLine = this.rope.lineAt(from);
|
|
1385
1627
|
const endLine = this.rope.lineAt(to);
|
|
1386
1628
|
const caretLine = this.rope.lineAt(selection.head);
|
|
1629
|
+
|
|
1387
1630
|
for (let shift = direction; ; shift += direction) {
|
|
1388
1631
|
const line = caretLine + shift;
|
|
1632
|
+
|
|
1389
1633
|
if (line < 0 || line >= this.getLineCount()) {
|
|
1390
1634
|
return null;
|
|
1391
1635
|
}
|
|
1636
|
+
|
|
1392
1637
|
if (span !== undefined && span(line).first !== line) {
|
|
1393
1638
|
continue;
|
|
1394
1639
|
}
|
|
1640
|
+
|
|
1395
1641
|
const start = this.offsetAt({ line: startLine + shift, column: source.startColumn });
|
|
1396
1642
|
const end = this.offsetAt({ line: endLine + shift, column: source.endColumn });
|
|
1643
|
+
|
|
1397
1644
|
if (from !== to && start === end) {
|
|
1398
1645
|
continue;
|
|
1399
1646
|
}
|
|
1647
|
+
|
|
1400
1648
|
const caret = this.offsetAt({ line, column: source.caretColumn });
|
|
1401
1649
|
const cloned = from === to ? { anchor: caret, head: caret } : selection.head === to ? { anchor: start, head: end } : { anchor: end, head: start };
|
|
1402
1650
|
const range = rangeOf(cloned);
|
|
1651
|
+
|
|
1403
1652
|
if (
|
|
1404
1653
|
existing.some((other) => {
|
|
1405
1654
|
const taken = rangeOf(other);
|
|
1655
|
+
|
|
1406
1656
|
return range.from === range.to && taken.from === taken.to ? range.from === taken.from : range.from < taken.to && range.to > taken.from;
|
|
1407
1657
|
})
|
|
1408
1658
|
) {
|
|
1409
1659
|
return null;
|
|
1410
1660
|
}
|
|
1661
|
+
|
|
1411
1662
|
return { ...source, selection: cloned };
|
|
1412
1663
|
}
|
|
1413
1664
|
}
|
|
1414
1665
|
|
|
1415
1666
|
private changeLineIndentation(command: 'indent' | 'outdent', tabSize: number, options: CommandOptions): boolean {
|
|
1416
1667
|
const indentation = options.insertSpaces === false ? '\t' : ' '.repeat(tabSize);
|
|
1668
|
+
|
|
1417
1669
|
const edits = this.selectedLines().map((index) => {
|
|
1418
1670
|
const line = this.getLine(index);
|
|
1671
|
+
|
|
1419
1672
|
if (command === 'indent') {
|
|
1420
1673
|
return { from: line.start, to: line.start, text: indentation };
|
|
1421
1674
|
}
|
|
1422
|
-
|
|
1675
|
+
|
|
1676
|
+
const length = line.text.startsWith('\t') ? 1 : Math.min(/^ */.exec(line.text)![0].length, tabSize);
|
|
1677
|
+
|
|
1423
1678
|
return { from: line.start, to: line.start + length, text: '' };
|
|
1424
1679
|
});
|
|
1680
|
+
|
|
1425
1681
|
return this.applyEdits(edits, { source: 'command' });
|
|
1426
1682
|
}
|
|
1427
1683
|
|
|
1428
1684
|
private toggleComment(command: 'toggleLineComment' | 'toggleBlockComment', tabSize: number, options: CommandOptions, language: string): boolean {
|
|
1429
1685
|
const commentOptions = { language, tabSize, insertSpaces: options.insertSpaces !== false, lineToken: options.commentToken, lineSpan: options.lineSpan };
|
|
1430
1686
|
const source = this.editSource(language);
|
|
1687
|
+
|
|
1431
1688
|
const plan: EditPlan | null =
|
|
1432
1689
|
command === 'toggleLineComment'
|
|
1433
1690
|
? planLineComments(source, this.selections, commentOptions)
|
|
1434
1691
|
: planBlockComment(source, this.selections, commentOptions);
|
|
1692
|
+
|
|
1435
1693
|
return plan !== null && this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
1436
1694
|
}
|
|
1437
1695
|
|
|
1438
1696
|
/* The lines the carets and selections touch, sorted. */
|
|
1439
1697
|
private selectedLines(span?: LineSpanOf): number[] {
|
|
1440
1698
|
const indices = new Set<number>();
|
|
1699
|
+
|
|
1441
1700
|
for (const selection of this.selections) {
|
|
1442
1701
|
let { first, last } = this.touchedLines(selection);
|
|
1702
|
+
|
|
1443
1703
|
if (span) {
|
|
1444
1704
|
first = span(first).first;
|
|
1445
1705
|
last = Math.max(last, span(last).last);
|
|
1446
1706
|
}
|
|
1707
|
+
|
|
1447
1708
|
for (let line = first; line <= last; line++) {
|
|
1448
1709
|
indices.add(line);
|
|
1449
1710
|
}
|
|
1450
1711
|
}
|
|
1712
|
+
|
|
1451
1713
|
return [...indices].sort((left, right) => left - right);
|
|
1452
1714
|
}
|
|
1453
1715
|
|
|
@@ -1456,6 +1718,7 @@ export class DocumentModel {
|
|
|
1456
1718
|
const { from, to } = rangeOf(selection);
|
|
1457
1719
|
const first = this.rope.lineAt(from);
|
|
1458
1720
|
const last = this.rope.lineAt(to);
|
|
1721
|
+
|
|
1459
1722
|
return { first, last: last > first && this.rope.lineBounds(last).start === to ? last - 1 : last };
|
|
1460
1723
|
}
|
|
1461
1724
|
|
|
@@ -1463,56 +1726,72 @@ export class DocumentModel {
|
|
|
1463
1726
|
private duplicate(span?: LineSpanOf): boolean {
|
|
1464
1727
|
const carets = this.selections.filter((selection) => selection.anchor === selection.head);
|
|
1465
1728
|
const lines = new Set<number>();
|
|
1729
|
+
|
|
1466
1730
|
for (const caret of carets) {
|
|
1467
1731
|
const line = this.rope.lineAt(caret.head);
|
|
1468
1732
|
const { first, last } = span ? span(line) : { first: line, last: line };
|
|
1733
|
+
|
|
1469
1734
|
for (let index = first; index <= last; index++) {
|
|
1470
1735
|
lines.add(index);
|
|
1471
1736
|
}
|
|
1472
1737
|
}
|
|
1738
|
+
|
|
1473
1739
|
const groups = blocks([...lines].sort((left, right) => left - right));
|
|
1740
|
+
|
|
1474
1741
|
interface Copy {
|
|
1475
1742
|
at: number;
|
|
1476
1743
|
text: string;
|
|
1477
1744
|
/* Where each selection that rides on this copy stands once `shift` characters went in before it. */
|
|
1478
1745
|
select(shift: number): Selection[];
|
|
1479
1746
|
}
|
|
1747
|
+
|
|
1480
1748
|
const copies: Copy[] = [];
|
|
1749
|
+
|
|
1481
1750
|
for (const selection of this.selections) {
|
|
1482
1751
|
const { from, to } = rangeOf(selection);
|
|
1752
|
+
|
|
1483
1753
|
if (from !== to) {
|
|
1484
1754
|
const text = this.slice(from, to);
|
|
1485
1755
|
copies.push({ at: to, text, select: (shift) => [{ anchor: to + shift, head: to + shift + text.length }] });
|
|
1486
1756
|
}
|
|
1487
1757
|
}
|
|
1758
|
+
|
|
1488
1759
|
for (const group of groups) {
|
|
1489
1760
|
const first = this.rope.lineBounds(group.first);
|
|
1490
1761
|
const last = this.rope.lineBounds(group.last);
|
|
1491
1762
|
const text = last.next > last.end ? this.slice(first.start, last.next) : this.newlineAt(group.last) + this.slice(first.start, last.end);
|
|
1763
|
+
|
|
1492
1764
|
const riders = carets.filter((caret) => {
|
|
1493
1765
|
const line = this.rope.lineAt(caret.head);
|
|
1766
|
+
|
|
1494
1767
|
return line >= group.first && line <= group.last;
|
|
1495
1768
|
});
|
|
1769
|
+
|
|
1496
1770
|
copies.push({
|
|
1497
1771
|
at: last.next,
|
|
1498
1772
|
text,
|
|
1499
1773
|
select: (shift) => riders.map((caret) => ({ anchor: caret.anchor + shift + text.length, head: caret.head + shift + text.length }))
|
|
1500
1774
|
});
|
|
1501
1775
|
}
|
|
1776
|
+
|
|
1502
1777
|
copies.sort((left, right) => left.at - right.at);
|
|
1503
1778
|
const edits: TextEdit[] = [];
|
|
1504
1779
|
const selections: Selection[] = [];
|
|
1505
1780
|
let shift = 0;
|
|
1781
|
+
|
|
1506
1782
|
for (const copy of copies) {
|
|
1507
1783
|
selections.push(...copy.select(shift));
|
|
1508
1784
|
const previous = edits.at(-1);
|
|
1785
|
+
|
|
1509
1786
|
if (previous?.from === copy.at) {
|
|
1510
1787
|
previous.text += copy.text;
|
|
1511
1788
|
} else {
|
|
1512
1789
|
edits.push({ from: copy.at, to: copy.at, text: copy.text });
|
|
1513
1790
|
}
|
|
1791
|
+
|
|
1514
1792
|
shift += copy.text.length;
|
|
1515
1793
|
}
|
|
1794
|
+
|
|
1516
1795
|
return this.applyEdits(edits, { source: 'command', selections });
|
|
1517
1796
|
}
|
|
1518
1797
|
|
|
@@ -1520,24 +1799,31 @@ export class DocumentModel {
|
|
|
1520
1799
|
private deleteLines(indices: readonly number[]): boolean {
|
|
1521
1800
|
const groups = blocks(indices);
|
|
1522
1801
|
const columns = new Map<number, number>();
|
|
1802
|
+
|
|
1523
1803
|
for (const selection of this.selections) {
|
|
1524
1804
|
columns.set(this.rope.lineAt(selection.head), this.positionAt(selection.head).column);
|
|
1525
1805
|
}
|
|
1806
|
+
|
|
1526
1807
|
const edits = groups.map((block) => {
|
|
1527
1808
|
let from = this.rope.lineBounds(block.first).start;
|
|
1528
1809
|
const to = this.rope.lineBounds(block.last).next;
|
|
1810
|
+
|
|
1529
1811
|
// The last line has no break of its own, so the one before it goes too.
|
|
1530
1812
|
if (block.last === this.getLineCount() - 1 && block.first > 0) {
|
|
1531
1813
|
from = this.rope.lineBounds(block.first - 1).end;
|
|
1532
1814
|
}
|
|
1815
|
+
|
|
1533
1816
|
return { from, to, text: '' };
|
|
1534
1817
|
});
|
|
1818
|
+
|
|
1535
1819
|
const selections: Selection[] = [];
|
|
1536
1820
|
let removed = 0;
|
|
1821
|
+
|
|
1537
1822
|
for (const [position, block] of groups.entries()) {
|
|
1538
1823
|
const edit = edits[position]!;
|
|
1539
1824
|
const column = Math.max(0, ...[...columns].filter(([line]) => line >= block.first && line <= block.last).map(([, value]) => value));
|
|
1540
1825
|
const target = block.last + 1 < this.getLineCount() ? block.last + 1 : block.first - 1;
|
|
1826
|
+
|
|
1541
1827
|
if (target >= 0) {
|
|
1542
1828
|
const bounds = this.rope.lineBounds(target);
|
|
1543
1829
|
const start = bounds.start - (block.last + 1 < this.getLineCount() ? edit.to - edit.from : 0) - removed;
|
|
@@ -1546,19 +1832,24 @@ export class DocumentModel {
|
|
|
1546
1832
|
} else {
|
|
1547
1833
|
selections.push({ anchor: 0, head: 0 });
|
|
1548
1834
|
}
|
|
1835
|
+
|
|
1549
1836
|
removed += edit.to - edit.from;
|
|
1550
1837
|
}
|
|
1838
|
+
|
|
1551
1839
|
return this.applyEdits(edits, { source: 'command', selections });
|
|
1552
1840
|
}
|
|
1553
1841
|
|
|
1554
1842
|
private moveLines(indices: readonly number[], direction: -1 | 1, options: CommandOptions): boolean {
|
|
1555
1843
|
const span: LineSpanOf = options.lineSpan ?? ((line) => ({ first: line, last: line }));
|
|
1556
1844
|
const groups = blocks(indices).filter((block) => (direction === -1 ? block.first > 0 : block.last < this.getLineCount() - 1));
|
|
1845
|
+
|
|
1557
1846
|
if (groups.length === 0) {
|
|
1558
1847
|
return false;
|
|
1559
1848
|
}
|
|
1849
|
+
|
|
1560
1850
|
const targets = new Map<number, { start: number; end: number; next: number }>();
|
|
1561
1851
|
const moves: { from: number; to: number }[] = [];
|
|
1852
|
+
|
|
1562
1853
|
const edits = groups.map((block) => {
|
|
1563
1854
|
const own = range(block.first, block.last);
|
|
1564
1855
|
// The line it swaps with is a whole row of the screen, so a collapsed fold is never cut in two.
|
|
@@ -1570,6 +1861,7 @@ export class DocumentModel {
|
|
|
1570
1861
|
let at = this.rope.lineBounds(first).start;
|
|
1571
1862
|
const from = at;
|
|
1572
1863
|
const parts: string[] = [];
|
|
1864
|
+
|
|
1573
1865
|
for (const [index, original] of order.entries()) {
|
|
1574
1866
|
const line = this.getLine(original);
|
|
1575
1867
|
// Each line keeps the line break of the slot it moves into, so mixed LF and CRLF stay where they were.
|
|
@@ -1577,55 +1869,78 @@ export class DocumentModel {
|
|
|
1577
1869
|
const ending = this.slice(slot.end, slot.next);
|
|
1578
1870
|
parts.push(line.text, ending);
|
|
1579
1871
|
targets.set(original, { start: at, end: at + line.text.length, next: at + line.text.length + ending.length });
|
|
1872
|
+
|
|
1580
1873
|
if (original !== first + index) {
|
|
1581
1874
|
moves.push({ from: original, to: first + index });
|
|
1582
1875
|
}
|
|
1876
|
+
|
|
1583
1877
|
at += line.text.length + ending.length;
|
|
1584
1878
|
}
|
|
1879
|
+
|
|
1585
1880
|
return { from, to: this.rope.lineBounds(last).next, text: parts.join('') };
|
|
1586
1881
|
});
|
|
1882
|
+
|
|
1587
1883
|
const mapOffsetThroughMove = (offset: number, from: number, to: number): number => {
|
|
1588
1884
|
const line = this.rope.lineAt(offset);
|
|
1885
|
+
|
|
1589
1886
|
if (to > from && offset === to && line > 0 && this.rope.lineBounds(line).start === to) {
|
|
1590
1887
|
return (targets.get(line - 1) ?? this.rope.lineBounds(line - 1)).next;
|
|
1591
1888
|
}
|
|
1889
|
+
|
|
1592
1890
|
const target = targets.get(line);
|
|
1891
|
+
|
|
1593
1892
|
return target ? Math.min(target.end, target.start + offset - this.rope.lineBounds(line).start) : offset;
|
|
1594
1893
|
};
|
|
1894
|
+
|
|
1595
1895
|
const selections = this.selections.map((selection) => {
|
|
1596
1896
|
const { from, to } = rangeOf(selection);
|
|
1897
|
+
|
|
1597
1898
|
return { anchor: mapOffsetThroughMove(selection.anchor, from, to), head: mapOffsetThroughMove(selection.head, from, to) };
|
|
1598
1899
|
});
|
|
1900
|
+
|
|
1599
1901
|
const changed = this.applyEdits(edits, { source: 'command', selections });
|
|
1902
|
+
|
|
1600
1903
|
if (changed) {
|
|
1601
1904
|
options.onLinesMoved?.(moves);
|
|
1602
1905
|
}
|
|
1906
|
+
|
|
1603
1907
|
return changed;
|
|
1604
1908
|
}
|
|
1605
1909
|
|
|
1606
1910
|
/* Enter, or the same Enter from the end of the line (`below`) or with the caret left in front of the break (`split`). */
|
|
1607
1911
|
private insertNewline(options: EnterOptions, mode: 'enter' | 'below' | 'split'): boolean {
|
|
1608
1912
|
const source = this.editSource(options.language);
|
|
1913
|
+
|
|
1609
1914
|
const plan = (reach: boolean): EnterPlan[] =>
|
|
1610
1915
|
this.selections.map((selection) => {
|
|
1611
1916
|
const { from, to } = rangeOf(selection);
|
|
1917
|
+
|
|
1612
1918
|
if (mode === 'below') {
|
|
1613
|
-
const end = this.
|
|
1919
|
+
const end = this.boundsOf(selection.head).end;
|
|
1920
|
+
|
|
1614
1921
|
return planEnter(source, end, end, { ...options, reach });
|
|
1615
1922
|
}
|
|
1616
|
-
|
|
1923
|
+
|
|
1924
|
+
const before = this.slice(this.boundsOf(from).start, from);
|
|
1925
|
+
|
|
1617
1926
|
if (mode === 'split' && /^[\t ]*$/.test(before)) {
|
|
1618
1927
|
const start = from - before.length;
|
|
1928
|
+
|
|
1619
1929
|
return { from: start, to: start, text: before + this.newlineAt(this.rope.lineAt(from)), caret: before.length };
|
|
1620
1930
|
}
|
|
1931
|
+
|
|
1621
1932
|
const planned = planEnter(source, from, to, { ...options, reach });
|
|
1933
|
+
|
|
1622
1934
|
return mode === 'split' ? { ...planned, caret: from - planned.from } : planned;
|
|
1623
1935
|
});
|
|
1936
|
+
|
|
1624
1937
|
let plans = plan(true);
|
|
1625
1938
|
const sorted = [...plans].sort((left, right) => left.from - right.from);
|
|
1939
|
+
|
|
1626
1940
|
if (sorted.some((current, index) => index > 0 && current.from < sorted[index - 1]!.to)) {
|
|
1627
1941
|
plans = plan(false);
|
|
1628
1942
|
}
|
|
1943
|
+
|
|
1629
1944
|
return this.applySelectionEdits(
|
|
1630
1945
|
plans.map((entry) => ({ from: entry.from, to: entry.to, text: entry.text, anchor: entry.caret, head: entry.caret })),
|
|
1631
1946
|
{ source: 'command', typed: mode === 'enter' }
|
|
@@ -1640,6 +1955,7 @@ export class DocumentModel {
|
|
|
1640
1955
|
const line = this.rope.lineBounds(index);
|
|
1641
1956
|
const text = this.getLine(index).text;
|
|
1642
1957
|
const indent = whitespaceOf(text) + (/^[\t ]*[)\]}]/.test(text) ? unit : '');
|
|
1958
|
+
|
|
1643
1959
|
return { from: line.start, to: line.start, text: indent + this.newlineAt(index), anchor: indent.length, head: indent.length };
|
|
1644
1960
|
}),
|
|
1645
1961
|
{ source: 'command' }
|
|
@@ -1648,16 +1964,20 @@ export class DocumentModel {
|
|
|
1648
1964
|
|
|
1649
1965
|
private editLines(command: 'joinLines' | 'toggleCase' | 'autoIndentLines', options: { language: string; unit: string }): boolean {
|
|
1650
1966
|
const source = this.editSource(options.language);
|
|
1967
|
+
|
|
1651
1968
|
const plan: EditPlan | null =
|
|
1652
1969
|
command === 'joinLines'
|
|
1653
1970
|
? planJoinLines(source, this.selections, options)
|
|
1654
1971
|
: command === 'toggleCase'
|
|
1655
1972
|
? planToggleCase(source, this.selections)
|
|
1656
1973
|
: planAutoIndent(source, this.selectedLines(), options);
|
|
1974
|
+
|
|
1657
1975
|
if (plan === null) {
|
|
1658
1976
|
return false;
|
|
1659
1977
|
}
|
|
1978
|
+
|
|
1660
1979
|
const edits = [...plan.edits].sort((left, right) => left.from - right.from);
|
|
1980
|
+
|
|
1661
1981
|
// Without a plan of its own the selections keep their start in front of the indentation that goes in at it.
|
|
1662
1982
|
const selections =
|
|
1663
1983
|
plan.selections.length > 0
|
|
@@ -1666,8 +1986,10 @@ export class DocumentModel {
|
|
|
1666
1986
|
const { from, to } = rangeOf(selection);
|
|
1667
1987
|
const start = mapOffset(from, edits, from !== to);
|
|
1668
1988
|
const end = mapOffset(to, edits);
|
|
1989
|
+
|
|
1669
1990
|
return selection.anchor <= selection.head ? { anchor: start, head: end } : { anchor: end, head: start };
|
|
1670
1991
|
});
|
|
1992
|
+
|
|
1671
1993
|
return this.applyEdits(plan.edits, { source: 'command', selections });
|
|
1672
1994
|
}
|
|
1673
1995
|
|
|
@@ -1691,19 +2013,26 @@ export class DocumentModel {
|
|
|
1691
2013
|
*/
|
|
1692
2014
|
private isUnclosedBrace(offset: number, language: string): boolean {
|
|
1693
2015
|
const context = isPhp(language) ? this.typingContext(offset, language) : undefined;
|
|
2016
|
+
|
|
1694
2017
|
const index =
|
|
1695
2018
|
context?.embedded === 'javascript' && context.embedStart !== undefined ? this.scriptBracketIndex(context.embedStart) : this.bracketIndex(language);
|
|
2019
|
+
|
|
1696
2020
|
if (index === undefined) {
|
|
1697
2021
|
return false;
|
|
1698
2022
|
}
|
|
2023
|
+
|
|
1699
2024
|
if (index.unmatched.has(offset)) {
|
|
1700
2025
|
return true;
|
|
1701
2026
|
}
|
|
2027
|
+
|
|
1702
2028
|
const closer = index.pairs.get(offset);
|
|
1703
|
-
|
|
2029
|
+
|
|
2030
|
+
const indentAt = (at: number): number => whitespaceOf(this.lineOf(at).text).length;
|
|
2031
|
+
|
|
1704
2032
|
if (closer === undefined || indentAt(closer) >= indentAt(offset)) {
|
|
1705
2033
|
return false;
|
|
1706
2034
|
}
|
|
2035
|
+
|
|
1707
2036
|
return [...index.unmatched].some((at) => at < offset && this.rope.charAt(at) === '{');
|
|
1708
2037
|
}
|
|
1709
2038
|
|
|
@@ -1711,34 +2040,45 @@ export class DocumentModel {
|
|
|
1711
2040
|
if (this.scriptCache?.revision !== this.revision) {
|
|
1712
2041
|
this.scriptCache = { revision: this.revision, sources: new Map() };
|
|
1713
2042
|
}
|
|
2043
|
+
|
|
1714
2044
|
const cached = this.scriptCache.sources.get(start);
|
|
2045
|
+
|
|
1715
2046
|
if (cached !== undefined && (cached.text !== undefined || cached.limit >= limit)) {
|
|
1716
2047
|
return cached.text !== undefined && cached.text.length > limit ? { limit } : cached;
|
|
1717
2048
|
}
|
|
2049
|
+
|
|
1718
2050
|
const end = Math.min(this.rope.length, start + limit + 11);
|
|
1719
2051
|
let work = bracketScanLimit;
|
|
2052
|
+
|
|
1720
2053
|
const prepared = scriptSource(this.slice(start, end), start, end === this.rope.length, (at) => {
|
|
1721
2054
|
const position = this.positionAt(at);
|
|
1722
2055
|
work -= position.column + 1;
|
|
2056
|
+
|
|
1723
2057
|
return work < 0 ? null : this.typingContext(at, 'php');
|
|
1724
2058
|
});
|
|
2059
|
+
|
|
1725
2060
|
const result = { limit, text: prepared !== undefined && prepared.length <= limit ? prepared : undefined };
|
|
1726
2061
|
this.scriptCache.sources.set(start, result);
|
|
2062
|
+
|
|
1727
2063
|
return result;
|
|
1728
2064
|
}
|
|
1729
2065
|
|
|
1730
2066
|
private scriptBracketIndex(start: number): BracketIndex | undefined {
|
|
1731
2067
|
const source = this.scriptSource(start, bracketScanLimit);
|
|
2068
|
+
|
|
1732
2069
|
if (source.text === undefined) {
|
|
1733
2070
|
return undefined;
|
|
1734
2071
|
}
|
|
2072
|
+
|
|
1735
2073
|
if (source.index === undefined) {
|
|
1736
2074
|
const index = scanBrackets(source.text, 'javascript');
|
|
2075
|
+
|
|
1737
2076
|
source.index = {
|
|
1738
2077
|
pairs: new Map([...index.pairs].map(([from, to]) => [from + start, to + start])),
|
|
1739
2078
|
unmatched: new Set([...index.unmatched].map((at) => at + start))
|
|
1740
2079
|
};
|
|
1741
2080
|
}
|
|
2081
|
+
|
|
1742
2082
|
return source.index;
|
|
1743
2083
|
}
|
|
1744
2084
|
|
|
@@ -1747,12 +2087,16 @@ export class DocumentModel {
|
|
|
1747
2087
|
if (this.rope.length > bracketScanLimit) {
|
|
1748
2088
|
return undefined;
|
|
1749
2089
|
}
|
|
2090
|
+
|
|
1750
2091
|
const cache = this.bracketCache;
|
|
2092
|
+
|
|
1751
2093
|
if (cache?.revision === this.revision && cache.language === language) {
|
|
1752
2094
|
return cache.index;
|
|
1753
2095
|
}
|
|
2096
|
+
|
|
1754
2097
|
const index = scanBrackets(this.getText(), language);
|
|
1755
2098
|
this.bracketCache = { revision: this.revision, language, index };
|
|
2099
|
+
|
|
1756
2100
|
return index;
|
|
1757
2101
|
}
|
|
1758
2102
|
|
|
@@ -1765,6 +2109,7 @@ export class DocumentModel {
|
|
|
1765
2109
|
return this.deleteRanges(
|
|
1766
2110
|
this.selections.map((selection) => {
|
|
1767
2111
|
let { from, to } = rangeOf(selection);
|
|
2112
|
+
|
|
1768
2113
|
if (from === to) {
|
|
1769
2114
|
if (!backwards) {
|
|
1770
2115
|
to = deletionStep(this.rope, to, 1);
|
|
@@ -1777,6 +2122,7 @@ export class DocumentModel {
|
|
|
1777
2122
|
return this.unindent(from, indent, language) ?? { from: this.backspaceStart(from, indent.tabSize), to, text: '' };
|
|
1778
2123
|
}
|
|
1779
2124
|
}
|
|
2125
|
+
|
|
1780
2126
|
return { from, to, text: '' };
|
|
1781
2127
|
})
|
|
1782
2128
|
);
|
|
@@ -1790,29 +2136,40 @@ export class DocumentModel {
|
|
|
1790
2136
|
private unindent(offset: number, indent: { tabSize: number; unit: string }, language: string): TextEdit | null {
|
|
1791
2137
|
const index = this.rope.lineAt(offset);
|
|
1792
2138
|
const line = this.rope.lineBounds(index);
|
|
2139
|
+
|
|
1793
2140
|
if (!/^[\t ]*$/.test(this.slice(line.start, offset))) {
|
|
1794
2141
|
return null;
|
|
1795
2142
|
}
|
|
2143
|
+
|
|
1796
2144
|
const proper = properIndent(this.editSource(language), index, { language, unit: indent.unit });
|
|
2145
|
+
|
|
1797
2146
|
if (proper === null) {
|
|
1798
2147
|
return null;
|
|
1799
2148
|
}
|
|
2149
|
+
|
|
1800
2150
|
let end = offset;
|
|
2151
|
+
|
|
1801
2152
|
while (end < line.end && /[\t ]/.test(this.rope.charAt(end))) {
|
|
1802
2153
|
end++;
|
|
1803
2154
|
}
|
|
2155
|
+
|
|
1804
2156
|
if (indentationColumn(this.slice(line.start, end), indent.tabSize) > indentationColumn(proper, indent.tabSize)) {
|
|
1805
2157
|
return { from: line.start, to: end, text: proper };
|
|
1806
2158
|
}
|
|
2159
|
+
|
|
1807
2160
|
if (index === 0) {
|
|
1808
2161
|
return { from: 0, to: end, text: '' };
|
|
1809
2162
|
}
|
|
2163
|
+
|
|
1810
2164
|
const previous = this.getLine(index - 1);
|
|
1811
2165
|
const start = previous.end - trailingBlankLength(previous.text);
|
|
2166
|
+
|
|
1812
2167
|
if (start !== previous.start || start === 0) {
|
|
1813
2168
|
const before = this.slice(previous.start, start);
|
|
2169
|
+
|
|
1814
2170
|
return { from: start, to: end, text: before === '' ? '' : spacingBetween(before, this.slice(end, line.end).trimEnd()) };
|
|
1815
2171
|
}
|
|
2172
|
+
|
|
1816
2173
|
return { from: start, to: end, text: proper };
|
|
1817
2174
|
}
|
|
1818
2175
|
|
|
@@ -1820,73 +2177,102 @@ export class DocumentModel {
|
|
|
1820
2177
|
private isEmptyPair(offset: number, language: string): boolean {
|
|
1821
2178
|
const opener = this.rope.charAt(offset - 1);
|
|
1822
2179
|
const closer = this.rope.charAt(offset);
|
|
2180
|
+
|
|
1823
2181
|
if (!closer || pairs[opener] !== closer || this.escaped(offset - 1)) {
|
|
1824
2182
|
return false;
|
|
1825
2183
|
}
|
|
2184
|
+
|
|
1826
2185
|
const context = this.typingContext(offset - 1, language);
|
|
2186
|
+
|
|
1827
2187
|
if (context.mode === 'html' && context.markup === 'equals' && (opener === '"' || opener === "'")) {
|
|
1828
2188
|
const inside = this.typingContext(offset, language);
|
|
2189
|
+
|
|
1829
2190
|
return inside.mode === 'html' && inside.markup === 'value' && inside.markupQuote === opener;
|
|
1830
2191
|
}
|
|
2192
|
+
|
|
1831
2193
|
return context.mode === 'code' || this.isInterpolation(offset - 1, opener, context);
|
|
1832
2194
|
}
|
|
1833
2195
|
|
|
1834
2196
|
/* Backspace inside the indentation of a line goes back to the previous tab stop, and at the start of a line joins it with the one above. */
|
|
1835
2197
|
private backspaceStart(offset: number, tabSize: number): number {
|
|
1836
|
-
const
|
|
2198
|
+
const index = this.rope.lineAt(offset);
|
|
2199
|
+
const line = this.rope.lineBounds(index);
|
|
2200
|
+
|
|
1837
2201
|
if (offset === line.start && line.start > 0) {
|
|
1838
2202
|
// Joining with the line above also takes the whitespace that trails it.
|
|
1839
|
-
const previous = this.getLine(
|
|
2203
|
+
const previous = this.getLine(index - 1);
|
|
2204
|
+
|
|
1840
2205
|
return previous.end - trailingBlankLength(previous.text);
|
|
1841
2206
|
}
|
|
2207
|
+
|
|
1842
2208
|
const prefix = this.slice(line.start, offset);
|
|
2209
|
+
|
|
1843
2210
|
if (prefix.length > 0 && /^[\t ]+$/.test(prefix)) {
|
|
1844
2211
|
return line.start + indentStopLength(prefix, tabSize);
|
|
1845
2212
|
}
|
|
2213
|
+
|
|
1846
2214
|
return deletionStep(this.rope, offset, -1);
|
|
1847
2215
|
}
|
|
1848
2216
|
|
|
1849
2217
|
private deleteRanges(ranges: TextEdit[]): boolean {
|
|
1850
2218
|
ranges.sort((left, right) => left.from - right.from);
|
|
1851
2219
|
const merged: TextEdit[] = [];
|
|
2220
|
+
|
|
1852
2221
|
for (const range of ranges) {
|
|
1853
2222
|
const previous = merged.at(-1);
|
|
2223
|
+
|
|
1854
2224
|
if (previous && range.from <= previous.to) {
|
|
1855
2225
|
previous.to = Math.max(previous.to, range.to);
|
|
1856
2226
|
} else {
|
|
1857
2227
|
merged.push({ ...range });
|
|
1858
2228
|
}
|
|
1859
2229
|
}
|
|
2230
|
+
|
|
1860
2231
|
return this.applyEdits(merged, { source: 'command' });
|
|
1861
2232
|
}
|
|
1862
2233
|
|
|
1863
2234
|
private applySelectionEdits(plans: readonly SelectionEdit[], options: DocumentEditOptions, transaction = selectionTransaction(plans)): boolean {
|
|
1864
2235
|
const before = this.getSelections();
|
|
1865
2236
|
const changed = this.applyEdits(transaction.edits, { ...options, selections: transaction.selections });
|
|
2237
|
+
|
|
1866
2238
|
return changed || !sameSelections(before, this.selections);
|
|
1867
2239
|
}
|
|
1868
2240
|
|
|
1869
2241
|
/* The line break of a line, or the document's first one for the last line, which has none. */
|
|
1870
2242
|
private newlineAt(index: number): string {
|
|
1871
2243
|
const line = this.rope.lineBounds(index);
|
|
2244
|
+
|
|
1872
2245
|
if (line.next > line.end) {
|
|
1873
2246
|
return this.slice(line.end, line.next);
|
|
1874
2247
|
}
|
|
2248
|
+
|
|
1875
2249
|
const first = this.rope.lineBounds(0);
|
|
2250
|
+
|
|
1876
2251
|
return first.next > first.end ? this.slice(first.end, first.next) : '\n';
|
|
1877
2252
|
}
|
|
1878
2253
|
|
|
1879
|
-
|
|
2254
|
+
private lineOf(offset: number): DocumentLine {
|
|
2255
|
+
return this.rope.getLine(this.rope.lineAt(offset));
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
private boundsOf(offset: number): Omit<DocumentLine, 'text'> {
|
|
2259
|
+
return this.rope.lineBounds(this.rope.lineAt(offset));
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
/* Preceded by an odd number of backslashes. */
|
|
1880
2263
|
private escaped(offset: number): boolean {
|
|
1881
2264
|
let count = 0;
|
|
2265
|
+
|
|
1882
2266
|
while (offset > 0 && this.rope.charAt(--offset) === '\\') {
|
|
1883
2267
|
count++;
|
|
1884
2268
|
}
|
|
2269
|
+
|
|
1885
2270
|
return count % 2 === 1;
|
|
1886
2271
|
}
|
|
1887
2272
|
|
|
1888
2273
|
private typingContext(offset: number, language: string): TypingContext {
|
|
1889
2274
|
const position = this.positionAt(offset);
|
|
2275
|
+
|
|
1890
2276
|
return this.typingContexts.at(position.line, position.column, language);
|
|
1891
2277
|
}
|
|
1892
2278
|
|
|
@@ -1904,41 +2290,55 @@ export class DocumentModel {
|
|
|
1904
2290
|
return undefined;
|
|
1905
2291
|
}
|
|
1906
2292
|
}
|
|
2293
|
+
|
|
1907
2294
|
if (this.rope.charAt(from) === ';') {
|
|
1908
2295
|
return from;
|
|
1909
2296
|
}
|
|
1910
|
-
|
|
2297
|
+
|
|
2298
|
+
const line = this.lineOf(from);
|
|
1911
2299
|
const prefix = this.slice(line.start, from).trimEnd();
|
|
2300
|
+
|
|
1912
2301
|
if (!prefix || /[([,=:+\-*/%&|!?]$/.test(prefix)) {
|
|
1913
2302
|
return undefined;
|
|
1914
2303
|
}
|
|
2304
|
+
|
|
1915
2305
|
let target = from;
|
|
1916
2306
|
let bracket = context.bracket;
|
|
2307
|
+
|
|
1917
2308
|
while (target < line.end) {
|
|
1918
2309
|
const character = this.rope.charAt(target);
|
|
2310
|
+
|
|
1919
2311
|
if (character === ' ' || character === '\t') {
|
|
1920
2312
|
target++;
|
|
2313
|
+
|
|
1921
2314
|
continue;
|
|
1922
2315
|
}
|
|
2316
|
+
|
|
1923
2317
|
if ((character !== ')' && character !== ']') || bracket?.close !== character) {
|
|
1924
2318
|
break;
|
|
1925
2319
|
}
|
|
2320
|
+
|
|
1926
2321
|
target++;
|
|
1927
2322
|
bracket = bracket.previous;
|
|
1928
2323
|
}
|
|
2324
|
+
|
|
1929
2325
|
if (target === from || !/[)\]]/.test(this.slice(from, target))) {
|
|
1930
2326
|
return undefined;
|
|
1931
2327
|
}
|
|
2328
|
+
|
|
1932
2329
|
const after = this.rope.charAt(target);
|
|
2330
|
+
|
|
1933
2331
|
if (target !== line.end && after !== ';' && after !== '}' && this.slice(target, target + 2) !== '//') {
|
|
1934
2332
|
return undefined;
|
|
1935
2333
|
}
|
|
2334
|
+
|
|
1936
2335
|
// Whitespace after the expression stays after its terminator.
|
|
1937
2336
|
if (after !== ';') {
|
|
1938
2337
|
while (target > from && /[\t ]/.test(this.rope.charAt(target - 1))) {
|
|
1939
2338
|
target--;
|
|
1940
2339
|
}
|
|
1941
2340
|
}
|
|
2341
|
+
|
|
1942
2342
|
return target;
|
|
1943
2343
|
}
|
|
1944
2344
|
|
|
@@ -1950,42 +2350,57 @@ export class DocumentModel {
|
|
|
1950
2350
|
wordSelectionAt(offset: number, camel = false): { from: number; to: number } | null {
|
|
1951
2351
|
const length = this.rope.length;
|
|
1952
2352
|
const at = offset >= length ? length - 1 : offset;
|
|
2353
|
+
|
|
1953
2354
|
if (at < 0) {
|
|
1954
2355
|
return null;
|
|
1955
2356
|
}
|
|
2357
|
+
|
|
1956
2358
|
const word = this.identifierRunAt(at, camel);
|
|
2359
|
+
|
|
1957
2360
|
if (word !== null) {
|
|
1958
2361
|
return word;
|
|
1959
2362
|
}
|
|
1960
|
-
|
|
2363
|
+
|
|
2364
|
+
const line = this.lineOf(at);
|
|
2365
|
+
|
|
1961
2366
|
return line.start === line.end ? { from: 0, to: length } : { from: line.start, to: line.end };
|
|
1962
2367
|
}
|
|
1963
2368
|
|
|
1964
2369
|
/* The identifier, or its camel hump, that an offset is in or at the end of; null when it is on something else. */
|
|
1965
2370
|
private identifierRunAt(offset: number, camel: boolean): { from: number; to: number } | null {
|
|
1966
2371
|
const length = this.rope.length;
|
|
2372
|
+
|
|
1967
2373
|
const isWord = (at: number): boolean => at >= 0 && at < length && identifier.test(this.slice(at, step(this.rope, at, 1)));
|
|
2374
|
+
|
|
1968
2375
|
let at = offset;
|
|
2376
|
+
|
|
1969
2377
|
if (at > 0 && !isWord(at) && isWord(at - 1)) {
|
|
1970
2378
|
at--;
|
|
1971
2379
|
}
|
|
2380
|
+
|
|
1972
2381
|
if (!isWord(at)) {
|
|
1973
2382
|
return null;
|
|
1974
2383
|
}
|
|
2384
|
+
|
|
1975
2385
|
if (camel) {
|
|
1976
2386
|
let start = at;
|
|
1977
2387
|
let end = at + 1;
|
|
2388
|
+
|
|
1978
2389
|
while (start > 0 && isWord(start - 1) && !isHumpBoundary(this.rope, start, true)) {
|
|
1979
2390
|
start--;
|
|
1980
2391
|
}
|
|
2392
|
+
|
|
1981
2393
|
while (end < length && isWord(end) && !isHumpBoundary(this.rope, end, false)) {
|
|
1982
2394
|
end++;
|
|
1983
2395
|
}
|
|
2396
|
+
|
|
1984
2397
|
if (start + 1 < end) {
|
|
1985
2398
|
return { from: start, to: end };
|
|
1986
2399
|
}
|
|
1987
2400
|
}
|
|
2401
|
+
|
|
1988
2402
|
const word = this.wordRange(at);
|
|
2403
|
+
|
|
1989
2404
|
return word.from === word.to ? null : word;
|
|
1990
2405
|
}
|
|
1991
2406
|
|
|
@@ -1994,14 +2409,17 @@ export class DocumentModel {
|
|
|
1994
2409
|
if (offset === 0) {
|
|
1995
2410
|
return 0;
|
|
1996
2411
|
}
|
|
2412
|
+
|
|
1997
2413
|
const line = this.rope.lineAt(offset);
|
|
1998
2414
|
const minimum = line > 0 ? this.rope.lineBounds(line - 1).end : 0;
|
|
1999
2415
|
let found = offset - 1;
|
|
2416
|
+
|
|
2000
2417
|
for (; found > minimum; found--) {
|
|
2001
2418
|
if (isWordBoundary(this.rope, found, camel, true) || this.isBetweenBrackets(found)) {
|
|
2002
2419
|
break;
|
|
2003
2420
|
}
|
|
2004
2421
|
}
|
|
2422
|
+
|
|
2005
2423
|
return found;
|
|
2006
2424
|
}
|
|
2007
2425
|
|
|
@@ -2010,20 +2428,25 @@ export class DocumentModel {
|
|
|
2010
2428
|
if (offset >= this.rope.length - 1) {
|
|
2011
2429
|
return offset;
|
|
2012
2430
|
}
|
|
2431
|
+
|
|
2013
2432
|
const line = this.rope.lineAt(offset);
|
|
2014
2433
|
let found = offset + 1;
|
|
2015
2434
|
let maximum = this.rope.lineBounds(line).end;
|
|
2435
|
+
|
|
2016
2436
|
if (found > maximum) {
|
|
2017
2437
|
if (line + 1 >= this.rope.lineCount) {
|
|
2018
2438
|
return offset;
|
|
2019
2439
|
}
|
|
2440
|
+
|
|
2020
2441
|
maximum = this.rope.lineBounds(line + 1).end;
|
|
2021
2442
|
}
|
|
2443
|
+
|
|
2022
2444
|
for (; found < maximum; found++) {
|
|
2023
2445
|
if (isWordBoundary(this.rope, found, camel, false) || this.isBetweenBrackets(found)) {
|
|
2024
2446
|
break;
|
|
2025
2447
|
}
|
|
2026
2448
|
}
|
|
2449
|
+
|
|
2027
2450
|
return found;
|
|
2028
2451
|
}
|
|
2029
2452
|
|
|
@@ -2034,20 +2457,27 @@ export class DocumentModel {
|
|
|
2034
2457
|
private wordRange(offset: number): { from: number; to: number } {
|
|
2035
2458
|
let from = offset;
|
|
2036
2459
|
let to = offset;
|
|
2460
|
+
|
|
2037
2461
|
while (from > 0) {
|
|
2038
2462
|
const previous = step(this.rope, from, -1);
|
|
2463
|
+
|
|
2039
2464
|
if (!identifier.test(this.slice(previous, from))) {
|
|
2040
2465
|
break;
|
|
2041
2466
|
}
|
|
2467
|
+
|
|
2042
2468
|
from = previous;
|
|
2043
2469
|
}
|
|
2470
|
+
|
|
2044
2471
|
while (to < this.rope.length) {
|
|
2045
2472
|
const next = step(this.rope, to, 1);
|
|
2473
|
+
|
|
2046
2474
|
if (!identifier.test(this.slice(to, next))) {
|
|
2047
2475
|
break;
|
|
2048
2476
|
}
|
|
2477
|
+
|
|
2049
2478
|
to = next;
|
|
2050
2479
|
}
|
|
2480
|
+
|
|
2051
2481
|
return { from, to };
|
|
2052
2482
|
}
|
|
2053
2483
|
|
|
@@ -2056,19 +2486,25 @@ export class DocumentModel {
|
|
|
2056
2486
|
const selections = this.selections.map((selection, index) => {
|
|
2057
2487
|
const { from, to } = rangeOf(selection);
|
|
2058
2488
|
const candidate = this.largerRange(given[index] ?? null, from, to) ?? this.lexicalExpansion(selection, from, to);
|
|
2489
|
+
|
|
2059
2490
|
if (!candidate) {
|
|
2060
2491
|
return selection;
|
|
2061
2492
|
}
|
|
2493
|
+
|
|
2062
2494
|
return selection.anchor > selection.head ? { anchor: candidate.to, head: candidate.from } : { anchor: candidate.from, head: candidate.to };
|
|
2063
2495
|
});
|
|
2496
|
+
|
|
2064
2497
|
const normalized = normalizeSelections(this.rope, selections);
|
|
2498
|
+
|
|
2065
2499
|
if (sameSelections(this.selections, normalized)) {
|
|
2066
2500
|
return false;
|
|
2067
2501
|
}
|
|
2502
|
+
|
|
2068
2503
|
this.selectionHistory.push(this.selections.map((selection) => ({ ...selection })));
|
|
2069
2504
|
this.selections = normalized;
|
|
2070
2505
|
this.groupOpen = false;
|
|
2071
2506
|
this.emit();
|
|
2507
|
+
|
|
2072
2508
|
return true;
|
|
2073
2509
|
}
|
|
2074
2510
|
|
|
@@ -2081,26 +2517,29 @@ export class DocumentModel {
|
|
|
2081
2517
|
return this.expandSelection(ranges);
|
|
2082
2518
|
}
|
|
2083
2519
|
|
|
2084
|
-
/* The range when it holds the stretch and is larger than it, else null. */
|
|
2085
2520
|
private largerRange(range: OffsetRange | null, from: number, to: number): OffsetRange | null {
|
|
2086
2521
|
return range !== null && range.from <= from && range.to >= to && (range.from < from || range.to > to) ? range : null;
|
|
2087
2522
|
}
|
|
2088
2523
|
|
|
2089
2524
|
private lexicalExpansion(selection: Selection, from: number, to: number): OffsetRange | undefined {
|
|
2090
|
-
const first = this.
|
|
2091
|
-
const last = this.
|
|
2525
|
+
const first = this.lineOf(from);
|
|
2526
|
+
const last = this.lineOf(to);
|
|
2527
|
+
|
|
2092
2528
|
const candidates = [
|
|
2093
2529
|
this.wordRange(selection.head),
|
|
2094
2530
|
{ from: first.start, to: last.end },
|
|
2095
2531
|
{ from: first.start, to: last.next },
|
|
2096
2532
|
{ from: 0, to: this.rope.length }
|
|
2097
2533
|
];
|
|
2534
|
+
|
|
2098
2535
|
if (first.start === last.start) {
|
|
2099
2536
|
candidates.push({ from: first.start + whitespaceOf(first.text).length, to: first.end - trailingBlankLength(first.text) });
|
|
2100
2537
|
}
|
|
2538
|
+
|
|
2101
2539
|
for (const range of this.structure()) {
|
|
2102
2540
|
candidates.push({ from: range.innerFrom, to: range.innerTo }, range);
|
|
2103
2541
|
}
|
|
2542
|
+
|
|
2104
2543
|
return candidates
|
|
2105
2544
|
.filter((range) => range.from <= from && range.to >= to && (range.from < from || range.to > to))
|
|
2106
2545
|
.sort((left, right) => left.to - left.from - (right.to - right.from))[0];
|
|
@@ -2108,12 +2547,15 @@ export class DocumentModel {
|
|
|
2108
2547
|
|
|
2109
2548
|
private shrinkSelection(): boolean {
|
|
2110
2549
|
const previous = this.selectionHistory.pop();
|
|
2550
|
+
|
|
2111
2551
|
if (!previous) {
|
|
2112
2552
|
return false;
|
|
2113
2553
|
}
|
|
2554
|
+
|
|
2114
2555
|
this.selections = previous;
|
|
2115
2556
|
this.groupOpen = false;
|
|
2116
2557
|
this.emit();
|
|
2558
|
+
|
|
2117
2559
|
return true;
|
|
2118
2560
|
}
|
|
2119
2561
|
|
|
@@ -2124,6 +2566,7 @@ export class DocumentModel {
|
|
|
2124
2566
|
|
|
2125
2567
|
private repeatedOccurrence(): { wholeWord: boolean; notFound: boolean } | null {
|
|
2126
2568
|
const session = this.occurrenceSession;
|
|
2569
|
+
|
|
2127
2570
|
return session !== undefined && session.signature === this.occurrenceSignature() ? session : null;
|
|
2128
2571
|
}
|
|
2129
2572
|
|
|
@@ -2132,10 +2575,10 @@ export class DocumentModel {
|
|
|
2132
2575
|
this.occurrencesExhausted = notFound;
|
|
2133
2576
|
}
|
|
2134
2577
|
|
|
2135
|
-
/* Whether a range touches a selection that is already there. */
|
|
2136
2578
|
private overlapsSelection(from: number, to: number): boolean {
|
|
2137
2579
|
return this.selections.some((selection) => {
|
|
2138
2580
|
const range = rangeOf(selection);
|
|
2581
|
+
|
|
2139
2582
|
return from === to && range.from === range.to ? from === range.from : from < range.to && to > range.from;
|
|
2140
2583
|
});
|
|
2141
2584
|
}
|
|
@@ -2151,39 +2594,49 @@ export class DocumentModel {
|
|
|
2151
2594
|
const wholeWord = repeated?.wholeWord === true;
|
|
2152
2595
|
const primary = this.selections.at(-1)!;
|
|
2153
2596
|
const { from, to } = rangeOf(primary);
|
|
2597
|
+
|
|
2154
2598
|
if (from === to) {
|
|
2155
2599
|
const word = this.identifierRunAt(primary.head, false);
|
|
2600
|
+
|
|
2156
2601
|
if (word === null) {
|
|
2157
2602
|
return false;
|
|
2158
2603
|
}
|
|
2604
|
+
|
|
2159
2605
|
const changed = this.changeSelections([...this.selections.slice(0, -1), { anchor: word.from, head: word.to }]);
|
|
2160
2606
|
this.rememberOccurrence(true, false);
|
|
2607
|
+
|
|
2161
2608
|
return changed;
|
|
2162
2609
|
}
|
|
2610
|
+
|
|
2163
2611
|
const needle = this.slice(from, to);
|
|
2164
2612
|
const start = notFoundBefore ? 0 : to;
|
|
2165
2613
|
const match = this.find(needle, { caseSensitive: true, wholeWord }).find((candidate) => candidate.from >= start);
|
|
2614
|
+
|
|
2166
2615
|
if (match === undefined) {
|
|
2167
2616
|
this.rememberOccurrence(wholeWord, true);
|
|
2617
|
+
|
|
2168
2618
|
return false;
|
|
2169
2619
|
}
|
|
2620
|
+
|
|
2170
2621
|
if (this.overlapsSelection(match.from, match.to)) {
|
|
2171
2622
|
this.rememberOccurrence(wholeWord, notFoundBefore);
|
|
2623
|
+
|
|
2172
2624
|
return false;
|
|
2173
2625
|
}
|
|
2626
|
+
|
|
2174
2627
|
const changed = this.changeSelections([...this.selections, occurrenceSelection(match, primary.head - from)]);
|
|
2175
2628
|
this.rememberOccurrence(wholeWord, false);
|
|
2629
|
+
|
|
2176
2630
|
return changed;
|
|
2177
2631
|
}
|
|
2178
2632
|
|
|
2179
2633
|
/* Takes the newest caret away, or the selection when there is one caret. */
|
|
2180
2634
|
private unselectOccurrence(): boolean {
|
|
2181
|
-
const
|
|
2182
|
-
|
|
2183
|
-
? this.changeSelections(this.selections.slice(0, -1))
|
|
2184
|
-
: this.changeSelections([{ anchor: this.selections[0]!.head, head: this.selections[0]!.head }]);
|
|
2635
|
+
const head = this.selections[0]!.head;
|
|
2636
|
+
const changed = this.changeSelections(this.selections.length > 1 ? this.selections.slice(0, -1) : [{ anchor: head, head }]);
|
|
2185
2637
|
this.occurrenceSession = undefined;
|
|
2186
2638
|
this.occurrencesExhausted = false;
|
|
2639
|
+
|
|
2187
2640
|
return changed;
|
|
2188
2641
|
}
|
|
2189
2642
|
|
|
@@ -2192,22 +2645,29 @@ export class DocumentModel {
|
|
|
2192
2645
|
const primary = this.selections.at(-1)!;
|
|
2193
2646
|
let { from, to } = rangeOf(primary);
|
|
2194
2647
|
let wholeWord = false;
|
|
2648
|
+
|
|
2195
2649
|
if (from === to) {
|
|
2196
2650
|
const word = this.identifierRunAt(primary.head, false);
|
|
2651
|
+
|
|
2197
2652
|
if (word === null) {
|
|
2198
2653
|
return false;
|
|
2199
2654
|
}
|
|
2655
|
+
|
|
2200
2656
|
({ from, to } = word);
|
|
2201
2657
|
wholeWord = true;
|
|
2202
2658
|
}
|
|
2659
|
+
|
|
2203
2660
|
// The word that was only a caret gets selected forward, so its caret is at its end.
|
|
2204
2661
|
const shift = wholeWord ? to - from : primary.head - from;
|
|
2205
2662
|
const matches = this.find(this.slice(from, to), { caseSensitive: true, wholeWord });
|
|
2663
|
+
|
|
2206
2664
|
if (matches.length === 0) {
|
|
2207
2665
|
return false;
|
|
2208
2666
|
}
|
|
2667
|
+
|
|
2209
2668
|
const changed = this.changeSelections(matches.map((match) => occurrenceSelection(match, shift)));
|
|
2210
2669
|
this.rememberOccurrence(wholeWord, false);
|
|
2670
|
+
|
|
2211
2671
|
return changed;
|
|
2212
2672
|
}
|
|
2213
2673
|
|
|
@@ -2215,21 +2675,26 @@ export class DocumentModel {
|
|
|
2215
2675
|
private addCaretPerSelectedLine(): boolean {
|
|
2216
2676
|
const forward = this.selections.at(-1)!.head >= this.selections.at(-1)!.anchor;
|
|
2217
2677
|
const carets: Selection[] = [];
|
|
2678
|
+
|
|
2218
2679
|
for (const selection of this.selections) {
|
|
2219
2680
|
const { first, last } = this.touchedLines(selection);
|
|
2681
|
+
|
|
2220
2682
|
for (let line = first; line <= last; line++) {
|
|
2221
2683
|
const end = this.rope.lineBounds(line).end;
|
|
2222
2684
|
carets.push({ anchor: end, head: end });
|
|
2223
2685
|
}
|
|
2224
2686
|
}
|
|
2687
|
+
|
|
2225
2688
|
// The primary caret is the one at the end the selection was dragged to.
|
|
2226
2689
|
const primary = forward ? carets.pop() : carets.shift();
|
|
2690
|
+
|
|
2227
2691
|
return this.changeSelections(primary === undefined ? carets : [...carets, primary]);
|
|
2228
2692
|
}
|
|
2229
2693
|
|
|
2230
2694
|
private changeSelections(selections: readonly Selection[]): boolean {
|
|
2231
2695
|
const before = this.getSelections();
|
|
2232
2696
|
this.setSelections(selections);
|
|
2697
|
+
|
|
2233
2698
|
return !sameSelections(before, this.selections);
|
|
2234
2699
|
}
|
|
2235
2700
|
|
|
@@ -2271,17 +2736,21 @@ export class DocumentModel {
|
|
|
2271
2736
|
typed?: boolean
|
|
2272
2737
|
): void {
|
|
2273
2738
|
const snapshot = this.getSnapshot();
|
|
2739
|
+
|
|
2274
2740
|
if (changes) {
|
|
2275
2741
|
snapshot.changes = changes.map((transaction) => transaction.map((change) => ({ ...change })));
|
|
2276
2742
|
snapshot.source = source;
|
|
2743
|
+
|
|
2277
2744
|
if (typed) {
|
|
2278
2745
|
snapshot.typed = true;
|
|
2279
2746
|
}
|
|
2747
|
+
|
|
2280
2748
|
if (contentEdits) {
|
|
2281
2749
|
let edits: ContentEdit[] | undefined;
|
|
2282
2750
|
Object.defineProperty(snapshot, 'contentEdits', { get: () => (edits ??= contentEdits()), enumerable: true });
|
|
2283
2751
|
}
|
|
2284
2752
|
}
|
|
2753
|
+
|
|
2285
2754
|
for (const listener of this.listeners) {
|
|
2286
2755
|
try {
|
|
2287
2756
|
listener(snapshot);
|