vzcode 0.25.0 → 0.27.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 (55) hide show
  1. package/dist/assets/index-5f9f376e.css +1 -0
  2. package/dist/assets/index-70b6d947.js +111 -0
  3. package/dist/index.html +2 -6
  4. package/package.json +7 -7
  5. package/src/client/App.tsx +110 -247
  6. package/src/client/CodeEditor/getOrCreateEditor.ts +6 -1
  7. package/src/client/CodeEditor/index.tsx +94 -16
  8. package/src/client/CodeEditor/{json1PresenceBroadcast.js → json1PresenceBroadcast.ts} +9 -3
  9. package/src/client/CodeEditor/{json1PresenceDisplay.js → json1PresenceDisplay.ts} +9 -48
  10. package/src/client/CodeEditor/style.scss +5 -1
  11. package/src/client/CodeErrorOverlay/index.tsx +27 -0
  12. package/src/client/CodeErrorOverlay/style.scss +30 -0
  13. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  14. package/src/client/PresenceNotifications/index.tsx +119 -106
  15. package/src/client/SplitPaneResizeContext.tsx +12 -7
  16. package/src/client/TabList/Tab.tsx +70 -0
  17. package/src/client/TabList/index.tsx +28 -69
  18. package/src/client/TabList/style.scss +26 -9
  19. package/src/client/{Settings.tsx → VZSettings.tsx} +34 -21
  20. package/src/client/{Sidebar/ConfirmationBox.tsx → VZSidebar/DeleteConfirmationModal.tsx} +17 -14
  21. package/src/client/{Sidebar → VZSidebar}/DirectoryListing.tsx +27 -26
  22. package/src/client/{Sidebar → VZSidebar}/FileListing.tsx +19 -19
  23. package/src/client/VZSidebar/Item.tsx +179 -0
  24. package/src/client/{Sidebar → VZSidebar}/Listing.tsx +20 -14
  25. package/src/client/{Sidebar → VZSidebar}/index.tsx +48 -30
  26. package/src/client/{Sidebar → VZSidebar}/styles.scss +17 -12
  27. package/src/client/bootstrap.ts +3 -0
  28. package/src/client/style.scss +8 -2
  29. package/src/client/useActions.ts +93 -0
  30. package/src/client/useEditorCache.ts +2 -1
  31. package/src/client/useFileCRUD.ts +103 -0
  32. package/src/client/useOpenDirectories.ts +25 -7
  33. package/src/client/useSubmitOperation.ts +25 -0
  34. package/src/client/usernameLocalStorage.ts +43 -0
  35. package/src/client/vzReducer/closeTabReducer.test.ts +177 -0
  36. package/src/client/vzReducer/closeTabsReducer.ts +68 -0
  37. package/src/client/vzReducer/createInitialState.ts +19 -0
  38. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  39. package/src/client/vzReducer/index.ts +107 -0
  40. package/src/client/vzReducer/openTabReducer.test.ts +110 -0
  41. package/src/client/vzReducer/openTabReducer.ts +57 -0
  42. package/src/client/vzReducer/setActiveFileIdReducer.ts +9 -0
  43. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  44. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  45. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  46. package/src/index.ts +40 -0
  47. package/src/server/index.js +13 -2
  48. package/src/types.ts +12 -5
  49. package/dist/assets/index-8628ebab.js +0 -111
  50. package/dist/assets/index-cf19de3b.css +0 -1
  51. package/src/client/PrettierErrorOverlay/index.tsx +0 -9
  52. package/src/client/PrettierErrorOverlay/style.scss +0 -9
  53. package/src/client/Sidebar/Item.tsx +0 -130
  54. package/src/client/reducer.ts +0 -119
  55. /package/src/client/{Sidebar → VZSidebar}/DirectoryArrowSVG.jsx +0 -0
package/dist/index.html CHANGED
@@ -24,12 +24,8 @@
24
24
  href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css"
25
25
  rel="stylesheet"
26
26
  />
