@adecore/editor 0.0.1 → 0.17.0-beta.2

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 (107) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +57 -0
  3. package/dist/attribution.d.ts +28 -0
  4. package/dist/attribution.js +55 -0
  5. package/dist/code-block.d.ts +2 -0
  6. package/dist/code-block.js +89 -0
  7. package/dist/column-selection.d.ts +9 -0
  8. package/dist/column-selection.js +52 -0
  9. package/dist/controller.d.ts +64 -0
  10. package/dist/controller.js +772 -0
  11. package/dist/editor.css +813 -0
  12. package/dist/engine.d.ts +2 -0
  13. package/dist/engine.js +610 -0
  14. package/dist/fake.d.ts +192 -0
  15. package/dist/fake.js +663 -0
  16. package/dist/find.d.ts +30 -0
  17. package/dist/find.js +108 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.js +4 -0
  20. package/dist/input.d.ts +9 -0
  21. package/dist/input.js +57 -0
  22. package/dist/keymap-table.d.ts +102 -0
  23. package/dist/keymap-table.js +130 -0
  24. package/dist/keymap.d.ts +36 -0
  25. package/dist/keymap.js +163 -0
  26. package/dist/layout.d.ts +167 -0
  27. package/dist/layout.js +583 -0
  28. package/dist/lens.d.ts +3 -0
  29. package/dist/lens.js +29 -0
  30. package/dist/listeners.d.ts +3 -0
  31. package/dist/listeners.js +12 -0
  32. package/dist/metrics.d.ts +8 -0
  33. package/dist/metrics.js +54 -0
  34. package/dist/modifier-gesture.d.ts +24 -0
  35. package/dist/modifier-gesture.js +98 -0
  36. package/dist/native-input.d.ts +26 -0
  37. package/dist/native-input.js +147 -0
  38. package/dist/offsets.d.ts +2 -0
  39. package/dist/offsets.js +14 -0
  40. package/dist/outline.d.ts +55 -0
  41. package/dist/outline.js +181 -0
  42. package/dist/overview.d.ts +17 -0
  43. package/dist/overview.js +55 -0
  44. package/dist/paint.d.ts +86 -0
  45. package/dist/paint.js +481 -0
  46. package/dist/php-html.d.ts +9 -0
  47. package/dist/php-html.js +36 -0
  48. package/dist/pointer.d.ts +49 -0
  49. package/dist/pointer.js +243 -0
  50. package/dist/scroll-animation.d.ts +3 -0
  51. package/dist/scroll-animation.js +45 -0
  52. package/dist/scroll.d.ts +28 -0
  53. package/dist/scroll.js +76 -0
  54. package/dist/semantic.d.ts +8 -0
  55. package/dist/semantic.js +47 -0
  56. package/dist/shiki.d.ts +7 -0
  57. package/dist/shiki.js +106 -0
  58. package/dist/smart-keys.d.ts +3 -0
  59. package/dist/smart-keys.js +14 -0
  60. package/dist/testing.d.ts +34 -0
  61. package/dist/testing.js +117 -0
  62. package/dist/theme-scopes.d.ts +9 -0
  63. package/dist/theme-scopes.js +61 -0
  64. package/dist/tokens.d.ts +28 -0
  65. package/dist/tokens.js +152 -0
  66. package/dist/tracked-range.d.ts +5 -0
  67. package/dist/tracked-range.js +20 -0
  68. package/dist/types.d.ts +344 -0
  69. package/dist/types.js +1 -0
  70. package/dist/view.d.ts +383 -0
  71. package/dist/view.js +2134 -0
  72. package/package.json +63 -3
  73. package/src/attribution.ts +85 -0
  74. package/src/code-block.ts +100 -0
  75. package/src/column-selection.ts +63 -0
  76. package/src/controller.ts +838 -0
  77. package/src/editor.css +813 -0
  78. package/src/engine.ts +764 -0
  79. package/src/fake.ts +838 -0
  80. package/src/find.ts +128 -0
  81. package/src/index.ts +69 -0
  82. package/src/input.ts +63 -0
  83. package/src/keymap-table.ts +163 -0
  84. package/src/keymap.ts +204 -0
  85. package/src/layout.ts +754 -0
  86. package/src/lens.ts +32 -0
  87. package/src/listeners.ts +15 -0
  88. package/src/metrics.ts +65 -0
  89. package/src/modifier-gesture.ts +115 -0
  90. package/src/native-input.ts +179 -0
  91. package/src/offsets.ts +16 -0
  92. package/src/outline.ts +248 -0
  93. package/src/overview.ts +80 -0
  94. package/src/paint.ts +608 -0
  95. package/src/php-html.ts +43 -0
  96. package/src/pointer.ts +326 -0
  97. package/src/scroll-animation.ts +48 -0
  98. package/src/scroll.ts +102 -0
  99. package/src/semantic.ts +54 -0
  100. package/src/shiki.ts +118 -0
  101. package/src/smart-keys.ts +17 -0
  102. package/src/testing.ts +157 -0
  103. package/src/theme-scopes.ts +75 -0
  104. package/src/tokens.ts +175 -0
  105. package/src/tracked-range.ts +22 -0
  106. package/src/types.ts +659 -0
  107. package/src/view.ts +2437 -0
