@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/src/editor.css CHANGED
@@ -12,10 +12,14 @@
12
12
  --editor-guide-active: color-mix(in srgb, var(--text) 24%, transparent);
13
13
  --editor-margin: color-mix(in srgb, var(--text) 14%, transparent);
14
14
  --editor-whitespace: color-mix(in srgb, var(--text) 30%, transparent);
15
+ --editor-bidi: var(--status-needs-you);
15
16
  --editor-hatch: color-mix(in srgb, var(--text) 12%, transparent);
16
17
  --editor-added: var(--status-idle);
17
18
  --editor-modified: var(--accent);
18
19
  --editor-deleted: var(--status-error);
20
+ --editor-mark-number-mix: 72%;
21
+ --editor-mark-bar-width: 3px;
22
+ --editor-mark-gap: 4px;
19
23
  --editor-sticky-shadow: color-mix(in srgb, black 12%, transparent);
20
24
  --agent-1: #c4602f;
21
25
  --agent-2: #7c4dd8;
@@ -34,6 +38,7 @@
34
38
  --editor-margin: color-mix(in srgb, var(--text) 16%, transparent);
35
39
  --editor-whitespace: color-mix(in srgb, var(--text) 32%, transparent);
36
40
  --editor-hatch: color-mix(in srgb, var(--text) 10%, transparent);
41
+ --editor-mark-number-mix: 82%;
37
42
  --editor-sticky-shadow: color-mix(in srgb, black 60%, transparent);
38
43
  }
39
44
 
@@ -69,6 +74,7 @@
69
74
  overflow: auto;
70
75
  overflow-anchor: none;
71
76
  overscroll-behavior: contain;
77
+ touch-action: pan-x pan-y pinch-zoom;
72
78
  scrollbar-width: thin;
73
79
  scrollbar-color: color-mix(in srgb, var(--text) 25%, transparent) transparent;
74
80
  }
@@ -177,10 +183,23 @@
177
183
  white-space: pre;
178
184
  letter-spacing: 0;
179
185
  direction: ltr;
186
+ unicode-bidi: bidi-override;
180
187
  }
181
188
  .se-run {
182
189
  position: absolute;
183
190
  white-space: pre;
191
+ unicode-bidi: bidi-override;
192
+ }
193
+ .se-bidi {
194
+ position: absolute;
195
+ box-sizing: border-box;
196
+ overflow: hidden;
197
+ border-radius: var(--radius-xs);
198
+ box-shadow: inset 0 0 0 1px var(--editor-bidi);
199
+ color: var(--editor-bidi);
200
+ text-align: center;
201
+ user-select: none;
202
+ pointer-events: none;
184
203
  }
185
204
 
