@adecore/editor 0.39.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.
Files changed (53) hide show
  1. package/dist/controller.d.ts +1 -0
  2. package/dist/controller.js +17 -28
  3. package/dist/engine.js +2 -9
  4. package/dist/find.js +12 -13
  5. package/dist/layout.js +7 -2
  6. package/dist/paint.d.ts +2 -0
  7. package/dist/paint.js +23 -20
  8. package/dist/view.d.ts +3 -2
  9. package/dist/view.js +46 -59
  10. package/package.json +6 -6
  11. package/src/attribution.ts +14 -0
  12. package/src/code-block.ts +25 -0
  13. package/src/column-selection.ts +10 -0
  14. package/src/controller.ts +189 -28
  15. package/src/engine.ts +87 -9
  16. package/src/fake.ts +79 -0
  17. package/src/find-search.ts +20 -0
  18. package/src/find.ts +135 -14
  19. package/src/geometry-stops.ts +10 -0
  20. package/src/grammar-preparation.ts +23 -0
  21. package/src/index.ts +1 -0
  22. package/src/input.ts +13 -0
  23. package/src/keymap-table.ts +9 -0
  24. package/src/keymap.ts +25 -0
  25. package/src/layout-changes.ts +7 -0
  26. package/src/layout.ts +219 -3
  27. package/src/lens.ts +3 -0
  28. package/src/listeners.ts +1 -0
  29. package/src/metrics.ts +14 -0
  30. package/src/modifier-gesture.ts +17 -0
  31. package/src/native-input.ts +27 -0
  32. package/src/offsets.ts +10 -0
  33. package/src/outline.ts +38 -0
  34. package/src/overview.ts +13 -0
  35. package/src/paint.ts +140 -20
  36. package/src/php-html.ts +1 -0
  37. package/src/pointer.ts +50 -0
  38. package/src/scroll-animation.ts +10 -0
  39. package/src/scroll.ts +7 -0
  40. package/src/search-client.ts +32 -0
  41. package/src/search-worker.ts +38 -0
  42. package/src/semantic.ts +12 -0
  43. package/src/shiki-document.ts +19 -0
  44. package/src/shiki-worker.ts +68 -0
  45. package/src/shiki.ts +37 -0
  46. package/src/testing.ts +17 -0
  47. package/src/theme-scopes.ts +14 -0
  48. package/src/tokens.ts +31 -0
  49. package/src/touch.ts +16 -0
  50. package/src/tracked-range.ts +5 -0
  51. package/src/uniform-geometry.ts +10 -0
  52. package/src/view.ts +481 -75
  53. 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
 
@@ -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
  }
@@ -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
  }