vzcode 1.10.0 → 1.12.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 (35) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{index-DXNuoOqQ.js → index-4cYZN5Uf.js} +57 -57
  3. package/dist/assets/{index--bvX81xc.js → index-CvSVuMne.js} +57 -57
  4. package/dist/assets/worker-CJWrLBN2.js +286 -0
  5. package/dist/index.html +1 -1
  6. package/package.json +13 -12
  7. package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +5 -2
  8. package/src/client/AIAssist/startAIAssist.ts +1 -1
  9. package/src/client/CodeEditor/getOrCreateEditor.ts +4 -0
  10. package/src/client/CodeEditor/index.tsx +2 -0
  11. package/src/client/CodeEditor/json1PresenceDisplay.ts +128 -90
  12. package/src/client/TabList/Tab.tsx +1 -2
  13. package/src/client/TabList/index.tsx +1 -1
  14. package/src/client/VZCodeContext.tsx +17 -14
  15. package/src/client/VZKeyboardShortcutsDoc.tsx +2 -1
  16. package/src/client/VZSidebar/Search.tsx +7 -2
  17. package/src/client/VZSidebar/index.tsx +87 -24
  18. package/src/client/tabsSearchParameters.ts +1 -2
  19. package/src/client/useActions.ts +10 -1
  20. package/src/client/useKeyboardShortcuts.ts +31 -1
  21. package/src/client/useURLSync.ts +1 -2
  22. package/src/client/vzReducer/closeTabReducer.test.ts +49 -33
  23. package/src/client/vzReducer/closeTabsReducer.ts +51 -19
  24. package/src/client/vzReducer/createInitialState.ts +8 -3
  25. package/src/client/vzReducer/findPane.ts +19 -0
  26. package/src/client/vzReducer/index.ts +17 -25
  27. package/src/client/vzReducer/openTabReducer.test.ts +38 -21
  28. package/src/client/vzReducer/openTabReducer.ts +26 -10
  29. package/src/client/vzReducer/searchReducer.ts +21 -1
  30. package/src/client/vzReducer/setActiveFileIdReducer.ts +23 -4
  31. package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +37 -8
  32. package/src/client/vzReducer/updatePane.ts +44 -0
  33. package/src/server/index.js +1 -0
  34. package/src/types.ts +69 -70
  35. package/dist/assets/worker-Cs0ZU3m1.js +0 -291
package/dist/index.html CHANGED
@@ -20,7 +20,7 @@
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-DXNuoOqQ.js"></script>
23
+ <script type="module" crossorigin src="/assets/index-4cYZN5Uf.js"></script>
24
24
  <link rel="stylesheet" crossorigin href="/assets/index-BKRpkJER.css">
25
25
  </head>
26
26
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -14,7 +14,7 @@
14
14
  "test-interactive": "cd test/sampleDirectories/kitchenSink && node ../../../src/server/index.js",
15
15
  "prettier": "prettier {*.*,**/*.*} --write",
16
16
  "tsc": "tsc --noEmit",
17
- "dev": "set EDITOR_PORT=5173 && concurrently \"npm run test-interactive\" \"vite\"",
17
+ "dev": "cross-env EDITOR_PORT=5173 concurrently \"npm run test-interactive\" \"vite\"",
18
18
  "build": "vite build",
19
19
  "build-release": "vite build --mode release",
20
20
  "preview": "concurrently \"npm run test-interactive\" \"vite preview\"",
@@ -111,14 +111,14 @@
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.4",
114
+ "@codemirror/view": "^6.28.6",
115
115
  "@lezer/highlight": "^1.2.0",
116
- "@replit/codemirror-indentation-markers": "^6.5.2",
116
+ "@replit/codemirror-indentation-markers": "^6.5.3",
117
117
  "@replit/codemirror-interact": "^6.3.1",
118
118
  "@replit/codemirror-lang-svelte": "^6.0.0",
119
119
  "@replit/codemirror-vscode-keymap": "^6.0.2",
