vzcode 1.2.0 → 1.4.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.
@@ -1,6 +1,154 @@
1
- import { useContext, useEffect } from 'react';
1
+ import { useEffect } from 'react';
2
2
  import { shouldTriggerRun } from './shouldTriggerRun';
3
- import { VZCodeContext } from './VZCodeContext';
3
+ import { syntaxTree } from '@codemirror/language';
4
+ import { SyntaxNode, SyntaxNodeRef } from '@lezer/common';
5
+ import { EditorView } from '@codemirror/view';
6
+ import { EditorState } from '@codemirror/state';
7
+
8
+ /*
9
+ The following is a helpful resource for the following Code Mirror Syntax Tree methods below
10
+ https://lezer.codemirror.net/docs/ref/#common
11
+ */
12
+
13
+ // Store the element in the current editor DOM to highlight, indicating a potential jump to definition is possible
14
+ let activeJumpingElement: HTMLSpanElement = null;
15
+
16
+ // Store the syntax node representing the destination within the syntax tree
17
+ let definingNode: SyntaxNode = null;
18
+
19
+ // Example nesting types for the specific language to find level in the syntax tree
20
+ const nestingTypes = new Set<string>([
21
+ 'FunctionDeclaration',
22
+ 'ClassDeclaration',
23
+ 'MethodDeclaration',
24
+ 'Block',
25
+ 'IfStatement',
26
+ 'ForStatement',
27
+ 'WhileStatement',
28
+ 'SwitchStatement',
29
+ 'TryStatement',
30
+ 'CatchClause',
31
+ 'WithStatement',
32
+ 'ArrowFunction',
33
+ 'ImportGroup',
34
+ ]);
35
+
36
+ // Example declaration types for the specific language to find definitions in the syntax tree
37
+ const declarationTypes = new Set<string>([
38
+ 'VariableDeclaration',
39
+ 'FunctionDeclaration',
40
+ 'ClassDeclaration',
41
+ 'MethodDeclaration',
42
+ 'PropertyDeclaration',
43
+ 'ImportDeclaration',
44
+ 'ExportDeclaration',
45
+ 'CallExpression',
46
+ 'ArrayPattern',
47
+ 'ObjectPattern',
48
+ 'PatternProperty',
49
+ 'ArgList',
50
+ 'TypeArgList',
51
+ 'ParamList',
52
+ 'ForOfSpec',
53
+ ]);
54
+
55
+ function getIdentifierContext(
56
+ identifier: SyntaxNode,
57
+ ): number {
58
+ let current: SyntaxNode = identifier;
59
+ let levels: number = 0;
60
+
61
+ // Traverse up the tree to find the total depth, only counting valid nesting types
62
+ while (current && current.type) {
63
+ const parentType: string = current.type.name;
64
+
65
+ if (nestingTypes.has(parentType)) {
66
+ levels++;
67
+ }
68
+
69
+ current = current.parent;
70
+ }
71
+
72
+ return levels;
73
+ }
74
+
75
+ function jumpToDefinition(
76
+ editor: EditorView,
77
+ node: SyntaxNode,
78
+ ): SyntaxNode {
79
+ const state: EditorState = editor.state;
80
+ const definitions: Array<{
81
+ identifier: SyntaxNode;
82
+ context: number;
83
+ }> = [];
84
+
85
+ // From an identifier in the syntax tree, fetch the name and context to find closest defining syntax node
86
+ const identifier: SyntaxNode = node;
87
+ const identifierName: string = state.doc.sliceString(
88
+ identifier.from,
89
+ identifier.to,
90
+ );
91
+ const context: number = getIdentifierContext(identifier);
92
+
93
+ if (identifier) {
94
+ syntaxTree(state).iterate({
95
+ enter(tree: SyntaxNodeRef) {
96
+ // Traverse syntax tree to find positions of respective identifier definitions within context
97
+ if (
98
+ declarationTypes.has(tree.name) ||
99
+ nestingTypes.has(tree.name)
100
+ ) {
101
+ const parent: SyntaxNode = tree.node;
102
+
103
+ // Fetch a host of potential identifiers in an attempt to find the defining syntax node
104
+ const children: Array<SyntaxNode> = [
105
+ ...parent.getChildren('VariableDefinition'),
106
+ ...parent.getChildren('PropertyDefinition'),
107
+ ...parent.getChildren('PropertyName'),
108
+ ...parent.getChildren('Identifier'),
109
+ ];
110
+
111
+ children.forEach((child: SyntaxNode) => {
112
+ const name: string = state.doc.sliceString(
113
+ child.from,
114
+ child.to,
115
+ );
116
+
117
+ // Ensure no jump to self by comparing syntax node id's
118
+ if (
119
+ name === identifierName &&
120
+ child.type.id !== identifier.type.id
121
+ ) {
122
+ definitions.push({
123
+ identifier: child,
124
+ context: getIdentifierContext(child),
125
+ });
126
+ }
127
+ });
128
+ }
129
+ },
130
+ });
131
+
132
+ if (definitions.length > 0) {
133
+ // Sort definitions by their context in the syntax tree
134
+ definitions.sort((a, b) => a.context - b.context);
135
+
136
+ let closestDefinition: SyntaxNode =
137
+ definitions[0].identifier;
138
+
139
+ for (let i = definitions.length - 1; i >= 0; i--) {
140
+ if (definitions[i].context <= context) {
141
+ closestDefinition = definitions[i].identifier;
142
+ break;
143
+ }
144
+ }
145
+
146
+ return closestDefinition;
147
+ }
148
+
149
+ return null;
150
+ }
151
+ }
4
152
 
