@adecore/editor 0.21.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/controller.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Keymap } from './keymap-table.ts';
2
2
  import type { CommandOptions, EditorCommand, Selection, TextEdit } from '@adecore/editor-core';
3
3
  import { replacementEdits } from './input.ts';
4
+ import { emit, subscribe } from './listeners.ts';
4
5
  import { chordMatches, type KeyAction, keyAction, type MoveKey } from './keymap.ts';
5
6
  import { DoubleModifierGesture } from './modifier-gesture.ts';
6
7
  import { NativeInput } from './native-input.ts';
@@ -28,8 +29,6 @@ export interface ControllerOptions {
28
29
  }
29
30
 
30
31
  const MODIFYING = new Set<EditorCommand>([
31
- 'undo',
32
- 'redo',
33
32
  'deleteWordLeft',
34
33
  'deleteWordRight',
35
34
  'deleteCamelLeft',
@@ -75,6 +74,10 @@ const VIEW_COMMANDS = new Set<EditorViewCommand>([
75
74
  /* The clipboard text of the last copy or cut of a bare caret, which pastes back as whole lines. Shared, since it can be pasted in another editor. */
76
75
  let lineClip: string | null = null;
77
76
 
77
+ function selectionSignature(selections: readonly Selection[]): string {
78
+ return selections.map((selection) => `${selection.anchor}:${selection.head}`).join(',');
79
+ }
80
+
78
81
  /* Typing in the same stretch is one undo step until a pause this long. */
79
82
  const TYPING_PAUSE_MS = 750;
80
83
 
@@ -375,7 +378,7 @@ export class InputController {
375
378
 
376
379
  private modelCommand(name: EditorCommand): boolean {
377
380
  const { model } = this.view;
378
- const modifies = MODIFYING.has(name) && name !== 'undo' && name !== 'redo';
381
+ const modifies = MODIFYING.has(name);
379
382
  if ((modifies || name === 'undo' || name === 'redo') && this.blockedByReadOnly()) {
380
383
  return false;
381
384
  }
@@ -449,7 +452,7 @@ export class InputController {
449
452
  /* Column mode on drops every caret but the first and keeps a selection as a box; off it makes one selection of the box. */
450
453
  private toggleColumnMode(): void {
451
454
  const { view } = this;
452
- const { model, layout } = view;
455
+ const { model } = view;
453
456
  this.block = null;
454
457
  if (view.columnMode) {
455
458
  const first = model.getSelections()[0]!;
@@ -462,9 +465,7 @@ export class InputController {
462
465
  const primary = model.getPrimary();
463
466
  model.setSelections([primary]);
464
467
  if (primary.anchor !== primary.head) {
465
- const anchor = layout.caret(primary.anchor);
466
- const head = layout.caret(primary.head);
467
- this.block = { anchor: { x: anchor.x, y: anchor.y }, head: { x: head.x, y: head.y }, signature: '' };
468
+ this.block = this.blockOf(primary, '');
468
469
  this.extendBlock({ x: 0, y: 0 });
469
470
  }
470
471
  }
@@ -472,13 +473,9 @@ export class InputController {
472
473
  /* Moves the corner of the box by a step and selects the columns between it and the other corner. */
473
474
  private extendBlock(step: { x: number; y: number }): void {
474
475
  const { model, layout } = this.view;
475
- const selections = model.getSelections();
476
- const signature = selections.map((selection) => `${selection.anchor}:${selection.head}`).join(',');
476
+ const signature = selectionSignature(model.getSelections());
477
477
  if (this.block === null || (this.block.signature !== '' && this.block.signature !== signature)) {
478
- const primary = model.getPrimary();
479
- const anchor = layout.caret(primary.anchor);
480
- const head = layout.caret(primary.head);
481
- this.block = { anchor: { x: anchor.x, y: anchor.y }, head: { x: head.x, y: head.y }, signature };
478
+ this.block = this.blockOf(model.getPrimary(), signature);
482
479
  }
483
480
  const block = this.block;
484
481
  block.head = {
@@ -486,10 +483,15 @@ export class InputController {
486
483
  y: Math.max(0, Math.min(layout.height - layout.metrics.lineHeight, block.head.y + step.y))
487
484
  };
488
485
  model.setSelections(columnSelections(layout, block.anchor, block.head));
489
- block.signature = model
490
- .getSelections()
491
- .map((selection) => `${selection.anchor}:${selection.head}`)
492
- .join(',');
486
+ block.signature = selectionSignature(model.getSelections());
487
+ }
488
+
489
+ /* A box between the two ends of a selection. */
490
+ private blockOf(selection: Selection, signature: string): NonNullable<InputController['block']> {
491
+ const { layout } = this.view;
492
+ const anchor = layout.caret(selection.anchor);
493
+ const head = layout.caret(selection.head);
494
+ return { anchor: { x: anchor.x, y: anchor.y }, head: { x: head.x, y: head.y }, signature };
493
495
  }
494
496
 
495
497
  private moveCarets(key: MoveKey, extend: boolean): void {
@@ -509,19 +511,17 @@ export class InputController {
509
511
  const { model, layout } = this.view;
510
512
  const vertical = key === 'ArrowUp' || key === 'ArrowDown';
511
513
  const direction = key === 'ArrowLeft' || key === 'ArrowUp' ? -1 : 1;
514
+ const edge = (selection: Selection): number =>
515
+ direction < 0 ? Math.min(selection.anchor, selection.head) : Math.max(selection.anchor, selection.head);
512
516
  const selections = model.getSelections().map((selection, index) => {
517
+ const collapses = !extend && selection.anchor !== selection.head;
513
518
  let head: number;
514
- if (!vertical && !extend && selection.anchor !== selection.head) {
515
- head = direction < 0 ? Math.min(selection.anchor, selection.head) : Math.max(selection.anchor, selection.head);
519
+ if (!vertical && collapses) {
520
+ head = edge(selection);
516
521
  } else if (vertical) {
517
522
  // A selection first gives way to the edge in the direction of the move, as the platform's Up and Down do.
518
- const hasSelection = !extend && selection.anchor !== selection.head;
519
- const from = hasSelection
520
- ? direction < 0
521
- ? Math.min(selection.anchor, selection.head)
522
- : Math.max(selection.anchor, selection.head)
523
- : selection.head;
524
- if (hasSelection) {
523
+ const from = collapses ? edge(selection) : selection.head;
524
+ if (collapses) {
525
525
  delete this.desiredXs[index];
526
526
  }
527
527
  this.desiredXs[index] ??= layout.caret(from).x;
@@ -634,24 +634,15 @@ export class InputController {
634
634
 
635
635
  /* A handler sees the key before the editor does, the page's own shortcuts included, since a list of suggestions owns the arrows while it is open. */
636
636
  onKeyDown(handler: EditorKeyHandler): () => void {
637
- this.keyHandlers.add(handler);
638
- return () => {
639
- this.keyHandlers.delete(handler);
640
- };
637
+ return subscribe(this.keyHandlers, handler);
641
638
  }
642
639
 
643
640
  onClick(handler: EditorClickHandler): () => void {
644
- this.clickHandlers.add(handler);
645
- return () => {
646
- this.clickHandlers.delete(handler);
647
- };
641
+ return subscribe(this.clickHandlers, handler);
648
642
  }
649
643
 
650
644
  onContextMenu(listener: (menu: EditorContextMenu) => void): () => void {
651
- this.contextListeners.add(listener);
652
- return () => {
653
- this.contextListeners.delete(listener);
654
- };
645
+ return subscribe(this.contextListeners, listener);
655
646
  }
656
647
 
657
648
  private keydown(event: KeyboardEvent): void {
@@ -728,9 +719,9 @@ export class InputController {
728
719
  ? selections
729
720
  .map((selection) => {
730
721
  const { first, last } = this.view.lineSpan(model.positionAt(selection.head).line);
731
- return { start: model.getLine(first).start, end: model.getLine(last).end, next: model.getLine(last).next };
722
+ const end = model.getLine(last);
723
+ return model.slice(model.getLine(first).start, end.next) + (end.next === end.end ? '\n' : '');
732
724
  })
733
- .map((lines) => model.slice(lines.start, lines.next) + (lines.next === lines.end ? '\n' : ''))
734
725
  .join('')
735
726
  : selections.map((selection) => model.slice(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head))).join('\n');
736
727
  event.clipboardData?.setData('text/plain', text);
@@ -794,9 +785,7 @@ export class InputController {
794
785
  .getSelections()
795
786
  .some((candidate) => Math.min(candidate.anchor, candidate.head) <= offset && offset <= Math.max(candidate.anchor, candidate.head));
796
787
  const menu: EditorContextMenu = { position: this.positionOf(offset), inSelection: selection, x: event.clientX, y: event.clientY };
797
- for (const listener of [...this.contextListeners]) {
798
- listener(menu);
799
- }
788
+ emit(this.contextListeners, menu);
800
789
  }
801
790
 
802
791
  /* A press on a pinned header goes to that header. */
package/src/engine.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { changedSpans, DocumentModel, type EditorSnapshot, replacementText } from '@adecore/editor-core';
1
+ import { changedSpans, DocumentModel, type EditorSnapshot, type FoldHints, replacementText } from '@adecore/editor-core';
2
2
  import { InputController } from './controller.ts';
3
3
  import { resolveSmartKeys } from './smart-keys.ts';
4
4
  import { emit, type Listener, subscribe } from './listeners.ts';
@@ -191,10 +191,7 @@ class SmartEditor implements Editor {
191
191
  }
192
192
 
193
193
  onTextChange(listener: (change: EditorTextChange) => void): () => void {
194
- this.textChanges.add(listener);
195
- return () => {
196
- this.textChanges.delete(listener);
197
- };
194
+ return subscribe(this.textChanges, listener);
198
195
  }
199
196
 
200
197
  private announceTextChange(edits: NonNullable<EditorSnapshot['contentEdits']>, source: EditorTextChange['source']): void {
@@ -205,9 +202,7 @@ class SmartEditor implements Editor {
205
202
  text: edit.text
206
203
  }))
207
204
  };
208
- for (const listener of [...this.textChanges]) {
209
- listener(change);
210
- }
205
+ emit(this.textChanges, change);
211
206
  }
212
207
 
213
208
  private caretMoved(head: number): void {
@@ -216,9 +211,7 @@ class SmartEditor implements Editor {
216
211
  }
217
212
  this.caretOffset = head;
218
213
  const position = this.positionAt(head);
219
- for (const listener of [...this.carets]) {
220
- listener(position);
221
- }
214
+ emit(this.carets, position);
222
215
  }
223
216
 
224
217
  getCaret(): EditorPosition {
@@ -230,15 +223,16 @@ class SmartEditor implements Editor {
230
223
  }
231
224
 
232
225
  getSelection(): EditorRange {
233
- const { anchor, head } = this.model.getPrimary();
234
- return { start: this.positionAt(Math.min(anchor, head)), end: this.positionAt(Math.max(anchor, head)) };
226
+ return this.rangeOf(this.model.getPrimary());
235
227
  }
236
228
 
237
229
  getSelections(): EditorRange[] {
238
- return this.model.getSelections().map(({ anchor, head }) => ({
239
- start: this.positionAt(Math.min(anchor, head)),
240
- end: this.positionAt(Math.max(anchor, head))
241
- }));
230
+ return this.model.getSelections().map((selection) => this.rangeOf(selection));
231
+ }
232
+
233
+ /* A selection as a range, start before end. */
234
+ private rangeOf({ anchor, head }: { anchor: number; head: number }): EditorRange {
235
+ return { start: this.positionAt(Math.min(anchor, head)), end: this.positionAt(Math.max(anchor, head)) };
242
236
  }
243
237
 
244
238
  getIndentation(): EditorIndentation {
@@ -267,10 +261,7 @@ class SmartEditor implements Editor {
267
261
  }
268
262
 
269
263
  onCaret(listener: (position: EditorPosition) => void): () => void {
270
- this.carets.add(listener);
271
- return () => {
272
- this.carets.delete(listener);
273
- };
264
+ return subscribe(this.carets, listener);
274
265
  }
275
266
 
276
267
  onHover(listener: (hover: EditorHover | null) => void): () => void {
@@ -625,7 +616,7 @@ class SmartEditor implements Editor {
625
616
  return this.view.onGutterAction(listener);
626
617
  }
627
618
 
628
- setGutterMarkers(markers: readonly EditorGutterMarker[], owner: string = 'default'): void {
619
+ setGutterMarkers(markers: readonly EditorGutterMarker[], owner: string = DEFAULT_WIDGET_OWNER): void {
629
620
  this.view.setGutterMarkers(owner, markers);
630
621
  }
631
622
 
@@ -667,30 +658,30 @@ class SmartEditor implements Editor {
667
658
  }
668
659
 
669
660
  setFoldHints(hints: EditorFoldHints | null): void {
670
- this.view.setFoldHints(
671
- hints === null
672
- ? null
673
- : {
674
- symbols: (hints.symbols ?? []).map((symbol) => ({
675
- from: this.offsetAt(symbol.range.start),
676
- to: this.offsetAt(symbol.range.end),
677
- body: symbol.body
678
- })),
679
- ranges: (hints.ranges ?? []).map((range) => {
680
- const first = this.model.getLine(Math.min(range.startLine, this.model.getLineCount() - 1));
681
- const last = this.model.getLine(Math.min(range.endLine, this.model.getLineCount() - 1));
682
- const within = (line: { start: number; end: number }, character: number): number =>
683
- Math.min(line.end, line.start + Math.max(0, character));
684
- return {
685
- from: first.start,
686
- to: last.start,
687
- ...(range.kind === undefined ? {} : { kind: range.kind }),
688
- ...(range.startCharacter === undefined ? {} : { head: within(first, range.startCharacter) }),
689
- ...(range.endCharacter === undefined ? {} : { tail: within(last, range.endCharacter) })
690
- };
691
- })
692
- }
693
- );
661
+ this.view.setFoldHints(hints === null ? null : this.foldHintsByOffset(hints));
662
+ }
663
+
664
+ private foldHintsByOffset(hints: EditorFoldHints): FoldHints {
665
+ const lineOf = (line: number): { start: number; end: number } => this.model.getLine(Math.min(line, this.model.getLineCount() - 1));
666
+ const within = (line: { start: number; end: number }, character: number): number => Math.min(line.end, line.start + Math.max(0, character));
667
+ return {
668
+ symbols: (hints.symbols ?? []).map((symbol) => ({
669
+ from: this.offsetAt(symbol.range.start),
670
+ to: this.offsetAt(symbol.range.end),
671
+ body: symbol.body
672
+ })),
673
+ ranges: (hints.ranges ?? []).map((range) => {
674
+ const first = lineOf(range.startLine);
675
+ const last = lineOf(range.endLine);
676
+ return {
677
+ from: first.start,
678
+ to: last.start,
679
+ ...(range.kind === undefined ? {} : { kind: range.kind }),
680
+ ...(range.startCharacter === undefined ? {} : { head: within(first, range.startCharacter) }),
681
+ ...(range.endCharacter === undefined ? {} : { tail: within(last, range.endCharacter) })
682
+ };
683
+ })
684
+ };
694
685
  }
695
686
 
696
687
  setSmartKeys(keys: Partial<EditorSmartKeys>): void {