vzcode 2.3.0 → 2.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.
- package/package.json +3 -2
- package/src/cli.ts +208 -0
- package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +42 -0
- package/src/client/AIAssist/AIAssistWidget/index.tsx +135 -0
- package/src/client/AIAssist/AIAssistWidget/style.scss +47 -0
- package/src/client/AIAssist/Spinner.jsx +76 -0
- package/src/client/AIAssist/formatFile.ts +27 -0
- package/src/client/AIAssist/generateFilesContext.ts +25 -0
- package/src/client/AIAssist/index.ts +2 -0
- package/src/client/AIAssist/startAIAssist.ts +95 -0
- package/src/client/App/index.tsx +152 -0
- package/src/client/App/style.scss +82 -0
- package/src/client/App/usePending.ts +51 -0
- package/src/client/App/useShareDB.ts +161 -0
- package/src/client/CodeEditor/Copilot/index.ts +74 -0
- package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +68 -0
- package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +174 -0
- package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +79 -0
- package/src/client/CodeEditor/InteractiveWidgets/index.ts +44 -0
- package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +20 -0
- package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +143 -0
- package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +48 -0
- package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +25 -0
- package/src/client/CodeEditor/basicSetup.ts +73 -0
- package/src/client/CodeEditor/getOrCreateEditor.tsx +615 -0
- package/src/client/CodeEditor/index.tsx +202 -0
- package/src/client/CodeEditor/json1PresenceBroadcast.ts +41 -0
- package/src/client/CodeEditor/json1PresenceDisplay.ts +350 -0
- package/src/client/CodeEditor/rainbowBrackets.ts +122 -0
- package/src/client/CodeEditor/style.scss +156 -0
- package/src/client/CodeEditor/typescriptExtension/worker.ts +32 -0
- package/src/client/CodeErrorOverlay/index.tsx +77 -0
- package/src/client/CodeErrorOverlay/style.scss +33 -0
- package/src/client/Fonts/fonts.ts +8 -0
- package/src/client/Icons/AdjustmentSVG.tsx +19 -0
- package/src/client/Icons/BugSVG.tsx +20 -0
- package/src/client/Icons/CloseSVG.tsx +19 -0
- package/src/client/Icons/CssSVG.tsx +16 -0
- package/src/client/Icons/DirectoryArrowSVG.tsx +19 -0
- package/src/client/Icons/EditSVG.tsx +19 -0
- package/src/client/Icons/FileSVG.tsx +19 -0
- package/src/client/Icons/FolderSVG.tsx +20 -0
- package/src/client/Icons/GearSVG.tsx +16 -0
- package/src/client/Icons/HtmlSVG.tsx +16 -0
- package/src/client/Icons/JavaScriptSVG.tsx +16 -0
- package/src/client/Icons/JsonSVG.tsx +16 -0
- package/src/client/Icons/MarkdownSVG.tsx +16 -0
- package/src/client/Icons/MicOffSVG.tsx +35 -0
- package/src/client/Icons/MicSVG.tsx +25 -0
- package/src/client/Icons/NewSVG.tsx +19 -0
- package/src/client/Icons/PinSVG.tsx +14 -0
- package/src/client/Icons/PlaySVG.tsx +16 -0
- package/src/client/Icons/QuestionMarkSVG.tsx +18 -0
- package/src/client/Icons/ReactSVG.tsx +16 -0
- package/src/client/Icons/SearchSVG.tsx +20 -0
- package/src/client/Icons/SparklesSVG.tsx +23 -0
- package/src/client/Icons/SplitEditorSVG.tsx +34 -0
- package/src/client/Icons/StopSVG.tsx +15 -0
- package/src/client/Icons/SvelteSVG.tsx +16 -0
- package/src/client/Icons/TrashSVG.tsx +19 -0
- package/src/client/Icons/TypeScriptSVG.tsx +16 -0
- package/src/client/Icons/index.tsx +26 -0
- package/src/client/ImageViewer/index.tsx +91 -0
- package/src/client/ImageViewer/style.scss +41 -0
- package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
- package/src/client/PresenceNotifications/index.tsx +158 -0
- package/src/client/PresenceNotifications/style.scss +17 -0
- package/src/client/RunCodeWidget/index.tsx +117 -0
- package/src/client/RunCodeWidget/style.scss +24 -0
- package/src/client/SplitPaneResizeContext.tsx +159 -0
- package/src/client/TabList/Tab.tsx +73 -0
- package/src/client/TabList/index.tsx +35 -0
- package/src/client/TabList/style.scss +51 -0
- package/src/client/VZCodeContext/index.tsx +29 -0
- package/src/client/VZCodeContext/types.ts +248 -0
- package/src/client/VZCodeContext/useVZCodeState.ts +602 -0
- package/src/client/VZKeyboardShortcutsDoc.tsx +148 -0
- package/src/client/VZLeft.tsx +28 -0
- package/src/client/VZMiddle.tsx +172 -0
- package/src/client/VZResizer/index.tsx +128 -0
- package/src/client/VZResizer/styles.scss +33 -0
- package/src/client/VZRight.tsx +113 -0
- package/src/client/VZSettings.tsx +275 -0
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +243 -0
- package/src/client/VZSidebar/AIChat/DiffView.scss +136 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +176 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +79 -0
- package/src/client/VZSidebar/AIChat/MessageList.tsx +190 -0
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +35 -0
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +156 -0
- package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +19 -0
- package/src/client/VZSidebar/AIChat/index.tsx +357 -0
- package/src/client/VZSidebar/AIChat/styles.scss +646 -0
- package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
- package/src/client/VZSidebar/CreateDirModal.tsx +111 -0
- package/src/client/VZSidebar/CreateFileModal.tsx +111 -0
- package/src/client/VZSidebar/DeleteConfirmationModal.tsx +76 -0
- package/src/client/VZSidebar/DirectoryListing.tsx +89 -0
- package/src/client/VZSidebar/EmptyState.tsx +11 -0
- package/src/client/VZSidebar/FileListing.tsx +91 -0
- package/src/client/VZSidebar/FileTypeIcon.tsx +49 -0
- package/src/client/VZSidebar/Item.tsx +237 -0
- package/src/client/VZSidebar/Listing.tsx +47 -0
- package/src/client/VZSidebar/Search.tsx +527 -0
- package/src/client/VZSidebar/VisualEditor.tsx +282 -0
- package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
- package/src/client/VZSidebar/aiCopyPaste.ts +199 -0
- package/src/client/VZSidebar/index.tsx +728 -0
- package/src/client/VZSidebar/styles.scss +612 -0
- package/src/client/VZSidebar/useDragAndDrop.tsx +408 -0
- package/src/client/bootstrap.ts +26 -0
- package/src/client/buildWorker.ts +3 -0
- package/src/client/featureFlags.ts +8 -0
- package/src/client/generateRequestId.ts +6 -0
- package/src/client/getFileTree.ts +53 -0
- package/src/client/index.ts +19 -0
- package/src/client/main.jsx +10 -0
- package/src/client/presenceColor.ts +8 -0
- package/src/client/shouldTriggerRun.ts +31 -0
- package/src/client/sortFileTree.ts +21 -0
- package/src/client/tabsSearchParameters.test.ts +79 -0
- package/src/client/tabsSearchParameters.ts +137 -0
- package/src/client/themes/index.ts +7 -0
- package/src/client/themes/themes.ts +77 -0
- package/src/client/themes/uiwImports.ts +32 -0
- package/src/client/themes/useDynamicTheme.ts +29 -0
- package/src/client/themes/vizhubTheme/colors.ts +80 -0
- package/src/client/themes/vizhubTheme/index.ts +193 -0
- package/src/client/useActions.ts +288 -0
- package/src/client/useESLint/index.ts +72 -0
- package/src/client/useESLint/worker.ts +113 -0
- package/src/client/useEditorCache.ts +35 -0
- package/src/client/useFileCRUD.ts +228 -0
- package/src/client/useKeyboardShortcuts.ts +451 -0
- package/src/client/useOpenDirectories.ts +99 -0
- package/src/client/usePresenceAutoFollow.ts +85 -0
- package/src/client/usePrettier/index.ts +178 -0
- package/src/client/usePrettier/worker.ts +109 -0
- package/src/client/useRunCode.tsx +15 -0
- package/src/client/useRuntimeError.ts +32 -0
- package/src/client/useSubmitOperation.ts +15 -0
- package/src/client/useURLSync.ts +137 -0
- package/src/client/usernameLocalStorage.ts +43 -0
- package/src/client/utils/fileExtension.ts +10 -0
- package/src/client/utils/isImageFile.ts +12 -0
- package/src/client/vzReducer/aiChatReducer.ts +44 -0
- package/src/client/vzReducer/closeTabReducer.test.ts +205 -0
- package/src/client/vzReducer/closeTabsReducer.ts +96 -0
- package/src/client/vzReducer/createInitialState.ts +38 -0
- package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
- package/src/client/vzReducer/findPane.ts +19 -0
- package/src/client/vzReducer/index.ts +250 -0
- package/src/client/vzReducer/openTabReducer.test.ts +127 -0
- package/src/client/vzReducer/openTabReducer.ts +74 -0
- package/src/client/vzReducer/searchReducer.test.ts +292 -0
- package/src/client/vzReducer/searchReducer.ts +181 -0
- package/src/client/vzReducer/setActiveFileIdReducer.ts +28 -0
- package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +76 -0
- package/src/client/vzReducer/setIsDocOpenReducer.ts +9 -0
- package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
- package/src/client/vzReducer/setThemeReducer.ts +9 -0
- package/src/client/vzReducer/setUsernameReducer.ts +9 -0
- package/src/client/vzReducer/splitCurrentPaneReducer.ts +66 -0
- package/src/client/vzReducer/toggleAutoFollowReducer.ts +12 -0
- package/src/client/vzReducer/updatePane.ts +40 -0
- package/src/client/vzReducer/updatePresenceIndicatorReducer.ts +30 -0
- package/src/client/vzReducer/visualEditorReducer.ts +9 -0
- package/src/ot.ts +24 -0
- package/src/randomId.ts +15 -0
- package/src/reactRouterExports.ts +15 -0
- package/src/runCode.ts +27 -0
- package/src/server/aiChatHandler/aiEditing.ts +151 -0
- package/src/server/aiChatHandler/chatOperations.ts +476 -0
- package/src/server/aiChatHandler/errorHandling.ts +66 -0
- package/src/server/aiChatHandler/index.ts +199 -0
- package/src/server/aiChatHandler/llmStreaming.ts +419 -0
- package/src/server/aiChatHandler/validation.ts +24 -0
- package/src/server/computeInitialDocument.ts +220 -0
- package/src/server/config.ts +24 -0
- package/src/server/featureFlags.ts +8 -0
- package/src/server/generateAIResponse.ts +188 -0
- package/src/server/handleAIAssist.ts +53 -0
- package/src/server/handleAIChatMessage.ts +2 -0
- package/src/server/handleAICopilot.ts +97 -0
- package/src/server/index.ts +379 -0
- package/src/server/isDirectory.ts +1 -0
- package/src/server/livekit.ts +24 -0
- package/src/server/prettier.ts +115 -0
- package/src/server/setupEnv.ts +8 -0
- package/src/submitOperation.ts +25 -0
- package/src/types.ts +205 -0
- package/src/utils/fileDiff.ts +117 -0
- 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
|
+
};
|