vzcode 1.12.0 → 1.13.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/dist/index.html CHANGED
@@ -20,8 +20,8 @@
20
20
  href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
21
21
  rel="stylesheet"
22
22
  />
23
- <script type="module" crossorigin src="/assets/index-4cYZN5Uf.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-BKRpkJER.css">
23
+ <script type="module" crossorigin src="/assets/index-BpeGnq7b.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-Wxh-Jaj6.css">
25
25
  </head>
26
26
  <body>
27
27
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -111,7 +111,7 @@
111
111
  "@codemirror/lang-markdown": "^6.2.5",
112
112
  "@codemirror/state": "^6.4.1",
113
113
  "@codemirror/theme-one-dark": "^6.1.2",
114
- "@codemirror/view": "^6.28.6",
114
+ "@codemirror/view": "^6.29.1",
115
115
  "@lezer/highlight": "^1.2.0",
116
116
  "@replit/codemirror-indentation-markers": "^6.5.3",
117
117
  "@replit/codemirror-interact": "^6.3.1",
@@ -140,14 +140,14 @@
140
140
  "json0-ot-diff": "^1.1.2",
141
141
  "ngrok": "^4.3.3",
142
142
  "open": "^10.1.0",
143
- "openai": "^4.52.7",
143
+ "openai": "^4.53.2",
144
144
  "prettier-plugin-svelte": "^3.2.6",
145
145
  "react": "^18.3.1",
146
146
  "react-bootstrap": "^2.10.4",
147
147
  "react-dom": "^18.3.1",
148
148
  "react-router-dom": "^6.25.1",
149
- "sharedb": "^5.0.2",
150
- "sharedb-client-browser": "^5.0.2",
149
+ "sharedb": "^5.0.3",
150
+ "sharedb-client-browser": "^5.0.3",
151
151
  "vizhub-ui": "^3.23.0",
152
152
  "ws": "^8.18.0"
153
153
  },
@@ -159,12 +159,12 @@
159
159
  "cross-env": "^7.0.3",
160
160
  "prettier": "^3.3.3",
161
161
  "sass": "^1.77.8",
162
- "typescript": "^5.5.3",
163
- "vite": "^5.3.4",
164
- "vitest": "^2.0.3"
162
+ "typescript": "^5.5.4",
163
+ "vite": "^5.3.5",
164
+ "vitest": "^2.0.4"
165
165
  },
166
166
  "optionalDependencies": {
167
- "@rollup/rollup-darwin-arm64": "^4.18.1",
168
- "@rollup/rollup-win32-x64-msvc": "^4.18.1"
167
+ "@rollup/rollup-darwin-arm64": "^4.19.1",
168
+ "@rollup/rollup-win32-x64-msvc": "^4.19.1"
169
169
  }
170
170
  }
@@ -18,6 +18,7 @@ import { Diagnostic, linter } from '@codemirror/lint';
18
18
  import { json1Presence, textUnicode } from '../../ot';
