@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/testing.ts CHANGED
@@ -52,6 +52,8 @@ export interface KeyOptions {
52
52
  metaKey?: boolean;
53
53
  altKey?: boolean;
54
54
  shiftKey?: boolean;
55
+ /* 'touch', 'pen' or 'mouse' for a pointer event. */
56
+ pointerType?: string;
55
57
  }
56
58
 
57
59
  /* Fires a keydown at the element and says whether the editor took the key. */
package/src/tokens.ts CHANGED
@@ -26,6 +26,8 @@ export class TokenCache {
26
26
  private entries: (Entry | undefined)[] = [];
27
27
  /* Every line above it is colored for the text as it is now. */
28
28
  private frontier = 0;
29
+ /* Colors survive a rebuild, but the states below this boundary cannot settle recoloring. */
30
+ private staleFrom = Number.POSITIVE_INFINITY;
29
31
  /* The last line the edit touched; lines below it may be told right by their old state. */
30
32
  private reusableFrom = Number.POSITIVE_INFINITY;
31
33
  /* The state the last line of the edited stretch ended in before the edit; the same state now means the lines below are right. */
@@ -48,6 +50,7 @@ export class TokenCache {
48
50
  this.tokenizer = tokenizer;
49
51
  this.lines = this.source.getLineCount();
50
52
  this.entries = [];
53
+ this.staleFrom = Number.POSITIVE_INFINITY;
51
54
  this.frontier = 0;
52
55
  this.forgetPivot();
53
56
  }
@@ -90,6 +93,7 @@ export class TokenCache {
90
93
  const first = batches[0]?.[0];
91
94
  if (first === undefined) {
92
95
  this.entries = [];
96
+ this.staleFrom = Number.POSITIVE_INFINITY;
93
97
  this.frontier = 0;
94
98
  this.forgetPivot();
95
99
  return;
@@ -104,8 +108,20 @@ export class TokenCache {
104
108
  const lastNew = last === undefined ? -1 : lineOf(last.from + shift + last.insertedLength);
105
109
  const lastOld = lastNew - delta;
106
110
  this.frontier = Math.min(this.frontier, firstLine);
111
+ if (last === undefined && Number.isFinite(this.staleFrom)) {
112
+ // A history batch has no single mapping for the old grammar boundary.
113
+ this.staleFrom = 0;
114
+ this.frontier = 0;
115
+ } else if (this.staleFrom > lastOld) {
116
+ this.staleFrom += delta;
117
+ } else if (this.staleFrom > firstLine) {
118
+ this.staleFrom = firstLine;
119
+ }
107
120
  if (last === undefined || lastOld < firstLine || this.entries.length <= firstLine) {
108
121
  this.entries.length = Math.min(this.entries.length, firstLine);
122
+ if (this.staleFrom >= this.entries.length) {
123
+ this.staleFrom = Number.POSITIVE_INFINITY;
124
+ }
109
125
  this.forgetPivot();
110
126
  return;
111
127
  }
@@ -116,6 +132,17 @@ export class TokenCache {
116
132
  this.reusableFrom = lastNew + 1;
117
133
  }
118
134
 
135
+ /* Old colors stay visible until the rebuilt grammar reaches each line. */
136
+ revalidate(): boolean {
137
+ if (this.tokenizer?.stale?.() !== true) {
138
+ return false;
139
+ }
140
+ this.frontier = 0;
141
+ this.staleFrom = 0;
142
+ this.forgetPivot();
143
+ return true;
144
+ }
145
+
119
146
  /*
120
147
  * Colors lines from the frontier until `until` is reached, the colors are right for the rest of
121
148
  * the document, or `budget` milliseconds are spent. Returns the first line that changed, so a
@@ -126,10 +153,7 @@ export class TokenCache {
126
153
  if (tokenizer === null) {
127
154
  return null;
128
155
  }
129
- if (tokenizer.stale?.() === true) {
130
- this.frontier = 0;
131
- this.forgetPivot();
132
- }
156
+ this.revalidate();
133
157
  const count = this.source.getLineCount();
134
158
  const limit = Math.min(until, count - 1);
135
159
  const deadline = now() + budget;
@@ -148,11 +172,16 @@ export class TokenCache {
148
172
  }
149
173
  this.entries[line] = next;
150
174
  this.frontier = line + 1;
175
+ const fresh = line < this.staleFrom;
176
+ if (line === this.staleFrom) {
177
+ this.staleFrom = line + 1 >= this.entries.length ? Number.POSITIVE_INFINITY : line + 1;
178
+ }
151
179
  const settled =
152
- (line === this.pivotLine && tokenizer.sameState(this.pivotState, next.state)) ||
153
- (previous !== undefined && line >= this.reusableFrom && tokenizer.sameState(previous.state, next.state));
180
+ fresh &&
181
+ ((line === this.pivotLine && tokenizer.sameState(this.pivotState, next.state)) ||
182
+ (previous !== undefined && line >= this.reusableFrom && tokenizer.sameState(previous.state, next.state)));
154
183
  if (settled) {
155
- this.frontier = this.entries.length;
184
+ this.frontier = Math.min(this.entries.length, this.staleFrom);
156
185
  this.forgetPivot();
157
186
  break;
158
187
  }
package/src/touch.ts ADDED
@@ -0,0 +1,134 @@
1
+ import type { DocumentModel } from '@adecore/editor-core';
2
+
3
+ export interface TouchHost {
4
+ readonly model: DocumentModel;
5
+ offsetAt(clientX: number, clientY: number): number;
6
+ camelHumps(): boolean;
7
+ /* The lines a tap on a line number selects, whole. */
8
+ lineAt(offset: number): { from: number; to: number };
9
+ focus(): void;
10
+ }
11
+
12
+ /* A finger that moves further than this, in pixels, is scrolling and not tapping. */
13
+ const TAP_SLOP = 10;
14
+ /* A finger held still this long selects the word under it. */
15
+ const LONG_PRESS_MS = 500;
16
+ /* A second tap this soon after the first, this close to it, is a double tap. */
17
+ const DOUBLE_TAP_MS = 300;
18
+ const DOUBLE_TAP_DISTANCE = 30;
19
+
20
+ interface Contact {
21
+ id: number;
22
+ x: number;
23
+ y: number;
24
+ area: 'text' | 'gutter';
25
+ moved: boolean;
26
+ pressed: boolean;
27
+ /* Asked when the finger lifts as a tap, so a scroll never reaches a handler of the host. */
28
+ taken?: () => boolean;
29
+ }
30
+
31
+ /*
32
+ * A finger on the text. The viewport scrolls natively, so a drag is left to the browser and makes no
33
+ * selection; a tap places the caret, a double tap or a long press selects a word.
34
+ */
35
+ export class TouchSelection {
36
+ private contact: Contact | undefined;
37
+ private timer: ReturnType<typeof setTimeout> | undefined;
38
+ private lastTap: { time: number; x: number; y: number } | undefined;
39
+ private readonly host: TouchHost;
40
+
41
+ constructor(host: TouchHost) {
42
+ this.host = host;
43
+ }
44
+
45
+ /* Whether the finger that is down held still long enough to select a word. */
46
+ get pressed(): boolean {
47
+ return this.contact?.pressed === true;
48
+ }
49
+
50
+ start(event: PointerEvent, area: 'text' | 'gutter' = 'text', taken?: () => boolean): void {
51
+ this.cancel();
52
+ this.contact = { id: event.pointerId, x: event.clientX, y: event.clientY, area, moved: false, pressed: false, taken };
53
+ if (area === 'text') {
54
+ this.timer = setTimeout(() => this.press(), LONG_PRESS_MS);
55
+ }
56
+ }
57
+
58
+ move(event: PointerEvent): void {
59
+ const contact = this.contact;
60
+ if (contact === undefined || event.pointerId !== contact.id || contact.moved) {
61
+ return;
62
+ }
63
+ if (Math.abs(event.clientX - contact.x) > TAP_SLOP || Math.abs(event.clientY - contact.y) > TAP_SLOP) {
64
+ contact.moved = true;
65
+ this.clearTimer();
66
+ }
67
+ }
68
+
69
+ /* The finger lifted: a tap that did not scroll and was not a long press lands here. */
70
+ end(event: PointerEvent): void {
71
+ const contact = this.contact;
72
+ if (contact === undefined || event.pointerId !== contact.id) {
73
+ return;
74
+ }
75
+ this.cancel();
76
+ if (event.type === 'pointercancel' || contact.moved || contact.pressed) {
77
+ return;
78
+ }
79
+ this.tap(contact, event);
80
+ }
81
+
82
+ dispose(): void {
83
+ this.cancel();
84
+ }
85
+
86
+ private tap(contact: Contact, event: PointerEvent): void {
87
+ const { model } = this.host;
88
+ const offset = this.host.offsetAt(contact.x, contact.y);
89
+ this.host.focus();
90
+ if (contact.area === 'gutter') {
91
+ const line = this.host.lineAt(offset);
92
+ model.setSelections([{ anchor: line.from, head: line.to }]);
93
+ return;
94
+ }
95
+ if (contact.taken?.()) {
96
+ return;
97
+ }
98
+ const last = this.lastTap;
99
+ const double =
100
+ last !== undefined &&
101
+ event.timeStamp - last.time <= DOUBLE_TAP_MS &&
102
+ Math.abs(contact.x - last.x) <= DOUBLE_TAP_DISTANCE &&
103
+ Math.abs(contact.y - last.y) <= DOUBLE_TAP_DISTANCE;
104
+ this.lastTap = double ? undefined : { time: event.timeStamp, x: contact.x, y: contact.y };
105
+ const word = double ? model.wordSelectionAt(offset, this.host.camelHumps()) : null;
106
+ model.setSelections([word === null ? { anchor: offset, head: offset } : { anchor: word.from, head: word.to }]);
107
+ }
108
+
109
+ private press(): void {
110
+ const contact = this.contact;
111
+ this.timer = undefined;
112
+ if (contact === undefined || contact.moved) {
113
+ return;
114
+ }
115
+ contact.pressed = true;
116
+ this.lastTap = undefined;
117
+ const offset = this.host.offsetAt(contact.x, contact.y);
118
+ const word = this.host.model.wordSelectionAt(offset, this.host.camelHumps());
119
+ this.host.focus();
120
+ this.host.model.setSelections([word === null ? { anchor: offset, head: offset } : { anchor: word.from, head: word.to }]);
121
+ }
122
+
123
+ private clearTimer(): void {
124
+ if (this.timer !== undefined) {
125
+ clearTimeout(this.timer);
126
+ }
127
+ this.timer = undefined;
128
+ }
129
+
130
+ private cancel(): void {
131
+ this.clearTimer();
132
+ this.contact = undefined;
133
+ }
134
+ }
package/src/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { EditorSearchClientOptions } from './search-client.ts';
1
2
  import type { Keymap } from './keymap-table.ts';
2
3
  import type { EditorCommand, FoldRole } from '@adecore/editor-core';
3
4
 
@@ -44,6 +45,8 @@ export interface EditorFolds {
44
45
  export type EditorTheme = string;
45
46
 
46
47
  export interface EditorOptions {
48
+ /* Additional host shortcuts for this mount, preserving the engine's handBack chords. */
49
+ readonly handBack?: readonly KeyChord[];
47
50
  readonly text: string;
48
51
  /* A Shiki language id. Without one, or with one Shiki does not know, it is plain text. */
49
52
  readonly language?: string;
@@ -60,12 +63,16 @@ export interface EditorOptions {
60
63
  readonly smartKeys?: Partial<EditorSmartKeys>;
61
64
  /* A line at each indentation level, the one of the scope around the caret stronger; on unless false. */
62
65
  readonly guides?: boolean;
66
+ /* The headers of the blocks scrolled out of sight stay pinned on top; on unless false. */
67
+ readonly stickyScroll?: boolean;
63
68
  /* Spaces drawn as dots and tabs as arrows; off unless true. */
64
69
  readonly whitespace?: boolean;
65
70
  /* The column to draw a line at, such as the `max_line_length` of a project; none by default. */
66
71
  readonly rightMargin?: number | null;
67
72
  /* When the arrow that folds a block shows in the gutter; on hover by default. */
68
73
  readonly foldOutline?: EditorFoldOutline;
74
+ /* False disables automatic and selection folds, including fold commands. */
75
+ readonly folding?: boolean;
69
76
  /* What the editor says to a person itself, in the host's words. */
70
77
  readonly messages?: Partial<EditorMessages>;
71
78
  /* The accessible name of the editor, such as "Commit message", in the host's language; `Code editor` without one. */
@@ -86,6 +93,18 @@ export interface EditorOptions {
86
93
  export interface EditorMessages {
87
94
  /* Select next occurrence went past the last one. */
88
95
  readonly noMoreOccurrences: string;
96
+ /* The name of a gutter control that folds a block; `{start}` and `{end}` are the one-based first and last line. `Fold lines {start} to {end}` without one. */
97
+ readonly fold: string;
98
+ /* The same for the control that unfolds it; `Unfold lines {start} to {end}` without one. */
99
+ readonly unfold: string;
100
+ }
101
+
102
+ /* The list a host opens beside the text that the keyboard picks from, such as suggestions, as the input points at it for assistive technology. */
103
+ export interface EditorInputPopup {
104
+ /* The id of the element that holds the options. */
105
+ readonly controls: string;
106
+ /* The id of the option that Enter takes; none while the list has no active option. */
107
+ readonly activeDescendant?: string;
89
108
  }
90
109
 
91
110
  /* What the editor does by itself as a person types. Every one is on, except the camel humps. */
@@ -193,10 +212,20 @@ export interface EditorLineHighlight {
193
212
  readonly fill?: EditorMarkColor;
194
213
  /* A character in the gutter of each line, such as `+`, in the same color. */
195
214
  readonly sign?: string;
215
+ /*
216
+ * Draws the gutter of each line as a review does: the cell tinted in `color`, the number in a readable
217
+ * shade of it and a bar of whole pixels along the left edge, solid or striped, continuous over
218
+ * consecutive lines. It takes the place of the change mark on those lines.
219
+ */
220
+ readonly bar?: 'solid' | 'hatched';
221
+ /* Per line from `startLine`, UTF-16 character ranges with a stronger tint; they follow their text through edits. */
222
+ readonly emphasis?: ReadonlyArray<ReadonlyArray<readonly [number, number]>>;
196
223
  }
197
224
 
198
225
  /* How `renderCode` draws lines that are not part of the document. */
199
226
  export interface EditorCodeBlockOptions {
227
+ /* Prepared in the source document's grammar state. Omit to tokenize here; null draws plain text. */
228
+ readonly tokens?: ReadonlyArray<readonly LineToken[]> | null;
200
229
  /* One-based number of the first line, shown in the gutter column; without it the lines have none. */
201
230
  readonly firstLine?: number;
202
231
  /* A character in the gutter of each line, such as `-`. */
@@ -233,11 +262,18 @@ export interface EditorContentChange {
233
262
  readonly text: string;
234
263
  }
235
264
 
265
+ export interface EditorApplyEditsOptions {
266
+ readonly mapSelections?: (range: EditorRange) => EditorRange;
267
+ readonly mergeUndo?: boolean;
268
+ }
269
+
236
270
  export interface EditorTextChange {
237
271
  /* In order, so a language server can follow them one by one. */
238
272
  readonly changes: readonly EditorContentChange[];
239
273
  /* A person typing, one of the editor's own commands such as undo, or a change from outside. */
240
274
  readonly source: 'input' | 'command' | 'external';
275
+ /* Set when a person's typing made the change though it ran as a command, as Enter does; every change from the keyboard has source `input` without it. */
276
+ readonly typed?: boolean;
241
277
  }
242
278
 
243
279
  export type EditorMarkerSeverity = 'error' | 'warning' | 'info' | 'hint';
@@ -308,6 +344,32 @@ export interface EditorWidget {
308
344
  readonly placement?: 'above' | 'below';
309
345
  /* The height the row has until it has been measured. */
310
346
  readonly height?: number;
347
+ /* Uses height as exact pixels and never measures the row. */
348
+ readonly fixed?: boolean;
349
+ /* Fills the element the row is drawn in, which the editor makes again whenever the row scrolls back into view. */
350
+ render(container: HTMLElement): void;
351
+ }
352
+
353
+ /*
354
+ * Lines of the text drawn as a row of the host's own DOM instead, such as a documentation comment drawn
355
+ * as formatted text. The lines stay in the document: copying, searching and the undo history see them as
356
+ * they are, and the row is as tall as its content. Put the caret on one of the lines, or fold them,
357
+ * and the editor draws the text again; it is for the host to take the row away at that moment when it
358
+ * wants the lines edited.
359
+ */
360
+ export interface EditorReplacement {
361
+ readonly id: string;
362
+ /* Zero-based and inclusive; the row follows its lines through edits until its owner sets its replacements again. */
363
+ readonly startLine: number;
364
+ readonly endLine: number;
365
+ /* The height the row has until it has been measured. */
366
+ readonly height?: number;
367
+ /* What assistive technology calls the row. */
368
+ readonly label?: string;
369
+ /* Whether the caret may stop in the row's lines when it moves up or down. Without it a vertical move steps over the row. */
370
+ readonly enterable?: boolean;
371
+ /* The editor is about to scroll to a place inside the row's lines. The host shows the lines itself here, so the target is where the person will see it. */
372
+ onReveal?(): void;
311
373
  /* Fills the element the row is drawn in, which the editor makes again whenever the row scrolls back into view. */
312
374
  render(container: HTMLElement): void;
313
375
  }
@@ -362,6 +424,8 @@ export interface EditorSemanticToken {
362
424
  readonly length: number;
363
425
  /* Outermost first, as a grammar would have reported them. */
364
426
  readonly scopes: readonly string[];
427
+ /* Colors only the stretches of text the grammar classed as this kind, such as a server's SQL tokens inside a string. */
428
+ readonly within?: LineTokenKind;
365
429
  }
366
430
 
367
431
  /* How a theme draws a piece of code. The font style is Shiki's flags: 1 italic, 2 bold, 4 underline, 8 strikethrough. */
@@ -380,8 +444,41 @@ export type ScopeColorSource = (theme: EditorTheme) => Promise<ScopeColors | nul
380
444
  export interface EditorInlayHint {
381
445
  readonly position: EditorPosition;
382
446
  readonly label: string;
447
+ /* Names the hint in `onInlayHover` and `onInlayClick`; `hint-<index>` without one. */
448
+ readonly id?: string;
449
+ /* The label cut into pieces that the pointer tells apart; they add up to `label`. */
450
+ readonly parts?: readonly string[];
451
+ /* A gap of 4 pixels that keeps the pill off the character before it or the one after it. */
452
+ readonly paddingLeft?: boolean;
453
+ readonly paddingRight?: boolean;
383
454
  }
384
455
 
456
+ /* A hint, or one part of its label, under the pointer. */
457
+ export interface EditorInlayTarget {
458
+ readonly id: string;
459
+ /* The index into `parts`, or null for a hint without parts or the padding of one. */
460
+ readonly part: number | null;
461
+ /* The hint or part, in screen coordinates. */
462
+ readonly rect: EditorRect;
463
+ }
464
+
465
+ /* A press of the primary button on a hint. */
466
+ export interface EditorInlayClick {
467
+ readonly id: string;
468
+ readonly part: number | null;
469
+ /* Cmd on macOS, Ctrl elsewhere. */
470
+ readonly mod: boolean;
471
+ readonly alt: boolean;
472
+ readonly shift: boolean;
473
+ /* 2 for a second press on the same hint within half a second. */
474
+ readonly clicks: 1 | 2;
475
+ /* A finger, which has no hover; a press then stands in for it. */
476
+ readonly touch: boolean;
477
+ }
478
+
479
+ /* Returns true when the press was taken; the editor then leaves the caret and the selection where they are. */
480
+ export type EditorInlayClickHandler = (click: EditorInlayClick) => boolean;
481
+
385
482
  /*
386
483
  * A suggestion drawn after the caret in the editor's ghost color, which is not part of the document. It
387
484
  * may span lines. Any edit of the text takes it away, so the host sets it again for the text it was made for.
@@ -401,6 +498,9 @@ export interface EditorGutterAction {
401
498
  readonly label: string;
402
499
  }
403
500
 
501
+ /* The glyphs a gutter marker can have. */
502
+ export type EditorGutterIcon = 'sparkles' | 'book-open';
503
+
404
504
  /* A small button in the gutter on one line that the host keeps apart from the code action, such as the mark of a saved inline edit. */
405
505
  export interface EditorGutterMarker {
406
506
  /* What `onGutterMarker` says when it is pressed. */
@@ -408,6 +508,10 @@ export interface EditorGutterMarker {
408
508
  /* Zero-based; it stays on this line until the host sets its markers again. */
409
509
  readonly line: number;
410
510
  readonly label: string;
511
+ /* The glyph; the sparkles without one. */
512
+ readonly icon?: EditorGutterIcon;
513
+ /* Drawn as switched on, for a marker that toggles something. */
514
+ readonly pressed?: boolean;
411
515
  }
412
516
 
413
517
  /*
@@ -435,10 +539,19 @@ export interface EditorFindQuery {
435
539
 
436
540
  export interface EditorFindState {
437
541
  readonly count: number;
438
- /* Zero-based; null without a match. */
542
+ /* Zero-based in the marked cache; null with no match or a current match beyond that cache. */
439
543
  readonly current: number | null;
440
544
  /* The search is in the selection and nothing was selected. */
441
545
  readonly noSelection?: boolean;
546
+ /* More results exist than the marked count. */
547
+ readonly limited?: boolean;
548
+ readonly searching?: boolean;
549
+ readonly failed?: 'invalid' | 'timeout' | 'unavailable' | 'error';
550
+ readonly captureFailed?: 'timeout' | 'unavailable' | 'error';
551
+ readonly replaceFailed?: 'limit' | 'timeout' | 'unavailable' | 'error';
552
+ readonly replaced?: number;
553
+ /* Regex searches in large documents start only on an explicit step. */
554
+ readonly large?: boolean;
442
555
  }
443
556
 
444
557
  export interface EditorReplaceOptions {
@@ -484,15 +597,17 @@ export interface Editor {
484
597
  */
485
598
  find(query: EditorFindQuery | null): void;
486
599
  findStep(direction: 1 | -1): void;
487
- /* Replaces the match the find is on, with `$1` and the like expanded for a regular expression, and moves to the next one. False when there is none or the editor is read only. */
600
+ /* Replaces the match the find is on, with `$1` and the like expanded for a regular expression, and moves to the next one. Regex returns true when the worker operation starts; false when there is no match or the editor is read only. */
488
601
  replace(replacement: string, options?: EditorReplaceOptions): boolean;
489
- /* Replaces every match in one undo step, and says how many there were. */
602
+ /* Complete literal results return the number replaced; otherwise starts asynchronous replacement, returns zero and reports through onFind. */
490
603
  replaceAll(replacement: string, options?: EditorReplaceOptions): number;
604
+ /* Atomic, one undo step; excessive output or matches are refused through onFind. */
605
+ replaceAllAsync(replacement: string, options?: EditorReplaceOptions): Promise<number>;
491
606
  /* What a regular expression replacement writes for the match the find is on, drawn under it while the find is open; null takes it away. */
492
607
  setReplacePreview(replacement: string | null, options?: EditorReplaceOptions): void;
493
608
  /* Ends the find with a caret on every match, the one the find was on last, and says how many there were. */
494
609
  selectFindMatches(): number;
495
- /* Selects the next match of a query from the cursor, or the one before, going round at the ends, without a find bar. False when there is none. */
610
+ /* Selects the next match of a query from the cursor, or the one before, going round at the ends, without a find bar. Regex returns true when the worker request starts; literal returns whether a match was selected. */
496
611
  findFromCursor(query: EditorFindQuery, direction: 1 | -1): boolean;
497
612
  onFind(listener: (state: EditorFindState) => void): () => void;
498
613
  /* Takes the marks away and selects the match the find was on, so the cursor is where it stopped. */
@@ -502,6 +617,8 @@ export interface Editor {
502
617
  /* Changes what it does as a person types; the keys left out stay as they are. */
503
618
  setSmartKeys(keys: Partial<EditorSmartKeys>): void;
504
619
  setGuides(guides: boolean): void;
620
+ /* Optional so an editor from before it still fits. */
621
+ setStickyScroll?(sticky: boolean): void;
505
622
  setWhitespace(whitespace: boolean): void;
506
623
  /* The column to draw a line at; null takes it away. */
507
624
  setRightMargin(column: number | null): void;
@@ -509,8 +626,12 @@ export interface Editor {
509
626
  setLabel(label: string): void;
510
627
  /* What a language server knows about the folds: the bodies of symbols and the ranges it folds. They follow their text through edits until set again; null forgets them. */
511
628
  setFoldHints(hints: EditorFoldHints | null): void;
512
- /* Marks that follow their lines through edits until the host sets them again. */
513
- setChangeMarks(marks: readonly EditorChangeMark[]): void;
629
+ /*
630
+ * Marks that follow their lines through edits until the host sets them again. Every layer (default: the unnamed one)
631
+ * holds its own marks and the editor draws the union, so setting one layer never clears another; an empty list
632
+ * removes the layer. On a line two layers mark, the later one wins, in the order the layers last became non-empty.
633
+ */
634
+ setChangeMarks(marks: readonly EditorChangeMark[], layer?: string): void;
514
635
  /*
515
636
  * The blocks sticky scroll and the breadcrumb go by. The editor reads them from brackets and
516
637
  * indentation until the host has better, such as a language server's symbols; null goes back.
@@ -528,6 +649,16 @@ export interface Editor {
528
649
  setRemoteCursors(cursors: readonly EditorRemoteCursor[]): void;
529
650
  /* Lines tinted behind the text, replacing the ones set before. They follow their text through edits until set again, and cost one mapping of two offsets per highlight. */
530
651
  setLineHighlights(highlights: readonly EditorLineHighlight[]): void;
652
+ /* Source labels for zero-based document lines; a null label hides the number. Null restores document numbering. */
653
+ setLineNumbers(labels: ((line: number) => string | null) | null): void;
654
+ /*
655
+ * A visually hidden sentence for zero-based document lines that assistive technology reads with the line
656
+ * number, such as "Added, new line 12". A line with one hides its drawn number from assistive
657
+ * technology, so the sentence carries the number too. Null removes them.
658
+ */
659
+ setLineLabels(labels: ((line: number) => string | null) | null): void;
660
+ /* Prepared colors for displayed lines. Unknown or null results stay plain; null restores grammar tokenization. Keep the same array while a line is unchanged; call again when results change. */
661
+ setLineTokens(tokens: ((line: number) => readonly LineToken[] | null | undefined) | null): void;
531
662
  /*
532
663
  * Fills a widget row's element with lines of code that are not in the document, such as the lines an
533
664
  * agent removed, in the editor's face and colors for the language, a gutter column as wide as the
@@ -546,6 +677,12 @@ export interface Editor {
546
677
  setSemanticTokens(tokens: readonly EditorSemanticToken[] | null): void;
547
678
  /* Hints to draw as soft pills in the text, replacing the ones set before. They follow their text through edits until set again. */
548
679
  setInlayHints(hints: readonly EditorInlayHint[]): void;
680
+ /* The pointer moved onto a hint or one of its parts, or null when it left them. Optional so an editor from before it still fits. */
681
+ onInlayHover?(listener: (target: EditorInlayTarget | null) => void): () => void;
682
+ /* Handlers see a press of the primary button on a hint before the editor does, the first to take it winning. */
683
+ onInlayClick?(handler: EditorInlayClickHandler): () => void;
684
+ /* Draws a hint or one of its parts as a link, underlined with a pointing hand, until the host sets another or the hints are set again; null takes it away. */
685
+ setInlayLink?(target: { readonly id: string; readonly part: number | null } | null): void;
549
686
  /* A suggestion after the caret, replacing the one before; null takes it away. Any change of the text takes it away as well. */
550
687
  setGhostText(ghost: EditorGhostText | null): void;
551
688
  /* One button in the gutter, replacing the one before; null takes it away. It stays on its line until the host sets it again. */
@@ -559,6 +696,16 @@ export interface Editor {
559
696
  getCaret(): EditorPosition;
560
697
  /* In pixels, what `scrollTop` on mounting takes back. */
561
698
  getScrollTop(): number;
699
+ /* Immediate pixels, canceling an animated scroll and repainting before returning. */
700
+ setScrollTop(top: number): void;
701
+ /* Intrinsic text height of a zero-based line, estimated until drawn; zero for hidden or absent lines. */
702
+ getLineHeight(line: number): number;
703
+ /* The height of one line of text, as the font and the line height setting make it, which a row of the host or a fold never changes. */
704
+ getBaseLineHeight?(): number;
705
+ /* Text heights changed after a measurement, edit, wrap width or font change; excludes widget heights. */
706
+ onLineHeights(listener: () => void): () => void;
707
+ /* All rows in pixels, including the horizontal scrollbar, for a host that grows with its contents. */
708
+ getContentHeight(): number;
562
709
  /* Which lines are folded, for `folds` on mounting. */
563
710
  getFolds(): EditorFolds;
564
711
  /* The primary selection, start before end; empty at the caret. */
@@ -572,6 +719,12 @@ export interface Editor {
572
719
  setSelection(range: EditorRange, reveal?: EditorReveal): void;
573
720
  /* Replaces the selections with these, in the order given: the last is the primary one, as for the newest caret, and is scrolled into view. An empty list changes nothing. */
574
721
  setSelections(ranges: readonly EditorRange[], reveal?: EditorReveal): void;
722
+ /* Points the input at a list the host shows, so assistive technology reads its active option while focus stays in the text; null when the list closes. */
723
+ setInputPopup(popup: EditorInputPopup | null): void;
724
+ /* Says a sentence politely to assistive technology without drawing anything. Saying the same sentence again is heard again. */
725
+ announce(message: string): void;
726
+ /* Replaces the sentences the editor says on its own, such as the names of the fold controls. */
727
+ setMessages(messages: Partial<EditorMessages>): void;
575
728
  /* The caret moved or the text under it changed. */
576
729
  onCaret(listener: (position: EditorPosition) => void): () => void;
577
730
  /* The pointer moved onto another character, or null when it left the text. */
@@ -590,6 +743,12 @@ export interface Editor {
590
743
  * set its rows last. An empty list takes the owner's rows away.
591
744
  */
592
745
  setWidgets(widgets: readonly EditorWidget[], owner?: string): void;
746
+ /*
747
+ * Lines drawn as rows of the host's own DOM, replacing the ones the same owner set before; other
748
+ * owners keep theirs. Where two owners claim the same first line the owner that sorts first keeps
749
+ * it, and a line the person folded draws its fold instead. Optional so an editor from before it still fits.
750
+ */
751
+ setReplacements?(replacements: readonly EditorReplacement[], owner?: string): void;
593
752
  /* The actions of one owner (`default` without one), replacing the ones it set before and leaving every other owner's alone. A line that a fold hides has none. */
594
753
  setLineActions(actions: readonly EditorLineAction[], owner?: string): void;
595
754
  /* The code vision rows above declarations, replacing the ones set before. Separate from the widgets, which they never displace. */
@@ -604,12 +763,14 @@ export interface Editor {
604
763
  setSelectionRanges(provider: EditorSelectionRanges | null): void;
605
764
  /* Follows a range through every change of the text, `setText` and undo included, until it is disposed. Costs one mapping of two offsets per change. */
606
765
  trackRange(range: EditorRange): EditorTrackedRange;
607
- /* Replaces ranges of the current text at once, as one step of the undo history. */
608
- applyEdits(edits: readonly EditorContentChange[]): boolean;
766
+ /* Replaces ranges of the current text at once, as one step of the undo history. `mapSelections` places each selection in the new text, keeping its direction, and redo restores it. `mergeUndo` joins the step before it when the text is exactly as that step left it, so one undo takes both back. */
767
+ applyEdits(edits: readonly EditorContentChange[], options?: EditorApplyEditsOptions): boolean;
609
768
  setTheme(theme: EditorTheme): void;
610
769
  /* Reads the code face off the page again, after the page changed its size, family or ligatures. */
611
770
  refreshFont(): void;
612
771
  setReadOnly(readOnly: boolean, reason?: string): void;
772
+ /* Whether the editor refuses edits. Optional so an editor from before it still fits. */
773
+ isReadOnly?(): boolean;
613
774
  /* Runs an editing command on every caret, as its key would. False when it changed nothing. */
614
775
  runCommand(command: EditorRunCommand): boolean;
615
776
  focus(): void;
@@ -639,8 +800,13 @@ export interface LineToken {
639
800
  readonly color: string;
640
801
  /* Bit flags as Shiki writes them: 1 italic, 2 bold, 4 underline, 8 strikethrough. */
641
802
  readonly fontStyle: number;
803
+ /* What the grammar says the stretch is, when it says so; absent for plain code. */
804
+ readonly kind?: LineTokenKind;
642
805
  }
643
806
 
807
+ /* The classes of text a grammar tells apart from code. */
808
+ export type LineTokenKind = 'comment' | 'string' | 'regex';
809
+
644
810
  export interface TokenizedLine {
645
811
  readonly tokens: readonly LineToken[];
646
812
  /* What the next line starts from; only the tokenizer that made it reads it. */
@@ -657,12 +823,16 @@ export interface LineTokenizer {
657
823
  sameState(a: unknown, b: unknown): boolean;
658
824
  /* True once, when the grammar was rebuilt and the states handed out before it are void. */
659
825
  stale?(): boolean;
826
+ /* Loads missing embedded grammars named by text; true when a load can invalidate earlier states. */
827
+ prepare?(text: string): Promise<boolean>;
660
828
  }
661
829
 
662
830
  /* A tokenizer for a language in a theme, or null when there is no grammar for it. May load the grammar, so it is async; `text` is the document, for the embedded languages it names. */
663
831
  export type TokenizerSource = (language: string | undefined, theme: EditorTheme, text?: string) => Promise<LineTokenizer | null>;
664
832
 
665
833
  export interface SmartEditorEngineOptions {
834
+ /* One lazy search worker per mounted editor; regex search is unavailable without it. */
835
+ readonly searchWorker?: EditorSearchClientOptions['workerFactory'];
666
836
  readonly keymap?: Keymap;
667
837
  readonly tokenizer: TokenizerSource;
668
838
  /* Without it semantic tokens are ignored and the grammar's colors stand. */
@@ -0,0 +1,57 @@
1
+ import type { LayoutMetrics, LineGeometry } from './layout.ts';
2
+
3
+ export function uniformGeometry(
4
+ line: { start: number; end: number; text: string; next?: number },
5
+ metrics: LayoutMetrics,
6
+ breaks: number[],
7
+ triggers: number[],
8
+ indent: number
9
+ ): LineGeometry | null {
10
+ const rowStarts = [0, ...breaks];
11
+ const firstLength = rowStarts[1] ?? line.text.length;
12
+ let continuationLength = 0;
13
+ for (let row = 1; row < rowStarts.length; row++) {
14
+ continuationLength = Math.max(continuationLength, (rowStarts[row + 1] ?? line.text.length) - rowStarts[row]!);
15
+ }
16
+ // A shared prefix preserves repeated floating-point addition exactly, even for fallback font metrics.
17
+ if (Math.max(firstLength, continuationLength) > 4096) {
18
+ return null;
19
+ }
20
+ const positions = (start: number, length: number): number[] => {
21
+ const result = [start];
22
+ for (let index = 0; index < length; index++) {
23
+ result.push(result.at(-1)! + metrics.charWidth);
24
+ }
25
+ return result;
26
+ };
27
+ const uniform = { first: positions(0, firstLength), continuation: positions(indent, continuationLength) };
28
+ const geometry: LineGeometry = {
29
+ start: line.start,
30
+ end: line.end,
31
+ next: line.next ?? line.end,
32
+ text: line.text,
33
+ offsets: [],
34
+ before: [],
35
+ after: [],
36
+ rowOf: [],
37
+ rowStarts,
38
+ rowEnds: [],
39
+ rowTriggers: triggers,
40
+ runs: [],
41
+ inlays: [],
42
+ subRows: rowStarts.length,
43
+ width: 0,
44
+ uniform
45
+ };
46
+ for (let row = 0; row < rowStarts.length; row++) {
47
+ const from = rowStarts[row]!;
48
+ const to = rowStarts[row + 1] ?? line.text.length;
49
+ const end = uniform[row === 0 ? 'first' : 'continuation'][to - from]!;
50
+ geometry.rowEnds.push(end);
51
+ geometry.width = Math.max(geometry.width, end);
52
+ if (from < to) {
53
+ geometry.runs.push({ from, to, x: row === 0 ? 0 : indent, subRow: row, text: line.text.slice(from, to) });
54
+ }
55
+ }
56
+ return geometry;
57
+ }