package/dist/fake.js ADDED
@@ -0,0 +1,663 @@
1
+ import { emit, subscribe } from './listeners.js';
2
+ import { changedSpan } from '@adecore/editor-core';
3
+ import { mapTrackedRange } from './tracked-range.js';
4
+ function positionIn(text, offset) {
5
+ const before = text.slice(0, Math.max(0, offset));
6
+ const lines = before.split('\n');
7
+ return { line: lines.length - 1, character: lines.at(-1).length };
8
+ }
9
+ /* An editor without a DOM, for tests: `type`, `save` and `blur` do what a person would, the rest says what the client asked of it. */
10
+ export class FakeEditor {
11
+ element;
12
+ language;
13
+ path;
14
+ column;
15
+ scrollTop;
16
+ wrap;
17
+ theme;
18
+ readOnly;
19
+ readOnlyReason;
20
+ revealedLine;
21
+ focused = false;
22
+ indentation;
23
+ /* How often the client asked it to read the code face off the page again. */
24
+ fontRefreshes = 0;
25
+ disposed = false;
26
+ /* What the client asked to find last; null once it ended the find. */
27
+ findQuery = null;
28
+ findState = { count: 0, current: null };
29
+ text;
30
+ changes = new Set();
31
+ textChanges = new Set();
32
+ saves = new Set();
33
+ blurs = new Set();
34
+ finds = new Set();
35
+ constructor(element, options) {
36
+ this.element = element;
37
+ this.text = options.text;
38
+ this.language = options.language;
39
+ this.path = options.path;
40
+ this.wrap = options.wrap ?? false;
41
+ this.indentation = options.indentation ?? { tabSize: 4, insertSpaces: true };
42
+ this.smartKeys = options.smartKeys ?? {};
43
+ this.theme = options.theme;
44
+ this.readOnly = options.readOnly ?? false;
45
+ this.readOnlyReason = options.readOnlyReason;
46
+ this.label = options.label ?? 'Code editor';
47
+ this.revealedLine = options.line ?? null;
48
+ this.column = options.column ?? null;
49
+ this.scrollTop = options.scrollTop ?? null;
50
+ }
51
+ /* A person's edit: the text changes and every change listener hears it. A read-only editor refuses it, as the real editor does. */
52
+ type(text) {
53
+ this.assertLive();
54
+ if (this.readOnly || text === this.text) {
55
+ return;
56
+ }
57
+ this.replaceText(text, 'input');
58
+ emit(this.changes);
59
+ }
60
+ /* Mod+S inside the editor. */
61
+ save() {
62
+ this.assertLive();
63
+ emit(this.saves);
64
+ }
65
+ /* The focus leaving the editor. */
66
+ blur() {
67
+ this.assertLive();
68
+ this.focused = false;
69
+ emit(this.blurs);
70
+ }
71
+ getText() {
72
+ return this.text;
73
+ }
74
+ setText(text) {
75
+ this.assertLive();
76
+ this.replaceText(text);
77
+ }
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') {
80
+ const span = changedSpan(this.text, text);
81
+ const before = this.text;
82
+ this.text = text;
83
+ if (span === null) {
84
+ return;
85
+ }
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
+ if (source !== 'external') {
88
+ this.moveCaret(positionIn(text, span.start + span.text.length));
89
+ }
90
+ this.followTracked(span.start, span.end, span.text.length);
91
+ this.ghost = null;
92
+ const change = {
93
+ source,
94
+ changes: [{ range: { start: positionIn(before, span.start), end: positionIn(before, span.end) }, text: span.text }]
95
+ };
96
+ for (const listener of [...this.textChanges]) {
97
+ listener(change);
98
+ }
99
+ }
100
+ tracked = new Set();
101
+ /* The fake sees one change per edit, the stretch that differs, so a range it tracks is lost a little sooner than the real editor's. */
102
+ followTracked(from, to, insertedLength) {
103
+ for (const entry of this.tracked) {
104
+ const mapped = mapTrackedRange(entry.from, entry.to, [{ from, to, insertedLength }]);
105
+ if (mapped === null) {
106
+ this.tracked.delete(entry);
107
+ entry.lost = true;
108
+ }
109
+ else {
110
+ Object.assign(entry, mapped);
111
+ }
112
+ }
113
+ }
114
+ trackRange(range) {
115
+ const entry = { from: this.offsetAt(range.start), to: this.offsetAt(range.end), lost: false };
116
+ this.tracked.add(entry);
117
+ return {
118
+ get: () => (entry.lost ? null : { start: this.positionAt(entry.from), end: this.positionAt(entry.to) }),
119
+ dispose: () => {
120
+ entry.lost = true;
121
+ this.tracked.delete(entry);
122
+ }
123
+ };
124
+ }
125
+ onTextChange(listener) {
126
+ this.textChanges.add(listener);
127
+ return () => {
128
+ this.textChanges.delete(listener);
129
+ };
130
+ }
131
+ positionAt(offset) {
132
+ return positionIn(this.text, offset);
133
+ }
134
+ textInRange(range) {
135
+ return this.text.slice(this.offsetAt(range.start), this.offsetAt(range.end));
136
+ }
137
+ offsetAt(position) {
138
+ const lines = this.text.split('\n');
139
+ let offset = 0;
140
+ for (let line = 0; line < Math.min(position.line, lines.length - 1); line++) {
141
+ offset += lines[line].length + 1;
142
+ }
143
+ const current = lines[Math.min(position.line, lines.length - 1)];
144
+ return offset + Math.min(position.character, current.length);
145
+ }
146
+ onChange(listener) {
147
+ return subscribe(this.changes, listener);
148
+ }
149
+ onSave(listener) {
150
+ return subscribe(this.saves, listener);
151
+ }
152
+ onBlur(listener) {
153
+ return subscribe(this.blurs, listener);
154
+ }
155
+ /* How the client last asked the view to follow a jump. */
156
+ lastReveal = null;
157
+ revealLine(line, reveal = 'center') {
158
+ this.revealedLine = line;
159
+ this.lastReveal = reveal;
160
+ }
161
+ /* Counts plain occurrences only; the matcher itself is the editor's, and a test here is about the client's side. */
162
+ find(query) {
163
+ this.findQuery = query;
164
+ const needle = query === null || query.text === '' ? '' : query.caseSensitive ? query.text : query.text.toLowerCase();
165
+ const haystack = query?.caseSensitive === true ? this.text : this.text.toLowerCase();
166
+ const count = needle === '' ? 0 : haystack.split(needle).length - 1;
167
+ this.announceFind({ count, current: count === 0 ? null : 0 });
168
+ }
169
+ /* What the client asked to replace, one match at a time and all at once. */
170
+ replacements = [];
171
+ replaceOptions = [];
172
+ /* What the client last asked to show under the match. */
173
+ replacePreview = null;
174
+ /* What the client asked to select all matches of, and how many it was told there were. */
175
+ selectedMatches = 0;
176
+ lastFindFromCursor = null;
177
+ replacementsOfAll = [];
178
+ replace(replacement, options = {}) {
179
+ this.replacements.push(replacement);
180
+ this.replaceOptions.push(options);
181
+ return !this.readOnly && this.findState.count > 0;
182
+ }
183
+ replaceAll(replacement, options = {}) {
184
+ this.replacementsOfAll.push(replacement);
185
+ this.replaceOptions.push(options);
186
+ return this.readOnly ? 0 : this.findState.count;
187
+ }
188
+ setReplacePreview(replacement) {
189
+ this.replacePreview = replacement;
190
+ }
191
+ selectFindMatches() {
192
+ this.selectedMatches = this.findState.count;
193
+ this.find(null);
194
+ return this.selectedMatches;
195
+ }
196
+ findFromCursor(query, direction) {
197
+ this.lastFindFromCursor = { query, direction };
198
+ return true;
199
+ }
200
+ findStep(direction) {
201
+ const { count, current } = this.findState;
202
+ if (count > 0) {
203
+ this.announceFind({ count, current: ((current ?? 0) + direction + count) % count });
204
+ }
205
+ }
206
+ onFind(listener) {
207
+ this.finds.add(listener);
208
+ return () => {
209
+ this.finds.delete(listener);
210
+ };
211
+ }
212
+ endFind() {
213
+ this.find(null);
214
+ }
215
+ setWrap(wrap) {
216
+ this.wrap = wrap;
217
+ }
218
+ /* What the client handed it last; null while it reads the structure itself. */
219
+ blocks = null;
220
+ scopes = new Set();
221
+ /* What the client marked as changed last. */
222
+ changeMarks = [];
223
+ setChangeMarks(marks) {
224
+ this.changeMarks = marks;
225
+ }
226
+ /* The tinted lines the client set last. */
227
+ lineHighlights = [];
228
+ setLineHighlights(highlights) {
229
+ this.lineHighlights = highlights;
230
+ }
231
+ /* What the client drew as code outside the document: the text and how, and into which element. */
232
+ codeBlocks = [];
233
+ renderCode(container, text, options = {}) {
234
+ this.codeBlocks.push({ container, text, options });
235
+ }
236
+ /* The cursors of agents the client set last. */
237
+ remoteCursors = [];
238
+ setRemoteCursors(cursors) {
239
+ this.remoteCursors = cursors;
240
+ }
241
+ /* What the client marked as written by an agent last. */
242
+ attributionMarks = [];
243
+ attributionHovers = new Set();
244
+ setAttributionMarks(marks) {
245
+ this.attributionMarks = marks;
246
+ }
247
+ onAttributionHover(listener) {
248
+ this.attributionHovers.add(listener);
249
+ return () => {
250
+ this.attributionHovers.delete(listener);
251
+ };
252
+ }
253
+ /* The pointer on the bar of a mark, or leaving it. The rect is where `rectAt` puts the mark's first line unless given. */
254
+ hoverAttribution(id, rect) {
255
+ const mark = id === null ? undefined : this.attributionMarks.find((candidate) => candidate.id === id);
256
+ const hover = mark === undefined
257
+ ? null
258
+ : { id: mark.id, rect: rect ?? this.rectAt({ line: mark.startLine - 1, character: 0 }) ?? { left: 0, top: 0, right: 0, bottom: 0 } };
259
+ for (const listener of [...this.attributionHovers]) {
260
+ listener(hover);
261
+ }
262
+ }
263
+ /* What each owner put next to lines of the text last. */
264
+ widgetsByOwner = new Map();
265
+ /* The default owner's rows. */
266
+ get widgets() {
267
+ return this.widgetsByOwner.get('default') ?? [];
268
+ }
269
+ /* The row ids each owner has drawn, since the editor draws a row once and keeps it for as long as its id stays. */
270
+ drawnWidgets = new Map();
271
+ setWidgets(widgets, owner = 'default') {
272
+ if (widgets.length === 0) {
273
+ this.widgetsByOwner.delete(owner);
274
+ this.drawnWidgets.delete(owner);
275
+ return;
276
+ }
277
+ this.widgetsByOwner.set(owner, widgets);
278
+ const before = this.drawnWidgets.get(owner) ?? new Set();
279
+ for (const widget of widgets) {
280
+ if (!before.has(widget.id)) {
281
+ widget.render({});
282
+ }
283
+ }
284
+ this.drawnWidgets.set(owner, new Set(widgets.map((widget) => widget.id)));
285
+ }
286
+ lineActionsByOwner = new Map();
287
+ setLineActions(actions, owner = 'default') {
288
+ if (actions.length === 0) {
289
+ this.lineActionsByOwner.delete(owner);
290
+ }
291
+ else {
292
+ this.lineActionsByOwner.set(owner, actions);
293
+ }
294
+ }
295
+ /* The code vision rows the client set last. */
296
+ codeVision = [];
297
+ setCodeVision(rows) {
298
+ this.codeVision = rows;
299
+ }
300
+ /* The button the client set in the gutter last. */
301
+ gutterAction = null;
302
+ gutterActionListeners = new Set();
303
+ setGutterAction(action) {
304
+ this.gutterAction = action;
305
+ }
306
+ onGutterAction(listener) {
307
+ this.gutterActionListeners.add(listener);
308
+ return () => {
309
+ this.gutterActionListeners.delete(listener);
310
+ };
311
+ }
312
+ /* What each owner put in the gutter last. */
313
+ gutterMarkersByOwner = new Map();
314
+ gutterMarkerListeners = new Set();
315
+ setGutterMarkers(markers, owner = 'default') {
316
+ if (markers.length === 0) {
317
+ this.gutterMarkersByOwner.delete(owner);
318
+ }
319
+ else {
320
+ this.gutterMarkersByOwner.set(owner, markers);
321
+ }
322
+ }
323
+ onGutterMarker(listener) {
324
+ this.gutterMarkerListeners.add(listener);
325
+ return () => {
326
+ this.gutterMarkerListeners.delete(listener);
327
+ };
328
+ }
329
+ /* A press on a gutter marker. */
330
+ pressGutterMarker(id) {
331
+ for (const listener of [...this.gutterMarkerListeners]) {
332
+ listener(id);
333
+ }
334
+ }
335
+ /* A press on the gutter button, if there is one. */
336
+ pressGutterAction() {
337
+ if (this.gutterAction !== null) {
338
+ for (const listener of [...this.gutterActionListeners]) {
339
+ listener(this.gutterAction.line);
340
+ }
341
+ }
342
+ }
343
+ setBlocks(blocks) {
344
+ this.blocks = blocks;
345
+ }
346
+ onScope(listener) {
347
+ this.scopes.add(listener);
348
+ return () => {
349
+ this.scopes.delete(listener);
350
+ };
351
+ }
352
+ /* The caret moving into blocks, for a test of what the client draws from it. */
353
+ enterScope(scope) {
354
+ for (const listener of [...this.scopes]) {
355
+ listener(scope);
356
+ }
357
+ }
358
+ smartKeys;
359
+ guides = true;
360
+ whitespace = false;
361
+ rightMargin = null;
362
+ setGuides(guides) {
363
+ this.guides = guides;
364
+ }
365
+ setWhitespace(whitespace) {
366
+ this.whitespace = whitespace;
367
+ }
368
+ setRightMargin(column) {
369
+ this.rightMargin = column;
370
+ }
371
+ foldOutline = 'hover';
372
+ foldHints = null;
373
+ setFoldOutline(outline) {
374
+ this.foldOutline = outline;
375
+ }
376
+ label;
377
+ setLabel(label) {
378
+ this.label = label;
379
+ }
380
+ setFoldHints(hints) {
381
+ this.foldHints = hints;
382
+ }
383
+ setSmartKeys(keys) {
384
+ this.smartKeys = keys;
385
+ }
386
+ setIndentation(indentation) {
387
+ this.indentation = indentation;
388
+ }
389
+ /* What the client marked and highlighted last. */
390
+ markers = [];
391
+ highlights = [];
392
+ caret = { line: 0, character: 0 };
393
+ /* Where `rectAt` puts a position, which a test can move to simulate scrolling. */
394
+ origin = { left: 100, top: 100 };
395
+ charSize = { width: 8, height: 20 };
396
+ carets = new Set();
397
+ hovers = new Set();
398
+ views = new Set();
399
+ keys = new Set();
400
+ setMarkers(markers) {
401
+ this.markers = markers;
402
+ }
403
+ /* What the client drew as hints last. */
404
+ inlayHints = [];
405
+ setInlayHints(hints) {
406
+ this.inlayHints = hints;
407
+ }
408
+ /* The suggestion after the caret now, which any change of the text takes away as the real editor does. */
409
+ ghost = null;
410
+ setGhostText(ghost) {
411
+ this.ghost = ghost;
412
+ }
413
+ /* What the client classified last. */
414
+ semanticTokens = null;
415
+ setSemanticTokens(tokens) {
416
+ this.semanticTokens = tokens;
417
+ }
418
+ /* What the client drew as a link last. */
419
+ link = null;
420
+ setLink(range) {
421
+ this.link = range;
422
+ }
423
+ setHighlights(highlights) {
424
+ this.highlights = highlights;
425
+ }
426
+ getCaret() {
427
+ return this.caret;
428
+ }
429
+ /* The folds the client asked to keep. */
430
+ folds = { collapsed: [], custom: [] };
431
+ getFolds() {
432
+ return this.folds;
433
+ }
434
+ getScrollTop() {
435
+ return this.scrollTop ?? 0;
436
+ }
437
+ /* The selection a person made; without one the caret. */
438
+ selection = null;
439
+ getSelection() {
440
+ return this.selection ?? { start: this.caret, end: this.caret };
441
+ }
442
+ /* The other selections of the editor; the primary one is always last. */
443
+ otherSelections = [];
444
+ /* The other carets of the editor, which a test sets. */
445
+ get otherCarets() {
446
+ return this.otherSelections.map((range) => range.end);
447
+ }
448
+ set otherCarets(positions) {
449
+ this.otherSelections = positions.map((position) => ({ start: position, end: position }));
450
+ }
451
+ getSelections() {
452
+ return [...this.otherSelections, this.getSelection()];
453
+ }
454
+ setSelections(ranges, reveal = 'relative') {
455
+ const last = ranges.at(-1);
456
+ if (last === undefined) {
457
+ return;
458
+ }
459
+ this.otherSelections = ranges.slice(0, -1).map((range) => ({ ...range }));
460
+ this.setSelection(last, reveal);
461
+ }
462
+ getIndentation() {
463
+ return this.indentation;
464
+ }
465
+ setCaret(position, reveal = 'relative') {
466
+ this.lastReveal = reveal;
467
+ this.caret = position;
468
+ this.revealedLine = position.line + 1;
469
+ this.moveCaret(position);
470
+ }
471
+ setSelection(range, reveal = 'relative') {
472
+ this.lastReveal = reveal;
473
+ this.caret = range.end;
474
+ this.selection = range;
475
+ for (const listener of [...this.carets]) {
476
+ listener(range.end);
477
+ }
478
+ }
479
+ /* The caret a person's click or key moved. */
480
+ moveCaret(position) {
481
+ this.caret = position;
482
+ this.selection = null;
483
+ for (const listener of [...this.carets]) {
484
+ listener(position);
485
+ }
486
+ }
487
+ onCaret(listener) {
488
+ this.carets.add(listener);
489
+ return () => {
490
+ this.carets.delete(listener);
491
+ };
492
+ }
493
+ onHover(listener) {
494
+ this.hovers.add(listener);
495
+ return () => {
496
+ this.hovers.delete(listener);
497
+ };
498
+ }
499
+ /* The pointer resting on a position, or leaving the text. */
500
+ hover(position) {
501
+ const hover = position === null ? null : { position, rect: this.rectAt(position) };
502
+ for (const listener of [...this.hovers]) {
503
+ listener(hover);
504
+ }
505
+ }
506
+ rectAt(position) {
507
+ const left = this.origin.left + position.character * this.charSize.width;
508
+ const top = this.origin.top + position.line * this.charSize.height;
509
+ return { left, top, right: left + this.charSize.width, bottom: top + this.charSize.height };
510
+ }
511
+ onViewChange(listener) {
512
+ return subscribe(this.views, listener);
513
+ }
514
+ /* What `getVisibleRange` answers; the whole text until a test scrolls. */
515
+ visibleRange = null;
516
+ getVisibleRange() {
517
+ return this.visibleRange ?? { start: { line: 0, character: 0 }, end: this.positionAt(Number.MAX_SAFE_INTEGER) };
518
+ }
519
+ /* The editor scrolling, to a range of lines when a test says which. */
520
+ scroll(range) {
521
+ if (range !== undefined) {
522
+ this.visibleRange = range;
523
+ }
524
+ emit(this.views);
525
+ }
526
+ /* What `setSelectionRanges` was last given. */
527
+ selectionRanges = null;
528
+ setSelectionRanges(provider) {
529
+ this.selectionRanges = provider;
530
+ }
531
+ onKeyDown(handler) {
532
+ this.keys.add(handler);
533
+ return () => {
534
+ this.keys.delete(handler);
535
+ };
536
+ }
537
+ clickHandlers = new Set();
538
+ contextListeners = new Set();
539
+ onClick(handler) {
540
+ this.clickHandlers.add(handler);
541
+ return () => {
542
+ this.clickHandlers.delete(handler);
543
+ };
544
+ }
545
+ onContextMenu(listener) {
546
+ this.contextListeners.add(listener);
547
+ return () => {
548
+ this.contextListeners.delete(listener);
549
+ };
550
+ }
551
+ /* A press on a character as the editor sees it first; true when a handler took it, and the caret stays where it is otherwise only until the test moves it. */
552
+ click(click) {
553
+ return [...this.clickHandlers].some((handler) => handler(click));
554
+ }
555
+ /* The context menu asked for at a position. */
556
+ openContextMenu(menu) {
557
+ for (const listener of [...this.contextListeners]) {
558
+ listener(menu);
559
+ }
560
+ }
561
+ /* A key as the editor sees it first; true when a handler took it. */
562
+ press(event) {
563
+ const full = { ctrlKey: false, metaKey: false, altKey: false, shiftKey: false, preventDefault: () => undefined, ...event };
564
+ return [...this.keys].some((handler) => handler(full));
565
+ }
566
+ applyEdits(edits) {
567
+ if (this.readOnly || edits.length === 0) {
568
+ return false;
569
+ }
570
+ let text = this.text;
571
+ const ordered = edits
572
+ .map((edit) => ({ from: this.offsetAt(edit.range.start), to: this.offsetAt(edit.range.end), text: edit.text }))
573
+ .sort((a, b) => b.from - a.from);
574
+ for (const edit of ordered) {
575
+ text = text.slice(0, edit.from) + edit.text + text.slice(edit.to);
576
+ }
577
+ // The other carets follow the text as the real editor's do: one inside a replaced stretch ends behind what replaced it.
578
+ const others = this.otherSelections.map((range) => ({ anchor: this.offsetAt(range.start), head: this.offsetAt(range.end) }));
579
+ const follow = (offset) => {
580
+ let delta = 0;
581
+ for (const edit of [...ordered].reverse()) {
582
+ if (offset < edit.from) {
583
+ break;
584
+ }
585
+ if (offset < edit.to || (offset === edit.from && edit.from === edit.to)) {
586
+ return edit.from + delta + edit.text.length;
587
+ }
588
+ delta += edit.text.length - (edit.to - edit.from);
589
+ }
590
+ return offset + delta;
591
+ };
592
+ const moved = others.map((range) => ({ anchor: follow(range.anchor), head: follow(range.head) }));
593
+ this.replaceText(text, 'command');
594
+ this.otherSelections = moved.map((range) => ({
595
+ start: positionIn(text, Math.min(range.anchor, range.head)),
596
+ end: positionIn(text, Math.max(range.anchor, range.head))
597
+ }));
598
+ emit(this.changes);
599
+ return true;
600
+ }
601
+ setTheme(theme) {
602
+ this.theme = theme;
603
+ }
604
+ refreshFont() {
605
+ this.fontRefreshes += 1;
606
+ }
607
+ setReadOnly(readOnly, reason) {
608
+ this.readOnly = readOnly;
609
+ this.readOnlyReason = reason;
610
+ }
611
+ /* The commands run, in order; the fake has no commands of its own to carry out. */
612
+ commands = [];
613
+ runCommand(command) {
614
+ this.commands.push(command);
615
+ return false;
616
+ }
617
+ focus() {
618
+ this.focused = true;
619
+ }
620
+ dispose() {
621
+ this.disposed = true;
622
+ this.changes.clear();
623
+ this.textChanges.clear();
624
+ this.carets.clear();
625
+ this.hovers.clear();
626
+ this.attributionHovers.clear();
627
+ this.views.clear();
628
+ this.keys.clear();
629
+ this.saves.clear();
630
+ this.blurs.clear();
631
+ this.finds.clear();
632
+ this.scopes.clear();
633
+ this.tracked.clear();
634
+ }
635
+ announceFind(state) {
636
+ this.findState = state;
637
+ for (const listener of [...this.finds]) {
638
+ listener(state);
639
+ }
640
+ }
641
+ /* A test that drives an editor its client already disposed has found a leak. */
642
+ assertLive() {
643
+ if (this.disposed) {
644
+ throw new Error('The editor is disposed');
645
+ }
646
+ }
647
+ }
648
+ export class FakeEditorEngine {
649
+ editors = [];
650
+ mount(element, options) {
651
+ const editor = new FakeEditor(element, options);
652
+ this.editors.push(editor);
653
+ return editor;
654
+ }
655
+ /* The editor mounted last, which is the one a test of a single surface drives. */
656
+ get last() {
657
+ const editor = this.editors.at(-1);
658
+ if (!editor) {
659
+ throw new Error('No editor was mounted');
660
+ }
661
+ return editor;
662
+ }
663
+ }
package/dist/find.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { DocumentChange, DocumentModel } from '@adecore/editor-core';
2
+ import type { EditorFindQuery, EditorFindState } from './types.ts';
3
+ export interface FindMark {
4
+ from: number;
5
+ to: number;
6
+ }
7
+ export declare const FIND_LIMIT = 10000;
8
+ export declare class FindController {
9
+ private query;
10
+ private marks;
11
+ private index;
12
+ private bounds;
13
+ pendingAnchor: number | undefined;
14
+ private readonly model;
15
+ constructor(model: DocumentModel);
16
+ get activeQuery(): EditorFindQuery | null;
17
+ get active(): boolean;
18
+ get matches(): readonly FindMark[];
19
+ get current(): number | null;
20
+ get state(): EditorFindState;
21
+ get scope(): {
22
+ from: number;
23
+ to: number;
24
+ } | null;
25
+ get currentMark(): FindMark | null;
26
+ set(query: EditorFindQuery | null): void;
27
+ mapBounds(changes: readonly DocumentChange[]): void;
28
+ refresh(): void;
29
+ step(direction: 1 | -1): void;
30
+ }