@adecore/editor-core 0.39.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.
Files changed (53) hide show
  1. package/dist/block-folds.js +5 -2
  2. package/dist/bracket-tracker.js +4 -3
  3. package/dist/brackets.d.ts +2 -12
  4. package/dist/brackets.js +3 -15
  5. package/dist/closer-tracker.js +0 -1
  6. package/dist/doc-comments.js +0 -1
  7. package/dist/document.d.ts +2 -0
  8. package/dist/document.js +33 -35
  9. package/dist/edit-regions.js +3 -4
  10. package/dist/enter.d.ts +2 -0
  11. package/dist/enter.js +11 -9
  12. package/dist/fold-roles.js +2 -2
  13. package/dist/lexical-folds.d.ts +0 -1
  14. package/dist/lexical-folds.js +1 -4
  15. package/dist/lexical.d.ts +11 -5
  16. package/dist/lexical.js +13 -17
  17. package/dist/line-commands.js +7 -15
  18. package/dist/line-text.d.ts +2 -0
  19. package/dist/line-text.js +4 -0
  20. package/dist/markup-regions.js +2 -3
  21. package/dist/paste.js +2 -7
  22. package/dist/structure.js +2 -1
  23. package/dist/typing-context.js +5 -15
  24. package/package.json +2 -2
  25. package/src/block-folds.ts +39 -2
  26. package/src/bracket-tracker.ts +55 -12
  27. package/src/brackets.ts +35 -42
  28. package/src/closer-tracker.ts +6 -1
  29. package/src/comments.ts +103 -0
  30. package/src/doc-comments.ts +19 -1
  31. package/src/document.ts +505 -36
  32. package/src/edit-regions.ts +26 -4
  33. package/src/enter.ts +104 -9
  34. package/src/fold-roles.ts +34 -2
  35. package/src/index.ts +2 -0
  36. package/src/languages.ts +11 -0
  37. package/src/lexical-folds.ts +21 -5
  38. package/src/lexical.ts +31 -23
  39. package/src/line-commands.ts +81 -17
  40. package/src/line-text.ts +7 -0
  41. package/src/markup-regions.ts +101 -3
  42. package/src/offsets.ts +10 -0
  43. package/src/paste.ts +20 -7
  44. package/src/php-markup.ts +23 -0
  45. package/src/preserve-case.ts +12 -0
  46. package/src/rope.ts +70 -0
  47. package/src/script-source.ts +10 -0
  48. package/src/search.ts +38 -0
  49. package/src/structure.ts +29 -1
  50. package/src/text-span.ts +53 -0
  51. package/src/typing-context.ts +65 -41
  52. package/src/typing-handlers.ts +28 -0
  53. 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 text.match(/[\t ]*$/)?.[0].length ?? 0;
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
- return { from, to, text: wrapped.text, anchor: reversed ? end : start, head: reversed ? start : end };
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
- const indent = whitespaceOf(this.getLine(this.rope.lineAt(opener)).text);
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
- const scanned = scanBrackets(text.slice(0, offset) + opener + text.slice(offset), language);
1067
- return scanned.pairs.has(offset);
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 line = this.rope.lineBounds(this.rope.lineAt(offset));
1076
- const before = this.slice(line.start, offset);
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.rope.lineBounds(this.rope.lineAt(offset)).start, offset), tabSize);
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.getLine(this.rope.lineAt(offset));
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(this.firstNonSpace(line), line.start);
1379
+ return offset > row.start ? row.start : Math.max(indent, line.start);
1182
1380
  }
1183
- const indent = this.firstNonSpace(line);
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.getLine(this.rope.lineAt(offset));
1428
+ const line = this.lineOf(offset);
1224
1429
  const row = visualLine?.(offset) ?? line;
1225
- let end = row.end;
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
- const length = line.text.startsWith('\t') ? 1 : Math.min(line.text.match(/^ */)?.[0].length ?? 0, tabSize);
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.rope.lineBounds(this.rope.lineAt(selection.head)).end;
1919
+ const end = this.boundsOf(selection.head).end;
1920
+
1614
1921
  return planEnter(source, end, end, { ...options, reach });
1615
1922
  }
1616
- const before = this.slice(this.rope.lineBounds(this.rope.lineAt(from)).start, from);
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
- const indentAt = (at: number): number => whitespaceOf(this.getLine(this.rope.lineAt(at)).text).length;
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 line = this.rope.lineBounds(this.rope.lineAt(offset));
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(this.rope.lineAt(offset) - 1);
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
- /* Whether the character at `offset` is preceded by an odd number of backslashes. */
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
- const line = this.getLine(this.rope.lineAt(from));
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
- const line = this.getLine(this.rope.lineAt(at));
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.getLine(this.rope.lineAt(from));
2091
- const last = this.getLine(this.rope.lineAt(to));
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 changed =
2182
- this.selections.length > 1
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);