120
120
  "@teamwork/websocket-json-stream": "^2.0.0",
121
- "@typescript/vfs": "^1.5.3",
121
+ "@typescript/vfs": "^1.6.0",
122
122
  "@uiw/codemirror-theme-abcdef": "^4.23.0",
123
123
  "@uiw/codemirror-theme-dracula": "^4.23.0",
124
124
  "@uiw/codemirror-theme-eclipse": "^4.23.0",
@@ -140,12 +140,12 @@
140
140
  "json0-ot-diff": "^1.1.2",
141
141
  "ngrok": "^4.3.3",
142
142
  "open": "^10.1.0",
143
- "openai": "^4.52.4",
144
- "prettier-plugin-svelte": "^3.2.5",
143
+ "openai": "^4.52.7",
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
- "react-router-dom": "^6.24.1",
148
+ "react-router-dom": "^6.25.1",
149
149
  "sharedb": "^5.0.2",
150
150
  "sharedb-client-browser": "^5.0.2",
151
151
  "vizhub-ui": "^3.23.0",
@@ -156,11 +156,12 @@
156
156
  "@types/react-dom": "^18.3.0",
157
157
  "@vitejs/plugin-react": "^4.3.1",
158
158
  "concurrently": "^8.2.2",
159
- "prettier": "^3.3.2",
160
- "sass": "^1.77.6",
159
+ "cross-env": "^7.0.3",
160
+ "prettier": "^3.3.3",
161
+ "sass": "^1.77.8",
161
162
  "typescript": "^5.5.3",
162
- "vite": "^5.3.3",
163
- "vitest": "^2.0.1"
163
+ "vite": "^5.3.4",
164
+ "vitest": "^2.0.3"
164
165
  },
