@adecore/editor 0.22.1 → 0.23.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 (94) hide show
  1. package/dist/attribution.d.ts +9 -8
  2. package/dist/attribution.js +44 -9
  3. package/dist/bidi.d.ts +44 -0
  4. package/dist/bidi.js +26 -0
  5. package/dist/column-selection.js +2 -1
  6. package/dist/controller.d.ts +6 -1
  7. package/dist/controller.js +93 -3
  8. package/dist/editor.css +81 -1
  9. package/dist/engine.js +184 -41
  10. package/dist/fake.d.ts +43 -5
  11. package/dist/fake.js +117 -13
  12. package/dist/find-search.d.ts +26 -0
  13. package/dist/find-search.js +130 -0
  14. package/dist/find.d.ts +50 -2
  15. package/dist/find.js +575 -25
  16. package/dist/geometry-stops.d.ts +6 -0
  17. package/dist/geometry-stops.js +47 -0
  18. package/dist/grammar-preparation.d.ts +29 -0
  19. package/dist/grammar-preparation.js +147 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +2 -0
  22. package/dist/keymap-table.d.ts +10 -1
  23. package/dist/keymap-table.js +37 -1
  24. package/dist/keymap.d.ts +1 -0
  25. package/dist/keymap.js +4 -1
  26. package/dist/layout-changes.d.ts +6 -0
  27. package/dist/layout-changes.js +27 -0
  28. package/dist/layout.d.ts +45 -2
  29. package/dist/layout.js +202 -58
  30. package/dist/offsets.d.ts +1 -0
  31. package/dist/offsets.js +23 -0
  32. package/dist/outline.js +5 -4
  33. package/dist/paint.d.ts +11 -3
  34. package/dist/paint.js +180 -34
  35. package/dist/search-client.d.ts +27 -0
  36. package/dist/search-client.js +210 -0
  37. package/dist/search-protocol.d.ts +103 -0
  38. package/dist/search-protocol.js +3 -0
  39. package/dist/search-worker.d.ts +11 -0
  40. package/dist/search-worker.js +137 -0
  41. package/dist/semantic.d.ts +2 -1
  42. package/dist/semantic.js +6 -1
  43. package/dist/shiki-document.d.ts +8 -0
  44. package/dist/shiki-document.js +93 -0
  45. package/dist/shiki-worker.d.ts +65 -0
  46. package/dist/shiki-worker.js +265 -0
  47. package/dist/shiki.d.ts +1 -0
  48. package/dist/shiki.js +96 -33
  49. package/dist/testing.d.ts +1 -0
  50. package/dist/tokens.d.ts +2 -0
  51. package/dist/tokens.js +37 -7
  52. package/dist/touch.d.ts +27 -0
  53. package/dist/touch.js +100 -0
  54. package/dist/types.d.ts +84 -2
  55. package/dist/uniform-geometry.d.ts +7 -0
  56. package/dist/uniform-geometry.js +49 -0
  57. package/dist/view.d.ts +68 -17
  58. package/dist/view.js +424 -97
  59. package/dist/worker-client.d.ts +37 -0
  60. package/dist/worker-client.js +195 -0
  61. package/package.json +17 -2
  62. package/src/attribution.ts +50 -14
  63. package/src/bidi.ts +32 -0
  64. package/src/column-selection.ts +2 -1
  65. package/src/controller.ts +97 -3
  66. package/src/editor.css +81 -1
  67. package/src/engine.ts +216 -42
  68. package/src/fake.ts +152 -14
  69. package/src/find-search.ts +165 -0
  70. package/src/find.ts +622 -21
  71. package/src/geometry-stops.ts +51 -0
  72. package/src/grammar-preparation.ts +158 -0
  73. package/src/index.ts +10 -0
  74. package/src/keymap-table.ts +44 -2
  75. package/src/keymap.ts +5 -1
  76. package/src/layout-changes.ts +33 -0
  77. package/src/layout.ts +262 -63
  78. package/src/offsets.ts +24 -0
  79. package/src/outline.ts +5 -4
  80. package/src/paint.ts +202 -36
  81. package/src/search-client.ts +306 -0
  82. package/src/search-protocol.ts +100 -0
  83. package/src/search-worker.ts +171 -0
  84. package/src/semantic.ts +8 -2
  85. package/src/shiki-document.ts +143 -0
  86. package/src/shiki-worker.ts +344 -0
  87. package/src/shiki.ts +103 -33
  88. package/src/testing.ts +2 -0
  89. package/src/tokens.ts +36 -7
  90. package/src/touch.ts +134 -0
  91. package/src/types.ts +174 -8
  92. package/src/uniform-geometry.ts +57 -0
  93. package/src/view.ts +500 -102
  94. package/src/worker-client.ts +212 -0
