@adecore/editor 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/controller.d.ts +1 -0
  2. package/dist/controller.js +17 -28
  3. package/dist/engine.js +2 -9
  4. package/dist/find.js +12 -13
  5. package/dist/layout.js +7 -2
  6. package/dist/paint.d.ts +2 -0
  7. package/dist/paint.js +23 -20
  8. package/dist/view.d.ts +3 -2
  9. package/dist/view.js +46 -59
  10. package/package.json +6 -6
  11. package/src/attribution.ts +14 -0
  12. package/src/code-block.ts +25 -0
  13. package/src/column-selection.ts +10 -0
  14. package/src/controller.ts +189 -28
  15. package/src/engine.ts +87 -9
  16. package/src/fake.ts +79 -0
  17. package/src/find-search.ts +20 -0
  18. package/src/find.ts +135 -14
  19. package/src/geometry-stops.ts +10 -0
  20. package/src/grammar-preparation.ts +23 -0
  21. package/src/index.ts +1 -0
  22. package/src/input.ts +13 -0
  23. package/src/keymap-table.ts +9 -0
  24. package/src/keymap.ts +25 -0
  25. package/src/layout-changes.ts +7 -0
  26. package/src/layout.ts +219 -3
  27. package/src/lens.ts +3 -0
  28. package/src/listeners.ts +1 -0
  29. package/src/metrics.ts +14 -0
  30. package/src/modifier-gesture.ts +17 -0
  31. package/src/native-input.ts +27 -0
  32. package/src/offsets.ts +10 -0
  33. package/src/outline.ts +38 -0
  34. package/src/overview.ts +13 -0
  35. package/src/paint.ts +140 -20
  36. package/src/php-html.ts +1 -0
  37. package/src/pointer.ts +50 -0
  38. package/src/scroll-animation.ts +10 -0
  39. package/src/scroll.ts +7 -0
  40. package/src/search-client.ts +32 -0
  41. package/src/search-worker.ts +38 -0
  42. package/src/semantic.ts +12 -0
  43. package/src/shiki-document.ts +19 -0
  44. package/src/shiki-worker.ts +68 -0
  45. package/src/shiki.ts +37 -0
  46. package/src/testing.ts +17 -0
  47. package/src/theme-scopes.ts +14 -0
  48. package/src/tokens.ts +31 -0
  49. package/src/touch.ts +16 -0
  50. package/src/tracked-range.ts +5 -0
  51. package/src/uniform-geometry.ts +10 -0
  52. package/src/view.ts +481 -75
  53. package/src/worker-client.ts +54 -0