165
166
  "optionalDependencies": {
166
167
  "@rollup/rollup-darwin-arm64": "^4.18.1",
@@ -1,6 +1,9 @@
1
1
  import { EditorView, keymap } from '@codemirror/view';
2
- import { ShareDBDoc, VZCodeContent } from '../../types';
3
- import { TabState } from '../vzReducer';
2
+ import {
3
+ ShareDBDoc,
4
+ TabState,
5
+ VZCodeContent,
6
+ } from '../../types';
4
7
 
5
8
  export const AIAssistCodeMirrorKeyMap = ({
6
9
  shareDBDoc,
@@ -3,9 +3,9 @@ import {
3
3
  File,
4
4
  FileId,
5
5
  ShareDBDoc,
6
+ TabState,
6
7
  VZCodeContent,
7
8
  } from '../../types';
8
- import { TabState } from '../vzReducer';
9
9
  import { RequestId } from '../generateRequestId';
10
10
  import { formatFile } from './formatFile';
11
11
 
@@ -19,6 +19,7 @@ import { json1Presence, textUnicode } from '../../ot';
19
19
  import {
20
20
  FileId,
21
21
  ShareDBDoc,
22
+ TabState,
22
23
  Username,
23
24
  VZCodeContent,
24
25
  } from '../../types';
@@ -115,6 +116,7 @@ export const getOrCreateEditor = ({
115
116
  customInteractRules,
116
117
  allowGlobals,
117
118
  enableAutoFollowRef,
119
+ openTab,
118
120
  }: {
119
121
  fileId: FileId;
120
122
 
@@ -148,6 +150,7 @@ export const getOrCreateEditor = ({
148
150
  // Ref to a boolean that determines whether to
149
151
  // enable auto-following the cursors of remote users.
150
152
  enableAutoFollowRef: React.MutableRefObject<boolean>;
153
+ openTab: (tabState: TabState) => void;
151
154
  }): EditorCacheValue => {
152
155
  // Cache hit
153
156
  if (editorCache.has(fileId)) {
@@ -202,6 +205,7 @@ export const getOrCreateEditor = ({
202
205
  path: textPath,
203
206
  docPresence,
204
207
  enableAutoFollowRef,
208
+ openTab,
205
209
  }),
206
210
  );
207
211
  }
@@ -38,6 +38,7 @@ export const CodeEditor = ({
38
38
  theme,
39
39
  codeEditorRef,
40
40
  enableAutoFollow,
41
+ openTab,
41
42
  } = useContext(VZCodeContext);
42
43
 
43
44
  // Set `doc.data.isInteracting` to `true` when the user is interacting
@@ -97,6 +98,7 @@ export const CodeEditor = ({
97
98
  customInteractRules,
98
99
  allowGlobals,
99
100
  enableAutoFollowRef,
101
+ openTab,
100
102
  }),
101
103
  [
102
104
  activeFileId,
@@ -6,7 +6,13 @@ import {
6
6
  } from '@codemirror/view';
7
7
  import { Annotation, RangeSet } from '@codemirror/state';
8
8
  import ColorHash from 'color-hash';
9
- import { Username } from '../../types';
9
+ import {
10
+ FileId,
11
+ Presence,
12
+ PresenceId,
13
+ TabState,
14
+ Username,
15
+ } from '../../types';
10
16
 
11
17
  const debug = false;
12
18
 
@@ -26,6 +32,12 @@ export const json1PresenceDisplay = ({
26
32
  path,
27
33
  docPresence,
28
34
  enableAutoFollowRef,
35
+ openTab,
36
+ }: {
37
+ path: Array<string>;
38
+ docPresence: any;
39
+ enableAutoFollowRef: React.MutableRefObject<boolean>;
40
+ openTab: (tabState: TabState) => void;
29
41
  }) => [
30
42
  ViewPlugin.fromClass(
31
43
  class {
@@ -44,113 +56,139 @@ export const json1PresenceDisplay = ({
44
56
  // Mutable state local to this closure representing aggregated presence.
45
57
  // * Keys are presence ids
46
58
  // * Values are presence objects as defined by ot-json1-presence
47
- const presenceState = {};
59
+ const presenceState: Record<PresenceId, Presence> =
60
+ {};
48
61
 
49
62
  // Add the scroll event listener
50
63
  //This runs for the arrow key scrolling, it should result in the users scrolling to eachother's location.
51
- view.dom.addEventListener('scroll', () => {
52
- this.scrollToCursor(view);
53
- });
64
+ // view.dom.addEventListener('scroll', () => {
65
+ // this.scrollToCursor(view);
66
+ // });
54
67
  // Receive remote presence changes.
55
- docPresence.on('receive', (id, presence) => {
56
- if (debug) {
57
- console.log(
58
- `Received presence for id ${id}`,
68
+ docPresence.on(
69
+ 'receive',
70
+ (id: PresenceId, presence: Presence) => {
71
+ if (debug) {
72
+ console.log(
73
+ `Received presence for id ${id}`,
74
+ presence,
75
+ ); // Debug statement
76
+ }
77
+
78
+ // If presence === null, the user has disconnected / exited
79
+ if (!presence) {
80
+ delete presenceState[id];
81
+ return;
82
+ }
83
+
84
+ // Check if the presence is for the current file or not.
85
+ const isPresenceInCurrentFile = pathMatches(
86
+ path,
59
87
  presence,
60
- ); // Debug statement
61
- }
62
- // If presence === null, the user has disconnected / exited
63
- // We also check if the presence is for the current file or not.
64
- if (presence && pathMatches(path, presence)) {
65
- presenceState[id] = presence;
66
- } else {
67
- delete presenceState[id];
68
- }
69
- // Update decorations to reflect new presence state.
70
- // TODO consider mutating this rather than recomputing it on each change.
88
+ );
89
+
90
+ // If the presence is in the current file, update the presence state.
91
+ if (isPresenceInCurrentFile) {
92
+ presenceState[id] = presence;
93
+ } else if (presence) {
94
+ // Otherwise, delete the presence state.
95
+ delete presenceState[id];
71
96
 
72
- const presenceDecorations = [];
97
+ // If auto-follow is enabled, and the presence is NOT
98
+ // in the current file, then open the tab of the other user.
99
+ if (enableAutoFollowRef.current) {
100
+ openTab({
101
+ fileId: presence.start[1] as FileId,
102
+ isTransient: true,
103
+ });
104
+ }
105
+ }
106
+ // Update decorations to reflect new presence state.
107
+ // TODO consider mutating this rather than recomputing it on each change.
73
108
 
74
- // Object.keys(presenceState).map((id) => {
75
- for (const id of Object.keys(presenceState)) {
76
- const presence = presenceState[id];
77
- const { start, end } = presence;
78
- const from = start[start.length - 1];
79
- const to = end[end.length - 1];
80
- const userColor = new ColorHash({
81
- lightness: 0.75,
82
- }).rgb(id);
83
- const { username } = presence;
109
+ const presenceDecorations = [];
84
110
 
85
- presenceDecorations.push({
86
- from,
87
- to: from,
88
- value: Decoration.widget({
89
- side: -1,
90
- block: false,
91
- widget: new PresenceWidget(
92
- // TODO see if we can figure out why
93
- // updateDOM was not being called when passing
94
- // the presence id as the id
95
- // id,
96
- '' + Math.random(),
97
- userColor,
98
- username,
99
- ),
100
- }),
101
- });
111
+ // Object.keys(presenceState).map((id) => {
112
+ for (const id of Object.keys(presenceState)) {
113
+ const presence: Presence = presenceState[id];
114
+ const { start, end } = presence;
115
+ const from = +start[start.length - 1];
116
+ const to = +end[end.length - 1];
117
+ const userColor = new ColorHash({
118
+ lightness: 0.75,
119
+ }).rgb(id);
120
+ const { username } = presence;
102
121
 
103
- // This is `true` when the presence is a cursor,
104
- // with no selection.
105
- if (from !== to) {
106
- // This is the case when the presence is a selection.
107
122
  presenceDecorations.push({
108
123
  from,
109
- to,
110
- value: Decoration.mark({
111
- class: 'cm-json1-presence',
112
- attributes: {
113
- style: `
124
+ to: from,
125
+ value: Decoration.widget({
126
+ side: -1,
127
+ block: false,
128
+ widget: new PresenceWidget(
129
+ // TODO see if we can figure out why
130
+ // updateDOM was not being called when passing
131
+ // the presence id as the id
132
+ // id,
133
+ '' + Math.random(),
134
+ userColor,
135
+ username,
136
+ ),
137
+ }),
138
+ });
139
+
140
+ // This is `true` when the presence is a cursor,
141
+ // with no selection.
142
+ if (from !== to) {
143
+ // This is the case when the presence is a selection.
144
+ presenceDecorations.push({
145
+ from,
146
+ to,
147
+ value: Decoration.mark({
148
+ class: 'cm-json1-presence',
149
+ attributes: {
150
+ style: `
114
151
  background-color: rgba(${userColor}, 0.75);
115
152
  mix-blend-mode: luminosity;
116
153
  `,
117
- },
118
- }),
119
- });
154
+ },
155
+ }),
156
+ });
157
+ }
158
+ if (view.state.doc.length >= from) {
159
+ // Ensure position is valid
160
+ this.cursorPosition[id] = from; // Store the cursor position, important to run if we cant get the regular scroll to work
161
+ // console.log(`Stored cursor position for id ${id}: ${from}`); // Debug statement
162
+ } else {
163
+ // console.warn(`Invalid cursor position for id ${id}: ${from}`); // Debug statement
164
+ }
120
165
  }
121
- if (view.state.doc.length >= from) {
122
- // Ensure position is valid
123
- this.cursorPosition[id] = from; // Store the cursor position, important to run if we cant get the regular scroll to work
124
- // console.log(`Stored cursor position for id ${id}: ${from}`); // Debug statement
125
- } else {
126
- // console.warn(`Invalid cursor position for id ${id}: ${from}`); // Debug statement
127
- }
128
- }
129
166
 
130
- this.decorations = Decoration.set(
131
- presenceDecorations,
132
- // Without this argument, we get the following error:
133
- // Uncaught Error: Ranges must be added sorted by `from` position and `startSide`
134
- true,
135
- );
167
+ this.decorations = Decoration.set(
168
+ presenceDecorations,
169
+ // Without this argument, we get the following error:
170
+ // Uncaught Error: Ranges must be added sorted by `from` position and `startSide`
171
+ true,
172
+ );
136
173
 
137
- // Somehow this triggers re-rendering of the Decorations.
138
- // Not sure if this is the correct usage of the API.
139
- // Inspired by https://github.com/yjs/y-codemirror.next/blob/main/src/y-remote-selections.js
140
- // Set timeout so that the current CodeMirror update finishes
141
- // before the next ones that render presence begin.
142
- setTimeout(() => {
143
- view.dispatch({
144
- annotations: [presenceAnnotation.of(true)],
145
- });
146
- }, 0);
174
+ // Somehow this triggers re-rendering of the Decorations.
175
+ // Not sure if this is the correct usage of the API.
176
+ // Inspired by https://github.com/yjs/y-codemirror.next/blob/main/src/y-remote-selections.js
177
+ // Set timeout so that the current CodeMirror update finishes
178
+ // before the next ones that render presence begin.
179
+ setTimeout(() => {
180
+ view.dispatch({
181
+ annotations: [presenceAnnotation.of(true)],
182
+ });
183
+ }, 0);
147
184
 
148
- // Auto-follow all users when their presence is broadcast
149
- // by scrolling them into view.
150
- if (enableAutoFollowRef.current) {
151
- this.scrollToCursor(view);
152
- }
153
- });
185
+ // Auto-follow all users when their presence is broadcast
186
+ // by scrolling them into view.
187
+ if (enableAutoFollowRef.current) {
188
+ this.scrollToCursor(view);
189
+ }
190
+ },
191
+ );
154
192
  }
155
193
  // Method to scroll the view to keep the cursor in view
156
194
  scrollToCursor(view) {
@@ -1,6 +1,5 @@
1
1
  import { useCallback, useMemo } from 'react';
2
- import type { FileId } from '../../types';
3
- import type { TabState } from '../vzReducer';
2
+ import type { FileId, TabState } from '../../types';
4
3
  import { CloseSVG } from '../Icons';
5
4
 
6
5
  // Supports adding the file's containing folder to the tab name
@@ -1,8 +1,8 @@
1
1
  import { useContext } from 'react';
2
2
  import { VZCodeContext } from '../VZCodeContext';
3
- import { TabState } from '../vzReducer';
4
3
  import { Tab } from './Tab';
5
4
  import './style.scss';
5
+ import { TabState } from '../../types';
6
6
 
7
7
  // Displays the list of tabs above the code editor.
8
8
  export const TabList = () => {
@@ -14,14 +14,11 @@ import {
14
14
  VZCodeContent,
15
15
  SearchResults,
16
16
  SearchFileVisibility,
17
+ TabState,
17
18
  } from '../types';
18
19
  import { usePrettier } from './usePrettier';
19
20
  import { useTypeScript } from './useTypeScript';
20
- import {
21
- TabState,
22
- createInitialState,
23
- vzReducer,
24
- } from './vzReducer';
21
+ import { createInitialState, vzReducer } from './vzReducer';
25
22
  import {
26
23
  ThemeLabel,
27
24
  defaultTheme,
@@ -52,7 +49,9 @@ export type VZCodeContextValue = {
52
49
  submitOperation: (
53
50
  next: (content: VZCodeContent) => VZCodeContent,
54
51
  ) => void;
52
+ // TODO pull in this type from ShareDB if possible
55
53
  localPresence: any;
54
+ // TODO pull in this type from ShareDB if possible
56
55
  docPresence: any;
57
56
 
58
57
  files: Files | null;
@@ -76,13 +75,7 @@ export type VZCodeContextValue = {
76
75
  setActiveFileRight: () => void;
77
76
 
78
77
  tabList: Array<TabState>;
79
- openTab: ({
80
- fileId,
81
- isTransient,
82
- }: {
83
- fileId: string;
84
- isTransient?: boolean;
85
- }) => void;
78
+ openTab: (tabState: TabState) => void;
86
79
  closeTabs: (fileIds: string[]) => void;
87
80
 
88
81
  isSettingsOpen: boolean;
@@ -125,6 +118,7 @@ export type VZCodeContextValue = {
125
118
  id: string,
126
119
  visibility: SearchFileVisibility,
127
120
  ) => void;
121
+ toggleSearchFocused: () => void;
128
122
 
129
123
  isCreateFileModalOpen: boolean;
130
124
  handleOpenCreateFileModal: () => void;
@@ -230,8 +224,7 @@ export const VZCodeProvider = ({
230
224
 
231
225
  // Unpack state.
232
226
  const {
233
- tabList,
234
- activeFileId,
227
+ pane,
235
228
  theme,
236
229
  search,
237
230
  isSearchOpen,
@@ -242,6 +235,13 @@ export const VZCodeProvider = ({
242
235
  enableAutoFollow,
243
236
  } = state;
244
237
 
238
+ // TODO support splitPane type
239
+ if (pane.type !== 'leafPane') {
240
+ throw new Error('Expected leafPane');
241
+ }
242
+ const tabList = pane.tabList;
243
+ const activeFileId = pane.activeFileId;
244
+
245
245
  // Functions for dispatching actions to the reducer.
246
246
  const {
247
247
  setActiveFileId,
@@ -254,6 +254,7 @@ export const VZCodeProvider = ({
254
254
  setSearch,
255
255
  setSearchResults,
256
256
  setSearchFileVisibility,
257
+ toggleSearchFocused,
257
258
  setIsSettingsOpen,
258
259
  setIsDocOpen,
259
260
  closeSettings,
@@ -348,6 +349,7 @@ export const VZCodeProvider = ({
348
349
  handleOpenCreateFileModal,
349
350
  setActiveFileLeft,
350
351
  setActiveFileRight,
352
+ toggleSearchFocused,
351
353
  runPrettierRef,
352
354
  runCodeRef,
353
355
  sidebarRef,
@@ -390,6 +392,7 @@ export const VZCodeProvider = ({
390
392
  setSearch,
391
393
  setSearchResults,
392
394
  setSearchFileVisibility,
395
+ toggleSearchFocused,
393
396
 
394
397
  isSettingsOpen,
395
398
  setIsSettingsOpen,
@@ -69,7 +69,8 @@ export const VZKeyboardShortcutsDoc = ({
69
69
  <span>Close the current tab</span>
70
70
  </li>
71
71
  <li>
72
- <strong>Alt + n</strong> <br />
72
+ <strong>Alt + n or Ctrl + Shift + n</strong>{' '}
73
+ <br />
73
74
  <span>Open the create file modal</span>
74
75
  </li>
75
76
  <li>
@@ -46,7 +46,7 @@ export const Search = () => {
46
46
  shareDBDoc,
47
47
  editorCache,
48
48
  } = useContext(VZCodeContext);
49
- const { pattern, results } = search;
49
+ const { pattern, results, focused } = search;
50
50
 
51
51
  useEffect(() => {
52
52
  if (isMounted) {
@@ -67,10 +67,15 @@ export const Search = () => {
67
67
  return () => clearTimeout(delaySearch);
68
68
  } else {
69
69
  setIsMounted(true);
70
- inputRef.current.focus();
71
70
  }
72
71
  }, [pattern]);
73
72
 
73
+ useEffect(() => {
74
+ if (inputRef.current) {
75
+ inputRef.current.focus();
76
+ }
77
+ }, [focused]);
78
+
74
79
  return (
75
80
  <div>
76
81
  <Form.Group