27
- <link
28
- href="https://unpkg.com/bootstrap@5.3.0/dist/css/bootstrap.min.css"
29
- rel="stylesheet"
30
- />
31
- <script type="module" crossorigin src="/assets/index-8628ebab.js"></script>
32
- <link rel="stylesheet" href="/assets/index-cf19de3b.css">
27
+ <script type="module" crossorigin src="/assets/index-70b6d947.js"></script>
28
+ <link rel="stylesheet" href="/assets/index-5f9f376e.css">
33
29
  </head>
34
30
  <body>
35
31
  <div id="root"></div>
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Multiplayer code editor system",
5
- "main": "index.js",
5
+ "main": "src/index.ts",
6
6
  "type": "module",
7
- "types": "src/types.ts",
8
7
  "bin": "src/server/index.js",
9
8
  "files": [
10
9
  "dist/**",
@@ -52,7 +51,7 @@
52
51
  "@codemirror/lang-markdown": "^6.2.2",
53
52
  "@codemirror/state": "^6.3.1",
54
53
  "@codemirror/theme-one-dark": "^6.1.2",
55
- "@codemirror/view": "^6.21.3",
54
+ "@codemirror/view": "^6.21.4",
56
55
  "@lezer/highlight": "^1.1.6",
57
56
  "@replit/codemirror-interact": "^6.3.0",
58
57
  "@teamwork/websocket-json-stream": "^2.0.0",
@@ -76,18 +75,19 @@
76
75
  "open": "^9.1.0",
77
76
  "ot-json1": "^1.0.2",
78
77
  "react": "^18.2.0",
79
- "react-bootstrap": "^2.9.0",
78
+ "react-bootstrap": "^2.9.1",
80
79
  "react-dom": "^18.2.0",
81
80
  "sharedb": "^4.1.1",
82
81
  "sharedb-client-browser": "^4.3.1",
82
+ "vizhub-ui": "^3.5.0",
83
83
  "ws": "^8.14.2"
84
84
  },
85
85
  "devDependencies": {
86
- "@types/react": "^18.2.29",
86
+ "@types/react": "^18.2.33",
87
87
  "@types/react-dom": "^18.2.14",
88
88
  "@vitejs/plugin-react": "^4.1.0",
89
89
  "prettier": "^3.0.3",
90
- "sass": "^1.69.4",
90
+ "sass": "^1.69.5",
91
91
  "typescript": "^5.2.2",
92
92
  "vite": "^4.5.0",
93
93
  "vitest": "^0.34.6"
@@ -1,40 +1,39 @@
1
- import {
2
- useState,
3
- useEffect,
4
- useCallback,
5
- useRef,
6
- useReducer,
7
- } from 'react';
1
+ import { useState, useEffect, useReducer } from 'react';
8
2
  import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
9
3
  import { json1Presence } from '../ot';
10
4
  import { randomId } from '../randomId';
11
5
  import { CodeEditor } from './CodeEditor';
12
- import { diff } from './diff';
13
- import { Settings } from './Settings';
14
- import { Sidebar } from './Sidebar';
6
+ import { VZSettings } from './VZSettings';
7
+ import { VZSidebar } from './VZSidebar';
15
8
  import {
16
- FileId,
17
9
  Files,
18
10
  ShareDBDoc,
11
+ Username,
19
12
  VZCodeContent,
20
13
  } from '../types';
21
14
  import { TabList } from './TabList';
22
15
  import { useOpenDirectories } from './useOpenDirectories';
16
+ import { defaultTheme, useDynamicTheme } from './themes';
23
17
  import {
24
- ThemeLabel,
25
- defaultTheme,
26
- useDynamicTheme,
27
- } from './themes';
28
- import { useEditorCache } from './useEditorCache';
18
+ EditorCache,
19
+ useEditorCache,
20
+ } from './useEditorCache';
29
21
  import { usePrettier } from './usePrettier';
30
22
  // @ts-ignore
31
23
  import PrettierWorker from './usePrettier/worker?worker';