19
19
  import {
20
20
  FileId,
21
+ PaneId,
21
22
  ShareDBDoc,
22
23
  TabState,
23
24
  Username,
@@ -102,6 +103,7 @@ const getAtPath = (obj, path) => {
102
103
  // Gets or creates an `editorCache` entry for the given file id.
103
104
  // Looks in `editorCache` first, and if not found, creates a new editor.
104
105
  export const getOrCreateEditor = ({
106
+ paneId = 'root',
105
107
  fileId,
106
108
  shareDBDoc,
107
109
  content,
@@ -118,6 +120,8 @@ export const getOrCreateEditor = ({
118
120
  enableAutoFollowRef,
119
121
  openTab,
120
122
  }: {
123
+ // TODO pass this in from the outside
124
+ paneId?: PaneId;
121
125
  fileId: FileId;
122
126
 
123
127
  // The ShareDB document that contains the file.
@@ -153,8 +157,11 @@ export const getOrCreateEditor = ({
153
157
  openTab: (tabState: TabState) => void;
154
158
  }): EditorCacheValue => {
155
159
  // Cache hit
156
- if (editorCache.has(fileId)) {
157
- return editorCache.get(fileId);
160
+
161
+ const cacheKey = fileId + '|' + paneId;
162
+
163
+ if (editorCache.has(cacheKey)) {
164
+ return editorCache.get(cacheKey);
158
165
  }
159
166
 
160
167
  // Cache miss
@@ -385,7 +392,7 @@ export const getOrCreateEditor = ({
385
392
  const editorCacheValue = { editor, themeCompartment };
386
393
 
387
394
  // Populate the cache.
388
- editorCache.set(fileId, editorCacheValue);
395
+ editorCache.set(cacheKey, editorCacheValue);
389
396
 
390
397
  return editorCacheValue;
391
398
  };
@@ -99,6 +99,7 @@ export const CodeEditor = ({
99
99
  allowGlobals,
100
100
  enableAutoFollowRef,
101
101
  openTab,
102
+ // splitPaneView,
102
103
  }),
103
104
  [
104
105
  activeFileId,
@@ -121,6 +122,7 @@ export const CodeEditor = ({
121
122
  // Guard against cases where page is still loading.
122
123
  if (!codeEditorRef.current) return;
123
124
  if (!content) return;
125
+ // if (openSplitPane) return;
124
126
 
125
127
  // Add the editor and apply the prior scroll position to the DOM.
126
128
  codeEditorRef.current.appendChild(
@@ -0,0 +1,34 @@
1
+ export const SplitEditorSVG = () => {
2
+ return (
3
+ <svg
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width="24"
6
+ height="24"
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ >
10
+ <rect
11
+ x="5"
12
+ y="4"
13
+ width="7.5"
14
+ height="16"
15
+ rx="1"
16
+ ry="1"
17
+ stroke="currentColor"
18
+ strokeWidth="1.5"
19
+ fill="none"
20
+ />
21
+ <rect
22
+ x="12"
23
+ y="4"
24
+ width="7.5"
25
+ height="16"
26
+ rx="1"
27
+ ry="1"
28
+ stroke="currentColor"
29
+ strokeWidth="1.5"
30
+ fill="none"
31
+ />
32
+ </svg>
33
+ );
34
+ };
@@ -1,9 +1,14 @@
1
1
  import { useCallback, useContext, useState } from 'react';
2
2
  import { PlaySVG } from '../Icons';
3
+ import { SplitEditorSVG } from '../Icons/SplitEditorSVG';
3
4
  import { VZCodeContext } from '../VZCodeContext';
4
5
  import { OverlayTrigger, Tooltip } from '../bootstrap';
6
+
5
7
  import './style.scss';
6
8
 
9
+ // Feature flag for split pane feature (WIP)
10
+ const enableSplitPane = false;
11
+
7
12
  export const RunCodeWidget = ({
8
13
  runCodeWidgetTooltipText = (
9
14
  <>
@@ -14,7 +19,7 @@ export const RunCodeWidget = ({
14
19
  }: {
15
20
  runCodeWidgetTooltipText?: JSX.Element;
16
21
  }) => {
17
- const { runCodeRef, runPrettierRef } =
22
+ const { runCodeRef, runPrettierRef, splitCurrentPane } =
18
23
  useContext(VZCodeContext);
19
24
  const [isRunning, setIsRunning] = useState(false);
20
25
 
@@ -37,23 +42,50 @@ export const RunCodeWidget = ({
37
42
  setTimeout(() => setIsRunning(false), 1000);
38
43
  }, []);
39
44
 
45
+ const handleSplitEditor = useCallback(() => {
46
+ console.log('Split Editor');
47
+ console.log(splitCurrentPane);
48
+ splitCurrentPane();
49
+ }, [splitCurrentPane]);
50
+
40
51
  return (
41
- <div className="vz-code-run-code-widget">
42
- <OverlayTrigger
43
- placement="left"
44
- overlay={
45
- <Tooltip id="run-code-widget-tooltip">
46
- {runCodeWidgetTooltipText}
47
- </Tooltip>
48
- }
49
- >
50
- <i
51
- className={`icon-button icon-button-dark ${isRunning ? 'rotate-icon' : ''}`}
52
- onClick={handleClick}
52
+ <div>
53
+ <div className="vz-code-split-view-widget">
54
+ {enableSplitPane && (
55
+ <OverlayTrigger
56
+ placement="bottom"
57
+ overlay={
58
+ <Tooltip id="fork-file-tooltip">
59
+ Split Editor
60
+ </Tooltip>
61
+ }
62
+ >
63
+ <i
64
+ onClick={handleSplitEditor}
65
+ className="icon-button icon-button-dark"
66
+ >
67
+ <SplitEditorSVG />
68
+ </i>
69
+ </OverlayTrigger>
70
+ )}
71
+ </div>
72
+ <div className="vz-code-run-code-widget">
73
+ <OverlayTrigger
74
+ placement="bottom"
75
+ overlay={
76
+ <Tooltip id="run-code-widget-tooltip">
77
+ {runCodeWidgetTooltipText}
78
+ </Tooltip>
79
+ }
53
80
  >
54
- <PlaySVG />
55
- </i>
56
- </OverlayTrigger>
81
+ <i
82
+ className={`icon-button icon-button-dark ${isRunning ? 'rotate-icon' : ''}`}
83
+ onClick={handleClick}
84
+ >
85
+ <PlaySVG />
86
+ </i>
87
+ </OverlayTrigger>
88
+ </div>
57
89
  </div>
58
90
  );
59
91
  };
@@ -3,11 +3,16 @@
3
3
  right: 16px;
4
4
  top: 9px;
5
5
  color: var(--vh-color-neutral-04);
6
-
7
6
  .rotate-icon {
8
7
  animation: spin 1s linear;
9
8
  }
10
9
  }
10
+ .vz-code-split-view-widget {
11
+ position: absolute;
12
+ right: 51px;
13
+ top: 9px;
14
+ color: var(--vh-color-neutral-04);
15
+ }
11
16
 
12
17
  @keyframes spin {
13
18
  from {
@@ -15,6 +15,7 @@ import {
15
15
  SearchResults,
16
16
  SearchFileVisibility,
17
17
  TabState,
18
+ PresenceIndicator,
18
19
  } from '../types';
19
20
  import { usePrettier } from './usePrettier';
20
21
  import { useTypeScript } from './useTypeScript';
@@ -146,6 +147,13 @@ export type VZCodeContextValue = {
146
147
 
147
148
  enableAutoFollow: boolean;
148
149
  toggleAutoFollow: () => void;
150
+
151
+ updatePresenceIndicator: (
152
+ presenceIndicator: PresenceIndicator,
153
+ ) => void;
154
+
155
+ sidebarPresenceIndicators: Array<PresenceIndicator>;
156
+ splitCurrentPane: () => void;
149
157
  };
150
158
 
151
159
  export const VZCodeProvider = ({
@@ -223,6 +231,8 @@ export const VZCodeProvider = ({
223
231
  );
224
232
 
225
233
  // Unpack state.
234
+ // print the state object to see what it contains
235
+ // console.log('state: ', state);
226
236
  const {
227
237
  pane,
228
238
  theme,
@@ -233,6 +243,7 @@ export const VZCodeProvider = ({
233
243
  editorWantsFocus,
234
244
  username,
235
245
  enableAutoFollow,
246
+ sidebarPresenceIndicators,
236
247
  } = state;
237
248
 
238
249
  // TODO support splitPane type
@@ -262,6 +273,8 @@ export const VZCodeProvider = ({
262
273
  editorNoLongerWantsFocus,
263
274
  setUsername,
264
275
  toggleAutoFollow,
276
+ updatePresenceIndicator,
277
+ splitCurrentPane,
265
278
  } = useActions(dispatch);
266
279
 
267
280
  // Sync tab state to the URL.
@@ -439,6 +452,9 @@ export const VZCodeProvider = ({
439
452
 
440
453
  enableAutoFollow,
441
454
  toggleAutoFollow,
455
+ updatePresenceIndicator,
456
+ sidebarPresenceIndicators,
457
+ splitCurrentPane,
442
458
  };
443
459
 
444
460
  return (
@@ -23,6 +23,7 @@ export const DirectoryListing = ({
23
23
  deleteDirectory,
24
24
  isDirectoryOpen,
25
25
  toggleDirectory,
26
+ sidebarPresenceIndicators,
26
27
  } = useContext(VZCodeContext);
27
28
 
28
29
  const handleClick = useCallback(() => {
@@ -63,7 +64,7 @@ export const DirectoryListing = ({
63
64
  >
64
65
  <DirectoryArrowSVG />
65
66
  </div>
66
- {name}
67
+ <div className="name">{name}</div>
67
68
  </Item>
68
69
  {children && isOpen ? (
69
70
  <div className="indentation">
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useContext } from 'react';
2
2
  import { Item } from './Item';
3
- import { FileId } from '../../types';
3
+ import { FileId, PresenceIndicator } from '../../types';
4
4
  import {
5
5
  FileSVG,
6
6
  JavaScriptSVG,
@@ -46,12 +46,14 @@ export const FileListing = ({
46
46
  handleFileClick,
47
47
  handleFileDoubleClick,
48
48
  isActive,
49
+ presence,
49
50
  }: {
50
51
  name: string;
51
52
  fileId: FileId;
52
53
  handleFileClick: (fileId: FileId) => void;
53
54
  handleFileDoubleClick: (fileId: FileId) => void;
54
55
  isActive: boolean;
56
+ presence: PresenceIndicator[];
55
57
  }) => {
56
58
  const { renameFile, deleteFile } =
57
59
  useContext(VZCodeContext);
@@ -85,8 +87,26 @@ export const FileListing = ({
85
87
  handleRenameClick={handleRenameClick}
86
88
  isActive={isActive}
87
89
  >
88
- <i className="file-icon">{getExtensionIcon(name)}</i>
89
- {name}
90
+ <div className="name">
91
+ {/* Render presence indicators to the left of the file name */}
92
+ {presence.length > 0 && (
93
+ <div className="presence-indicators">
94
+ {presence.map((indicator, index) => (
95
+ <div
96
+ key={index}
97
+ className="presence-indicator"
98
+ >
99
+ {indicator.username[0]}{' '}
100
+ {/* Display the first letter of the username */}
101
+ </div>
102
+ ))}
103
+ </div>
104
+ )}
105
+ <i className="file-icon">
106
+ {getExtensionIcon(name)}
107
+ </i>
108
+ {name}
109
+ </div>
90
110
  </Item>
91
111
  );
92
112
  };
@@ -13,9 +13,9 @@ import { DeleteConfirmationModal } from './DeleteConfirmationModal';
13
13
  import { VZCodeContext } from '../VZCodeContext';
14
14
  import { ItemId } from '../../types';
15
15
  import { getExtensionIcon } from './FileListing';
16
+ import { SplitEditorSVG } from '../Icons/SplitEditorSVG';
17
+ import { SplitPaneResizeProvider } from '../SplitPaneResizeContext';
16
18
 
17
- // TODO support renaming directories
18
- // See https://github.com/vizhub-core/vzcode/issues/103
19
19
  const enableRenameDirectory = true;
20
20
 
21
21
  // A file or directory in the sidebar.
@@ -19,10 +19,16 @@ export const Listing = ({
19
19
  handleFileClick: (fileId: FileId) => void;
20
20
  handleFileDoubleClick: (fileId: FileId) => void;
21
21
  }) => {
22
- const { activeFileId } = useContext(VZCodeContext);
22
+ const { activeFileId, sidebarPresenceIndicators } =
23
+ useContext(VZCodeContext);
23
24
  const { name, file, fileId } = entity as FileTreeFile;
24
25
  const { path, children } = entity as FileTree;
25
26
 
27
+ const presence =
28
+ sidebarPresenceIndicators?.filter(
29
+ (indicator) => indicator.fileId === fileId,
30
+ ) || [];
31
+
26
32
  return file ? (
27
33
  <FileListing
28
34
  fileId={fileId}
@@ -30,6 +36,7 @@ export const Listing = ({
30
36
  handleFileClick={handleFileClick}
31
37
  handleFileDoubleClick={handleFileDoubleClick}
32
38
  isActive={fileId === activeFileId}
39
+ presence={presence}
33
40
  />
34
41
  ) : (
35
42
  <DirectoryListing
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  useCallback,
3
3
  useContext,
4
+ useEffect,
4
5
  useMemo,
5
6
  useState,
6
7
  } from 'react';
@@ -8,6 +9,9 @@ import {
8
9
  FileId,
9
10
  FileTree,
10
11
  FileTreeFile,
12
+ Presence,
13
+ PresenceId,
14
+ PresenceIndicator,
11
15
  } from '../../types';
12
16
  import { Tooltip, OverlayTrigger } from '../bootstrap';
13
17
  import { Search } from './Search';
@@ -117,6 +121,9 @@ export const VZSidebar = ({
117
121
  sidebarRef,
118
122
  enableAutoFollow,
119
123
  toggleAutoFollow,
124
+ docPresence,
125
+ updatePresenceIndicator,
126
+ sidebarPresenceIndicators,
120
127
  } = useContext(VZCodeContext);
121
128
 
122
129
  const fileTree = useMemo(
@@ -164,6 +171,42 @@ export const VZSidebar = ({
164
171
  handleDrop,
165
172
  } = useDragAndDrop();
166
173
 
174
+ // Track presence of remote users across files
175
+ // so that they can be displayed in the sidebar.
176
+ useEffect(() => {
177
+ docPresence;
178
+ }, []);
179
+
180
+ // Track the presence indicators for display in sidebar
181
+ useEffect(() => {
182
+ if (docPresence) {
183
+ const handleReceive = (
184
+ presenceId: PresenceId,
185
+ update: Presence,
186
+ ) => {
187
+ const presenceIndicator: PresenceIndicator = {
188
+ username: update.username,
189
+ fileId: update.start[1] as FileId,
190
+ };
191
+
192
+ console.log('Got presence!');
193
+ // console.log({presenceId,update})
194
+ console.log(
195
+ JSON.stringify(presenceIndicator, null, 2),
196
+ );
197
+
198
+ updatePresenceIndicator(presenceIndicator);
199
+ };
200
+
201
+ docPresence.on('receive', handleReceive);
202
+ return () => {
203
+ docPresence.off('receive', handleReceive);
204
+ };
205
+ }
206
+ }, [docPresence]);
207
+
208
+ console.log(sidebarPresenceIndicators);
209
+
167
210
  return (
168
211
  <div
169
212
  className="vz-sidebar"
@@ -253,4 +253,29 @@
253
253
  }
254
254
  }
255
255
  }
256
+ .name {
257
+ display: flex;
258
+ align-items: center;
259
+ white-space: nowrap;
260
+ overflow: hidden;
261
+ gap: 6px;
262
+ }
263
+
264
+ .presence-indicators {
265
+ display: flex;
266
+ gap: 4px;
267
+ margin-right: 6px; /* Space between the indicators and the file name */
268
+ }
269
+
270
+ .presence-indicator {
271
+ width: 16px;
272
+ height: 16px;
273
+ border-radius: 50%;
274
+ display: flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ font-size: 10px;
278
+ background-color: #ff5733;
279
+ color: #ffffff;
280
+ }
256
281
  }
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
2
2
  import { ThemeLabel } from './themes';
3
3
  import {
4
4
  FileId,
5
+ PresenceIndicator,
5
6
  SearchFileVisibility,
6
7
  ShareDBDoc,
7
8
  TabState,
@@ -172,6 +173,22 @@ export const useActions = (
172
173
  });
173
174
  }, [dispatch]);
174
175
 
176
+ const updatePresenceIndicator = useCallback(
177
+ (presenceIndicator: PresenceIndicator) => {
178
+ dispatch({
179
+ type: 'update_presence_indicator',
180
+ presenceIndicator,
181
+ });
182
+ },
183
+ [dispatch],
184
+ );
185
+
186
+ const splitCurrentPane = useCallback(() => {
187
+ dispatch({
188
+ type: 'split_current_pane',
189
+ });
190
+ }, [dispatch]);
191
+
175
192
  return {
176
193
  setActiveFileId,
177
194
  setActiveFileLeft,
@@ -191,5 +208,7 @@ export const useActions = (
191
208
  editorNoLongerWantsFocus,
192
209
  setUsername,
193
210
  toggleAutoFollow,
211
+ updatePresenceIndicator,
212
+ splitCurrentPane,
194
213
  };
195
214
  };
@@ -24,4 +24,5 @@ export const createInitialState = ({
24
24
  editorWantsFocus: false,
25
25
  username: initialUsername,
26
26
  enableAutoFollow: false,
27
+ sidebarPresenceIndicators: [],
27
28
  });
@@ -2,6 +2,7 @@ import {
2
2
  FileId,
3
3
  Pane,
4
4
  PaneId,
5
+ PresenceIndicator,
5
6
  SearchFileVisibility,
6
7
  SearchResults,
7
8
  ShareDBDoc,
@@ -29,6 +30,8 @@ import {
29
30
  toggleSearchFocusedReducer,
30
31
  } from './searchReducer';
31
32
  import { toggleAutoFollowReducer } from './toggleAutoFollowReducer';
33
+ import { updatePresenceIndicatorReducer } from './updatePresenceIndicatorReducer';
34
+ import { splitCurrentPaneReducer } from './splitCurrentPaneReducer';
32
35
  export { createInitialState } from './createInitialState';
33
36
 
34
37
  // The shape of the state managed by the reducer.
@@ -63,6 +66,10 @@ export type VZState = {
63
66
  // This is a feature that automatically scrolls the editor to the
64
67
  // currently active remote user's cursor position.
65
68
  enableAutoFollow: boolean;
69
+
70
+ // The list of remote users and which file they have open,
71
+ // for presence display in the sidebar.
72
+ sidebarPresenceIndicators: Array<PresenceIndicator>;
66
73
  };
67
74
 
68
75
  // The shape of the actions that can be dispatched to the reducer.
@@ -139,9 +146,19 @@ export type VZAction =
139
146
  // * Sets the username.
140
147
  | { type: 'set_username'; username: Username }
141
148
 
142
- // `toggle_auto_follow
149
+ // `toggle_auto_follow`
143
150
  // * Toggles the auto-follow feature.
144
- | { type: 'toggle_auto_follow' };
151
+ | { type: 'toggle_auto_follow' }
152
+
153
+ // `update_presence_indicator
154
+ // * Updates a single presence indicator
155
+ | {
156
+ type: 'update_presence_indicator';
157
+ presenceIndicator: PresenceIndicator;
158
+ }
159
+
160
+ // `split_current_pane`
161
+ | { type: 'split_current_pane' };
145
162
 
146
163
  const reducers = [
147
164
  setActiveFileIdReducer,
@@ -160,6 +177,8 @@ const reducers = [
160
177
  editorNoLongerWantsFocusReducer,
161
178
  setUsernameReducer,
162
179
  toggleAutoFollowReducer,
180
+ updatePresenceIndicatorReducer,
181
+ splitCurrentPaneReducer,
163
182
  ];
164
183
 
165
184
  export const vzReducer = (