package/src/controller.ts CHANGED
@@ -119,6 +119,13 @@ export class InputController {
119
119
  this.options = options;
120
120
  this.gesture = new DoubleModifierGesture(options.apple ? 'Alt' : 'Control');
121
121
  const { model, input, viewport } = view;
122
+
123
+ const lineAt = (offset: number): { from: number; to: number } => {
124
+ const row = view.layout.rowForLine(model.positionAt(offset).line);
125
+
126
+ return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
127
+ };
128
+
122
129
  this.native = new NativeInput(input, model, {
123
130
  readOnly: () => view.settings.readOnly,
124
131
  replace: (text) => this.replaceSelections(text),
@@ -134,6 +141,7 @@ export class InputController {
134
141
  view.revealCaret();
135
142
  }
136
143
  });
144
+
137
145
  this.pointer = new PointerSelection({
138
146
  model,
139
147
  viewport,
@@ -141,10 +149,7 @@ export class InputController {
141
149
  contentPoint: (x, y) => view.contentPoint(x, y),
142
150
  columnSelections: (from, to) => (sameCell(view.layout, from, to) ? null : columnSelections(view.layout, from, to)),
143
151
  visualLine: (offset) => view.layout.visualLine(offset),
144
- lineAt: (offset) => {
145
- const row = view.layout.rowForLine(model.positionAt(offset).line);
146
- return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
147
- },
152
+ lineAt,
148
153
  camelHumps: () => view.settings.smartKeys.camelHumps,
149
154
  columnMode: () => view.columnMode,
150
155
  selectionAt: (x, y) => (view.settings.readOnly ? null : view.selectionAtPoint(x, y)),
@@ -154,22 +159,23 @@ export class InputController {
154
159
  focus: () => view.focus(),
155
160
  scrolled: () => view.requestRender()
156
161
  });
162
+
157
163
  this.touch = new TouchSelection({
158
164
  model,
159
165
  offsetAt: (x, y) => view.offsetAtPoint(x, y),
160
166
  camelHumps: () => view.settings.smartKeys.camelHumps,
161
- lineAt: (offset) => {
162
- const row = view.layout.rowForLine(model.positionAt(offset).line);
163
- return row.kind === 'text' ? { from: model.getLine(row.line).start, to: model.getLine(row.lastLine).next } : { from: offset, to: offset };
164
- },
167
+ lineAt,
165
168
  focus: () => view.focus()
166
169
  });
170
+
167
171
  this.subscription = model.subscribe(() => {
168
172
  if (!this.keepDesiredXs) {
169
173
  this.desiredXs = [];
170
174
  }
175
+
171
176
  this.native.sync();
172
177
  });
178
+
173
179
  this.native.sync();
174
180
  this.attach();
175
181
  }
@@ -185,55 +191,69 @@ export class InputController {
185
191
  this.listen(input, 'keydown', (event) => this.keydown(event));
186
192
  this.listen(input, 'beforeinput', (event) => this.beforeInput(event));
187
193
  this.listen(input, 'input', () => this.native.onInput());
194
+
188
195
  this.listen(input, 'compositionstart', () => {
189
196
  this.historyGroup++;
190
197
  this.native.startComposition();
191
198
  });
199
+
192
200
  this.listen(input, 'compositionend', (event) => {
193
201
  this.native.endComposition(event.data);
194
202
  this.historyGroup++;
195
203
  this.view.render();
196
204
  });
205
+
197
206
  this.listen(input, 'select', () => this.native.readSelection());
198
207
  this.listen(input, 'focus', () => this.view.requestRender());
208
+
199
209
  this.listen(input, 'blur', () => {
200
210
  this.gesture.reset();
201
211
  this.view.requestRender();
202
212
  });
213
+
203
214
  this.listen(input, 'keyup', (event) => this.gesture.keyup(event));
215
+
204
216
  this.listen(root, 'focusout', (event) => {
205
217
  // A node by its type, as `instanceof Node` fails for one of another window, such as a window the opener mounted the editor in.
206
218
  const related = event.relatedTarget as Node | null;
219
+
207
220
  if (typeof related?.nodeType !== 'number' || !root.contains(related)) {
208
221
  this.options.blur();
209
222
  }
210
223
  });
224
+
211
225
  this.listen(input, 'paste', (event) => {
212
226
  event.preventDefault();
213
227
  this.paste(event.clipboardData?.getData('text/plain') ?? '');
214
228
  });
229
+
215
230
  this.listen(input, 'copy', (event) => this.copy(event, false));
216
231
  this.listen(input, 'cut', (event) => this.copy(event, true));
217
232
  this.listen(viewport, 'scroll', () => this.view.scrolled());
218
233
  this.listen(viewport, 'wheel', () => this.view.cancelScroll());
219
234
  this.listen(viewport, 'pointermove', (event) => this.view.hoverMoved(event));
235
+
220
236
  this.listen(viewport, 'pointerleave', () => {
221
237
  this.view.setHover(null);
222
238
  this.view.setInlayHover(null);
223
239
  });
240
+
224
241
  this.listen(viewport, 'pointerdown', (event) => this.pointerDown(event));
225
242
  this.listen(viewport, 'contextmenu', (event) => this.contextMenu(event));
226
243
  this.listen(this.view.gutterElement, 'pointerdown', (event) => this.gutterDown(event));
227
244
  this.listen(this.view.stickyElement, 'pointerdown', (event) => this.stickyDown(event));
245
+
228
246
  const move = (event: Event): void => {
229
247
  this.pointer.move(event as PointerEvent);
230
248
  this.touch.move(event as PointerEvent);
231
249
  };
250
+
232
251
  const end = (event: Event): void => {
233
252
  this.pointer.end(event as PointerEvent);
234
253
  this.touch.end(event as PointerEvent);
235
254
  this.historyGroup++;
236
255
  };
256
+
237
257
  document.addEventListener('pointermove', move);
238
258
  document.addEventListener('pointerup', end);
239
259
  document.addEventListener('pointercancel', end);
@@ -241,6 +261,7 @@ export class InputController {
241
261
  const visualViewport = document.defaultView?.visualViewport;
242
262
  let keyboardHeight = visualViewport?.height ?? 0;
243
263
  let keyboardScale = visualViewport?.scale ?? 1;
264
+
244
265
  const keyboard = (): void => {
245
266
  const height = visualViewport?.height ?? 0;
246
267
  const scale = visualViewport?.scale ?? 1;
@@ -248,11 +269,14 @@ export class InputController {
248
269
  const shrunk = height < keyboardHeight && scale === keyboardScale;
249
270
  keyboardHeight = height;
250
271
  keyboardScale = scale;
272
+
251
273
  if (shrunk && this.view.focused) {
252
274
  this.view.revealCaret();
253
275
  }
254
276
  };
277
+
255
278
  visualViewport?.addEventListener('resize', keyboard);
279
+
256
280
  this.cleanup.push(() => {
257
281
  visualViewport?.removeEventListener('resize', keyboard);
258
282
  document.removeEventListener('pointermove', move);
@@ -265,30 +289,28 @@ export class InputController {
265
289
  this.subscription.dispose();
266
290
  this.pointer.dispose();
267
291
  this.touch.dispose();
292
+
268
293
  for (const remove of this.cleanup) {
269
294
  remove();
270
295
  }
296
+
271
297
  this.cleanup.length = 0;
272
298
  this.keyHandlers.clear();
273
299
  }
274
300
 
275
- /* Tells a person why nothing happened, when the editor is read only and has said why. */
276
301
  private blockedByReadOnly(): boolean {
277
- if (!this.view.settings.readOnly) {
278
- return false;
279
- }
280
- if (this.view.settings.readOnlyReason) {
281
- this.view.notify(this.view.settings.readOnlyReason);
282
- }
283
- return true;
302
+ return this.view.blockedByReadOnly();
284
303
  }
285
304
 
286
305
  private inputGroup(): string {
287
306
  const now = Date.now();
307
+
288
308
  if (now - this.lastInputAt > TYPING_PAUSE_MS) {
289
309
  this.historyGroup++;
290
310
  }
311
+
291
312
  this.lastInputAt = now;
313
+
292
314
  return `view-input-${this.historyGroup}`;
293
315
  }
294
316
 
@@ -296,6 +318,7 @@ export class InputController {
296
318
  if (this.view.settings.readOnly) {
297
319
  return false;
298
320
  }
321
+
299
322
  return this.view.model.applyEdits(edits, { selections, source: 'input', historyGroup: this.inputGroup() });
300
323
  }
301
324
 
@@ -303,25 +326,28 @@ export class InputController {
303
326
  if (this.blockedByReadOnly()) {
304
327
  return;
305
328
  }
329
+
306
330
  const { model } = this.view;
307
- for (const selection of model.getSelections()) {
308
- this.view.ensureVisible(selection.head);
309
- }
331
+ this.unfoldCarets();
310
332
  const replacement = replacementEdits(model.getSelections(), this.native.normalizeNewlines(text));
311
333
  this.applyEdits(replacement.edits, replacement.selections);
312
334
  this.view.revealCaret();
313
335
  }
314
336
 
337
+ private unfoldCarets(): void {
338
+ for (const selection of this.view.model.getSelections()) {
339
+ this.view.ensureVisible(selection.head);
340
+ }
341
+ }
342
+
315
343
  private paste(text: string): void {
316
344
  if (this.blockedByReadOnly()) {
317
345
  return;
318
346
  }
319
- const { model } = this.view;
320
- for (const selection of model.getSelections()) {
321
- this.view.ensureVisible(selection.head);
322
- }
347
+
348
+ this.unfoldCarets();
323
349
  this.historyGroup++;
324
- model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
350
+ this.view.model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
325
351
  this.historyGroup++;
326
352
  this.view.revealCaret();
327
353
  }
@@ -329,6 +355,7 @@ export class InputController {
329
355
  private commandOptions(): CommandOptions {
330
356
  const { settings } = this.view;
331
357
  const keys = settings.smartKeys;
358
+
332
359
  return {
333
360
  tabSize: settings.tabSize,
334
361
  insertSpaces: settings.insertSpaces,
@@ -350,9 +377,11 @@ export class InputController {
350
377
  /* Folding is the view's, since it knows where the ranges are. */
351
378
  private viewCommand(name: EditorViewCommand): boolean {
352
379
  const { view } = this;
380
+
353
381
  if (view.settings.folding === false && name !== 'toggleColumnMode') {
354
382
  return false;
355
383
  }
384
+
356
385
  const run: Record<EditorViewCommand, () => void> = {
357
386
  collapseRegion: () => view.collapseRegion(),
358
387
  expandRegion: () => view.expandRegion(),
@@ -370,7 +399,9 @@ export class InputController {
370
399
  expandAllToLevel5: () => view.expandAllToLevel(5),
371
400
  toggleColumnMode: () => this.toggleColumnMode()
372
401
  };
402
+
373
403
  run[name]();
404
+
374
405
  return true;
375
406
  }
376
407
 
@@ -379,11 +410,14 @@ export class InputController {
379
410
  if (VIEW_COMMANDS.has(name as EditorViewCommand)) {
380
411
  return this.viewCommand(name as EditorViewCommand);
381
412
  }
413
+
382
414
  if (name === 'expandSelection' && this.selectionRanges !== null) {
383
415
  const provider = this.selectionRanges;
384
416
  this.expansions = this.expansions.then(() => this.expandThrough(provider));
417
+
385
418
  return true;
386
419
  }
420
+
387
421
  return this.modelCommand(name as EditorCommand);
388
422
  }
389
423
 
@@ -397,18 +431,25 @@ export class InputController {
397
431
  const selections = model.getSelections();
398
432
  const revision = model.getRevision();
399
433
  const stretches = selections.map(({ anchor, head }) => ({ from: Math.min(anchor, head), to: Math.max(anchor, head) }));
434
+
400
435
  const asked = stretches.map(({ from }) => {
401
436
  const { line, column } = model.positionAt(from);
437
+
402
438
  return { line, character: column };
403
439
  });
440
+
404
441
  const chains = await provider(asked).catch(() => null);
405
- if (model.getRevision() !== revision || JSON.stringify(model.getSelections()) !== JSON.stringify(selections)) {
442
+
443
+ if (model.getRevision() !== revision || selectionSignature(model.getSelections()) !== selectionSignature(selections)) {
406
444
  return;
407
445
  }
446
+
408
447
  if (chains === null) {
409
448
  this.modelCommand('expandSelection');
449
+
410
450
  return;
411
451
  }
452
+
412
453
  const ranges = stretches.map(
413
454
  ({ from, to }, index) =>
414
455
  (chains[index] ?? [])
@@ -418,6 +459,7 @@ export class InputController {
418
459
  }))
419
460
  .find((range) => range.from <= from && range.to >= to && (range.from < from || range.to > to)) ?? null
420
461
  );
462
+
421
463
  this.historyGroup++;
422
464
  model.expandSelectionTo(ranges);
423
465
  this.view.revealCaret();
@@ -426,35 +468,46 @@ export class InputController {
426
468
  private modelCommand(name: EditorCommand): boolean {
427
469
  const { model } = this.view;
428
470
  const modifies = MODIFYING.has(name);
471
+
429
472
  if ((modifies || name === 'undo' || name === 'redo') && this.blockedByReadOnly()) {
430
473
  return false;
431
474
  }
475
+
432
476
  this.historyGroup++;
477
+
433
478
  if (modifies) {
434
479
  for (const selection of model.getSelections()) {
435
480
  this.view.ensureVisible(selection.head);
436
481
  const position = model.positionAt(selection.head);
437
482
  const line = model.getLine(position.line);
483
+
438
484
  if ((name === 'smartBackspace' || name === 'deleteWordLeft') && selection.head === line.start && position.line > 0) {
439
485
  this.view.ensureVisible(model.getLine(position.line - 1).start);
440
486
  }
487
+
441
488
  if ((name === 'deleteForward' || name === 'deleteWordRight') && selection.head === line.end && position.line + 1 < model.getLineCount()) {
442
489
  this.view.ensureVisible(model.getLine(position.line + 1).start);
443
490
  }
444
491
  }
445
492
  }
493
+
446
494
  const folded = name === 'moveLineUp' || name === 'moveLineDown' ? this.view.getFolds().collapsed : [];
495
+
447
496
  const result = model.execute(name, {
448
497
  ...this.commandOptions(),
449
498
  onLinesMoved: (moves) => this.view.refoldMoved(folded, moves)
450
499
  });
500
+
451
501
  if (name === 'selectNextOccurrence' && model.occurrencesExhausted && this.view.settings.messages.noMoreOccurrences !== undefined) {
452
502
  this.view.notify(this.view.settings.messages.noMoreOccurrences);
453
503
  }
504
+
454
505
  if (name === 'smartEnd' || name === 'selectSmartEnd') {
455
506
  this.view.markRowEnd(model.getPrimary().head);
456
507
  }
508
+
457
509
  this.view.revealCaret();
510
+
458
511
  return result;
459
512
  }
460
513
 
@@ -462,9 +515,9 @@ export class InputController {
462
515
  if (this.blockedByReadOnly()) {
463
516
  return;
464
517
  }
465
- for (const selection of this.view.model.getSelections()) {
466
- this.view.ensureVisible(selection.head);
467
- }
518
+
519
+ this.unfoldCarets();
520
+
468
521
  // Every key goes through the model, which decides by the key before it too, as for the arrow of PHP.
469
522
  if (text.length === 1) {
470
523
  this.view.model.typeText(text, { ...this.commandOptions(), historyGroup: this.inputGroup() });
@@ -484,11 +537,14 @@ export class InputController {
484
537
  const scrollTop = direction < 0 ? from.y - page : Math.min(layout.height - height, from.y + page);
485
538
  this.view.scrollTo(from.x, Math.max(0, layout.lineBase(scrollTop)));
486
539
  const distance = direction < 0 ? height : height + lineHeight - 1;
540
+
487
541
  const selections = model.getSelections().map((selection, index) => {
488
542
  this.desiredXs[index] ??= layout.caret(selection.head).x;
489
543
  const head = layout.verticalOffset(selection.head, direction, this.desiredXs[index]!, distance);
544
+
490
545
  return { anchor: extend ? selection.anchor : head, head };
491
546
  });
547
+
492
548
  this.keepDesiredXs = true;
493
549
  model.setSelections(selections);
494
550
  this.keepDesiredXs = false;
@@ -501,16 +557,20 @@ export class InputController {
501
557
  const { view } = this;
502
558
  const { model } = view;
503
559
  this.block = null;
560
+
504
561
  if (view.columnMode) {
505
562
  const first = model.getSelections()[0]!;
506
563
  const last = model.getPrimary();
507
564
  view.setColumnMode(false);
508
565
  model.setSelections([{ anchor: first.anchor, head: last.head }]);
566
+
509
567
  return;
510
568
  }
569
+
511
570
  view.setColumnMode(true);
512
571
  const primary = model.getPrimary();
513
572
  model.setSelections([primary]);
573
+
514
574
  if (primary.anchor !== primary.head) {
515
575
  this.block = this.blockOf(primary, '');
516
576
  this.extendBlock({ x: 0, y: 0 });
@@ -521,14 +581,18 @@ export class InputController {
521
581
  private extendBlock(step: { x: number; y: number }): void {
522
582
  const { model, layout } = this.view;
523
583
  const signature = selectionSignature(model.getSelections());
584
+
524
585
  if (this.block === null || (this.block.signature !== '' && this.block.signature !== signature)) {
525
586
  this.block = this.blockOf(model.getPrimary(), signature);
526
587
  }
588
+
527
589
  const block = this.block;
590
+
528
591
  block.head = {
529
592
  x: Math.max(0, block.head.x + step.x),
530
593
  y: Math.max(0, Math.min(layout.height - layout.metrics.lineHeight, block.head.y + step.y))
531
594
  };
595
+
532
596
  model.setSelections(columnSelections(layout, block.anchor, block.head));
533
597
  block.signature = selectionSignature(model.getSelections());
534
598
  }
@@ -538,52 +602,68 @@ export class InputController {
538
602
  const { layout } = this.view;
539
603
  const anchor = layout.caret(selection.anchor);
540
604
  const head = layout.caret(selection.head);
605
+
541
606
  return { anchor: { x: anchor.x, y: anchor.y }, head: { x: head.x, y: head.y }, signature };
542
607
  }
543
608
 
544
609
  private moveCarets(key: MoveKey, extend: boolean): void {
545
610
  if (this.view.columnMode && extend && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
546
611
  const { lineHeight, charWidth } = this.view.layout.metrics;
612
+
547
613
  this.extendBlock({
548
614
  x: key === 'ArrowLeft' ? -charWidth : key === 'ArrowRight' ? charWidth : 0,
549
615
  y: key === 'ArrowUp' ? -lineHeight : key === 'ArrowDown' ? lineHeight : 0
550
616
  });
617
+
551
618
  this.view.revealCaret();
619
+
552
620
  return;
553
621
  }
622
+
554
623
  if (key === 'PageUp' || key === 'PageDown') {
555
624
  this.movePages(key === 'PageUp' ? -1 : 1, extend);
625
+
556
626
  return;
557
627
  }
628
+
558
629
  const { model, layout } = this.view;
559
630
  const vertical = key === 'ArrowUp' || key === 'ArrowDown';
560
631
  const direction = key === 'ArrowLeft' || key === 'ArrowUp' ? -1 : 1;
632
+
561
633
  const edge = (selection: Selection): number =>
562
634
  direction < 0 ? Math.min(selection.anchor, selection.head) : Math.max(selection.anchor, selection.head);
635
+
563
636
  const selections = model.getSelections().map((selection, index) => {
564
637
  const collapses = !extend && selection.anchor !== selection.head;
565
638
  let head: number;
639
+
566
640
  if (!vertical && collapses) {
567
641
  head = edge(selection);
568
642
  } else if (vertical) {
569
643
  // A selection first gives way to the edge in the direction of the move, as the platform's Up and Down do.
570
644
  const from = collapses ? edge(selection) : selection.head;
645
+
571
646
  if (collapses) {
572
647
  delete this.desiredXs[index];
573
648
  }
649
+
574
650
  this.desiredXs[index] ??= layout.caret(from).x;
575
651
  head = layout.verticalOffset(from, direction, this.desiredXs[index]!);
576
652
  } else {
577
653
  head = layout.horizontalOffset(selection.head, direction);
578
654
  }
655
+
579
656
  return { anchor: extend ? selection.anchor : head, head };
580
657
  });
658
+
581
659
  this.keepDesiredXs = vertical;
582
660
  model.setSelections(selections);
583
661
  this.keepDesiredXs = false;
662
+
584
663
  if (!vertical) {
585
664
  this.desiredXs = [];
586
665
  }
666
+
587
667
  this.historyGroup++;
588
668
  this.view.revealCaret();
589
669
  }
@@ -593,11 +673,13 @@ export class InputController {
593
673
  if (this.blockedByReadOnly()) {
594
674
  return;
595
675
  }
676
+
596
677
  const { model } = this.view;
597
678
  const text = model.slice(from, to);
598
679
  this.view.ensureVisible(offset);
599
680
  const start = copy || offset < from ? offset : offset - (to - from);
600
681
  this.historyGroup++;
682
+
601
683
  model.applyEdits(
602
684
  copy
603
685
  ? [{ from: offset, to: offset, text }]
@@ -610,6 +692,7 @@ export class InputController {
610
692
  selections: [{ anchor: start, head: start + text.length }]
611
693
  }
612
694
  );
695
+
613
696
  this.historyGroup++;
614
697
  this.view.revealCaret();
615
698
  }
@@ -617,17 +700,22 @@ export class InputController {
617
700
  private goToMatchingBracket(): void {
618
701
  const { model } = this.view;
619
702
  let changed = false;
703
+
620
704
  model.setSelections(
621
705
  model.getSelections().map((selection) => {
622
706
  const bracket = this.view.matchingBracket(selection);
707
+
623
708
  if (!bracket || bracket.mate === undefined) {
624
709
  return selection;
625
710
  }
711
+
626
712
  const head = bracket.mate > bracket.at ? bracket.mate + 1 : bracket.mate;
627
713
  changed = true;
714
+
628
715
  return { anchor: head, head };
629
716
  })
630
717
  );
718
+
631
719
  if (changed) {
632
720
  this.view.revealCaret();
633
721
  }
@@ -635,45 +723,55 @@ export class InputController {
635
723
 
636
724
  private run(action: KeyAction, event: KeyboardEvent): void {
637
725
  const { model } = this.view;
726
+
638
727
  switch (action.type) {
639
728
  case 'save':
640
729
  event.preventDefault();
641
730
  this.options.save();
731
+
642
732
  return;
643
733
  case 'command':
644
734
  event.preventDefault();
645
735
  this.command(action.command);
736
+
646
737
  return;
647
738
  case 'move':
648
739
  event.preventDefault();
649
740
  this.moveCarets(action.key, action.extend);
741
+
650
742
  return;
651
743
  case 'edge': {
652
744
  event.preventDefault();
653
745
  const head = action.end ? model.getLength() : 0;
654
746
  model.setSelections([{ anchor: action.extend ? model.getPrimary().anchor : head, head }]);
655
747
  this.view.revealCaret();
748
+
656
749
  return;
657
750
  }
658
751
  case 'bracket':
659
752
  event.preventDefault();
660
753
  this.goToMatchingBracket();
754
+
661
755
  return;
662
756
  case 'view':
663
757
  event.preventDefault();
664
758
  this.viewCommand(action.command);
759
+
665
760
  return;
666
761
  case 'escape': {
667
762
  const selections = model.getSelections();
763
+
668
764
  if (selections.length > 1 || selections[0]!.anchor !== selections[0]!.head) {
669
765
  event.preventDefault();
670
766
  // The oldest caret stays; the newest ones go.
671
767
  const head = selections[0]!.head;
672
768
  model.setSelections([{ anchor: head, head }]);
769
+
673
770
  if (selections.length > 1) {
674
771
  this.view.revealCaret();
675
772
  }
676
773
  }
774
+
677
775
  return;
678
776
  }
679
777
  }
@@ -700,23 +798,31 @@ export class InputController {
700
798
  if (event.isComposing || this.native.composing) {
701
799
  return;
702
800
  }
801
+
703
802
  const added = this.gesture.keydown(event);
803
+
704
804
  if (added !== null) {
705
805
  event.preventDefault();
706
806
  this.command(added === 'above' ? 'addCaretAbove' : 'addCaretBelow');
807
+
707
808
  return;
708
809
  }
810
+
709
811
  for (const handler of [...this.keyHandlers]) {
710
812
  if (handler(event)) {
711
813
  event.preventDefault();
814
+
712
815
  return;
713
816
  }
714
817
  }
818
+
715
819
  // The app's own shortcuts stay the app's, also where the editor has a binding for the same key.
716
820
  if (this.options.handBack.some((chord) => chordMatches(chord, event, this.options.apple))) {
717
821
  return;
718
822
  }
823
+
719
824
  const action = keyAction(event, this.options.apple, this.options.keymap);
825
+
720
826
  if (action) {
721
827
  this.run(action, event);
722
828
  }
@@ -726,36 +832,45 @@ export class InputController {
726
832
  if (this.view.settings.readOnly) {
727
833
  event.preventDefault();
728
834
  this.blockedByReadOnly();
835
+
729
836
  return;
730
837
  }
838
+
731
839
  if (this.native.composing || event.isComposing) {
732
840
  return;
733
841
  }
842
+
734
843
  switch (event.inputType) {
735
844
  case 'insertText':
736
845
  case 'insertReplacementText':
737
846
  event.preventDefault();
847
+
738
848
  if (event.data !== null) {
739
849
  this.typeText(event.data);
740
850
  }
851
+
741
852
  return;
742
853
  case 'insertLineBreak':
743
854
  case 'insertParagraph':
744
855
  event.preventDefault();
745
856
  this.command('insertNewline');
857
+
746
858
  return;
747
859
  case 'deleteContentBackward':
748
860
  event.preventDefault();
749
861
  this.command('smartBackspace');
862
+
750
863
  return;
751
864
  case 'deleteContentForward':
752
865
  event.preventDefault();
753
866
  this.command('deleteForward');
867
+
754
868
  return;
755
869
  case 'historyUndo':
756
870
  case 'historyRedo':
757
871
  event.preventDefault();
758
872
  this.command(event.inputType === 'historyUndo' ? 'undo' : 'redo');
873
+
759
874
  return;
760
875
  }
761
876
  }
@@ -766,32 +881,40 @@ export class InputController {
766
881
  const { model } = this.view;
767
882
  const selections = model.getSelections();
768
883
  const empty = selections.every((selection) => selection.anchor === selection.head);
884
+
769
885
  const text = empty
770
886
  ? selections
771
887
  .map((selection) => {
772
888
  const { first, last } = this.view.lineSpan(model.positionAt(selection.head).line);
773
889
  const end = model.getLine(last);
890
+
774
891
  return model.slice(model.getLine(first).start, end.next) + (end.next === end.end ? '\n' : '');
775
892
  })
776
893
  .join('')
777
894
  : selections.map((selection) => model.slice(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head))).join('\n');
895
+
778
896
  event.clipboardData?.setData('text/plain', text);
779
897
  lineClip = empty ? text : null;
898
+
780
899
  if (!cut || this.blockedByReadOnly()) {
781
900
  return;
782
901
  }
902
+
783
903
  this.historyGroup++;
904
+
784
905
  if (empty) {
785
906
  this.command('deleteLine');
786
907
  } else {
787
908
  this.replaceSelections('');
788
909
  }
910
+
789
911
  this.historyGroup++;
790
912
  }
791
913
 
792
914
  private pointerDown(event: PointerEvent): void {
793
915
  this.view.cancelScroll();
794
916
  const target = event.target as HTMLElement;
917
+
795
918
  // A press a part took already, such as a pinned header, which draws itself anew and so takes its target off the page before the press bubbles here.
796
919
  if (
797
920
  event.defaultPrevented ||
@@ -800,32 +923,42 @@ export class InputController {
800
923
  ) {
801
924
  return;
802
925
  }
926
+
803
927
  // Cancels the compatibility mouse events, whose focus change would close the keyboard a tap opened; scrolling is not affected.
804
928
  event.preventDefault();
929
+
805
930
  if (this.takenByInlayHandler(event)) {
806
931
  return;
807
932
  }
933
+
808
934
  if (event.pointerType === 'touch') {
809
935
  this.touch.start(event, 'text', () => this.takenByHost(event));
936
+
810
937
  return;
811
938
  }
939
+
812
940
  if (this.takenByHost(event)) {
813
941
  this.view.focus();
942
+
814
943
  return;
815
944
  }
945
+
816
946
  this.pointer.start(event);
817
947
  }
818
948
 
819
949
  /* Whether a handler of the host took a press on a hint, which then moves neither the caret nor the focus. */
820
950
  private takenByInlayHandler(event: PointerEvent): boolean {
821
951
  const inlay = this.inlayHandlers.size === 0 ? null : this.view.inlayAt(event.target);
952
+
822
953
  if (inlay === null) {
823
954
  return false;
824
955
  }
956
+
825
957
  const now = Date.now();
826
958
  const again = this.lastInlayPress?.id === inlay.id && now - this.lastInlayPress.at <= DOUBLE_PRESS_MS;
827
959
  this.lastInlayPress = again ? null : { id: inlay.id, at: now };
828
960
  const touch = event.pointerType === 'touch';
961
+
829
962
  const click: EditorInlayClick = {
830
963
  id: inlay.id,
831
964
  part: inlay.part,
@@ -835,12 +968,15 @@ export class InputController {
835
968
  clicks: again ? 2 : 1,
836
969
  touch
837
970
  };
971
+
838
972
  if (![...this.inlayHandlers].some((handler) => handler(click))) {
839
973
  return false;
840
974
  }
975
+
841
976
  if (!touch) {
842
977
  this.view.focus();
843
978
  }
979
+
844
980
  return true;
845
981
  }
846
982
 
@@ -849,37 +985,47 @@ export class InputController {
849
985
  if (this.clickHandlers.size === 0) {
850
986
  return false;
851
987
  }
988
+
852
989
  const offset = this.view.characterAtPoint(event.clientX, event.clientY, event.target);
990
+
853
991
  if (offset === null) {
854
992
  return false;
855
993
  }
994
+
856
995
  const click: EditorClick = {
857
996
  position: this.positionOf(offset),
858
997
  mod: this.options.apple ? event.metaKey : event.ctrlKey,
859
998
  alt: event.altKey,
860
999
  shift: event.shiftKey
861
1000
  };
1001
+
862
1002
  return [...this.clickHandlers].some((handler) => handler(click));
863
1003
  }
864
1004
 
865
1005
  private positionOf(offset: number): EditorPosition {
866
1006
  const { line, column } = this.view.model.positionAt(offset);
1007
+
867
1008
  return { line, character: column };
868
1009
  }
869
1010
 
870
1011
  private contextMenu(event: MouseEvent): void {
871
1012
  if (this.contextListeners.size === 0 && this.touch.pressed) {
872
1013
  event.preventDefault();
1014
+
873
1015
  return;
874
1016
  }
1017
+
875
1018
  if (this.contextListeners.size === 0 || (event.target as HTMLElement | null)?.closest?.('.se-sticky')) {
876
1019
  return;
877
1020
  }
1021
+
878
1022
  event.preventDefault();
879
1023
  const offset = this.view.offsetAtPoint(event.clientX, event.clientY);
1024
+
880
1025
  const selection = this.view.model
881
1026
  .getSelections()
882
1027
  .some((candidate) => Math.min(candidate.anchor, candidate.head) <= offset && offset <= Math.max(candidate.anchor, candidate.head));
1028
+
883
1029
  const menu: EditorContextMenu = { position: this.positionOf(offset), inSelection: selection, x: event.clientX, y: event.clientY };
884
1030
  emit(this.contextListeners, menu);
885
1031
  }
@@ -887,9 +1033,11 @@ export class InputController {
887
1033
  /* A press on a pinned header goes to that header. */
888
1034
  private stickyDown(event: PointerEvent): void {
889
1035
  const row = (event.target as HTMLElement | null)?.closest?.<HTMLElement>('.se-sticky-row');
1036
+
890
1037
  if (event.button !== 0 || !row) {
891
1038
  return;
892
1039
  }
1040
+
893
1041
  event.preventDefault();
894
1042
  this.view.jumpToHeader(Number(row.dataset.line), Number(row.dataset.depth));
895
1043
  this.view.focus();
@@ -898,31 +1046,44 @@ export class InputController {
898
1046
  /* A press on a line number selects the line. */
899
1047
  private gutterDown(event: PointerEvent): void {
900
1048
  this.view.cancelScroll();
1049
+
901
1050
  if (event.button !== 0) {
902
1051
  return;
903
1052
  }
1053
+
904
1054
  event.preventDefault();
905
1055
  /* On the press and not on click: a repaint in between replaces the gutter's buttons, and a click whose button left the DOM never arrives. */
906
1056
  const markerId = this.view.gutterMarkerIdOf(event.target);
1057
+
907
1058
  if (markerId !== null) {
908
1059
  this.view.pressGutterMarker(markerId);
1060
+
909
1061
  return;
910
1062
  }
1063
+
911
1064
  const actionLine = this.view.gutterActionLineOf(event.target);
1065
+
912
1066
  if (actionLine !== null) {
913
1067
  this.view.pressGutterAction(actionLine);
1068
+
914
1069
  return;
915
1070
  }
1071
+
916
1072
  const foldLine = this.view.foldLineOf(event.target);
1073
+
917
1074
  if (foldLine !== null) {
918
1075
  this.view.toggleFold(foldLine);
919
1076
  this.view.focus();
1077
+
920
1078
  return;
921
1079
  }
1080
+
922
1081
  if (event.pointerType === 'touch') {
923
1082
  this.touch.start(event, 'gutter');
1083
+
924
1084
  return;
925
1085
  }
1086
+
926
1087
  this.pointer.start(event, 'gutter');
927
1088
  this.view.focus();
928
1089
  }