vzcode 0.43.0 → 0.45.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
@@ -24,8 +24,8 @@
24
24
  href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css"
25
25
  rel="stylesheet"
26
26
  />
27
- <script type="module" crossorigin src="/assets/index-WNLp-4Rf.js"></script>
28
- <link rel="stylesheet" crossorigin href="/assets/index-vD_kE3p2.css">
27
+ <script type="module" crossorigin src="/assets/index-pub_uACz.js"></script>
28
+ <link rel="stylesheet" crossorigin href="/assets/index-iQRmsAKc.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -71,11 +71,11 @@
71
71
  "@codemirror/lang-markdown": "^6.2.3",
72
72
  "@codemirror/state": "^6.3.3",
73
73
  "@codemirror/theme-one-dark": "^6.1.2",
74
- "@codemirror/view": "^6.22.2",
74
+ "@codemirror/view": "^6.22.3",
75
75
  "@lezer/highlight": "^1.2.0",
76
76
  "@replit/codemirror-indentation-markers": "^6.5.0",
77
77
  "@replit/codemirror-interact": "^6.3.0",
78
- "@replit/codemirror-minimap": "^0.5.1",
78
+ "@replit/codemirror-minimap": "^0.5.2",
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",
@@ -96,26 +96,27 @@
96
96
  "diff-match-patch": "^1.0.5",
97
97
  "dotenv": "^16.3.1",
98
98
  "express": "^4.18.2",
99
+ "ignore": "^5.3.0",
99
100
  "json0-ot-diff": "^1.1.2",
100
101
  "ngrok": "^4.3.3",
101
- "open": "^9.1.0",
102
- "openai": "^4.20.1",
102
+ "open": "^10.0.0",
103
+ "openai": "^4.24.0",
103
104
  "react": "^18.2.0",
104
105
  "react-bootstrap": "^2.9.1",
105
106
  "react-dom": "^18.2.0",
106
107
  "sharedb": "^4.1.2",
107
108
  "sharedb-client-browser": "^4.4.0",
108
- "vizhub-ui": "^3.10.0",
109
- "ws": "^8.15.0"
109
+ "vizhub-ui": "^3.14.0",
110
+ "ws": "^8.15.1"
110
111
  },
111
112
  "devDependencies": {
112
- "@types/react": "^18.2.43",
113
- "@types/react-dom": "^18.2.17",
113
+ "@types/react": "^18.2.45",
114
+ "@types/react-dom": "^18.2.18",
114
115
  "@vitejs/plugin-react": "^4.2.1",
115
116
  "prettier": "^3.1.1",
116
117
  "sass": "^1.69.5",
117
118
  "typescript": "^5.3.3",
118
- "vite": "^5.0.7",
119
- "vitest": "^1.0.4"
119
+ "vite": "^5.0.10",
120
+ "vitest": "^1.1.0"
120
121
  }
121
122
  }
