vzcode 0.4.0 → 0.5.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
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>VZCode</title>
7
- <script type="module" crossorigin src="/assets/index-48dbd5c3.js"></script>
8
- <link rel="stylesheet" href="/assets/index-8d2d4b3b.css">
7
+ <script type="module" crossorigin src="/assets/index-523ecad7.js"></script>
8
+ <link rel="stylesheet" href="/assets/index-29b6a606.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
- "bin": "cli.js",
7
+ "bin": "src/server/index.js",
8
8
  "files": [
9
9
  "dist/**",
10
10
  "src/**"
11
11
  ],
12
12
  "scripts": {
13
- "test": "npm run build; cd test; node ../cli.js",
13
+ "test": "vitest",
14
+ "test-interactive": "npm run build; cd test/sampleDirectories/kitchenSink; node ../../../src/server/index.js",
14
15
  "prettier": "prettier {*.*,**/*.*} --write",
15
16
  "dev": "vite",
16
17
  "build": "vite build",
@@ -52,9 +53,12 @@
52
53
  "@teamwork/websocket-json-stream": "^2.0.0",
53
54
  "codemirror": "^6.0.1",
54
55
  "codemirror-ot": "^4.0.0",
56
+ "d3-array": "^3.2.3",
55
57
  "diff-match-patch": "^1.0.5",
58
+ "dotenv": "^16.0.3",
56
59
  "express": "^4.18.2",
57
60
  "json0-ot-diff": "^1.1.2",
61
+ "ngrok": "^4.3.3",
58
62
  "open": "^9.0.0",
59
63
  "react": "^18.2.0",
60
64
  "react-bootstrap": "^2.7.2",
@@ -69,6 +73,7 @@
69
73
  "@types/react-dom": "^18.0.11",
70
74
  "@vitejs/plugin-react": "^3.1.0",
71
75
  "prettier": "^2.8.6",
72
- "vite": "^4.2.1"
76
+ "vite": "^4.2.1",
77
+ "vitest": "^0.30.1"
73
78
  }
74
79
  }
@@ -1,10 +1,11 @@
1
1
  import { useState, useEffect, useMemo, useCallback } from 'react';
2
2
  import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
3
- import { json1Presence } from './ot';
3
+ import { json1Presence } from '../ot';
4
+ import { randomId } from '../randomId';
4
5
  import { CodeEditor } from './CodeEditor';
5
6
  import { diff } from './diff';
6
- import { randomId } from './randomId';
7
7
  import { Settings } from './settings';
8
+ import { Sidebar } from './Sidebar';
8
9
  import './style.css';
9
10
 
10
11
  // Register our custom JSON1 OT type that supports presence.
@@ -18,10 +19,6 @@ const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
18
19
  const socket = new WebSocket(wsProtocol + window.location.host + '/ws');
19
20
  const connection = new Connection(socket);
20
21
 
