vzcode 0.82.0 → 0.83.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/assets/{worker-1ri6mpan.js → index-CI1sbUJw.js} +63 -63
- package/dist/assets/{index-BKQfM_WG.js → index-CnHJy69d.js} +3 -3
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/client/App/index.tsx +1 -1
- package/src/client/CodeEditor/widgets.ts +1 -5
- package/src/client/useTypeScript/worker/constants.ts +73 -0
- package/src/client/useTypeScript/worker/getTSFileName.ts +4 -0
- package/src/client/useTypeScript/worker/handleMessageAutocompleteRequest.ts +63 -0
- package/src/client/useTypeScript/worker/handleMessageLintRequest.ts +84 -0
- package/src/client/useTypeScript/worker/handleMessageTranspileRequest.ts +20 -0
- package/src/client/useTypeScript/worker/handleMessageUpdateContent.ts +38 -0
- package/src/client/useTypeScript/worker/index.ts +121 -0
- package/src/client/useTypeScript/worker/isTS.ts +3 -0
- package/src/client/useTypeScript/{requestTypes.ts → worker/requestTypes.ts} +0 -1
- package/src/client/CodeEditor/codemirror-interact.ts +0 -409
- package/src/client/useTypeScript/worker.ts +0 -335
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as tsvfs from '@typescript/vfs';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { File, Files, VZCodeContent } from '../../../types';
|
|
4
|
+
import {
|
|
5
|
+
AutocompleteRequest,
|
|
6
|
+
AutocompleteResponse,
|
|
7
|
+
LinterRequest,
|
|
8
|
+
LinterResponse,
|
|
9
|
+
} from './requestTypes';
|
|
10
|
+
import {
|
|
11
|
+
LINT_ERROR_CODE_CANNOT_FIND_NAME,
|
|
12
|
+
compilerOptions,
|
|
13
|
+
excludedErrorCodes,
|
|
14
|
+
} from './constants';
|
|
15
|
+
import { handleMessageUpdateContent } from './handleMessageUpdateContent';
|
|
16
|
+
import { handleMessageAutocompleteRequest } from './handleMessageAutocompleteRequest';
|
|
17
|
+
import { handleMessageLintRequest } from './handleMessageLintRequest';
|
|
18
|
+
import { handleMessageTranspileRequest } from './handleMessageTranspileRequest';
|
|
19
|
+
|
|
20
|
+
let env: tsvfs.VirtualTypeScriptEnvironment = null;
|
|
21
|
+
|
|
22
|
+
const debug = false;
|
|
23
|
+
|
|
24
|
+
// This is a place for things we only do _once_.
|
|
25
|
+
const initializeFileSystem = async () => {
|
|
26
|
+
if (debug) {
|
|
27
|
+
console.log('initializeFileSystem');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// `true` breaks in a Web Worker because
|
|
31
|
+
// this uses `localStorage` under the hood,
|
|
32
|
+
// which is not available in a Web Worker.
|
|
33
|
+
const cache = false;
|
|
34
|
+
|
|
35
|
+
const fsMap = await tsvfs.createDefaultMapFromCDN(
|
|
36
|
+
compilerOptions,
|
|
37
|
+
ts.version,
|
|
38
|
+
cache,
|
|
39
|
+
ts,
|
|
40
|
+
);
|
|
41
|
+
const sys: ts.System = tsvfs.createSystem(fsMap);
|
|
42
|
+
|
|
43
|
+
// We'll add files to this later.
|
|
44
|
+
const rootFiles = [];
|
|
45
|
+
|
|
46
|
+
env = tsvfs.createVirtualTypeScriptEnvironment(
|
|
47
|
+
sys,
|
|
48
|
+
rootFiles,
|
|
49
|
+
ts,
|
|
50
|
+
compilerOptions,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (debug) {
|
|
54
|
+
console.log('initializeFileSystem done');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const setFile = (tsFileName: string, text: string) => {
|
|
59
|
+
const existingFile = env.getSourceFile(tsFileName);
|
|
60
|
+
if (existingFile === undefined) {
|
|
61
|
+
env.createFile(tsFileName, text);
|
|
62
|
+
} else {
|
|
63
|
+
env.updateFile(tsFileName, text);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// This is a promise that resolves when the file system is initialized.
|
|
68
|
+
let fileSystemInitializationPromise = null;
|
|
69
|
+
async function ensureFileSystemInitialized() {
|
|
70
|
+
if (!fileSystemInitializationPromise) {
|
|
71
|
+
fileSystemInitializationPromise =
|
|
72
|
+
initializeFileSystem();
|
|
73
|
+
}
|
|
74
|
+
await fileSystemInitializationPromise;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onmessage = async ({ data }) => {
|
|
78
|
+
if (debug) {
|
|
79
|
+
console.log('message received');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Ensure the file system is initialized.
|
|
83
|
+
await ensureFileSystemInitialized();
|
|
84
|
+
|
|
85
|
+
// Sanity check - should never happen.
|
|
86
|
+
if (env === null) {
|
|
87
|
+
throw new Error('File system not initialized');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
switch (data.event) {
|
|
91
|
+
case 'update-content':
|
|
92
|
+
await handleMessageUpdateContent({
|
|
93
|
+
debug,
|
|
94
|
+
data,
|
|
95
|
+
setFile,
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case 'autocomplete-request':
|
|
100
|
+
await handleMessageAutocompleteRequest({
|
|
101
|
+
debug,
|
|
102
|
+
data,
|
|
103
|
+
env,
|
|
104
|
+
setFile,
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case 'lint-request':
|
|
109
|
+
await handleMessageLintRequest({
|
|
110
|
+
debug,
|
|
111
|
+
data,
|
|
112
|
+
env,
|
|
113
|
+
setFile,
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'transpile-request':
|
|
118
|
+
await handleMessageTranspileRequest({ data });
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
// From https://github.com/replit/codemirror-interact/blob/master/src/interact.ts
|
|
2
|
-
// as of March 13, 2024
|
|
3
|
-
// Copied into this repo so that we can leverage the changes in PR
|
|
4
|
-
// https://github.com/replit/codemirror-interact/pull/19
|
|
5
|
-
// TODO move back to using the npm package once the PR is merged
|
|
6
|
-
|
|
7
|
-
// TODO: custom style
|
|
8
|
-
// TODO: custom state for each rule?
|
|
9
|
-
import {
|
|
10
|
-
EditorView,
|
|
11
|
-
ViewPlugin,
|
|
12
|
-
PluginValue,
|
|
13
|
-
Decoration,
|
|
14
|
-
} from '@codemirror/view';
|
|
15
|
-
import {
|
|
16
|
-
StateEffect,
|
|
17
|
-
StateField,
|
|
18
|
-
Facet,
|
|
19
|
-
MapMode,
|
|
20
|
-
} from '@codemirror/state';
|
|
21
|
-
|
|
22
|
-
interface Target {
|
|
23
|
-
pos: number;
|
|
24
|
-
text: string;
|
|
25
|
-
rule: InteractRule;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface InteractRule {
|
|
29
|
-
regexp: RegExp;
|
|
30
|
-
cursor?: string;
|
|
31
|
-
style?: any;
|
|
32
|
-
className?: string;
|
|
33
|
-
onClick?: (
|
|
34
|
-
text: string,
|
|
35
|
-
setText: (t: string) => void,
|
|
36
|
-
e: MouseEvent,
|
|
37
|
-
) => void;
|
|
38
|
-
onDrag?: (
|
|
39
|
-
text: string,
|
|
40
|
-
setText: (t: string) => void,
|
|
41
|
-
e: MouseEvent,
|
|
42
|
-
) => void;
|
|
43
|
-
onDragStart?: (
|
|
44
|
-
text: string,
|
|
45
|
-
setText: (t: string) => void,
|
|
46
|
-
e: MouseEvent,
|
|
47
|
-
) => void;
|
|
48
|
-
onDragEnd?: (
|
|
49
|
-
text: string,
|
|
50
|
-
setText: (t: string) => void,
|
|
51
|
-
) => void;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const interactField = StateField.define<Target | null>({
|
|
55
|
-
create: () => null,
|
|
56
|
-
update: (value, tr) => {
|
|
57
|
-
for (const e of tr.effects) {
|
|
58
|
-
if (e.is(setInteract)) {
|
|
59
|
-
return e.value;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!value) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!tr.changes.empty) {
|
|
68
|
-
const newPos = tr.changes.mapPos(
|
|
69
|
-
value.pos,
|
|
70
|
-
-1,
|
|
71
|
-
MapMode.TrackDel,
|
|
72
|
-
);
|
|
73
|
-
const newEnd = tr.changes.mapPos(
|
|
74
|
-
value.pos + value.text.length,
|
|
75
|
-
-1,
|
|
76
|
-
MapMode.TrackDel,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
if (newPos === null || newEnd === null) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// if the text doesn't match anymore, we'll just return null
|
|
84
|
-
// rather than checking if the rule matches again
|
|
85
|
-
if (
|
|
86
|
-
tr.newDoc.sliceString(newPos, newEnd) !== value.text
|
|
87
|
-
) {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return { ...value, pos: newPos };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return value;
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
provide: (field) => [
|
|
98
|
-
EditorView.decorations.from(field, (target) => {
|
|
99
|
-
if (!target) {
|
|
100
|
-
return Decoration.none;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const from = target.pos;
|
|
104
|
-
const to = target.pos + target.text.length;
|
|
105
|
-
const className = target.rule.className;
|
|
106
|
-
|
|
107
|
-
return Decoration.set(
|
|
108
|
-
mark({ className }).range(from, to),
|
|
109
|
-
);
|
|
110
|
-
}),
|
|
111
|
-
EditorView.contentAttributes.from(field, (target) => {
|
|
112
|
-
if (!target || !target.rule.cursor) {
|
|
113
|
-
return { style: '' };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return { style: `cursor: ${target.rule.cursor}` };
|
|
117
|
-
}),
|
|
118
|
-
],
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const setInteract = StateEffect.define<Target | null>();
|
|
122
|
-
|
|
123
|
-
const mark = (e: { className?: string }) =>
|
|
124
|
-
Decoration.mark({
|
|
125
|
-
class: `cm-interact ${e?.className ?? ''}`,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const interactTheme = EditorView.theme({
|
|
129
|
-
'.cm-interact': {
|
|
130
|
-
background: 'rgba(128, 128, 255, 0.2)',
|
|
131
|
-
borderRadius: '4px',
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* A rule that defines a type of value and its interaction.
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* ```
|
|
140
|
-
* // a number dragger
|
|
141
|
-
* interactRule.of({
|
|
142
|
-
* // the regexp matching the value
|
|
143
|
-
* regexp: /-?\b\d+\.?\d*\b/g,
|
|
144
|
-
* // set cursor to 'ew-resize'on hover
|
|
145
|
-
* cursor: 'ew-resize'
|
|
146
|
-
* // change number value based on mouse X movement on drag
|
|
147
|
-
* onDrag: (text, setText, e) => {
|
|
148
|
-
* const newVal = Number(text) + e.movementX;
|
|
149
|
-
* if (isNaN(newVal)) return;
|
|
150
|
-
* setText(newVal.toString());
|
|
151
|
-
* },
|
|
152
|
-
* })
|
|
153
|
-
* ```
|
|
154
|
-
*/
|
|
155
|
-
export const interactRule = Facet.define<InteractRule>();
|
|
156
|
-
|
|
157
|
-
export const interactModKey = Facet.define<ModKey, ModKey>({
|
|
158
|
-
combine: (values) => values[values.length - 1],
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
interface ViewState extends PluginValue {
|
|
162
|
-
target: Target | null;
|
|
163
|
-
dragging: boolean;
|
|
164
|
-
mouseX: number;
|
|
165
|
-
mouseY: number;
|
|
166
|
-
getMatch(): Target | null;
|
|
167
|
-
updateText(target: Target): (text: string) => void;
|
|
168
|
-
setTarget(target: Target | null): void;
|
|
169
|
-
isModKeyDown(e: KeyboardEvent | MouseEvent): boolean;
|
|
170
|
-
startDrag(e: MouseEvent): void;
|
|
171
|
-
endDrag(): void;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const interactViewPlugin = ViewPlugin.define<ViewState>(
|
|
175
|
-
(view) => ({
|
|
176
|
-
target: null,
|
|
177
|
-
dragging: false,
|
|
178
|
-
mouseX: 0,
|
|
179
|
-
mouseY: 0,
|
|
180
|
-
|
|
181
|
-
// Get current match under cursor from all rules
|
|
182
|
-
getMatch() {
|
|
183
|
-
const rules = view.state.facet(interactRule);
|
|
184
|
-
const pos = view.posAtCoords({
|
|
185
|
-
x: this.mouseX,
|
|
186
|
-
y: this.mouseY,
|
|
187
|
-
});
|
|
188
|
-
if (!pos) return null;
|
|
189
|
-
const line = view.state.doc.lineAt(pos);
|
|
190
|
-
const lpos = pos - line.from;
|
|
191
|
-
let match = null;
|
|
192
|
-
|
|
193
|
-
for (const rule of rules) {
|
|
194
|
-
// @ts-ignore
|
|
195
|
-
for (const m of line.text.matchAll(rule.regexp)) {
|
|
196
|
-
if (m.index === undefined) continue;
|
|
197
|
-
const text = m[0];
|
|
198
|
-
if (!text) continue;
|
|
199
|
-
const start = m.index;
|
|
200
|
-
const end = m.index + text.length;
|
|
201
|
-
if (lpos < start || lpos > end) continue;
|
|
202
|
-
// If there are overlap matches from different rules, use the smaller one
|
|
203
|
-
if (!match || text.length < match.text.length) {
|
|
204
|
-
match = {
|
|
205
|
-
rule: rule,
|
|
206
|
-
pos: line.from + start,
|
|
207
|
-
text: text,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return match;
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
updateText(target) {
|
|
217
|
-
return (text) => {
|
|
218
|
-
view.dispatch({
|
|
219
|
-
effects: setInteract.of({ ...target, text }),
|
|
220
|
-
changes: {
|
|
221
|
-
from: target.pos,
|
|
222
|
-
to: target.pos + target.text.length,
|
|
223
|
-
insert: text,
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
};
|
|
227
|
-
},
|
|
228
|
-
|
|
229
|
-
setTarget(target) {
|
|
230
|
-
this.target = target;
|
|
231
|
-
view.dispatch({ effects: setInteract.of(target) });
|
|
232
|
-
},
|
|
233
|
-
|
|
234
|
-
isModKeyDown(e) {
|
|
235
|
-
const modkey = view.state.facet(interactModKey);
|
|
236
|
-
|
|
237
|
-
const isMac =
|
|
238
|
-
Boolean(window.navigator) &&
|
|
239
|
-
window.navigator.userAgent.includes('Macintosh');
|
|
240
|
-
|
|
241
|
-
switch (modkey) {
|
|
242
|
-
case 'alt':
|
|
243
|
-
return e.altKey;
|
|
244
|
-
case 'shift':
|
|
245
|
-
return e.shiftKey;
|
|
246
|
-
case 'ctrl':
|
|
247
|
-
return e.ctrlKey;
|
|
248
|
-
case 'meta':
|
|
249
|
-
return e.metaKey;
|
|
250
|
-
case 'mod':
|
|
251
|
-
return isMac ? e.metaKey : e.ctrlKey;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
throw new Error(`Invalid mod key: ${modkey}`);
|
|
255
|
-
},
|
|
256
|
-
|
|
257
|
-
update(update) {
|
|
258
|
-
const target = update.state.field(
|
|
259
|
-
interactField,
|
|
260
|
-
false,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
// the field isn't mounted
|
|
264
|
-
if (target === undefined) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (this.target !== target) {
|
|
269
|
-
this.target = target;
|
|
270
|
-
if (target === null) {
|
|
271
|
-
this.endDrag();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
|
|
276
|
-
startDrag(e: MouseEvent) {
|
|
277
|
-
if (this.dragging) return;
|
|
278
|
-
if (!this.target) return;
|
|
279
|
-
this.dragging = true;
|
|
280
|
-
if (!this.target.rule.onDragStart) return;
|
|
281
|
-
this.target.rule.onDragStart(
|
|
282
|
-
this.target.text,
|
|
283
|
-
this.updateText(this.target),
|
|
284
|
-
e,
|
|
285
|
-
);
|
|
286
|
-
},
|
|
287
|
-
|
|
288
|
-
endDrag() {
|
|
289
|
-
if (!this.dragging) return;
|
|
290
|
-
this.dragging = false;
|
|
291
|
-
if (!this.target?.rule.onDragEnd) return;
|
|
292
|
-
this.target.rule.onDragEnd(
|
|
293
|
-
this.target.text,
|
|
294
|
-
this.updateText(this.target),
|
|
295
|
-
);
|
|
296
|
-
},
|
|
297
|
-
}),
|
|
298
|
-
{
|
|
299
|
-
eventHandlers: {
|
|
300
|
-
mousedown(e, _view) {
|
|
301
|
-
if (!this.isModKeyDown(e)) return;
|
|
302
|
-
if (!this.target) return;
|
|
303
|
-
|
|
304
|
-
e.preventDefault();
|
|
305
|
-
|
|
306
|
-
// if (this.target.rule.onClick) {
|
|
307
|
-
// this.target.rule.onClick(
|
|
308
|
-
// this.target.text,
|
|
309
|
-
// this.updateText(this.target),
|
|
310
|
-
// e,
|
|
311
|
-
// );
|
|
312
|
-
// }
|
|
313
|
-
|
|
314
|
-
if (this.target.rule.onClick) {
|
|
315
|
-
this.target.rule.onClick(
|
|
316
|
-
this.target.text,
|
|
317
|
-
(text) => {
|
|
318
|
-
this.target &&
|
|
319
|
-
this.updateText(this.target)(text);
|
|
320
|
-
},
|
|
321
|
-
e,
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (this.target.rule.onDrag) {
|
|
326
|
-
this.startDrag(e);
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
|
|
330
|
-
mousemove(e, _view) {
|
|
331
|
-
this.mouseX = e.clientX;
|
|
332
|
-
this.mouseY = e.clientY;
|
|
333
|
-
|
|
334
|
-
if (!this.isModKeyDown(e)) {
|
|
335
|
-
if (this.target) {
|
|
336
|
-
this.setTarget(null);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (this.target && this.dragging) {
|
|
343
|
-
if (this.target.rule.onDrag) {
|
|
344
|
-
this.target.rule.onDrag(
|
|
345
|
-
this.target.text,
|
|
346
|
-
this.updateText(this.target),
|
|
347
|
-
e,
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
} else {
|
|
351
|
-
this.setTarget(this.getMatch());
|
|
352
|
-
}
|
|
353
|
-
},
|
|
354
|
-
|
|
355
|
-
mouseup(e, _view) {
|
|
356
|
-
this.endDrag();
|
|
357
|
-
|
|
358
|
-
if (this.target && !this.isModKeyDown(e)) {
|
|
359
|
-
this.setTarget(null);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (this.isModKeyDown(e)) {
|
|
363
|
-
this.setTarget(this.getMatch());
|
|
364
|
-
}
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
mouseleave(e, _view) {
|
|
368
|
-
this.endDrag();
|
|
369
|
-
if (this.target) {
|
|
370
|
-
this.setTarget(null);
|
|
371
|
-
}
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
// TODO: fix these keybindings
|
|
375
|
-
// these currently don't do anything because CodeMirror's keybinding
|
|
376
|
-
// system prevents these events from firing.
|
|
377
|
-
|
|
378
|
-
keydown(e, _view) {
|
|
379
|
-
if (!this.target && this.isModKeyDown(e)) {
|
|
380
|
-
this.setTarget(this.getMatch());
|
|
381
|
-
}
|
|
382
|
-
},
|
|
383
|
-
|
|
384
|
-
keyup(e, _view) {
|
|
385
|
-
if (this.target && !this.isModKeyDown(e)) {
|
|
386
|
-
this.endDrag();
|
|
387
|
-
this.setTarget(null);
|
|
388
|
-
}
|
|
389
|
-
},
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
type ModKey = 'alt' | 'shift' | 'meta' | 'ctrl' | 'mod';
|
|
395
|
-
|
|
396
|
-
interface InteractConfig {
|
|
397
|
-
rules?: InteractRule[];
|
|
398
|
-
key?: ModKey;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const interact = (cfg: InteractConfig = {}) => [
|
|
402
|
-
interactField,
|
|
403
|
-
interactTheme,
|
|
404
|
-
interactViewPlugin,
|
|
405
|
-
interactModKey.of(cfg.key ?? 'alt'),
|
|
406
|
-
(cfg.rules ?? []).map((r) => interactRule.of(r)),
|
|
407
|
-
];
|
|
408
|
-
|
|
409
|
-
export default interact;
|