vzcode 0.70.0 → 0.71.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-Bfl2hCW9.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-DOd2vdTJ.css">
23
+ <script type="module" crossorigin src="/assets/index-HJAv0wn0.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-B4YHcAhs.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": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -79,15 +79,15 @@
79
79
  "@replit/codemirror-vscode-keymap": "^6.0.2",
80
80
  "@teamwork/websocket-json-stream": "^2.0.0",
81
81
  "@typescript/vfs": "^1.5.0",
82
- "@uiw/codemirror-theme-abcdef": "^4.21.21",
83
- "@uiw/codemirror-theme-dracula": "^4.21.21",
84
- "@uiw/codemirror-theme-eclipse": "^4.21.21",
85
- "@uiw/codemirror-theme-github": "^4.21.21",
86
- "@uiw/codemirror-theme-material": "^4.21.21",
87
- "@uiw/codemirror-theme-nord": "^4.21.21",
88
- "@uiw/codemirror-theme-okaidia": "^4.21.21",
89
- "@uiw/codemirror-theme-xcode": "^4.21.21",
90
- "@uiw/codemirror-themes": "^4.21.21",
82
+ "@uiw/codemirror-theme-abcdef": "^4.21.22",
83
+ "@uiw/codemirror-theme-dracula": "^4.21.22",
84
+ "@uiw/codemirror-theme-eclipse": "^4.21.22",
85
+ "@uiw/codemirror-theme-github": "^4.21.22",
86
+ "@uiw/codemirror-theme-material": "^4.21.22",
87
+ "@uiw/codemirror-theme-nord": "^4.21.22",
88
+ "@uiw/codemirror-theme-okaidia": "^4.21.22",
89
+ "@uiw/codemirror-theme-xcode": "^4.21.22",
90
+ "@uiw/codemirror-themes": "^4.21.22",
91
91
  "body-parser": "^1.20.2",
92
92
  "codemirror": "^6.0.1",
93
93
  "codemirror-ot": "^4.2.0",
@@ -108,11 +108,11 @@
108
108
  "react-router-dom": "^6.22.1",
109
109
  "sharedb": "^4.1.2",
110
110
  "sharedb-client-browser": "^4.4.0",
111
- "vizhub-ui": "^3.19.0",
111
+ "vizhub-ui": "^3.20.0",
112
112
  "ws": "^8.16.0"
113
113
  },