21
- // A feature flag to disable settings for release, until it's completed.
22
- // See https://vitejs.dev/guide/env-and-mode.html
23
- const disableSettings = import.meta.env.VITE_DISABLE_SETTINGS === 'true';
24
-
25
22
  function App() {
26
23
  // The ShareDB document.
27
24
  const [shareDBDoc, setShareDBDoc] = useState(null);
@@ -51,10 +48,6 @@ function App() {
51
48
  // The current theme.
52
49
  const [theme, setTheme] = useState('OneDark');
53
50
 
54
- // The currently hovered over file, for showing
55
- // "utils", meaning buttons for rename and delete.
56
- const [utils, setUtils] = useState(false);
57
-
58
51
  // True to show the settings modal.
59
52
  const [settings, setSettings] = useState(false);
60
53
 
@@ -144,7 +137,7 @@ function App() {
144
137
  );
145
138
 
146
139
  // Called when a file in the sidebar is double-clicked.
147
- const renameFile = useCallback(
140
+ const handleRenameFileClick = useCallback(
148
141
  (key) => {
149
142
  const newName = prompt('Enter new name');
150
143
  if (newName) {
@@ -176,6 +169,16 @@ function App() {
176
169
  [shareDBDoc, closeTab]
177
170
  );
178
171
 
172
+ const handleFileClick = useCallback(
173
+ (fileId) => {
174
+ setActiveFileId(fileId);
175
+ if (!tabList.includes(fileId)) {
176
+ setTabList([...tabList, fileId]);
177
+ }
178
+ },
179
+ [tabList]
180
+ );
181
+
179
182
  // TODO prompt the user "Are you sure?"
180
183
  const handleDeleteFileClick = useCallback(
181
184
  (fileId) => (event) => {
@@ -205,6 +208,11 @@ function App() {
205
208
  setSettings(false);
206
209
  }, []);
207
210
 
211
+ const fileNameSplit = (fileName) => {
212
+ const split = fileName.split('/');
213
+ return split[split.length - 1];
214
+ };
215
+
208
216
  return (
209
217
  <>
210
218
  <Settings
@@ -223,7 +231,7 @@ function App() {
223
231
  setActiveFileId(fileId);
224
232
  }}
225
233
  >
226
- {tabValid ? data[fileId].name : ''}
234
+ {tabValid ? fileNameSplit(data[fileId].name) : ''}
227
235
  <div
228
236
  className={activeFileId ? 'bx bx-x tab-close' : ''}
229
237
  onClick={handleCloseTabClick(fileId)}
@@ -231,74 +239,15 @@ function App() {
231
239
  </div>
232
240
  ))}
233
241
  </div>
234
- <div className="sidebar show">
235
- <div className="nav-links">
236
- <div className="show-menu">
237
- <div className="sub-menu">
238
- <div className="files">
239
- <div className="full-Box">
240
- <div>
241
- <a className="link-name" href="#">
242
- Files
243
- </a>
244
- </div>
245
- <div>
246
- <i
247
- className="bx bxs-file-plus newBTN"
248
- style={{ color: '#dbdde1' }}
249
- onClick={createFile}
250
- ></i>
251
- </div>
252
- </div>
253
- </div>
254
- {data
255
- ? Object.keys(data).map((key) => (
256
- <div className="file" key={key}>
257
- <div
258
- className="full-Box"
259
- onClick={() => {
260
- setActiveFileId(key);
261
- if (!tabList.includes(key)) {
262
- setTabList([...tabList, key]);
263
- }
264
- }}
265
- onMouseEnter={() => {
266
- setUtils(key);
267
- }}
268
- onMouseLeave={() => {
269
- setUtils(null);
270
- }}
271
- >
272
- <div className={utils === key ? 'hover-name' : 'name'}>
273
- {data[key].name}
274
- </div>
275
- <div className={utils === key ? 'utils' : 'noUtils'}>
276
- <i
277
- className="bx bxs-edit utilities"
278
- style={{ color: '#abdafb' }}
279
- onClick={() => {
280
- renameFile(key);
281
- }}
282
- ></i>
283
- <i
284
- className="bx bx-trash"
285
- style={{ color: '#eb336c' }}
286
- onClick={handleDeleteFileClick(key)}
287
- ></i>
288
- </div>
289
- </div>
290
- </div>
291
- ))
292
- : null}
293
- </div>
294
- </div>
295
- {disableSettings ? null : (
296
- <div className="settings" onClick={() => setSettings(!settings)}>
297
- Settings
298
- </div>
299
- )}
300
- </div>
301
- </div>
242
+ <Sidebar
243
+ createFile={createFile}
244
+ data={data}
245
+ handleRenameFileClick={handleRenameFileClick}
246
+ handleDeleteFileClick={handleDeleteFileClick}
247
+ handleFileClick={handleFileClick}
248
+ setSettings={setSettings}
249
+ settings={settings}
250
+ />
302
251
  {data && activeFileId ? (
303
252
  <CodeEditor
304
253
  className="editor"
@@ -0,0 +1,62 @@
1
+ import { useCallback } from 'react';
2
+ import { Item } from './Item';
3
+ import { FileOrDirectory } from './FileOrDirectory';
4
+ import { DirectoryArrowSVG } from './DirectoryArrowSVG';
5
+
6
+ export const Directory = ({
7
+ name,
8
+ path,
9
+ children,
10
+ handleRenameFileClick,
11
+ handleDeleteFileClick,
12
+ handleFileClick,
13
+ openDirectories,
14
+ toggleDirectory,
15
+ }) => {
16
+ const handleClick = useCallback(() => {
17
+ toggleDirectory(path);
18
+ }, [toggleDirectory]);
19
+
20
+ const handleDeleteClick = useCallback(() => {
21
+ console.log('TODO handleDeleteDirectoryClick');
22
+ }, []);
23
+
24
+ const handleRenameClick = useCallback(() => {
25
+ console.log('TODO handleRenameDirectoryClick');
26
+ }, []);
27
+
28
+ const isOpen = openDirectories[path];
29
+ return (
30
+ <>
31
+ <Item
32
+ name={name}
33
+ handleClick={handleClick}
34
+ handleDeleteClick={handleDeleteClick}
35
+ handleRenameClick={handleRenameClick}
36
+ >
37
+ <div
38
+ className="arrow-wrapper"
39
+ style={{ transform: `rotate(${isOpen ? 90 : 0}deg)` }}
40
+ >
41
+ <DirectoryArrowSVG />
42
+ </div>
43
+ {name}
44
+ </Item>
45
+ {children && isOpen ? (
46
+ <div className="indentation">
47
+ {children.map((entity) => (
48
+ <FileOrDirectory
49
+ entity={entity}
50
+ key={entity.fileId || entity.path}
51
+ handleRenameFileClick={handleRenameFileClick}
52
+ handleDeleteFileClick={handleDeleteFileClick}
53
+ handleFileClick={handleFileClick}
54
+ openDirectories={openDirectories}
55
+ toggleDirectory={toggleDirectory}
56
+ />
57
+ ))}
58
+ </div>
59
+ ) : null}
60
+ </>
61
+ );
62
+ };
@@ -0,0 +1,5 @@
1
+ export const DirectoryArrowSVG = ({ height = 10, fill = 'currentcolor' }) => (
2
+ <svg height={height} viewBox={'4 4 16 16'}>
3
+ <path fill={fill} d="M8 17l9-5-9-5v10z" />
4
+ </svg>
5
+ );
@@ -0,0 +1,32 @@
1
+ import { useCallback } from 'react';
2
+ import { Item } from './Item';
3
+
4
+ export const File = ({
5
+ name,
6
+ fileId,
7
+ handleRenameFileClick,
8
+ handleDeleteFileClick,
9
+ handleFileClick,
10
+ }) => {
11
+ const handleClick = useCallback(() => {
12
+ handleFileClick(fileId);
13
+ }, [fileId, handleFileClick]);
14
+
15
+ const handleDeleteClick = useCallback(() => {
16
+ handleDeleteFileClick(fileId);
17
+ }, [fileId]);
18
+
19
+ const handleRenameClick = useCallback(() => {
20
+ handleRenameFileClick(fileId);
21
+ }, [fileId]);
22
+
23
+ return (
24
+ <Item
25
+ handleClick={handleClick}
26
+ handleDeleteClick={handleDeleteClick}
27
+ handleRenameClick={handleRenameClick}
28
+ >
29
+ {name}
30
+ </Item>
31
+ );
32
+ };
@@ -0,0 +1,33 @@
1
+ import { File } from './File';
2
+ import { Directory } from './Directory';
3
+
4
+ export const FileOrDirectory = ({
5
+ entity,
6
+ handleRenameFileClick,
7
+ handleDeleteFileClick,
8
+ handleFileClick,
9
+ openDirectories,
10
+ toggleDirectory,
11
+ }) => {
12
+ const { name, path, fileId, file, children } = entity;
13
+ return file ? (
14
+ <File
15
+ fileId={fileId}
16
+ name={name}
17
+ handleRenameFileClick={handleRenameFileClick}
18
+ handleDeleteFileClick={handleDeleteFileClick}
19
+ handleFileClick={handleFileClick}
20
+ />
21
+ ) : (
22
+ <Directory
23
+ name={name}
24
+ path={path}
25
+ children={children}
26
+ handleRenameFileClick={handleRenameFileClick}
27
+ handleDeleteFileClick={handleDeleteFileClick}
28
+ handleFileClick={handleFileClick}
29
+ openDirectories={openDirectories}
30
+ toggleDirectory={toggleDirectory}
31
+ />
32
+ );
33
+ };
@@ -0,0 +1,43 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export const Item = ({
4
+ children,
5
+ handleClick,
6
+ handleRenameClick,
7
+ handleDeleteClick,
8
+ }) => {
9
+ const [isHovered, setIsHovered] = useState(false);
10
+
11
+ const handleMouseEnter = useCallback(() => {
12
+ setIsHovered(true);
13
+ }, []);
14
+
15
+ const handleMouseLeave = useCallback(() => {
16
+ setIsHovered(false);
17
+ }, []);
18
+
19
+ return (
20
+ <div
21
+ className="full-box file-or-directory"
22
+ onClick={handleClick}
23
+ onMouseEnter={handleMouseEnter}
24
+ onMouseLeave={handleMouseLeave}
25
+ >
26
+ <div className="name">{children}</div>
27
+ {isHovered ? (
28
+ <div className="utils">
29
+ <i
30
+ className="bx bxs-edit utilities"
31
+ style={{ color: '#abdafb' }}
32
+ onClick={handleRenameClick}
33
+ ></i>
34
+ <i
35
+ className="bx bx-trash"
36
+ style={{ color: '#eb336c' }}
37
+ onClick={handleDeleteClick}
38
+ ></i>
39
+ </div>
40
+ ) : null}
41
+ </div>
42
+ );
43
+ };
@@ -0,0 +1,64 @@
1
+ import { useMemo } from 'react';
2
+ import { getFileTree } from '../getFileTree';
3
+ import { sortFileTree } from '../sortFileTree';
4
+ import { disableSettings } from '../featureFlags';
5
+ import { FileOrDirectory } from './FileOrDirectory';
6
+ import { useOpenDirectories } from './useOpenDirectories';
7
+
8
+ import './styles.css';
9
+
10
+ export const Sidebar = ({
11
+ createFile,
12
+ data,
13
+ handleRenameFileClick,
14
+ handleDeleteFileClick,
15
+ handleFileClick,
16
+ setSettings,
17
+ settings,
18
+ }) => {
19
+ const fileTree = useMemo(
20
+ () => (data ? sortFileTree(getFileTree(data)) : null),
21
+ [data]
22
+ );
23
+
24
+ const { openDirectories, toggleDirectory } = useOpenDirectories();
25
+
26
+ return (
27
+ <div className="vz-sidebar">
28
+ <div className="files">
29
+ <div className="full-box">
30
+ <div>
31
+ <a className="link-name" href="#">
32
+ Files
33
+ </a>
34
+ </div>
35
+ <div>
36
+ <i
37
+ className="bx bxs-file-plus new-btn"
38
+ style={{ color: '#dbdde1' }}
39
+ onClick={createFile}
40
+ ></i>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ {fileTree
45
+ ? fileTree.children.map((entity) => (
46
+ <FileOrDirectory
47
+ entity={entity}
48
+ key={entity.fileId || entity.path}
49
+ handleRenameFileClick={handleRenameFileClick}
50
+ handleDeleteFileClick={handleDeleteFileClick}
51
+ handleFileClick={handleFileClick}
52
+ openDirectories={openDirectories}
53
+ toggleDirectory={toggleDirectory}
54
+ />
55
+ ))
56
+ : null}
57
+ {disableSettings ? null : (
58
+ <div className="settings" onClick={() => setSettings(!settings)}>
59
+ Settings
60
+ </div>
61
+ )}
62
+ </div>
63
+ );
64
+ };
@@ -0,0 +1,88 @@
1
+ .vz-sidebar {
2
+ /* TODO remove position: fixed, overhaul CSS to use Flexbox for entire app */
3
+
4
+ position: fixed;
5
+ top: 0;
6
+ left: 0;
7
+ height: 100%;
8
+ width: 200px;
9
+ background: rgb(54, 65, 89);
10
+ }
11
+
12
+ .vz-sidebar .full-box {
13
+ padding: 7px;
14
+ display: flex;
15
+ flex-direction: row;
16
+ align-items: center;
17
+ justify-content: space-between;
18
+ }
19
+
20
+ .vz-sidebar .file-or-directory {
21
+ display: flex;
22
+ flex-direction: row;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ cursor: pointer;
26
+ margin: 0 10px;
27
+ color: rgb(219, 221, 225);
28
+ border-radius: 5px;
29
+ }
30
+
31
+ .vz-sidebar .link-name {
32
+ margin-top: 30px;
33
+ margin-left: 10px;
34
+ color: rgb(154, 160, 172);
35
+ font-size: 12px;
36
+ text-decoration: none;
37
+ }
38
+
39
+ .vz-sidebar .new-btn {
40
+ justify-content: right;
41
+ margin-right: 10px;
42
+ cursor: pointer;
43
+ }
44
+
45
+ .vz-sidebar .utils {
46
+ display: flex;
47
+ }
48
+
49
+ .vz-sidebar .utilities {
50
+ margin-right: 3px;
51
+ }
52
+
53
+ .vz-sidebar .settings {
54
+ position: absolute;
55
+ bottom: 0;
56
+ height: 30px;
57
+ width: 180px;
58
+ z-index: 100;
59
+ color: rgb(219, 221, 225);
60
+ cursor: pointer;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ padding: 14px 0;
65
+ margin: 10px;
66
+ border-radius: 5px;
67
+ }
68
+
69
+ .vz-sidebar .file-or-directory:hover,
70
+ .vz-sidebar .settings:hover {
71
+ background-color: rgb(74, 84, 105);
72
+ }
73
+
74
+ .vz-sidebar .name {
75
+ display: flex;
76
+ text-overflow: ellipsis;
77
+ white-space: nowrap;
78
+ overflow: hidden;
79
+ }
80
+
81
+ /* Responsible for indenting directories */
82
+ .vz-sidebar .indentation {
83
+ margin-left: 20px;
84
+ }
85
+
86
+ .vz-sidebar .arrow-wrapper {
87
+ margin-right: 4px;
88
+ }
@@ -0,0 +1,39 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ // TODO bring this feature back
4
+ // When a page is opened with an active file,
5
+ // make sure all the directories leading to that file
6
+ // are opened automatically.
7
+ //export const initialOpenDirectories = (activeFile) => {
8
+ // const openDirectories = {};
9
+ // if (activeFile) {
10
+ // const path = activeFile.split('/');
11
+ // for (let i = 1; i < path.length; i++) {
12
+ // openDirectories[path.slice(0, i).join('/')] = true;
13
+ // }
14
+ // }
15
+ // return openDirectories;
16
+ //};
17
+
18
+ // Inspired by
19
+ // https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/useOpenDirectories.js
20
+ export const useOpenDirectories = (activeFile) => {
21
+ //const [openDirectories, setOpenDirectories] = useState(
22
+ // initialOpenDirectories(activeFile)
23
+ //);
24
+ const [openDirectories, setOpenDirectories] = useState({});
25
+
26
+ // TODO consider useReducer to avoid event listener churn
27
+ const toggleDirectory = useCallback(
28
+ (path) => {
29
+ console.log(openDirectories[path], 'here');
30
+ setOpenDirectories(
31
+ Object.assign({}, openDirectories, {
32
+ [path]: !openDirectories[path],
33
+ })
34
+ );
35
+ },
36
+ [openDirectories]
37
+ );
38
+ return { openDirectories, toggleDirectory };
39
+ };
@@ -1,6 +1,6 @@
1
1
  import jsondiff from 'json0-ot-diff';
2
2
  import diffMatchPatch from 'diff-match-patch';
3
- import { json1Presence, textUnicode } from './ot';
3
+ import { json1Presence, textUnicode } from '../ot';
4
4
 
5
5
  export const diff = (a, b) =>
6
6
  jsondiff(a, b, diffMatchPatch, json1Presence, textUnicode);
@@ -0,0 +1,3 @@
1
+ // A feature flag to disable settings for release, until it's completed.
2
+ // See https://vitejs.dev/guide/env-and-mode.html
3
+ export const disableSettings = import.meta.env.VITE_DISABLE_SETTINGS === 'true';
@@ -0,0 +1,49 @@
1
+ // Inspired by
2
+ // https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/FileTree/getFileTree.js
3
+ export const getFileTree = (files) => {
4
+ const tree = { name: 'files' };
5
+ for (const fileId of Object.keys(files)) {
6
+ const file = files[fileId];
7
+ const path = file.name.split('/');
8
+ const n = path.length;
9
+ let node = tree;
10
+
11
+ // Walk the path.
12
+ for (let i = 0; i < n - 1; i++) {
13
+ // Search for an existing child.
14
+ let child;
15
+ const name = path[i];
16
+ if (node.children) {
17
+ for (let j = 0; j < node.children.length; j++) {
18
+ const nodeChild = node.children[j];
19
+ if (nodeChild.name === name) {
20
+ child = nodeChild;
21
+ break;
22
+ }
23
+ }
24
+ }
25
+
26
+ // Create a child if none with matching name exists.
27
+ if (!child) {
28
+ child = {
29
+ name: path[i],
30
+ path: path.slice(0, i + 1).join('/'),
31
+ };
32
+ (node.children || (node.children = [])).push(child);
33
+ }
34
+
35
+ node = child;
36
+ }
37
+
38
+ // file.text being `null` signifies
39
+ // that this is a directory.
40
+ if (file.text !== null) {
41
+ (node.children || (node.children = [])).push({
42
+ name: path[n - 1],
43
+ file,
44
+ fileId,
45
+ });
46
+ }
47
+ }
48
+ return tree;
49
+ };
@@ -6,7 +6,7 @@ import { html } from '@codemirror/lang-html';
6
6
  import { css } from '@codemirror/lang-css';
7
7
  import { oneDark } from '@codemirror/theme-one-dark';
8
8
  import { json1Sync } from 'codemirror-ot';
9
- import { json1Presence, textUnicode } from './ot';
9
+ import { json1Presence, textUnicode } from '../ot';
10
10
  import { json1PresenceBroadcast } from './json1PresenceBroadcast';
11
11
  import { json1PresenceDisplay } from './json1PresenceDisplay';
12
12
 
@@ -23,7 +23,6 @@ export const getOrCreateEditor = ({
23
23
  shareDBDoc,
24
24
  localPresence,
25
25
  docPresence,
26
- theme,
27
26
  }) => {
28
27
  const data = shareDBDoc.data;
29
28
 
@@ -5,7 +5,6 @@ import {
5
5
  Decoration,
6
6
  } from '@codemirror/view';
7
7
  import { Annotation, RangeSet } from '@codemirror/state';
8
- import { randomId } from './randomId';
9
8
 
10
9
  // Deals with receiving the broadcasted presence cursor locations
11
10
  // from other clients and displaying them.
@@ -1,4 +1,4 @@
1
- import { useState, useCallback } from 'react';
1
+ import { useCallback } from 'react';
2
2
  import Modal from 'react-bootstrap/Modal';
3
3
  import Form from 'react-bootstrap/Form';
4
4
  import Button from 'react-bootstrap/Button';