vzcode 0.5.0 → 0.7.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 +20 -5
- package/dist/assets/index-ccd52a89.css +1 -0
- package/dist/assets/index-d2d6b59b.js +89 -0
- package/dist/index.html +17 -3
- package/package.json +26 -21
- package/src/client/{App.jsx → App.tsx} +75 -120
- package/src/client/CodeEditor/getOrCreateEditor.ts +113 -0
- package/src/client/CodeEditor/index.tsx +50 -0
- package/src/client/{json1PresenceDisplay.js → CodeEditor/json1PresenceDisplay.js} +2 -2
- package/src/client/CodeEditor/style.scss +11 -0
- package/src/client/Settings.tsx +76 -0
- package/src/client/Sidebar/DirectoryListing.tsx +77 -0
- package/src/client/Sidebar/{File.jsx → FileListing.tsx} +14 -4
- package/src/client/Sidebar/Listing.tsx +44 -0
- package/src/client/Sidebar/index.tsx +81 -0
- package/src/client/Sidebar/styles.scss +84 -0
- package/src/client/TabList/index.tsx +82 -0
- package/src/client/TabList/style.scss +32 -0
- package/src/client/{getFileTree.js → getFileTree.ts} +6 -3
- package/src/client/main.jsx +1 -1
- package/src/client/{sortFileTree.js → sortFileTree.ts} +7 -3
- package/src/client/style.scss +49 -0
- package/src/client/themes.ts +31 -0
- package/src/client/{Sidebar/useOpenDirectories.js → useOpenDirectories.ts} +18 -14
- package/src/client/useTabsState.ts +51 -0
- package/src/server/index.js +0 -0
- package/src/types.ts +69 -0
- package/dist/assets/index-29b6a606.css +0 -1
- package/dist/assets/index-523ecad7.js +0 -89
- package/src/client/CodeEditor.jsx +0 -31
- package/src/client/Sidebar/Directory.jsx +0 -62
- package/src/client/Sidebar/FileOrDirectory.jsx +0 -33
- package/src/client/Sidebar/index.jsx +0 -64
- package/src/client/Sidebar/styles.css +0 -88
- package/src/client/getOrCreateEditor.js +0 -84
- package/src/client/settings.jsx +0 -141
- package/src/client/style.css +0 -120
- /package/src/client/{json1PresenceBroadcast.js → CodeEditor/json1PresenceBroadcast.js} +0 -0
package/dist/index.html
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
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
|
-
<
|
|
8
|
-
<link rel="
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<link
|
|
14
|
+
href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css"
|
|
15
|
+
rel="stylesheet"
|
|
16
|
+
/>
|
|
17
|
+
<link
|
|
18
|
+
href="https://unpkg.com/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
|
19
|
+
rel="stylesheet"
|
|
20
|
+
/>
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-d2d6b59b.js"></script>
|
|
22
|
+
<link rel="stylesheet" href="/assets/index-ccd52a89.css">
|
|
9
23
|
</head>
|
|
10
24
|
<body>
|
|
11
25
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"types": "src/types.ts",
|
|
7
8
|
"bin": "src/server/index.js",
|
|
8
9
|
"files": [
|
|
9
10
|
"dist/**",
|
|
10
11
|
"src/**"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
|
-
"test": "vitest",
|
|
14
|
+
"test": "vitest run",
|
|
14
15
|
"test-interactive": "npm run build; cd test/sampleDirectories/kitchenSink; node ../../../src/server/index.js",
|
|
15
16
|
"prettier": "prettier {*.*,**/*.*} --write",
|
|
17
|
+
"tsc": "tsc --noEmit",
|
|
16
18
|
"dev": "vite",
|
|
17
19
|
"build": "vite build",
|
|
18
20
|
"build-release": "vite build --mode release",
|
|
@@ -44,36 +46,39 @@
|
|
|
44
46
|
},
|
|
45
47
|
"homepage": "https://github.com/vizhub-core/vzcode#readme",
|
|
46
48
|
"dependencies": {
|
|
47
|
-
"@codemirror/lang-css": "^6.
|
|
48
|
-
"@codemirror/lang-html": "^6.4.
|
|
49
|
-
"@codemirror/lang-javascript": "^6.1.
|
|
50
|
-
"@codemirror/lang-markdown": "^6.
|
|
51
|
-
"@codemirror/state": "^6.2.
|
|
52
|
-
"@codemirror/theme-one-dark": "^6.1.
|
|
49
|
+
"@codemirror/lang-css": "^6.2.0",
|
|
50
|
+
"@codemirror/lang-html": "^6.4.5",
|
|
51
|
+
"@codemirror/lang-javascript": "^6.1.9",
|
|
52
|
+
"@codemirror/lang-markdown": "^6.2.0",
|
|
53
|
+
"@codemirror/state": "^6.2.1",
|
|
54
|
+
"@codemirror/theme-one-dark": "^6.1.2",
|
|
53
55
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
56
|
+
"@uiw/codemirror-themes-all": "^4.21.8",
|
|
54
57
|
"codemirror": "^6.0.1",
|
|
55
58
|
"codemirror-ot": "^4.0.0",
|
|
56
|
-
"d3-array": "^3.2.
|
|
59
|
+
"d3-array": "^3.2.4",
|
|
57
60
|
"diff-match-patch": "^1.0.5",
|
|
58
|
-
"dotenv": "^16.
|
|
61
|
+
"dotenv": "^16.3.1",
|
|
59
62
|
"express": "^4.18.2",
|
|
60
63
|
"json0-ot-diff": "^1.1.2",
|
|
61
64
|
"ngrok": "^4.3.3",
|
|
62
|
-
"open": "^9.
|
|
65
|
+
"open": "^9.1.0",
|
|
63
66
|
"react": "^18.2.0",
|
|
64
|
-
"react-bootstrap": "^2.
|
|
67
|
+
"react-bootstrap": "^2.8.0",
|
|
65
68
|
"react-dom": "^18.2.0",
|
|
66
|
-
"react-select": "^5.7.
|
|
67
|
-
"sharedb": "^
|
|
68
|
-
"sharedb-client-browser": "^4.2.
|
|
69
|
+
"react-select": "^5.7.4",
|
|
70
|
+
"sharedb": "^4.0.0",
|
|
71
|
+
"sharedb-client-browser": "^4.2.1",
|
|
69
72
|
"ws": "^8.13.0"
|
|
70
73
|
},
|
|
71
74
|
"devDependencies": {
|
|
72
|
-
"@types/react": "^18.
|
|
73
|
-
"@types/react-dom": "^18.
|
|
74
|
-
"@vitejs/plugin-react": "^
|
|
75
|
-
"prettier": "^
|
|
76
|
-
"
|
|
77
|
-
"
|
|
75
|
+
"@types/react": "^18.2.17",
|
|
76
|
+
"@types/react-dom": "^18.2.7",
|
|
77
|
+
"@vitejs/plugin-react": "^4.0.3",
|
|
78
|
+
"prettier": "^3.0.0",
|
|
79
|
+
"sass": "^1.64.1",
|
|
80
|
+
"vite": "^4.4.7",
|
|
81
|
+
"vitest": "^0.33.0",
|
|
82
|
+
"typescript": "^5.1.6"
|
|
78
83
|
}
|
|
79
84
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { useState, useEffect,
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
|
|
3
3
|
import { json1Presence } from '../ot';
|
|
4
4
|
import { randomId } from '../randomId';
|
|
5
5
|
import { CodeEditor } from './CodeEditor';
|
|
6
6
|
import { diff } from './diff';
|
|
7
|
-
import { Settings } from './
|
|
7
|
+
import { Settings } from './Settings';
|
|
8
8
|
import { Sidebar } from './Sidebar';
|
|
9
|
-
import '
|
|
9
|
+
import { FileId, Files } from '../types';
|
|
10
|
+
import { TabList } from './TabList';
|
|
11
|
+
import { useOpenDirectories } from './useOpenDirectories';
|
|
12
|
+
import { useTabsState } from './useTabsState';
|
|
13
|
+
import { defaultTheme } from './themes';
|
|
14
|
+
import './style.scss';
|
|
10
15
|
|
|
11
16
|
// Register our custom JSON1 OT type that supports presence.
|
|
12
17
|
// See https://github.com/vizhub-core/json1-presence
|
|
@@ -34,22 +39,30 @@ function App() {
|
|
|
34
39
|
|
|
35
40
|
// The `doc.data` part of the ShareDB document,
|
|
36
41
|
// updated on each change to decouple rendering from ShareDB.
|
|
37
|
-
const [data, setData] = useState(null);
|
|
38
|
-
|
|
39
|
-
// True if the file menu is open.
|
|
40
|
-
const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
|
|
42
|
+
const [data, setData] = useState<Files>(null);
|
|
41
43
|
|
|
42
44
|
// The id of the currently open file tab.
|
|
43
|
-
const [activeFileId, setActiveFileId] = useState(null);
|
|
45
|
+
const [activeFileId, setActiveFileId] = useState<FileId>(null);
|
|
44
46
|
|
|
45
47
|
// The ordered list of tabs.
|
|
46
|
-
const [tabList, setTabList] = useState([]);
|
|
48
|
+
const [tabList, setTabList] = useState<Array<FileId>>([]);
|
|
49
|
+
|
|
50
|
+
// Logic for opening and closing tabs.
|
|
51
|
+
const { closeTab, openTab } = useTabsState(
|
|
52
|
+
activeFileId,
|
|
53
|
+
setActiveFileId,
|
|
54
|
+
tabList,
|
|
55
|
+
setTabList,
|
|
56
|
+
);
|
|
47
57
|
|
|
48
58
|
// The current theme.
|
|
49
|
-
const [theme, setTheme] = useState(
|
|
59
|
+
const [theme, setTheme] = useState<string>(defaultTheme);
|
|
50
60
|
|
|
51
61
|
// True to show the settings modal.
|
|
52
|
-
const [
|
|
62
|
+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
63
|
+
|
|
64
|
+
// The set of open directories.
|
|
65
|
+
const { isDirectoryOpen, toggleDirectory } = useOpenDirectories();
|
|
53
66
|
|
|
54
67
|
// Set up the connection to ShareDB.
|
|
55
68
|
useEffect(() => {
|
|
@@ -75,7 +88,7 @@ function App() {
|
|
|
75
88
|
// Listen for all changes and update `data`.
|
|
76
89
|
// This decouples rendering logic from ShareDB.
|
|
77
90
|
// This callback gets called on each change.
|
|
78
|
-
shareDBDoc.on('op', (
|
|
91
|
+
shareDBDoc.on('op', () => {
|
|
79
92
|
setData(shareDBDoc.data);
|
|
80
93
|
});
|
|
81
94
|
|
|
@@ -97,100 +110,58 @@ function App() {
|
|
|
97
110
|
|
|
98
111
|
// TODO unsubscribe from presence
|
|
99
112
|
// TODO unsubscribe from doc
|
|
113
|
+
return () => {
|
|
114
|
+
// shareDBDoc.destroy();
|
|
115
|
+
// docPresence.destroy();
|
|
116
|
+
};
|
|
100
117
|
}, []);
|
|
101
118
|
|
|
102
|
-
// Called when a tab is closed.
|
|
103
|
-
const closeTab = useCallback(
|
|
104
|
-
(fileIdToRemove) => {
|
|
105
|
-
const i = tabList.findIndex((fileId) => fileId === fileIdToRemove);
|
|
106
|
-
|
|
107
|
-
// Support calling `closeTab` on a fileId that is not open,
|
|
108
|
-
// such as when a file is deleted..
|
|
109
|
-
if (i !== -1) {
|
|
110
|
-
// Remove the tab from the tab list.
|
|
111
|
-
const newTabList = [...tabList.slice(0, i), ...tabList.slice(i + 1)];
|
|
112
|
-
setTabList(newTabList);
|
|
113
|
-
|
|
114
|
-
// If we are closing the active file,
|
|
115
|
-
if (activeFileId === fileIdToRemove) {
|
|
116
|
-
// set the new active file to the next tab over,
|
|
117
|
-
if (newTabList.length > 0) {
|
|
118
|
-
setActiveFileId(i === 0 ? newTabList[i] : newTabList[i - 1]);
|
|
119
|
-
} else {
|
|
120
|
-
// or clear out the active file
|
|
121
|
-
// if we've closed the last tab.
|
|
122
|
-
setActiveFileId(null);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
[tabList, activeFileId]
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const handleCloseTabClick = useCallback(
|
|
131
|
-
(fileIdToRemove) => (event) => {
|
|
132
|
-
// Stop propagation so that the outer listener doesn't fire.
|
|
133
|
-
event.stopPropagation();
|
|
134
|
-
closeTab(fileIdToRemove);
|
|
135
|
-
},
|
|
136
|
-
[closeTab]
|
|
137
|
-
);
|
|
138
|
-
|
|
139
119
|
// Called when a file in the sidebar is double-clicked.
|
|
140
120
|
const handleRenameFileClick = useCallback(
|
|
141
|
-
(
|
|
121
|
+
(fileId: FileId) => {
|
|
142
122
|
const newName = prompt('Enter new name');
|
|
143
123
|
if (newName) {
|
|
144
124
|
const currentDocument = shareDBDoc.data;
|
|
145
125
|
const nextDocument = {
|
|
146
126
|
...currentDocument,
|
|
147
|
-
[
|
|
148
|
-
...currentDocument[
|
|
127
|
+
[fileId]: {
|
|
128
|
+
...currentDocument[fileId],
|
|
149
129
|
name: newName,
|
|
150
130
|
},
|
|
151
131
|
};
|
|
152
132
|
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
153
133
|
}
|
|
154
134
|
},
|
|
155
|
-
[shareDBDoc]
|
|
135
|
+
[shareDBDoc],
|
|
156
136
|
);
|
|
157
137
|
|
|
158
138
|
const deleteFile = useCallback(
|
|
159
|
-
(
|
|
139
|
+
(fileId: FileId) => {
|
|
160
140
|
// Close the tab in case it's open.
|
|
161
141
|
// TODO consistently use either "key" or "fileId" naming
|
|
162
|
-
closeTab(
|
|
142
|
+
closeTab(fileId);
|
|
163
143
|
|
|
164
144
|
const currentDocument = shareDBDoc.data;
|
|
165
145
|
const nextDocument = { ...currentDocument };
|
|
166
|
-
delete nextDocument[
|
|
146
|
+
delete nextDocument[fileId];
|
|
167
147
|
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
168
148
|
},
|
|
169
|
-
[shareDBDoc, closeTab]
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
const handleFileClick = useCallback(
|
|
173
|
-
(fileId) => {
|
|
174
|
-
setActiveFileId(fileId);
|
|
175
|
-
if (!tabList.includes(fileId)) {
|
|
176
|
-
setTabList([...tabList, fileId]);
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
[tabList]
|
|
149
|
+
[shareDBDoc, closeTab],
|
|
180
150
|
);
|
|
181
151
|
|
|
182
152
|
// TODO prompt the user "Are you sure?"
|
|
183
153
|
const handleDeleteFileClick = useCallback(
|
|
184
|
-
(fileId
|
|
154
|
+
(fileId: FileId, event: React.MouseEvent) => {
|
|
185
155
|
// Stop propagation so that the outer listener doesn't fire,
|
|
186
156
|
// which would try to open this file in a tab.
|
|
187
157
|
event.stopPropagation();
|
|
188
158
|
deleteFile(fileId);
|
|
189
159
|
},
|
|
190
|
-
[deleteFile]
|
|
160
|
+
[deleteFile],
|
|
191
161
|
);
|
|
192
162
|
|
|
193
163
|
const createFile = useCallback(() => {
|
|
164
|
+
// TODO better UI, maybe Bootstrap modal? Maybe edit inline?
|
|
194
165
|
const name = prompt('Enter new file name');
|
|
195
166
|
if (!name) return;
|
|
196
167
|
const currentDocument = shareDBDoc.data;
|
|
@@ -201,64 +172,48 @@ function App() {
|
|
|
201
172
|
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
202
173
|
}, [shareDBDoc]);
|
|
203
174
|
|
|
204
|
-
// True if we are ready to actually render the active tab.
|
|
205
|
-
const tabValid = data && activeFileId;
|
|
206
|
-
|
|
207
175
|
const handleSettingsClose = useCallback(() => {
|
|
208
|
-
|
|
176
|
+
setIsSettingsOpen(false);
|
|
209
177
|
}, []);
|
|
210
178
|
|
|
211
|
-
const fileNameSplit = (fileName) => {
|
|
212
|
-
const split = fileName.split('/');
|
|
213
|
-
return split[split.length - 1];
|
|
214
|
-
};
|
|
215
|
-
|
|
216
179
|
return (
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
{tabValid ? fileNameSplit(data[fileId].name) : ''}
|
|
235
|
-
<div
|
|
236
|
-
className={activeFileId ? 'bx bx-x tab-close' : ''}
|
|
237
|
-
onClick={handleCloseTabClick(fileId)}
|
|
238
|
-
></div>
|
|
239
|
-
</div>
|
|
240
|
-
))}
|
|
180
|
+
<div className="app">
|
|
181
|
+
<div className="left">
|
|
182
|
+
<Sidebar
|
|
183
|
+
createFile={createFile}
|
|
184
|
+
files={data}
|
|
185
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
186
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
187
|
+
handleFileClick={openTab}
|
|
188
|
+
setIsSettingsOpen={setIsSettingsOpen}
|
|
189
|
+
isDirectoryOpen={isDirectoryOpen}
|
|
190
|
+
toggleDirectory={toggleDirectory}
|
|
191
|
+
/>
|
|
192
|
+
<Settings
|
|
193
|
+
show={isSettingsOpen}
|
|
194
|
+
onClose={handleSettingsClose}
|
|
195
|
+
setTheme={setTheme}
|
|
196
|
+
/>
|
|
241
197
|
</div>
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
handleDeleteFileClick={handleDeleteFileClick}
|
|
247
|
-
handleFileClick={handleFileClick}
|
|
248
|
-
setSettings={setSettings}
|
|
249
|
-
settings={settings}
|
|
250
|
-
/>
|
|
251
|
-
{data && activeFileId ? (
|
|
252
|
-
<CodeEditor
|
|
253
|
-
className="editor"
|
|
254
|
-
shareDBDoc={shareDBDoc}
|
|
255
|
-
localPresence={localPresence}
|
|
256
|
-
docPresence={docPresence}
|
|
198
|
+
<div className="right">
|
|
199
|
+
<TabList
|
|
200
|
+
files={data}
|
|
201
|
+
tabList={tabList}
|
|
257
202
|
activeFileId={activeFileId}
|
|
258
|
-
|
|
203
|
+
setActiveFileId={setActiveFileId}
|
|
204
|
+
closeTab={closeTab}
|
|
259
205
|
/>
|
|
260
|
-
|
|
261
|
-
|
|
206
|
+
{data && activeFileId ? (
|
|
207
|
+
<CodeEditor
|
|
208
|
+
shareDBDoc={shareDBDoc}
|
|
209
|
+
localPresence={localPresence}
|
|
210
|
+
docPresence={docPresence}
|
|
211
|
+
activeFileId={activeFileId}
|
|
212
|
+
theme={theme}
|
|
213
|
+
/>
|
|
214
|
+
) : null}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
262
217
|
);
|
|
263
218
|
}
|
|
264
219
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { EditorView, basicSetup } from 'codemirror';
|
|
2
|
+
import { Compartment, EditorState } from '@codemirror/state';
|
|
3
|
+
import { javascript } from '@codemirror/lang-javascript';
|
|
4
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
5
|
+
import { html } from '@codemirror/lang-html';
|
|
6
|
+
import { css } from '@codemirror/lang-css';
|
|
7
|
+
import { json1Sync } from 'codemirror-ot';
|
|
8
|
+
import { json1Presence, textUnicode } from '../../ot';
|
|
9
|
+
import { FileId, ShareDBDoc } from '../../types';
|
|
10
|
+
import { json1PresenceBroadcast } from './json1PresenceBroadcast';
|
|
11
|
+
import { json1PresenceDisplay } from './json1PresenceDisplay';
|
|
12
|
+
|
|
13
|
+
// Singleton cache of CodeMirror instances
|
|
14
|
+
// These are created, but never destroyed.
|
|
15
|
+
// TODO consider invalidating this cache at some point.
|
|
16
|
+
// * Keys are file ids
|
|
17
|
+
// * Values are CodeMirror instances (TODO add CodeMirror type)
|
|
18
|
+
const editorCache = new Map<FileId, any>();
|
|
19
|
+
|
|
20
|
+
// Gets a value at a path in an object.
|
|
21
|
+
// e.g. getAtPath({a: {b: 1}}, ['a', 'b']) === 1
|
|
22
|
+
const getAtPath = (obj, path) => {
|
|
23
|
+
let current = obj;
|
|
24
|
+
for (const key of path) {
|
|
25
|
+
if (!current) return undefined;
|
|
26
|
+
current = current[key];
|
|
27
|
+
}
|
|
28
|
+
return current;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Gets or creates a CodeMirror editor for the given file id.
|
|
32
|
+
export const getOrCreateEditor = ({
|
|
33
|
+
fileId,
|
|
34
|
+
shareDBDoc,
|
|
35
|
+
filesPath,
|
|
36
|
+
localPresence,
|
|
37
|
+
docPresence,
|
|
38
|
+
theme,
|
|
39
|
+
}: {
|
|
40
|
+
fileId: FileId;
|
|
41
|
+
shareDBDoc: ShareDBDoc<any>;
|
|
42
|
+
filesPath: string[];
|
|
43
|
+
localPresence: any;
|
|
44
|
+
docPresence: any;
|
|
45
|
+
theme: any;
|
|
46
|
+
}) => {
|
|
47
|
+
const data = shareDBDoc.data;
|
|
48
|
+
const textPath = [...filesPath, fileId, 'text'];
|
|
49
|
+
const namePath = [...filesPath, fileId, 'name'];
|
|
50
|
+
|
|
51
|
+
const text = getAtPath(data, textPath);
|
|
52
|
+
const name = getAtPath(data, namePath);
|
|
53
|
+
|
|
54
|
+
const fileExtension = name.split('.').pop();
|
|
55
|
+
|
|
56
|
+
// TODO revisit if this is the right approach for dynamic themes
|
|
57
|
+
let themeSet = new Compartment();
|
|
58
|
+
|
|
59
|
+
const extensions = [];
|
|
60
|
+
|
|
61
|
+
// This plugin implements multiplayer editing,
|
|
62
|
+
// real-time synchronozation of changes across clients.
|
|
63
|
+
// Does not deal with showing others' cursors.
|
|
64
|
+
extensions.push(
|
|
65
|
+
json1Sync({
|
|
66
|
+
shareDBDoc,
|
|
67
|
+
path: textPath,
|
|
68
|
+
json1: json1Presence,
|
|
69
|
+
textUnicode,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Deals with broadcasting changes in cursor location and selection.
|
|
74
|
+
if (localPresence)
|
|
75
|
+
extensions.push(json1PresenceBroadcast({ path: textPath, localPresence }));
|
|
76
|
+
|
|
77
|
+
// Deals with receiving the broadcas from other clients and displaying them.
|
|
78
|
+
if (docPresence)
|
|
79
|
+
extensions.push(json1PresenceDisplay({ path: textPath, docPresence }));
|
|
80
|
+
|
|
81
|
+
extensions.push(basicSetup);
|
|
82
|
+
extensions.push(themeSet.of(theme));
|
|
83
|
+
|
|
84
|
+
// TODO handle dynamic changing of the file extension.
|
|
85
|
+
if (
|
|
86
|
+
fileExtension === 'js' ||
|
|
87
|
+
fileExtension === 'json' ||
|
|
88
|
+
fileExtension === 'jsx' ||
|
|
89
|
+
fileExtension === 'ts'
|
|
90
|
+
) {
|
|
91
|
+
extensions.push(javascript());
|
|
92
|
+
} else if (fileExtension === 'html') {
|
|
93
|
+
extensions.push(html());
|
|
94
|
+
} else if (fileExtension === 'css') {
|
|
95
|
+
extensions.push(css());
|
|
96
|
+
} else if (fileExtension === 'md') {
|
|
97
|
+
extensions.push(markdown());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let editor = editorCache.get(fileId);
|
|
101
|
+
if (!editor) {
|
|
102
|
+
editor = new EditorView({
|
|
103
|
+
state: EditorState.create({
|
|
104
|
+
doc: text,
|
|
105
|
+
extensions,
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Populate the cache.
|
|
110
|
+
editorCache.set(fileId, editor);
|
|
111
|
+
}
|
|
112
|
+
return editor;
|
|
113
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useRef, useLayoutEffect } from 'react';
|
|
2
|
+
import { FileId } from '../../types';
|
|
3
|
+
import { defaultTheme, themesByLabel } from '../themes';
|
|
4
|
+
import { getOrCreateEditor } from './getOrCreateEditor';
|
|
5
|
+
import './style.scss';
|
|
6
|
+
|
|
7
|
+
export const CodeEditor = ({
|
|
8
|
+
activeFileId,
|
|
9
|
+
shareDBDoc,
|
|
10
|
+
localPresence,
|
|
11
|
+
docPresence,
|
|
12
|
+
theme = defaultTheme,
|
|
13
|
+
filesPath = [],
|
|
14
|
+
}: {
|
|
15
|
+
activeFileId: FileId;
|
|
16
|
+
shareDBDoc: any;
|
|
17
|
+
localPresence?: any;
|
|
18
|
+
docPresence?: any;
|
|
19
|
+
theme?: string;
|
|
20
|
+
|
|
21
|
+
// The path of the files object in the ShareDB document.
|
|
22
|
+
// Defaults to the root of the document.
|
|
23
|
+
filesPath?: string[];
|
|
24
|
+
}) => {
|
|
25
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
26
|
+
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
if (!ref.current) return;
|
|
29
|
+
if (!shareDBDoc) return;
|
|
30
|
+
|
|
31
|
+
// `getOrCreateEditor` gets called
|
|
32
|
+
// every time the active file changes.
|
|
33
|
+
const editor = getOrCreateEditor({
|
|
34
|
+
fileId: activeFileId,
|
|
35
|
+
shareDBDoc,
|
|
36
|
+
filesPath,
|
|
37
|
+
localPresence,
|
|
38
|
+
docPresence,
|
|
39
|
+
// TODO refactor this, make dynamic themes work
|
|
40
|
+
theme: themesByLabel[theme],
|
|
41
|
+
});
|
|
42
|
+
ref.current.appendChild(editor.dom);
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
ref.current.removeChild(editor.dom);
|
|
46
|
+
};
|
|
47
|
+
}, [shareDBDoc, activeFileId]);
|
|
48
|
+
|
|
49
|
+
return <div className="vz-code-editor" ref={ref} />;
|
|
50
|
+
};
|
|
@@ -58,7 +58,7 @@ export const json1PresenceDisplay = ({ path, docPresence }) => [
|
|
|
58
58
|
}),
|
|
59
59
|
// Without this argument, we get the following error:
|
|
60
60
|
// Uncaught Error: Ranges must be added sorted by `from` position and `startSide`
|
|
61
|
-
true
|
|
61
|
+
true,
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
// Somehow this triggers re-rendering of the Decorations.
|
|
@@ -74,7 +74,7 @@ export const json1PresenceDisplay = ({ path, docPresence }) => [
|
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
decorations: (v) => v.decorations,
|
|
77
|
-
}
|
|
77
|
+
},
|
|
78
78
|
),
|
|
79
79
|
presenceTheme,
|
|
80
80
|
];
|
|
@@ -0,0 +1,76 @@
|
|
|
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 { themes } from './themes';
|
|
7
|
+
|
|
8
|
+
const saveTimes = [
|
|
9
|
+
{ value: 1, label: '1 second' },
|
|
10
|
+
{ value: 5, label: '5 seconds' },
|
|
11
|
+
{ value: 10, label: '10 seconds' },
|
|
12
|
+
{ value: 30, label: '30 seconds' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const Settings = ({ show, onClose, setTheme }) => {
|
|
16
|
+
const handleChange = useCallback((selectedOption) => {
|
|
17
|
+
setTheme(selectedOption.label);
|
|
18
|
+
// TODO make this work.
|
|
19
|
+
// Sketch of algorithm:
|
|
20
|
+
// for each editor in the editor cache,
|
|
21
|
+
// update the theme
|
|
22
|
+
// editor.dispatch({
|
|
23
|
+
// effects: themeSet.reconfigure(selectedOption.value),
|
|
24
|
+
// });
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const handleSaveTimeChange = useCallback((selectedOption) => {
|
|
28
|
+
const time = [{ value: selectedOption.value }];
|
|
29
|
+
fetch('/saveTime', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(time),
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return show ? (
|
|
39
|
+
<Modal
|
|
40
|
+
className="vz-settings"
|
|
41
|
+
show={show}
|
|
42
|
+
onHide={onClose}
|
|
43
|
+
animation={false}
|
|
44
|
+
>
|
|
45
|
+
<Modal.Header closeButton>
|
|
46
|
+
<Modal.Title>Settings</Modal.Title>
|
|
47
|
+
</Modal.Header>
|
|
48
|
+
<Modal.Body>
|
|
49
|
+
<Form.Group className="mb-3" controlId="formFork">
|
|
50
|
+
<Form.Label>Username</Form.Label>
|
|
51
|
+
<Form.Control type="text" placeholder="Enter username" />
|
|
52
|
+
<Form.Text className="text-muted">
|
|
53
|
+
Enter a username to be displayed on your cursor
|
|
54
|
+
</Form.Text>
|
|
55
|
+
</Form.Group>
|
|
56
|
+
<Form.Group className="mb-3" controlId="formFork">
|
|
57
|
+
<Form.Label>Theme</Form.Label>
|
|
58
|
+
<Select options={themes} onChange={handleChange} />
|
|
59
|
+
<Form.Text className="text-muted">
|
|
60
|
+
Select a color theme for the editor
|
|
61
|
+
</Form.Text>
|
|
62
|
+
</Form.Group>
|
|
63
|
+
<Form.Group className="mb-3" controlId="formFork">
|
|
64
|
+
<Form.Label>Auto-Save Time</Form.Label>
|
|
65
|
+
<Select options={saveTimes} onChange={handleSaveTimeChange} />
|
|
66
|
+
<Form.Text className="text-muted">Select an auto save time</Form.Text>
|
|
67
|
+
</Form.Group>
|
|
68
|
+
</Modal.Body>
|
|
69
|
+
<Modal.Footer>
|
|
70
|
+
<Button variant="primary" onClick={onClose}>
|
|
71
|
+
Done
|
|
72
|
+
</Button>
|
|
73
|
+
</Modal.Footer>
|
|
74
|
+
</Modal>
|
|
75
|
+
) : null;
|
|
76
|
+
};
|