@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/src/engine.ts ADDED
@@ -0,0 +1,764 @@
1
+ import { changedSpans, DocumentModel, type EditorSnapshot, replacementText } from '@adecore/editor-core';
2
+ import { InputController } from './controller.ts';
3
+ import { resolveSmartKeys } from './smart-keys.ts';
4
+ import { emit, type Listener, subscribe } from './listeners.ts';
5
+ import type {
6
+ Editor,
7
+ EditorAttributionHover,
8
+ EditorAttributionMark,
9
+ EditorBlock,
10
+ EditorCodeBlockOptions,
11
+ EditorLineAction,
12
+ EditorLineHighlight,
13
+ EditorChangeMark,
14
+ EditorCodeVision,
15
+ EditorClickHandler,
16
+ EditorContextMenu,
17
+ EditorContentChange,
18
+ EditorEngine,
19
+ EditorFindQuery,
20
+ EditorFoldHints,
21
+ EditorFoldOutline,
22
+ EditorFolds,
23
+ EditorIndentation,
24
+ EditorFindState,
25
+ EditorReplaceOptions,
26
+ EditorGutterAction,
27
+ EditorGutterMarker,
28
+ EditorHighlight,
29
+ EditorHover,
30
+ EditorGhostText,
31
+ EditorInlayHint,
32
+ EditorKeyHandler,
33
+ EditorMarker,
34
+ EditorOptions,
35
+ EditorPosition,
36
+ EditorRemoteCursor,
37
+ EditorRange,
38
+ EditorReveal,
39
+ EditorRect,
40
+ EditorRunCommand,
41
+ EditorSelectionRanges,
42
+ EditorSemanticToken,
43
+ EditorSmartKeys,
44
+ EditorTextChange,
45
+ EditorTrackedRange,
46
+ EditorTheme,
47
+ EditorWidget,
48
+ SmartEditorEngineOptions
49
+ } from './types.ts';
50
+ import { fillLens, lensSignature } from './lens.ts';
51
+ import { EditorView, type ViewSettings } from './view.ts';
52
+
53
+ const DEFAULT_TAB_SIZE = 4;
54
+ const DEFAULT_LABEL = 'Code editor';
55
+ const DEFAULT_WIDGET_OWNER = 'default';
56
+
57
+ class SmartEditor implements Editor {
58
+ private readonly model: DocumentModel;
59
+ private readonly settings: ViewSettings;
60
+ private readonly view: EditorView;
61
+ private readonly controller: InputController;
62
+ private readonly changes = new Set<Listener>();
63
+ private readonly textChanges = new Set<(change: EditorTextChange) => void>();
64
+ private readonly carets = new Set<(position: EditorPosition) => void>();
65
+ private caretOffset = 0;
66
+ private readonly saves = new Set<Listener>();
67
+ private readonly blurs = new Set<Listener>();
68
+ private readonly subscription: { dispose(): void };
69
+ private language: string | undefined;
70
+ private theme: EditorTheme;
71
+ private revision: number;
72
+ private lensRows = new Map<string, EditorCodeVision>();
73
+ private lensRevisions = new Map<string, { signature: string; revision: number }>();
74
+ private tokenizerRequest = 0;
75
+ private settingText = false;
76
+ private disposed = false;
77
+
78
+ private readonly engine: SmartEditorEngineOptions;
79
+
80
+ constructor(engine: SmartEditorEngineOptions, element: HTMLElement, options: EditorOptions) {
81
+ this.engine = engine;
82
+ this.model = new DocumentModel(options.text);
83
+ this.language = options.language;
84
+ this.theme = options.theme;
85
+ this.settings = {
86
+ language: options.language,
87
+ tabSize: options.indentation?.tabSize ?? DEFAULT_TAB_SIZE,
88
+ insertSpaces: options.indentation?.insertSpaces ?? true,
89
+ readOnly: options.readOnly ?? false,
90
+ readOnlyReason: options.readOnlyReason,
91
+ wrap: options.wrap ?? false,
92
+ smartKeys: resolveSmartKeys(options.smartKeys),
93
+ messages: options.messages ?? {},
94
+ label: options.label ?? DEFAULT_LABEL,
95
+ guides: options.guides !== false,
96
+ whitespace: options.whitespace === true,
97
+ rightMargin: options.rightMargin ?? null,
98
+ foldOutline: options.foldOutline ?? 'hover'
99
+ };
100
+ this.view = new EditorView(element, this.model, this.settings);
101
+ this.controller = new InputController(this.view, {
102
+ handBack: engine.handBack ?? [],
103
+ keymap: engine.keymap,
104
+ apple: engine.apple ?? false,
105
+ save: () => emit(this.saves),
106
+ blur: () => emit(this.blurs)
107
+ });
108
+ this.revision = this.model.getRevision();
109
+ this.caretOffset = this.model.getPrimary().head;
110
+ this.subscription = this.model.subscribe((snapshot) => {
111
+ this.caretMoved(snapshot.selections.at(-1)!.head);
112
+ if (snapshot.revision !== this.revision) {
113
+ this.revision = snapshot.revision;
114
+ if (snapshot.contentEdits !== undefined && snapshot.contentEdits.length > 0) {
115
+ this.announceTextChange(snapshot.contentEdits, snapshot.source ?? 'external');
116
+ }
117
+ if (!this.settingText) {
118
+ emit(this.changes);
119
+ }
120
+ }
121
+ });
122
+ this.loadTokenizer();
123
+ if (options.folds !== undefined || options.foldDefaults !== undefined) {
124
+ this.view.restoreFolds(options.folds ?? null, options.foldDefaults);
125
+ }
126
+ this.placeCursor(options);
127
+ }
128
+
129
+ /* Where the editor opens: on a line and column, scrolled to a position, or with that line in view. */
130
+ private placeCursor(options: EditorOptions): void {
131
+ if (options.line !== undefined) {
132
+ const line = this.clampLine(options.line);
133
+ const bounds = this.model.getLine(line);
134
+ const offset = Math.min(bounds.end, bounds.start + Math.max(0, (options.column ?? 1) - 1));
135
+ this.model.setSelections([{ anchor: offset, head: offset }]);
136
+ if (options.scrollTop === undefined) {
137
+ // A file opens where it is told to, without travelling there.
138
+ this.view.revealOffset(offset, 'center', false);
139
+ }
140
+ }
141
+ if (options.scrollTop !== undefined) {
142
+ this.view.render();
143
+ this.view.viewport.scrollTop = options.scrollTop;
144
+ this.view.render();
145
+ }
146
+ }
147
+
148
+ private loadTokenizer(): void {
149
+ const request = ++this.tokenizerRequest;
150
+ void this.engine
151
+ .scopeColors?.(this.theme)
152
+ .catch(() => null)
153
+ .then((colors) => {
154
+ if (!this.disposed && request === this.tokenizerRequest) {
155
+ this.view.setScopeColors(colors ?? null);
156
+ }
157
+ });
158
+ void this.engine
159
+ .tokenizer(this.language, this.theme, this.model.getText())
160
+ .catch(() => null)
161
+ .then((tokenizer) => {
162
+ if (!this.disposed && request === this.tokenizerRequest) {
163
+ this.view.setTokenizer(tokenizer);
164
+ }
165
+ });
166
+ }
167
+
168
+ getText(): string {
169
+ return this.model.getText();
170
+ }
171
+
172
+ setText(text: string): void {
173
+ const spans = changedSpans(this.model.getText(), text);
174
+ if (spans.length === 0) {
175
+ return;
176
+ }
177
+ this.settingText = true;
178
+ try {
179
+ // Edits and not a reset, which would drop the undo history and put the cursor on line one.
180
+ this.model.applyEdits(
181
+ spans.map((span) => ({ from: span.start, to: span.end, text: span.text })),
182
+ { source: 'external' }
183
+ );
184
+ } finally {
185
+ this.settingText = false;
186
+ }
187
+ }
188
+
189
+ onChange(listener: Listener): () => void {
190
+ return subscribe(this.changes, listener);
191
+ }
192
+
193
+ onTextChange(listener: (change: EditorTextChange) => void): () => void {
194
+ this.textChanges.add(listener);
195
+ return () => {
196
+ this.textChanges.delete(listener);
197
+ };
198
+ }
199
+
200
+ private announceTextChange(edits: NonNullable<EditorSnapshot['contentEdits']>, source: EditorTextChange['source']): void {
201
+ const change: EditorTextChange = {
202
+ source,
203
+ changes: edits.map((edit) => ({
204
+ range: { start: { line: edit.start.line, character: edit.start.column }, end: { line: edit.end.line, character: edit.end.column } },
205
+ text: edit.text
206
+ }))
207
+ };
208
+ for (const listener of [...this.textChanges]) {
209
+ listener(change);
210
+ }
211
+ }
212
+
213
+ private caretMoved(head: number): void {
214
+ if (head === this.caretOffset) {
215
+ return;
216
+ }
217
+ this.caretOffset = head;
218
+ const position = this.positionAt(head);
219
+ for (const listener of [...this.carets]) {
220
+ listener(position);
221
+ }
222
+ }
223
+
224
+ getCaret(): EditorPosition {
225
+ return this.positionAt(this.model.getPrimary().head);
226
+ }
227
+
228
+ getScrollTop(): number {
229
+ return this.view.scrollPlace.y;
230
+ }
231
+
232
+ 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)) };
235
+ }
236
+
237
+ 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
+ }));
242
+ }
243
+
244
+ getIndentation(): EditorIndentation {
245
+ return { tabSize: this.settings.tabSize, insertSpaces: this.settings.insertSpaces };
246
+ }
247
+
248
+ setCaret(position: EditorPosition, reveal: EditorReveal = 'relative'): void {
249
+ const offset = this.offsetAt(position);
250
+ this.model.setSelections([{ anchor: offset, head: offset }]);
251
+ this.view.revealOffset(offset, reveal);
252
+ }
253
+
254
+ setSelection(range: EditorRange, reveal: EditorReveal = 'relative'): void {
255
+ const head = this.offsetAt(range.end);
256
+ this.model.setSelections([{ anchor: this.offsetAt(range.start), head }]);
257
+ this.view.revealOffset(head, reveal);
258
+ }
259
+
260
+ setSelections(ranges: readonly EditorRange[], reveal: EditorReveal = 'relative'): void {
261
+ const last = ranges.at(-1);
262
+ if (last === undefined) {
263
+ return;
264
+ }
265
+ this.model.setSelections(ranges.map((range) => ({ anchor: this.offsetAt(range.start), head: this.offsetAt(range.end) })));
266
+ this.view.revealOffset(this.offsetAt(last.end), reveal);
267
+ }
268
+
269
+ onCaret(listener: (position: EditorPosition) => void): () => void {
270
+ this.carets.add(listener);
271
+ return () => {
272
+ this.carets.delete(listener);
273
+ };
274
+ }
275
+
276
+ onHover(listener: (hover: EditorHover | null) => void): () => void {
277
+ return this.view.onHover((offset) => {
278
+ const rect = offset === null ? null : this.view.clientRectOf(offset);
279
+ listener(offset === null || rect === null ? null : { position: this.positionAt(offset), rect });
280
+ });
281
+ }
282
+
283
+ rectAt(position: EditorPosition): EditorRect | null {
284
+ return this.view.clientRectOf(this.offsetAt(position));
285
+ }
286
+
287
+ onViewChange(listener: Listener): () => void {
288
+ return this.view.onViewChange(listener);
289
+ }
290
+
291
+ onClick(handler: EditorClickHandler): () => void {
292
+ return this.controller.onClick(handler);
293
+ }
294
+
295
+ onContextMenu(listener: (menu: EditorContextMenu) => void): () => void {
296
+ return this.controller.onContextMenu(listener);
297
+ }
298
+
299
+ onKeyDown(handler: EditorKeyHandler): () => void {
300
+ return this.controller.onKeyDown(handler);
301
+ }
302
+
303
+ setSelectionRanges(provider: EditorSelectionRanges | null): void {
304
+ this.controller.setSelectionRanges(provider);
305
+ }
306
+
307
+ applyEdits(edits: readonly EditorContentChange[]): boolean {
308
+ if (this.settings.readOnly || edits.length === 0) {
309
+ return false;
310
+ }
311
+ try {
312
+ return this.model.applyEdits(
313
+ edits.map((edit) => ({ from: this.offsetAt(edit.range.start), to: this.offsetAt(edit.range.end), text: edit.text })),
314
+ { source: 'command' }
315
+ );
316
+ } catch {
317
+ return false;
318
+ }
319
+ }
320
+
321
+ setMarkers(markers: readonly EditorMarker[]): void {
322
+ this.view.setMarkers(
323
+ markers.map((marker) => ({
324
+ severity: marker.severity,
325
+ from: this.offsetAt(marker.range.start),
326
+ to: this.offsetAt(marker.range.end),
327
+ unnecessary: marker.unnecessary === true,
328
+ deprecated: marker.deprecated === true,
329
+ ...(marker.message === undefined ? {} : { message: marker.message })
330
+ }))
331
+ );
332
+ }
333
+
334
+ setInlayHints(hints: readonly EditorInlayHint[]): void {
335
+ this.view.setInlays(hints.map((hint, index) => ({ id: `hint-${index}`, at: this.offsetAt(hint.position), text: hint.label })));
336
+ }
337
+
338
+ setGhostText(ghost: EditorGhostText | null): void {
339
+ this.view.setGhost(ghost === null ? null : { at: this.offsetAt(ghost.position), text: ghost.text, accessory: ghost.accessory });
340
+ }
341
+
342
+ setSemanticTokens(tokens: readonly EditorSemanticToken[] | null): void {
343
+ this.view.setSemanticTokens(
344
+ tokens === null
345
+ ? null
346
+ : tokens.map((token) => {
347
+ const from = this.offsetAt({ line: token.line, character: token.character });
348
+ return { from, to: from + token.length, scopes: token.scopes };
349
+ })
350
+ );
351
+ }
352
+
353
+ setHighlights(highlights: readonly EditorHighlight[]): void {
354
+ this.view.setOccurrences(
355
+ highlights.map((highlight) => ({ from: this.offsetAt(highlight.range.start), to: this.offsetAt(highlight.range.end), kind: highlight.kind }))
356
+ );
357
+ }
358
+
359
+ setLink(range: EditorRange | null): void {
360
+ this.view.setLink(range === null ? null : { from: this.offsetAt(range.start), to: this.offsetAt(range.end) });
361
+ }
362
+
363
+ getVisibleRange(): EditorRange {
364
+ const { first, last } = this.view.visibleLines();
365
+ return { start: { line: first, character: 0 }, end: this.positionAt(this.model.getLine(last).end) };
366
+ }
367
+
368
+ positionAt(offset: number): EditorPosition {
369
+ const { line, column } = this.model.positionAt(offset);
370
+ return { line, character: column };
371
+ }
372
+
373
+ textInRange(range: EditorRange): string {
374
+ return this.model.slice(this.offsetAt(range.start), this.offsetAt(range.end));
375
+ }
376
+
377
+ offsetAt(position: EditorPosition): number {
378
+ return this.model.offsetAt({ line: Math.max(0, position.line), column: Math.max(0, position.character) });
379
+ }
380
+
381
+ onSave(listener: Listener): () => void {
382
+ return subscribe(this.saves, listener);
383
+ }
384
+
385
+ onBlur(listener: Listener): () => void {
386
+ return subscribe(this.blurs, listener);
387
+ }
388
+
389
+ revealLine(line: number, reveal: EditorReveal = 'center'): void {
390
+ const bounds = this.model.getLine(this.clampLine(line));
391
+ this.model.setSelections([{ anchor: bounds.start, head: bounds.start }]);
392
+ this.view.revealOffset(bounds.start, reveal);
393
+ }
394
+
395
+ find(query: EditorFindQuery | null): void {
396
+ this.view.setFind(query, true);
397
+ }
398
+
399
+ findStep(direction: 1 | -1): void {
400
+ this.view.stepFind(direction);
401
+ }
402
+
403
+ replace(replacement: string, replaceOptions: EditorReplaceOptions = {}): boolean {
404
+ const { find } = this.view;
405
+ const mark = find.currentMark;
406
+ const query = find.activeQuery;
407
+ if (mark === null || query === null || this.blockedByReadOnly()) {
408
+ return false;
409
+ }
410
+ try {
411
+ const options = { caseSensitive: query.caseSensitive, wholeWord: query.wholeWord, regex: query.regex, from: mark.from, to: mark.to, maxResults: 1 };
412
+ const match = this.model.find(query.text, options)[0];
413
+ if (match === undefined) {
414
+ return false;
415
+ }
416
+ // The next match is looked for after what was written, or a replacement that holds the query would find itself again.
417
+ find.pendingAnchor = mark.from + replacementText(this.model.getText(), match, replacement, false, replaceOptions.preserveCase).length;
418
+ if (!this.model.replace(match, replacement, { preserveCase: replaceOptions.preserveCase })) {
419
+ find.pendingAnchor = undefined;
420
+ return false;
421
+ }
422
+ } catch {
423
+ find.pendingAnchor = undefined;
424
+ return false;
425
+ }
426
+ this.view.revealFind();
427
+ this.view.render();
428
+ return true;
429
+ }
430
+
431
+ replaceAll(replacement: string, options: EditorReplaceOptions = {}): number {
432
+ const query = this.view.find.activeQuery;
433
+ if (query === null || this.blockedByReadOnly()) {
434
+ return 0;
435
+ }
436
+ try {
437
+ return this.model.replaceAll(query.text, replacement, {
438
+ caseSensitive: query.caseSensitive,
439
+ wholeWord: query.wholeWord,
440
+ regex: query.regex,
441
+ preserveCase: options.preserveCase,
442
+ ...this.view.find.scope
443
+ });
444
+ } catch {
445
+ return 0;
446
+ }
447
+ }
448
+
449
+ setReplacePreview(replacement: string | null, options: EditorReplaceOptions = {}): void {
450
+ this.view.setReplacePreview(replacement, options.preserveCase === true);
451
+ }
452
+
453
+ selectFindMatches(): number {
454
+ const { find } = this.view;
455
+ const marks = find.matches;
456
+ if (marks.length === 0) {
457
+ return 0;
458
+ }
459
+ const current = find.current ?? marks.length - 1;
460
+ const ordered = [...marks.slice(0, current), ...marks.slice(current + 1), marks[current]!];
461
+ this.view.setFind(null, false);
462
+ for (const mark of ordered) {
463
+ this.view.ensureVisible(mark.from);
464
+ }
465
+ this.model.setSelections(ordered.map((mark) => ({ anchor: mark.from, head: mark.to })));
466
+ this.view.revealCaret();
467
+ return ordered.length;
468
+ }
469
+
470
+ findFromCursor(query: EditorFindQuery, direction: 1 | -1): boolean {
471
+ if (query.text === '') {
472
+ return false;
473
+ }
474
+ let matches;
475
+ try {
476
+ matches = this.model.find(query.text, { caseSensitive: query.caseSensitive, wholeWord: query.wholeWord, regex: query.regex });
477
+ } catch {
478
+ return false;
479
+ }
480
+ if (matches.length === 0) {
481
+ return false;
482
+ }
483
+ const { anchor, head } = this.model.getPrimary();
484
+ const from = Math.min(anchor, head);
485
+ const to = Math.max(anchor, head);
486
+ const target =
487
+ direction === 1 ? (matches.find((match) => match.from >= to) ?? matches[0]!) : (matches.findLast((match) => match.to <= from) ?? matches.at(-1)!);
488
+ this.view.ensureVisible(target.from);
489
+ this.model.setSelections([direction === 1 ? { anchor: target.from, head: target.to } : { anchor: target.to, head: target.from }]);
490
+ this.view.revealCaret(target.from > head ? 'centerDown' : 'centerUp');
491
+ return true;
492
+ }
493
+
494
+ private blockedByReadOnly(): boolean {
495
+ if (!this.settings.readOnly) {
496
+ return false;
497
+ }
498
+ if (this.settings.readOnlyReason) {
499
+ this.view.notify(this.settings.readOnlyReason);
500
+ }
501
+ return true;
502
+ }
503
+
504
+ onFind(listener: (state: EditorFindState) => void): () => void {
505
+ return this.view.onFind(listener);
506
+ }
507
+
508
+ endFind(): void {
509
+ const mark = this.view.find.currentMark;
510
+ this.view.setFind(null, false);
511
+ if (mark) {
512
+ this.view.ensureVisible(mark.from);
513
+ this.model.setSelections([{ anchor: mark.from, head: mark.to }]);
514
+ this.view.revealCaret();
515
+ }
516
+ }
517
+
518
+ setWrap(wrap: boolean): void {
519
+ this.settings.wrap = wrap;
520
+ if (wrap) {
521
+ this.view.viewport.scrollLeft = 0;
522
+ }
523
+ this.view.applySettings();
524
+ }
525
+
526
+ setChangeMarks(marks: readonly EditorChangeMark[]): void {
527
+ this.view.setChangeMarks(marks);
528
+ }
529
+
530
+ setAttributionMarks(marks: readonly EditorAttributionMark[]): void {
531
+ this.view.setAttributionMarks(marks);
532
+ }
533
+
534
+ setLineHighlights(highlights: readonly EditorLineHighlight[]): void {
535
+ this.view.setLineHighlights(highlights);
536
+ }
537
+
538
+ renderCode(container: HTMLElement, text: string, options: EditorCodeBlockOptions = {}): void {
539
+ this.view.renderCode(container, text, options);
540
+ }
541
+
542
+ setRemoteCursors(cursors: readonly EditorRemoteCursor[]): void {
543
+ this.view.setRemoteCarets(cursors.map((cursor) => ({ id: cursor.id, name: cursor.name, color: cursor.color, at: this.offsetAt(cursor.position) })));
544
+ }
545
+
546
+ onAttributionHover(listener: (hover: EditorAttributionHover | null) => void): () => void {
547
+ return this.view.onAttributionHover(listener);
548
+ }
549
+
550
+ setWidgets(widgets: readonly EditorWidget[], owner: string = DEFAULT_WIDGET_OWNER): void {
551
+ this.view.setBlockWidgets(
552
+ owner,
553
+ widgets.map((widget) => ({
554
+ id: `widget:${owner}:${widget.id}`,
555
+ at: this.offsetAt({ line: widget.line, character: 0 }),
556
+ placement: widget.placement ?? 'below',
557
+ height: widget.height,
558
+ render: (container: HTMLElement) => {
559
+ container.classList.add('se-widget');
560
+ widget.render(container);
561
+ }
562
+ }))
563
+ );
564
+ }
565
+
566
+ setLineActions(actions: readonly EditorLineAction[], owner: string = DEFAULT_WIDGET_OWNER): void {
567
+ this.view.setLineActions(
568
+ owner,
569
+ actions.map((action) => ({
570
+ id: action.id,
571
+ at: this.offsetAt({ line: action.line, character: 0 }),
572
+ render: (container) => action.render(container)
573
+ }))
574
+ );
575
+ }
576
+
577
+ trackRange(range: EditorRange): EditorTrackedRange {
578
+ const tracked = this.view.trackRange(this.offsetAt(range.start), this.offsetAt(range.end));
579
+ return {
580
+ get: () => {
581
+ const offsets = tracked.get();
582
+ return offsets === null ? null : { start: this.positionAt(offsets.from), end: this.positionAt(offsets.to) };
583
+ },
584
+ dispose: () => tracked.dispose()
585
+ };
586
+ }
587
+
588
+ setCodeVision(rows: readonly EditorCodeVision[]): void {
589
+ const current = new Map(rows.map((row) => [row.id, row]));
590
+ const revisions = new Map<string, { signature: string; revision: number }>();
591
+ for (const row of rows) {
592
+ const signature = lensSignature(row.entries);
593
+ const before = this.lensRevisions.get(row.id);
594
+ revisions.set(row.id, {
595
+ signature,
596
+ revision: before === undefined || before.signature === signature ? (before?.revision ?? 0) : before.revision + 1
597
+ });
598
+ }
599
+ this.lensRows = current;
600
+ this.lensRevisions = revisions;
601
+ this.view.setLenses(
602
+ rows.map((row) => ({
603
+ id: `lens:${row.id}`,
604
+ at: this.indentOffsetOf(row.line),
605
+ placement: 'above' as const,
606
+ lens: true,
607
+ revision: revisions.get(row.id)!.revision,
608
+ render: (container: HTMLElement) =>
609
+ fillLens(container, row.entries, (id) => this.lensRows.get(row.id)?.entries.find((entry) => entry.id === id))
610
+ }))
611
+ );
612
+ }
613
+
614
+ /* Where the text of a line begins after its indentation, so a row sitting there stays with the declaration when something is typed in front of it. */
615
+ private indentOffsetOf(line: number): number {
616
+ const { start, text } = this.model.getLine(Math.max(0, Math.min(line, this.model.getLineCount() - 1)));
617
+ return start + (/^[ \t]*/.exec(text)?.[0].length ?? 0);
618
+ }
619
+
620
+ setGutterAction(action: EditorGutterAction | null): void {
621
+ this.view.setGutterAction(action);
622
+ }
623
+
624
+ onGutterAction(listener: (line: number) => void): () => void {
625
+ return this.view.onGutterAction(listener);
626
+ }
627
+
628
+ setGutterMarkers(markers: readonly EditorGutterMarker[], owner: string = 'default'): void {
629
+ this.view.setGutterMarkers(owner, markers);
630
+ }
631
+
632
+ onGutterMarker(listener: (id: string) => void): () => void {
633
+ return this.view.onGutterMarker(listener);
634
+ }
635
+
636
+ setBlocks(blocks: readonly EditorBlock[] | null): void {
637
+ this.view.setBlocks(blocks);
638
+ }
639
+
640
+ onScope(listener: (scope: readonly EditorBlock[]) => void): () => void {
641
+ return this.view.onScope(listener);
642
+ }
643
+
644
+ setGuides(guides: boolean): void {
645
+ this.settings.guides = guides;
646
+ this.view.applySettings();
647
+ }
648
+
649
+ setWhitespace(whitespace: boolean): void {
650
+ this.settings.whitespace = whitespace;
651
+ this.view.applySettings();
652
+ }
653
+
654
+ setRightMargin(column: number | null): void {
655
+ this.settings.rightMargin = column;
656
+ this.view.applySettings();
657
+ }
658
+
659
+ setFoldOutline(outline: EditorFoldOutline): void {
660
+ this.settings.foldOutline = outline;
661
+ this.view.applySettings();
662
+ }
663
+
664
+ setLabel(label: string): void {
665
+ this.settings.label = label;
666
+ this.view.applySettings();
667
+ }
668
+
669
+ 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
+ from: this.model.getLine(Math.min(range.startLine, this.model.getLineCount() - 1)).start,
681
+ to: this.model.getLine(Math.min(range.endLine, this.model.getLineCount() - 1)).start,
682
+ ...(range.kind === undefined ? {} : { kind: range.kind })
683
+ }))
684
+ }
685
+ );
686
+ }
687
+
688
+ setSmartKeys(keys: Partial<EditorSmartKeys>): void {
689
+ this.settings.smartKeys = { ...this.settings.smartKeys, ...keys };
690
+ }
691
+
692
+ setIndentation(indentation: EditorIndentation): void {
693
+ if (indentation.tabSize === this.settings.tabSize && indentation.insertSpaces === this.settings.insertSpaces) {
694
+ return;
695
+ }
696
+ this.settings.tabSize = indentation.tabSize;
697
+ this.settings.insertSpaces = indentation.insertSpaces;
698
+ this.view.applySettings();
699
+ }
700
+
701
+ setTheme(theme: EditorTheme): void {
702
+ if (theme !== this.theme) {
703
+ this.theme = theme;
704
+ this.loadTokenizer();
705
+ }
706
+ }
707
+
708
+ refreshFont(): void {
709
+ this.view.refreshFont();
710
+ }
711
+
712
+ setReadOnly(readOnly: boolean, reason?: string): void {
713
+ this.settings.readOnly = readOnly;
714
+ this.settings.readOnlyReason = reason;
715
+ this.view.applySettings();
716
+ }
717
+
718
+ runCommand(command: EditorRunCommand): boolean {
719
+ return this.controller.command(command);
720
+ }
721
+
722
+ getFolds(): EditorFolds {
723
+ return this.view.getFolds();
724
+ }
725
+
726
+ focus(): void {
727
+ this.view.focus();
728
+ }
729
+
730
+ dispose(): void {
731
+ if (this.disposed) {
732
+ return;
733
+ }
734
+ this.disposed = true;
735
+ this.changes.clear();
736
+ this.textChanges.clear();
737
+ this.carets.clear();
738
+ this.saves.clear();
739
+ this.blurs.clear();
740
+ this.subscription.dispose();
741
+ this.controller.dispose();
742
+ this.view.dispose();
743
+ }
744
+
745
+ private clampLine(line: number): number {
746
+ return Math.min(Math.max(1, Math.trunc(line) || 1), this.model.getLineCount()) - 1;
747
+ }
748
+ }
749
+
750
+ class SmartEditorEngine implements EditorEngine {
751
+ private readonly options: SmartEditorEngineOptions;
752
+
753
+ constructor(options: SmartEditorEngineOptions) {
754
+ this.options = options;
755
+ }
756
+
757
+ mount(element: HTMLElement, options: EditorOptions): Editor {
758
+ return new SmartEditor(this.options, element, options);
759
+ }
760
+ }
761
+
762
+ export function createSmartEditorEngine(options: SmartEditorEngineOptions): EditorEngine {
763
+ return new SmartEditorEngine(options);
764
+ }