vzcode 0.81.0 → 0.83.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,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-CnwdGSLW.js"></script>
23
+ <script type="module" crossorigin src="/assets/index-CnHJy69d.js"></script>
24
24
  <link rel="stylesheet" crossorigin href="/assets/index-J8-kRnnE.css">
25
25
  </head>
26
26
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "0.81.0",
3
+ "version": "0.83.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -74,7 +74,7 @@
74
74
  "@codemirror/view": "^6.26.0",
75
75
  "@lezer/highlight": "^1.2.0",
76
76
  "@replit/codemirror-indentation-markers": "^6.5.1",
77
- "@replit/codemirror-interact": "^6.3.0",
77
+ "@replit/codemirror-interact": "^6.3.1",
78
78
  "@replit/codemirror-lang-svelte": "^6.0.0",
79
79
  "@replit/codemirror-vscode-keymap": "^6.0.2",
80
80
  "@teamwork/websocket-json-stream": "^2.0.0",
@@ -112,7 +112,7 @@
112
112
  "ws": "^8.16.0"
113
113
  },
114
114
  "devDependencies": {
115
- "@types/react": "^18.2.71",
115
+ "@types/react": "^18.2.72",
116
116
  "@types/react-dom": "^18.2.22",
117
117
  "@vitejs/plugin-react": "^4.2.1",
118
118
  "concurrently": "^8.2.2",
@@ -55,6 +55,17 @@ export const AIAssistWidget = ({
55
55
  aiAssistOptions,
56
56
  });
57
57
 
58
+ // Introduce a delay before running Prettier
59
+ // and the code, to give the ShareDB ops a chance
60
+ // to be applied and to propagate via WebSockets
61
+ // to the client. This is necessary because sometimes
62
+ // the ShareDB ops from the AI assist actually don't
63
+ // reach the client until AFTER the response comes back
64
+ // from `await startAIAssist`.
65
+ await new Promise((resolve) =>
66
+ setTimeout(resolve, 1000),
67
+ );
68
+
58
69
  // Trigger a Prettier run after the AI Assist.
59
70
  const runPrettier = runPrettierRef.current;
60
71
  if (runPrettier !== null) {
@@ -69,7 +80,7 @@ export const AIAssistWidget = ({
69
80
 
70
81
  // Handles the case that the user has started,
71
82
  // stopped, and started again before the first request
72
- // has finished, bu comparing the current stream ID
83
+ // has finished, by comparing the current stream ID
73
84
  // with the stream ID that was active when the request
74
85
  // was started.
75
86
  setAiStreamId((currentAIStreamId) =>
@@ -92,7 +92,7 @@ function App() {
92
92
  >
93
93
  <div className="app">
94
94
  <VZLeft />
95
- <VZMiddle />
95
+ <VZMiddle allowGlobals={true} />
96
96
  {enableRightPanel ? <VZRight /> : null}
97
97
  <VZResizer side="left" />
98
98
  {enableRightPanel ? (
@@ -91,6 +91,7 @@ export const getOrCreateEditor = ({
91
91
  usernameRef,
92
92
  typeScriptWorker,
93
93
  customInteractRules,
94
+ allowGlobals,
94
95
  }: {
95
96
  fileId: FileId;
96
97
 
@@ -119,6 +120,7 @@ export const getOrCreateEditor = ({
119
120
  aiAssistEndpoint?: string;
120
121
  typeScriptWorker: Worker;
121
122
  customInteractRules?: Array<InteractRule>;
123
+ allowGlobals: boolean;
122
124
  }): EditorCacheValue => {
123
125
  // Cache hit
124
126
  if (editorCache.has(fileId)) {
@@ -276,6 +278,7 @@ export const getOrCreateEditor = ({
276
278
  fileName: name,
277
279
  shareDBDoc,
278
280
  fileId,
281
+ allowGlobals,
279
282
  }) as unknown as () => Diagnostic[],
280
283
  //Needs the unknown because we are returning a Promise<Diagnostic>
281
284
  ),
@@ -9,16 +9,18 @@ import { Username } from '../../types';
9
9
  import { EditorCacheValue } from '../useEditorCache';
10
10
  import { getOrCreateEditor } from './getOrCreateEditor';
11
11
  import { VZCodeContext } from '../VZCodeContext';
12
- import './style.scss';
13
12
  import { InteractRule } from '@replit/codemirror-interact';
13
+ import './style.scss';
14
14
 
15
15
  // The path in the ShareDB document where the files live.
16
16
  const filesPath = ['files'];
17
17
 
18
18
  export const CodeEditor = ({
19
19
  customInteractRules,
20
+ allowGlobals,
20
21
  }: {
21
22
  customInteractRules?: Array<InteractRule>;
23
+ allowGlobals: boolean;
22
24
  }) => {
23
25
  const {
24
26
  activeFileId,
@@ -85,6 +87,7 @@ export const CodeEditor = ({
85
87
  usernameRef,
86
88
  typeScriptWorker,
87
89
  customInteractRules,
90
+ allowGlobals,
88
91
  }),
89
92
  [
90
93
  activeFileId,
@@ -10,6 +10,7 @@ export const typeScriptLinter = ({
10
10
  fileName,
11
11
  shareDBDoc,
12
12
  fileId,
13
+ allowGlobals,
13
14
  }) => {
14
15
  return async () => {
15
16
  const requestId = generateRequestId();
@@ -20,6 +21,7 @@ export const typeScriptLinter = ({
20
21
  fileName,
21
22
  fileContent,
22
23
  requestId,
24
+ allowGlobals,
23
25
  };
24
26
  typeScriptWorker.postMessage(linterRequest);
25
27
 
@@ -1,9 +1,6 @@
1
- // TODO move back to this way of importing when this PR is merged:
2
- // https://github.com/replit/codemirror-interact/pull/19/files
3
- // import interact from '@replit/codemirror-interact';
4
1
  import interact, {
5
2
  InteractRule,
6
- } from './codemirror-interact';
3
+ } from '@replit/codemirror-interact';
7
4
 
8
5
  import {
9
6
  ViewPlugin,
@@ -56,6 +53,7 @@ export const widgets = ({
56
53
  const updateHex = (e: Event) => {
57
54
  const el: HTMLInputElement =
58
55
  e.target as HTMLInputElement;
56
+ if (onInteract) onInteract();
59
57
  if (el.value) {
60
58
  setText(
61
59
  valueIsUpper
@@ -22,6 +22,7 @@ export const VZMiddle = ({
22
22
  aiAssistTooltipText,
23
23
  aiAssistClickOverride,
24
24
  customInteractRules,
25
+ allowGlobals = false,
25
26
  }: {
26
27
  enableAIAssist?: boolean;
27
28
  aiAssistEndpoint?: string;
@@ -29,6 +30,7 @@ export const VZMiddle = ({
29
30
  aiAssistTooltipText?: string;
30
31
  aiAssistClickOverride?: () => void;
31
32
  customInteractRules?: Array<InteractRule>;
33
+ allowGlobals?: boolean;
32
34
  }) => {
33
35
  const { codeEditorWidth } = useContext(
34
36
  SplitPaneResizeContext,
@@ -58,6 +60,7 @@ export const VZMiddle = ({
58
60
  {isClient && content && activeFileId && (
59
61
  <CodeEditor
60
62
  customInteractRules={customInteractRules}
63
+ allowGlobals={allowGlobals}
61
64
  />
62
65
  )}
63
66
  {isClient &&
@@ -64,9 +64,6 @@ export const VZSettings = ({
64
64
  }, [selectedFont]);
65
65
 
66
66
  useEffect(() => {
67
- // const fontSize = event.target.value;
68
- // const root = document.documentElement;
69
- // root.style.setProperty('--vzcode-font-size', fontSize);
70
67
  document.body.style.setProperty(
71
68
  '--vzcode-font-size',
72
69
  selectedFontSize,
@@ -109,7 +106,6 @@ export const VZSettings = ({
109
106
 
110
107
  <Form.Group className="mb-3" controlId="formFork">
111
108
  <Form.Label>Theme</Form.Label>
112
-
113
109
  <select
114
110
  className="form-select"
115
111
  value={theme}
@@ -121,7 +117,6 @@ export const VZSettings = ({
121
117
  </option>
122
118
  ))}
123
119
  </select>
124
-
125
120
  <Form.Text className="text-muted">
126
121
  Select a color theme for the editor
127
122
  </Form.Text>
@@ -129,7 +124,6 @@ export const VZSettings = ({
129
124
 
130
125
  <Form.Group className="mb-3" controlId="formFork">
131
126
  <Form.Label>Font</Form.Label>
132
-
133
127
  <select
134
128
  className="form-select"
135
129
  onChange={handleFontChange}
@@ -141,7 +135,6 @@ export const VZSettings = ({
141
135
  </option>
142
136
  ))}
143
137
  </select>
144
-
145
138
  <Form.Text className="text-muted">
146
139
  Select font for the editor
147
140
  </Form.Text>
@@ -149,7 +142,6 @@ export const VZSettings = ({
149
142
 
150
143
  <Form.Group className="mb-3" controlId="formFork">
151
144
  <Form.Label>Font Size</Form.Label>
152
-
153
145
  <select
154
146
  className="form-select"
155
147
  onChange={handleFontSizeChange}
@@ -161,11 +153,53 @@ export const VZSettings = ({
161
153
  </option>
162
154
  ))}
163
155
  </select>
164
-
165
156
  <Form.Text className="text-muted">
166
157
  Select a font size for the editor
167
158
  </Form.Text>
168
159
  </Form.Group>
160
+
161
+ {/* Keyboard Shortcuts Documentation */}
162
+ <Form.Group className="mb-3" controlId="formFork">
163
+ <Form.Label>Keyboard Shortcuts</Form.Label>
164
+ <ul>
165
+ <li>
166
+ <strong>Alt-w</strong> <span>⌥W</span>
167
+ <br />
168
+ <span>Close the current tab</span>
169
+ </li>
170
+ <li>
171
+ <strong>Alt-n</strong> <span>⌥N</span>
172
+ <br />
173
+ <span>Open the create file modal</span>
174
+ </li>
175
+ <li>
176
+ <strong>Alt-PageUp</strong> <span>⌥⇞</span>
177
+ <br />
178
+ <span>
179
+ Change the active tab to the previous one
180
+ </span>
181
+ </li>
182
+ <li>
183
+ <strong>Alt-PageDown</strong> <span>⌥⇟</span>
184
+ <br />
185
+ <span>
186
+ Change the active tab to the next one
187
+ </span>
188
+ </li>
189
+ <li>
190
+ <strong>Ctrl-s</strong> or{' '}
191
+ <strong>Shift-Enter</strong> <span>⌘S</span>{' '}
192
+ or <span>⇧↩</span>
193
+ <br />
194
+ <span>
195
+ Run the code and format it with Prettier
196
+ </span>
197
+ </li>
198
+ </ul>
199
+ <Form.Text className="text-muted">
200
+ Keyboard shortcuts for quick actions
201
+ </Form.Text>
202
+ </Form.Group>
169
203
  </Modal.Body>
170
204
  <Modal.Footer>
171
205
  <Button variant="primary" onClick={closeSettings}>
@@ -0,0 +1,73 @@
1
+ import ts from 'typescript';
2
+
3
+ export const compilerOptions: ts.CompilerOptions = {
4
+ lib: ['dom', 'esnext'],
5
+
6
+ // Disable warnings around "Any type".
7
+ noImplicitAny: false,
8
+
9
+ // Disable warnings around "Implicit any in parameter".
10
+ noImplicitThis: false,
11
+
12
+ // Allow JavaScript files to be processed
13
+ allowJs: true,
14
+
15
+ // Disable type checking for JavaScript files
16
+ // checkJs: false,
17
+
18
+ // Skip type checking of declaration files
19
+ skipLibCheck: true,
20
+
21
+ // Support React JSX
22
+ jsx: ts.JsxEmit.React,
23
+ };
24
+
25
+ // Be less aggressive for non-TS files,
26
+ // e.g. files that end in .js or .jsx.
27
+ // if (!isTS(fileName)) {
28
+ // This code is for errors like:
29
+ // "Binding element 'data' implicitly has an 'any' type."
30
+ const LINT_ERROR_CODE_ANY = 7031;
31
+
32
+ // This code is for errors like:
33
+ // "Parameter 'selection' implicitly has an 'any' type.""
34
+ const LINT_ERROR_CODE_ANY_PARAM = 7006;
35
+
36
+ // This code is for errors like:
37
+ // "Cannot find module 'd3' or its corresponding type declarations."
38
+ const LINT_ERROR_CODE_IMPORT = 2307;
39
+
40
+ // This code is for errors like:
41
+ // "Variable 'mic' implicitly has type 'any' in some locations where its type cannot be determined."
42
+ const LINT_ERROR_CODE_ANY_TYPE = 7034;
43
+
44
+ // "Element implicitly has an 'any' type because expression
45
+ // of type '"test"' can't be used to index type '{}'."
46
+ const LINT_ERROR_CODE_ANY_TYPE_KEYS = 7053;
47
+
48
+ // "Property 'id' does not exist on type { ... }"
49
+ // Happens on objects with dynamic keys.
50
+ // Not valid in TypeScript, but common in JavaScript.
51
+ const LINT_ERROR_CODE_NON_EXISTENT_PROPERTY = 2339;
52
+
53
+ // "Type 'Set<unknown>' can only be iterated through when using the '--downlevelIteration'
54
+ // flag or with a '--target' of 'es2015' or higher."
55
+ const LINT_ERROR_CODE_ITERATED_THROUGH = 2802;
56
+
57
+ // Argument of type '{ Month: string; High: number; Temp: number; Low: number; }'
58
+ // is not assignable to parameter of type 'never'.
59
+ const LINT_ERROR_CODE_ASSIGNABLE_TO_NEVER = 2345;
60
+
61
+ // Cannot find name 'd3'.
62
+ export const LINT_ERROR_CODE_CANNOT_FIND_NAME = 2304;
63
+
64
+ export const excludedErrorCodes = new Set([
65
+ LINT_ERROR_CODE_ANY,
66
+ LINT_ERROR_CODE_IMPORT,
67
+ LINT_ERROR_CODE_ANY_PARAM,
68
+ LINT_ERROR_CODE_ANY_TYPE,
69
+ LINT_ERROR_CODE_ANY_TYPE_KEYS,
70
+ LINT_ERROR_CODE_NON_EXISTENT_PROPERTY,
71
+ LINT_ERROR_CODE_ITERATED_THROUGH,
72
+ LINT_ERROR_CODE_ASSIGNABLE_TO_NEVER,
73
+ ]);
@@ -0,0 +1,4 @@
1
+ // replace .js or .jsx with .ts or .tsx,
2
+ // to support TypeScript completions on non-TS files.
3
+ export const getTSFileName = (fileName: string) =>
4
+ fileName.replace(/\.jsx?$/, '.tsx');
@@ -0,0 +1,63 @@
1
+ import { getTSFileName } from './getTSFileName';
2
+ import { isTS } from './isTS';
3
+ import {
4
+ AutocompleteRequest,
5
+ AutocompleteResponse,
6
+ } from './requestTypes';
7
+
8
+ // This function is called when the worker receives a message with the type
9
+ // 'autocomplete-request'. It gets completions at the specified position in the
10
+ // file and sends them back to the main thread.
11
+ export const handleMessageAutocompleteRequest = async ({
12
+ debug,
13
+ data,
14
+ env,
15
+ setFile,
16
+ }) => {
17
+ if (debug) {
18
+ console.log('autocomplete-request message received');
19
+ }
20
+
21
+ // Should not happen.
22
+ if (env === null) {
23
+ console.log('env is null');
24
+ return;
25
+ }
26
+
27
+ // Example of `data`:
28
+ // {
29
+ // "event": "autocomplete-request",
30
+ // "pos": 8,
31
+ // "location": "index.js",
32
+ // "requestId": "0.9090605799171392"
33
+ // }
34
+
35
+ const autocompleteRequest: AutocompleteRequest = data;
36
+ const { fileName, fileContent, position, requestId } =
37
+ autocompleteRequest;
38
+
39
+ const tsFileName = getTSFileName(fileName);
40
+
41
+ let completions = null;
42
+ if (isTS(tsFileName) && fileContent !== '') {
43
+ // Update the file in the file system to the
44
+ // absolute latest version. This is critical
45
+ // for correct completions.
46
+ setFile(tsFileName, fileContent);
47
+
48
+ completions =
49
+ env.languageService.getCompletionsAtPosition(
50
+ tsFileName,
51
+ position,
52
+ {},
53
+ );
54
+ }
55
+
56
+ const autocompleteResponse: AutocompleteResponse = {
57
+ event: 'post-completions',
58
+ completions,
59
+ requestId,
60
+ };
61
+
62
+ postMessage(autocompleteResponse);
63
+ };
@@ -0,0 +1,84 @@
1
+ import ts from 'typescript';
2
+ import type { Diagnostic } from '@codemirror/lint';
3
+ import {
4
+ LinterRequest,
5
+ LinterResponse,
6
+ } from './requestTypes';
7
+ import { getTSFileName } from './getTSFileName';
8
+ import { isTS } from './isTS';
9
+ import {
10
+ LINT_ERROR_CODE_CANNOT_FIND_NAME,
11
+ excludedErrorCodes,
12
+ } from './constants';
13
+
14
+ // Inspired by: https://stackblitz.com/edit/codemirror-6-typescript?file=client%2Findex.ts%3AL44-L44
15
+ const convertToCodeMirrorDiagnostic = (
16
+ tsErrors: ts.Diagnostic[],
17
+ ): Array<Diagnostic> =>
18
+ tsErrors.map((tsError: ts.Diagnostic) => ({
19
+ from: tsError.start,
20
+ to: tsError.start + tsError.length,
21
+ severity: 'error',
22
+ message:
23
+ typeof tsError.messageText === 'string'
24
+ ? tsError.messageText
25
+ : tsError.messageText.messageText,
26
+ }));
27
+
28
+ export const handleMessageLintRequest = async ({
29
+ debug,
30
+ data,
31
+ env,
32
+ setFile,
33
+ }) => {
34
+ if (debug) {
35
+ console.log('Lint Request');
36
+ }
37
+ const linterRequest: LinterRequest = data;
38
+ const { fileName, fileContent, requestId, allowGlobals } =
39
+ linterRequest;
40
+
41
+ const tsFileName = getTSFileName(fileName);
42
+ let tsErrors = [];
43
+ if (isTS(tsFileName) && fileContent !== '') {
44
+ // We are updating the server with the latest content
45
+ // when we autocomplete, so it's always up to date.
46
+ setFile(tsFileName, fileContent);
47
+
48
+ // Creates an array of diagnostic objects containing
49
+ // both semantic and syntactic diagnostics.
50
+ tsErrors = env.languageService
51
+ .getSemanticDiagnostics(tsFileName)
52
+ .concat(
53
+ env.languageService.getSyntacticDiagnostics(
54
+ tsFileName,
55
+ ),
56
+ );
57
+
58
+ if (debug) {
59
+ console.log('tsErrors');
60
+ console.log(tsErrors);
61
+ }
62
+
63
+ tsErrors = tsErrors.filter(
64
+ (error: { code: number }) => {
65
+ if (
66
+ allowGlobals &&
67
+ error.code === LINT_ERROR_CODE_CANNOT_FIND_NAME
68
+ ) {
69
+ return false;
70
+ }
71
+ return !excludedErrorCodes.has(error.code);
72
+ },
73
+ );
74
+
75
+ tsErrors = convertToCodeMirrorDiagnostic(tsErrors);
76
+ }
77
+
78
+ const linterResponse: LinterResponse = {
79
+ event: 'post-error-linter',
80
+ tsErrors,
81
+ requestId,
82
+ };
83
+ postMessage(linterResponse);
84
+ };
@@ -0,0 +1,20 @@
1
+ import ts from 'typescript';
2
+ import { compilerOptions } from './constants';
3
+
4
+ // Handle the transpile-request event, which
5
+ // transpiles TypeScript to JavaScript.
6
+ export const handleMessageTranspileRequest = async ({
7
+ data,
8
+ }) => {
9
+ const tsCode = data.tsCode;
10
+
11
+ const jsCode = ts.transpileModule(tsCode, {
12
+ compilerOptions: compilerOptions,
13
+ }).outputText;
14
+
15
+ postMessage({
16
+ event: 'transpile-response',
17
+ jsCode,
18
+ fileId: data.fileId,
19
+ });
20
+ };
@@ -0,0 +1,38 @@
1
+ import { File, Files, VZCodeContent } from '../../../types';
2
+ import { getTSFileName } from './getTSFileName';
3
+ import { isTS } from './isTS';
4
+
5
+ // Handle the update-content event, which
6
+ // updates the files as they change.
7
+ // This handles the cases where:
8
+ // * The file system is populated for the first time.
9
+ // * Files are edited by remote users.
10
+ export const handleMessageUpdateContent = async ({
11
+ debug,
12
+ data,
13
+ setFile,
14
+ }) => {
15
+ if (debug) {
16
+ console.log('update-content message received');
17
+ }
18
+ // Unpack the files
19
+ const content: VZCodeContent = data.details;
20
+ const files: Files = content.files;
21
+
22
+ // Iterate over the files
23
+ for (const fileId of Object.keys(files)) {
24
+ const file: File = files[fileId];
25
+ const { name, text } = file;
26
+
27
+ const tsFileName = getTSFileName(name);
28
+
29
+ if (!isTS(tsFileName)) {
30
+ continue;
31
+ }
32
+
33
+ setFile(tsFileName, text);
34
+ // TODO - Handle renaming files.
35
+ // TODO - Handle deleting files.
36
+ // TODO - Handle directories.
37
+ }
38
+ };