@adecore/editor 0.42.0 → 0.44.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
@@ -15,7 +15,9 @@ import type {
15
15
  EditorInlayClick,
16
16
  EditorInlayClickHandler,
17
17
  EditorContextMenu,
18
+ EditorCopy,
18
19
  EditorKeyHandler,
20
+ EditorPasteOptions,
19
21
  EditorPosition,
20
22
  EditorRunCommand,
21
23
  EditorSelectionRanges,
@@ -45,9 +47,12 @@ const MODIFYING = new Set<EditorCommand>([
45
47
  'deleteLine',
46
48
  'moveLineUp',
47
49
  'moveLineDown',
50
+ 'moveStatementUp',
51
+ 'moveStatementDown',
48
52
  'toggleLineComment',
49
53
  'toggleBlockComment',
50
54
  'startNewLine',
55
+ 'completeCurrentStatement',
51
56
  'startNewLineBefore',
52
57
  'splitLine',
53
58
  'joinLines',
@@ -63,9 +68,13 @@ const MODIFYING = new Set<EditorCommand>([
63
68
  'reverseLines',
64
69
  'uniqueLines',
65
70
  'convertIndentsToSpaces',
66
- 'convertIndentsToTabs'
71
+ 'convertIndentsToTabs',
72
+ 'hippieCompletion',
73
+ 'hippieBackwardCompletion'
67
74
  ]);
68
75
 
76
+ const CODE_BLOCK_COMMANDS = new Set<EditorCommand>(['codeBlockStart', 'codeBlockEnd', 'codeBlockStartWithSelection', 'codeBlockEndWithSelection']);
77
+
69
78
  const VIEW_COMMANDS = new Set<EditorViewCommand>([
70
79
  'collapseRegion',
71
80
  'expandRegion',
@@ -84,7 +93,12 @@ const VIEW_COMMANDS = new Set<EditorViewCommand>([
84
93
  'toggleColumnMode',
85
94
  'increaseFontSize',
86
95
  'decreaseFontSize',
87
- 'resetFontSize'
96
+ 'resetFontSize',
97
+ 'methodUp',
98
+ 'methodDown',
99
+ 'nextChange',
100
+ 'previousChange',
101
+ 'pasteAsPlainText'
88
102
  ]);
89
103
 
90
104
  const ZOOM_STEPS = { increaseFontSize: 1, decreaseFontSize: -1, resetFontSize: 0 } as const satisfies Partial<Record<EditorViewCommand, -1 | 0 | 1>>;
@@ -92,6 +106,14 @@ const ZOOM_STEPS = { increaseFontSize: 1, decreaseFontSize: -1, resetFontSize: 0
92
106
  /* 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. */
93
107
  let lineClip: string | null = null;
94
108
 
109
+ /*
110
+ * Makes `text` the copy that pastes back as whole lines (or not), for a paste from a history that kept what the
111
+ * copy was. The clipboard itself is the caller's.
112
+ */
113
+ export function rememberCopy(text: string, wholeLines: boolean): void {
114
+ lineClip = wholeLines ? text : null;
115
+ }
116
+
95
117
  function selectionSignature(selections: readonly Selection[]): string {
96
118
  return selections.map((selection) => `${selection.anchor}:${selection.head}`).join(',');
97
119
  }
@@ -111,6 +133,9 @@ export class InputController {
111
133
  private readonly touch: TouchSelection;
112
134
  private readonly cleanup: (() => void)[] = [];
113
135
  private readonly keyHandlers = new Set<EditorKeyHandler>();
136
+ private readonly navigateListeners = new Set<(from: number) => void>();
137
+ private readonly copyListeners = new Set<(copy: EditorCopy) => void>();
138
+ private wordSources: (() => readonly string[]) | null = null;
114
139
  private readonly gesture: DoubleModifierGesture;
115
140
  private readonly clickHandlers = new Set<EditorClickHandler>();
116
141
  private readonly inlayHandlers = new Set<EditorInlayClickHandler>();
@@ -240,7 +265,7 @@ export class InputController {
240
265
 
241
266
  this.listen(input, 'paste', (event) => {
242
267
  event.preventDefault();
243
- this.paste(event.clipboardData?.getData('text/plain') ?? '');
268
+ this.pasteText(event.clipboardData?.getData('text/plain') ?? '');
244
269
  });
245
270
 
246
271
  this.listen(input, 'copy', (event) => this.copy(event, false));
@@ -312,6 +337,8 @@ export class InputController {
312
337
 
313
338
  this.cleanup.length = 0;
314
339
  this.keyHandlers.clear();
340
+ this.navigateListeners.clear();
341
+ this.copyListeners.clear();
315
342
  }
316
343
 
317
344
  private blockedByReadOnly(): boolean {
@@ -356,16 +383,30 @@ export class InputController {
356
383
  }
357
384
  }
358
385
 
359
- private paste(text: string): void {
386
+ /* Pastes as a paste from the clipboard does; false when a read-only editor refused. */
387
+ pasteText(text: string, options: EditorPasteOptions = {}): boolean {
360
388
  if (this.blockedByReadOnly()) {
361
- return;
389
+ return false;
362
390
  }
363
391
 
364
392
  this.unfoldCarets();
365
393
  this.historyGroup++;
366
- this.view.model.paste(this.native.normalizeNewlines(text), { ...this.commandOptions(), wholeLines: text !== '' && text === lineClip });
394
+
395
+ const pasted = this.view.model.paste(this.native.normalizeNewlines(text), {
396
+ ...this.commandOptions(),
397
+ ...(options.reindent === false ? { indentOnPaste: false } : {}),
398
+ wholeLines: options.wholeLines ?? (text !== '' && text === lineClip)
399
+ });
400
+
367
401
  this.historyGroup++;
368
402
  this.view.revealCaret();
403
+
404
+ return pasted;
405
+ }
406
+
407
+ /* Told what a copy or cut put on the clipboard. */
408
+ onCopy(listener: (copy: EditorCopy) => void): () => void {
409
+ return subscribe(this.copyListeners, listener);
369
410
  }
370
411
 
371
412
  private commandOptions(): CommandOptions {
@@ -381,15 +422,23 @@ export class InputController {
381
422
  tabOutOfClosers: keys.tabOutOfClosers,
382
423
  smartEnter: keys.smartIndentOnEnter,
383
424
  indentOnPaste: keys.indentOnPaste,
425
+ indentGuides: settings.guides,
384
426
  smartSemicolon: keys.smartSemicolon,
385
427
  smartArrow: keys.smartArrow,
386
428
  language: settings.language ?? 'plaintext',
387
429
  camelCase: keys.camelHumps,
388
430
  visualLine: (offset) => this.view.rowOf(offset),
389
- lineSpan: (line) => this.view.lineSpan(line)
431
+ lineSpan: (line) => this.view.lineSpan(line),
432
+ structure: () => this.view.structureRanges(),
433
+ ...(this.wordSources === null ? {} : { wordSources: this.wordSources })
390
434
  };
391
435
  }
392
436
 
437
+ /* The texts Hippie completion takes words from after those of this document; null takes them away. */
438
+ setWordSources(source: (() => readonly string[]) | null): void {
439
+ this.wordSources = source;
440
+ }
441
+
393
442
  /* Folding is the view's, since it knows where the ranges are. */
394
443
  private viewCommand(name: EditorViewCommand): boolean {
395
444
  const { view } = this;
@@ -400,11 +449,43 @@ export class InputController {
400
449
  return true;
401
450
  }
402
451
 
452
+ if (name === 'methodUp' || name === 'methodDown') {
453
+ const from = view.model.getPrimary().head;
454
+ const moved = view.goToMethod(name === 'methodUp' ? -1 : 1);
455
+
456
+ if (moved) {
457
+ emit(this.navigateListeners, from);
458
+ }
459
+
460
+ return moved;
461
+ }
462
+
463
+ if (name === 'pasteAsPlainText') {
464
+ // The clipboard is read once a person asks, and an editor with no way to read it does nothing.
465
+ if (this.blockedByReadOnly() || globalThis.navigator?.clipboard?.readText === undefined) {
466
+ return false;
467
+ }
468
+
469
+ void navigator.clipboard.readText().then(
470
+ (text) => this.pasteText(text, { reindent: false }),
471
+ () => undefined
472
+ );
473
+
474
+ return true;
475
+ }
476
+
477
+ if (name === 'nextChange' || name === 'previousChange') {
478
+ return view.goToChange(name === 'nextChange' ? 1 : -1);
479
+ }
480
+
403
481
  if (view.settings.folding === false && name !== 'toggleColumnMode') {
404
482
  return false;
405
483
  }
406
484
 
407
- const run: Record<Exclude<EditorViewCommand, keyof typeof ZOOM_STEPS>, () => void> = {
485
+ const run: Record<
486
+ Exclude<EditorViewCommand, keyof typeof ZOOM_STEPS | 'methodUp' | 'methodDown' | 'nextChange' | 'previousChange' | 'pasteAsPlainText'>,
487
+ () => void
488
+ > = {
408
489
  collapseRegion: () => view.collapseRegion(),
409
490
  expandRegion: () => view.expandRegion(),
410
491
  collapseAllRegions: () => view.foldAll(true),
@@ -529,7 +610,11 @@ export class InputController {
529
610
  }
530
611
  }
531
612
 
532
- const folded = name === 'moveLineUp' || name === 'moveLineDown' ? this.view.getFolds().collapsed : [];
613
+ const before = model.getPrimary().head;
614
+ const folded =
615
+ name === 'moveLineUp' || name === 'moveLineDown' || name === 'moveStatementUp' || name === 'moveStatementDown'
616
+ ? this.view.getFolds().collapsed
617
+ : [];
533
618
 
534
619
  const result = model.execute(name, {
535
620
  ...this.commandOptions(),
@@ -544,11 +629,24 @@ export class InputController {
544
629
  this.view.markRowEnd(model.getPrimary().head);
545
630
  }
546
631
 
632
+ if (name === 'hippieCompletion' || name === 'hippieBackwardCompletion') {
633
+ this.view.setHippieSource(model.hippieSource);
634
+ }
635
+
547
636
  this.view.revealCaret();
548
637
 
638
+ if (CODE_BLOCK_COMMANDS.has(name) && model.getPrimary().head !== before) {
639
+ emit(this.navigateListeners, before);
640
+ }
641
+
549
642
  return result;
550
643
  }
551
644
 
645
+ /* Told the offset the primary caret left when a command that navigates moved it. */
646
+ onNavigate(listener: (from: number) => void): () => void {
647
+ return subscribe(this.navigateListeners, listener);
648
+ }
649
+
552
650
  private typeText(text: string): void {
553
651
  if (this.blockedByReadOnly()) {
554
652
  return;
@@ -837,6 +935,8 @@ export class InputController {
837
935
  return;
838
936
  }
839
937
 
938
+ this.view.setHippieSource(null);
939
+
840
940
  const added = this.gesture.keydown(event);
841
941
 
842
942
  if (added !== null) {
@@ -933,6 +1033,7 @@ export class InputController {
933
1033
 
934
1034
  event.clipboardData?.setData('text/plain', text);
935
1035
  lineClip = empty ? text : null;
1036
+ emit(this.copyListeners, { text, wholeLines: empty, cut });
936
1037
 
937
1038
  if (!cut || this.blockedByReadOnly()) {
938
1039
  return;
package/src/editor.css CHANGED
@@ -652,11 +652,16 @@
652
652
  .se-find-match,
653
653
  .se-find-current,
654
654
  .se-occurrence,
655
+ .se-hippie-source,
655
656
  .se-bracket-match,
656
657
  .se-bracket-unmatched {
657
658
  position: absolute;
658
659
  border-radius: var(--radius-xs);
659
660
  }
661
+ .se-hippie-source {
662
+ background: color-mix(in srgb, var(--accent) 22%, transparent);
663
+ border-radius: var(--radius-sm);
664
+ }
660
665
  .se-find-match {
661
666
  background: var(--find-match);
662
667
  }
@@ -747,6 +752,14 @@
747
752
  white-space: pre;
748
753
  cursor: default;
749
754
  }
755
+ .se-swatch {
756
+ height: 12px;
757
+ margin-top: calc((var(--code-line-height, 20px) - 12px) / 2);
758
+ padding: 0;
759
+ border-radius: var(--radius-sm);
760
+ box-shadow: inset 0 0 0 1px var(--border);
761
+ cursor: pointer;
762
+ }
750
763
  .se-inlay-link {
751
764
  text-decoration: underline;
752
765
  text-underline-offset: 2px;
package/src/engine.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { changedSpans, DocumentModel, type EditorSnapshot, type FoldHints, replacementText } from '@adecore/editor-core';
2
2
  import { MAX_REPLACEMENT_CHARACTERS } from './search-protocol.ts';
3
+ import { placeAt, postfixOperands, surroundShape, unwrapTargets } from './text-queries.ts';
3
4
  import { GrammarPreparation } from './grammar-preparation.ts';
4
5
  import { InputController } from './controller.ts';
5
6
  import { resolveSmartKeys } from './smart-keys.ts';
@@ -17,6 +18,7 @@ import type {
17
18
  EditorCodeVision,
18
19
  EditorClickHandler,
19
20
  EditorContextMenu,
21
+ EditorCopy,
20
22
  EditorContentChange,
21
23
  EditorEngine,
22
24
  EditorFindQuery,
@@ -36,11 +38,16 @@ import type {
36
38
  EditorInlayHint,
37
39
  EditorInlayTarget,
38
40
  EditorKeyHandler,
41
+ EditorPasteOptions,
39
42
  EditorInputPopup,
40
43
  EditorMarker,
41
44
  EditorMessages,
42
45
  EditorOptions,
43
46
  EditorPosition,
47
+ EditorPostfixOperand,
48
+ EditorSurroundShape,
49
+ EditorTextPlace,
50
+ EditorUnwrapTarget,
44
51
  EditorRemoteCursor,
45
52
  EditorRange,
46
53
  EditorReveal,
@@ -90,6 +97,8 @@ class SmartEditor implements Editor {
90
97
  private lensRevisions = new Map<string, { signature: string; revision: number }>();
91
98
  private tokenizerRequest = 0;
92
99
  private settingText = false;
100
+ /* The hints of each owner, in the order the owners first set theirs. */
101
+ private readonly inlayOwners = new Map<string, readonly EditorInlayHint[]>();
93
102
  private disposed = false;
94
103
 
95
104
  private readonly engine: SmartEditorEngineOptions;
@@ -429,15 +438,21 @@ class SmartEditor implements Editor {
429
438
  );
430
439
  }
431
440
 
432
- setInlayHints(hints: readonly EditorInlayHint[]): void {
441
+ setInlayHints(hints: readonly EditorInlayHint[], owner: string = DEFAULT_WIDGET_OWNER): void {
442
+ if (hints.length === 0) {
443
+ this.inlayOwners.delete(owner);
444
+ } else {
445
+ this.inlayOwners.set(owner, hints);
446
+ }
433
447
  this.view.setInlays(
434
- hints.map((hint, index) => ({
448
+ [...this.inlayOwners.values()].flat().map((hint, index) => ({
435
449
  id: hint.id ?? `hint-${index}`,
436
450
  at: this.offsetAt(hint.position),
437
451
  text: hint.label,
438
452
  ...(hint.parts === undefined ? {} : { parts: hint.parts }),
439
453
  ...(hint.paddingLeft ? { gapLeft: INLAY_GAP } : {}),
440
- ...(hint.paddingRight ? { gapRight: INLAY_GAP } : {})
454
+ ...(hint.paddingRight ? { gapRight: INLAY_GAP } : {}),
455
+ ...(hint.swatch === undefined ? {} : { swatch: hint.swatch })
441
456
  }))
442
457
  );
443
458
  }
@@ -938,6 +953,30 @@ class SmartEditor implements Editor {
938
953
  this.view.setFoldHints(hints === null ? null : this.foldHintsByOffset(hints));
939
954
  }
940
955
 
956
+ setDeclarations(positions: readonly EditorPosition[] | null): void {
957
+ this.view.setDeclarations(positions === null ? null : positions.map((position) => this.offsetAt(position)));
958
+ }
959
+
960
+ onNavigate(listener: (from: EditorPosition) => void): () => void {
961
+ return this.controller.onNavigate((from) => listener(this.positionAt(from)));
962
+ }
963
+
964
+ hasFocus(): boolean {
965
+ return this.view.focused;
966
+ }
967
+
968
+ setWordSources(source: (() => readonly string[]) | null): void {
969
+ this.controller.setWordSources(source);
970
+ }
971
+
972
+ onCopy(listener: (copy: EditorCopy) => void): () => void {
973
+ return this.controller.onCopy(listener);
974
+ }
975
+
976
+ paste(text: string, options?: EditorPasteOptions): boolean {
977
+ return this.controller.pasteText(text, options);
978
+ }
979
+
941
980
  private foldHintsByOffset(hints: EditorFoldHints): FoldHints {
942
981
  const lineOf = (line: number): { start: number; end: number } => this.model.getLine(Math.min(line, this.model.getLineCount() - 1));
943
982
 
@@ -1033,6 +1072,24 @@ class SmartEditor implements Editor {
1033
1072
  return this.settings.readOnly;
1034
1073
  }
1035
1074
 
1075
+ placeAt(position: EditorPosition): EditorTextPlace {
1076
+ return placeAt(this.model, this, position, this.settings.language ?? 'plaintext');
1077
+ }
1078
+
1079
+ postfixOperands(dot: EditorPosition): readonly EditorPostfixOperand[] {
1080
+ return postfixOperands(this.model, this, dot, this.settings.language ?? 'plaintext');
1081
+ }
1082
+
1083
+ surroundShape(range: EditorRange): EditorSurroundShape | null {
1084
+ return surroundShape(this.model, this, range, this.settings.language ?? 'plaintext');
1085
+ }
1086
+
1087
+ unwrapTargets(position: EditorPosition): readonly EditorUnwrapTarget[] {
1088
+ const unit = this.settings.insertSpaces ? ' '.repeat(this.settings.tabSize) : '\t';
1089
+
1090
+ return unwrapTargets(this.model, this, position, this.settings.language ?? 'plaintext', unit);
1091
+ }
1092
+
1036
1093
  runCommand(command: EditorRunCommand): boolean {
1037
1094
  return this.controller.command(command);
1038
1095
  }
package/src/fake.ts CHANGED
@@ -34,6 +34,10 @@ import type {
34
34
  EditorMessages,
35
35
  EditorOptions,
36
36
  EditorPosition,
37
+ EditorPostfixOperand,
38
+ EditorSurroundShape,
39
+ EditorTextPlace,
40
+ EditorUnwrapTarget,
37
41
  EditorRemoteCursor,
38
42
  EditorRange,
39
43
  EditorReveal,
@@ -46,6 +50,8 @@ import type {
46
50
  EditorSemanticToken,
47
51
  EditorSmartKeys,
48
52
  EditorApplyEditsOptions,
53
+ EditorCopy,
54
+ EditorPasteOptions,
49
55
  EditorTextChange,
50
56
  EditorTrackedRange,
51
57
  EditorTheme,
@@ -53,7 +59,8 @@ import type {
53
59
  EditorWidget
54
60
  } from './types.ts';
55
61
  import { emit, type Listener, subscribe } from './listeners.ts';
56
- import { changedSpan, changedSpans } from '@adecore/editor-core';
62
+ import { changedSpan, changedSpans, DocumentModel } from '@adecore/editor-core';
63
+ import { placeAt, postfixOperands, surroundShape, unwrapTargets } from './text-queries.ts';
57
64
  import { mapTrackedRange } from './tracked-range.ts';
58
65
 
59
66
  interface OffsetSelection {
@@ -823,6 +830,78 @@ export class FakeEditor implements Editor {
823
830
  this.foldHints = hints;
824
831
  }
825
832
 
833
+ declarations: readonly EditorPosition[] | null = null;
834
+
835
+ setDeclarations(positions: readonly EditorPosition[] | null): void {
836
+ this.declarations = positions;
837
+ }
838
+
839
+ private readonly navigations = new Set<(from: EditorPosition) => void>();
840
+
841
+ onNavigate(listener: (from: EditorPosition) => void): () => void {
842
+ return subscribe(this.navigations, listener);
843
+ }
844
+
845
+ /* Stands for a command that navigated: tells the listeners the caret left `from`. */
846
+ navigate(from: EditorPosition): void {
847
+ emit(this.navigations, from);
848
+ }
849
+
850
+ hasFocus(): boolean {
851
+ return this.focused;
852
+ }
853
+
854
+ private readonly copies = new Set<(copy: EditorCopy) => void>();
855
+
856
+ onCopy(listener: (copy: EditorCopy) => void): () => void {
857
+ return subscribe(this.copies, listener);
858
+ }
859
+
860
+ /* Stands for a copy or cut of a person: tells the listeners what went on the clipboard. */
861
+ copy(text: string, wholeLines = false, cut = false): void {
862
+ emit(this.copies, { text, wholeLines, cut });
863
+ }
864
+
865
+ /* Every paste a host asked for, with its options. */
866
+ readonly pastes: Array<{ text: string; options: EditorPasteOptions | undefined }> = [];
867
+
868
+ /* Puts the text at every caret, one line per caret when the lines are as many as the carets, and leaves the carets behind it. */
869
+ paste(text: string, options?: EditorPasteOptions): boolean {
870
+ this.assertLive();
871
+ this.pastes.push({ text, options });
872
+
873
+ if (this.readOnly || text === '') {
874
+ return false;
875
+ }
876
+
877
+ const lines = text.split('\n');
878
+ const selections = [...this.selections].sort((left, right) => Math.min(left.anchor, left.head) - Math.min(right.anchor, right.head));
879
+ const perCaret = lines.length === selections.length;
880
+
881
+ const edits = selections.map((selection, index) => ({
882
+ from: Math.min(selection.anchor, selection.head),
883
+ to: Math.max(selection.anchor, selection.head),
884
+ text: perCaret ? lines[index]! : text
885
+ }));
886
+
887
+ return this.edit(edits, 'command', false, () => {
888
+ let shift = 0;
889
+
890
+ return edits.map((edit) => {
891
+ const caret = edit.from + shift + edit.text.length;
892
+ shift += edit.text.length - (edit.to - edit.from);
893
+
894
+ return { anchor: caret, head: caret };
895
+ });
896
+ });
897
+ }
898
+
899
+ wordSources: (() => readonly string[]) | null = null;
900
+
901
+ setWordSources(source: (() => readonly string[]) | null): void {
902
+ this.wordSources = source;
903
+ }
904
+
826
905
  setSmartKeys(keys: Partial<EditorSmartKeys>): void {
827
906
  this.smartKeys = keys;
828
907
  }
@@ -847,11 +926,17 @@ export class FakeEditor implements Editor {
847
926
  this.markers = markers;
848
927
  }
849
928
 
850
- /* What the host drew as hints last. */
929
+ /* What the host drew as hints last, every owner's together. */
851
930
  inlayHints: readonly EditorInlayHint[] = [];
931
+ private readonly inlayOwners = new Map<string, readonly EditorInlayHint[]>();
852
932
 
853
- setInlayHints(hints: readonly EditorInlayHint[]): void {
854
- this.inlayHints = hints;
933
+ setInlayHints(hints: readonly EditorInlayHint[], owner = 'default'): void {
934
+ if (hints.length === 0) {
935
+ this.inlayOwners.delete(owner);
936
+ } else {
937
+ this.inlayOwners.set(owner, hints);
938
+ }
939
+ this.inlayHints = [...this.inlayOwners.values()].flat();
855
940
  }
856
941
 
857
942
  private readonly inlayHovers = new Set<(target: EditorInlayTarget | null) => void>();
@@ -1190,6 +1275,24 @@ export class FakeEditor implements Editor {
1190
1275
  return this.readOnly;
1191
1276
  }
1192
1277
 
1278
+ placeAt(position: EditorPosition): EditorTextPlace {
1279
+ return placeAt(new DocumentModel(this.text), this, position, this.language ?? 'plaintext');
1280
+ }
1281
+
1282
+ postfixOperands(dot: EditorPosition): readonly EditorPostfixOperand[] {
1283
+ return postfixOperands(new DocumentModel(this.text), this, dot, this.language ?? 'plaintext');
1284
+ }
1285
+
1286
+ surroundShape(range: EditorRange): EditorSurroundShape | null {
1287
+ return surroundShape(new DocumentModel(this.text), this, range, this.language ?? 'plaintext');
1288
+ }
1289
+
1290
+ unwrapTargets(position: EditorPosition): readonly EditorUnwrapTarget[] {
1291
+ const unit = this.indentation.insertSpaces ? ' '.repeat(this.indentation.tabSize) : '\t';
1292
+
1293
+ return unwrapTargets(new DocumentModel(this.text), this, position, this.language ?? 'plaintext', unit);
1294
+ }
1295
+
1193
1296
  /* The commands run, in order; of its own the fake carries out only undo and redo. */
1194
1297
  readonly commands: EditorRunCommand[] = [];
1195
1298
 
@@ -1208,6 +1311,8 @@ export class FakeEditor implements Editor {
1208
1311
  this.changes.clear();
1209
1312
  this.textChanges.clear();
1210
1313
  this.carets.clear();
1314
+ this.navigations.clear();
1315
+ this.copies.clear();
1211
1316
  this.hovers.clear();
1212
1317
  this.inlayHovers.clear();
1213
1318
  this.inlayHandlers.clear();
package/src/index.ts CHANGED
@@ -3,7 +3,9 @@ export { AGENT_COLORS } from './attribution.ts';
3
3
  export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from '@adecore/editor-core';
4
4
  export { DEFAULT_TODO_WORDS, findTodos, type TodoMatch, type TodoOptions } from '@adecore/editor-core';
5
5
  export { detectIndentation, type DetectedIndentation } from '@adecore/editor-core';
6
+ export { commentSyntax, type CommentSyntax } from '@adecore/editor-core';
6
7
  export { createSmartEditorEngine } from './engine.ts';
8
+ export { rememberCopy } from './controller.ts';
7
9
  export { DEFAULT_SMART_KEYS } from './smart-keys.ts';
8
10
  export { shikiScopeColors, shikiTokenizers } from './shiki.ts';
9
11
 
@@ -61,6 +63,12 @@ export type {
61
63
  EditorRemoteCursor,
62
64
  EditorRange,
63
65
  EditorApplyEditsOptions,
66
+ EditorCopy,
67
+ EditorPasteOptions,
68
+ EditorPostfixOperand,
69
+ EditorSurroundShape,
70
+ EditorTextPlace,
71
+ EditorUnwrapTarget,
64
72
  EditorTextChange,
65
73
  EditorTrackedRange,
66
74
  EditorRunCommand,
@@ -33,10 +33,15 @@ export const KEYMAP = {
33
33
  deleteLine: bind('Mod+Backspace', 'Mod+Y'),
34
34
  moveLineUp: bind('Alt+Shift+ArrowUp'),
35
35
  moveLineDown: bind('Alt+Shift+ArrowDown'),
36
+ moveStatementUp: bind('Mod+Shift+ArrowUp'),
37
+ moveStatementDown: bind('Mod+Shift+ArrowDown'),
36
38
  toggleLineComment: bind('Mod+/'),
37
39
  toggleBlockComment: bind('Mod+Alt+/', 'Mod+Shift+/'),
38
40
  joinLines: bind('Ctrl+Shift+J'),
39
41
  startNewLine: bind('Shift+Enter'),
42
+ completeCurrentStatement: bind('Mod+Shift+Enter'),
43
+ hippieCompletion: bind('Alt+/'),
44
+ hippieBackwardCompletion: bind('Alt+Shift+/'),
40
45
  startNewLineBefore: bind('Mod+Alt+Enter'),
41
46
  splitLine: bind('Mod+Enter'),
42
47
  toggleCase: bind('Mod+Shift+U'),
@@ -49,8 +54,21 @@ export const KEYMAP = {
49
54
  convertIndentsToTabs: bind(null),
50
55
 
51
56
  expandSelection: bind('Alt+ArrowUp', 'Ctrl+W'),
52
- shrinkSelection: bind('Alt+ArrowDown'),
57
+ // Off macOS Alt+ArrowDown is Next Method, as in IntelliJ, so Shrink Selection takes its own chord there.
58
+ shrinkSelection: bind('Alt+ArrowDown', 'Ctrl+Shift+W'),
53
59
  matchBrace: bind('Ctrl+M', 'Ctrl+Shift+M'),
60
+ methodUp: bind('Ctrl+Shift+ArrowUp', 'Alt+ArrowUp'),
61
+ methodDown: bind('Ctrl+Shift+ArrowDown', 'Alt+ArrowDown'),
62
+ lastEditLocation: bind('Mod+Shift+Backspace'),
63
+ nextEditLocation: bind(null),
64
+ pasteFromHistory: bind('Mod+Shift+V'),
65
+ pasteAsPlainText: bind('Mod+Alt+Shift+V'),
66
+ nextChange: bind('Ctrl+Alt+Shift+ArrowDown'),
67
+ previousChange: bind('Ctrl+Alt+Shift+ArrowUp'),
68
+ codeBlockStart: bind('Mod+Alt+[', 'Ctrl+['),
69
+ codeBlockEnd: bind('Mod+Alt+]', 'Ctrl+]'),
70
+ codeBlockStartWithSelection: bind('Mod+Alt+Shift+[', 'Ctrl+Shift+['),
71
+ codeBlockEndWithSelection: bind('Mod+Alt+Shift+]', 'Ctrl+Shift+]'),
54
72
 
55
73
  selectNextOccurrence: bind('Ctrl+G', 'Alt+J'),
56
74
  unselectOccurrence: bind('Ctrl+Shift+G', 'Alt+Shift+J'),
@@ -72,9 +90,10 @@ export const KEYMAP = {
72
90
  smartEnd: bind('Meta+ArrowRight', null),
73
91
  selectSmartEnd: bind('Meta+Shift+ArrowRight', null),
74
92
  textStart: bind('Meta+ArrowUp', null),
75
- selectTextStart: bind('Meta+Shift+ArrowUp', null),
93
+ // IntelliJ binds Cmd+Shift+ArrowUp and ArrowDown to Move Statement on macOS, where Cmd+Shift+Home and End still select to the text edges.
94
+ selectTextStart: bind(null),
76
95
  textEnd: bind('Meta+ArrowDown', null),
77
- selectTextEnd: bind('Meta+Shift+ArrowDown', null),
96
+ selectTextEnd: bind(null),
78
97
 
79
98
  collapse: bind('Mod+-'),
80
99
  expand: bind('Mod+='),
@@ -104,6 +123,13 @@ export const KEYMAP = {
104
123
  renameSymbol: bind('Shift+F6'),
105
124
  organizeImports: bind('Ctrl+Alt+O'),
106
125
  formatDocument: bind('Mod+Alt+L'),
126
+ insertLiveTemplate: bind('Mod+J'),
127
+ surroundWith: bind('Mod+Alt+T'),
128
+ surroundWithLiveTemplate: bind('Mod+Alt+J'),
129
+ unwrap: bind('Mod+Shift+Delete'),
130
+ saveAsLiveTemplate: bind(null),
131
+ // Surround with Emmet has no chord of its own: it is a row of Surround with Live Template, which has this one.
132
+ wrapWithAbbreviation: bind(null),
107
133
  selectionToChat: bind(null),
108
134
  inlineEdit: bind(null),
109
135
  suggestInline: bind(null),
@@ -116,6 +142,7 @@ export const KEYMAP = {
116
142
  peekDefinition: bind('Alt+Space', 'Ctrl+Shift+I'),
117
143
  peekReferences: bind('Alt+F7'),
118
144
  callHierarchy: bind('Ctrl+Alt+H'),
145
+ typeHierarchy: bind('Ctrl+H'),
119
146
  toggleRenderedDocs: bind('Ctrl+Alt+Q'),
120
147
  historyBack: bind('Mod+[', 'Ctrl+Alt+ArrowLeft'),
121
148
  historyForward: bind('Mod+]', 'Ctrl+Alt+ArrowRight'),
@@ -142,7 +169,31 @@ export type OptionalKeymapId =
142
169
  | 'reverseLines'
143
170
  | 'uniqueLines'
144
171
  | 'convertIndentsToSpaces'
145
- | 'convertIndentsToTabs';
172
+ | 'convertIndentsToTabs'
173
+ | 'completeCurrentStatement'
174
+ | 'hippieCompletion'
175
+ | 'hippieBackwardCompletion'
176
+ | 'moveStatementUp'
177
+ | 'moveStatementDown'
178
+ | 'lastEditLocation'
179
+ | 'nextEditLocation'
180
+ | 'methodUp'
181
+ | 'methodDown'
182
+ | 'pasteFromHistory'
183
+ | 'pasteAsPlainText'
184
+ | 'nextChange'
185
+ | 'previousChange'
186
+ | 'codeBlockStart'
187
+ | 'codeBlockEnd'
188
+ | 'codeBlockStartWithSelection'
189
+ | 'codeBlockEndWithSelection'
190
+ | 'wrapWithAbbreviation'
191
+ | 'insertLiveTemplate'
192
+ | 'surroundWith'
193
+ | 'surroundWithLiveTemplate'
194
+ | 'unwrap'
195
+ | 'saveAsLiveTemplate'
196
+ | 'typeHierarchy';
146
197
 
147
198
  export type Keymap = Readonly<Record<Exclude<KeymapId, OptionalKeymapId>, KeyBinding> & Partial<Record<OptionalKeymapId, KeyBinding>>>;
148
199
  export type KeymapOverrides = Partial<Record<KeymapId, Partial<KeyBinding>>>;