vzcode 2.3.0 → 2.5.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 (198) hide show
  1. package/dist/assets/{index-DuUjtrt6.js → index-DB93Pffa.js} +84 -84
  2. package/dist/index.html +1 -1
  3. package/package.json +3 -2
  4. package/src/cli.ts +208 -0
  5. package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +42 -0
  6. package/src/client/AIAssist/AIAssistWidget/index.tsx +135 -0
  7. package/src/client/AIAssist/AIAssistWidget/style.scss +47 -0
  8. package/src/client/AIAssist/Spinner.jsx +76 -0
  9. package/src/client/AIAssist/formatFile.ts +27 -0
  10. package/src/client/AIAssist/generateFilesContext.ts +25 -0
  11. package/src/client/AIAssist/index.ts +2 -0
  12. package/src/client/AIAssist/startAIAssist.ts +95 -0
  13. package/src/client/App/index.tsx +152 -0
  14. package/src/client/App/style.scss +82 -0
  15. package/src/client/App/usePending.ts +51 -0
  16. package/src/client/App/useShareDB.ts +161 -0
  17. package/src/client/CodeEditor/Copilot/index.ts +74 -0
  18. package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +12 -0
  19. package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +68 -0
  20. package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +174 -0
  21. package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +79 -0
  22. package/src/client/CodeEditor/InteractiveWidgets/index.ts +44 -0
  23. package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +12 -0
  24. package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +20 -0
  25. package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +143 -0
  26. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +48 -0
  27. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +12 -0
  28. package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +25 -0
  29. package/src/client/CodeEditor/basicSetup.ts +73 -0
  30. package/src/client/CodeEditor/getOrCreateEditor.tsx +615 -0
  31. package/src/client/CodeEditor/index.tsx +202 -0
  32. package/src/client/CodeEditor/json1PresenceBroadcast.ts +41 -0
  33. package/src/client/CodeEditor/json1PresenceDisplay.ts +350 -0
  34. package/src/client/CodeEditor/rainbowBrackets.ts +122 -0
  35. package/src/client/CodeEditor/style.scss +156 -0
  36. package/src/client/CodeEditor/typescriptExtension/worker.ts +32 -0
  37. package/src/client/CodeErrorOverlay/index.tsx +77 -0
  38. package/src/client/CodeErrorOverlay/style.scss +33 -0
  39. package/src/client/Fonts/fonts.ts +8 -0
  40. package/src/client/Icons/AdjustmentSVG.tsx +19 -0
  41. package/src/client/Icons/BugSVG.tsx +20 -0
  42. package/src/client/Icons/CloseSVG.tsx +19 -0
  43. package/src/client/Icons/CssSVG.tsx +16 -0
  44. package/src/client/Icons/DirectoryArrowSVG.tsx +19 -0
  45. package/src/client/Icons/EditSVG.tsx +19 -0
  46. package/src/client/Icons/FileSVG.tsx +19 -0
  47. package/src/client/Icons/FolderSVG.tsx +20 -0
  48. package/src/client/Icons/GearSVG.tsx +16 -0
  49. package/src/client/Icons/HtmlSVG.tsx +16 -0
  50. package/src/client/Icons/JavaScriptSVG.tsx +16 -0
  51. package/src/client/Icons/JsonSVG.tsx +16 -0
  52. package/src/client/Icons/MarkdownSVG.tsx +16 -0
  53. package/src/client/Icons/MicOffSVG.tsx +35 -0
  54. package/src/client/Icons/MicSVG.tsx +25 -0
  55. package/src/client/Icons/NewSVG.tsx +19 -0
  56. package/src/client/Icons/PinSVG.tsx +14 -0
  57. package/src/client/Icons/PlaySVG.tsx +16 -0
  58. package/src/client/Icons/QuestionMarkSVG.tsx +18 -0
  59. package/src/client/Icons/ReactSVG.tsx +16 -0
  60. package/src/client/Icons/SearchSVG.tsx +20 -0
  61. package/src/client/Icons/SparklesSVG.tsx +23 -0
  62. package/src/client/Icons/SplitEditorSVG.tsx +34 -0
  63. package/src/client/Icons/StopSVG.tsx +15 -0
  64. package/src/client/Icons/SvelteSVG.tsx +16 -0
  65. package/src/client/Icons/TrashSVG.tsx +19 -0
  66. package/src/client/Icons/TypeScriptSVG.tsx +16 -0
  67. package/src/client/Icons/index.tsx +26 -0
  68. package/src/client/ImageViewer/index.tsx +91 -0
  69. package/src/client/ImageViewer/style.scss +41 -0
  70. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  71. package/src/client/PresenceNotifications/index.tsx +158 -0
  72. package/src/client/PresenceNotifications/style.scss +17 -0
  73. package/src/client/RunCodeWidget/index.tsx +117 -0
  74. package/src/client/RunCodeWidget/style.scss +24 -0
  75. package/src/client/SplitPaneResizeContext.tsx +159 -0
  76. package/src/client/TabList/Tab.tsx +73 -0
  77. package/src/client/TabList/index.tsx +35 -0
  78. package/src/client/TabList/style.scss +51 -0
  79. package/src/client/VZCodeContext/index.tsx +29 -0
  80. package/src/client/VZCodeContext/types.ts +251 -0
  81. package/src/client/VZCodeContext/useVZCodeState.ts +606 -0
  82. package/src/client/VZKeyboardShortcutsDoc.tsx +148 -0
  83. package/src/client/VZLeft.tsx +28 -0
  84. package/src/client/VZMiddle.tsx +172 -0
  85. package/src/client/VZResizer/index.tsx +128 -0
  86. package/src/client/VZResizer/styles.scss +33 -0
  87. package/src/client/VZRight.tsx +113 -0
  88. package/src/client/VZSettings.tsx +275 -0
  89. package/src/client/VZSidebar/AIChat/ChatInput.tsx +243 -0
  90. package/src/client/VZSidebar/AIChat/DiffView.scss +136 -0
  91. package/src/client/VZSidebar/AIChat/DiffView.tsx +176 -0
  92. package/src/client/VZSidebar/AIChat/Message.tsx +79 -0
  93. package/src/client/VZSidebar/AIChat/MessageList.tsx +190 -0
  94. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +35 -0
  95. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +156 -0
  96. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +19 -0
  97. package/src/client/VZSidebar/AIChat/index.tsx +359 -0
  98. package/src/client/VZSidebar/AIChat/styles.scss +646 -0
  99. package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
  100. package/src/client/VZSidebar/CreateDirModal.tsx +111 -0
  101. package/src/client/VZSidebar/CreateFileModal.tsx +111 -0
  102. package/src/client/VZSidebar/DeleteConfirmationModal.tsx +76 -0
  103. package/src/client/VZSidebar/DirectoryListing.tsx +89 -0
  104. package/src/client/VZSidebar/EmptyState.tsx +11 -0
  105. package/src/client/VZSidebar/FileListing.tsx +91 -0
  106. package/src/client/VZSidebar/FileTypeIcon.tsx +49 -0
  107. package/src/client/VZSidebar/Item.tsx +237 -0
  108. package/src/client/VZSidebar/Listing.tsx +47 -0
  109. package/src/client/VZSidebar/Search.tsx +527 -0
  110. package/src/client/VZSidebar/VisualEditor.tsx +282 -0
  111. package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
  112. package/src/client/VZSidebar/aiCopyPaste.ts +199 -0
  113. package/src/client/VZSidebar/index.tsx +728 -0
  114. package/src/client/VZSidebar/styles.scss +612 -0
  115. package/src/client/VZSidebar/useDragAndDrop.tsx +408 -0
  116. package/src/client/bootstrap.ts +26 -0
  117. package/src/client/buildWorker.ts +3 -0
  118. package/src/client/featureFlags.ts +8 -0
  119. package/src/client/generateRequestId.ts +6 -0
  120. package/src/client/getFileTree.ts +53 -0
  121. package/src/client/index.ts +19 -0
  122. package/src/client/main.jsx +10 -0
  123. package/src/client/presenceColor.ts +8 -0
  124. package/src/client/shouldTriggerRun.ts +31 -0
  125. package/src/client/sortFileTree.ts +21 -0
  126. package/src/client/tabsSearchParameters.test.ts +79 -0
  127. package/src/client/tabsSearchParameters.ts +137 -0
  128. package/src/client/themes/index.ts +7 -0
  129. package/src/client/themes/themes.ts +77 -0
  130. package/src/client/themes/uiwImports.ts +32 -0
  131. package/src/client/themes/useDynamicTheme.ts +29 -0
  132. package/src/client/themes/vizhubTheme/colors.ts +80 -0
  133. package/src/client/themes/vizhubTheme/index.ts +193 -0
  134. package/src/client/useActions.ts +288 -0
  135. package/src/client/useESLint/index.ts +72 -0
  136. package/src/client/useESLint/worker.ts +113 -0
  137. package/src/client/useEditorCache.ts +35 -0
  138. package/src/client/useFileCRUD.ts +228 -0
  139. package/src/client/useKeyboardShortcuts.ts +451 -0
  140. package/src/client/useOpenDirectories.ts +99 -0
  141. package/src/client/usePresenceAutoFollow.ts +85 -0
  142. package/src/client/usePrettier/index.ts +178 -0
  143. package/src/client/usePrettier/worker.ts +109 -0
  144. package/src/client/useRunCode.tsx +15 -0
  145. package/src/client/useRuntimeError.ts +32 -0
  146. package/src/client/useSubmitOperation.ts +15 -0
  147. package/src/client/useURLSync.ts +137 -0
  148. package/src/client/usernameLocalStorage.ts +43 -0
  149. package/src/client/utils/fileExtension.ts +10 -0
  150. package/src/client/utils/isImageFile.ts +12 -0
  151. package/src/client/vzReducer/aiChatReducer.ts +44 -0
  152. package/src/client/vzReducer/closeTabReducer.test.ts +205 -0
  153. package/src/client/vzReducer/closeTabsReducer.ts +96 -0
  154. package/src/client/vzReducer/createInitialState.ts +38 -0
  155. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  156. package/src/client/vzReducer/findPane.ts +19 -0
  157. package/src/client/vzReducer/index.ts +250 -0
  158. package/src/client/vzReducer/openTabReducer.test.ts +127 -0
  159. package/src/client/vzReducer/openTabReducer.ts +74 -0
  160. package/src/client/vzReducer/searchReducer.test.ts +292 -0
  161. package/src/client/vzReducer/searchReducer.ts +181 -0
  162. package/src/client/vzReducer/setActiveFileIdReducer.ts +28 -0
  163. package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +76 -0
  164. package/src/client/vzReducer/setIsDocOpenReducer.ts +9 -0
  165. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  166. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  167. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  168. package/src/client/vzReducer/splitCurrentPaneReducer.ts +66 -0
  169. package/src/client/vzReducer/toggleAutoFollowReducer.ts +12 -0
  170. package/src/client/vzReducer/updatePane.ts +40 -0
  171. package/src/client/vzReducer/updatePresenceIndicatorReducer.ts +30 -0
  172. package/src/client/vzReducer/visualEditorReducer.ts +9 -0
  173. package/src/ot.ts +24 -0
  174. package/src/randomId.ts +15 -0
  175. package/src/reactRouterExports.ts +15 -0
  176. package/src/runCode.ts +27 -0
  177. package/src/server/aiChatHandler/aiEditing.ts +151 -0
  178. package/src/server/aiChatHandler/chatOperations.ts +476 -0
  179. package/src/server/aiChatHandler/errorHandling.ts +66 -0
  180. package/src/server/aiChatHandler/index.ts +199 -0
  181. package/src/server/aiChatHandler/llmStreaming.ts +419 -0
  182. package/src/server/aiChatHandler/validation.ts +24 -0
  183. package/src/server/computeInitialDocument.ts +220 -0
  184. package/src/server/config.ts +24 -0
  185. package/src/server/featureFlags.ts +8 -0
  186. package/src/server/generateAIResponse.ts +188 -0
  187. package/src/server/handleAIAssist.ts +53 -0
  188. package/src/server/handleAIChatMessage.ts +2 -0
  189. package/src/server/handleAICopilot.ts +97 -0
  190. package/src/server/index.ts +379 -0
  191. package/src/server/isDirectory.ts +1 -0
  192. package/src/server/livekit.ts +24 -0
  193. package/src/server/prettier.ts +115 -0
  194. package/src/server/setupEnv.ts +8 -0
  195. package/src/submitOperation.ts +25 -0
  196. package/src/types.ts +205 -0
  197. package/src/utils/fileDiff.ts +117 -0
  198. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,228 @@
