@adecore/editor 0.22.1 → 0.23.1

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 +189 -41
  10. package/dist/fake.d.ts +45 -5
  11. package/dist/fake.js +121 -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 +86 -2
  55. package/dist/uniform-geometry.d.ts +7 -0
  56. package/dist/uniform-geometry.js +49 -0
  57. package/dist/view.d.ts +69 -17
  58. package/dist/view.js +427 -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 +222 -42
  68. package/src/fake.ts +157 -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 +178 -8
  92. package/src/uniform-geometry.ts +57 -0
  93. package/src/view.ts +505 -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,
@@ -93,13 +106,16 @@ class SmartEditor implements Editor {
93
106
  messages: options.messages ?? {},
94
107
  label: options.label ?? DEFAULT_LABEL,
95
108
  guides: options.guides !== false,
109
+ stickyScroll: options.stickyScroll !== false,
96
110
  whitespace: options.whitespace === true,
97
111
  rightMargin: options.rightMargin ?? null,
98
- foldOutline: options.foldOutline ?? 'hover'
112
+ foldOutline: options.foldOutline ?? 'hover',
113
+ folding: options.folding !== false
99
114
  };
100
115
  this.view = new EditorView(element, this.model, this.settings);
116
+ this.grammarPreparation = new GrammarPreparation(this.model, () => this.view.grammarChanged());
101
117
  this.controller = new InputController(this.view, {
102
- handBack: engine.handBack ?? [],
118
+ handBack: [...(engine.handBack ?? []), ...(options.handBack ?? [])],
103
119
  keymap: engine.keymap,
104
120
  apple: engine.apple ?? false,
105
121
  save: () => emit(this.saves),
@@ -111,8 +127,9 @@ class SmartEditor implements Editor {
111
127
  this.caretMoved(snapshot.selections.at(-1)!.head);
112
128
  if (snapshot.revision !== this.revision) {
113
129
  this.revision = snapshot.revision;
130
+ this.grammarPreparation.edited(snapshot.changes ?? []);
114
131
  if (snapshot.contentEdits !== undefined && snapshot.contentEdits.length > 0) {
115
- this.announceTextChange(snapshot.contentEdits, snapshot.source ?? 'external');
132
+ this.announceTextChange(snapshot.contentEdits, snapshot.source ?? 'external', snapshot.typed === true);
116
133
  }
117
134
  if (!this.settingText) {
118
135
  emit(this.changes);
@@ -147,6 +164,7 @@ class SmartEditor implements Editor {
147
164
 
148
165
  private loadTokenizer(): void {
149
166
  const request = ++this.tokenizerRequest;
167
+ this.grammarPreparation.begin();
150
168
  void this.engine
151
169
  .scopeColors?.(this.theme)
152
170
  .catch(() => null)
@@ -161,6 +179,7 @@ class SmartEditor implements Editor {
161
179
  .then((tokenizer) => {
162
180
  if (!this.disposed && request === this.tokenizerRequest) {
163
181
  this.view.setTokenizer(tokenizer);
182
+ this.grammarPreparation.ready(tokenizer);
164
183
  }
165
184
  });
166
185
  }
@@ -194,9 +213,10 @@ class SmartEditor implements Editor {
194
213
  return subscribe(this.textChanges, listener);
195
214
  }
196
215
 
197
- private announceTextChange(edits: NonNullable<EditorSnapshot['contentEdits']>, source: EditorTextChange['source']): void {
216
+ private announceTextChange(edits: NonNullable<EditorSnapshot['contentEdits']>, source: EditorTextChange['source'], typed: boolean): void {
198
217
  const change: EditorTextChange = {
199
218
  source,
219
+ ...(typed ? { typed } : {}),
200
220
  changes: edits.map((edit) => ({
201
221
  range: { start: { line: edit.start.line, character: edit.start.column }, end: { line: edit.end.line, character: edit.end.column } },
202
222
  text: edit.text
@@ -222,6 +242,26 @@ class SmartEditor implements Editor {
222
242
  return this.view.scrollPlace.y;
223
243
  }
224
244
 
245
+ setScrollTop(top: number): void {
246
+ this.view.setScrollTop(top);
247
+ }
248
+
249
+ getLineHeight(line: number): number {
250
+ return this.view.getLineHeight(line);
251
+ }
252
+
253
+ getBaseLineHeight(): number {
254
+ return this.view.layout.metrics.lineHeight;
255
+ }
256
+
257
+ onLineHeights(listener: Listener): () => void {
258
+ return this.view.onLineHeights(listener);
259
+ }
260
+
261
+ getContentHeight(): number {
262
+ return this.view.contentHeight;
263
+ }
264
+
225
265
  getSelection(): EditorRange {
226
266
  return this.rangeOf(this.model.getPrimary());
227
267
  }
@@ -295,14 +335,32 @@ class SmartEditor implements Editor {
295
335
  this.controller.setSelectionRanges(provider);
296
336
  }
297
337
 
298
- applyEdits(edits: readonly EditorContentChange[]): boolean {
338
+ applyEdits(edits: readonly EditorContentChange[], options: EditorApplyEditsOptions = {}): boolean {
299
339
  if (this.settings.readOnly || edits.length === 0) {
300
340
  return false;
301
341
  }
302
342
  try {
343
+ const { mapSelections } = options;
303
344
  return this.model.applyEdits(
304
345
  edits.map((edit) => ({ from: this.offsetAt(edit.range.start), to: this.offsetAt(edit.range.end), text: edit.text })),
305
- { source: 'command' }
346
+ {
347
+ source: 'command',
348
+ joinPrevious: options.mergeUndo === true,
349
+ selectionPositions:
350
+ mapSelections &&
351
+ this.model.getSelections().map((selection) => {
352
+ const { start, end } = mapSelections(this.rangeOf(selection));
353
+ const from = { line: start.line, column: start.character };
354
+ const to = { line: end.line, column: end.character };
355
+ if (selection.head < selection.anchor) {
356
+ return { anchor: to, head: from };
357
+ }
358
+ return {
359
+ anchor: from,
360
+ head: selection.head === selection.anchor && start.line === end.line && start.character === end.character ? from : to
361
+ };
362
+ })
363
+ }
306
364
  );
307
365
  } catch {
308
366
  return false;
@@ -323,7 +381,28 @@ class SmartEditor implements Editor {
323
381
  }
324
382
 
325
383
  setInlayHints(hints: readonly EditorInlayHint[]): void {
326
- this.view.setInlays(hints.map((hint, index) => ({ id: `hint-${index}`, at: this.offsetAt(hint.position), text: hint.label })));
384
+ this.view.setInlays(
385
+ hints.map((hint, index) => ({
386
+ id: hint.id ?? `hint-${index}`,
387
+ at: this.offsetAt(hint.position),
388
+ text: hint.label,
389
+ ...(hint.parts === undefined ? {} : { parts: hint.parts }),
390
+ ...(hint.paddingLeft ? { gapLeft: INLAY_GAP } : {}),
391
+ ...(hint.paddingRight ? { gapRight: INLAY_GAP } : {})
392
+ }))
393
+ );
394
+ }
395
+
396
+ onInlayHover(listener: (target: EditorInlayTarget | null) => void): () => void {
397
+ return this.view.onInlayHover(listener);
398
+ }
399
+
400
+ onInlayClick(handler: EditorInlayClickHandler): () => void {
401
+ return this.controller.onInlayClick(handler);
402
+ }
403
+
404
+ setInlayLink(target: { readonly id: string; readonly part: number | null } | null): void {
405
+ this.view.setInlayLink(target);
327
406
  }
328
407
 
329
408
  setGhostText(ghost: EditorGhostText | null): void {
@@ -336,7 +415,7 @@ class SmartEditor implements Editor {
336
415
  ? null
337
416
  : tokens.map((token) => {
338
417
  const from = this.offsetAt({ line: token.line, character: token.character });
339
- return { from, to: from + token.length, scopes: token.scopes };
418
+ return { from, to: from + token.length, scopes: token.scopes, within: token.within };
340
419
  })
341
420
  );
342
421
  }
@@ -393,19 +472,23 @@ class SmartEditor implements Editor {
393
472
 
394
473
  replace(replacement: string, replaceOptions: EditorReplaceOptions = {}): boolean {
395
474
  const { find } = this.view;
396
- const mark = find.currentMark;
397
- const query = find.activeQuery;
398
- if (mark === null || query === null || this.blockedByReadOnly()) {
475
+ const match = find.currentMatch;
476
+ if (this.blockedByReadOnly()) {
399
477
  return false;
400
478
  }
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) {
479
+ if (find.activeQuery?.regex === true) {
480
+ if (find.currentMark === null) {
405
481
  return false;
406
482
  }
483
+ void find.replaceOne(replacement, replaceOptions.preserveCase === true, () => !this.disposed && !this.settings.readOnly);
484
+ return true;
485
+ }
486
+ if (match === null) {
487
+ return false;
488
+ }
489
+ try {
407
490
  // 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;
491
+ find.pendingAnchor = match.to;
409
492
  if (!this.model.replace(match, replacement, { preserveCase: replaceOptions.preserveCase })) {
410
493
  find.pendingAnchor = undefined;
411
494
  return false;
@@ -420,21 +503,47 @@ class SmartEditor implements Editor {
420
503
  }
421
504
 
422
505
  replaceAll(replacement: string, options: EditorReplaceOptions = {}): number {
423
- const query = this.view.find.activeQuery;
424
- if (query === null || this.blockedByReadOnly()) {
506
+ const { find } = this.view;
507
+ const query = find.activeQuery;
508
+ if (query === null || find.state.noSelection === true || this.blockedByReadOnly()) {
425
509
  return 0;
426
510
  }
427
511
  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 {
512
+ const matches = find.completeMatches;
513
+ if (matches !== null) {
514
+ let characters = 0;
515
+ for (const match of matches) {
516
+ characters += replacementText('', match, replacement, false, options.preserveCase, MAX_REPLACEMENT_CHARACTERS).length;
517
+ if (characters > MAX_REPLACEMENT_CHARACTERS) {
518
+ find.reportReplaceFailed('limit');
519
+ return 0;
520
+ }
521
+ }
522
+ const count = this.model.replaceMatches(matches, replacement, { preserveCase: options.preserveCase });
523
+ find.reportReplaced(count);
524
+ return count;
525
+ }
526
+ void this.replaceAllAsync(replacement, options);
527
+ return 0;
528
+ } catch (error) {
529
+ find.reportReplaceFailed(error instanceof RangeError ? 'limit' : 'error');
530
+ return 0;
531
+ }
532
+ }
533
+
534
+ async replaceAllAsync(replacement: string, options: EditorReplaceOptions = {}): Promise<number> {
535
+ const { find } = this.view;
536
+ if (this.disposed || this.blockedByReadOnly() || find.activeQuery === null || find.state.noSelection) {
537
+ return 0;
538
+ }
539
+ if (find.state.limited) {
540
+ find.reportReplaceFailed('limit');
436
541
  return 0;
437
542
  }
543
+ if (find.completeMatches !== null) {
544
+ return this.replaceAll(replacement, options);
545
+ }
546
+ return find.replaceAll(replacement, options.preserveCase === true, () => !this.disposed && !this.settings.readOnly);
438
547
  }
439
548
 
440
549
  setReplacePreview(replacement: string | null, options: EditorReplaceOptions = {}): void {
@@ -462,24 +571,39 @@ class SmartEditor implements Editor {
462
571
  if (query.text === '') {
463
572
  return false;
464
573
  }
465
- let matches;
574
+ const { anchor, head } = this.model.getPrimary();
575
+ const offset = direction === 1 ? Math.max(anchor, head) : Math.min(anchor, head);
576
+ const reveal = (target: { from: number; to: number } | null): boolean => {
577
+ if (target === null || this.disposed) {
578
+ return false;
579
+ }
580
+ this.view.ensureVisible(target.from);
581
+ this.model.setSelections([direction === 1 ? { anchor: target.from, head: target.to } : { anchor: target.to, head: target.from }]);
582
+ this.view.revealCaret(target.from > head ? 'centerDown' : 'centerUp');
583
+ return true;
584
+ };
585
+ if (query.regex) {
586
+ const revision = this.model.getRevision();
587
+ void this.view.find.nextFromCursor(query, offset, direction).then((target) => {
588
+ const current = this.model.getPrimary();
589
+ if (this.model.getRevision() === revision && current.anchor === anchor && current.head === head) {
590
+ reveal(target);
591
+ }
592
+ });
593
+ return true;
594
+ }
466
595
  try {
467
- matches = this.model.find(query.text, { caseSensitive: query.caseSensitive, wholeWord: query.wholeWord, regex: query.regex });
596
+ return reveal(
597
+ this.model.findNext(query.text, offset, {
598
+ caseSensitive: query.caseSensitive,
599
+ wholeWord: query.wholeWord,
600
+ backwards: direction === -1,
601
+ wrap: true
602
+ })
603
+ );
468
604
  } catch {
469
605
  return false;
470
606
  }
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
607
  }
484
608
 
485
609
  private blockedByReadOnly(): boolean {
@@ -514,8 +638,8 @@ class SmartEditor implements Editor {
514
638
  this.view.applySettings();
515
639
  }
516
640
 
517
- setChangeMarks(marks: readonly EditorChangeMark[]): void {
518
- this.view.setChangeMarks(marks);
641
+ setChangeMarks(marks: readonly EditorChangeMark[], layer?: string): void {
642
+ this.view.setChangeMarks(marks, layer);
519
643
  }
520
644
 
521
645
  setAttributionMarks(marks: readonly EditorAttributionMark[]): void {
@@ -526,6 +650,32 @@ class SmartEditor implements Editor {
526
650
  this.view.setLineHighlights(highlights);
527
651
  }
528
652
 
653
+ setLineLabels(labels: ((line: number) => string | null) | null): void {
654
+ this.view.setLineLabels(labels);
655
+ }
656
+
657
+ setInputPopup(popup: EditorInputPopup | null): void {
658
+ this.view.setInputPopup(popup);
659
+ }
660
+
661
+ announce(message: string): void {
662
+ this.view.announce(message);
663
+ }
664
+
665
+ setMessages(messages: Partial<EditorMessages>): void {
666
+ this.settings.messages = { ...this.settings.messages, ...messages };
667
+ this.view.repaint();
668
+ }
669
+
670
+ setLineNumbers(labels: ((line: number) => string | null) | null): void {
671
+ this.view.setLineNumbers(labels);
672
+ }
673
+
674
+ setLineTokens(tokens: ((line: number) => readonly LineToken[] | null | undefined) | null): void {
675
+ this.grammarPreparation.pause(tokens !== null);
676
+ this.view.setLineTokens(tokens);
677
+ }
678
+
529
679
  renderCode(container: HTMLElement, text: string, options: EditorCodeBlockOptions = {}): void {
530
680
  this.view.renderCode(container, text, options);
531
681
  }
@@ -546,6 +696,7 @@ class SmartEditor implements Editor {
546
696
  at: this.offsetAt({ line: widget.line, character: 0 }),
547
697
  placement: widget.placement ?? 'below',
548
698
  height: widget.height,
699
+ fixed: widget.fixed,
549
700
  render: (container: HTMLElement) => {
550
701
  container.classList.add('se-widget');
551
702
  widget.render(container);
@@ -554,6 +705,25 @@ class SmartEditor implements Editor {
554
705
  );
555
706
  }
556
707
 
708
+ setReplacements(replacements: readonly EditorReplacement[], owner: string = DEFAULT_WIDGET_OWNER): void {
709
+ this.view.setReplacements(
710
+ owner,
711
+ replacements.map((replacement) => ({
712
+ id: `replacement:${owner}:${replacement.id}`,
713
+ from: this.offsetAt({ line: replacement.startLine, character: 0 }),
714
+ to: this.model.getLine(Math.max(0, Math.min(replacement.endLine, this.model.getLineCount() - 1))).end,
715
+ height: replacement.height,
716
+ label: replacement.label,
717
+ enterable: replacement.enterable,
718
+ onReveal: replacement.onReveal === undefined ? undefined : () => replacement.onReveal!(),
719
+ render: (container: HTMLElement) => {
720
+ container.classList.add('se-widget-replacement');
721
+ replacement.render(container);
722
+ }
723
+ }))
724
+ );
725
+ }
726
+
557
727
  setLineActions(actions: readonly EditorLineAction[], owner: string = DEFAULT_WIDGET_OWNER): void {
558
728
  this.view.setLineActions(
559
729
  owner,
@@ -637,6 +807,11 @@ class SmartEditor implements Editor {
637
807
  this.view.applySettings();
638
808
  }
639
809
 
810
+ setStickyScroll(sticky: boolean): void {
811
+ this.settings.stickyScroll = sticky;
812
+ this.view.applySettings();
813
+ }
814
+
640
815
  setWhitespace(whitespace: boolean): void {
641
816
  this.settings.whitespace = whitespace;
642
817
  this.view.applySettings();
@@ -714,6 +889,10 @@ class SmartEditor implements Editor {
714
889
  this.view.applySettings();
715
890
  }
716
891
 
892
+ isReadOnly(): boolean {
893
+ return this.settings.readOnly;
894
+ }
895
+
717
896
  runCommand(command: EditorRunCommand): boolean {
718
897
  return this.controller.command(command);
719
898
  }
@@ -731,6 +910,7 @@ class SmartEditor implements Editor {
731
910
  return;
732
911
  }
733
912
  this.disposed = true;
913
+ this.grammarPreparation.dispose();
734
914
  this.changes.clear();
735
915
  this.textChanges.clear();
736
916
  this.carets.clear();