vzcode 0.28.0 → 0.29.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 (45) hide show
  1. package/dist/assets/index-836bdce7.css +1 -0
  2. package/dist/assets/index-c7f894a9.js +115 -0
  3. package/dist/assets/worker-a2979249.js +366 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +10 -6
  6. package/src/client/AIAssist.ts +110 -19
  7. package/src/client/AIAssistWidget/index.tsx +101 -0
  8. package/src/client/AIAssistWidget/style.scss +10 -0
  9. package/src/client/App/VZCodeContext.tsx +251 -0
  10. package/src/client/App/VZLeft.tsx +56 -0
  11. package/src/client/App/VZMiddle.tsx +91 -0
  12. package/src/client/App/VZRight.tsx +15 -0
  13. package/src/client/App/index.tsx +104 -0
  14. package/src/client/{style.scss → App/style.scss} +14 -3
  15. package/src/client/App/useShareDB.ts +103 -0
  16. package/src/client/CodeEditor/getOrCreateEditor.ts +54 -6
  17. package/src/client/CodeEditor/index.tsx +9 -0
  18. package/src/client/CodeEditor/json1PresenceDisplay.ts +38 -9
  19. package/src/client/CodeEditor/typeScriptCompletions.ts +108 -0
  20. package/src/client/CodeEditor/typeScriptLinter.ts +45 -0
  21. package/src/client/CodeErrorOverlay/index.tsx +27 -1
  22. package/src/client/CodeErrorOverlay/style.scss +11 -11
  23. package/src/client/SplitPaneResizeContext.tsx +195 -39
  24. package/src/client/TabList/index.tsx +1 -27
  25. package/src/client/{Resizer → VZResizer}/index.tsx +37 -13
  26. package/src/client/VZSettings.tsx +1 -1
  27. package/src/client/VZSidebar/CreateFileModal.tsx +93 -0
  28. package/src/client/VZSidebar/DeleteConfirmationModal.tsx +42 -24
  29. package/src/client/VZSidebar/index.tsx +69 -9
  30. package/src/client/useFileCRUD.ts +19 -19
  31. package/src/client/useOpenDirectories.ts +12 -8
  32. package/src/client/usePrettier/index.ts +13 -1
  33. package/src/client/useTypeScript/index.ts +44 -0
  34. package/src/client/useTypeScript/requestTypes.ts +29 -0
  35. package/src/client/useTypeScript/worker.ts +222 -0
  36. package/src/client/usernameLocalStorage.ts +1 -1
  37. package/src/client/vzReducer/createInitialState.ts +4 -5
  38. package/src/index.ts +5 -1
  39. package/src/server/handleAIAssist.js +76 -12
  40. package/src/server/index.js +55 -2
  41. package/src/types.ts +78 -1
  42. package/dist/assets/index-a9585cbc.js +0 -111
  43. package/dist/assets/index-d30c9f1b.css +0 -1
  44. package/src/client/App.tsx +0 -286
  45. /package/src/client/{Resizer → VZResizer}/styles.scss +0 -0