package/dist/fake.js CHANGED
@@ -76,7 +76,7 @@ export class FakeEditor {
76
76
  this.replaceText(text);
77
77
  }
78
78
  /* The text becomes `text`, and listeners of the text hear the one stretch that differs, as the real editor reports it. */
79
- replaceText(text, source = 'external') {
79
+ replaceText(text, source = 'external', selections) {
80
80
  const span = changedSpan(this.text, text);
81
81
  const before = this.text;
82
82
  this.text = text;
@@ -85,7 +85,12 @@ export class FakeEditor {
85
85
  }
86
86
  // As in the real editor the caret lands behind what was typed, and listeners of the caret hear it before those of the text.
87
87
  if (source !== 'external') {
88
- this.moveCaret(positionIn(text, span.start + span.text.length));
88
+ if (selections === undefined) {
89
+ this.moveCaret(positionIn(text, span.start + span.text.length));
90
+ }
91
+ else {
92
+ this.setSelections(selections);
93
+ }
89
94
  }
90
95
  this.followTracked(span.start, span.end, span.text.length);
91
96
  this.ghost = null;
@@ -180,6 +185,9 @@ export class FakeEditor {
180
185
  this.replaceOptions.push(options);
181
186
  return this.readOnly ? 0 : this.findState.count;
182
187
  }
188
+ async replaceAllAsync(replacement, options = {}) {
189
+ return this.replaceAll(replacement, options);
190
+ }
183
191
  setReplacePreview(replacement) {
184
192
  this.replacePreview = replacement;
185
193
  }
@@ -210,10 +218,18 @@ export class FakeEditor {
210
218
  /* What the host handed it last; null while it reads the structure itself. */
211
219
  blocks = null;
212
220
  scopes = new Set();
213
- /* What the host marked as changed last. */
214
- changeMarks = [];
215
- setChangeMarks(marks) {
216
- this.changeMarks = marks;
221
+ /* The marks of every layer, in the order the layers last became non-empty. */
222
+ get changeMarks() {
223
+ return [...this.changeLayers.values()].flat();
224
+ }
225
+ changeLayers = new Map();
226
+ setChangeMarks(marks, layer = '') {
227
+ if (marks.length === 0) {
228
+ this.changeLayers.delete(layer);
229
+ }
230
+ else {
231
+ this.changeLayers.set(layer, marks);
232
+ }
217
233
  }
218
234
  /* The tinted lines the host set last. */
219
235
  lineHighlights = [];
@@ -222,6 +238,32 @@ export class FakeEditor {
222
238
  }
223
239
  /* What the host drew as code outside the document: the text and how, and into which element. */
224
240
  codeBlocks = [];
241
+ lineNumbers = null;
242
+ lineTokens = null;
243
+ setLineNumbers(labels) {
244
+ this.lineNumbers = labels;
245
+ }
246
+ lineLabels = null;
247
+ setLineLabels(labels) {
248
+ this.lineLabels = labels;
249
+ }
250
+ /* What the input points at, as the last call left it. */
251
+ inputPopup = null;
252
+ setInputPopup(popup) {
253
+ this.inputPopup = popup;
254
+ }
255
+ /* Every sentence said, oldest first. */
256
+ announcements = [];
257
+ announce(message) {
258
+ this.announcements.push(message);
259
+ }
260
+ messages = {};
261
+ setMessages(messages) {
262
+ this.messages = { ...this.messages, ...messages };
263
+ }
264
+ setLineTokens(tokens) {
265
+ this.lineTokens = tokens;
266
+ }
225
267
  renderCode(container, text, options = {}) {
226
268
  this.codeBlocks.push({ container, text, options });
227
269
  }
@@ -265,11 +307,20 @@ export class FakeEditor {
265
307
  const before = this.drawnWidgets.get(owner) ?? new Set();
266
308
  for (const widget of widgets) {
267
309
  if (!before.has(widget.id)) {
268
- widget.render({});
310
+ widget.render(this.element.ownerDocument?.createElement('div') ?? {});
269
311
  }
270
312
  }
271
313
  this.drawnWidgets.set(owner, new Set(widgets.map((widget) => widget.id)));
272
314
  }
315
+ replacementsByOwner = new Map();
316
+ setReplacements(replacements, owner = 'default') {
317
+ if (replacements.length === 0) {
318
+ this.replacementsByOwner.delete(owner);
319
+ }
320
+ else {
321
+ this.replacementsByOwner.set(owner, replacements);
322
+ }
323
+ }
273
324
  lineActionsByOwner = new Map();
274
325
  setLineActions(actions, owner = 'default') {
275
326
  if (actions.length === 0) {
@@ -367,6 +418,7 @@ export class FakeEditor {
367
418
  charSize = { width: 8, height: 20 };
368
419
  carets = new Set();
369
420
  hovers = new Set();
421
+ lineHeights = new Set();
370
422
  views = new Set();
371
423
  keys = new Set();
372
424
  setMarkers(markers) {
@@ -377,6 +429,28 @@ export class FakeEditor {
377
429
  setInlayHints(hints) {
378
430
  this.inlayHints = hints;
379
431
  }
432
+ inlayHovers = new Set();
433
+ inlayHandlers = new Set();
434
+ /* The hint, or part of one, the last `setInlayLink` drew as a link. */
435
+ inlayLink = null;
436
+ onInlayHover(listener) {
437
+ return subscribe(this.inlayHovers, listener);
438
+ }
439
+ /* The pointer resting on a hint or a part of one, or leaving them. */
440
+ hoverInlay(id, part = null) {
441
+ emit(this.inlayHovers, id === null ? null : { id, part, rect: { left: 0, top: 0, right: 40, bottom: 20 } });
442
+ }
443
+ onInlayClick(handler) {
444
+ return subscribe(this.inlayHandlers, handler);
445
+ }
446
+ /* A press on a hint as the editor sees it first; true when a handler took it. */
447
+ clickInlay(click) {
448
+ const full = { part: null, mod: false, alt: false, shift: false, clicks: 1, touch: false, ...click };
449
+ return [...this.inlayHandlers].some((handler) => handler(full));
450
+ }
451
+ setInlayLink(target) {
452
+ this.inlayLink = target;
453
+ }
380
454
  /* The suggestion after the caret now, which any change of the text takes away as the real editor does. */
381
455
  ghost = null;
382
456
  setGhostText(ghost) {
@@ -408,6 +482,26 @@ export class FakeEditor {
408
482
  }
409
483
  /* The selection a person made; without one the caret. */
410
484
  selection = null;
485
+ /* A test supplies the measured height; this fake has no layout. */
486
+ contentHeight = 0;
487
+ setScrollTop(top) {
488
+ if (Number.isFinite(top) && this.getScrollTop() !== Math.max(0, top)) {
489
+ this.scrollTop = Math.max(0, top);
490
+ emit(this.views);
491
+ }
492
+ }
493
+ getLineHeight(line) {
494
+ return Number.isInteger(line) && line >= 0 && line < this.text.split('\n').length ? this.charSize.height : 0;
495
+ }
496
+ getBaseLineHeight() {
497
+ return this.charSize.height;
498
+ }
499
+ onLineHeights(listener) {
500
+ return subscribe(this.lineHeights, listener);
501
+ }
502
+ getContentHeight() {
503
+ return this.contentHeight;
504
+ }
411
505
  getSelection() {
412
506
  return this.selection ?? { start: this.caret, end: this.caret };
413
507
  }
@@ -512,7 +606,8 @@ export class FakeEditor {
512
606
  const full = { ctrlKey: false, metaKey: false, altKey: false, shiftKey: false, preventDefault: () => undefined, ...event };
513
607
  return [...this.keys].some((handler) => handler(full));
514
608
  }
515
- applyEdits(edits) {
609
+ /* The fake keeps no undo history, so `mergeUndo` changes nothing here. It keeps no selection direction either, so a test of `mapSelections` keeping a backward selection needs the real editor. */
610
+ applyEdits(edits, options = {}) {
516
611
  if (this.readOnly || edits.length === 0) {
517
612
  return false;
518
613
  }
@@ -539,11 +634,14 @@ export class FakeEditor {
539
634
  return offset + delta;
540
635
  };
541
636
  const moved = others.map((range) => ({ anchor: follow(range.anchor), head: follow(range.head) }));
542
- this.replaceText(text, 'command');
543
- this.otherSelections = moved.map((range) => ({
544
- start: positionIn(text, Math.min(range.anchor, range.head)),
545
- end: positionIn(text, Math.max(range.anchor, range.head))
546
- }));
637
+ const mapped = options.mapSelections && this.getSelections().map((range) => options.mapSelections(range));
638
+ this.replaceText(text, 'command', mapped);
639
+ if (mapped === undefined) {
640
+ this.otherSelections = moved.map((range) => ({
641
+ start: positionIn(text, Math.min(range.anchor, range.head)),
642
+ end: positionIn(text, Math.max(range.anchor, range.head))
643
+ }));
644
+ }
547
645
  emit(this.changes);
548
646
  return true;
549
647
  }
@@ -557,6 +655,9 @@ export class FakeEditor {
557
655
  this.readOnly = readOnly;
558
656
  this.readOnlyReason = reason;
559
657
  }
658
+ isReadOnly() {
659
+ return this.readOnly;
660
+ }
560
661
  /* The commands run, in order; the fake has no commands of its own to carry out. */
561
662
  commands = [];
562
663
  runCommand(command) {
@@ -572,8 +673,11 @@ export class FakeEditor {
572
673
  this.textChanges.clear();
573
674
  this.carets.clear();
574
675
  this.hovers.clear();
676
+ this.inlayHovers.clear();
677
+ this.inlayHandlers.clear();
575
678
  this.attributionHovers.clear();
576
679
  this.views.clear();
680
+ this.lineHeights.clear();
577
681
  this.keys.clear();
578
682
  this.saves.clear();
579
683
  this.blurs.clear();
@@ -0,0 +1,26 @@
1
+ import { type EditorSearchClient } from './search-client.ts';
2
+ export interface FindSearchOptions {
3
+ readonly createClient: () => EditorSearchClient | null;
4
+ readonly schedule?: (callback: () => void, delay: number) => () => void;
5
+ readonly now?: () => number;
6
+ }
7
+ type SearchKind = 'live' | 'preview' | 'command';
8
+ export declare function scheduleFind(callback: () => void, delay: number): () => void;
9
+ export declare class FindSearchQueue {
10
+ private client;
11
+ private active;
12
+ private pending;
13
+ private cancelTimer;
14
+ private started;
15
+ private disposed;
16
+ private epoch;
17
+ private readonly options;
18
+ constructor(options: FindSearchOptions);
19
+ get busy(): boolean;
20
+ request<Result>(run: (client: EditorSearchClient, isCurrent: () => boolean) => Promise<Result>, kind?: SearchKind, valid?: () => boolean): Promise<Result>;
21
+ private now;
22
+ private pump;
23
+ release(): void;
24
+ dispose(): void;
25
+ }
26
+ export {};
@@ -0,0 +1,130 @@
1
+ import { EditorSearchError } from './search-client.js';
2
+ export function scheduleFind(callback, delay) {
3
+ const timer = setTimeout(callback, delay);
4
+ return () => clearTimeout(timer);
5
+ }
6
+ function superseded() {
7
+ return new EditorSearchError('superseded', 'The find operation is no longer current.');
8
+ }
9
+ /* Commands finish atomically; short background requests reuse the worker before newer work starts. */
10
+ export class FindSearchQueue {
11
+ client;
12
+ active;
13
+ pending = [];
14
+ cancelTimer;
15
+ started = 0;
16
+ disposed = false;
17
+ epoch = 0;
18
+ options;
19
+ constructor(options) {
20
+ this.options = options;
21
+ }
22
+ get busy() {
23
+ return this.active !== undefined || this.pending.length > 0;
24
+ }
25
+ request(run, kind = 'live', valid) {
26
+ if (this.disposed) {
27
+ return Promise.reject(new EditorSearchError('disposed', 'The find session was disposed.'));
28
+ }
29
+ return new Promise((resolve, reject) => {
30
+ this.pending = this.pending.filter((job) => {
31
+ if (job.kind !== 'command' && (job.kind === kind || (kind !== 'preview' && job.kind === 'preview'))) {
32
+ job.cancelled = true;
33
+ job.reject(superseded());
34
+ return false;
35
+ }
36
+ return true;
37
+ });
38
+ this.pending.push({ kind, valid, run, resolve: (value) => resolve(value), reject, cancelled: false });
39
+ this.pending.sort((left, right) => priority(left.kind) - priority(right.kind));
40
+ if (this.active === undefined) {
41
+ this.pump();
42
+ }
43
+ else if ((kind !== 'preview' || this.active.valid?.() === false) &&
44
+ (this.active.kind !== 'command' || this.active.valid?.() === false) &&
45
+ this.cancelTimer === undefined) {
46
+ this.cancelTimer = (this.options.schedule ?? scheduleFind)(() => {
47
+ this.cancelTimer = undefined;
48
+ if (this.pending.length > 0 && this.active !== undefined && (this.active.kind !== 'command' || this.active.valid?.() === false)) {
49
+ this.active.cancelled = true;
50
+ this.active.reject(superseded());
51
+ this.client?.cancel();
52
+ }
53
+ }, Math.max(0, 200 - (this.now() - this.started)));
54
+ }
55
+ });
56
+ }
57
+ now() {
58
+ return this.options.now?.() ?? performance.now();
59
+ }
60
+ pump() {
61
+ if (this.disposed || this.active !== undefined) {
62
+ return;
63
+ }
64
+ const job = this.pending.shift();
65
+ if (job === undefined) {
66
+ return;
67
+ }
68
+ this.active = job;
69
+ this.started = this.now();
70
+ const epoch = this.epoch;
71
+ const alive = () => !this.disposed && epoch === this.epoch && !job.cancelled;
72
+ const isCurrent = () => alive() && job.valid?.() !== false;
73
+ void Promise.resolve()
74
+ .then(() => {
75
+ if (!isCurrent()) {
76
+ throw superseded();
77
+ }
78
+ this.client ??= this.options.createClient();
79
+ if (this.client === null) {
80
+ throw new EditorSearchError('unavailable', 'Regex search needs a search worker.');
81
+ }
82
+ const client = this.client;
83
+ const guarded = {
84
+ search: (...args) => (isCurrent() ? client.search(...args) : Promise.reject(superseded())),
85
+ match: (...args) => (isCurrent() ? client.match(...args) : Promise.reject(superseded())),
86
+ next: (...args) => (isCurrent() ? client.next(...args) : Promise.reject(superseded())),
87
+ replacements: (...args) => (isCurrent() ? client.replacements(...args) : Promise.reject(superseded())),
88
+ cancel: () => client.cancel(),
89
+ release: () => client.release(),
90
+ dispose: () => client.dispose()
91
+ };
92
+ return job.run(guarded, isCurrent);
93
+ })
94
+ .then((value) => {
95
+ if (alive()) {
96
+ job.resolve(value);
97
+ }
98
+ else {
99
+ job.reject(superseded());
100
+ }
101
+ }, job.reject)
102
+ .finally(() => {
103
+ this.active = undefined;
104
+ this.cancelTimer?.();
105
+ this.cancelTimer = undefined;
106
+ this.pump();
107
+ });
108
+ }
109
+ release() {
110
+ this.epoch++;
111
+ this.active?.reject(superseded());
112
+ for (const job of this.pending) {
113
+ job.cancelled = true;
114
+ job.reject(superseded());
115
+ }
116
+ this.pending = [];
117
+ this.cancelTimer?.();
118
+ this.cancelTimer = undefined;
119
+ this.client?.release();
120
+ }
121
+ dispose() {
122
+ this.disposed = true;
123
+ this.release();
124
+ this.client?.dispose();
125
+ this.client = undefined;
126
+ }
127
+ }
128
+ function priority(kind) {
129
+ return kind === 'command' ? 0 : kind === 'live' ? 1 : 2;
130
+ }
package/dist/find.d.ts CHANGED
@@ -1,18 +1,45 @@
1
- import type { DocumentChange, DocumentModel } from '@adecore/editor-core';
1
+ import type { DocumentChange, DocumentModel, FindMatch } from '@adecore/editor-core';
2
+ import { type FindSearchOptions } from './find-search.ts';
2
3
  import type { EditorFindQuery, EditorFindState } from './types.ts';
3
4
  export interface FindMark {
4
5
  from: number;
5
6
  to: number;
6
7
  }
7
8
  export declare const FIND_LIMIT = 10000;
9
+ export interface FindControllerOptions extends FindSearchOptions {
10
+ readonly changed: (reveal?: boolean) => void;
11
+ }
8
12
  export declare class FindController {
9
13
  private query;
10
14
  private marks;
15
+ private cache?;
11
16
  private index;
17
+ private outside;
18
+ private seeking;
19
+ private replacing;
20
+ private replaceEpoch;
21
+ private replaced;
22
+ private replaceFailed;
12
23
  private bounds;
13
24
  pendingAnchor: number | undefined;
14
25
  private readonly model;
15
- constructor(model: DocumentModel);
26
+ private readonly options;
27
+ private readonly search;
28
+ private regexCache;
29
+ private snapshotMemo;
30
+ private retained;
31
+ private epoch;
32
+ private searching;
33
+ private failed;
34
+ private captureFailed;
35
+ private pendingStep;
36
+ private cancelSearch;
37
+ private cancelMatch;
38
+ private preview;
39
+ private disposed;
40
+ private revision;
41
+ private cursorRequests;
42
+ constructor(model: DocumentModel, options?: FindControllerOptions);
16
43
  get activeQuery(): EditorFindQuery | null;
17
44
  get active(): boolean;
18
45
  get matches(): readonly FindMark[];
@@ -23,8 +50,29 @@ export declare class FindController {
23
50
  to: number;
24
51
  } | null;
25
52
  get currentMark(): FindMark | null;
53
+ matchesBetween(from: number, to: number): readonly FindMark[];
54
+ get currentMatch(): FindMatch | null;
55
+ get completeMatches(): readonly FindMatch[] | null;
56
+ private get cachedMatches();
57
+ private cacheKey;
26
58
  set(query: EditorFindQuery | null): void;
27
59
  mapBounds(changes: readonly DocumentChange[]): void;
28
60
  refresh(): void;
29
61
  step(direction: 1 | -1): void;
62
+ private get freshRegex();
63
+ private resetRegex;
64
+ private refreshRegex;
65
+ private startRegex;
66
+ private snapshot;
67
+ setPreview(active: boolean): void;
68
+ private preparePreview;
69
+ ensureMatch(command?: boolean): Promise<FindMatch | null>;
70
+ private chooseCurrent;
71
+ private seek;
72
+ nextFromCursor(query: EditorFindQuery, offset: number, direction: 1 | -1): Promise<FindMark | null>;
73
+ reportReplaceFailed(failure: NonNullable<EditorFindState['replaceFailed']>): void;
74
+ reportReplaced(count: number): void;
75
+ replaceOne(replacement: string, preserveCase: boolean, canApply: () => boolean): Promise<boolean>;
76
+ replaceAll(replacement: string, preserveCase: boolean, canApply: () => boolean): Promise<number>;
77
+ dispose(): void;
30
78
  }