@adecore/editor 0.38.0 → 0.40.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/controller.d.ts +1 -0
- package/dist/controller.js +17 -28
- package/dist/engine.js +2 -9
- package/dist/find.js +12 -13
- package/dist/layout.js +7 -2
- package/dist/paint.d.ts +2 -0
- package/dist/paint.js +23 -20
- package/dist/view.d.ts +3 -2
- package/dist/view.js +46 -59
- package/package.json +6 -6
- package/src/attribution.ts +14 -0
- package/src/code-block.ts +25 -0
- package/src/column-selection.ts +10 -0
- package/src/controller.ts +189 -28
- package/src/engine.ts +87 -9
- package/src/fake.ts +79 -0
- package/src/find-search.ts +20 -0
- package/src/find.ts +135 -14
- package/src/geometry-stops.ts +10 -0
- package/src/grammar-preparation.ts +23 -0
- package/src/index.ts +1 -0
- package/src/input.ts +13 -0
- package/src/keymap-table.ts +9 -0
- package/src/keymap.ts +25 -0
- package/src/layout-changes.ts +7 -0
- package/src/layout.ts +219 -3
- package/src/lens.ts +3 -0
- package/src/listeners.ts +1 -0
- package/src/metrics.ts +14 -0
- package/src/modifier-gesture.ts +17 -0
- package/src/native-input.ts +27 -0
- package/src/offsets.ts +10 -0
- package/src/outline.ts +38 -0
- package/src/overview.ts +13 -0
- package/src/paint.ts +140 -20
- package/src/php-html.ts +1 -0
- package/src/pointer.ts +50 -0
- package/src/scroll-animation.ts +10 -0
- package/src/scroll.ts +7 -0
- package/src/search-client.ts +32 -0
- package/src/search-worker.ts +38 -0
- package/src/semantic.ts +12 -0
- package/src/shiki-document.ts +19 -0
- package/src/shiki-worker.ts +68 -0
- package/src/shiki.ts +37 -0
- package/src/testing.ts +17 -0
- package/src/theme-scopes.ts +14 -0
- package/src/tokens.ts +31 -0
- package/src/touch.ts +16 -0
- package/src/tracked-range.ts +5 -0
- package/src/uniform-geometry.ts +10 -0
- package/src/view.ts +481 -75
- package/src/worker-client.ts +54 -0
|
@@ -2,10 +2,13 @@ import type { DocumentChange, DocumentModel } from '@adecore/editor-core';
|
|
|
2
2
|
import type { LineTokenizer } from './types.ts';
|
|
3
3
|
|
|
4
4
|
type Schedule = (callback: () => void, delay: number) => () => void;
|
|
5
|
+
|
|
5
6
|
const scheduleTimer: Schedule = (callback, delay) => {
|
|
6
7
|
const timer = setTimeout(callback, delay);
|
|
8
|
+
|
|
7
9
|
return () => clearTimeout(timer);
|
|
8
10
|
};
|
|
11
|
+
|
|
9
12
|
const MAX_BUFFER = 64 * 1024;
|
|
10
13
|
|
|
11
14
|
/* Edits collect language declarations without making every key wait for grammar loading. */
|
|
@@ -47,6 +50,7 @@ export class GrammarPreparation {
|
|
|
47
50
|
if (this.disposed || this.paused) {
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
53
|
+
|
|
50
54
|
if (this.tokenizer === null || batches.length !== 1) {
|
|
51
55
|
this.full = true;
|
|
52
56
|
this.lastLine = -1;
|
|
@@ -55,32 +59,42 @@ export class GrammarPreparation {
|
|
|
55
59
|
} else if (!this.full) {
|
|
56
60
|
let shift = 0;
|
|
57
61
|
let through = -1;
|
|
62
|
+
|
|
58
63
|
for (const change of batches[0]!) {
|
|
59
64
|
const first = this.model.positionAt(change.from + shift).line;
|
|
60
65
|
const last = this.model.positionAt(change.from + shift + change.insertedLength).line;
|
|
61
66
|
shift += change.insertedLength - (change.to - change.from);
|
|
67
|
+
|
|
62
68
|
for (let line = Math.max(first, through + 1); line <= last; line++) {
|
|
63
69
|
const text = this.model.getLine(line).text;
|
|
64
70
|
const replacing = batches[0]!.length === 1 && first === last && line === this.lastLine;
|
|
71
|
+
|
|
65
72
|
if (replacing) {
|
|
66
73
|
this.size -= this.pending.pop()!.length + 1;
|
|
67
74
|
}
|
|
75
|
+
|
|
68
76
|
this.size += text.length + 1;
|
|
77
|
+
|
|
69
78
|
if (this.size > MAX_BUFFER) {
|
|
70
79
|
this.full = true;
|
|
71
80
|
this.pending = [];
|
|
72
81
|
this.lastLine = -1;
|
|
82
|
+
|
|
73
83
|
break;
|
|
74
84
|
}
|
|
85
|
+
|
|
75
86
|
this.pending.push(text);
|
|
76
87
|
this.lastLine = line;
|
|
77
88
|
}
|
|
89
|
+
|
|
78
90
|
through = last;
|
|
91
|
+
|
|
79
92
|
if (this.full) {
|
|
80
93
|
break;
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
96
|
}
|
|
97
|
+
|
|
84
98
|
this.scheduleFlush();
|
|
85
99
|
}
|
|
86
100
|
|
|
@@ -88,9 +102,11 @@ export class GrammarPreparation {
|
|
|
88
102
|
if (this.paused === paused) {
|
|
89
103
|
return;
|
|
90
104
|
}
|
|
105
|
+
|
|
91
106
|
this.paused = paused;
|
|
92
107
|
this.generation++;
|
|
93
108
|
this.clear();
|
|
109
|
+
|
|
94
110
|
if (!paused) {
|
|
95
111
|
this.full = true;
|
|
96
112
|
this.scheduleFlush();
|
|
@@ -118,8 +134,10 @@ export class GrammarPreparation {
|
|
|
118
134
|
if (this.disposed || this.paused || this.tokenizer?.prepare === undefined || (!this.full && this.pending.length === 0)) {
|
|
119
135
|
return;
|
|
120
136
|
}
|
|
137
|
+
|
|
121
138
|
this.cancelTimer?.();
|
|
122
139
|
this.due = false;
|
|
140
|
+
|
|
123
141
|
this.cancelTimer = this.schedule(() => {
|
|
124
142
|
this.cancelTimer = null;
|
|
125
143
|
this.due = true;
|
|
@@ -129,9 +147,11 @@ export class GrammarPreparation {
|
|
|
129
147
|
|
|
130
148
|
private async flush(): Promise<void> {
|
|
131
149
|
const tokenizer = this.tokenizer;
|
|
150
|
+
|
|
132
151
|
if (this.running || this.disposed || this.paused || tokenizer?.prepare === undefined || !this.due) {
|
|
133
152
|
return;
|
|
134
153
|
}
|
|
154
|
+
|
|
135
155
|
const generation = this.generation;
|
|
136
156
|
const text = this.full ? this.model.getText() : this.pending.join('\n');
|
|
137
157
|
this.pending = [];
|
|
@@ -140,8 +160,10 @@ export class GrammarPreparation {
|
|
|
140
160
|
this.full = false;
|
|
141
161
|
this.due = false;
|
|
142
162
|
this.running = true;
|
|
163
|
+
|
|
143
164
|
try {
|
|
144
165
|
await tokenizer.prepare(text);
|
|
166
|
+
|
|
145
167
|
if (!this.disposed && !this.paused && generation === this.generation && tokenizer === this.tokenizer) {
|
|
146
168
|
// A load completed while paused may already have rebuilt this grammar.
|
|
147
169
|
this.changed();
|
|
@@ -150,6 +172,7 @@ export class GrammarPreparation {
|
|
|
150
172
|
// Keep the current colors; a later edit may retry the failed load.
|
|
151
173
|
} finally {
|
|
152
174
|
this.running = false;
|
|
175
|
+
|
|
153
176
|
if (this.due) {
|
|
154
177
|
void this.flush();
|
|
155
178
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from '@adecore/edi
|
|
|
4
4
|
export { createSmartEditorEngine } from './engine.ts';
|
|
5
5
|
export { DEFAULT_SMART_KEYS } from './smart-keys.ts';
|
|
6
6
|
export { shikiScopeColors, shikiTokenizers } from './shiki.ts';
|
|
7
|
+
|
|
7
8
|
export type {
|
|
8
9
|
Editor,
|
|
9
10
|
EditorAttributionHover,
|
package/src/input.ts
CHANGED
|
@@ -3,22 +3,28 @@ import type { Selection, TextEdit } from '@adecore/editor-core';
|
|
|
3
3
|
/* The one stretch two texts differ in, found from both ends and never inside a surrogate pair. */
|
|
4
4
|
export function findChange(before: string, after: string): TextEdit {
|
|
5
5
|
let from = 0;
|
|
6
|
+
|
|
6
7
|
while (from < before.length && from < after.length && before[from] === after[from]) {
|
|
7
8
|
from++;
|
|
8
9
|
}
|
|
10
|
+
|
|
9
11
|
if (from > 0 && /[\uDC00-\uDFFF]/.test(before[from] ?? '') && /[\uD800-\uDBFF]/.test(before[from - 1]!)) {
|
|
10
12
|
from--;
|
|
11
13
|
}
|
|
14
|
+
|
|
12
15
|
let endBefore = before.length;
|
|
13
16
|
let endAfter = after.length;
|
|
17
|
+
|
|
14
18
|
while (endBefore > from && endAfter > from && before[endBefore - 1] === after[endAfter - 1]) {
|
|
15
19
|
endBefore--;
|
|
16
20
|
endAfter--;
|
|
17
21
|
}
|
|
22
|
+
|
|
18
23
|
if (endBefore < before.length && /[\uDC00-\uDFFF]/.test(before[endBefore] ?? '') && /[\uD800-\uDBFF]/.test(before[endBefore - 1] ?? '')) {
|
|
19
24
|
endBefore++;
|
|
20
25
|
endAfter++;
|
|
21
26
|
}
|
|
27
|
+
|
|
22
28
|
return { from, to: endBefore, text: after.slice(from, endAfter) };
|
|
23
29
|
}
|
|
24
30
|
|
|
@@ -27,15 +33,18 @@ export function replacementEdits(selections: readonly Selection[], text: string)
|
|
|
27
33
|
const sorted = selections
|
|
28
34
|
.map((selection, index) => ({ from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head), index }))
|
|
29
35
|
.sort((left, right) => left.from - right.from);
|
|
36
|
+
|
|
30
37
|
const edits: TextEdit[] = [];
|
|
31
38
|
const positions: Selection[] = [];
|
|
32
39
|
let delta = 0;
|
|
40
|
+
|
|
33
41
|
for (const range of sorted) {
|
|
34
42
|
edits.push({ from: range.from, to: range.to, text });
|
|
35
43
|
const head = range.from + delta + text.length;
|
|
36
44
|
positions[range.index] = { anchor: head, head };
|
|
37
45
|
delta += text.length - (range.to - range.from);
|
|
38
46
|
}
|
|
47
|
+
|
|
39
48
|
return { edits, selections: positions };
|
|
40
49
|
}
|
|
41
50
|
|
|
@@ -46,15 +55,19 @@ export function textareaText(text: string): string {
|
|
|
46
55
|
/* A textarea normalizes CRLF; the model keeps its own UTF-16 offsets. */
|
|
47
56
|
export function modelOffset(text: string, nativeOffset: number): number {
|
|
48
57
|
let native = 0;
|
|
58
|
+
|
|
49
59
|
for (let index = 0; index < text.length; index++) {
|
|
50
60
|
if (native === nativeOffset) {
|
|
51
61
|
return index;
|
|
52
62
|
}
|
|
63
|
+
|
|
53
64
|
if (text[index] === '\r' && text[index + 1] === '\n') {
|
|
54
65
|
continue;
|
|
55
66
|
}
|
|
67
|
+
|
|
56
68
|
native++;
|
|
57
69
|
}
|
|
70
|
+
|
|
58
71
|
return text.length;
|
|
59
72
|
}
|
|
60
73
|
|
package/src/keymap-table.ts
CHANGED
|
@@ -151,8 +151,10 @@ const MODIFIERS = new Map<string, 'mod' | 'ctrl' | 'meta' | 'alt' | 'shift'>([
|
|
|
151
151
|
export function parseChord(text: string): KeyChord {
|
|
152
152
|
const words = text.endsWith('++') ? [...text.slice(0, -2).split('+'), '+'] : text.split('+');
|
|
153
153
|
const chord = { mod: false, ctrl: false, meta: false, alt: false, shift: false, key: '' };
|
|
154
|
+
|
|
154
155
|
for (const word of words) {
|
|
155
156
|
const modifier = MODIFIERS.get(word);
|
|
157
|
+
|
|
156
158
|
if (modifier !== undefined) {
|
|
157
159
|
chord[modifier] = true;
|
|
158
160
|
} else if (word === '' || chord.key !== '') {
|
|
@@ -161,9 +163,11 @@ export function parseChord(text: string): KeyChord {
|
|
|
161
163
|
chord.key = word.length === 1 ? word.toUpperCase() : word;
|
|
162
164
|
}
|
|
163
165
|
}
|
|
166
|
+
|
|
164
167
|
if (chord.key === '') {
|
|
165
168
|
throw new Error(`The chord "${text}" names no key`);
|
|
166
169
|
}
|
|
170
|
+
|
|
167
171
|
return chord;
|
|
168
172
|
}
|
|
169
173
|
|
|
@@ -184,10 +188,13 @@ const BASE_PUNCTUATION: Readonly<Record<string, string>> = {
|
|
|
184
188
|
/* The character a physical key has with no layer on it, null for a key that has none, such as an arrow or Enter. */
|
|
185
189
|
function baseCharacter(code: string): string | null {
|
|
186
190
|
const letter = /^Key([A-Z])$/.exec(code);
|
|
191
|
+
|
|
187
192
|
if (letter !== null) {
|
|
188
193
|
return letter[1].toLowerCase();
|
|
189
194
|
}
|
|
195
|
+
|
|
190
196
|
const digit = /^(?:Digit|Numpad)([0-9])$/.exec(code);
|
|
197
|
+
|
|
191
198
|
return digit !== null ? digit[1] : (BASE_PUNCTUATION[code] ?? null);
|
|
192
199
|
}
|
|
193
200
|
|
|
@@ -200,6 +207,8 @@ export function typesInsteadOfChord(event: { key: string; code?: string; ctrlKey
|
|
|
200
207
|
if (apple || !event.ctrlKey || !event.altKey || event.code === undefined || [...event.key].length !== 1) {
|
|
201
208
|
return false;
|
|
202
209
|
}
|
|
210
|
+
|
|
203
211
|
const base = baseCharacter(event.code);
|
|
212
|
+
|
|
204
213
|
return base !== null && event.key.toLowerCase() !== base;
|
|
205
214
|
}
|
package/src/keymap.ts
CHANGED
|
@@ -108,23 +108,31 @@ const FOLD_CODES: Readonly<Record<string, readonly string[]>> = { '-': ['Minus',
|
|
|
108
108
|
/* Letters and punctuation by the physical key when the event has one, since Option changes the character on macOS and Shift on every platform. */
|
|
109
109
|
function keyMatches(chordKey: string, event: KeyLike): boolean {
|
|
110
110
|
const code = event.code;
|
|
111
|
+
|
|
111
112
|
if (/^[A-Z]$/.test(chordKey)) {
|
|
112
113
|
return code ? code === `Key${chordKey}` : event.key.toUpperCase() === chordKey;
|
|
113
114
|
}
|
|
115
|
+
|
|
114
116
|
if (/^[0-9]$/.test(chordKey)) {
|
|
115
117
|
return code ? code === `Digit${chordKey}` || code === `Numpad${chordKey}` : event.key === chordKey;
|
|
116
118
|
}
|
|
119
|
+
|
|
117
120
|
const fold = FOLD_CODES[chordKey];
|
|
121
|
+
|
|
118
122
|
if (fold !== undefined) {
|
|
119
123
|
return (code !== undefined && fold.includes(code)) || event.key === chordKey || (chordKey === '=' && event.key === '+');
|
|
120
124
|
}
|
|
125
|
+
|
|
121
126
|
const punctuation = PUNCTUATION_CODES[chordKey];
|
|
127
|
+
|
|
122
128
|
if (punctuation !== undefined) {
|
|
123
129
|
return code ? code === punctuation : event.key === chordKey;
|
|
124
130
|
}
|
|
131
|
+
|
|
125
132
|
if (chordKey === 'Space') {
|
|
126
133
|
return event.key === ' ' || code === 'Space';
|
|
127
134
|
}
|
|
135
|
+
|
|
128
136
|
return event.key === chordKey;
|
|
129
137
|
}
|
|
130
138
|
|
|
@@ -133,8 +141,10 @@ export function chordMatches(chord: KeyChord, event: KeyLike, apple: boolean): b
|
|
|
133
141
|
if (typesInsteadOfChord(event, apple)) {
|
|
134
142
|
return false;
|
|
135
143
|
}
|
|
144
|
+
|
|
136
145
|
const wantsMeta = chord.meta || (chord.mod && apple);
|
|
137
146
|
const wantsCtrl = chord.ctrl || (chord.mod && !apple);
|
|
147
|
+
|
|
138
148
|
return (
|
|
139
149
|
wantsMeta === event.metaKey &&
|
|
140
150
|
wantsCtrl === event.ctrlKey &&
|
|
@@ -155,6 +165,7 @@ function bound(apple: boolean, keymap: Keymap): BoundAction[] {
|
|
|
155
165
|
return KEYMAP_IDS.flatMap((id) => {
|
|
156
166
|
const text = chordOf(id, apple, keymap);
|
|
157
167
|
const action = ACTIONS[id];
|
|
168
|
+
|
|
158
169
|
return text === null || action === undefined ? [] : [{ chord: parseChord(text), action }];
|
|
159
170
|
});
|
|
160
171
|
}
|
|
@@ -165,47 +176,61 @@ function bound(apple: boolean, keymap: Keymap): BoundAction[] {
|
|
|
165
176
|
*/
|
|
166
177
|
export function keyAction(event: KeyLike, apple: boolean, keymap: Keymap = KEYMAP): KeyAction | null {
|
|
167
178
|
let resolved = bindings.get(keymap);
|
|
179
|
+
|
|
168
180
|
if (resolved === undefined) {
|
|
169
181
|
resolved = { mac: bound(true, keymap), other: bound(false, keymap) };
|
|
170
182
|
bindings.set(keymap, resolved);
|
|
171
183
|
}
|
|
184
|
+
|
|
172
185
|
for (const { chord, action } of resolved[apple ? 'mac' : 'other']) {
|
|
173
186
|
if (chordMatches(chord, event, apple)) {
|
|
174
187
|
return action;
|
|
175
188
|
}
|
|
176
189
|
}
|
|
190
|
+
|
|
177
191
|
const key = event.key.toLowerCase();
|
|
178
192
|
const mod = apple ? event.metaKey : event.ctrlKey;
|
|
179
193
|
const { altKey: alt, shiftKey: shift, ctrlKey: ctrl, metaKey: meta } = event;
|
|
194
|
+
|
|
180
195
|
if (mod && !alt && (key === 'home' || key === 'end')) {
|
|
181
196
|
return { type: 'edge', end: key === 'end', extend: shift };
|
|
182
197
|
}
|
|
198
|
+
|
|
183
199
|
if (mod || alt || ctrl || meta) {
|
|
184
200
|
return null;
|
|
185
201
|
}
|
|
202
|
+
|
|
186
203
|
if (key === 'home') {
|
|
187
204
|
return command(shift ? 'selectSmartHome' : 'smartHome');
|
|
188
205
|
}
|
|
206
|
+
|
|
189
207
|
if (key === 'end') {
|
|
190
208
|
return command(shift ? 'selectSmartEnd' : 'smartEnd');
|
|
191
209
|
}
|
|
210
|
+
|
|
192
211
|
if (key === 'tab') {
|
|
193
212
|
return command(shift ? 'outdent' : 'insertTab');
|
|
194
213
|
}
|
|
214
|
+
|
|
195
215
|
if (key === 'enter') {
|
|
196
216
|
return command('insertNewline');
|
|
197
217
|
}
|
|
218
|
+
|
|
198
219
|
if (key === 'backspace') {
|
|
199
220
|
return command('smartBackspace');
|
|
200
221
|
}
|
|
222
|
+
|
|
201
223
|
if (key === 'delete') {
|
|
202
224
|
return command('deleteForward');
|
|
203
225
|
}
|
|
226
|
+
|
|
204
227
|
if (MOVES.has(event.key)) {
|
|
205
228
|
return { type: 'move', key: event.key as MoveKey, extend: shift };
|
|
206
229
|
}
|
|
230
|
+
|
|
207
231
|
if (key === 'escape') {
|
|
208
232
|
return { type: 'escape' };
|
|
209
233
|
}
|
|
234
|
+
|
|
210
235
|
return null;
|
|
211
236
|
}
|
package/src/layout-changes.ts
CHANGED
|
@@ -8,26 +8,33 @@ export interface LineBounds {
|
|
|
8
8
|
|
|
9
9
|
export function lineChangeMapper(batches: readonly (readonly DocumentChange[])[]): (bounds: LineBounds) => LineBounds | null {
|
|
10
10
|
const steps = batches.map((changes) => ({ changes, map: offsetMapper(changes) }));
|
|
11
|
+
|
|
11
12
|
return (bounds) => {
|
|
12
13
|
let { start, end } = bounds;
|
|
14
|
+
|
|
13
15
|
for (const { changes, map } of steps) {
|
|
14
16
|
let first = 0;
|
|
15
17
|
let last = changes.length;
|
|
18
|
+
|
|
16
19
|
while (first < last) {
|
|
17
20
|
const middle = (first + last) >>> 1;
|
|
21
|
+
|
|
18
22
|
if (changes[middle]!.to < start) {
|
|
19
23
|
first = middle + 1;
|
|
20
24
|
} else {
|
|
21
25
|
last = middle;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
28
|
+
|
|
24
29
|
// Touching either boundary can merge lines or change a CRLF ending.
|
|
25
30
|
if (changes[first] !== undefined && changes[first]!.from <= end) {
|
|
26
31
|
return null;
|
|
27
32
|
}
|
|
33
|
+
|
|
28
34
|
start = map(start);
|
|
29
35
|
end = map(end);
|
|
30
36
|
}
|
|
37
|
+
|
|
31
38
|
return { start, end };
|
|
32
39
|
};
|
|
33
40
|
}
|