@@ -0,0 +1,93 @@
1
+ import {
2
+ useState,
3
+ useCallback,
4
+ useRef,
5
+ useEffect,
6
+ } from 'react';
7
+ import { Modal, Form, Button } from '../bootstrap';
8
+
9
+ export const CreateFileModal = ({
10
+ show,
11
+ onClose,
12
+ onRename,
13
+ initialFileName = '',
14
+ }) => {
15
+ const [newName, setNewName] = useState(initialFileName);
16
+ const inputRef = useRef(null);
17
+
18
+ useEffect(() => {
19
+ if (show && inputRef.current) {
20
+ inputRef.current.focus();
21
+ }
22
+ }, [show]);
23
+
24
+ const handleNameChange = useCallback((event) => {
25
+ setNewName(event.target.value);
26
+ }, []);
27
+
28
+ const handleRenameClick = useCallback(() => {
29
+ onRename(newName);
30
+ setNewName('');
31
+ }, [newName, onRename]);
32
+
33
+ // Returns true if file name is valid, false otherwise.
34
+ const validateFileName = useCallback(
35
+ (fileName: string) => {
36
+ const regex =
37
+ /^[a-zA-Z0-9](?:[a-zA-Z0-9 ./+=_-]*[a-zA-Z0-9])?$/;
38
+ return regex.test(fileName);
39
+ },
40
+ [],
41
+ );
42
+
43
+ const handleKeyDown = useCallback(
44
+ (e) => {
45
+ if (
46
+ e.key === 'Enter' &&
47
+ (e.ctrlKey ||
48
+ e.shiftKey ||
49
+ (!e.altKey && !e.metaKey))
50
+ ) {
51
+ handleRenameClick();
52
+ }
53
+ },
54
+ [handleRenameClick],
55
+ );
56
+
57
+ return show ? (
58
+ <Modal
59
+ show={show}
60
+ onHide={onClose}
61
+ animation={false}
62
+ onKeyDown={handleKeyDown}
63
+ >
64
+ <Modal.Header closeButton>
65
+ <Modal.Title>Create File</Modal.Title>
66
+ </Modal.Header>
67
+ <Modal.Body>
68
+ <Form.Group className="mb-3" controlId="fileName">
69
+ <Form.Label>File Name</Form.Label>
70
+ <Form.Control
71
+ type="text"
72
+ value={newName}
73
+ onChange={handleNameChange}
74
+ ref={inputRef}
75
+ spellCheck="false"
76
+ />
77
+ <Form.Text className="text-muted">
78
+ Enter the name for your file
79
+ </Form.Text>
80
+ </Form.Group>
81
+ </Modal.Body>
82
+ <Modal.Footer>
83
+ <Button
84
+ variant="primary"
85
+ onClick={handleRenameClick}
86
+ disabled={!validateFileName(newName)}
87
+ >
88
+ Create File
89
+ </Button>
90
+ </Modal.Footer>
91
+ </Modal>
92
+ ) : null;
93
+ };
@@ -1,36 +1,54 @@
1
1
  import { Button, Modal } from '../bootstrap';
2
+ import { useCallback } from 'react';
2
3
 
3
4
  // Confirmation modal for deleting a file or directory.
4
5
  // Inspired by https://react-bootstrap.netlify.app/docs/components/modal/#live-demo