186
205
  .se-gutter {
@@ -216,6 +235,31 @@
216
235
  .se-active-number {
217
236
  color: var(--text-muted);
218
237
  }
238
+ /* A line a host marked with a bar: the number in a shade of the mark that reads on the tint, and a bar along the edge. */
239
+ /* The tint stops short of the code, so the cell and the line read as two surfaces. */
240
+ .se-line-marked {
241
+ align-items: flex-start;
242
+ background: linear-gradient(to right, var(--se-tint, transparent) calc(100% - var(--editor-mark-gap)), transparent 0);
243
+ }
244
+ .se-line-marked > span:first-child {
245
+ display: flex;
246
+ align-items: center;
247
+ height: var(--se-line);
248
+ color: color-mix(in srgb, var(--se-mark) var(--editor-mark-number-mix), var(--text));
249
+ }
250
+ .se-line-bar {
251
+ position: absolute;
252
+ top: 0;
253
+ left: 0;
254
+ width: var(--editor-mark-bar-width);
255
+ height: 100%;
256
+ }
257
+ .se-line-bar-solid {
258
+ background: var(--se-mark);
259
+ }
260
+ .se-line-bar-hatched {
261
+ background: repeating-linear-gradient(135deg, var(--se-mark) 0 2px, transparent 2px 4px);
262
+ }
219
263
  .se-fold-toggle {
220
264
  position: absolute;
221
265
  top: 0;
@@ -299,6 +343,14 @@
299
343
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z'/%3E%3C/svg%3E")
300
344
  center / contain no-repeat;
301
345
  }
346
+ .se-gutter-marker[aria-pressed='true'] {
347
+ color: var(--accent);
348
+ }
349
+ /* Lucide's book-open. */
350
+ .se-gutter-marker[data-icon='book-open']::before {
351
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 7v14'/%3E%3Cpath d='M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z'/%3E%3C/svg%3E")
352
+ center / contain no-repeat;
353
+ }
302
354
  .se-change {
303
355
  position: absolute;
304
356
  top: 0;
@@ -337,9 +389,13 @@
337
389
  font-size: max(12px, calc(var(--code-font-size, 13px) - 2px));
338
390
  line-height: 1;
339
391
  }
340
- .se-line-highlight {
392
+ .se-line-highlight,
393
+ .se-line-emphasis {
341
394
  position: absolute;
342
395
  }
396
+ .se-line-emphasis {
397
+ border-radius: 2px;
398
+ }
343
399
 
344
400
  /* Code a host draws in a widget row: the editor's face, with the gutter column the document has. */
345
401
  .se-code-block {
@@ -685,6 +741,11 @@
685
741
  white-space: pre;
686
742
  cursor: default;
687
743
  }
744
+ .se-inlay-link {
745
+ text-decoration: underline;
746
+ text-underline-offset: 2px;
747
+ cursor: pointer;
748
+ }
688
749
  .se-composition {
689
750
  position: absolute;
690
751
  white-space: pre;
@@ -717,6 +778,13 @@
717
778
  white-space: pre-wrap;
718
779
  overflow-wrap: anywhere;
719
780
  }
781
+ /* Lines the host drew in place of the text: a row in the text column that scrolls with the code, selectable as the page's own text is. */
782
+ .se-block.se-replacement {
783
+ padding: 0;
784
+ color: var(--text);
785
+ white-space: normal;
786
+ user-select: text;
787
+ }
720
788
  /* The host's own row runs the width of the editor over the gutter and under the pinned headers, so the code layer has no stacking of its own. */
721
789
  .se-block.se-widget {
722
790
  z-index: 4;
@@ -790,6 +858,18 @@
790
858
  white-space: pre;
791
859
  pointer-events: none;
792
860
  }
861
+ .se-sr-only,
862
+ .se-live {
863
+ position: absolute;
864
+ width: 1px;
865
+ height: 1px;
866
+ margin: -1px;
867
+ padding: 0;
868
+ overflow: hidden;
869
+ clip-path: inset(50%);
870
+ white-space: nowrap;
871
+ border: 0;
872
+ }
793
873
  .se-notice {
794
874
  position: absolute;
795
875
  right: 16px;
package/src/engine.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { changedSpans, DocumentModel, type EditorSnapshot, type FoldHints, replacementText } from '@adecore/editor-core';
2
+ import { MAX_REPLACEMENT_CHARACTERS } from './search-protocol.ts';
3
+ import { GrammarPreparation } from './grammar-preparation.ts';
2
4
  import { InputController } from './controller.ts';
3
5
  import { resolveSmartKeys } from './smart-keys.ts';
4
6
  import { emit, type Listener, subscribe } from './listeners.ts';
@@ -10,6 +12,7 @@ import type {
10
12
  EditorCodeBlockOptions,
11
13
  EditorLineAction,
12
14
  EditorLineHighlight,
15
+ LineToken,
13
16
  EditorChangeMark,
14
17
  EditorCodeVision,
15
18
  EditorClickHandler,
@@ -25,12 +28,17 @@ import type {
25
28
  EditorReplaceOptions,
26
29
  EditorGutterAction,
27
30
  EditorGutterMarker,
31
+ EditorReplacement,
28
32
  EditorHighlight,
29
33
  EditorHover,
30
34
  EditorGhostText,
35
+ EditorInlayClickHandler,
31
36
  EditorInlayHint,
37
+ EditorInlayTarget,
32
38
  EditorKeyHandler,
39
+ EditorInputPopup,
33
40
  EditorMarker,
41
+ EditorMessages,
34
42
  EditorOptions,
35
43
  EditorPosition,
36
44
  EditorRemoteCursor,
@@ -41,6 +49,7 @@ import type {
41
49
  EditorSelectionRanges,
42
50
  EditorSemanticToken,
43
51
  EditorSmartKeys,
52
+ EditorApplyEditsOptions,
44
53
  EditorTextChange,
45
54
  EditorTrackedRange,
46
55
  EditorTheme,
@@ -53,12 +62,15 @@ import { EditorView, type ViewSettings } from './view.ts';
53
62
  const DEFAULT_TAB_SIZE = 4;
54
63
  const DEFAULT_LABEL = 'Code editor';
55
64
  const DEFAULT_WIDGET_OWNER = 'default';
65
+ /* The space a hint's padding keeps clear; a whole pixel count so the pill stays on the pixel grid. */
66
+ const INLAY_GAP = 4;
56
67
 
57
68
  class SmartEditor implements Editor {
58
69
  private readonly model: DocumentModel;
59
70
  private readonly settings: ViewSettings;
60
71
  private readonly view: EditorView;
61
72
  private readonly controller: InputController;
73
+ private readonly grammarPreparation: GrammarPreparation;
62
74
  private readonly changes = new Set<Listener>();
63
75
  private readonly textChanges = new Set<(change: EditorTextChange) => void>();
64
76
  private readonly carets = new Set<(position: EditorPosition) => void>();
@@ -83,6 +95,7 @@ class SmartEditor implements Editor {
83
95
  this.language = options.language;
84
96
  this.theme = options.theme;
85
97
  this.settings = {
98
+ searchWorker: engine.searchWorker,
86
99
  language: options.language,
87
100
  tabSize: options.indentation?.tabSize ?? DEFAULT_TAB_SIZE,
88
101
  insertSpaces: options.indentation?.insertSpaces ?? true,
@@ -95,11 +108,13 @@ class SmartEditor implements Editor {
95
108
  guides: options.guides !== false,
96
109
  whitespace: options.whitespace === true,
97
110
  rightMargin: options.rightMargin ?? null,
98
- foldOutline: options.foldOutline ?? 'hover'
111
+ foldOutline: options.foldOutline ?? 'hover',
112
+ folding: options.folding !== false
99
113
  };
100
114
  this.view = new EditorView(element, this.model, this.settings);
115
+ this.grammarPreparation = new GrammarPreparation(this.model, () => this.view.grammarChanged());
101
116
  this.controller = new InputController(this.view, {
102
- handBack: engine.handBack ?? [],
117
+ handBack: [...(engine.handBack ?? []), ...(options.handBack ?? [])],
103
118
  keymap: engine.keymap,
104
119
  apple: engine.apple ?? false,
105
120
  save: () => emit(this.saves),
@@ -111,8 +126,9 @@ class SmartEditor implements Editor {
111
126
  this.caretMoved(snapshot.selections.at(-1)!.head);
112
127
  if (snapshot.revision !== this.revision) {
113
128
  this.revision = snapshot.revision;
129
+ this.grammarPreparation.edited(snapshot.changes ?? []);
114
130
  if (snapshot.contentEdits !== undefined && snapshot.contentEdits.length > 0) {
115
- this.announceTextChange(snapshot.contentEdits, snapshot.source ?? 'external');
131
+ this.announceTextChange(snapshot.contentEdits, snapshot.source ?? 'external', snapshot.typed === true);
116
132
  }
117
133
  if (!this.settingText) {
118
134
  emit(this.changes);
@@ -147,6 +163,7 @@ class SmartEditor implements Editor {
147
163
 
148
164
  private loadTokenizer(): void {
149
165
  const request = ++this.tokenizerRequest;
166
+ this.grammarPreparation.begin();
150
167
  void this.engine
151
168
  .scopeColors?.(this.theme)
152
169
  .catch(() => null)
@@ -161,6 +178,7 @@ class SmartEditor implements Editor {
161
178
  .then((tokenizer) => {
162
179
  if (!this.disposed && request === this.tokenizerRequest) {
163
180
  this.view.setTokenizer(tokenizer);
181
+ this.grammarPreparation.ready(tokenizer);
164
182
  }
165
183
  });
166
184
  }
@@ -194,9 +212,10 @@ class SmartEditor implements Editor {
194
212
  return subscribe(this.textChanges, listener);
195
213
  }
196
214
 
197
- private announceTextChange(edits: NonNullable<EditorSnapshot['contentEdits']>, source: EditorTextChange['source']): void {
215
+ private announceTextChange(edits: NonNullable<EditorSnapshot['contentEdits']>, source: EditorTextChange['source'], typed: boolean): void {
198
216
  const change: EditorTextChange = {
199
217
  source,
218
+ ...(typed ? { typed } : {}),
200
219
  changes: edits.map((edit) => ({
201
220
  range: { start: { line: edit.start.line, character: edit.start.column }, end: { line: edit.end.line, character: edit.end.column } },
202
221
  text: edit.text
@@ -222,6 +241,26 @@ class SmartEditor implements Editor {
222
241
  return this.view.scrollPlace.y;
223
242
  }
224
243
 
244
+ setScrollTop(top: number): void {
245
+ this.view.setScrollTop(top);
246
+ }
247
+
248
+ getLineHeight(line: number): number {
249
+ return this.view.getLineHeight(line);
250
+ }
251
+
252
+ getBaseLineHeight(): number {
253
+ return this.view.layout.metrics.lineHeight;
254
+ }
255
+
256
+ onLineHeights(listener: Listener): () => void {
257
+ return this.view.onLineHeights(listener);
258
+ }
259
+
260
+ getContentHeight(): number {
261
+ return this.view.contentHeight;
262
+ }
263
+
225
264
  getSelection(): EditorRange {
226
265
  return this.rangeOf(this.model.getPrimary());
227
266
  }
@@ -295,14 +334,32 @@ class SmartEditor implements Editor {
295
334
  this.controller.setSelectionRanges(provider);
296
335
  }
297
336
 
298
- applyEdits(edits: readonly EditorContentChange[]): boolean {
337
+ applyEdits(edits: readonly EditorContentChange[], options: EditorApplyEditsOptions = {}): boolean {
299
338
  if (this.settings.readOnly || edits.length === 0) {
300
339
  return false;
301
340
  }
302
341
  try {
342
+ const { mapSelections } = options;
303
343
  return this.model.applyEdits(
304
344
  edits.map((edit) => ({ from: this.offsetAt(edit.range.start), to: this.offsetAt(edit.range.end), text: edit.text })),
305
- { source: 'command' }
345
+ {
346
+ source: 'command',
347
+ joinPrevious: options.mergeUndo === true,
348
+ selectionPositions:
349
+ mapSelections &&
350
+ this.model.getSelections().map((selection) => {
351
+ const { start, end } = mapSelections(this.rangeOf(selection));
352
+ const from = { line: start.line, column: start.character };
353
+ const to = { line: end.line, column: end.character };
354
+ if (selection.head < selection.anchor) {
355
+ return { anchor: to, head: from };
356
+ }
357
+ return {
358
+ anchor: from,
359
+ head: selection.head === selection.anchor && start.line === end.line && start.character === end.character ? from : to
360
+ };
361
+ })
362
+ }
306
363
  );
307
364
  } catch {
308
365
  return false;
@@ -323,7 +380,28 @@ class SmartEditor implements Editor {
323
380
  }
324
381
 
325
382
  setInlayHints(hints: readonly EditorInlayHint[]): void {
326
- this.view.setInlays(hints.map((hint, index) => ({ id: `hint-${index}`, at: this.offsetAt(hint.position), text: hint.label })));
383
+ this.view.setInlays(
384
+ hints.map((hint, index) => ({
385
+ id: hint.id ?? `hint-${index}`,
386
+ at: this.offsetAt(hint.position),
387
+ text: hint.label,
388
+ ...(hint.parts === undefined ? {} : { parts: hint.parts }),
389
+ ...(hint.paddingLeft ? { gapLeft: INLAY_GAP } : {}),
390
+ ...(hint.paddingRight ? { gapRight: INLAY_GAP } : {})
391
+ }))
392
+ );
393
+ }
394
+
395
+ onInlayHover(listener: (target: EditorInlayTarget | null) => void): () => void {
396
+ return this.view.onInlayHover(listener);
397
+ }
398
+
399
+ onInlayClick(handler: EditorInlayClickHandler): () => void {
400
+ return this.controller.onInlayClick(handler);
401
+ }
402
+
403
+ setInlayLink(target: { readonly id: string; readonly part: number | null } | null): void {
404
+ this.view.setInlayLink(target);
327
405
  }
328
406
 
329
407
  setGhostText(ghost: EditorGhostText | null): void {
@@ -336,7 +414,7 @@ class SmartEditor implements Editor {
336
414
  ? null
337
415
  : tokens.map((token) => {
338
416
  const from = this.offsetAt({ line: token.line, character: token.character });
339
- return { from, to: from + token.length, scopes: token.scopes };
417
+ return { from, to: from + token.length, scopes: token.scopes, within: token.within };
340
418
  })
341
419
  );
342
420
  }
@@ -393,19 +471,23 @@ class SmartEditor implements Editor {
393
471
 
394
472
  replace(replacement: string, replaceOptions: EditorReplaceOptions = {}): boolean {
395
473
  const { find } = this.view;
396
- const mark = find.currentMark;
397
- const query = find.activeQuery;
398
- if (mark === null || query === null || this.blockedByReadOnly()) {
474
+ const match = find.currentMatch;
475
+ if (this.blockedByReadOnly()) {
399
476
  return false;
400
477
  }
401
- try {
402
- const options = { caseSensitive: query.caseSensitive, wholeWord: query.wholeWord, regex: query.regex, from: mark.from, to: mark.to, maxResults: 1 };
403
- const match = this.model.find(query.text, options)[0];
404
- if (match === undefined) {
478
+ if (find.activeQuery?.regex === true) {
479
+ if (find.currentMark === null) {
405
480
  return false;
406
481
  }
482
+ void find.replaceOne(replacement, replaceOptions.preserveCase === true, () => !this.disposed && !this.settings.readOnly);
483
+ return true;
484
+ }
485
+ if (match === null) {
486
+ return false;
487
+ }
488
+ try {
407
489
  // The next match is looked for after what was written, or a replacement that holds the query would find itself again.
408
- find.pendingAnchor = mark.from + replacementText(this.model.getText(), match, replacement, false, replaceOptions.preserveCase).length;
490
+ find.pendingAnchor = match.to;
409
491
  if (!this.model.replace(match, replacement, { preserveCase: replaceOptions.preserveCase })) {
410
492
  find.pendingAnchor = undefined;
411
493
  return false;
@@ -420,23 +502,49 @@ class SmartEditor implements Editor {
420
502
  }
421
503
 
422
504
  replaceAll(replacement: string, options: EditorReplaceOptions = {}): number {
423
- const query = this.view.find.activeQuery;
424
- if (query === null || this.blockedByReadOnly()) {
505
+ const { find } = this.view;
506
+ const query = find.activeQuery;
507
+ if (query === null || find.state.noSelection === true || this.blockedByReadOnly()) {
425
508
  return 0;
426
509
  }
427
510
  try {
428
- return this.model.replaceAll(query.text, replacement, {
429
- caseSensitive: query.caseSensitive,
430
- wholeWord: query.wholeWord,
431
- regex: query.regex,
432
- preserveCase: options.preserveCase,
433
- ...this.view.find.scope
434
- });
435
- } catch {
511
+ const matches = find.completeMatches;
512
+ if (matches !== null) {
513
+ let characters = 0;
514
+ for (const match of matches) {
515
+ characters += replacementText('', match, replacement, false, options.preserveCase, MAX_REPLACEMENT_CHARACTERS).length;
516
+ if (characters > MAX_REPLACEMENT_CHARACTERS) {
517
+ find.reportReplaceFailed('limit');
518
+ return 0;
519
+ }
520
+ }
521
+ const count = this.model.replaceMatches(matches, replacement, { preserveCase: options.preserveCase });
522
+ find.reportReplaced(count);
523
+ return count;
524
+ }
525
+ void this.replaceAllAsync(replacement, options);
526
+ return 0;
527
+ } catch (error) {
528
+ find.reportReplaceFailed(error instanceof RangeError ? 'limit' : 'error');
436
529
  return 0;
437
530
  }
438
531
  }
439
532
 
533
+ async replaceAllAsync(replacement: string, options: EditorReplaceOptions = {}): Promise<number> {
534
+ const { find } = this.view;
535
+ if (this.disposed || this.blockedByReadOnly() || find.activeQuery === null || find.state.noSelection) {
536
+ return 0;
537
+ }
538
+ if (find.state.limited) {
539
+ find.reportReplaceFailed('limit');
540
+ return 0;
541
+ }
542
+ if (find.completeMatches !== null) {
543
+ return this.replaceAll(replacement, options);
544
+ }
545
+ return find.replaceAll(replacement, options.preserveCase === true, () => !this.disposed && !this.settings.readOnly);
546
+ }
547
+
440
548
  setReplacePreview(replacement: string | null, options: EditorReplaceOptions = {}): void {
441
549
  this.view.setReplacePreview(replacement, options.preserveCase === true);
442
550
  }
@@ -462,24 +570,39 @@ class SmartEditor implements Editor {
462
570
  if (query.text === '') {
463
571
  return false;
464
572
  }
465
- let matches;
573
+ const { anchor, head } = this.model.getPrimary();
574
+ const offset = direction === 1 ? Math.max(anchor, head) : Math.min(anchor, head);
575
+ const reveal = (target: { from: number; to: number } | null): boolean => {
576
+ if (target === null || this.disposed) {
577
+ return false;
578
+ }
579
+ this.view.ensureVisible(target.from);
580
+ this.model.setSelections([direction === 1 ? { anchor: target.from, head: target.to } : { anchor: target.to, head: target.from }]);
581
+ this.view.revealCaret(target.from > head ? 'centerDown' : 'centerUp');
582
+ return true;
583
+ };
584
+ if (query.regex) {
585
+ const revision = this.model.getRevision();
586
+ void this.view.find.nextFromCursor(query, offset, direction).then((target) => {
587
+ const current = this.model.getPrimary();
588
+ if (this.model.getRevision() === revision && current.anchor === anchor && current.head === head) {
589
+ reveal(target);
590
+ }
591
+ });
592
+ return true;
593
+ }
466
594
  try {
467
- matches = this.model.find(query.text, { caseSensitive: query.caseSensitive, wholeWord: query.wholeWord, regex: query.regex });
595
+ return reveal(
596
+ this.model.findNext(query.text, offset, {
597
+ caseSensitive: query.caseSensitive,
598
+ wholeWord: query.wholeWord,
599
+ backwards: direction === -1,
600
+ wrap: true
601
+ })
602
+ );
468
603
  } catch {
469
604
  return false;
470
605
  }
471
- if (matches.length === 0) {
472
- return false;
473
- }
474
- const { anchor, head } = this.model.getPrimary();
475
- const from = Math.min(anchor, head);
476
- const to = Math.max(anchor, head);
477
- const target =
478
- direction === 1 ? (matches.find((match) => match.from >= to) ?? matches[0]!) : (matches.findLast((match) => match.to <= from) ?? matches.at(-1)!);
479
- this.view.ensureVisible(target.from);
480
- this.model.setSelections([direction === 1 ? { anchor: target.from, head: target.to } : { anchor: target.to, head: target.from }]);
481
- this.view.revealCaret(target.from > head ? 'centerDown' : 'centerUp');
482
- return true;
483
606
  }
484
607
 
485
608
  private blockedByReadOnly(): boolean {
@@ -514,8 +637,8 @@ class SmartEditor implements Editor {
514
637
  this.view.applySettings();
515
638
  }
516
639
 
517
- setChangeMarks(marks: readonly EditorChangeMark[]): void {
518
- this.view.setChangeMarks(marks);
640
+ setChangeMarks(marks: readonly EditorChangeMark[], layer?: string): void {
641
+ this.view.setChangeMarks(marks, layer);
519
642
  }
520
643
 
521
644
  setAttributionMarks(marks: readonly EditorAttributionMark[]): void {
@@ -526,6 +649,32 @@ class SmartEditor implements Editor {
526
649
  this.view.setLineHighlights(highlights);
527
650
  }
528
651
 
652
+ setLineLabels(labels: ((line: number) => string | null) | null): void {
653
+ this.view.setLineLabels(labels);
654
+ }
655
+
656
+ setInputPopup(popup: EditorInputPopup | null): void {
657
+ this.view.setInputPopup(popup);
658
+ }
659
+
660
+ announce(message: string): void {
661
+ this.view.announce(message);
662
+ }
663
+
664
+ setMessages(messages: Partial<EditorMessages>): void {
665
+ this.settings.messages = { ...this.settings.messages, ...messages };
666
+ this.view.repaint();
667
+ }
668
+
669
+ setLineNumbers(labels: ((line: number) => string | null) | null): void {
670
+ this.view.setLineNumbers(labels);
671
+ }
672
+
673
+ setLineTokens(tokens: ((line: number) => readonly LineToken[] | null | undefined) | null): void {
674
+ this.grammarPreparation.pause(tokens !== null);
675
+ this.view.setLineTokens(tokens);
676
+ }
677
+
529
678
  renderCode(container: HTMLElement, text: string, options: EditorCodeBlockOptions = {}): void {
530
679
  this.view.renderCode(container, text, options);
531
680
  }
@@ -546,6 +695,7 @@ class SmartEditor implements Editor {
546
695
  at: this.offsetAt({ line: widget.line, character: 0 }),
547
696
  placement: widget.placement ?? 'below',
548
697
  height: widget.height,
698
+ fixed: widget.fixed,
549
699
  render: (container: HTMLElement) => {
550
700
  container.classList.add('se-widget');
551
701
  widget.render(container);
@@ -554,6 +704,25 @@ class SmartEditor implements Editor {
554
704
  );
555
705
  }
556
706
 
707
+ setReplacements(replacements: readonly EditorReplacement[], owner: string = DEFAULT_WIDGET_OWNER): void {
708
+ this.view.setReplacements(
709
+ owner,
710
+ replacements.map((replacement) => ({
711
+ id: `replacement:${owner}:${replacement.id}`,
712
+ from: this.offsetAt({ line: replacement.startLine, character: 0 }),
713
+ to: this.model.getLine(Math.max(0, Math.min(replacement.endLine, this.model.getLineCount() - 1))).end,
714
+ height: replacement.height,
715
+ label: replacement.label,
716
+ enterable: replacement.enterable,
717
+ onReveal: replacement.onReveal === undefined ? undefined : () => replacement.onReveal!(),
718
+ render: (container: HTMLElement) => {
719
+ container.classList.add('se-widget-replacement');
720
+ replacement.render(container);
721
+ }
722
+ }))
723
+ );
724
+ }
725
+
557
726
  setLineActions(actions: readonly EditorLineAction[], owner: string = DEFAULT_WIDGET_OWNER): void {
558
727
  this.view.setLineActions(
559
728
  owner,
@@ -714,6 +883,10 @@ class SmartEditor implements Editor {
714
883
  this.view.applySettings();
715
884
  }
716
885
 
886
+ isReadOnly(): boolean {
887
+ return this.settings.readOnly;
888
+ }
889
+
717
890
  runCommand(command: EditorRunCommand): boolean {
718
891
  return this.controller.command(command);
719
892
  }
@@ -731,6 +904,7 @@ class SmartEditor implements Editor {
731
904
  return;
732
905
  }
733
906
  this.disposed = true;
907
+ this.grammarPreparation.dispose();
734
908
  this.changes.clear();
735
909
  this.textChanges.clear();
736
910
  this.carets.clear();