5
153
  // This module implements the keyboard shortcuts
6
154
  // for the VZCode editor.
@@ -10,6 +158,7 @@ import { VZCodeContext } from './VZCodeContext';
10
158
  // * Alt-PageUp: Change the active tab to the previous one
11
159
  // * Alt-PageDown: Change the active tab to the next one
12
160
  // * Ctrl-s or Shift-Enter: Run the code and format it with Prettier
161
+ // * Ctrl-Click: Jump to closest definition for a potential identifier
13
162
  export const useKeyboardShortcuts = ({
14
163
  closeTabs,
15
164
  activeFileId,
@@ -18,6 +167,9 @@ export const useKeyboardShortcuts = ({
18
167
  setActiveFileRight,
19
168
  runPrettierRef,
20
169
  runCodeRef,
170
+ sidebarRef,
171
+ editorCache,
172
+ codeEditorRef,
21
173
  }) => {
22
174
  useEffect(() => {
23
175
  const handleKeyPress = (event: KeyboardEvent) => {
@@ -32,12 +184,21 @@ export const useKeyboardShortcuts = ({
32
184
 
33
185
  // Run the code
34
186
  const runCode = runCodeRef.current;
187
+
35
188
  if (runCode !== null) {
36
189
  runCode();
37
190
  }
38
191
  return;
39
192
  }
40
193
 
194
+ if (event.ctrlKey === true) {
195
+ // On holding CTRL key, search for a potential definition jump using mouse location
196
+ document.addEventListener(
197
+ 'mouseover',
198
+ handleMouseOver,
199
+ );
200
+ }
201
+
41
202
  if (event.altKey === true) {
42
203
  // Alt-w: Close the current tab
43
204
  if (event.key === 'w') {
@@ -65,15 +226,149 @@ export const useKeyboardShortcuts = ({
65
226
  setActiveFileRight();
66
227
  return;
67
228
  }
229
+
230
+ if (event.key === '1') {
231
+ if (sidebarRef.current) {
232
+ sidebarRef.current.focus();
233
+ }
234
+ }
235
+
236
+ if (event.key === '2') {
237
+ if (codeEditorRef.current) {
238
+ codeEditorRef.current.focus();
239
+ }
240
+ }
241
+ }
242
+ };
243
+
244
+ const resetActiveJumpingElement = (): void => {
245
+ if (activeJumpingElement) {
246
+ activeJumpingElement.style.cursor = 'initial';
247
+ activeJumpingElement.style.textDecoration = 'none';
248
+ activeJumpingElement = definingNode = null;
249
+ }
250
+
251
+ document.removeEventListener(
252
+ 'mouseover',
253
+ handleMouseOver,
254
+ );
255
+ document.removeEventListener(
256
+ 'mousedown',
257
+ jumpToDefinitionHandler,
258
+ );
259
+ };
260
+
261
+ const jumpToDefinitionHandler = (
262
+ event: MouseEvent,
263
+ ): void => {
264
+ // Ensure the current destination node is defined and current cursor position matches highlighted element
265
+ if (
266
+ !definingNode ||
267
+ (event.target as HTMLSpanElement) !==
268
+ activeJumpingElement
269
+ ) {
270
+ return;
271
+ }
272
+
273
+ // Move current cursor and center view in the editor to destination node
274
+ const editor: EditorView =
275
+ editorCache.get(activeFileId).editor;
276
+ const closestDefinition: SyntaxNode = definingNode;
277
+
278
+ editor.dispatch({
279
+ selection: {
280
+ anchor: closestDefinition.from,
281
+ head: closestDefinition.to,
282
+ },
283
+ scrollIntoView: true,
284
+ effects: EditorView.scrollIntoView(
285
+ closestDefinition.from,
286
+ {
287
+ y: 'center',
288
+ },
289
+ ),
290
+ });
291
+
292
+ resetActiveJumpingElement();
293
+ };
294
+
295
+ const handleKeyRelease = (event: KeyboardEvent) => {
296
+ // On releasing CTRL key, reset all active definition jumping elements and listeners
297
+ if (!event.ctrlKey) {
298
+ resetActiveJumpingElement();
299
+ }
300
+ };
301
+
302
+ const handleMouseOver = (event: MouseEvent) => {
303
+ const editor: EditorView =
304
+ editorCache.get(activeFileId).editor;
305
+ const tree = syntaxTree(editor.state);
306
+ const element = event.target as HTMLSpanElement;
307
+
308
+ // Ensure the identifier element can be found and is within the current editor DOM
309
+ if (
310
+ element == null ||
311
+ !editor.dom.contains(element)
312
+ ) {
313
+ return;
314
+ }
315
+
316
+ const position: number = editor.posAtDOM(element);
317
+ const identifier: SyntaxNode = tree.resolveInner(
318
+ position,
319
+ 1,
320
+ );
321
+
322
+ // All valid identifiers must be span elements to find a potential defining jump in editor
323
+ if (
324
+ identifier &&
325
+ element instanceof HTMLSpanElement
326
+ ) {
327
+ const potentialJump: SyntaxNode = jumpToDefinition(
328
+ editor,
329
+ identifier,
330
+ );
331
+
332
+ // Only allowing to jump to other definition nodes
333
+ if (
334
+ potentialJump &&
335
+ identifier.type.id !== potentialJump.type.id
336
+ ) {
337
+ if (activeJumpingElement) {
338
+ activeJumpingElement.style.cursor = 'initial';
339
+ activeJumpingElement.style.textDecoration =
340
+ 'none';
341
+ }
342
+
343
+ activeJumpingElement = element;
344
+ definingNode = potentialJump;
345
+
346
+ activeJumpingElement.style.cursor = 'pointer';
347
+ activeJumpingElement.style.textDecoration =
348
+ 'underline';
349
+
350
+ // CTRL + Click: Jump to relative definition, which is removed on CTRL key release
351
+ document.addEventListener(
352
+ 'mousedown',
353
+ jumpToDefinitionHandler,
354
+ { once: true },
355
+ );
356
+ }
68
357
  }
69
358
  };
70
359
 
71
360
  document.addEventListener('keydown', handleKeyPress);
361
+ document.addEventListener('keyup', handleKeyRelease);
362
+
72
363
  return () => {
73
364
  document.removeEventListener(
74
365
  'keydown',
75
366
  handleKeyPress,
76
367
  );
368
+ document.removeEventListener(
369
+ 'keyup',
370
+ handleKeyRelease,
371
+ );
77
372
  };
78
373
  }, [
79
374
  handleOpenCreateFileModal,
@@ -61,6 +61,17 @@ const LINT_ERROR_CODE_ASSIGNABLE_TO_NEVER = 2345;
61
61
  // Cannot find name 'd3'.
62
62
  export const LINT_ERROR_CODE_CANNOT_FIND_NAME = 2304;
63
63
 
64
+ // Type '{}' is missing the following properties from type '{ indx:
65
+ //Ignore specific TypeScript warning on object reassignment
66
+ const LINT_ERROR_CODE_OBJ_REASSINGMENT = 2739;
67
+
68
+ // Type 'any' is not assignable to type 'never'.
69
+ const LINT_ERROR_CODE_ANY_NOT_ASSIGNABLE_TO_NEVER = 2322;
70
+
71
+ // This code is for errors like:
72
+ // "Object is of type 'unknown'."
73
+ const LINT_ERROR_CODE_UNKNOWN = 18046;
74
+
64
75
  export const excludedErrorCodes = new Set([
65
76
  LINT_ERROR_CODE_ANY,
66
77
  LINT_ERROR_CODE_IMPORT,
@@ -70,4 +81,7 @@ export const excludedErrorCodes = new Set([
70
81
  LINT_ERROR_CODE_NON_EXISTENT_PROPERTY,
71
82
  LINT_ERROR_CODE_ITERATED_THROUGH,
72
83
  LINT_ERROR_CODE_ASSIGNABLE_TO_NEVER,
84
+ LINT_ERROR_CODE_OBJ_REASSINGMENT,
85
+ LINT_ERROR_CODE_ANY_NOT_ASSIGNABLE_TO_NEVER,
86
+ LINT_ERROR_CODE_UNKNOWN,
73
87
  ]);
@@ -1,17 +1,6 @@
1
1
  import * as tsvfs from '@typescript/vfs';
2
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';
3
+ import { compilerOptions } from './constants';
15
4
  import { handleMessageUpdateContent } from './handleMessageUpdateContent';
16
5
  import { handleMessageAutocompleteRequest } from './handleMessageAutocompleteRequest';
17
6
  import { handleMessageLintRequest } from './handleMessageLintRequest';
@@ -310,9 +310,12 @@ server.listen(port, async () => {
310
310
  open(url);
311
311
  })();
312
312
  } else {
313
+ // Sets the port to the one specified in the environment
314
+ // variable (for development) or the default port.
315
+ let livePort = process.env.EDITOR_PORT || port;
313
316
  console.log(
314
- `Editor is live at http://localhost:${port}`,
317
+ `Editor is live at http://localhost:${livePort}`,
315
318
  );
316
- open(`http://localhost:${port}`);
319
+ open(`http://localhost:${livePort}`);
317
320
  }
318
321
  });