@adecore/editor 0.22.0 → 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.
- package/dist/attribution.d.ts +9 -8
- package/dist/attribution.js +44 -9
- package/dist/bidi.d.ts +44 -0
- package/dist/bidi.js +26 -0
- package/dist/column-selection.js +2 -1
- package/dist/controller.d.ts +6 -1
- package/dist/controller.js +93 -3
- package/dist/editor.css +81 -1
- package/dist/engine.js +184 -41
- package/dist/fake.d.ts +43 -5
- package/dist/fake.js +117 -13
- package/dist/find-search.d.ts +26 -0
- package/dist/find-search.js +130 -0
- package/dist/find.d.ts +50 -2
- package/dist/find.js +575 -25
- package/dist/geometry-stops.d.ts +6 -0
- package/dist/geometry-stops.js +47 -0
- package/dist/grammar-preparation.d.ts +29 -0
- package/dist/grammar-preparation.js +147 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/keymap-table.d.ts +10 -1
- package/dist/keymap-table.js +37 -1
- package/dist/keymap.d.ts +1 -0
- package/dist/keymap.js +4 -1
- package/dist/layout-changes.d.ts +6 -0
- package/dist/layout-changes.js +27 -0
- package/dist/layout.d.ts +45 -2
- package/dist/layout.js +202 -58
- package/dist/offsets.d.ts +1 -0
- package/dist/offsets.js +23 -0
- package/dist/outline.js +5 -4
- package/dist/paint.d.ts +11 -3
- package/dist/paint.js +180 -34
- package/dist/search-client.d.ts +27 -0
- package/dist/search-client.js +210 -0
- package/dist/search-protocol.d.ts +103 -0
- package/dist/search-protocol.js +3 -0
- package/dist/search-worker.d.ts +11 -0
- package/dist/search-worker.js +137 -0
- package/dist/semantic.d.ts +2 -1
- package/dist/semantic.js +6 -1
- package/dist/shiki-document.d.ts +8 -0
- package/dist/shiki-document.js +93 -0
- package/dist/shiki-worker.d.ts +65 -0
- package/dist/shiki-worker.js +265 -0
- package/dist/shiki.d.ts +1 -0
- package/dist/shiki.js +96 -33
- package/dist/testing.d.ts +1 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +37 -7
- package/dist/touch.d.ts +27 -0
- package/dist/touch.js +100 -0
- package/dist/types.d.ts +84 -2
- package/dist/uniform-geometry.d.ts +7 -0
- package/dist/uniform-geometry.js +49 -0
- package/dist/view.d.ts +68 -17
- package/dist/view.js +424 -97
- package/dist/worker-client.d.ts +37 -0
- package/dist/worker-client.js +195 -0
- package/package.json +17 -2
- package/src/attribution.ts +50 -14
- package/src/bidi.ts +32 -0
- package/src/column-selection.ts +2 -1
- package/src/controller.ts +97 -3
- package/src/editor.css +81 -1
- package/src/engine.ts +216 -42
- package/src/fake.ts +152 -14
- package/src/find-search.ts +165 -0
- package/src/find.ts +622 -21
- package/src/geometry-stops.ts +51 -0
- package/src/grammar-preparation.ts +158 -0
- package/src/index.ts +10 -0
- package/src/keymap-table.ts +44 -2
- package/src/keymap.ts +5 -1
- package/src/layout-changes.ts +33 -0
- package/src/layout.ts +262 -63
- package/src/offsets.ts +24 -0
- package/src/outline.ts +5 -4
- package/src/paint.ts +202 -36
- package/src/search-client.ts +306 -0
- package/src/search-protocol.ts +100 -0
- package/src/search-worker.ts +171 -0
- package/src/semantic.ts +8 -2
- package/src/shiki-document.ts +143 -0
- package/src/shiki-worker.ts +344 -0
- package/src/shiki.ts +103 -33
- package/src/testing.ts +2 -0
- package/src/tokens.ts +36 -7
- package/src/touch.ts +134 -0
- package/src/types.ts +174 -8
- package/src/uniform-geometry.ts +57 -0
- package/src/view.ts +500 -102
- 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
|
-
|
|
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
|
-
|
|
153
|
-
(
|
|
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;
|
|
@@ -66,6 +69,8 @@ export interface EditorOptions {
|
|
|
66
69
|
readonly rightMargin?: number | null;
|
|
67
70
|
/* When the arrow that folds a block shows in the gutter; on hover by default. */
|
|
68
71
|
readonly foldOutline?: EditorFoldOutline;
|
|
72
|
+
/* False disables automatic and selection folds, including fold commands. */
|
|
73
|
+
readonly folding?: boolean;
|
|
69
74
|
/* What the editor says to a person itself, in the host's words. */
|
|
70
75
|
readonly messages?: Partial<EditorMessages>;
|
|
71
76
|
/* The accessible name of the editor, such as "Commit message", in the host's language; `Code editor` without one. */
|
|
@@ -86,6 +91,18 @@ export interface EditorOptions {
|
|
|
86
91
|
export interface EditorMessages {
|
|
87
92
|
/* Select next occurrence went past the last one. */
|
|
88
93
|
readonly noMoreOccurrences: string;
|
|
94
|
+
/* 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. */
|
|
95
|
+
readonly fold: string;
|
|
96
|
+
/* The same for the control that unfolds it; `Unfold lines {start} to {end}` without one. */
|
|
97
|
+
readonly unfold: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* 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. */
|
|
101
|
+
export interface EditorInputPopup {
|
|
102
|
+
/* The id of the element that holds the options. */
|
|
103
|
+
readonly controls: string;
|
|
104
|
+
/* The id of the option that Enter takes; none while the list has no active option. */
|
|
105
|
+
readonly activeDescendant?: string;
|
|
89
106
|
}
|
|
90
107
|
|
|
91
108
|
/* What the editor does by itself as a person types. Every one is on, except the camel humps. */
|
|
@@ -193,10 +210,20 @@ export interface EditorLineHighlight {
|
|
|
193
210
|
readonly fill?: EditorMarkColor;
|
|
194
211
|
/* A character in the gutter of each line, such as `+`, in the same color. */
|
|
195
212
|
readonly sign?: string;
|
|
213
|
+
/*
|
|
214
|
+
* Draws the gutter of each line as a review does: the cell tinted in `color`, the number in a readable
|
|
215
|
+
* shade of it and a bar of whole pixels along the left edge, solid or striped, continuous over
|
|
216
|
+
* consecutive lines. It takes the place of the change mark on those lines.
|
|
217
|
+
*/
|
|
218
|
+
readonly bar?: 'solid' | 'hatched';
|
|
219
|
+
/* Per line from `startLine`, UTF-16 character ranges with a stronger tint; they follow their text through edits. */
|
|
220
|
+
readonly emphasis?: ReadonlyArray<ReadonlyArray<readonly [number, number]>>;
|
|
196
221
|
}
|
|
197
222
|
|
|
198
223
|
/* How `renderCode` draws lines that are not part of the document. */
|
|
199
224
|
export interface EditorCodeBlockOptions {
|
|
225
|
+
/* Prepared in the source document's grammar state. Omit to tokenize here; null draws plain text. */
|
|
226
|
+
readonly tokens?: ReadonlyArray<readonly LineToken[]> | null;
|
|
200
227
|
/* One-based number of the first line, shown in the gutter column; without it the lines have none. */
|
|
201
228
|
readonly firstLine?: number;
|
|
202
229
|
/* A character in the gutter of each line, such as `-`. */
|
|
@@ -233,11 +260,18 @@ export interface EditorContentChange {
|
|
|
233
260
|
readonly text: string;
|
|
234
261
|
}
|
|
235
262
|
|
|
263
|
+
export interface EditorApplyEditsOptions {
|
|
264
|
+
readonly mapSelections?: (range: EditorRange) => EditorRange;
|
|
265
|
+
readonly mergeUndo?: boolean;
|
|
266
|
+
}
|
|
267
|
+
|
|
236
268
|
export interface EditorTextChange {
|
|
237
269
|
/* In order, so a language server can follow them one by one. */
|
|
238
270
|
readonly changes: readonly EditorContentChange[];
|
|
239
271
|
/* A person typing, one of the editor's own commands such as undo, or a change from outside. */
|
|
240
272
|
readonly source: 'input' | 'command' | 'external';
|
|
273
|
+
/* 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. */
|
|
274
|
+
readonly typed?: boolean;
|
|
241
275
|
}
|
|
242
276
|
|
|
243
277
|
export type EditorMarkerSeverity = 'error' | 'warning' | 'info' | 'hint';
|
|
@@ -308,6 +342,32 @@ export interface EditorWidget {
|
|
|
308
342
|
readonly placement?: 'above' | 'below';
|
|
309
343
|
/* The height the row has until it has been measured. */
|
|
310
344
|
readonly height?: number;
|
|
345
|
+
/* Uses height as exact pixels and never measures the row. */
|
|
346
|
+
readonly fixed?: boolean;
|
|
347
|
+
/* Fills the element the row is drawn in, which the editor makes again whenever the row scrolls back into view. */
|
|
348
|
+
render(container: HTMLElement): void;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/*
|
|
352
|
+
* Lines of the text drawn as a row of the host's own DOM instead, such as a documentation comment drawn
|
|
353
|
+
* as formatted text. The lines stay in the document: copying, searching and the undo history see them as
|
|
354
|
+
* they are, and the row is as tall as its content. Put the caret on one of the lines, or fold them,
|
|
355
|
+
* and the editor draws the text again; it is for the host to take the row away at that moment when it
|
|
356
|
+
* wants the lines edited.
|
|
357
|
+
*/
|
|
358
|
+
export interface EditorReplacement {
|
|
359
|
+
readonly id: string;
|
|
360
|
+
/* Zero-based and inclusive; the row follows its lines through edits until its owner sets its replacements again. */
|
|
361
|
+
readonly startLine: number;
|
|
362
|
+
readonly endLine: number;
|
|
363
|
+
/* The height the row has until it has been measured. */
|
|
364
|
+
readonly height?: number;
|
|
365
|
+
/* What assistive technology calls the row. */
|
|
366
|
+
readonly label?: string;
|
|
367
|
+
/* 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. */
|
|
368
|
+
readonly enterable?: boolean;
|
|
369
|
+
/* 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. */
|
|
370
|
+
onReveal?(): void;
|
|
311
371
|
/* Fills the element the row is drawn in, which the editor makes again whenever the row scrolls back into view. */
|
|
312
372
|
render(container: HTMLElement): void;
|
|
313
373
|
}
|
|
@@ -362,6 +422,8 @@ export interface EditorSemanticToken {
|
|
|
362
422
|
readonly length: number;
|
|
363
423
|
/* Outermost first, as a grammar would have reported them. */
|
|
364
424
|
readonly scopes: readonly string[];
|
|
425
|
+
/* Colors only the stretches of text the grammar classed as this kind, such as a server's SQL tokens inside a string. */
|
|
426
|
+
readonly within?: LineTokenKind;
|
|
365
427
|
}
|
|
366
428
|
|
|
367
429
|
/* 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 +442,41 @@ export type ScopeColorSource = (theme: EditorTheme) => Promise<ScopeColors | nul
|
|
|
380
442
|
export interface EditorInlayHint {
|
|
381
443
|
readonly position: EditorPosition;
|
|
382
444
|
readonly label: string;
|
|
445
|
+
/* Names the hint in `onInlayHover` and `onInlayClick`; `hint-<index>` without one. */
|
|
446
|
+
readonly id?: string;
|
|
447
|
+
/* The label cut into pieces that the pointer tells apart; they add up to `label`. */
|
|
448
|
+
readonly parts?: readonly string[];
|
|
449
|
+
/* A gap of 4 pixels that keeps the pill off the character before it or the one after it. */
|
|
450
|
+
readonly paddingLeft?: boolean;
|
|
451
|
+
readonly paddingRight?: boolean;
|
|
383
452
|
}
|
|
384
453
|
|
|
454
|
+
/* A hint, or one part of its label, under the pointer. */
|
|
455
|
+
export interface EditorInlayTarget {
|
|
456
|
+
readonly id: string;
|
|
457
|
+
/* The index into `parts`, or null for a hint without parts or the padding of one. */
|
|
458
|
+
readonly part: number | null;
|
|
459
|
+
/* The hint or part, in screen coordinates. */
|
|
460
|
+
readonly rect: EditorRect;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* A press of the primary button on a hint. */
|
|
464
|
+
export interface EditorInlayClick {
|
|
465
|
+
readonly id: string;
|
|
466
|
+
readonly part: number | null;
|
|
467
|
+
/* Cmd on macOS, Ctrl elsewhere. */
|
|
468
|
+
readonly mod: boolean;
|
|
469
|
+
readonly alt: boolean;
|
|
470
|
+
readonly shift: boolean;
|
|
471
|
+
/* 2 for a second press on the same hint within half a second. */
|
|
472
|
+
readonly clicks: 1 | 2;
|
|
473
|
+
/* A finger, which has no hover; a press then stands in for it. */
|
|
474
|
+
readonly touch: boolean;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* Returns true when the press was taken; the editor then leaves the caret and the selection where they are. */
|
|
478
|
+
export type EditorInlayClickHandler = (click: EditorInlayClick) => boolean;
|
|
479
|
+
|
|
385
480
|
/*
|
|
386
481
|
* A suggestion drawn after the caret in the editor's ghost color, which is not part of the document. It
|
|
387
482
|
* 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 +496,9 @@ export interface EditorGutterAction {
|
|
|
401
496
|
readonly label: string;
|
|
402
497
|
}
|
|
403
498
|
|
|
499
|
+
/* The glyphs a gutter marker can have. */
|
|
500
|
+
export type EditorGutterIcon = 'sparkles' | 'book-open';
|
|
501
|
+
|
|
404
502
|
/* 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
503
|
export interface EditorGutterMarker {
|
|
406
504
|
/* What `onGutterMarker` says when it is pressed. */
|
|
@@ -408,6 +506,10 @@ export interface EditorGutterMarker {
|
|
|
408
506
|
/* Zero-based; it stays on this line until the host sets its markers again. */
|
|
409
507
|
readonly line: number;
|
|
410
508
|
readonly label: string;
|
|
509
|
+
/* The glyph; the sparkles without one. */
|
|
510
|
+
readonly icon?: EditorGutterIcon;
|
|
511
|
+
/* Drawn as switched on, for a marker that toggles something. */
|
|
512
|
+
readonly pressed?: boolean;
|
|
411
513
|
}
|
|
412
514
|
|
|
413
515
|
/*
|
|
@@ -435,10 +537,19 @@ export interface EditorFindQuery {
|
|
|
435
537
|
|
|
436
538
|
export interface EditorFindState {
|
|
437
539
|
readonly count: number;
|
|
438
|
-
/* Zero-based; null
|
|
540
|
+
/* Zero-based in the marked cache; null with no match or a current match beyond that cache. */
|
|
439
541
|
readonly current: number | null;
|
|
440
542
|
/* The search is in the selection and nothing was selected. */
|
|
441
543
|
readonly noSelection?: boolean;
|
|
544
|
+
/* More results exist than the marked count. */
|
|
545
|
+
readonly limited?: boolean;
|
|
546
|
+
readonly searching?: boolean;
|
|
547
|
+
readonly failed?: 'invalid' | 'timeout' | 'unavailable' | 'error';
|
|
548
|
+
readonly captureFailed?: 'timeout' | 'unavailable' | 'error';
|
|
549
|
+
readonly replaceFailed?: 'limit' | 'timeout' | 'unavailable' | 'error';
|
|
550
|
+
readonly replaced?: number;
|
|
551
|
+
/* Regex searches in large documents start only on an explicit step. */
|
|
552
|
+
readonly large?: boolean;
|
|
442
553
|
}
|
|
443
554
|
|
|
444
555
|
export interface EditorReplaceOptions {
|
|
@@ -484,15 +595,17 @@ export interface Editor {
|
|
|
484
595
|
*/
|
|
485
596
|
find(query: EditorFindQuery | null): void;
|
|
486
597
|
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.
|
|
598
|
+
/* 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
599
|
replace(replacement: string, options?: EditorReplaceOptions): boolean;
|
|
489
|
-
/*
|
|
600
|
+
/* Complete literal results return the number replaced; otherwise starts asynchronous replacement, returns zero and reports through onFind. */
|
|
490
601
|
replaceAll(replacement: string, options?: EditorReplaceOptions): number;
|
|
602
|
+
/* Atomic, one undo step; excessive output or matches are refused through onFind. */
|
|
603
|
+
replaceAllAsync(replacement: string, options?: EditorReplaceOptions): Promise<number>;
|
|
491
604
|
/* 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
605
|
setReplacePreview(replacement: string | null, options?: EditorReplaceOptions): void;
|
|
493
606
|
/* Ends the find with a caret on every match, the one the find was on last, and says how many there were. */
|
|
494
607
|
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.
|
|
608
|
+
/* 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
609
|
findFromCursor(query: EditorFindQuery, direction: 1 | -1): boolean;
|
|
497
610
|
onFind(listener: (state: EditorFindState) => void): () => void;
|
|
498
611
|
/* Takes the marks away and selects the match the find was on, so the cursor is where it stopped. */
|
|
@@ -509,8 +622,12 @@ export interface Editor {
|
|
|
509
622
|
setLabel(label: string): void;
|
|
510
623
|
/* 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
624
|
setFoldHints(hints: EditorFoldHints | null): void;
|
|
512
|
-
/*
|
|
513
|
-
|
|
625
|
+
/*
|
|
626
|
+
* Marks that follow their lines through edits until the host sets them again. Every layer (default: the unnamed one)
|
|
627
|
+
* holds its own marks and the editor draws the union, so setting one layer never clears another; an empty list
|
|
628
|
+
* removes the layer. On a line two layers mark, the later one wins, in the order the layers last became non-empty.
|
|
629
|
+
*/
|
|
630
|
+
setChangeMarks(marks: readonly EditorChangeMark[], layer?: string): void;
|
|
514
631
|
/*
|
|
515
632
|
* The blocks sticky scroll and the breadcrumb go by. The editor reads them from brackets and
|
|
516
633
|
* indentation until the host has better, such as a language server's symbols; null goes back.
|
|
@@ -528,6 +645,16 @@ export interface Editor {
|
|
|
528
645
|
setRemoteCursors(cursors: readonly EditorRemoteCursor[]): void;
|
|
529
646
|
/* 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
647
|
setLineHighlights(highlights: readonly EditorLineHighlight[]): void;
|
|
648
|
+
/* Source labels for zero-based document lines; a null label hides the number. Null restores document numbering. */
|
|
649
|
+
setLineNumbers(labels: ((line: number) => string | null) | null): void;
|
|
650
|
+
/*
|
|
651
|
+
* A visually hidden sentence for zero-based document lines that assistive technology reads with the line
|
|
652
|
+
* number, such as "Added, new line 12". A line with one hides its drawn number from assistive
|
|
653
|
+
* technology, so the sentence carries the number too. Null removes them.
|
|
654
|
+
*/
|
|
655
|
+
setLineLabels(labels: ((line: number) => string | null) | null): void;
|
|
656
|
+
/* 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. */
|
|
657
|
+
setLineTokens(tokens: ((line: number) => readonly LineToken[] | null | undefined) | null): void;
|
|
531
658
|
/*
|
|
532
659
|
* Fills a widget row's element with lines of code that are not in the document, such as the lines an
|
|
533
660
|
* agent removed, in the editor's face and colors for the language, a gutter column as wide as the
|
|
@@ -546,6 +673,12 @@ export interface Editor {
|
|
|
546
673
|
setSemanticTokens(tokens: readonly EditorSemanticToken[] | null): void;
|
|
547
674
|
/* Hints to draw as soft pills in the text, replacing the ones set before. They follow their text through edits until set again. */
|
|
548
675
|
setInlayHints(hints: readonly EditorInlayHint[]): void;
|
|
676
|
+
/* 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. */
|
|
677
|
+
onInlayHover?(listener: (target: EditorInlayTarget | null) => void): () => void;
|
|
678
|
+
/* Handlers see a press of the primary button on a hint before the editor does, the first to take it winning. */
|
|
679
|
+
onInlayClick?(handler: EditorInlayClickHandler): () => void;
|
|
680
|
+
/* 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. */
|
|
681
|
+
setInlayLink?(target: { readonly id: string; readonly part: number | null } | null): void;
|
|
549
682
|
/* A suggestion after the caret, replacing the one before; null takes it away. Any change of the text takes it away as well. */
|
|
550
683
|
setGhostText(ghost: EditorGhostText | null): void;
|
|
551
684
|
/* 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 +692,16 @@ export interface Editor {
|
|
|
559
692
|
getCaret(): EditorPosition;
|
|
560
693
|
/* In pixels, what `scrollTop` on mounting takes back. */
|
|
561
694
|
getScrollTop(): number;
|
|
695
|
+
/* Immediate pixels, canceling an animated scroll and repainting before returning. */
|
|
696
|
+
setScrollTop(top: number): void;
|
|
697
|
+
/* Intrinsic text height of a zero-based line, estimated until drawn; zero for hidden or absent lines. */
|
|
698
|
+
getLineHeight(line: number): number;
|
|
699
|
+
/* 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. */
|
|
700
|
+
getBaseLineHeight?(): number;
|
|
701
|
+
/* Text heights changed after a measurement, edit, wrap width or font change; excludes widget heights. */
|
|
702
|
+
onLineHeights(listener: () => void): () => void;
|
|
703
|
+
/* All rows in pixels, including the horizontal scrollbar, for a host that grows with its contents. */
|
|
704
|
+
getContentHeight(): number;
|
|
562
705
|
/* Which lines are folded, for `folds` on mounting. */
|
|
563
706
|
getFolds(): EditorFolds;
|
|
564
707
|
/* The primary selection, start before end; empty at the caret. */
|
|
@@ -572,6 +715,12 @@ export interface Editor {
|
|
|
572
715
|
setSelection(range: EditorRange, reveal?: EditorReveal): void;
|
|
573
716
|
/* 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
717
|
setSelections(ranges: readonly EditorRange[], reveal?: EditorReveal): void;
|
|
718
|
+
/* 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. */
|
|
719
|
+
setInputPopup(popup: EditorInputPopup | null): void;
|
|
720
|
+
/* Says a sentence politely to assistive technology without drawing anything. Saying the same sentence again is heard again. */
|
|
721
|
+
announce(message: string): void;
|
|
722
|
+
/* Replaces the sentences the editor says on its own, such as the names of the fold controls. */
|
|
723
|
+
setMessages(messages: Partial<EditorMessages>): void;
|
|
575
724
|
/* The caret moved or the text under it changed. */
|
|
576
725
|
onCaret(listener: (position: EditorPosition) => void): () => void;
|
|
577
726
|
/* The pointer moved onto another character, or null when it left the text. */
|
|
@@ -590,6 +739,12 @@ export interface Editor {
|
|
|
590
739
|
* set its rows last. An empty list takes the owner's rows away.
|
|
591
740
|
*/
|
|
592
741
|
setWidgets(widgets: readonly EditorWidget[], owner?: string): void;
|
|
742
|
+
/*
|
|
743
|
+
* Lines drawn as rows of the host's own DOM, replacing the ones the same owner set before; other
|
|
744
|
+
* owners keep theirs. Where two owners claim the same first line the owner that sorts first keeps
|
|
745
|
+
* it, and a line the person folded draws its fold instead. Optional so an editor from before it still fits.
|
|
746
|
+
*/
|
|
747
|
+
setReplacements?(replacements: readonly EditorReplacement[], owner?: string): void;
|
|
593
748
|
/* 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
749
|
setLineActions(actions: readonly EditorLineAction[], owner?: string): void;
|
|
595
750
|
/* The code vision rows above declarations, replacing the ones set before. Separate from the widgets, which they never displace. */
|
|
@@ -604,12 +759,14 @@ export interface Editor {
|
|
|
604
759
|
setSelectionRanges(provider: EditorSelectionRanges | null): void;
|
|
605
760
|
/* 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
761
|
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;
|
|
762
|
+
/* 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. */
|
|
763
|
+
applyEdits(edits: readonly EditorContentChange[], options?: EditorApplyEditsOptions): boolean;
|
|
609
764
|
setTheme(theme: EditorTheme): void;
|
|
610
765
|
/* Reads the code face off the page again, after the page changed its size, family or ligatures. */
|
|
611
766
|
refreshFont(): void;
|
|
612
767
|
setReadOnly(readOnly: boolean, reason?: string): void;
|
|
768
|
+
/* Whether the editor refuses edits. Optional so an editor from before it still fits. */
|
|
769
|
+
isReadOnly?(): boolean;
|
|
613
770
|
/* Runs an editing command on every caret, as its key would. False when it changed nothing. */
|
|
614
771
|
runCommand(command: EditorRunCommand): boolean;
|
|
615
772
|
focus(): void;
|
|
@@ -639,8 +796,13 @@ export interface LineToken {
|
|
|
639
796
|
readonly color: string;
|
|
640
797
|
/* Bit flags as Shiki writes them: 1 italic, 2 bold, 4 underline, 8 strikethrough. */
|
|
641
798
|
readonly fontStyle: number;
|
|
799
|
+
/* What the grammar says the stretch is, when it says so; absent for plain code. */
|
|
800
|
+
readonly kind?: LineTokenKind;
|
|
642
801
|
}
|
|
643
802
|
|
|
803
|
+
/* The classes of text a grammar tells apart from code. */
|
|
804
|
+
export type LineTokenKind = 'comment' | 'string' | 'regex';
|
|
805
|
+
|
|
644
806
|
export interface TokenizedLine {
|
|
645
807
|
readonly tokens: readonly LineToken[];
|
|
646
808
|
/* What the next line starts from; only the tokenizer that made it reads it. */
|
|
@@ -657,12 +819,16 @@ export interface LineTokenizer {
|
|
|
657
819
|
sameState(a: unknown, b: unknown): boolean;
|
|
658
820
|
/* True once, when the grammar was rebuilt and the states handed out before it are void. */
|
|
659
821
|
stale?(): boolean;
|
|
822
|
+
/* Loads missing embedded grammars named by text; true when a load can invalidate earlier states. */
|
|
823
|
+
prepare?(text: string): Promise<boolean>;
|
|
660
824
|
}
|
|
661
825
|
|
|
662
826
|
/* 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
827
|
export type TokenizerSource = (language: string | undefined, theme: EditorTheme, text?: string) => Promise<LineTokenizer | null>;
|
|
664
828
|
|
|
665
829
|
export interface SmartEditorEngineOptions {
|
|
830
|
+
/* One lazy search worker per mounted editor; regex search is unavailable without it. */
|
|
831
|
+
readonly searchWorker?: EditorSearchClientOptions['workerFactory'];
|
|
666
832
|
readonly keymap?: Keymap;
|
|
667
833
|
readonly tokenizer: TokenizerSource;
|
|
668
834
|
/* 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
|
+
}
|