32
24
  import { SplitPaneResizeProvider } from './SplitPaneResizeContext';
33
25
  import { Resizer } from './Resizer';
34
26
  import { PresenceNotifications } from './PresenceNotifications';
35
- import { reducer } from './reducer';
27
+ import { CodeErrorOverlay } from './CodeErrorOverlay';
28
+ import { vzReducer, createInitialState } from './vzReducer';
29
+ import { useActions } from './useActions';
30
+ import { useFileCRUD } from './useFileCRUD';
31
+ import { useSubmitOperation } from './useSubmitOperation';
32
+ import {
33
+ useInitialUsername,
34
+ usePersistUsername,
35
+ } from './usernameLocalStorage';
36
36
  import './style.scss';
37
- import { PrettierErrorOverlay } from './PrettierErrorOverlay';
38
37
 
39
38
  // Instantiate the Prettier worker.
40
39
  const prettierWorker = new PrettierWorker();
@@ -60,30 +59,6 @@ function App() {
60
59
  const [shareDBDoc, setShareDBDoc] =
61
60
  useState<ShareDBDoc<VZCodeContent> | null>(null);
62
61
 
63
- // A helper function to submit operations to the ShareDB document
64
-
65
- const submitOperation: (
66
- next: (content: VZCodeContent) => VZCodeContent,
67
- ) => void = useCallback(
68
- (next) => {
69
- const content: VZCodeContent = shareDBDoc.data;
70
- const op = diff(content, next(content));
71
- if (op && shareDBDoc) {
72
- shareDBDoc.submitOp(op);
73
- }
74
- },
75
- [shareDBDoc],
76
- );
77
-
78
- // Auto-run Pretter after local changes.
79
- const { prettierError } = usePrettier(
80
- shareDBDoc,
81
- submitOperation,
82
- prettierWorker,
83
- );
84
-
85
- console.log('prettierError', prettierError);
86
-
87
62
  // Local ShareDB presence, for broadcasting our cursor position
88
63
  // so other clients can see it.
89
64
  // See https://share.github.io/sharedb/api/local-presence
@@ -125,6 +100,12 @@ function App() {
125
100
  // This decouples rendering logic from ShareDB.
126
101
  // This callback gets called on each change.
127
102
  shareDBDoc.on('op', () => {
103
+ // TODO consider excluding file contents from this,
104
+ // because currently we are re-rendering the entire
105
+ // document on each file change (each keystroke while editing).
106
+ // This is not required, because no part of the app outside
107
+ // of the CodeMirror integration uses those file contents.
108
+ // The Sidebar only needs to know file names, not contents.
128
109
  setContent(shareDBDoc.data);
129
110
  });
130
111
 
@@ -141,7 +122,14 @@ function App() {
141
122
  });
142
123
 
143
124
  // Set up our local presence for broadcasting this client's presence.
144
- setLocalPresence(docPresence.create(randomId()));
125
+ const generateTimestampedId = () => {
126
+ const timestamp = Date.now().toString(36);
127
+ const randomPart = randomId();
128
+ return `${timestamp}-${randomPart}`;
129
+ };
130
+ setLocalPresence(
131
+ docPresence.create(generateTimestampedId()),
132
+ );
145
133
 
146
134
  // Store docPresence so child components can listen for changes.
147
135
  setDocPresence(docPresence);
@@ -155,212 +143,77 @@ function App() {
155
143
  };
156
144
  }, []);
157
145
 
158
- // https://react.dev/reference/react/useReducer
159
- const [state, dispatch] = useReducer(reducer, {
160
- tabList: [],
161
- activeFileId: null,
162
- theme: defaultTheme,
163
- isSettingsOpen: false,
164
- });
146
+ // Get the initial username from localStorage.
147
+ const initialUsername: Username = useInitialUsername();
165
148
 