6
+
5
7
  export const DeleteConfirmationModal = ({
6
8
  show,
7
9
  onClose,
8
10
  onConfirm,
9
11
  isDirectory,
10
12
  name,
11
- }) => (
12
- <Modal show={show} onHide={onClose} centered>
13
- <Modal.Header closeButton onClick={onClose}>
14
- <Modal.Title>
15
- Delete {isDirectory ? 'Directory' : 'File'}
16
- </Modal.Title>
17
- </Modal.Header>
13
+ }) => {
14
+ const handleSubmit = useCallback(
15
+ (event) => {
16
+ event.preventDefault();
17
+ onConfirm();
18
+ },
19
+ [onConfirm],
20
+ );
21
+
22
+ return (
23
+ <Modal show={show} onHide={onClose} centered>
24
+ <form onSubmit={handleSubmit}>
25
+ <Modal.Header closeButton onClick={onClose}>
26
+ <Modal.Title>
27
+ Delete {isDirectory ? 'Directory' : 'File'}
28
+ </Modal.Title>
29
+ </Modal.Header>
18
30
 
19
- <Modal.Body>
20
- <p>
21
- This will delete the{' '}
22
- {isDirectory ? 'directory' : 'file'}{' '}
23
- <strong>{name}</strong>.
24
- </p>
25
- </Modal.Body>
31
+ <Modal.Body>
32
+ <p>
33
+ This will delete the{' '}
34
+ {isDirectory ? 'directory' : 'file'}{' '}
35
+ <strong>{name}</strong>.
36
+ </p>
37
+ </Modal.Body>
26
38
 
27
- <Modal.Footer>
28
- <Button variant="secondary" onClick={onClose}>
29
- Cancel
30
- </Button>
31
- <Button variant="primary" onClick={onConfirm}>
32
- Delete
33
- </Button>
34
- </Modal.Footer>
35
- </Modal>
36
- );
39
+ <Modal.Footer>
40
+ <Button
41
+ variant="secondary"
42
+ type="button"
43
+ onClick={onClose}
44
+ >
45
+ Cancel
46
+ </Button>
47
+ <Button variant="primary" type="submit">
48
+ Delete
49
+ </Button>
50
+ </Modal.Footer>
51
+ </form>
52
+ </Modal>
53
+ );
54
+ };
@@ -1,4 +1,10 @@
1
- import { useCallback, useContext, useMemo } from 'react';
1
+ import {
2
+ useCallback,
3
+ useContext,
4
+ useMemo,
5
+ useState,
6
+ useEffect,
7
+ } from 'react';
2
8
  import {
3
9
  FileId,
4
10
  FileTree,
@@ -11,14 +17,15 @@ import { sortFileTree } from '../sortFileTree';
11
17
  import { disableSettings } from '../featureFlags';
12
18
  import { SplitPaneResizeContext } from '../SplitPaneResizeContext';
13
19
  import { Listing } from './Listing';
20
+ import { CreateFileModal } from './CreateFileModal';
14
21
  import './styles.scss';
15
22
 
16
- const CreateFileButton = ({ createFile }) => {
23
+ const CreateFileButton = ({ handleCreateFile }) => {
17
24
  return (
18
25
  <i
19
26
  className="bx bxs-file-plus new-btn"
20
27
  style={{ color: '#dbdde1' }}
21
- onClick={createFile}
28
+ onClick={handleCreateFile}
22
29
  // TODO better tooltip
23
30
  title="Create file"
24
31
  ></i>
@@ -31,13 +38,14 @@ export const VZSidebar = ({
31
38
  deleteFile,
32
39
  deleteDirectory,
33
40
  openTab,
41
+ closeTabs,
34
42
  setIsSettingsOpen,
35
43
  isDirectoryOpen,
36
44
  toggleDirectory,
37
45
  activeFileId,
38
46
  }: {
39
47
  files: Files;
40
- createFile: () => void;
48
+ createFile: (fileName) => void;
41
49
  renameFile: (fileId: FileId, newName: string) => void;
42
50
  deleteFile: (fileId: FileId) => void;
43
51
  deleteDirectory: (path: FileTreePath) => void;
@@ -48,6 +56,7 @@ export const VZSidebar = ({
48
56
  fileId: FileId;
49
57
  isTransient?: boolean;
50
58
  }) => void;
59
+ closeTabs: (fileIds: FileId[]) => void;
51
60
  setIsSettingsOpen: (isSettingsOpen: boolean) => void;
52
61
  isDirectoryOpen: (path: string) => boolean;
53
62
  toggleDirectory: (path: string) => void;
@@ -58,11 +67,53 @@ export const VZSidebar = ({
58
67
  [files],
59
68
  );
60
69
 
70
+ const [isModalOpen, setIsModalOpen] = useState(false); // State to control the modal's visibility
71
+
72
+ const handleCreateFile = useCallback(() => {
73
+ setIsModalOpen(true);
74
+ }, [setIsModalOpen]);
75
+
76
+ const handleCloseModal = useCallback(() => {
77
+ setIsModalOpen(false);
78
+ }, [setIsModalOpen]);
79
+
80
+ const handleRename = useCallback(
81
+ (newFileName: string) => {
82
+ createFile(newFileName);
83
+ setIsModalOpen(false);
84
+ },
85
+ [createFile, setIsModalOpen],
86
+ );
87
+
88
+ const handleKeyPress = useCallback(
89
+ (event: { altKey: boolean; key: string }) => {
90
+ if (event.altKey == true) {
91
+ if (event.key == 'w') {
92
+ closeTabs([activeFileId]);
93
+ }
94
+ if (event.key == 'n') {
95
+ handleCreateFile();
96
+ }
97
+ }
98
+ },
99
+ [createFile, closeTabs, activeFileId],
100
+ );
101
+
102
+ useEffect(() => {
103
+ document.addEventListener('keydown', handleKeyPress);
104
+ return () => {
105
+ document.removeEventListener(
106
+ 'keydown',
107
+ handleKeyPress,
108
+ );
109
+ };
110
+ }, [handleKeyPress]);
111
+
61
112
  const handleSettingsClick = useCallback(() => {
62
113
  setIsSettingsOpen(true);
63
114
  }, []);
64
115
 
65
- const { codeEditorWidth } = useContext(
116
+ const { sidebarWidth } = useContext(
66
117
  SplitPaneResizeContext,
67
118
  );
68
119
 
@@ -91,13 +142,15 @@ export const VZSidebar = ({
91
142
  return (
92
143
  <div
93
144
  className="vz-sidebar"
94
- style={{ width: codeEditorWidth + 'px' }}
145
+ style={{ width: sidebarWidth + 'px' }}
95
146
  >
96
147
  <div className="files">
97
148
  <div className="full-box">
98
149
  <div className="sidebar-section-hint">Files</div>
99
150
  <div>
100
- <CreateFileButton createFile={createFile} />
151
+ <CreateFileButton
152
+ handleCreateFile={handleCreateFile}
153
+ />
101
154
  </div>
102
155
  </div>
103
156
  {filesExist ? (
@@ -130,7 +183,9 @@ export const VZSidebar = ({
130
183
  your first file.
131
184
  </div>
132
185
 
133
- <CreateFileButton createFile={createFile} />
186
+ <CreateFileButton
187
+ handleCreateFile={handleCreateFile}
188
+ />
134
189
  </div>
135
190
  )}
136
191
  </div>
@@ -139,9 +194,14 @@ export const VZSidebar = ({
139
194
  className="settings"
140
195
  onClick={handleSettingsClick}
141
196
  >
142
- Settings
197
+ Editor Settings
143
198
  </div>
144
199
  )}
200
+ <CreateFileModal
201
+ show={isModalOpen}
202
+ onClose={handleCloseModal}
203
+ onRename={handleRename}
204
+ />
145
205
  </div>
146
206
  );
147
207
  };
@@ -1,4 +1,4 @@
1
- import { useCallback } from 'react';
1
+ import { useCallback, useState } from 'react';
2
2
  import {
3
3
  FileId,
4
4
  FileTreePath,
@@ -26,24 +26,24 @@ export const useFileCRUD = ({
26
26
  }) => void;
27
27
  }) => {
28
28
  // Create a new file
29
- const createFile = useCallback(() => {
30
- // TODO use Bootstrap modals not native prompts
31
- // See https://github.com/vizhub-core/vzcode/issues/252
32
- const name = prompt('Enter new file name');
33
- if (name) {
34
- const fileId: FileId = randomId();
35
- submitOperation((document: VZCodeContent) => ({
36
- ...document,
37
- files: {
38
- ...document.files,
39
- [fileId]: { name, text: '' },
40
- },
41
- }));
42
- // When a new file is created, open it in a new tab
43
- // and focus the editor on it.
44
- openTab({ fileId, isTransient: false });
45
- }
46
- }, [submitOperation]);
29
+ const createFile = useCallback(
30
+ (name: string) => {
31
+ if (name) {
32
+ const fileId: FileId = randomId();
33
+ submitOperation((document: VZCodeContent) => ({
34
+ ...document,
35
+ files: {
36
+ ...document.files,
37
+ [fileId]: { name, text: '' },
38
+ },
39
+ }));
40
+ // When a new file is created, open it in a new tab
41
+ // and focus the editor on it.
42
+ openTab({ fileId, isTransient: false });
43
+ }
44
+ },
45
+ [submitOperation],
46
+ );
47
47
 
48
48
  // Called when a file in the sidebar is renamed.
49
49
  const renameFile = useCallback(
@@ -15,17 +15,22 @@ import { useState, useCallback } from 'react';
15
15
  // return openDirectories;
16
16
  //};
17
17
 
18
+ type VZPath = string;
19
+
18
20
  // Inspired by
19
21
  // https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/useOpenDirectories.js
20
22
  // TODO move this into reducer
21
- export const useOpenDirectories = () => {
23
+ export const useOpenDirectories = (): {
24
+ isDirectoryOpen: (path: VZPath) => boolean;
25
+ toggleDirectory: (path: VZPath) => void;
26
+ } => {
22
27
  // The set of open directories by path/
23
28
  const [openDirectories, setOpenDirectories] = useState<
24
- Set<string>
29
+ Set<VZPath>
25
30
  >(new Set());
26
31
 
27
32
  // Whether a directory is open.
28
- const isDirectoryOpen: (path: string) => boolean =
33
+ const isDirectoryOpen: (path: VZPath) => boolean =
29
34
  useCallback(
30
35
  (path) => openDirectories.has(path),
31
36
  [openDirectories],
@@ -37,21 +42,20 @@ export const useOpenDirectories = () => {
37
42
  * If it's closed, it will open it.
38
43
  */
39
44
  const toggleDirectory = useCallback(
40
- (directoryPath: string) => {
45
+ (path: VZPath) => {
41
46
  // Create a copy of the currently open directories
42
47
  const updatedOpenDirectories = new Set(
43
48
  openDirectories,
44
49
  );
45
50
 
46
51
  // Check if the directory is currently open
47
- const directoryIsOpen =
48
- isDirectoryOpen(directoryPath);
52
+ const directoryIsOpen = isDirectoryOpen(path);
49
53
 
50
54
  // Toggle the directory's state
51
55
  if (directoryIsOpen) {
52
- updatedOpenDirectories.delete(directoryPath);
56
+ updatedOpenDirectories.delete(path);
53
57
  } else {
54
- updatedOpenDirectories.add(directoryPath);
58
+ updatedOpenDirectories.add(path);
55
59
  }
56
60
 
57
61
  // Update the state with the modified set of open directories
@@ -7,7 +7,7 @@ import {
7
7
  import { useEffect, useRef, useState } from 'react';
8
8
 
9
9
  // The time in milliseconds by which auto-saving is debounced.
10
- const autoPrettierDebounceTimeMS = 1200;
10
+ export const autoPrettierDebounceTimeMS = 1200;
11
11
 
12
12
  // Computes a file extension from a file name.
13
13
  // Example: 'foo.js' => '.js'
@@ -128,6 +128,10 @@ export const usePrettier = (
128
128
  );
129
129
  }
130
130
 
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
+
131
135
  // Listen for changes
132
136
  shareDBDoc.on(
133
137
  'op batch',
@@ -186,6 +190,14 @@ export const usePrettier = (
186
190
  'message',
187
191
  handleMessage,
188
192
  );
193
+ window.removeEventListener(
194
+ 'keydown',
195
+ debouncePrettier,
196
+ );
197
+ window.removeEventListener(
198
+ 'mousemove',
199
+ debouncePrettier,
200
+ );
189
201
 
190
202
  // Clear the timeout
191
203
  clearTimeout(debounceTimeout);
@@ -0,0 +1,44 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import { VZCodeContent } from '../../types';
3
+ import { autoPrettierDebounceTimeMS } from '../usePrettier';
4
+
5
+ // We don't want to send the message _before_ Prettier runs,
6
+ // so we need to wait at least as long as Prettier takes to run,
7
+ // plus some "wiggle room" which is amount of time it may take
8
+ // to aactually run Prettier and update the document.
9
+ const wiggleRoom = 500;
10
+
11
+ // The time in milliseconds by which auto-saving is debounced.
12
+ const sendMessageDebounceTimeMS =
13
+ autoPrettierDebounceTimeMS + wiggleRoom;
14
+
15
+ export const useTypeScript = ({
16
+ content,
17
+ typeScriptWorker,
18
+ }: {
19
+ content: VZCodeContent;
20
+ typeScriptWorker: Worker;
21
+ }) => {
22
+ // When Content changes, update the TypeScript worker
23
+ // with the new content, but debounced.
24
+
25
+ // This keeps track of the setTimeout ID across renders.
26
+ const debounceTimeoutId = useRef<number | null>(null);
27
+
28
+ const debounceUpdateContent = useCallback(
29
+ (content: VZCodeContent) => {
30
+ clearTimeout(debounceTimeoutId.current);
31
+ debounceTimeoutId.current = window.setTimeout(() => {
32
+ typeScriptWorker.postMessage({
33
+ event: 'update-content',
34
+ details: content,
35
+ });
36
+ }, sendMessageDebounceTimeMS);
37
+ },
38
+ [typeScriptWorker],
39
+ );
40
+
41
+ useEffect(() => {
42
+ debounceUpdateContent(content);
43
+ }, [content]);
44
+ };
@@ -0,0 +1,29 @@
1
+ import ts from 'typescript';
2
+ import { VZCodeContent } from '../../types';
3
+
4
+ export type AutocompleteRequest = {
5
+ event: 'autocomplete-request';
6
+ fileName: string;
7
+ fileContent: string;
8
+ position: number;
9
+ requestId: string;
10
+ };
11
+
12
+ export type AutocompleteResponse = {
13
+ event: 'post-completions';
14
+ completions: any;
15
+ requestId: string;
16
+ };
17
+
18
+ export type LinterRequest = {
19
+ event: 'lint-request';
20
+ fileName: string;
21
+ fileContent: string;
22
+ requestId: string;
23
+ };
24
+
25
+ export type LinterResponse = {
26
+ event: 'post-error-linter';
27
+ tsErrors: ts.Diagnostic[];
28
+ requestId: string;
29
+ };