114
114
  "devDependencies": {
115
- "@types/react": "^18.2.55",
115
+ "@types/react": "^18.2.56",
116
116
  "@types/react-dom": "^18.2.19",
117
117
  "@vitejs/plugin-react": "^4.2.1",
118
118
  "concurrently": "^8.2.2",
@@ -11,11 +11,11 @@ import {
11
11
  RequestId,
12
12
  generateRequestId,
13
13
  } from '../../generateRequestId';
14
- import { SparklesSVG, StopSVG } from '../../Icons';
14
+ import { SparklesSVG } from '../../Icons';
15
15
  import { startAIAssist } from '../startAIAssist';
16
- import './style.scss';
17
16
  import { Spinner } from '../Spinner';
18
17
  import { VZCodeContext } from '../../VZCodeContext';
18
+ import './style.scss';
19
19
 
20
20
  const enableStopGeneration = false;
21
21
 
@@ -45,7 +45,8 @@ export const AIAssistWidget = ({
45
45
  const [aiStreamId, setAiStreamId] =
46
46
  useState<RequestId | null>(null);
47
47
 
48
- const { runPrettierRef } = useContext(VZCodeContext);
48
+ const { runPrettierRef, runCodeRef } =
49
+ useContext(VZCodeContext);
49
50
 
50
51
  const handleClick = useCallback(async () => {
51
52
  const isCurrentlyGenerationg = aiStreamId !== null;
@@ -72,6 +73,12 @@ export const AIAssistWidget = ({
72
73
  runPrettier();
73
74
  }
74
75
 
76
+ // Run the code after the AI Assist.
77
+ const runCode = runCodeRef.current;
78
+ if (runCode !== null) {
79
+ runCode();
80
+ }
81
+
75
82
  // Handles the case that the user has started,
76
83
  // stopped, and started again before the first request
77
84
  // has finished, bu comparing the current stream ID
@@ -1,7 +1,9 @@
1
1
  .vz-code-ai-assist-widget {
2
2
  position: absolute;
3
- bottom: 32px;
4
- right: 32px;
3
+ // bottom: 32px;
4
+ // right: 32px;
5
+ bottom: 16px;
6
+ right: 16px;
5
7
  color: var(--vh-color-caution-01);
6
8
  }
7
9
 
@@ -1,3 +1,2 @@
1
1
  export { AIAssistWidget } from './AIAssistWidget';
2
2
  export { AIAssistCodeMirrorKeyMap } from './AIAssistCodeMirrorKeyMap';
3
- export type { AIAssistState } from './AIAssistState';
@@ -80,13 +80,14 @@ export const CodeEditor = ({
80
80
  clearTimeout(interactTimeoutRef.current);
81
81
  }
82
82
 
83
- // Set `isInteracting: false` after a delay.
83
+ // Set `isInteracting: undefined` after a delay.
84
84
  interactTimeoutRef.current = setTimeout(() => {
85
85
  interactTimeoutRef.current = null;
86
- submitOperation((document) => ({
87
- ...document,
88
- isInteracting: false,
89
- }));
86
+
87
+ // This logic deletes the `isInteracting` property from the document.
88
+ submitOperation(
89
+ ({ isInteracting, ...newDocument }) => newDocument,
90
+ );
90
91
  }, 800);
91
92
  }, [submitOperation]);
92
93
 
@@ -0,0 +1,16 @@
1
+ export const PlaySVG = () => (
2
+ <svg
3
+ width="24"
4
+ height="24"
5
+ fill="none"
6
+ viewBox="0 0 24 24"
7
+ strokeWidth={1.5}
8
+ stroke="currentColor"
9
+ >
10
+ <path
11
+ strokeLinecap="round"
12
+ strokeLinejoin="round"
13
+ d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z"
14
+ />
15
+ </svg>
16
+ );
@@ -8,3 +8,4 @@ export { TrashSVG } from './TrashSVG';
8
8
  export { BugSVG } from './BugSVG';
9
9
  export { FileSVG } from './FileSVG';
10
10
  export { DirectoryArrowSVG } from './DirectoryArrowSVG';
11
+ export { PlaySVG } from './PlaySVG';
@@ -0,0 +1,53 @@
1
+ import { useCallback, useContext } from 'react';
2
+ import { PlaySVG } from '../Icons';
3
+ import { VZCodeContext } from '../VZCodeContext';
4
+ import { OverlayTrigger, Tooltip } from '../bootstrap';
5
+ import './style.scss';
6
+
7
+ export const RunCodeWidget = ({
8
+ runCodeWidgetTooltipText = (
9
+ <>
10
+ <strong>Run Code</strong>
11
+ <div>(Shift + Enter or Ctrl + s)</div>
12
+ </>
13
+ ),
14
+ }: {
15
+ runCodeWidgetTooltipText?: JSX.Element;
16
+ }) => {
17
+ const { runCodeRef, runPrettierRef } =
18
+ useContext(VZCodeContext);
19
+
20
+ const handleClick = useCallback(() => {
21
+ // Run Prettier
22
+ const runPrettier = runPrettierRef.current;
23
+ if (runPrettier !== null) {
24
+ runPrettier();
25
+ }
26
+
27
+ // Run the code
28
+ const runCode = runCodeRef.current;
29
+ if (runCode !== null) {
30
+ runCode();
31
+ }
32
+ }, []);
33
+
34
+ return (
35
+ <div className="vz-code-run-code-widget">
36
+ <OverlayTrigger
37
+ placement="left"
38
+ overlay={
39
+ <Tooltip id="run-code-widget-tooltip">
40
+ {runCodeWidgetTooltipText}
41
+ </Tooltip>
42
+ }
43
+ >
44
+ <i
45
+ className="icon-button icon-button-dark"
46
+ onClick={handleClick}
47
+ >
48
+ <PlaySVG />
49
+ </i>
50
+ </OverlayTrigger>
51
+ </div>
52
+ );
53
+ };
@@ -0,0 +1,7 @@
1
+ .vz-code-run-code-widget {
2
+ position: absolute;
3
+ right: 16px;
4
+ top: 9px;
5
+ // color: var(--vh-color-caution-01);
6
+ color: var(--vh-color-neutral-04);
7
+ }
@@ -12,10 +12,10 @@
12
12
  align-items: center;
13
13
  display: flex;
14
14
  gap: 8px;
15
- padding: 8px 8px 8px 16px;
16
- border-top-left-radius: 4px;
17
- border-top-right-radius: 4px;
18
- margin-top: 5px;
15
+ padding: 6px 6px 6px 16px;
16
+ border-top-left-radius: 8px;
17
+ border-top-right-radius: 8px;
18
+ margin-top: 6px;
19
19
  margin-left: 2px;
20
20
  margin-right: 2px;
21
21
  cursor: pointer;
@@ -7,6 +7,7 @@ import {
7
7
  import {
8
8
  Files,
9
9
  ShareDBDoc,
10
+ SubmitOperation,
10
11
  Username,
11
12
  VZCodeContent,
12
13
  } from '../types';
@@ -31,6 +32,7 @@ import {
31
32
  } from './useEditorCache';
32
33
  import { useURLSync } from './useURLSync';
33
34
  import { useKeyboardShortcuts } from './useKeyboardShortcuts';
35
+ import { useRunCode } from './useRunCode';
34
36
 
35
37
  // This context centralizes all the "smart" logic
36
38
  // to do with the application state. This includes
@@ -98,6 +100,7 @@ export type VZCodeContextValue = {
98
100
  runPrettierRef: React.MutableRefObject<
99
101
  null | (() => void)
100
102
  >;
103
+ runCodeRef: React.MutableRefObject<null | (() => void)>;
101
104
  };
102
105
 
103
106
  export const VZCodeProvider = ({
@@ -115,9 +118,7 @@ export const VZCodeProvider = ({
115
118
  }: {
116
119
  content: VZCodeContent;
117
120
  shareDBDoc: ShareDBDoc<VZCodeContent>;
118
- submitOperation: (
119
- next: (content: VZCodeContent) => VZCodeContent,
120
- ) => void;
121
+ submitOperation: SubmitOperation;
121
122
  localPresence: any;
122
123
  docPresence: any;
123
124
  prettierWorker: Worker;
@@ -143,6 +144,8 @@ export const VZCodeProvider = ({
143
144
  enableManualPretter,
144
145
  });
145
146
 
147
+ const runCodeRef = useRunCode(submitOperation);
148
+
146
149
  // The error message shows either:
147
150
  // * `prettierError` - errors from Prettier, client-side only
148
151
  // * `codeError` - errors from an external source, such as
@@ -247,19 +250,21 @@ export const VZCodeProvider = ({
247
250
  [createFile, setIsCreateFileModalOpen],
248
251
  );
249
252
 
253
+ // Isolate the files object from the document.
254
+ const files: Files | null = content
255
+ ? content.files
256
+ : null;
257
+
250
258
  useKeyboardShortcuts({
251
259
  closeTabs,
252
260
  activeFileId,
253
261
  handleOpenCreateFileModal,
254
262
  setActiveFileLeft,
255
263
  setActiveFileRight,
264
+ runPrettierRef,
265
+ runCodeRef,
256
266
  });
257
267
 
258
- // Isolate the files object from the document.
259
- const files: Files | null = content
260
- ? content.files
261
- : null;
262
-
263
268
  // The value provided by this context.
264
269
  const value: VZCodeContextValue = {
265
270
  content,
@@ -309,6 +314,7 @@ export const VZCodeProvider = ({
309
314
  handleCloseCreateFileModal,
310
315
  handleCreateFileClick,
311
316
  runPrettierRef,
317
+ runCodeRef,
312
318
  };
313
319
 
314
320
  return (
@@ -6,6 +6,7 @@ import { CodeErrorOverlay } from './CodeErrorOverlay';
6
6
  import { PresenceNotifications } from './PresenceNotifications';
7
7
  import { AIAssistWidget } from './AIAssist/AIAssistWidget';
8
8
  import { VZCodeContext } from './VZCodeContext';
9
+ import { RunCodeWidget } from './RunCodeWidget';
9
10
 
10
11
  // The middle portion of the VZCode environment, containing:
11
12
  // * The list of tabs at the top
@@ -106,6 +107,7 @@ export const VZMiddle = ({
106
107
  aiAssistClickOverride={aiAssistClickOverride}
107
108
  />
108
109
  ) : null}
110
+ <RunCodeWidget />
109
111
  <CodeErrorOverlay
110
112
  errorMessage={errorMessage}
111
113
  content={content}
@@ -1,8 +1,6 @@
1
1
  // This module is responsible for checking
2
2
  // keyboard events and determining whether
3
3
  // a run should be triggered or not.
4
- //
5
- // The most common case is when the us
6
4
  export const shouldTriggerRun = (event: KeyboardEvent) => {
7
5
  const isMac = /Mac|iMac|MacBook/i.test(
8
6
  navigator.userAgent,
@@ -1,41 +1,73 @@
1
- import { useCallback, useEffect } from 'react';
1
+ import { useContext, useEffect } from 'react';
2
+ import { shouldTriggerRun } from './shouldTriggerRun';
3
+ import { VZCodeContext } from './VZCodeContext';
2
4
 
5
+ // This module implements the keyboard shortcuts
6
+ // for the VZCode editor.
7
+ // These include:
8
+ // * Alt-w: Close the current tab
9
+ // * Alt-n: Open the create file modal
10
+ // * Alt-PageUp: Change the active tab to the previous one
11
+ // * Alt-PageDown: Change the active tab to the next one
12
+ // * Ctrl-s or Shift-Enter: Run the code and format it with Prettier
3
13
  export const useKeyboardShortcuts = ({
4
14
  closeTabs,
5
15
  activeFileId,
6
16
  handleOpenCreateFileModal,
7
17
  setActiveFileLeft,
8
18
  setActiveFileRight,
19
+ runPrettierRef,
20
+ runCodeRef,
9
21
  }) => {
10
- const handleKeyPress = useCallback(
11
- (event: KeyboardEvent) => {
22
+ useEffect(() => {
23
+ const handleKeyPress = (event: KeyboardEvent) => {
24
+ if (shouldTriggerRun(event)) {
25
+ event.preventDefault();
26
+
27
+ // Run Prettier
28
+ const runPrettier = runPrettierRef.current;
29
+ if (runPrettier !== null) {
30
+ runPrettier();
31
+ }
32
+
33
+ // Run the code
34
+ const runCode = runCodeRef.current;
35
+ if (runCode !== null) {
36
+ runCode();
37
+ }
38
+ return;
39
+ }
40
+
12
41
  if (event.altKey === true) {
42
+ // Alt-w: Close the current tab
13
43
  if (event.key === 'w') {
44
+ // TODO clean this up so we can remove `activeFileId`
45
+ // as a dependency
46
+ // TODO closeActiveTab()
14
47
  closeTabs([activeFileId]);
48
+ return;
15
49
  }
50
+
51
+ // Alt-n: Open the create file modal
16
52
  if (event.key === 'n') {
17
53
  handleOpenCreateFileModal();
54
+ return;
18
55
  }
56
+
57
+ // Alt-PageUp: Change the active tab to the previous one
19
58
  if (event.key === 'PageUp') {
20
- // Change the active tab to the previous one
21
59
  setActiveFileLeft();
60
+ return;
22
61
  }
62
+
63
+ // Alt-PageDown: Change the active tab to the next one
23
64
  if (event.key === 'PageDown') {
24
- // Change the active tab to the next one
25
65
  setActiveFileRight();
66
+ return;
26
67
  }
27
68
  }
28
- },
29
- [
30
- handleOpenCreateFileModal,
31
- closeTabs,
32
- activeFileId,
33
- setActiveFileLeft,
34
- setActiveFileRight,
35
- ],
36
- );
69
+ };
37
70
 
38
- useEffect(() => {
39
71
  document.addEventListener('keydown', handleKeyPress);
40
72
  return () => {
41
73
  document.removeEventListener(
@@ -43,5 +75,11 @@ export const useKeyboardShortcuts = ({
43
75
  handleKeyPress,
44
76
  );
45
77
  };
46
- }, [handleKeyPress]);
78
+ }, [
79
+ handleOpenCreateFileModal,
80
+ closeTabs,
81
+ activeFileId,
82
+ setActiveFileLeft,
83
+ setActiveFileRight,
84
+ ]);
47
85
  };
@@ -31,14 +31,12 @@ export const usePrettier = ({
31
31
  shareDBDoc,
32
32
  submitOperation,
33
33
  prettierWorker,
34
- enableManualPretter,
35
34
  }: {
36
35
  shareDBDoc: ShareDBDoc<VZCodeContent> | null;
37
36
  submitOperation: (
38
37
  next: (content: VZCodeContent) => VZCodeContent,
39
38
  ) => void;
40
39
  prettierWorker: Worker;
41
- enableManualPretter: boolean;
42
40
  }) => {
43
41
  // The set of files that have been modified
44
42
  // since the last Prettier run.
@@ -139,114 +137,48 @@ export const usePrettier = ({
139
137
  };
140
138
  runPrettierRef.current = runPrettier;
141
139
 
142
- let debouncePrettier;
143
- let debounceTimeout;
144
- let handleKeyDown;
145
- if (enableManualPretter) {
146
- handleKeyDown = (event: KeyboardEvent) => {
147
- if (shouldTriggerRun(event)) {
148
- event.preventDefault();
149
- runPrettier();
150
- }
151
- };
152
- document.addEventListener('keydown', handleKeyDown);
153
- } else {
154
- // Function to debounce running Prettier
155
-
156
- debouncePrettier = () => {
157
- clearTimeout(debounceTimeout);
158
- debounceTimeout = setTimeout(
159
- runPrettier,
160
- autoPrettierDebounceTimeMS,
161
- );
162
- };
163
- // Stops prettier from running when you perform a mouse move or a keydown
164
- window.addEventListener(
165
- 'mousemove',
166
- debouncePrettier,
167
- );
168
- window.addEventListener('keydown', debouncePrettier);
169
- }
140
+ const handleOpBatch = (op: JSONOp) => {
141
+ // Only act on changes coming from the client
142
+ // OR changes coming from the AI Assist that we
143
+ // if (isLocal) {
144
+ // If the op is coming from Prettier itself,
145
+ // then do nothing.
146
+ if (isApplyingOpRef.current) {
147
+ // console.log('ignoring op from Prettier');
148
+ return;
149
+ }
150
+
151
+ // Check if the path of this op is the text content of a file
152
+ if (
153
+ op &&
154
+ op.length === 4 &&
155
+ op[0] === 'files' &&
156
+ typeof op[1] === 'string' &&
157
+ op[2] === 'text'
158
+ ) {
159
+ // Get the file id
160
+ const fileId: FileId = op[1];
161
+
162
+ // Add the file id to the set of dirty files
163
+ dirtyFilesRef.current.add(fileId);
164
+ }
165
+ };
170
166
 
171
167
  // Listen for changes
172
- shareDBDoc.on(
173
- 'op batch',
174
- (op: JSONOp, isLocal: boolean) => {
175
- // Only act on changes coming from the client
176
- // OR changes coming from the AI Assist that we
177
- // if (isLocal) {
178
- // If the op is coming from Prettier itself,
179
- // then do nothing.
180
- if (isApplyingOpRef.current) {
181
- // console.log('ignoring op from Prettier');
182
- return;
183
- }
184
-
185
- // Example op that we want to act on:
186
- // op [
187
- // "files",
188
- // "2514857669",
189
- // "text",
190
- // {
191
- // "es": [
192
- // 18,
193
- // "d"
194
- // ]
195
- // }
196
- // ]
197
-
198
- // Check if the path of this op is the text content of a file
199
- if (
200
- op &&
201
- op.length === 4 &&
202
- op[0] === 'files' &&
203
- typeof op[1] === 'string' &&
204
- op[2] === 'text'
205
- ) {
206
- // Get the file id
207
- const fileId: FileId = op[1];
208
-
209
- // Add the file id to the set of dirty files
210
- dirtyFilesRef.current.add(fileId);
211
-
212
- // Debounce running Prettier
213
- if (!enableManualPretter) {
214
- debouncePrettier();
215
- }
216
- // console.log('Op is for file', fileId);
217
- }
218
- // } else {
219
- // // console.log('ignoring op from remote', op, isLocal);
220
- // }
221
- },
222
- );
168
+ shareDBDoc.on('op batch', handleOpBatch);
223
169
 
224
- // In the event that the component unmounts,
170
+ // Clean up the event listeners
225
171
  return () => {
226
- // Remove the event listener
227
172
  prettierWorker.removeEventListener(
228
173
  'message',
229
174
  handleMessage,
230
175
  );
231
- if (enableManualPretter) {
232
- document.removeEventListener(
233
- 'keydown',
234
- handleKeyDown,
235
- );
236
- } else {
237
- window.removeEventListener(
238
- 'keydown',
239
- debouncePrettier,
240
- );
241
- window.removeEventListener(
242
- 'mousemove',
243
- debouncePrettier,
244
- );
245
-
246
- // Clear the timeout
247
- clearTimeout(debounceTimeout);
248
- }
176
+
177
+ shareDBDoc.removeListener('op batch', handleOpBatch);
249
178
  };
250
179
  }, [shareDBDoc]);
251
- return { prettierError, runPrettierRef }; // Return the errors for use in other files
180
+
181
+ // Return the errors and run prettier function ref
182
+ // for use elsewhere.
183
+ return { prettierError, runPrettierRef };
252
184
  };
@@ -0,0 +1,28 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { SubmitOperation, VZCodeContent } from '../types';
3
+
4
+ // Runs the code by flashing `isInteracting` to `true`.
5
+ export const useRunCode = (
6
+ submitOperation: SubmitOperation,
7
+ ) => {
8
+ const runCodeRef = useRef(null);
9
+ useEffect(() => {
10
+ const runCode = () => {
11
+ submitOperation((content: VZCodeContent) => ({
12
+ ...content,
13
+ isInteracting: true,
14
+ }));
15
+ setTimeout(() => {
16
+ // This somewhat cryptic logic
17
+ // deletes the `isInteracting` property
18
+ // from the document.
19
+ submitOperation(
20
+ ({ isInteracting, ...newDocument }) =>
21
+ newDocument,
22
+ );
23
+ }, 0);
24
+ };
25
+ runCodeRef.current = runCode;
26
+ }, [submitOperation]);
27
+ return runCodeRef;
28
+ };
@@ -87,7 +87,8 @@ export const computeInitialDocument = ({ fullPath }) => {
87
87
  // using an interactive code widget such as number dragger.
88
88
  // * When true, the auto-save to the file system is changed to be
89
89
  // more frequent (throttled not debounced).
90
- isInteracting: false,
90
+ // isInteracting: false,
91
+ // This is `undefined` initially.
91
92
  };
92
93
 
93
94
  /**