166
- //Reducer states
167
- const tabList = state.tabList;
168
- const activeFileId = state.activeFileId;
169
- const theme = state.theme;
170
- const isSettingsOpen = state.isSettingsOpen;
171
- // TODO phase this out as we complete the refactoring
172
- // It's here now for backwards compatibility
173
-
174
- const setTabList = useCallback(
175
- (tabList) => {
176
- dispatch({
177
- type: 'set_tab_list',
178
- tabList,
179
- });
180
- },
181
- [dispatch],
182
- );
183
-
184
- const setActiveFileId = useCallback(
185
- (activeFileId) => {
186
- dispatch({
187
- type: 'set_active_fileId',
188
- activeFileId,
189
- });
190
- },
191
- [dispatch],
192
- );
193
-
194
- const openTab = useCallback(
195
- (fileId: FileId) => {
196
- dispatch({
197
- type: 'open_tab',
198
- fileId,
199
- });
200
- },
201
- [dispatch],
202
- );
203
-
204
- const closeTab = useCallback(
205
- (fileIdToRemove: FileId) => {
206
- dispatch({
207
- type: 'close_tab',
208
- fileIdToRemove,
209
- });
210
- },
211
- [dispatch],
212
- );
213
-
214
- const multiCloseTab = useCallback(
215
- (idsToDelete: Array<FileId>) => {
216
- dispatch({
217
- type: 'multi_close_tab',
218
- idsToDelete,
219
- });
220
- },
221
- [dispatch],
222
- );
149
+ const submitOperation: (
150
+ next: (content: VZCodeContent) => VZCodeContent,
151
+ ) => void = useSubmitOperation(shareDBDoc);
223
152
 