@@ -294,7 +294,20 @@ export const getOrCreateEditor = ({
294
294
 
295
295
  // VSCode keybindings
296
296
  // See https://github.com/replit/codemirror-vscode-keymap#usage
297
- extensions.push(keymap.of(vscodeKeymap));
297
+ // extensions.push(keymap.of(vscodeKeymap));
298
+ extensions.push(
299
+ keymap.of(
300
+ vscodeKeymap.map((binding) => {
301
+ // Here we override the Shift+Enter behavior specifically,
302
+ // as that can be used to trigger a manual save/Prettier,
303
+ // and the default behavior from the keymap interferes.
304
+ if (binding.key === 'Enter') {
305
+ delete binding.shift;
306
+ }
307
+ return binding;
308
+ }),
309
+ ),
310
+ );
298
311
 
299
312
  const editor = new EditorView({
300
313
  state: EditorState.create({
@@ -94,6 +94,7 @@ export const VZCodeProvider = ({
94
94
  initialUsername,
95
95
  children,
96
96
  codeError = null,
97
+ enableManualPretter = false,
97
98
  }: {
98
99
  content: VZCodeContent;
99
100
  shareDBDoc: ShareDBDoc<VZCodeContent>;
@@ -107,17 +108,19 @@ export const VZCodeProvider = ({
107
108
  initialUsername: Username;
108
109
  children: React.ReactNode;
109
110
  codeError?: string | null;
111
+ enableManualPretter?: boolean;
110
112
  }) => {
111
113
  // Auto-run Pretter after local changes.
112
114
  const {
113
115
  prettierError,
114
116
  }: {
115
117
  prettierError: string | null;
116
- } = usePrettier(
118
+ } = usePrettier({
117
119
  shareDBDoc,
118
120
  submitOperation,
119
121
  prettierWorker,
120
- );
122
+ enableManualPretter,
123
+ });
121
124
 
122
125
  // The error message shows either:
123
126
  // * `prettierError` - errors from Prettier, client-side only
@@ -6,7 +6,7 @@ import { VZCodeContext } from './VZCodeContext';
6
6
  // The middle portion of the VZCode environment, containing:
7
7
  // * The sidebar
8
8
  // * The settings modal
9
- export const VZLeft = () => {
9
+ export const VZLeft = ({ enableUsernameField = true }) => {
10
10
  // TODO leverage this context in deeper levels of the component tree.
11
11
  const {
12
12
  files,
@@ -50,6 +50,7 @@ export const VZLeft = () => {
50
50
  setTheme={setTheme}
51
51
  username={username}
52
52
  setUsername={setUsername}
53
+ enableUsernameField={enableUsernameField}
53
54
  />
54
55
  </div>
55
56
  );
@@ -13,7 +13,7 @@ import { VZCodeContext } from './VZCodeContext';
13
13
  // * The code error overlay
14
14
  // * The presence notifications
15
15
  // * The UI for AI Assist
16
- export const VZMiddle = () => {
16
+ export const VZMiddle = ({ enableAIAssist = true }) => {
17
17
  const { codeEditorWidth } = useContext(
18
18
  SplitPaneResizeContext,
19
19
  );
@@ -78,7 +78,7 @@ export const VZMiddle = () => {
78
78
  docPresence={docPresence}
79
79
  localPresence={localPresence}
80
80
  />
81
- {content && activeFileId ? (
81
+ {enableAIAssist && content && activeFileId ? (
82
82
  <AIAssistWidget
83
83
  activeFileId={activeFileId}
84
84
  shareDBDoc={shareDBDoc}
@@ -12,3 +12,4 @@ export {
12
12
  SplitPaneResizeContext,
13
13
  } from './SplitPaneResizeContext';
14
14
  export { useSubmitOperation } from './useSubmitOperation';
15
+ export { shouldTriggerRun } from './usePrettier';
@@ -1,4 +1,4 @@
1
- import { JSONOp } from 'ot-json1';
1
+ import { JSONOp } from '../../ot';
2
2
  import {
3
3
  FileId,
4
4
  ShareDBDoc,
@@ -20,16 +20,58 @@ const extension = (fileName: string) => {
20
20
  }
21
21
  };
22
22
 
23
- export const usePrettier = (
24
- shareDBDoc: ShareDBDoc<VZCodeContent> | null,
23
+ // export const shouldTriggerRun = (event: KeyboardEvent) => {
24
+ // if(event.ctrlKey && event.key === 's') {
25
+ // return true;
26
+ // }
27
+ // }
28
+
29
+ export const shouldTriggerRun = (event: KeyboardEvent) => {
30
+ const isMac = /Mac|iMac|MacBook/i.test(
31
+ navigator.userAgent,
32
+ );
33
+ const ctrlOrCmd = isMac ? event.metaKey : event.ctrlKey;
34
+
35
+ if (ctrlOrCmd && event.key === 's') {
36
+ // Save
37
+ return true;
38
+ } else if (
39
+ event.key === 'F5' || // Run (F5)
40
+ (ctrlOrCmd && event.key === 'Enter') || // Ctrl+Enter or Cmd+Enter
41
+ (event.shiftKey && event.key === 'Enter') || // Shift+Enter
42
+ // Let's not override the browser's default behavior for refresh page.
43
+ // (ctrlOrCmd && event.key === 'r') || // Ctrl+R or Cmd+R
44
+
45
+ (event.altKey && event.key === 'Enter') || // Alt+Enter
46
+ (ctrlOrCmd &&
47
+ event.shiftKey &&
48
+ (event.key === 'B' || event.key === 'b')) || // Ctrl+Shift+B or Cmd+Shift+B
49
+ (ctrlOrCmd && event.shiftKey && event.key === 'F10') || // Ctrl+Shift+F10 or Cmd+Shift+F10
50
+ event.key === 'F8' ||
51
+ event.key === 'F9'
52
+ ) {
53
+ return true;
54
+ }
55
+ return false;
56
+ };
57
+
58
+ export const usePrettier = ({
59
+ shareDBDoc,
60
+ submitOperation,
61
+ prettierWorker,
62
+ enableManualPretter,
63
+ }: {
64
+ shareDBDoc: ShareDBDoc<VZCodeContent> | null;
25
65
  submitOperation: (
26
66
  next: (content: VZCodeContent) => VZCodeContent,
27
- ) => void,
28
- prettierWorker: Worker,
29
- ) => {
67
+ ) => void;
68
+ prettierWorker: Worker;
69
+ enableManualPretter: boolean;
70
+ }) => {
30
71
  // The set of files that have been modified
31
72
  // since the last Prettier run.
32
- const dirtyFiles: Set<FileId> = new Set<FileId>();
73
+ // const dirtyFiles: Set<FileId> = new Set<FileId>();
74
+ const dirtyFilesRef = useRef<Set<FileId>>(new Set());
33
75
 
34
76
  // State to hold the error from Prettier
35
77
  // `null` means no errors
@@ -95,6 +137,9 @@ export const usePrettier = (
95
137
  // Get the files
96
138
  const files = content.files;
97
139
 
140
+ // Get the set of dirty files
141
+ const dirtyFiles = dirtyFilesRef.current;
142
+
98
143
  // Get the dirty files
99
144
  const dirtyFileIds = Array.from(dirtyFiles);
100
145
 
@@ -118,20 +163,35 @@ export const usePrettier = (
118
163
  }
119
164
  };
120
165
 
121
- // Function to debounce running Prettier
166
+ let debouncePrettier;
122
167
  let debounceTimeout;
123
- function debouncePrettier() {
124
- clearTimeout(debounceTimeout);
125
- debounceTimeout = setTimeout(
126
- runPrettier,
127
- autoPrettierDebounceTimeMS,
168
+ let handleKeyDown;
169
+ if (enableManualPretter) {
170
+ handleKeyDown = (event: KeyboardEvent) => {
171
+ if (shouldTriggerRun(event)) {
172
+ event.preventDefault();
173
+ runPrettier();
174
+ }
175
+ };
176
+ document.addEventListener('keydown', handleKeyDown);
177
+ } else {
178
+ // Function to debounce running Prettier
179
+
180
+ debouncePrettier = () => {
181
+ clearTimeout(debounceTimeout);
182
+ debounceTimeout = setTimeout(
183
+ runPrettier,
184
+ autoPrettierDebounceTimeMS,
185
+ );
186
+ };
187
+ //Stops prettier from running when you perform a mouse move or a keydown
188
+ window.addEventListener(
189
+ 'mousemove',
190
+ debouncePrettier,
128
191
  );
192
+ window.addEventListener('keydown', debouncePrettier);
129
193
  }
130
194
 
131
- //Stops prettier from running when you perform a mouse move or a keydown
132
- window.addEventListener('mousemove', debouncePrettier);
133
- window.addEventListener('keydown', debouncePrettier);
134
-
135
195
  // Listen for changes
136
196
  shareDBDoc.on(
137
197
  'op batch',
@@ -170,11 +230,12 @@ export const usePrettier = (
170
230
  const fileId: FileId = op[1];
171
231
 
172
232
  // Add the file id to the set of dirty files
173
- dirtyFiles.add(fileId);
233
+ dirtyFilesRef.current.add(fileId);
174
234
 
175
235
  // Debounce running Prettier
176
- debouncePrettier();
177
-
236
+ if (!enableManualPretter) {
237
+ debouncePrettier();
238
+ }
178
239
  // console.log('Op is for file', fileId);
179
240
  }
180
241
  } else {
@@ -190,17 +251,24 @@ export const usePrettier = (
190
251
  'message',
191
252
  handleMessage,
192
253
  );
193
- window.removeEventListener(
194
- 'keydown',
195
- debouncePrettier,
196
- );
197
- window.removeEventListener(
198
- 'mousemove',
199
- debouncePrettier,
200
- );
254
+ if (enableManualPretter) {
255
+ document.removeEventListener(
256
+ 'keydown',
257
+ handleKeyDown,
258
+ );
259
+ } else {
260
+ window.removeEventListener(
261
+ 'keydown',
262
+ debouncePrettier,
263
+ );
264
+ window.removeEventListener(
265
+ 'mousemove',
266
+ debouncePrettier,
267
+ );
201
268
 
202
- // Clear the timeout
203
- clearTimeout(debounceTimeout);
269
+ // Clear the timeout
270
+ clearTimeout(debounceTimeout);
271
+ }
204
272
  };
205
273
  }, [shareDBDoc]);
206
274
  return { prettierError }; // Return the errors for use in other files
package/src/index.ts CHANGED
@@ -21,4 +21,5 @@ export {
21
21
  SplitPaneResizeProvider,
22
22
  SplitPaneResizeContext,
23
23
  useSubmitOperation,
24
+ shouldTriggerRun,
24
25
  } from './client';
@@ -4,10 +4,68 @@ import { randomId } from '../randomId.js';
4
4
  import {
5
5
  enableDirectories,
6
6
  debugDirectories,
7
+ debugIgnore,
7
8
  } from './featureFlags.js';
9
+ import createIgnore from 'ignore';
10
+ import { ignoreFilePattern, baseIgnore } from './config.js';
8
11
 
9
12
  const isDirectory = (file) => file.endsWith('/');
10
13
 
14
+ /**
15
+ * @param {string} fullPath - absolut path of the workspace root
16
+ * @param {string} currentDirectoryPath - path where the ignore file is found, relative to fullPath
17
+ * @param {string} fileName - name of the ignore file
18
+ * @returns {string[]} parsed lines
19
+ */
20
+ const parseIgnoreFile = (
21
+ fullPath,
22
+ currentDirectory,
23
+ fileName,
24
+ ) => {
25
+ const filePath = path.join(
26
+ fullPath,
27
+ currentDirectory,
28
+ fileName,
29
+ );
30
+ const content = fs.readFileSync(filePath, 'utf8');
31
+ const globs = content
32
+ .split(/[\n\r]+/)
33
+ .filter(
34
+ // remove blank line and comments
35
+ (line) => line.length > 0 && !line.startsWith('#'),
36
+ )
37
+ .map((line) => {
38
+ const { bang, slash, glob } = line.match(
39
+ /^(?<bang>!?)(?<slash>\/?)(?<glob>.*)$/,
40
+ ).groups;
41
+ const hasSlash =
42
+ Boolean(slash) || /\/.*\S/.test(glob);
43
+ const relativeGlob = path.posix.join(
44
+ currentDirectory.replace(
45
+ // escape characters with special meaning in glob expressions
46
+ /[*?!# \[\]\\]/g,
47
+ (char) => '\\' + char,
48
+ ),
49
+ // a pattern that doesn't include a slash (not counting a trailing one) matches files in any descendant directory od the current one
50
+ hasSlash ? '' : '**',
51
+ glob,
52
+ );
53
+ // preserve leading `!` and `/` characters
54
+ return bang + slash + relativeGlob;
55
+ });
56
+ if (debugIgnore) {
57
+ console.debug(
58
+ 'at',
59
+ currentDirectory,
60
+ 'parsing',
61
+ fileName,
62
+ 'obtained globs',
63
+ globs,
64
+ );
65
+ }
66
+ return globs;
67
+ };
68
+
11
69
  // Lists files from the file system,
12
70
  // converts them into the VZCode internal
13
71
  // ShareDB-compatible data structure.
@@ -15,8 +73,6 @@ export const computeInitialDocument = ({ fullPath }) => {
15
73
  // Isolate files, not directories.
16
74
  // Inspired by https://stackoverflow.com/questions/41472161/fs-readdir-ignore-directories
17
75
 
18
- const unsearchedDirectories = [''];
19
-
20
76
  // Initialize the document using our data structure for representing files.
21
77
  const initialDocument = {
22
78
  // `files`
@@ -34,39 +90,92 @@ export const computeInitialDocument = ({ fullPath }) => {
34
90
  isInteracting: false,
35
91
  };
36
92
 
37
- // Stack for recursively traversing directories.
93
+ /**
94
+ * Stack for recursively traversing directories.
95
+ * @type {string[]}
96
+ */
38
97
  let files = [];
39
98
 
99
+ const ignoreFileMatcher = createIgnore().add(
100
+ ignoreFilePattern,
101
+ );
102
+ const isIgnoreFile = (fileName) =>
103
+ ignoreFileMatcher.ignores(fileName);
104
+
105
+ const unsearchedDirectories = [
106
+ {
107
+ currentDirectory: '.',
108
+ ignore: createIgnore().add(baseIgnore),
109
+ },
110
+ ];
111
+
40
112
  while (unsearchedDirectories.length !== 0) {
41
- const currentDirectory = unsearchedDirectories.pop();
113
+ const { currentDirectory, ignore: parentIgnore } =
114
+ unsearchedDirectories.pop();
42
115
  const currentDirectoryPath = path.join(
43
116
  fullPath,
44
117
  currentDirectory,
45
118
  );
46
- const newFiles = fs
119
+ const dirEntries = fs
47
120
  .readdirSync(currentDirectoryPath, {
48
121
  withFileTypes: true,
49
122
  })
50
123
  .filter((dirent) =>
51
124
  enableDirectories ? true : dirent.isFile(),
125
+ );
126
+ // find .ignore or .gitignore files in the current directory
127
+ const ignoreFiles = dirEntries
128
+ .filter(
129
+ (dirent) =>
130
+ dirent.isFile() && isIgnoreFile(dirent.name),
52
131
  )
53
-
132
+ .map((file) => file.name);
133
+ let ignore = parentIgnore;
134
+ if (ignoreFiles.length > 0) {
135
+ const globs = ignoreFiles.flatMap((fileName) =>
136
+ parseIgnoreFile(
137
+ fullPath,
138
+ currentDirectory,
139
+ fileName,
140
+ ),
141
+ );
142
+ ignore = createIgnore().add(parentIgnore).add(globs);
143
+ }
144
+ const newFiles = dirEntries
145
+ .filter((dirent) => {
146
+ const relativePath =
147
+ path.posix.join(currentDirectory, dirent.name) +
148
+ (dirent.isDirectory() ? '/' : '');
149
+ const keep = !ignore.ignores(relativePath);
150
+ if (debugIgnore && !keep) {
151
+ console.debug(
152
+ 'at',
153
+ currentDirectory,
154
+ 'ignoring',
155
+ relativePath,
156
+ );
157
+ }
158
+ return keep;
159
+ })
54
160
  // Add a trailing slash for directories
55
161
  .map((dirent) => {
56
- const relativePath =
57
- currentDirectory === ''
58
- ? dirent.name
59
- : currentDirectory + '/' + dirent.name;
162
+ const relativePath = path.posix.join(
163
+ currentDirectory,
164
+ dirent.name,
165
+ );
60
166
 
61
- if (dirent.isFile()) {
167
+ if (!dirent.isDirectory()) {
62
168
  return relativePath;
63
169
  }
64
- unsearchedDirectories.push(relativePath);
170
+ unsearchedDirectories.push({
171
+ currentDirectory: relativePath,
172
+ ignore,
173
+ });
65
174
  return relativePath + '/';
66
175
  });
67
176
  // console.log(currentDirectory);
68
177
 
69
- files = [...files, ...newFiles];
178
+ files.push(...newFiles);
70
179
  }
71
180
 
72
181
  files.forEach((file) => {
@@ -0,0 +1,24 @@
1
+ /**
2
+ *patterns describing which files should e comsidered as ignore files when creating the file tree.
3
+ *
4
+ * Format: one pattern per line, in gitignore format as defimed by https://git-scm.com/docs/gitignore#_pattern_format
5
+ */
6
+ export const ignoreFilePattern =
7
+ process.env.IGNORE_FILE_PATTERN ??
8
+ `
9
+ .vzignore
10
+ .ignore
11
+ .gitignore
12
+ `;
13
+
14
+ /**
15
+ * the base ignore patterns, that should be applied before any ignore file found when creating the file tree.
16
+ *
17
+ * These patterns apply even in the absence of an ignore file.
18
+ */
19
+ export const baseIgnore =
20
+ process.env.BASE_IGNORE ??
21
+ `
22
+ .git/
23
+ node_modules/
24
+ `;
@@ -1,4 +1,10 @@
1
1
  // Feature flag for directories, disabled until it's fully working.
2
2
  export const enableDirectories = true;
3
3
 
4
- export const debugDirectories = false;
4
+ export const debugDirectories = Boolean(
5
+ process.env.DEBUG_FILE_TREE,
6
+ );
7
+
8
+ export const debugIgnore = Boolean(
9
+ process.env.DEBUG_IGNORE,
10
+ );
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import './setupEnv.js';
2
3
  import http from 'http';
3
4
  import express from 'express';
4
5
  import ShareDB from 'sharedb';
@@ -9,7 +10,6 @@ import path from 'path';
9
10
  import { fileURLToPath } from 'url';
10
11
  import open from 'open';
11
12
  import ngrok from 'ngrok';
12
- import dotenv from 'dotenv';
13
13
  import bodyParser from 'body-parser';
14
14
  import { json1Presence } from '../ot.js';
15
15
  import { computeInitialDocument } from './computeInitialDocument.js';
@@ -21,8 +21,6 @@ import {
21
21
 
22
22
  const { replaceOp } = json1Presence;
23
23
 
24
- dotenv.config({ path: '../../.env' });
25
-
26
24
  // The time in milliseconds by which auto-saving is debounced.
27
25
  const autoSaveDebounceTimeMS = 800;
28
26
 
@@ -0,0 +1,18 @@
1
+ import dotenv from 'dotenv';
2
+ import { fileURLToPath } from 'url';
3
+ import { join } from 'node:path';
4
+
5
+ const dir = fileURLToPath(import.meta.url);
6
+ dotenv.config({
7
+ path: join(dir, '../../../.env'),
8
+ });
9
+ if (process.env.DEBUG_SETUP_ENV) {
10
+ console.log(
11
+ 'environment variables set up',
12
+ Object.fromEntries(
13
+ Object.entries(process.env).filter(
14
+ (entry) => !entry[0].endsWith('PATH'),
15
+ ),
16
+ ),
17
+ );
18
+ }
package/src/types.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  // FileId
2
2
  // * A unique ID for a file.
3
3
 
4
- import { JSONOp } from 'ot-json1';
5
-
6
4
  // * This is a random string.
7
5
  export type FileId = string;
8
6
 
@@ -60,6 +58,8 @@ export interface FileTreeFile {
60
58
  fileId: FileId;
61
59
  }
62
60
 
61
+ export type JSONOp = any;
62
+
63
63
  // `ShareDBDoc`
64
64
  // A ShareDB Document.
65
65
  // TODO better types? Integrate with upstream how?
@@ -142,7 +142,7 @@ export type AIStreamStatus = {
142
142
  export type VZCodeContent = {
143
143
  // `files`
144
144
  // * The files in the VZCode instance.
145
- files: Files;
145
+ files?: Files;
146
146
 
147
147
  // `isInteracting`
148
148
  // * `true` when the user is interacting