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,379 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import WebSocketJSONStream from '@teamwork/websocket-json-stream';
|
|
3
|
+
import bodyParser from 'body-parser';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import open from 'open';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import ShareDB from 'sharedb';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { WebSocketServer } from 'ws';
|
|
12
|
+
import { json1Presence } from '../ot.js';
|
|
13
|
+
import { computeInitialDocument } from './computeInitialDocument.js';
|
|
14
|
+
import { handleAIAssist } from './handleAIAssist.js';
|
|
15
|
+
import { handleAICopilot } from './handleAICopilot.js';
|
|
16
|
+
import { handleAIChatMessage } from './handleAIChatMessage.js';
|
|
17
|
+
|
|
18
|
+
import { isDirectory } from './isDirectory.js';
|
|
19
|
+
import { createToken } from './livekit.js';
|
|
20
|
+
import './setupEnv.js';
|
|
21
|
+
|
|
22
|
+
// Import the image file utility
|
|
23
|
+
const isImageFile = (fileName) => {
|
|
24
|
+
return (
|
|
25
|
+
fileName.match(
|
|
26
|
+
/\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
|
|
27
|
+
) !== null
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// The time in milliseconds by which auto-saving is debounced.
|
|
32
|
+
const autoSaveDebounceTimeMS = 800;
|
|
33
|
+
|
|
34
|
+
// The time in milliseconds by which auto-saving is throttled
|
|
35
|
+
// when the user is interacting with the widgets in the editor.
|
|
36
|
+
const throttleTimeMS = 100;
|
|
37
|
+
|
|
38
|
+
// Helper function to get the port from command line arguments or use default.
|
|
39
|
+
function getPortFromArgs(defaultPort = 3030) {
|
|
40
|
+
const portArg = process.argv.find((arg) =>
|
|
41
|
+
arg.startsWith('--port='),
|
|
42
|
+
);
|
|
43
|
+
if (portArg) {
|
|
44
|
+
return parseInt(portArg.split('=')[1], 10);
|
|
45
|
+
}
|
|
46
|
+
return defaultPort;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Server port.
|
|
50
|
+
const port = getPortFromArgs();
|
|
51
|
+
|
|
52
|
+
const initialDocument = computeInitialDocument({
|
|
53
|
+
// Use the current working directory to look for files.
|
|
54
|
+
fullPath: process.cwd(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Register our custom OT type,
|
|
58
|
+
// because it does not ship with ShareDB.
|
|
59
|
+
ShareDB.types.register(json1Presence.type);
|
|
60
|
+
|
|
61
|
+
const app = express();
|
|
62
|
+
|
|
63
|
+
// Use ShareDB over WebSocket
|
|
64
|
+
const shareDBBackend = new ShareDB({
|
|
65
|
+
// Enable presence
|
|
66
|
+
// See https://github.com/share/sharedb/blob/master/examples/rich-text-presence/server.js#L9
|
|
67
|
+
presence: true,
|
|
68
|
+
doNotForwardSendPresenceErrorsToClient: true,
|
|
69
|
+
});
|
|
70
|
+
const server = http.createServer(app);
|
|
71
|
+
const wss = new WebSocketServer({ server });
|
|
72
|
+
wss.on('connection', (ws) => {
|
|
73
|
+
const clientStream = new WebSocketJSONStream(ws);
|
|
74
|
+
shareDBBackend.listen(clientStream);
|
|
75
|
+
|
|
76
|
+
// Prevent server crashes on errors.
|
|
77
|
+
clientStream.on('error', (error) => {
|
|
78
|
+
console.log('clientStream error: ' + error.message);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Handle errors
|
|
82
|
+
ws.on('error', (error) => {
|
|
83
|
+
console.log('ws error: ' + error.message);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Handle disconnections
|
|
87
|
+
ws.on('close', () => {
|
|
88
|
+
clientStream.end();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Serve static files
|
|
93
|
+
const filename = fileURLToPath(import.meta.url);
|
|
94
|
+
const dirname = path.dirname(filename);
|
|
95
|
+
const dir = path.join(dirname, '..', '..', 'dist');
|
|
96
|
+
app.use(express.static(dir));
|
|
97
|
+
|
|
98
|
+
// Create the initial "document",
|
|
99
|
+
// which is a representation of files on disk.
|
|
100
|
+
const shareDBConnection = shareDBBackend.connect();
|
|
101
|
+
const shareDBDoc = shareDBConnection.get('documents', '1');
|
|
102
|
+
|
|
103
|
+
// Set up presence for AI editing following the same pattern as useShareDB.ts
|
|
104
|
+
const docPresence = shareDBConnection.getDocPresence(
|
|
105
|
+
'documents',
|
|
106
|
+
'1',
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Create local presence for AI editing with a unique ID
|
|
110
|
+
const generateAIEditId = () => {
|
|
111
|
+
const timestamp = Date.now().toString(36);
|
|
112
|
+
return `ai-edit-${timestamp}`;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const createAIEditLocalPresence = () =>
|
|
116
|
+
docPresence.create(generateAIEditId());
|
|
117
|
+
|
|
118
|
+
shareDBDoc.create(initialDocument, json1Presence.type.uri);
|
|
119
|
+
|
|
120
|
+
// Handle AI Assist requests.
|
|
121
|
+
app.post(
|
|
122
|
+
'/ai-assist',
|
|
123
|
+
bodyParser.json(),
|
|
124
|
+
handleAIAssist(shareDBDoc),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Handle AI Copilot requests.
|
|
128
|
+
app.post(
|
|
129
|
+
'/ai-copilot',
|
|
130
|
+
bodyParser.json(),
|
|
131
|
+
handleAICopilot(),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Handle AI Chat Message requests.
|
|
135
|
+
app.post(
|
|
136
|
+
'/ai-chat-message',
|
|
137
|
+
bodyParser.json(),
|
|
138
|
+
handleAIChatMessage({
|
|
139
|
+
shareDBDoc,
|
|
140
|
+
createAIEditLocalPresence,
|
|
141
|
+
onCreditDeduction: undefined,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Livekit Token Generator
|
|
146
|
+
app.get('/livekit-token', async (req, res) => {
|
|
147
|
+
const { room, username } = req.query;
|
|
148
|
+
res.send(await createToken(room, username));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// The state of the document when files were last auto-saved.
|
|
152
|
+
let previousDocument = initialDocument;
|
|
153
|
+
|
|
154
|
+
// Saves the files that changed.
|
|
155
|
+
const save = () => {
|
|
156
|
+
const currentDocument = shareDBDoc.data;
|
|
157
|
+
|
|
158
|
+
// Take a look at each file (key) previously and currently.
|
|
159
|
+
const allKeys = Object.keys({
|
|
160
|
+
...currentDocument.files,
|
|
161
|
+
...previousDocument.files,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const directoriesToDelete = [];
|
|
165
|
+
|
|
166
|
+
for (const key of allKeys) {
|
|
167
|
+
const previous = previousDocument.files[key];
|
|
168
|
+
const current = currentDocument.files[key];
|
|
169
|
+
|
|
170
|
+
// If this file was neither created nor deleted...
|
|
171
|
+
if (previous && current) {
|
|
172
|
+
// Handle changing of text content.
|
|
173
|
+
if (previous.text !== current.text) {
|
|
174
|
+
if (isImageFile(current.name)) {
|
|
175
|
+
// Write image files as binary from base64
|
|
176
|
+
const buffer = Buffer.from(
|
|
177
|
+
current.text,
|
|
178
|
+
'base64',
|
|
179
|
+
);
|
|
180
|
+
fs.writeFileSync(current.name, buffer);
|
|
181
|
+
} else {
|
|
182
|
+
// Write non-image files as text
|
|
183
|
+
fs.writeFileSync(current.name, current.text);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// // Handle renaming files.
|
|
188
|
+
// if (previous.name !== current.name) {
|
|
189
|
+
|
|
190
|
+
// const previousPath = previous.name.split('/');
|
|
191
|
+
// const currentPath = current.name.split('/');
|
|
192
|
+
|
|
193
|
+
// // - If the path is length 1 for previous and current,
|
|
194
|
+
// // then this should be true.
|
|
195
|
+
// // - If the path length is the sane for previous and current,
|
|
196
|
+
// // AND all the entries leading up to the last one are the same
|
|
197
|
+
// // between previous and current, AND the last entry in the arrays
|
|
198
|
+
// // (the file name itself) is different between previous and current
|
|
199
|
+
// const onlyFileNameChanged =
|
|
200
|
+
// (previousPath.length === 1 &&
|
|
201
|
+
// currentPath.length === 1) ||
|
|
202
|
+
// (previousPath.length === currentPath.length &&
|
|
203
|
+
// previousPath
|
|
204
|
+
// .slice(0, -1)
|
|
205
|
+
// .every(
|
|
206
|
+
// (val, index) => val === currentPath[index],
|
|
207
|
+
// ) &&
|
|
208
|
+
// previousPath.at(-1) !== currentPath.at(-1));
|
|
209
|
+
|
|
210
|
+
// // This logic works if only the file name itself changes.
|
|
211
|
+
// if (onlyFileNameChanged) {
|
|
212
|
+
// fs.renameSync(previous.name, current.name);
|
|
213
|
+
// }
|
|
214
|
+
// {
|
|
215
|
+
// // If we get here, then the directory may have been renamed,
|
|
216
|
+
// // or the file has been moved between directories.
|
|
217
|
+
// // If we get here, then the directory may have been renamed,
|
|
218
|
+
// // or the file has been moved between directories.
|
|
219
|
+
// const newDir = path.dirname(current.name);
|
|
220
|
+
|
|
221
|
+
// // Check if the new directory exists, if not, create it
|
|
222
|
+
// if (!fs.existsSync(newDir)) {
|
|
223
|
+
// fs.mkdirSync(newDir, { recursive: true });
|
|
224
|
+
// }
|
|
225
|
+
|
|
226
|
+
// // Move the file to the new directory
|
|
227
|
+
// fs.renameSync(previous.name, current.name);
|
|
228
|
+
// }
|
|
229
|
+
// }
|
|
230
|
+
// Handle renaming files.
|
|
231
|
+
if (previous.name !== current.name) {
|
|
232
|
+
if (isDirectory(previous.name)) {
|
|
233
|
+
// If the directory itself is being renamed,
|
|
234
|
+
// Phase I: Ignore it, and get the files moved
|
|
235
|
+
// Phase II: Keep track of these, and delete them after
|
|
236
|
+
// all the files moved.
|
|
237
|
+
// directoriesToDelete.push(previous.name)
|
|
238
|
+
directoriesToDelete.push(previous.name);
|
|
239
|
+
} else {
|
|
240
|
+
const newDir = path.dirname(current.name);
|
|
241
|
+
|
|
242
|
+
// Check if the new directory exists, if not, create it
|
|
243
|
+
if (!fs.existsSync(newDir)) {
|
|
244
|
+
fs.mkdirSync(newDir, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Move the file to the new directory
|
|
248
|
+
fs.renameSync(previous.name, current.name);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// handle deleting files and directories.
|
|
254
|
+
if (previous && !current) {
|
|
255
|
+
let stats = fs.statSync(previous.name);
|
|
256
|
+
//Check if the file path we are trying to delete is a directory
|
|
257
|
+
if (!stats.isDirectory()) {
|
|
258
|
+
fs.unlinkSync(previous.name);
|
|
259
|
+
} else {
|
|
260
|
+
//Performs directory deletion.
|
|
261
|
+
fs.rm(
|
|
262
|
+
previous.name,
|
|
263
|
+
{
|
|
264
|
+
recursive: true, //Makes sure that all files in directory are deleted.
|
|
265
|
+
},
|
|
266
|
+
(error) => {
|
|
267
|
+
if (error) {
|
|
268
|
+
console.log(error);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Handle creating files and Directories.
|
|
276
|
+
if (!previous && current) {
|
|
277
|
+
//File Creation
|
|
278
|
+
if (!isDirectory(current.name)) {
|
|
279
|
+
if (isImageFile(current.name)) {
|
|
280
|
+
// Write image files as binary from base64
|
|
281
|
+
const buffer = Buffer.from(
|
|
282
|
+
current.text,
|
|
283
|
+
'base64',
|
|
284
|
+
);
|
|
285
|
+
fs.writeFileSync(current.name, buffer);
|
|
286
|
+
} else {
|
|
287
|
+
// Write non-image files as text
|
|
288
|
+
fs.writeFileSync(current.name, current.text);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
fs.mkdirSync(current.name, { recursive: true });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// TODO deleted all directories under directoriesToDelete
|
|
296
|
+
|
|
297
|
+
for (const dir of directoriesToDelete) {
|
|
298
|
+
//Performs directory deletion.
|
|
299
|
+
fs.rm(
|
|
300
|
+
dir,
|
|
301
|
+
{
|
|
302
|
+
recursive: true, //Makes sure that all files in directory are deleted.
|
|
303
|
+
},
|
|
304
|
+
(error) => {
|
|
305
|
+
if (error) {
|
|
306
|
+
console.log(error);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
previousDocument = currentDocument;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// // Listen for when users modify files.
|
|
316
|
+
// // Files get written to disk after `autoSaveDebounceTimeMS`
|
|
317
|
+
// // milliseconds of inactivity.
|
|
318
|
+
// let timeout;
|
|
319
|
+
// shareDBDoc.subscribe(() => {
|
|
320
|
+
// shareDBDoc.on('op', () => {
|
|
321
|
+
// // console.log(shareDBDoc.data.isInteracting);
|
|
322
|
+
// clearTimeout(timeout);
|
|
323
|
+
// timeout = setTimeout(save, autoSaveDebounceTimeMS);
|
|
324
|
+
// });
|
|
325
|
+
// });
|
|
326
|
+
|
|
327
|
+
let lastExecutedTime = Date.now();
|
|
328
|
+
let lastChangeTimeout; // This timeout ensures the last change is saved
|
|
329
|
+
|
|
330
|
+
// Function to throttle the saving
|
|
331
|
+
function throttleSave() {
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
const timeSinceLastExecution = now - lastExecutedTime;
|
|
334
|
+
|
|
335
|
+
if (timeSinceLastExecution > throttleTimeMS) {
|
|
336
|
+
save();
|
|
337
|
+
lastExecutedTime = now;
|
|
338
|
+
} else {
|
|
339
|
+
// Clear the previous timeout and set a new one to save the last change
|
|
340
|
+
clearTimeout(lastChangeTimeout);
|
|
341
|
+
lastChangeTimeout = setTimeout(() => {
|
|
342
|
+
save();
|
|
343
|
+
lastExecutedTime = now;
|
|
344
|
+
}, throttleTimeMS - timeSinceLastExecution);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Function to debounce the saving
|
|
349
|
+
let debounceTimeout;
|
|
350
|
+
function debounceSave() {
|
|
351
|
+
clearTimeout(debounceTimeout);
|
|
352
|
+
debounceTimeout = setTimeout(
|
|
353
|
+
save,
|
|
354
|
+
autoSaveDebounceTimeMS,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Subscribe to listen for modifications
|
|
359
|
+
shareDBDoc.subscribe(() => {
|
|
360
|
+
shareDBDoc.on('op', () => {
|
|
361
|
+
if (shareDBDoc.data.isInteracting) {
|
|
362
|
+
throttleSave();
|
|
363
|
+
} else {
|
|
364
|
+
debounceSave();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
server.listen(port, async () => {
|
|
370
|
+
// Note: ngrok support can be added when the package is properly installed
|
|
371
|
+
// Sets the port to the one specified in the environment
|
|
372
|
+
// variable (for development) or the default port.
|
|
373
|
+
let livePort = process.env.EDITOR_PORT || port;
|
|
374
|
+
console.log(`EDITOR_PORT: ${process.env.EDITOR_PORT}`);
|
|
375
|
+
console.log(
|
|
376
|
+
`Editor is live at http://localhost:${livePort}`,
|
|
377
|
+
);
|
|
378
|
+
open(`http://localhost:${livePort}`);
|
|
379
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isDirectory = (file) => file.endsWith('/');
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AccessToken } from 'livekit-server-sdk';
|
|
2
|
+
|
|
3
|
+
const { LIVEKIT_API_KEY, LIVEKIT_API_SECRET } = process.env;
|
|
4
|
+
|
|
5
|
+
export const createToken = async (roomName, username) => {
|
|
6
|
+
// If this room doesn't exist, it'll be automatically created when the first
|
|
7
|
+
// client joins
|
|
8
|
+
// Identifier to be used for participant.
|
|
9
|
+
// It's available as LocalParticipant.identity with livekit-client SDK
|
|
10
|
+
// console.log(username);
|
|
11
|
+
const participantName = `${username}`;
|
|
12
|
+
|
|
13
|
+
const at = new AccessToken(
|
|
14
|
+
LIVEKIT_API_KEY,
|
|
15
|
+
LIVEKIT_API_SECRET,
|
|
16
|
+
{
|
|
17
|
+
identity: participantName,
|
|
18
|
+
// Token to expire after 10 minutes
|
|
19
|
+
ttl: '10m',
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
at.addGrant({ roomJoin: true, room: roomName });
|
|
23
|
+
return await at.toJwt();
|
|
24
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { format } from 'prettier/standalone';
|
|
2
|
+
import * as prettierPluginBabel from 'prettier/plugins/babel';
|
|
3
|
+
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
|
4
|
+
import * as prettierPluginHtml from 'prettier/plugins/html';
|
|
5
|
+
import * as prettierPluginMarkdown from 'prettier/plugins/markdown';
|
|
6
|
+
import * as prettierPluginCSS from 'prettier/plugins/postcss';
|
|
7
|
+
import * as prettierPluginTypescript from 'prettier/plugins/typescript';
|
|
8
|
+
import { VizFiles, VizFileId } from '@vizhub/viz-types';
|
|
9
|
+
|
|
10
|
+
// Parser mappings - matches client-side implementation
|
|
11
|
+
const parsers = {
|
|
12
|
+
js: 'babel',
|
|
13
|
+
jsx: 'babel',
|
|
14
|
+
mjs: 'babel',
|
|
15
|
+
cjs: 'babel',
|
|
16
|
+
ts: 'typescript',
|
|
17
|
+
tsx: 'typescript',
|
|
18
|
+
css: 'css',
|
|
19
|
+
html: 'html',
|
|
20
|
+
json: 'json',
|
|
21
|
+
json5: 'json5',
|
|
22
|
+
md: 'markdown',
|
|
23
|
+
markdown: 'markdown',
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
// Prettier plugins - matches client-side implementation
|
|
27
|
+
const plugins = [
|
|
28
|
+
prettierPluginBabel,
|
|
29
|
+
prettierPluginEstree,
|
|
30
|
+
prettierPluginHtml,
|
|
31
|
+
prettierPluginMarkdown,
|
|
32
|
+
prettierPluginTypescript,
|
|
33
|
+
prettierPluginCSS,
|
|
34
|
+
] as any;
|
|
35
|
+
|
|
36
|
+
// Prettier options - matches client-side implementation and .prettierrc
|
|
37
|
+
const prettierOptions = {
|
|
38
|
+
proseWrap: 'always' as const,
|
|
39
|
+
singleQuote: true,
|
|
40
|
+
printWidth: 60,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extracts file extension from filename
|
|
45
|
+
* Example: 'foo.js' => 'js'
|
|
46
|
+
*/
|
|
47
|
+
const getFileExtension = (fileName: string): string => {
|
|
48
|
+
const match = fileName.match(/\.([^.]+)$/);
|
|
49
|
+
return match ? match[1] : '';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Formats a single file using Prettier
|
|
54
|
+
*/
|
|
55
|
+
export const formatFile = async (
|
|
56
|
+
fileText: string,
|
|
57
|
+
fileName: string,
|
|
58
|
+
): Promise<string | null> => {
|
|
59
|
+
const fileExtension = getFileExtension(fileName);
|
|
60
|
+
const parser =
|
|
61
|
+
parsers[fileExtension as keyof typeof parsers];
|
|
62
|
+
|
|
63
|
+
// If no parser is found, return null (no formatting)
|
|
64
|
+
if (!parser) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const formatted = await format(fileText, {
|
|
70
|
+
parser,
|
|
71
|
+
plugins,
|
|
72
|
+
...prettierOptions,
|
|
73
|
+
});
|
|
74
|
+
return formatted;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Log error but don't throw - return original text
|
|
77
|
+
console.error(
|
|
78
|
+
`Prettier formatting error for ${fileName}:`,
|
|
79
|
+
error,
|
|
80
|
+
);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Formats multiple files using Prettier
|
|
87
|
+
* Only formats files that have supported extensions
|
|
88
|
+
* Returns a map of fileId -> formatted text for successfully formatted files
|
|
89
|
+
*/
|
|
90
|
+
export const formatFiles = async (
|
|
91
|
+
files: VizFiles,
|
|
92
|
+
fileIds?: VizFileId[],
|
|
93
|
+
): Promise<{ [fileId: VizFileId]: string }> => {
|
|
94
|
+
const results: { [fileId: VizFileId]: string } = {};
|
|
95
|
+
const targetFileIds = fileIds || Object.keys(files);
|
|
96
|
+
|
|
97
|
+
// Process files in parallel for better performance
|
|
98
|
+
const formatPromises = targetFileIds.map(
|
|
99
|
+
async (fileId) => {
|
|
100
|
+
const file = files[fileId];
|
|
101
|
+
if (!file) return;
|
|
102
|
+
|
|
103
|
+
const formatted = await formatFile(
|
|
104
|
+
file.text,
|
|
105
|
+
file.name,
|
|
106
|
+
);
|
|
107
|
+
if (formatted !== null && formatted !== file.text) {
|
|
108
|
+
results[fileId] = formatted;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await Promise.all(formatPromises);
|
|
114
|
+
return results;
|
|
115
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { diff } from './ot.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a submitOperation function that can be used to submit diff-based operations to ShareDB.
|
|
5
|
+
* This is the core logic extracted from useSubmitOperation for reuse across client and server.
|
|
6
|
+
*/
|
|
7
|
+
export const createSubmitOperation = <T>(shareDBDoc: {
|
|
8
|
+
data: T;
|
|
9
|
+
submitOp: (op: any) => void;
|
|
10
|
+
}): ((next: (data: T) => T) => void) => {
|
|
11
|
+
return (next) => {
|
|
12
|
+
const data: T = shareDBDoc.data;
|
|
13
|
+
const op = diff(data, next(data));
|
|
14
|
+
if (op && shareDBDoc) {
|
|
15
|
+
shareDBDoc.submitOp(op);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Type definition for the submitOperation function
|
|
22
|
+
*/
|
|
23
|
+
export type SubmitOperationFunction<T> = (
|
|
24
|
+
next: (data: T) => T,
|
|
25
|
+
) => void;
|