1
+ import { useCallback } from 'react';
2
+ import { FileTreePath } from '../types';
3
+ import { VizFileId, VizContent } from '@vizhub/viz-types';
4
+ import { randomId } from '../randomId';
5
+ import { EditorCache } from './useEditorCache';
6
+ import { getFileExtension } from './utils/fileExtension';
7
+ import { getLanguageExtension } from './CodeEditor/getOrCreateEditor';
8
+ import { editorCacheKey } from './useEditorCache';
9
+
10
+ // CRUD operations for files and directories
11
+ // CRUD = Create, Read, Update, Delete
12
+ export const useFileCRUD = ({
13
+ submitOperation,
14
+ closeTabs,
15
+ openTab,
16
+ editorCache,
17
+ content,
18
+ }: {
19
+ submitOperation: (
20
+ operation: (document: VizContent) => VizContent,
21
+ ) => void;
22
+ closeTabs: (idsToDelete: Array<VizFileId>) => void;
23
+ openTab: ({
24
+ fileId,
25
+ isTransient,
26
+ }: {
27
+ fileId: VizFileId;
28
+ isTransient: boolean;
29
+ }) => void;
30
+ editorCache: EditorCache;
31
+ content: VizContent;
32
+ }) => {
33
+ // Create a new file
34
+ const createFile = useCallback(
35
+ (name: string, text = '') => {
36
+ if (name) {
37
+ const fileId: VizFileId = randomId();
38
+ submitOperation((document: VizContent) => ({
39
+ ...document,
40
+ files: {
41
+ ...document.files,
42
+ [fileId]: { name, text },
43
+ },
44
+ }));
45
+ // When a new file is created, open it in a new tab
46
+ // and focus the editor on it.
47
+ openTab({ fileId, isTransient: false });
48
+ }
49
+ },
50
+ [submitOperation],
51
+ );
52
+
53
+ const createDirectory = useCallback(
54
+ (name: string, text = null) => {
55
+ if (name) {
56
+ name += '/';
57
+ const fileId: VizFileId = randomId();
58
+ submitOperation((document: VizContent) => ({
59
+ ...document,
60
+ files: {
61
+ ...document.files,
62
+ [fileId]: { name, text },
63
+ },
64
+ }));
65
+ }
66
+ },
67
+ [submitOperation],
68
+ );
69
+
70
+ // Called when a file in the sidebar is renamed.
71
+ const renameFile = useCallback(
72
+ (fileId: VizFileId, newName: string) => {
73
+ submitOperation((document: VizContent) => ({
74
+ ...document,
75
+ files: {
76
+ ...document.files,
77
+ [fileId]: {
78
+ ...document.files[fileId],
79
+ name:
80
+ document.files[fileId].name.substring(
81
+ 0,
82
+ document.files[fileId].name.lastIndexOf(
83
+ '/',
84
+ ) + 1,
85
+ ) + newName,
86
+ },
87
+ },
88
+ }));
89
+
90
+ const oldName = content.files[fileId].name;
91
+ const oldExtension = getFileExtension(oldName);
92
+
93
+ const newExtension = getFileExtension(newName);
94
+
95
+ const didExtensionChange =
96
+ newExtension !== oldExtension;
97
+ if (didExtensionChange) {
98
+ // Update the language compartment of the corresponding CodeMirror editor
99
+ // with the new language, based on the new extension, only if it changed.
100
+ // Search through all cached editors for this file ID
101
+ for (const [
102
+ cacheKey,
103
+ cachedEditor,
104
+ ] of editorCache.entries()) {
105
+ // Check if this cache entry is for the current file
106
+ if (cacheKey.startsWith(fileId + '|')) {
107
+ if (
108
+ cachedEditor &&
109
+ cachedEditor.languageCompartment
110
+ ) {
111
+ const newLanguageExtension =
112
+ getLanguageExtension(newExtension);
113
+
114
+ // Reconfigure the language compartment with the new language extension
115
+ cachedEditor.editor.dispatch({
116
+ effects:
117
+ cachedEditor.languageCompartment.reconfigure(
118
+ newLanguageExtension
119
+ ? [newLanguageExtension]
120
+ : [],
121
+ ),
122
+ });
123
+ // Continue to update all instances of this file in different panes
124
+ }
125
+ }
126
+ }
127
+ }
128
+ },
129
+ [submitOperation],
130
+ );
131
+
132
+ // Renames a directory
133
+ const renameDirectory = useCallback(
134
+ (
135
+ path: FileTreePath,
136
+ oldName: string,
137
+ newName: string,
138
+ ) => {
139
+ submitOperation((document: VizContent) => {
140
+ const updatedFiles = Object.keys(
141
+ document.files,
142
+ ).reduce((acc, key) => {
143
+ const file = document.files[key];
144
+ const fileName = file.name;
145
+ //See if it's actually in directory
146
+ if (fileName.includes(path + '/')) {
147
+ //Create New Name for System
148
+ const pathPart = fileName.substring(
149
+ 0,
150
+ fileName.indexOf(path) + path.length,
151
+ );
152
+ const oldNamePos =
153
+ pathPart.lastIndexOf(oldName);
154
+ const fileNewName =
155
+ fileName.substring(0, oldNamePos) +
156
+ newName +
157
+ fileName.substring(
158
+ oldNamePos + oldName.length,
159
+ );
160
+ //Return with new names
161
+ acc[key] = { ...file, name: fileNewName };
162
+ } else {
163
+ acc[key] = file;
164
+ }
165
+ return acc;
166
+ }, {});
167
+
168
+ return { ...document, files: updatedFiles };
169
+ });
170
+ },
171
+ [submitOperation],
172
+ );
173
+
174
+ // Deletes a file
175
+ const deleteFile = useCallback(
176
+ (fileId: VizFileId) => {
177
+ closeTabs([fileId]);
178
+ submitOperation((document) => {
179
+ const updatedFiles = { ...document.files };
180
+ delete updatedFiles[fileId];
181
+ return { ...document, files: updatedFiles };
182
+ });
183
+ },
184
+ [submitOperation, closeTabs],
185
+ );
186
+
187
+ // Deletes a directory and all files within it
188
+ const deleteDirectory = useCallback(
189
+ (path: FileTreePath) => {
190
+ const tabsToClose: Array<VizFileId> = [];
191
+ submitOperation((document: VizContent) => {
192
+ const updatedFiles = { ...document.files };
193
+ for (const key in updatedFiles) {
194
+ if (updatedFiles[key].name.includes(path + '/')) {
195
+ tabsToClose.push(key);
196
+ delete updatedFiles[key];
197
+ }
198
+ }
199
+ return { ...document, files: updatedFiles };
200
+ });
201
+ closeTabs(tabsToClose);
202
+ },
203
+ [submitOperation, closeTabs],
204
+ );
205
+
206
+ // Deletes all files
207
+ const deleteAllFiles = useCallback(() => {
208
+ const tabsToClose: Array<VizFileId> = [];
209
+ submitOperation((document: VizContent) => {
210
+ // Collect all file IDs for closing tabs
211
+ for (const fileId in document.files) {
212
+ tabsToClose.push(fileId);
213
+ }
214
+ return { ...document, files: {} };
215
+ });
216
+ closeTabs(tabsToClose);
217
+ }, [submitOperation, closeTabs]);
218
+
219
+ return {
220
+ createFile,
221
+ renameFile,
222
+ deleteFile,
223
+ createDirectory,
224
+ renameDirectory,
225
+ deleteDirectory,
226
+ deleteAllFiles,
227
+ };
228
+ };
@@ -0,0 +1,451 @@
1
+ import { useContext, useEffect } from 'react';
2
+ import { shouldTriggerRun } from './shouldTriggerRun';
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
+ import { VizFileId } from '@vizhub/viz-types';
8
+ import { PaneId } from '../types';
9
+ import { editorCacheKey } from './useEditorCache';
10
+
11
+ /*
12
+ The following is a helpful resource for the following Code Mirror Syntax Tree methods below
13
+ https://lezer.codemirror.net/docs/ref/#common
14
+ */
15
+
16
+ // Store the element in the current editor DOM to highlight, indicating a potential jump to definition is possible
17
+ let activeJumpingElement: HTMLSpanElement = null;
18
+
19
+ // Store the syntax node representing the destination within the syntax tree
20
+ let definingNode: SyntaxNode = null;
21
+
22
+ // Example nesting types for the specific language to find level in the syntax tree
23
+ const nestingTypes = new Set<string>([
24
+ 'FunctionDeclaration',
25
+ 'ClassDeclaration',
26
+ 'MethodDeclaration',
27
+ 'Block',
28
+ 'IfStatement',
29
+ 'ForStatement',
30
+ 'WhileStatement',
31
+ 'SwitchStatement',
32
+ 'TryStatement',
33
+ 'CatchClause',
34
+ 'WithStatement',
35
+ 'ArrowFunction',
36
+ 'ImportGroup',
37
+ ]);
38
+
39
+ // Example declaration types for the specific language to find definitions in the syntax tree
40
+ const declarationTypes = new Set<string>([
41
+ 'VariableDeclaration',
42
+ 'FunctionDeclaration',
43
+ 'ClassDeclaration',
44
+ 'MethodDeclaration',
45
+ 'PropertyDeclaration',
46
+ 'ImportDeclaration',
47
+ 'ExportDeclaration',
48
+ 'CallExpression',
49
+ 'ArrayPattern',
50
+ 'ObjectPattern',
51
+ 'PatternProperty',
52
+ 'ArgList',
53
+ 'TypeArgList',
54
+ 'ParamList',
55
+ 'ForOfSpec',
56
+ ]);
57
+
58
+ function getIdentifierContext(
59
+ identifier: SyntaxNode,
60
+ ): number {
61
+ let current: SyntaxNode = identifier;
62
+ let levels: number = 0;
63
+
64
+ // Traverse up the tree to find the total depth, only counting valid nesting types
65
+ while (current && current.type) {
66
+ const parentType: string = current.type.name;
67
+
68
+ if (nestingTypes.has(parentType)) {
69
+ levels++;
70
+ }
71
+
72
+ current = current.parent;
73
+ }
74
+
75
+ return levels;
76
+ }
77
+
78
+ function jumpToDefinition(
79
+ editor: EditorView,
80
+ node: SyntaxNode,
81
+ ): SyntaxNode {
82
+ const state: EditorState = editor.state;
83
+ const definitions: Array<{
84
+ identifier: SyntaxNode;
85
+ context: number;
86
+ }> = [];
87
+
88
+ // From an identifier in the syntax tree, fetch the name and context to find closest defining syntax node
89
+ const identifier: SyntaxNode = node;
90
+ const identifierName: string = state.doc.sliceString(
91
+ identifier.from,
92
+ identifier.to,
93
+ );
94
+ const context: number = getIdentifierContext(identifier);
95
+
96
+ if (identifier) {
97
+ syntaxTree(state).iterate({
98
+ enter(tree: SyntaxNodeRef) {
99
+ // Traverse syntax tree to find positions of respective identifier definitions within context
100
+ if (
101
+ declarationTypes.has(tree.name) ||
102
+ nestingTypes.has(tree.name)
103
+ ) {
104
+ const parent: SyntaxNode = tree.node;
105
+
106
+ // Fetch a host of potential identifiers in an attempt to find the defining syntax node
107
+ const children: Array<SyntaxNode> = [
108
+ ...parent.getChildren('VariableDefinition'),
109
+ ...parent.getChildren('PropertyDefinition'),
110
+ ...parent.getChildren('PropertyName'),
111
+ ...parent.getChildren('Identifier'),
112
+ ];
113
+
114
+ children.forEach((child: SyntaxNode) => {
115
+ const name: string = state.doc.sliceString(
116
+ child.from,
117
+ child.to,
118
+ );
119
+
120
+ // Ensure no jump to self by comparing syntax node id's
121
+ if (
122
+ name === identifierName &&
123
+ child.type.id !== identifier.type.id
124
+ ) {
125
+ definitions.push({
126
+ identifier: child,
127
+ context: getIdentifierContext(child),
128
+ });
129
+ }
130
+ });
131
+ }
132
+ },
133
+ });
134
+
135
+ if (definitions.length > 0) {
136
+ // Sort definitions by their context in the syntax tree
137
+ definitions.sort((a, b) => a.context - b.context);
138
+
139
+ let closestDefinition: SyntaxNode =
140
+ definitions[0].identifier;
141
+
142
+ for (let i = definitions.length - 1; i >= 0; i--) {
143
+ if (definitions[i].context <= context) {
144
+ closestDefinition = definitions[i].identifier;
145
+ break;
146
+ }
147
+ }
148
+
149
+ return closestDefinition;
150
+ }
151
+
152
+ return null;
153
+ }
154
+ }
155
+
156
+ // Sidebar keyboard shortcuts in form Ctrl + Shift + <key>
157
+ const sideBarKeyBoardMap = {
158
+ E: 'files-icon',
159
+ F: 'search-icon',
160
+ K: 'shortcut-icon',
161
+ B: 'bug-icon',
162
+ S: 'settings-icon',
163
+ N: 'new-file-icon',
164
+ D: 'new-directory-icon',
165
+ A: 'auto-focus-icon',
166
+ };
167
+
168
+ // This module implements the keyboard shortcuts
169
+ // for the VZCode editor.
170
+ // These include:
171
+ // * Alt-w: Close the current tab
172
+ // * Alt-n: Open the create file modal
173
+ // * Alt-PageUp: Change the active tab to the previous one
174
+ // * Alt-PageDown: Change the active tab to the next one
175
+ // * Ctrl-s or Shift-Enter: Run the code and format it with Prettier
176
+ // * Ctrl-Click: Jump to closest definition for a potential identifier
177
+ export const useKeyboardShortcuts = ({
178
+ closeTabs,
179
+ activeFileId,
180
+ activePaneId,
181
+ handleOpenCreateFileModal,
182
+ setActiveFileLeft,
183
+ setActiveFileRight,
184
+ toggleSearchFocused,
185
+ runPrettierRef,
186
+ runCodeRef,
187
+ sidebarRef,
188
+ editorCache,
189
+ codeEditorRef,
190
+ }: {
191
+ closeTabs: (fileIds: VizFileId[]) => void;
192
+ activeFileId: VizFileId | null;
193
+ activePaneId: PaneId;
194
+ handleOpenCreateFileModal: () => void;
195
+ setActiveFileLeft: () => void;
196
+ setActiveFileRight: () => void;
197
+ toggleSearchFocused: () => void;
198
+ runPrettierRef: React.MutableRefObject<() => void>;
199
+ runCodeRef: React.MutableRefObject<() => void>;
200
+ sidebarRef: React.RefObject<HTMLDivElement>;
201
+ editorCache: Map<string, { editor: EditorView }>;
202
+ codeEditorRef: React.RefObject<HTMLDivElement>;
203
+ }) => {
204
+ useEffect(() => {
205
+ // This key is needed to look up the current editor in the editor cache.
206
+ const cacheKey = editorCacheKey(
207
+ activeFileId,
208
+ activePaneId,
209
+ );
210
+
211
+ const handleKeyPress = (event: KeyboardEvent) => {
212
+ if (shouldTriggerRun(event)) {
213
+ event.preventDefault();
214
+
215
+ // Run Prettier
216
+ const runPrettier = runPrettierRef.current;
217
+ if (runPrettier !== null) {
218
+ runPrettier();
219
+ }
220
+
221
+ // Run the code
222
+ const runCode = runCodeRef.current;
223
+
224
+ if (runCode !== null) {
225
+ runCode();
226
+ }
227
+ return;
228
+ }
229
+
230
+ if (event.ctrlKey && event.shiftKey) {
231
+ // Handle keyboard shortcuts related to the side bar icons
232
+ document
233
+ .getElementById(sideBarKeyBoardMap[event.key])
234
+ ?.click();
235
+
236
+ // Ensure the search input is always focused
237
+ if (event.key === 'F') {
238
+ toggleSearchFocused();
239
+ }
240
+ } else if (event.ctrlKey && event.key === ',') {
241
+ document
242
+ .getElementById(sideBarKeyBoardMap['S'])
243
+ ?.click();
244
+ }
245
+
246
+ if (event.ctrlKey === true) {
247
+ // On holding CTRL key, search for a potential definition jump using mouse location
248
+ document.addEventListener(
249
+ 'mouseover',
250
+ handleMouseOver,
251
+ );
252
+ }
253
+
254
+ if (event.altKey === true) {
255
+ // Alt-w: Close the current tab
256
+ if (event.key === 'w') {
257
+ // TODO clean this up so we can remove `activeFileId`
258
+ // as a dependency
259
+ // TODO closeActiveTab()
260
+ if (activeFileId) {
261
+ closeTabs([activeFileId]);
262
+ }
263
+ return;
264
+ }
265
+
266
+ // Alt-n: Open the create file modal
267
+ if (event.key === 'n') {
268
+ handleOpenCreateFileModal();
269
+ return;
270
+ }
271
+
272
+ // Alt-PageUp: Change the active tab to the previous one
273
+ if (event.key === 'PageUp') {
274
+ setActiveFileLeft();
275
+ return;
276
+ }
277
+
278
+ // Alt-PageDown: Change the active tab to the next one
279
+ if (event.key === 'PageDown') {
280
+ setActiveFileRight();
281
+ return;
282
+ }
283
+
284
+ if (event.key === '1') {
285
+ if (sidebarRef.current) {
286
+ sidebarRef.current.focus();
287
+ }
288
+ }
289
+
290
+ if (event.key === '2') {
291
+ if (codeEditorRef.current) {
292
+ codeEditorRef.current.focus();
293
+ }
294
+ }
295
+ }
296
+ };
297
+
298
+ const resetActiveJumpingElement = (): void => {
299
+ if (activeJumpingElement) {
300
+ activeJumpingElement.style.cursor = 'initial';
301
+ activeJumpingElement.style.textDecoration = 'none';
302
+ activeJumpingElement = definingNode = null;
303
+ }
304
+
305
+ document.removeEventListener(
306
+ 'mouseover',
307
+ handleMouseOver,
308
+ );
309
+ document.removeEventListener(
310
+ 'mousedown',
311
+ jumpToDefinitionHandler,
312
+ );
313
+ };
314
+
315
+ const jumpToDefinitionHandler = (
316
+ event: MouseEvent,
317
+ ): void => {
318
+ // Ensure the current destination node is defined
319
+ // and current cursor position matches highlighted element
320
+ if (
321
+ !definingNode ||
322
+ (event.target as HTMLSpanElement) !==
323
+ activeJumpingElement
324
+ ) {
325
+ return;
326
+ }
327
+
328
+ // Don't crash if no active file
329
+ if (!activeFileId) {
330
+ return;
331
+ }
332
+
333
+ // Move current cursor and center view in the editor to destination node
334
+
335
+ const editor: EditorView =
336
+ editorCache.get(cacheKey).editor;
337
+ const closestDefinition: SyntaxNode = definingNode;
338
+
339
+ editor.dispatch({
340
+ selection: {
341
+ anchor: closestDefinition.from,
342
+ head: closestDefinition.to,
343
+ },
344
+ scrollIntoView: true,
345
+ effects: EditorView.scrollIntoView(
346
+ closestDefinition.from,
347
+ {
348
+ y: 'center',
349
+ },
350
+ ),
351
+ });
352
+
353
+ resetActiveJumpingElement();
354
+ };
355
+
356
+ const handleKeyRelease = (event: KeyboardEvent) => {
357
+ // On releasing CTRL key, reset all active definition jumping elements and listeners
358
+ if (!event.ctrlKey) {
359
+ resetActiveJumpingElement();
360
+ }
361
+ };
362
+
363
+ const handleMouseOver = (event: MouseEvent) => {
364
+ if (!activeFileId) {
365
+ return;
366
+ }
367
+
368
+ const editor: EditorView =
369
+ editorCache.get(cacheKey).editor;
370
+ const tree = syntaxTree(editor.state);
371
+ const element = event.target as HTMLSpanElement;
372
+
373
+ // Ensure the identifier element can be found and is within the current editor DOM
374
+ if (
375
+ element == null ||
376
+ !editor.dom.contains(element)
377
+ ) {
378
+ return;
379
+ }
380
+
381
+ let position: number;
382
+ try {
383
+ position = editor.posAtDOM(element);
384
+ } catch (error) {
385
+ // Element is not within the current editor's document
386
+ return;
387
+ }
388
+ const identifier: SyntaxNode = tree.resolveInner(
389
+ position,
390
+ 1,
391
+ );
392
+
393
+ // All valid identifiers must be span elements to find a potential defining jump in editor
394
+ if (
395
+ identifier &&
396
+ element instanceof HTMLSpanElement
397
+ ) {
398
+ const potentialJump: SyntaxNode = jumpToDefinition(
399
+ editor,
400
+ identifier,
401
+ );
402
+
403
+ // Only allowing to jump to other definition nodes
404
+ if (
405
+ potentialJump &&
406
+ identifier.type.id !== potentialJump.type.id
407
+ ) {
408
+ if (activeJumpingElement) {
409
+ activeJumpingElement.style.cursor = 'initial';
410
+ activeJumpingElement.style.textDecoration =
411
+ 'none';
412
+ }
413
+
414
+ activeJumpingElement = element;
415
+ definingNode = potentialJump;
416
+
417
+ activeJumpingElement.style.cursor = 'pointer';
418
+ activeJumpingElement.style.textDecoration =
419
+ 'underline';
420
+
421
+ // CTRL + Click: Jump to relative definition, which is removed on CTRL key release
422
+ document.addEventListener(
423
+ 'mousedown',
424
+ jumpToDefinitionHandler,
425
+ { once: true },
426
+ );
427
+ }
428
+ }
429
+ };
430
+
431
+ document.addEventListener('keydown', handleKeyPress);
432
+ document.addEventListener('keyup', handleKeyRelease);
433
+
434
+ return () => {
435
+ document.removeEventListener(
436
+ 'keydown',
437
+ handleKeyPress,
438
+ );
439
+ document.removeEventListener(
440
+ 'keyup',
441
+ handleKeyRelease,
442
+ );
443
+ };
444
+ }, [
445
+ handleOpenCreateFileModal,
446
+ closeTabs,
447
+ activeFileId,
448
+ setActiveFileLeft,
449
+ setActiveFileRight,
450
+ ]);
451
+ };