vzcode 0.4.0 → 0.6.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/README.md +47 -5
- package/dist/assets/index-41607816.js +89 -0
- package/dist/assets/index-8bac68c9.css +1 -0
- package/dist/index.html +16 -2
- package/package.json +29 -19
- package/src/{App.jsx → client/App.tsx} +83 -126
- package/src/client/Sidebar/DirectoryArrowSVG.jsx +5 -0
- package/src/client/Sidebar/DirectoryListing.tsx +76 -0
- package/src/client/Sidebar/FileListing.tsx +42 -0
- package/src/client/Sidebar/Item.jsx +43 -0
- package/src/client/Sidebar/Listing.tsx +44 -0
- package/src/client/Sidebar/index.tsx +80 -0
- package/src/client/Sidebar/styles.scss +84 -0
- package/src/client/Sidebar/useOpenDirectories.ts +37 -0
- package/src/{diff.js → client/diff.js} +1 -1
- package/src/client/featureFlags.js +3 -0
- package/src/client/getFileTree.ts +52 -0
- package/src/{getOrCreateEditor.js → client/getOrCreateEditor.js} +6 -6
- package/src/{json1PresenceDisplay.js → client/json1PresenceDisplay.js} +0 -1
- package/src/client/settings.jsx +97 -0
- package/src/client/sortFileTree.ts +19 -0
- package/src/client/style.scss +93 -0
- package/src/server/computeInitialDocument.js +69 -0
- package/src/server/featureFlags.js +4 -0
- package/{cli.js → src/server/index.js} +24 -30
- package/src/types.ts +57 -0
- package/dist/assets/index-48dbd5c3.js +0 -89
- package/dist/assets/index-8d2d4b3b.css +0 -1
- package/src/settings.jsx +0 -141
- package/src/style.css +0 -215
- /package/src/{CodeEditor.jsx → client/CodeEditor.jsx} +0 -0
- /package/src/{json1PresenceBroadcast.js → client/json1PresenceBroadcast.js} +0 -0
- /package/src/{main.jsx → client/main.jsx} +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { getFileTree } from '../getFileTree';
|
|
3
|
+
import { sortFileTree } from '../sortFileTree';
|
|
4
|
+
import { disableSettings } from '../featureFlags';
|
|
5
|
+
import { Listing } from './Listing';
|
|
6
|
+
import { useOpenDirectories } from './useOpenDirectories';
|
|
7
|
+
|
|
8
|
+
import './styles.scss';
|
|
9
|
+
import { FileId, FileTree, FileTreeFile, Files } from '../../types';
|
|
10
|
+
|
|
11
|
+
export const Sidebar = ({
|
|
12
|
+
createFile,
|
|
13
|
+
files,
|
|
14
|
+
handleRenameFileClick,
|
|
15
|
+
handleDeleteFileClick,
|
|
16
|
+
handleFileClick,
|
|
17
|
+
setIsSettingsOpen,
|
|
18
|
+
isSettingsOpen,
|
|
19
|
+
}: {
|
|
20
|
+
files: Files;
|
|
21
|
+
createFile?: () => void;
|
|
22
|
+
handleRenameFileClick?: (fileId: FileId) => void;
|
|
23
|
+
handleDeleteFileClick?: (fileId: FileId, event: React.MouseEvent) => void;
|
|
24
|
+
handleFileClick?: (fileId: FileId) => void;
|
|
25
|
+
setIsSettingsOpen?: (isSettingsOpen: boolean) => void;
|
|
26
|
+
isSettingsOpen?: boolean;
|
|
27
|
+
}) => {
|
|
28
|
+
const fileTree = useMemo(
|
|
29
|
+
() => (files ? sortFileTree(getFileTree(files)) : null),
|
|
30
|
+
[files]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const { openDirectories, toggleDirectory } = useOpenDirectories();
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="vz-sidebar">
|
|
37
|
+
<div className="files">
|
|
38
|
+
<div className="full-box">
|
|
39
|
+
<div>
|
|
40
|
+
<a className="link-name" href="#">
|
|
41
|
+
Files
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
44
|
+
<div>
|
|
45
|
+
<i
|
|
46
|
+
className="bx bxs-file-plus new-btn"
|
|
47
|
+
style={{ color: '#dbdde1' }}
|
|
48
|
+
onClick={createFile}
|
|
49
|
+
></i>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
{fileTree
|
|
53
|
+
? fileTree.children.map((entity) => {
|
|
54
|
+
const { fileId } = entity as FileTreeFile;
|
|
55
|
+
const { path } = entity as FileTree;
|
|
56
|
+
return (
|
|
57
|
+
<Listing
|
|
58
|
+
entity={entity}
|
|
59
|
+
key={fileId || path}
|
|
60
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
61
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
62
|
+
handleFileClick={handleFileClick}
|
|
63
|
+
openDirectories={openDirectories}
|
|
64
|
+
toggleDirectory={toggleDirectory}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
})
|
|
68
|
+
: null}
|
|
69
|
+
</div>
|
|
70
|
+
{disableSettings ? null : (
|
|
71
|
+
<div
|
|
72
|
+
className="settings"
|
|
73
|
+
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
|
74
|
+
>
|
|
75
|
+
Settings
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
.vz-sidebar {
|
|
2
|
+
height: 100%;
|
|
3
|
+
width: 200px;
|
|
4
|
+
background: rgb(54, 65, 89);
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
|
|
9
|
+
.full-box {
|
|
10
|
+
padding: 7px;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: row;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.files {
|
|
18
|
+
overflow: auto;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.file-or-directory {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: row;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
margin: 0 10px;
|
|
28
|
+
color: rgb(219, 221, 225);
|
|
29
|
+
border-radius: 5px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.link-name {
|
|
33
|
+
margin-top: 30px;
|
|
34
|
+
margin-left: 10px;
|
|
35
|
+
color: rgb(154, 160, 172);
|
|
36
|
+
font-size: 12px;
|
|
37
|
+
text-decoration: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.new-btn {
|
|
41
|
+
justify-content: right;
|
|
42
|
+
margin-right: 10px;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.utils {
|
|
47
|
+
display: flex;
|
|
48
|
+
}
|
|
49
|
+
.utilities {
|
|
50
|
+
margin-right: 3px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.file-or-directory:hover,
|
|
54
|
+
.settings:hover {
|
|
55
|
+
background-color: rgb(74, 84, 105);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.name {
|
|
59
|
+
display: flex;
|
|
60
|
+
text-overflow: ellipsis;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.indentation {
|
|
66
|
+
margin-left: 20px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.arrow-wrapper {
|
|
70
|
+
margin-right: 4px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.settings {
|
|
74
|
+
height: 30px;
|
|
75
|
+
color: rgb(219, 221, 225);
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
padding: 14px 0;
|
|
81
|
+
margin: 10px;
|
|
82
|
+
border-radius: 5px;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 = () => {
|
|
21
|
+
const [openDirectories, setOpenDirectories] = useState<{
|
|
22
|
+
[path: string]: boolean;
|
|
23
|
+
}>({});
|
|
24
|
+
|
|
25
|
+
// TODO consider useReducer to avoid event listener churn
|
|
26
|
+
const toggleDirectory = useCallback(
|
|
27
|
+
(path) => {
|
|
28
|
+
setOpenDirectories(
|
|
29
|
+
Object.assign({}, openDirectories, {
|
|
30
|
+
[path]: !openDirectories[path],
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
[openDirectories]
|
|
35
|
+
);
|
|
36
|
+
return { openDirectories, toggleDirectory };
|
|
37
|
+
};
|
|
@@ -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 '
|
|
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,52 @@
|
|
|
1
|
+
// Inspired by
|
|
2
|
+
|
|
3
|
+
import { Files, FileTree } from '../types';
|
|
4
|
+
|
|
5
|
+
// https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/FileTree/getFileTree.js
|
|
6
|
+
export const getFileTree = (files: Files): FileTree => {
|
|
7
|
+
const tree: FileTree = { name: 'files' };
|
|
8
|
+
for (const fileId of Object.keys(files)) {
|
|
9
|
+
const file = files[fileId];
|
|
10
|
+
const path = file.name.split('/');
|
|
11
|
+
const n = path.length;
|
|
12
|
+
let node = tree;
|
|
13
|
+
|
|
14
|
+
// Walk the path.
|
|
15
|
+
for (let i = 0; i < n - 1; i++) {
|
|
16
|
+
// Search for an existing child.
|
|
17
|
+
let child: FileTree | undefined;
|
|
18
|
+
const name = path[i];
|
|
19
|
+
if (node.children) {
|
|
20
|
+
for (let j = 0; j < node.children.length; j++) {
|
|
21
|
+
const nodeChild = node.children[j];
|
|
22
|
+
if (nodeChild.name === name) {
|
|
23
|
+
child = nodeChild;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create a child if none with matching name exists.
|
|
30
|
+
if (!child) {
|
|
31
|
+
child = {
|
|
32
|
+
name: path[i],
|
|
33
|
+
path: path.slice(0, i + 1).join('/'),
|
|
34
|
+
};
|
|
35
|
+
(node.children || (node.children = [])).push(child);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
node = child;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// file.text being `null` signifies
|
|
42
|
+
// that this is a directory.
|
|
43
|
+
if (file.text !== null) {
|
|
44
|
+
(node.children || (node.children = [])).push({
|
|
45
|
+
name: path[n - 1],
|
|
46
|
+
file,
|
|
47
|
+
fileId,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return tree;
|
|
52
|
+
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { EditorView, basicSetup } from 'codemirror';
|
|
2
|
-
import { EditorState } from '@codemirror/state';
|
|
2
|
+
import { Compartment, EditorState } from '@codemirror/state';
|
|
3
3
|
import { javascript } from '@codemirror/lang-javascript';
|
|
4
4
|
import { markdown } from '@codemirror/lang-markdown';
|
|
5
5
|
import { html } from '@codemirror/lang-html';
|
|
6
6
|
import { css } from '@codemirror/lang-css';
|
|
7
|
-
import { oneDark } from '@codemirror/theme-one-dark';
|
|
8
7
|
import { json1Sync } from 'codemirror-ot';
|
|
9
|
-
import { json1Presence, textUnicode } from '
|
|
8
|
+
import { json1Presence, textUnicode } from '../ot';
|
|
10
9
|
import { json1PresenceBroadcast } from './json1PresenceBroadcast';
|
|
11
10
|
import { json1PresenceDisplay } from './json1PresenceDisplay';
|
|
12
11
|
|
|
@@ -23,7 +22,7 @@ export const getOrCreateEditor = ({
|
|
|
23
22
|
shareDBDoc,
|
|
24
23
|
localPresence,
|
|
25
24
|
docPresence,
|
|
26
|
-
theme
|
|
25
|
+
theme
|
|
27
26
|
}) => {
|
|
28
27
|
const data = shareDBDoc.data;
|
|
29
28
|
|
|
@@ -32,6 +31,8 @@ export const getOrCreateEditor = ({
|
|
|
32
31
|
// The path for this file in the ShareDB document.
|
|
33
32
|
const path = [fileId, 'text'];
|
|
34
33
|
|
|
34
|
+
let themeSet = new Compartment();
|
|
35
|
+
|
|
35
36
|
const extensions = [
|
|
36
37
|
// This plugin implements multiplayer editing,
|
|
37
38
|
// real-time synchronozation of changes across clients.
|
|
@@ -50,7 +51,7 @@ export const getOrCreateEditor = ({
|
|
|
50
51
|
json1PresenceDisplay({ path, docPresence }),
|
|
51
52
|
|
|
52
53
|
basicSetup,
|
|
53
|
-
|
|
54
|
+
themeSet.of(theme),
|
|
54
55
|
];
|
|
55
56
|
|
|
56
57
|
if (
|
|
@@ -70,7 +71,6 @@ export const getOrCreateEditor = ({
|
|
|
70
71
|
|
|
71
72
|
let editor = editorCache.get(fileId);
|
|
72
73
|
if (!editor) {
|
|
73
|
-
// Cache miss --> mint a new editor.
|
|
74
74
|
editor = new EditorView({
|
|
75
75
|
state: EditorState.create({
|
|
76
76
|
doc: data[fileId].text,
|
|
@@ -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.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import Modal from 'react-bootstrap/Modal';
|
|
3
|
+
import Form from 'react-bootstrap/Form';
|
|
4
|
+
import Button from 'react-bootstrap/Button';
|
|
5
|
+
import Select from 'react-select';
|
|
6
|
+
import { oneDark } from '@codemirror/theme-one-dark';
|
|
7
|
+
import { okaidia } from '@uiw/codemirror-theme-okaidia';
|
|
8
|
+
import { abcdef } from '@uiw/codemirror-theme-abcdef';
|
|
9
|
+
import { dracula } from '@uiw/codemirror-theme-dracula';
|
|
10
|
+
import { eclipse } from '@uiw/codemirror-theme-eclipse';
|
|
11
|
+
import { githubDark } from '@uiw/codemirror-theme-github';
|
|
12
|
+
import { material } from '@uiw/codemirror-theme-material';
|
|
13
|
+
import { nord } from '@uiw/codemirror-theme-nord';
|
|
14
|
+
import { xcodeLight } from '@uiw/codemirror-theme-xcode';
|
|
15
|
+
|
|
16
|
+
// TODO document where this list came from
|
|
17
|
+
// TODO research which themes are available for CodeMirror 6
|
|
18
|
+
const themes = [
|
|
19
|
+
{ value: abcdef, label: 'abcdef' },
|
|
20
|
+
{ value: dracula, label: 'darcula' },
|
|
21
|
+
{ value: eclipse, label: 'eclipse' },
|
|
22
|
+
{ value: material, label: 'material' },
|
|
23
|
+
{ value: nord, label: 'nord' },
|
|
24
|
+
{ value: oneDark, label: 'One Dark' },
|
|
25
|
+
{ value: okaidia, label: 'okaidia' },
|
|
26
|
+
{ value: githubDark, label: 'github' },
|
|
27
|
+
{ value: xcodeLight, label: 'xcode' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const saveTimes = [
|
|
31
|
+
{ value: 1, label: '1 second' },
|
|
32
|
+
{ value: 5, label: '5 seconds' },
|
|
33
|
+
{ value: 10, label: '10 seconds' },
|
|
34
|
+
{ value: 30, label: '30 seconds' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const Settings = ({ show, onClose, setTheme, editor }) => {
|
|
38
|
+
const handleChange = useCallback((selectedOption) => {
|
|
39
|
+
setTheme(selectedOption.value);
|
|
40
|
+
editor.dispatch({
|
|
41
|
+
effects: themeSet.reconfigure(selectedOption.value),
|
|
42
|
+
});
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const handleSaveTimeChange = useCallback((selectedOption) => {
|
|
46
|
+
const time = [{ value: selectedOption.value }];
|
|
47
|
+
fetch('/saveTime', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(time),
|
|
53
|
+
});
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
return show ? (
|
|
57
|
+
<Modal show={show} onHide={onClose} animation={false}>
|
|
58
|
+
<Modal.Header closeButton>
|
|
59
|
+
<Modal.Title>Settings</Modal.Title>
|
|
60
|
+
</Modal.Header>
|
|
61
|
+
<Modal.Body>
|
|
62
|
+
<Form.Group className="mb-3" controlId="formFork">
|
|
63
|
+
<Form.Label>Username</Form.Label>
|
|
64
|
+
<Form.Control type="text" placeholder="Enter username" />
|
|
65
|
+
<Form.Text className="text-muted">
|
|
66
|
+
Enter a username to be displayed on your cursor
|
|
67
|
+
</Form.Text>
|
|
68
|
+
</Form.Group>
|
|
69
|
+
<Form.Group className="mb-3" controlId="formFork">
|
|
70
|
+
<Form.Label>Theme</Form.Label>
|
|
71
|
+
<Select
|
|
72
|
+
classvalue="themes"
|
|
73
|
+
options={themes}
|
|
74
|
+
onChange={handleChange}
|
|
75
|
+
/>
|
|
76
|
+
<Form.Text className="text-muted">
|
|
77
|
+
Select a color theme for the editor
|
|
78
|
+
</Form.Text>
|
|
79
|
+
</Form.Group>
|
|
80
|
+
<Form.Group className="mb-3" controlId="formFork">
|
|
81
|
+
<Form.Label>Auto-Save Time</Form.Label>
|
|
82
|
+
<Select
|
|
83
|
+
classvalue="themes"
|
|
84
|
+
options={saveTimes}
|
|
85
|
+
onChange={handleSaveTimeChange}
|
|
86
|
+
/>
|
|
87
|
+
<Form.Text className="text-muted">Select an auto save time</Form.Text>
|
|
88
|
+
</Form.Group>
|
|
89
|
+
</Modal.Body>
|
|
90
|
+
<Modal.Footer>
|
|
91
|
+
<Button variant="primary" onClick={onClose}>
|
|
92
|
+
Done
|
|
93
|
+
</Button>
|
|
94
|
+
</Modal.Footer>
|
|
95
|
+
</Modal>
|
|
96
|
+
) : null;
|
|
97
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Recursively sorts the file tree in-place.
|
|
2
|
+
// Sorts first to group directories before files.
|
|
3
|
+
// Sorts second alphabetically.
|
|
4
|
+
|
|
5
|
+
import { FileTree, FileTreeFile } from '../types';
|
|
6
|
+
|
|
7
|
+
// From https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/FileTree/sortFileTree.js
|
|
8
|
+
export const sortFileTree = (fileTree: FileTree) => {
|
|
9
|
+
if (fileTree.children) {
|
|
10
|
+
fileTree.children.sort((a, b) => {
|
|
11
|
+
const aIsFile = (a as FileTreeFile).file ? 1 : 0;
|
|
12
|
+
const bIsFile = (b as FileTreeFile).file ? 1 : 0;
|
|
13
|
+
// Directories before files.
|
|
14
|
+
return aIsFile - bIsFile || a.name.localeCompare(b.name);
|
|
15
|
+
});
|
|
16
|
+
fileTree.children.forEach(sortFileTree);
|
|
17
|
+
}
|
|
18
|
+
return fileTree;
|
|
19
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--vzcode-font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
|
|
3
|
+
Liberation Mono, monospace;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/* width */
|
|
7
|
+
::-webkit-scrollbar {
|
|
8
|
+
width: 10px;
|
|
9
|
+
height: 10px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Track */
|
|
13
|
+
::-webkit-scrollbar-track {
|
|
14
|
+
background: rgb(41, 44, 52);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Handle */
|
|
18
|
+
::-webkit-scrollbar-thumb {
|
|
19
|
+
background: rgb(37, 37, 38);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Handle on hover */
|
|
23
|
+
::-webkit-scrollbar-thumb:hover {
|
|
24
|
+
background: #555;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
html,
|
|
28
|
+
body,
|
|
29
|
+
#root,
|
|
30
|
+
.app {
|
|
31
|
+
height: 100%;
|
|
32
|
+
}
|
|
33
|
+
.app {
|
|
34
|
+
/* TODO use a variable for this */
|
|
35
|
+
background-color: rgb(41, 44, 52);
|
|
36
|
+
|
|
37
|
+
font-family: 'Poppins', sans-serif;
|
|
38
|
+
display: flex;
|
|
39
|
+
|
|
40
|
+
.left {
|
|
41
|
+
display: flex;
|
|
42
|
+
}
|
|
43
|
+
.right {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex: 1;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
|
|
49
|
+
.tab-list {
|
|
50
|
+
display: flex;
|
|
51
|
+
background-color: rgb(37, 37, 38);
|
|
52
|
+
|
|
53
|
+
.tab {
|
|
54
|
+
background: rgb(45, 45, 45);
|
|
55
|
+
color: #fff;
|
|
56
|
+
font-family: var(--vzcode-font-family);
|
|
57
|
+
align-items: center;
|
|
58
|
+
display: flex;
|
|
59
|
+
padding-top: 2px;
|
|
60
|
+
padding-left: 10px;
|
|
61
|
+
padding-right: 10px;
|
|
62
|
+
border-top-left-radius: 10px;
|
|
63
|
+
border-top-right-radius: 10px;
|
|
64
|
+
margin-top: 5px;
|
|
65
|
+
margin-left: 2px;
|
|
66
|
+
margin-right: 2px;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
|
|
69
|
+
.tab-close {
|
|
70
|
+
margin-left: 5px;
|
|
71
|
+
}
|
|
72
|
+
.tab-close:hover {
|
|
73
|
+
border-radius: 30%;
|
|
74
|
+
background-color: rgb(37, 37, 38);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
.tab.active {
|
|
78
|
+
background-color: rgb(41, 44, 52);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.editor {
|
|
83
|
+
flex: 1;
|
|
84
|
+
display: flex;
|
|
85
|
+
font-family: var(--vzcode-font-family);
|
|
86
|
+
overflow-x: auto;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.themes {
|
|
92
|
+
color: rgb(37, 37, 38);
|
|
93
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { randomId } from '../randomId.js';
|
|
4
|
+
import { enableDirectories, debugDirectories } from './featureFlags.js';
|
|
5
|
+
|
|
6
|
+
const isDirectory = (file) => file.endsWith('/');
|
|
7
|
+
|
|
8
|
+
// Lists files from the file system,
|
|
9
|
+
// converts them into the VZCode internal
|
|
10
|
+
// ShareDB-compatible data structure.
|
|
11
|
+
export const computeInitialDocument = ({ fullPath }) => {
|
|
12
|
+
// Isolate files, not directories.
|
|
13
|
+
// Inspired by https://stackoverflow.com/questions/41472161/fs-readdir-ignore-directories
|
|
14
|
+
|
|
15
|
+
const unsearchedDirectories = [''];
|
|
16
|
+
|
|
17
|
+
// Initialize the document using our data structure for representing files.
|
|
18
|
+
// * Keys are file ids, which are random numbers.
|
|
19
|
+
// * Values are objects with properties:
|
|
20
|
+
// * text - the text content of the file
|
|
21
|
+
// * name - the file name
|
|
22
|
+
const initialDocument = {};
|
|
23
|
+
|
|
24
|
+
// Stack for recursively traversing directories.
|
|
25
|
+
let files = [];
|
|
26
|
+
|
|
27
|
+
while (unsearchedDirectories.length !== 0) {
|
|
28
|
+
const currentDirectory = unsearchedDirectories.pop();
|
|
29
|
+
const currentDirectoryPath = path.join(fullPath, currentDirectory);
|
|
30
|
+
const newFiles = fs
|
|
31
|
+
.readdirSync(currentDirectoryPath, { withFileTypes: true })
|
|
32
|
+
.filter((dirent) => (enableDirectories ? true : dirent.isFile()))
|
|
33
|
+
|
|
34
|
+
// Add a trailing slash for directories
|
|
35
|
+
.map((dirent) => {
|
|
36
|
+
const relativePath =
|
|
37
|
+
currentDirectory === ''
|
|
38
|
+
? dirent.name
|
|
39
|
+
: currentDirectory + '/' + dirent.name;
|
|
40
|
+
|
|
41
|
+
if (dirent.isFile()) {
|
|
42
|
+
return relativePath;
|
|
43
|
+
}
|
|
44
|
+
unsearchedDirectories.push(relativePath);
|
|
45
|
+
return relativePath + '/';
|
|
46
|
+
});
|
|
47
|
+
// console.log(currentDirectory);
|
|
48
|
+
|
|
49
|
+
files = [...files, ...newFiles];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
files.forEach((file) => {
|
|
53
|
+
const id = randomId();
|
|
54
|
+
initialDocument[id] = {
|
|
55
|
+
text: isDirectory(file)
|
|
56
|
+
? null
|
|
57
|
+
: fs.readFileSync(path.join(fullPath, file), 'utf-8'),
|
|
58
|
+
name: file,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (debugDirectories) {
|
|
63
|
+
console.log('files:');
|
|
64
|
+
console.log(files);
|
|
65
|
+
console.log('initialDocument:');
|
|
66
|
+
console.log(JSON.stringify(initialDocument, null, 2));
|
|
67
|
+
}
|
|
68
|
+
return initialDocument;
|
|
69
|
+
};
|
|
@@ -7,9 +7,13 @@ import WebSocketJSONStream from '@teamwork/websocket-json-stream';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
|
-
import { json1Presence } from './src/ot.js';
|
|
11
|
-
import { randomId } from './src/randomId.js';
|
|
12
10
|
import open from 'open';
|
|
11
|
+
import ngrok from 'ngrok';
|
|
12
|
+
import dotenv from 'dotenv';
|
|
13
|
+
import { computeInitialDocument } from './computeInitialDocument.js';
|
|
14
|
+
import { json1Presence } from '../ot.js';
|
|
15
|
+
|
|
16
|
+
dotenv.config({ path: '../../.env' });
|
|
13
17
|
|
|
14
18
|
// The time in milliseconds by which auto-saving is debounced.
|
|
15
19
|
const autoSaveDebounceTimeMS = 800;
|
|
@@ -17,28 +21,9 @@ const autoSaveDebounceTimeMS = 800;
|
|
|
17
21
|
// Server port.
|
|
18
22
|
const port = 3030;
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Isolate files, not directories.
|
|
24
|
-
// Inspired by https://stackoverflow.com/questions/41472161/fs-readdir-ignore-directories
|
|
25
|
-
const files = fs
|
|
26
|
-
.readdirSync(fullPath, { withFileTypes: true })
|
|
27
|
-
.filter((dirent) => dirent.isFile())
|
|
28
|
-
.map((dirent) => dirent.name);
|
|
29
|
-
|
|
30
|
-
// Initialize the document using our data structure for representing files.
|
|
31
|
-
// * Keys are file ids, which are random numbers.
|
|
32
|
-
// * Values are objects with properties:
|
|
33
|
-
// * text - the text content of the file
|
|
34
|
-
// * name - the file name
|
|
35
|
-
const initialDocument = {};
|
|
36
|
-
files.forEach((file) => {
|
|
37
|
-
const id = randomId();
|
|
38
|
-
initialDocument[id] = {
|
|
39
|
-
text: fs.readFileSync(file, 'utf-8'),
|
|
40
|
-
name: file,
|
|
41
|
-
};
|
|
24
|
+
const initialDocument = computeInitialDocument({
|
|
25
|
+
// Use the current working directory to look for files.
|
|
26
|
+
fullPath: process.cwd(),
|
|
42
27
|
});
|
|
43
28
|
|
|
44
29
|
// Register our custom OT type,
|
|
@@ -66,9 +51,9 @@ wss.on('connection', (ws) => {
|
|
|
66
51
|
});
|
|
67
52
|
|
|
68
53
|
// Serve static files
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const dir = path.join(
|
|
54
|
+
const filename = fileURLToPath(import.meta.url);
|
|
55
|
+
const dirname = path.dirname(filename);
|
|
56
|
+
const dir = path.join(dirname, '..', '..', 'dist');
|
|
72
57
|
app.use(express.static(dir));
|
|
73
58
|
|
|
74
59
|
// Create the initial "document",
|
|
@@ -127,7 +112,16 @@ shareDBDoc.subscribe(() => {
|
|
|
127
112
|
});
|
|
128
113
|
});
|
|
129
114
|
|
|
130
|
-
server.listen(port, () => {
|
|
131
|
-
|
|
132
|
-
|
|
115
|
+
server.listen(port, async () => {
|
|
116
|
+
if (process.env.NGROK_TOKEN) {
|
|
117
|
+
(async function () {
|
|
118
|
+
await ngrok.authtoken(process.env.NGROK_TOKEN);
|
|
119
|
+
const url = await ngrok.connect(port);
|
|
120
|
+
console.log(`Editor is live at ${url}`);
|
|
121
|
+
open(url);
|
|
122
|
+
})();
|
|
123
|
+
} else {
|
|
124
|
+
console.log(`Editor is live at http://localhost:${port}`);
|
|
125
|
+
open(`http://localhost:${port}`);
|
|
126
|
+
}
|
|
133
127
|
});
|