@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.
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 +184 -41
  10. package/dist/fake.d.ts +43 -5
  11. package/dist/fake.js +117 -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 +84 -2
  55. package/dist/uniform-geometry.d.ts +7 -0
  56. package/dist/uniform-geometry.js +49 -0
  57. package/dist/view.d.ts +68 -17
  58. package/dist/view.js +424 -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 +216 -42
  68. package/src/fake.ts +152 -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 +174 -8
  92. package/src/uniform-geometry.ts +57 -0
  93. package/src/view.ts +500 -102
  94. package/src/worker-client.ts +212 -0
@@ -0,0 +1,51 @@
1
+ import type { Affinity, LineGeometry } from './layout.ts';
2
+
3
+ export function stopCount(geometry: LineGeometry): number {
4
+ return geometry.uniform === undefined ? geometry.offsets.length : geometry.text.length + 1;
5
+ }
6
+
7
+ export function offsetOfStop(geometry: LineGeometry, stop: number): number {
8
+ return geometry.uniform === undefined ? geometry.offsets[stop]! : stop;
9
+ }
10
+
11
+ export function rowOfStop(geometry: LineGeometry, stop: number): number {
12
+ if (geometry.uniform === undefined) {
13
+ return geometry.rowOf[stop]!;
14
+ }
15
+ let first = 0;
16
+ let last = geometry.rowStarts.length;
17
+ while (first < last) {
18
+ const middle = (first + last) >>> 1;
19
+ if (geometry.rowStarts[middle]! <= stop) {
20
+ first = middle + 1;
21
+ } else {
22
+ last = middle;
23
+ }
24
+ }
25
+ return Math.max(0, first - 1);
26
+ }
27
+
28
+ export function xOfStop(geometry: LineGeometry, stop: number, affinity: Affinity = 'before'): number {
29
+ if (geometry.uniform === undefined) {
30
+ return geometry[affinity][stop]!;
31
+ }
32
+ const row = rowOfStop(geometry, stop);
33
+ return geometry.uniform[row === 0 ? 'first' : 'continuation'][stop - geometry.rowStarts[row]!]!;
34
+ }
35
+
36
+ export function stopAtOffset(geometry: LineGeometry, offset: number): number {
37
+ if (geometry.uniform !== undefined) {
38
+ return Math.min(geometry.text.length, Math.max(0, Math.ceil(offset)));
39
+ }
40
+ let first = 0;
41
+ let last = geometry.offsets.length;
42
+ while (first < last) {
43
+ const middle = (first + last) >>> 1;
44
+ if (geometry.offsets[middle]! < offset) {
45
+ first = middle + 1;
46
+ } else {
47
+ last = middle;
48
+ }
49
+ }
50
+ return Math.min(geometry.offsets.length - 1, first);
51
+ }
@@ -0,0 +1,158 @@
1
+ import type { DocumentChange, DocumentModel } from '@adecore/editor-core';
2
+ import type { LineTokenizer } from './types.ts';
3
+
4
+ type Schedule = (callback: () => void, delay: number) => () => void;
5
+ const scheduleTimer: Schedule = (callback, delay) => {
6
+ const timer = setTimeout(callback, delay);
7
+ return () => clearTimeout(timer);
8
+ };
9
+ const MAX_BUFFER = 64 * 1024;
10
+
11
+ /* Edits collect language declarations without making every key wait for grammar loading. */
12
+ export class GrammarPreparation {
13
+ private tokenizer: LineTokenizer | null = null;
14
+ private generation = 0;
15
+ private pending: string[] = [];
16
+ private size = 0;
17
+ private lastLine = -1;
18
+ private full = false;
19
+ private running = false;
20
+ private due = false;
21
+ private paused = false;
22
+ private disposed = false;
23
+ private cancelTimer: (() => void) | null = null;
24
+
25
+ private readonly model: Pick<DocumentModel, 'getText' | 'getLine' | 'positionAt'>;
26
+ private readonly changed: () => void;
27
+ private readonly schedule: Schedule;
28
+
29
+ constructor(model: Pick<DocumentModel, 'getText' | 'getLine' | 'positionAt'>, changed: () => void, schedule: Schedule = scheduleTimer) {
30
+ this.model = model;
31
+ this.changed = changed;
32
+ this.schedule = schedule;
33
+ }
34
+
35
+ begin(): void {
36
+ this.generation++;
37
+ this.tokenizer = null;
38
+ this.clear();
39
+ }
40
+
41
+ ready(tokenizer: LineTokenizer | null): void {
42
+ this.tokenizer = tokenizer;
43
+ this.scheduleFlush();
44
+ }
45
+
46
+ edited(batches: readonly (readonly DocumentChange[])[]): void {
47
+ if (this.disposed || this.paused) {
48
+ return;
49
+ }
50
+ if (this.tokenizer === null || batches.length !== 1) {
51
+ this.full = true;
52
+ this.lastLine = -1;
53
+ } else if (this.tokenizer.prepare === undefined) {
54
+ return;
55
+ } else if (!this.full) {
56
+ let shift = 0;
57
+ let through = -1;
58
+ for (const change of batches[0]!) {
59
+ const first = this.model.positionAt(change.from + shift).line;
60
+ const last = this.model.positionAt(change.from + shift + change.insertedLength).line;
61
+ shift += change.insertedLength - (change.to - change.from);
62
+ for (let line = Math.max(first, through + 1); line <= last; line++) {
63
+ const text = this.model.getLine(line).text;
64
+ const replacing = batches[0]!.length === 1 && first === last && line === this.lastLine;
65
+ if (replacing) {
66
+ this.size -= this.pending.pop()!.length + 1;
67
+ }
68
+ this.size += text.length + 1;
69
+ if (this.size > MAX_BUFFER) {
70
+ this.full = true;
71
+ this.pending = [];
72
+ this.lastLine = -1;
73
+ break;
74
+ }
75
+ this.pending.push(text);
76
+ this.lastLine = line;
77
+ }
78
+ through = last;
79
+ if (this.full) {
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ this.scheduleFlush();
85
+ }
86
+
87
+ pause(paused: boolean): void {
88
+ if (this.paused === paused) {
89
+ return;
90
+ }
91
+ this.paused = paused;
92
+ this.generation++;
93
+ this.clear();
94
+ if (!paused) {
95
+ this.full = true;
96
+ this.scheduleFlush();
97
+ }
98
+ }
99
+
100
+ dispose(): void {
101
+ this.disposed = true;
102
+ this.generation++;
103
+ this.clear();
104
+ this.tokenizer = null;
105
+ }
106
+
107
+ private clear(): void {
108
+ this.cancelTimer?.();
109
+ this.cancelTimer = null;
110
+ this.pending = [];
111
+ this.size = 0;
112
+ this.lastLine = -1;
113
+ this.full = false;
114
+ this.due = false;
115
+ }
116
+
117
+ private scheduleFlush(): void {
118
+ if (this.disposed || this.paused || this.tokenizer?.prepare === undefined || (!this.full && this.pending.length === 0)) {
119
+ return;
120
+ }
121
+ this.cancelTimer?.();
122
+ this.due = false;
123
+ this.cancelTimer = this.schedule(() => {
124
+ this.cancelTimer = null;
125
+ this.due = true;
126
+ void this.flush();
127
+ }, 200);
128
+ }
129
+
130
+ private async flush(): Promise<void> {
131
+ const tokenizer = this.tokenizer;
132
+ if (this.running || this.disposed || this.paused || tokenizer?.prepare === undefined || !this.due) {
133
+ return;
134
+ }
135
+ const generation = this.generation;
136
+ const text = this.full ? this.model.getText() : this.pending.join('\n');
137
+ this.pending = [];
138
+ this.size = 0;
139
+ this.lastLine = -1;
140
+ this.full = false;
141
+ this.due = false;
142
+ this.running = true;
143
+ try {
144
+ await tokenizer.prepare(text);
145
+ if (!this.disposed && !this.paused && generation === this.generation && tokenizer === this.tokenizer) {
146
+ // A load completed while paused may already have rebuilt this grammar.
147
+ this.changed();
148
+ }
149
+ } catch {
150
+ // Keep the current colors; a later edit may retry the failed load.
151
+ } finally {
152
+ this.running = false;
153
+ if (this.due) {
154
+ void this.flush();
155
+ }
156
+ }
157
+ }
158
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
+ export { MAX_REPLACEMENT_PAGE, MAX_REPLACEMENT_CHARACTERS } from './search-protocol.ts';
1
2
  export { AGENT_COLORS } from './attribution.ts';
3
+ export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from '@adecore/editor-core';
2
4
  export { createSmartEditorEngine } from './engine.ts';
3
5
  export { DEFAULT_SMART_KEYS } from './smart-keys.ts';
4
6
  export { shikiScopeColors, shikiTokenizers } from './shiki.ts';
@@ -28,18 +30,24 @@ export type {
28
30
  EditorFindState,
29
31
  EditorReplaceOptions,
30
32
  EditorGutterAction,
33
+ EditorGutterIcon,
31
34
  EditorGutterMarker,
35
+ EditorReplacement,
32
36
  EditorIndentation,
33
37
  EditorContentChange,
34
38
  EditorHighlight,
35
39
  EditorHighlightKind,
36
40
  EditorHover,
37
41
  EditorGhostText,
42
+ EditorInlayClick,
43
+ EditorInlayClickHandler,
38
44
  EditorInlayHint,
45
+ EditorInlayTarget,
39
46
  EditorKeyHandler,
40
47
  EditorLineAction,
41
48
  EditorLineHighlight,
42
49
  EditorMarker,
50
+ EditorInputPopup,
43
51
  EditorMessages,
44
52
  EditorMarkerSeverity,
45
53
  EditorReveal,
@@ -49,6 +57,7 @@ export type {
49
57
  EditorPosition,
50
58
  EditorRemoteCursor,
51
59
  EditorRange,
60
+ EditorApplyEditsOptions,
52
61
  EditorTextChange,
53
62
  EditorTrackedRange,
54
63
  EditorRunCommand,
@@ -59,6 +68,7 @@ export type {
59
68
  FoldRole,
60
69
  KeyChord,
61
70
  LineToken,
71
+ LineTokenKind,
62
72
  ScopeColorSource,
63
73
  ScopeColors,
64
74
  ScopeStyle,
@@ -105,6 +105,8 @@ export const KEYMAP = {
105
105
  goToImplementation: bind('Mod+Alt+B'),
106
106
  peekDefinition: bind('Alt+Space', 'Ctrl+Shift+I'),
107
107
  peekReferences: bind('Alt+F7'),
108
+ callHierarchy: bind('Ctrl+Alt+H'),
109
+ toggleRenderedDocs: bind('Ctrl+Alt+Q'),
108
110
  historyBack: bind('Mod+[', 'Ctrl+Alt+ArrowLeft'),
109
111
  historyForward: bind('Mod+]', 'Ctrl+Alt+ArrowRight'),
110
112
  recentLocations: bind('Mod+Shift+E'),
@@ -120,7 +122,10 @@ export const KEYMAP = {
120
122
 
121
123
  export type KeymapId = keyof typeof KEYMAP;
122
124
 
123
- export type Keymap = Readonly<Record<KeymapId, KeyBinding>>;
125
+ /* Commands added after a host may have written a full keymap of its own; a keymap without one falls back to the built-in chord. */
126
+ export type OptionalKeymapId = 'toggleRenderedDocs';
127
+
128
+ export type Keymap = Readonly<Record<Exclude<KeymapId, OptionalKeymapId>, KeyBinding> & Partial<Record<OptionalKeymapId, KeyBinding>>>;
124
129
  export type KeymapOverrides = Partial<Record<KeymapId, Partial<KeyBinding>>>;
125
130
 
126
131
  export const KEYMAP_IDS = Object.keys(KEYMAP) as KeymapId[];
@@ -131,7 +136,7 @@ export function resolveKeymap(overrides: KeymapOverrides = {}): Keymap {
131
136
 
132
137
  /* The chord of a command on a platform, or null when the platform has none. */
133
138
  export function chordOf(id: KeymapId, apple: boolean, keymap: Keymap = KEYMAP): string | null {
134
- return keymap[id][apple ? 'mac' : 'other'];
139
+ return (keymap[id] ?? KEYMAP[id])[apple ? 'mac' : 'other'];
135
140
  }
136
141
 
137
142
  const MODIFIERS = new Map<string, 'mod' | 'ctrl' | 'meta' | 'alt' | 'shift'>([
@@ -161,3 +166,40 @@ export function parseChord(text: string): KeyChord {
161
166
  }
162
167
  return chord;
163
168
  }
169
+
170
+ const BASE_PUNCTUATION: Readonly<Record<string, string>> = {
171
+ Comma: ',',
172
+ Period: '.',
173
+ Slash: '/',
174
+ Semicolon: ';',
175
+ Quote: "'",
176
+ Backquote: '`',
177
+ Backslash: '\\',
178
+ BracketLeft: '[',
179
+ BracketRight: ']',
180
+ Minus: '-',
181
+ Equal: '='
182
+ };
183
+
184
+ /* 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
+ function baseCharacter(code: string): string | null {
186
+ const letter = /^Key([A-Z])$/.exec(code);
187
+ if (letter !== null) {
188
+ return letter[1].toLowerCase();
189
+ }
190
+ const digit = /^(?:Digit|Numpad)([0-9])$/.exec(code);
191
+ return digit !== null ? digit[1] : (BASE_PUNCTUATION[code] ?? null);
192
+ }
193
+
194
+ /*
195
+ * Whether Ctrl and Alt together type a character instead of pressing a chord. AltGr reports both, on Windows even
196
+ * for the left Ctrl and Alt, and a browser may report AltGraph for Option, so the state is no evidence: the character is.
197
+ * Never on Apple platforms, where Option types with no Ctrl.
198
+ */
199
+ export function typesInsteadOfChord(event: { key: string; code?: string; ctrlKey: boolean; altKey: boolean }, apple: boolean): boolean {
200
+ if (apple || !event.ctrlKey || !event.altKey || event.code === undefined || [...event.key].length !== 1) {
201
+ return false;
202
+ }
203
+ const base = baseCharacter(event.code);
204
+ return base !== null && event.key.toLowerCase() !== base;
205
+ }
package/src/keymap.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { EditorCommand } from '@adecore/editor-core';
2
- import { chordOf, KEYMAP, KEYMAP_IDS, type Keymap, type KeymapId, parseChord } from './keymap-table.ts';
2
+ import { chordOf, KEYMAP, KEYMAP_IDS, type Keymap, type KeymapId, parseChord, typesInsteadOfChord } from './keymap-table.ts';
3
3
  import type { EditorViewCommand, KeyChord } from './types.ts';
4
4
 
5
5
  export interface KeyLike {
@@ -10,6 +10,7 @@ export interface KeyLike {
10
10
  metaKey: boolean;
11
11
  altKey: boolean;
12
12
  shiftKey: boolean;
13
+ getModifierState?(key: string): boolean;
13
14
  }
14
15
 
15
16
  export type MoveKey = 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown' | 'PageUp' | 'PageDown';
@@ -129,6 +130,9 @@ function keyMatches(chordKey: string, event: KeyLike): boolean {
129
130
 
130
131
  /* Whether an event is a chord, with `mod` as Cmd on macOS and Ctrl elsewhere and every modifier the chord does not name up. */
131
132
  export function chordMatches(chord: KeyChord, event: KeyLike, apple: boolean): boolean {
133
+ if (typesInsteadOfChord(event, apple)) {
134
+ return false;
135
+ }
132
136
  const wantsMeta = chord.meta || (chord.mod && apple);
133
137
  const wantsCtrl = chord.ctrl || (chord.mod && !apple);
134
138
  return (
@@ -0,0 +1,33 @@
1
+ import type { DocumentChange } from '@adecore/editor-core';
2
+ import { offsetMapper } from './offsets.ts';
3
+
4
+ export interface LineBounds {
5
+ start: number;
6
+ end: number;
7
+ }
8
+
9
+ export function lineChangeMapper(batches: readonly (readonly DocumentChange[])[]): (bounds: LineBounds) => LineBounds | null {
10
+ const steps = batches.map((changes) => ({ changes, map: offsetMapper(changes) }));
11
+ return (bounds) => {
12
+ let { start, end } = bounds;
13
+ for (const { changes, map } of steps) {
14
+ let first = 0;
15
+ let last = changes.length;
16
+ while (first < last) {
17
+ const middle = (first + last) >>> 1;
18
+ if (changes[middle]!.to < start) {
19
+ first = middle + 1;
20
+ } else {
21
+ last = middle;
22
+ }
23
+ }
24
+ // Touching either boundary can merge lines or change a CRLF ending.
25
+ if (changes[first] !== undefined && changes[first]!.from <= end) {
26
+ return null;
27
+ }
28
+ start = map(start);
29
+ end = map(end);
30
+ }
31
+ return { start, end };
32
+ };
33
+ }