224
- const setTheme = useCallback(
225
- (themeLabel: ThemeLabel) => {
226
- dispatch({
227
- type: 'set_Theme',
228
- themeLabel: themeLabel,
229
- });
230
- },
231
- [dispatch],
153
+ // Auto-run Pretter after local changes.
154
+ const {
155
+ prettierError,
156
+ }: {
157
+ prettierError: string | null;
158
+ } = usePrettier(
159
+ shareDBDoc,
160
+ submitOperation,
161
+ prettierWorker,
232
162
  );
233
163
 
234
- // True to show the settings modal.
235
- const setIsSettingsOpen = useCallback(
236
- (value: boolean) => {
237
- dispatch({
238
- type: 'set_Is_Settings_Open',
239
- value: value,
240
- });
241
- },
242
- [dispatch],
164
+ // https://react.dev/reference/react/useReducer
165
+ const [state, dispatch] = useReducer(
166
+ vzReducer,
167
+ { defaultTheme, initialUsername },
168
+ createInitialState,
243
169
  );
244
170
 
245
- // Cache of CodeMirror editors by file id.
246
- const editorCache = useEditorCache();
247
-
248
- // Handle dynamic theme changes.
249
- useDynamicTheme(editorCache, theme);
171
+ // Unpack state.
172
+ const {
173
+ tabList,
174
+ activeFileId,
175
+ theme,
176
+ isSettingsOpen,
177
+ editorWantsFocus,
178
+ username,
179
+ } = state;
180
+
181
+ // Functions for dispatching actions to the reducer.
182
+ const {
183
+ setActiveFileId,
184
+ openTab,
185
+ closeTabs,
186
+ setTheme,
187
+ setIsSettingsOpen,
188
+ closeSettings,
189
+ editorNoLongerWantsFocus,
190
+ setUsername,
191
+ } = useActions(dispatch);
250
192
 
251
193
  // The set of open directories.
194
+ // TODO move this into reducer/useActions
252
195
  const { isDirectoryOpen, toggleDirectory } =
253
196
  useOpenDirectories();
254
197
 
255
- // TODO move this logic to a hook called `useFileCRUD`
256
- // Include
257
- // - `createFile`
258
- // - `handleRenameFileClick`
259
- // - `deleteFile`
260
- // - `handleDeleteFileClick`
261
- const createFile = useCallback(() => {
262
- const name = prompt('Enter new file name');
263
- if (name) {
264
- submitOperation((document) => ({
265
- ...document,
266
- files: {
267
- ...document.files,
268
- [randomId()]: { name, text: '' },
269
- },
270
- }));
271
- }
272
- }, [submitOperation]);
273
-
274
- // Called when a file in the sidebar is renamed.
275
- const handleRenameFileClick = useCallback(
276
- (fileId: FileId, newName: string) => {
277
- submitOperation((document) => ({
278
- ...document,
279
- files: {
280
- ...document.files,
281
- [fileId]: {
282
- ...document.files[fileId],
283
- name: newName,
284
- },
285
- },
286
- }));
287
- },
288
- [submitOperation],
289
- );
290
-
291
- const deleteFile = useCallback(
292
- (fileId: FileId) => {
293
- submitOperation((document) => {
294
- const updatedFiles = { ...document.files };
295
- delete updatedFiles[fileId];
296
- return { ...document, files: updatedFiles };
297
- });
298
- closeTab(fileId);
299
- },
300
- [submitOperation, closeTab],
301
- );
198
+ usePersistUsername(username);
302
199
 
303
- const deleteDirectory = useCallback(
304
- (path: FileId) => {
305
- let tabsToDelete: Array<FileId> = [];
306
- submitOperation((document) => {
307
- const updatedFiles = { ...document.files };
308
- for (const key in updatedFiles) {
309
- if (updatedFiles[key].name.includes(path)) {
310
- tabsToDelete.push(key);
311
- delete updatedFiles[key];
312
- }
313
- }
314
- return { ...document, files: updatedFiles };
315
- });
316
- multiCloseTab(tabsToDelete);
317
- },
318
- [submitOperation, multiCloseTab],
319
- );
320
-
321
- const handleDeleteClick = useCallback(
322
- (key: string) => {
323
- //Regex to identify if the key is a file path or a file id.
324
- if (/^[0-9]*$/.test(key)) {
325
- if (key.length == 8) {
326
- deleteFile(key);
327
- } else {
328
- deleteDirectory(key);
329
- }
330
- } else {
331
- deleteDirectory(key);
332
- }
333
- },
334
- [deleteFile, deleteDirectory],
335
- );
336
-
337
- const handleSettingsClose = useCallback(() => {
338
- setIsSettingsOpen(false);
339
- }, []);
200
+ // Cache of CodeMirror editors by file id.
201
+ const editorCache: EditorCache = useEditorCache();
340
202
 
341
- // Set `doc.data.isInteracting` to `true` when the user is interacting
342
- // via interactive code widgets (e.g. Alt+drag), and `false` when they are not.
343
- const interactTimeoutRef = useRef(null);
344
- const handleInteract = useCallback(() => {
345
- // Set `isInteracting: true` if not already set.
346
- if (!interactTimeoutRef.current) {
347
- submitOperation((document) => ({
348
- ...document,
349
- isInteracting: true,
350
- }));
351
- } else {
352
- clearTimeout(interactTimeoutRef.current);
353
- }
203
+ // Handle dynamic theme changes.
204
+ useDynamicTheme(editorCache, theme);
354
205
 
355
- // Set `isInteracting: false` after a delay.
356
- interactTimeoutRef.current = setTimeout(() => {
357
- interactTimeoutRef.current = null;
358
- submitOperation((document) => ({
359
- ...document,
360
- isInteracting: false,
361
- }));
362
- }, 800);
363
- }, [submitOperation]);
206
+ // Handle file CRUD operations.
207
+ const {
208
+ createFile,
209
+ renameFile,
210
+ deleteFile,
211
+ deleteDirectory,
212
+ } = useFileCRUD({
213
+ submitOperation,
214
+ closeTabs,
215
+ openTab,
216
+ });
364
217
 
365
218
  // Isolate the files object from the document.
366
219
  const files: Files | null = content
@@ -371,21 +224,25 @@ function App() {
371
224
  <SplitPaneResizeProvider>
372
225
  <div className="app">
373
226
  <div className="left">
374
- <Sidebar
375
- createFile={createFile}
227
+ <VZSidebar
376
228
  files={files}
377
- handleRenameFileClick={handleRenameFileClick}
378
- handleDeleteFileClick={handleDeleteClick}
379
- handleFileClick={openTab}
229
+ createFile={createFile}
230
+ renameFile={renameFile}
231
+ deleteFile={deleteFile}
232
+ deleteDirectory={deleteDirectory}
233
+ openTab={openTab}
380
234
  setIsSettingsOpen={setIsSettingsOpen}
381
235
  isDirectoryOpen={isDirectoryOpen}
382
236
  toggleDirectory={toggleDirectory}
237
+ activeFileId={activeFileId}
383
238
  />
384
- <Settings
239
+ <VZSettings
385
240
  show={isSettingsOpen}
386
- onClose={handleSettingsClose}
241
+ onClose={closeSettings}
387
242
  theme={theme}
388
243
  setTheme={setTheme}
244
+ username={username}
245
+ setUsername={setUsername}
389
246
  />
390
247
  </div>
391
248
  <div className="right">
@@ -394,24 +251,30 @@ function App() {
394
251
  tabList={tabList}
395
252
  activeFileId={activeFileId}
396
253
  setActiveFileId={setActiveFileId}
397
- closeTab={closeTab}
254
+ openTab={openTab}
255
+ closeTabs={closeTabs}
256
+ createFile={createFile}
398
257
  />
399
258
  {content && activeFileId ? (
400
259
  <CodeEditor
401
260
  shareDBDoc={shareDBDoc}
261
+ submitOperation={submitOperation}
402
262
  localPresence={localPresence}
403
263
  docPresence={docPresence}
404
264
  activeFileId={activeFileId}
405
265
  theme={theme}
406
- onInteract={handleInteract}
407
266
  editorCache={editorCache}
267
+ editorWantsFocus={editorWantsFocus}
268
+ editorNoLongerWantsFocus={
269
+ editorNoLongerWantsFocus
270
+ }
271
+ username={username}
408
272
  />
409
273
  ) : null}
410
- <PrettierErrorOverlay
411
- prettierError={prettierError}
412
- />
274
+ <CodeErrorOverlay errorMessage={prettierError} />
413
275
  <PresenceNotifications
414
276
  docPresence={docPresence}
277
+ localPresence={localPresence}
415
278
  />
416
279
  </div>
417
280
  <Resizer />
@@ -12,6 +12,7 @@ import { json1Presence, textUnicode } from '../../ot';
12
12
  import {
13
13
  FileId,
14
14
  ShareDBDoc,
15
+ Username,
15
16
  VZCodeContent,
16
17
  } from '../../types';
17
18
  import { json1PresenceBroadcast } from './json1PresenceBroadcast';
@@ -64,6 +65,7 @@ export const getOrCreateEditor = ({
64
65
  theme,
65
66
  onInteract,
66
67
  editorCache,
68
+ usernameRef,
67
69
  }: {
68
70
  fileId: FileId;
69
71
 
@@ -82,6 +84,7 @@ export const getOrCreateEditor = ({
82
84
  theme: ThemeLabel;
83
85
  onInteract?: () => void;
84
86
  editorCache: EditorCache;
87
+ usernameRef: React.MutableRefObject<Username>;
85
88
  }): EditorCacheValue => {
86
89
  // Cache hit
87
90
  if (editorCache.has(fileId)) {
@@ -118,13 +121,15 @@ export const getOrCreateEditor = ({
118
121
  );
119
122
 
120
123
  // Deals with broadcasting changes in cursor location and selection.
121
- if (localPresence)
124
+ if (localPresence) {
122
125
  extensions.push(
123
126
  json1PresenceBroadcast({
124
127
  path: textPath,
125
128
  localPresence,
129
+ usernameRef,
126
130
  }),
127
131
  );
132
+ }
128
133
 
129
134
  // Deals with receiving the broadcas from other clients and displaying them.
130
135
  if (docPresence)