vzcode 0.6.0 → 0.8.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 +0 -1
- package/dist/assets/index-80e9e6df.js +89 -0
- package/dist/assets/index-ccd52a89.css +1 -0
- package/dist/index.html +3 -3
- package/package.json +18 -17
- package/src/client/App.tsx +40 -93
- package/src/client/CodeEditor/getOrCreateEditor.ts +115 -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/CodeEditor/widgets.ts +48 -0
- package/src/client/{settings.jsx → Settings.tsx} +18 -39
- package/src/client/Sidebar/DirectoryListing.tsx +6 -5
- package/src/client/Sidebar/FileListing.tsx +1 -1
- package/src/client/Sidebar/Listing.tsx +3 -3
- package/src/client/Sidebar/index.tsx +13 -12
- package/src/client/TabList/index.tsx +82 -0
- package/src/client/TabList/style.scss +32 -0
- package/src/client/main.jsx +1 -1
- package/src/client/style.scss +0 -44
- package/src/client/themes.ts +31 -0
- package/src/client/{Sidebar/useOpenDirectories.ts → useOpenDirectories.ts} +17 -11
- package/src/client/useTabsState.ts +51 -0
- package/src/types.ts +12 -0
- package/dist/assets/index-41607816.js +0 -89
- package/dist/assets/index-8bac68c9.css +0 -1
- package/src/client/CodeEditor.jsx +0 -31
- package/src/client/getOrCreateEditor.js +0 -85
- /package/src/client/{json1PresenceBroadcast.js → CodeEditor/json1PresenceBroadcast.js} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.vz-code-editor{flex:1;display:flex;font-family:var(--vzcode-font-family);overflow-x:auto}.vz-code-editor .cm-editor{flex:1}.vz-sidebar{height:100%;width:200px;background:rgb(54,65,89);display:flex;flex-direction:column;justify-content:space-between}.vz-sidebar .full-box{padding:7px;display:flex;flex-direction:row;align-items:center;justify-content:space-between}.vz-sidebar .files{overflow:auto}.vz-sidebar .file-or-directory{display:flex;flex-direction:row;align-items:center;justify-content:space-between;cursor:pointer;margin:0 10px;color:#dbdde1;border-radius:5px}.vz-sidebar .link-name{margin-top:30px;margin-left:10px;color:#9aa0ac;font-size:12px;text-decoration:none}.vz-sidebar .new-btn{justify-content:right;margin-right:10px;cursor:pointer}.vz-sidebar .utils{display:flex}.vz-sidebar .utilities{margin-right:3px}.vz-sidebar .file-or-directory:hover,.vz-sidebar .settings:hover{background-color:#4a5469}.vz-sidebar .name{display:flex;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.vz-sidebar .indentation{margin-left:20px}.vz-sidebar .arrow-wrapper{margin-right:4px}.vz-sidebar .settings{height:30px;color:#dbdde1;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:14px 0;margin:10px;border-radius:5px}.vz-tab-list{display:flex;background-color:#252526}.vz-tab-list .tab{background:rgb(45,45,45);color:#fff;font-family:var(--vzcode-font-family);align-items:center;display:flex;padding-top:2px;padding-left:10px;padding-right:10px;border-top-left-radius:10px;border-top-right-radius:10px;margin-top:5px;margin-left:2px;margin-right:2px;cursor:pointer}.vz-tab-list .tab .tab-close{margin-left:5px}.vz-tab-list .tab .tab-close:hover{border-radius:30%;background-color:#252526}.vz-tab-list .tab.active{background-color:#292c34}:root{--vzcode-font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:rgb(41,44,52)}::-webkit-scrollbar-thumb{background:rgb(37,37,38)}::-webkit-scrollbar-thumb:hover{background:#555}html,body,#root,.app{height:100%}.app{background-color:#292c34;font-family:Poppins,sans-serif;display:flex}.app .left{display:flex}.app .right{display:flex;flex:1;flex-direction:column;min-width:0}
|
package/dist/index.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
href="https://unpkg.com/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
|
19
19
|
rel="stylesheet"
|
|
20
20
|
/>
|
|
21
|
-
<script type="module" crossorigin src="/assets/index-
|
|
22
|
-
<link rel="stylesheet" href="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-80e9e6df.js"></script>
|
|
22
|
+
<link rel="stylesheet" href="/assets/index-ccd52a89.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body>
|
|
25
25
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"src/**"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "vitest",
|
|
14
|
+
"test": "vitest run",
|
|
15
15
|
"test-interactive": "npm run build; cd test/sampleDirectories/kitchenSink; node ../../../src/server/index.js",
|
|
16
16
|
"prettier": "prettier {*.*,**/*.*} --write",
|
|
17
17
|
"tsc": "tsc --noEmit",
|
|
@@ -46,39 +46,40 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://github.com/vizhub-core/vzcode#readme",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@codemirror/lang-css": "^6.2.
|
|
50
|
-
"@codemirror/lang-html": "^6.4.
|
|
49
|
+
"@codemirror/lang-css": "^6.2.1",
|
|
50
|
+
"@codemirror/lang-html": "^6.4.5",
|
|
51
51
|
"@codemirror/lang-javascript": "^6.1.9",
|
|
52
|
-
"@codemirror/lang-markdown": "^6.
|
|
52
|
+
"@codemirror/lang-markdown": "^6.2.0",
|
|
53
53
|
"@codemirror/state": "^6.2.1",
|
|
54
54
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
55
|
+
"@replit/codemirror-interact": "^6.1.0",
|
|
55
56
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
56
|
-
"@uiw/codemirror-themes-all": "^4.
|
|
57
|
+
"@uiw/codemirror-themes-all": "^4.21.9",
|
|
57
58
|
"codemirror": "^6.0.1",
|
|
58
59
|
"codemirror-ot": "^4.0.0",
|
|
59
60
|
"d3-array": "^3.2.4",
|
|
60
61
|
"diff-match-patch": "^1.0.5",
|
|
61
|
-
"dotenv": "^16.3.
|
|
62
|
+
"dotenv": "^16.3.1",
|
|
62
63
|
"express": "^4.18.2",
|
|
63
64
|
"json0-ot-diff": "^1.1.2",
|
|
64
65
|
"ngrok": "^4.3.3",
|
|
65
66
|
"open": "^9.1.0",
|
|
66
67
|
"react": "^18.2.0",
|
|
67
|
-
"react-bootstrap": "^2.
|
|
68
|
+
"react-bootstrap": "^2.8.0",
|
|
68
69
|
"react-dom": "^18.2.0",
|
|
69
|
-
"react-select": "^5.7.
|
|
70
|
+
"react-select": "^5.7.4",
|
|
70
71
|
"sharedb": "^4.0.0",
|
|
71
72
|
"sharedb-client-browser": "^4.2.1",
|
|
72
73
|
"ws": "^8.13.0"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
75
|
-
"@types/react": "^18.2.
|
|
76
|
-
"@types/react-dom": "^18.2.
|
|
77
|
-
"@vitejs/plugin-react": "^4.0.
|
|
78
|
-
"prettier": "^
|
|
79
|
-
"sass": "^1.
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
76
|
+
"@types/react": "^18.2.20",
|
|
77
|
+
"@types/react-dom": "^18.2.7",
|
|
78
|
+
"@vitejs/plugin-react": "^4.0.4",
|
|
79
|
+
"prettier": "^3.0.1",
|
|
80
|
+
"sass": "^1.65.1",
|
|
81
|
+
"typescript": "^5.1.6",
|
|
82
|
+
"vite": "^4.4.9",
|
|
83
|
+
"vitest": "^0.34.1"
|
|
83
84
|
}
|
|
84
85
|
}
|
package/src/client/App.tsx
CHANGED
|
@@ -4,11 +4,14 @@ 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 { FileId, Files } from '../types';
|
|
10
|
+
import { TabList } from './TabList';
|
|
11
|
+
import { useOpenDirectories } from './useOpenDirectories';
|
|
12
|
+
import { useTabsState } from './useTabsState';
|
|
13
|
+
import { defaultTheme } from './themes';
|
|
9
14
|
import './style.scss';
|
|
10
|
-
import { oneDark } from '@codemirror/theme-one-dark';
|
|
11
|
-
import { FileId } from '../types';
|
|
12
15
|
|
|
13
16
|
// Register our custom JSON1 OT type that supports presence.
|
|
14
17
|
// See https://github.com/vizhub-core/json1-presence
|
|
@@ -36,20 +39,31 @@ function App() {
|
|
|
36
39
|
|
|
37
40
|
// The `doc.data` part of the ShareDB document,
|
|
38
41
|
// updated on each change to decouple rendering from ShareDB.
|
|
39
|
-
const [data, setData] = useState(null);
|
|
42
|
+
const [data, setData] = useState<Files>(null);
|
|
40
43
|
|
|
41
44
|
// The id of the currently open file tab.
|
|
42
|
-
const [activeFileId, setActiveFileId] = useState(null);
|
|
45
|
+
const [activeFileId, setActiveFileId] = useState<FileId>(null);
|
|
43
46
|
|
|
44
47
|
// The ordered list of tabs.
|
|
45
|
-
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
|
+
);
|
|
46
57
|
|
|
47
58
|
// The current theme.
|
|
48
|
-
const [theme, setTheme] = useState(
|
|
59
|
+
const [theme, setTheme] = useState<string>(defaultTheme);
|
|
49
60
|
|
|
50
61
|
// True to show the settings modal.
|
|
51
62
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
52
63
|
|
|
64
|
+
// The set of open directories.
|
|
65
|
+
const { isDirectoryOpen, toggleDirectory } = useOpenDirectories();
|
|
66
|
+
|
|
53
67
|
// Set up the connection to ShareDB.
|
|
54
68
|
useEffect(() => {
|
|
55
69
|
// Since there is only ever a single document,
|
|
@@ -74,13 +88,13 @@ function App() {
|
|
|
74
88
|
// Listen for all changes and update `data`.
|
|
75
89
|
// This decouples rendering logic from ShareDB.
|
|
76
90
|
// This callback gets called on each change.
|
|
77
|
-
shareDBDoc.on('op', (
|
|
91
|
+
shareDBDoc.on('op', () => {
|
|
78
92
|
setData(shareDBDoc.data);
|
|
79
93
|
});
|
|
80
94
|
|
|
81
95
|
// Set up presence.
|
|
82
96
|
// See https://github.com/share/sharedb/blob/master/examples/rich-text-presence/client.js#L53
|
|
83
|
-
const docPresence =
|
|
97
|
+
const docPresence = connection.getDocPresence(collection, id);
|
|
84
98
|
|
|
85
99
|
// Subscribe to receive remote presence updates.
|
|
86
100
|
docPresence.subscribe(function (error) {
|
|
@@ -96,45 +110,12 @@ function App() {
|
|
|
96
110
|
|
|
97
111
|
// TODO unsubscribe from presence
|
|
98
112
|
// TODO unsubscribe from doc
|
|
113
|
+
return () => {
|
|
114
|
+
// shareDBDoc.destroy();
|
|
115
|
+
// docPresence.destroy();
|
|
116
|
+
};
|
|
99
117
|
}, []);
|
|
100
118
|
|
|
101
|
-
// Called when a tab is closed.
|
|
102
|
-
const closeTab = useCallback(
|
|
103
|
-
(fileIdToRemove) => {
|
|
104
|
-
const i = tabList.findIndex((fileId) => fileId === fileIdToRemove);
|
|
105
|
-
|
|
106
|
-
// Support calling `closeTab` on a fileId that is not open,
|
|
107
|
-
// such as when a file is deleted..
|
|
108
|
-
if (i !== -1) {
|
|
109
|
-
// Remove the tab from the tab list.
|
|
110
|
-
const newTabList = [...tabList.slice(0, i), ...tabList.slice(i + 1)];
|
|
111
|
-
setTabList(newTabList);
|
|
112
|
-
|
|
113
|
-
// If we are closing the active file,
|
|
114
|
-
if (activeFileId === fileIdToRemove) {
|
|
115
|
-
// set the new active file to the next tab over,
|
|
116
|
-
if (newTabList.length > 0) {
|
|
117
|
-
setActiveFileId(i === 0 ? newTabList[i] : newTabList[i - 1]);
|
|
118
|
-
} else {
|
|
119
|
-
// or clear out the active file
|
|
120
|
-
// if we've closed the last tab.
|
|
121
|
-
setActiveFileId(null);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
[tabList, activeFileId]
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const handleCloseTabClick = useCallback(
|
|
130
|
-
(fileIdToRemove) => (event) => {
|
|
131
|
-
// Stop propagation so that the outer listener doesn't fire.
|
|
132
|
-
event.stopPropagation();
|
|
133
|
-
closeTab(fileIdToRemove);
|
|
134
|
-
},
|
|
135
|
-
[closeTab]
|
|
136
|
-
);
|
|
137
|
-
|
|
138
119
|
// Called when a file in the sidebar is double-clicked.
|
|
139
120
|
const handleRenameFileClick = useCallback(
|
|
140
121
|
(fileId: FileId) => {
|
|
@@ -151,7 +132,7 @@ function App() {
|
|
|
151
132
|
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
152
133
|
}
|
|
153
134
|
},
|
|
154
|
-
[shareDBDoc]
|
|
135
|
+
[shareDBDoc],
|
|
155
136
|
);
|
|
156
137
|
|
|
157
138
|
const deleteFile = useCallback(
|
|
@@ -165,17 +146,7 @@ function App() {
|
|
|
165
146
|
delete nextDocument[fileId];
|
|
166
147
|
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
167
148
|
},
|
|
168
|
-
[shareDBDoc, closeTab]
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const handleFileClick = useCallback(
|
|
172
|
-
(fileId: FileId) => {
|
|
173
|
-
setActiveFileId(fileId);
|
|
174
|
-
if (!tabList.includes(fileId)) {
|
|
175
|
-
setTabList([...tabList, fileId]);
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
[tabList]
|
|
149
|
+
[shareDBDoc, closeTab],
|
|
179
150
|
);
|
|
180
151
|
|
|
181
152
|
// TODO prompt the user "Are you sure?"
|
|
@@ -186,7 +157,7 @@ function App() {
|
|
|
186
157
|
event.stopPropagation();
|
|
187
158
|
deleteFile(fileId);
|
|
188
159
|
},
|
|
189
|
-
[deleteFile]
|
|
160
|
+
[deleteFile],
|
|
190
161
|
);
|
|
191
162
|
|
|
192
163
|
const createFile = useCallback(() => {
|
|
@@ -201,20 +172,10 @@ 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
|
-
//adding the folder that the is in to the tab name
|
|
214
|
-
if (split.length === 1) return split[split.length - 1];
|
|
215
|
-
return split[split.length - 2] + '/' + split[split.length - 1];
|
|
216
|
-
};
|
|
217
|
-
|
|
218
179
|
return (
|
|
219
180
|
<div className="app">
|
|
220
181
|
<div className="left">
|
|
@@ -223,9 +184,10 @@ function App() {
|
|
|
223
184
|
files={data}
|
|
224
185
|
handleRenameFileClick={handleRenameFileClick}
|
|
225
186
|
handleDeleteFileClick={handleDeleteFileClick}
|
|
226
|
-
handleFileClick={
|
|
187
|
+
handleFileClick={openTab}
|
|
227
188
|
setIsSettingsOpen={setIsSettingsOpen}
|
|
228
|
-
|
|
189
|
+
isDirectoryOpen={isDirectoryOpen}
|
|
190
|
+
toggleDirectory={toggleDirectory}
|
|
229
191
|
/>
|
|
230
192
|
<Settings
|
|
231
193
|
show={isSettingsOpen}
|
|
@@ -234,30 +196,15 @@ function App() {
|
|
|
234
196
|
/>
|
|
235
197
|
</div>
|
|
236
198
|
<div className="right">
|
|
237
|
-
<
|
|
238
|
-
{
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
: null
|
|
245
|
-
}
|
|
246
|
-
onClick={() => {
|
|
247
|
-
setActiveFileId(fileId);
|
|
248
|
-
}}
|
|
249
|
-
>
|
|
250
|
-
{tabValid ? fileNameSplit(data[fileId].name) : ''}
|
|
251
|
-
<div
|
|
252
|
-
className={activeFileId ? 'bx bx-x tab-close' : ''}
|
|
253
|
-
onClick={handleCloseTabClick(fileId)}
|
|
254
|
-
></div>
|
|
255
|
-
</div>
|
|
256
|
-
))}
|
|
257
|
-
</div>
|
|
199
|
+
<TabList
|
|
200
|
+
files={data}
|
|
201
|
+
tabList={tabList}
|
|
202
|
+
activeFileId={activeFileId}
|
|
203
|
+
setActiveFileId={setActiveFileId}
|
|
204
|
+
closeTab={closeTab}
|
|
205
|
+
/>
|
|
258
206
|
{data && activeFileId ? (
|
|
259
207
|
<CodeEditor
|
|
260
|
-
className="editor"
|
|
261
208
|
shareDBDoc={shareDBDoc}
|
|
262
209
|
localPresence={localPresence}
|
|
263
210
|
docPresence={docPresence}
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
import { widgets } from './widgets';
|
|
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
|
+
extensions.push(widgets());
|
|
101
|
+
|
|
102
|
+
let editor = editorCache.get(fileId);
|
|
103
|
+
if (!editor) {
|
|
104
|
+
editor = new EditorView({
|
|
105
|
+
state: EditorState.create({
|
|
106
|
+
doc: text,
|
|
107
|
+
extensions,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Populate the cache.
|
|
112
|
+
editorCache.set(fileId, editor);
|
|
113
|
+
}
|
|
114
|
+
return editor;
|
|
115
|
+
};
|
|
@@ -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,48 @@
|
|
|
1
|
+
import interact from '@replit/codemirror-interact';
|
|
2
|
+
|
|
3
|
+
// Interactive code widgets.
|
|
4
|
+
// * Number dragger
|
|
5
|
+
// * Boolean toggler
|
|
6
|
+
// * URL clicker
|
|
7
|
+
// * TODO color picker
|
|
8
|
+
// Inspired by:
|
|
9
|
+
// https://github.com/replit/codemirror-interact/blob/master/dev/index.ts
|
|
10
|
+
export const widgets = () =>
|
|
11
|
+
interact({
|
|
12
|
+
rules: [
|
|
13
|
+
// a rule for a number dragger
|
|
14
|
+
{
|
|
15
|
+
// the regexp matching the value
|
|
16
|
+
regexp: /-?\b\d+\.?\d*\b/g,
|
|
17
|
+
// set cursor to "ew-resize" on hover
|
|
18
|
+
cursor: 'ew-resize',
|
|
19
|
+
// change number value based on mouse X movement on drag
|
|
20
|
+
onDrag: (text, setText, e) => {
|
|
21
|
+
const newVal = Number(text) + e.movementX;
|
|
22
|
+
if (isNaN(newVal)) return;
|
|
23
|
+
setText(newVal.toString());
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
// bool toggler
|
|
27
|
+
{
|
|
28
|
+
regexp: /true|false/g,
|
|
29
|
+
cursor: 'pointer',
|
|
30
|
+
onClick: (text, setText) => {
|
|
31
|
+
switch (text) {
|
|
32
|
+
case 'true':
|
|
33
|
+
return setText('false');
|
|
34
|
+
case 'false':
|
|
35
|
+
return setText('true');
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
// url clicker
|
|
40
|
+
{
|
|
41
|
+
regexp: /https?:\/\/[^ "]+/g,
|
|
42
|
+
cursor: 'pointer',
|
|
43
|
+
onClick: (text) => {
|
|
44
|
+
window.open(text);
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
@@ -3,29 +3,7 @@ import Modal from 'react-bootstrap/Modal';
|
|
|
3
3
|
import Form from 'react-bootstrap/Form';
|
|
4
4
|
import Button from 'react-bootstrap/Button';
|
|
5
5
|
import Select from 'react-select';
|
|
6
|
-
import {
|
|
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
|
-
];
|
|
6
|
+
import { themes } from './themes';
|
|
29
7
|
|
|
30
8
|
const saveTimes = [
|
|
31
9
|
{ value: 1, label: '1 second' },
|
|
@@ -34,12 +12,16 @@ const saveTimes = [
|
|
|
34
12
|
{ value: 30, label: '30 seconds' },
|
|
35
13
|
];
|
|
36
14
|
|
|
37
|
-
export const Settings = ({ show, onClose, setTheme
|
|
15
|
+
export const Settings = ({ show, onClose, setTheme }) => {
|
|
38
16
|
const handleChange = useCallback((selectedOption) => {
|
|
39
|
-
setTheme(selectedOption.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
// });
|
|
43
25
|
}, []);
|
|
44
26
|
|
|
45
27
|
const handleSaveTimeChange = useCallback((selectedOption) => {
|
|
@@ -54,7 +36,12 @@ export const Settings = ({ show, onClose, setTheme, editor }) => {
|
|
|
54
36
|
}, []);
|
|
55
37
|
|
|
56
38
|
return show ? (
|
|
57
|
-
<Modal
|
|
39
|
+
<Modal
|
|
40
|
+
className="vz-settings"
|
|
41
|
+
show={show}
|
|
42
|
+
onHide={onClose}
|
|
43
|
+
animation={false}
|
|
44
|
+
>
|
|
58
45
|
<Modal.Header closeButton>
|
|
59
46
|
<Modal.Title>Settings</Modal.Title>
|
|
60
47
|
</Modal.Header>
|
|
@@ -68,22 +55,14 @@ export const Settings = ({ show, onClose, setTheme, editor }) => {
|
|
|
68
55
|
</Form.Group>
|
|
69
56
|
<Form.Group className="mb-3" controlId="formFork">
|
|
70
57
|
<Form.Label>Theme</Form.Label>
|
|
71
|
-
<Select
|
|
72
|
-
classvalue="themes"
|
|
73
|
-
options={themes}
|
|
74
|
-
onChange={handleChange}
|
|
75
|
-
/>
|
|
58
|
+
<Select options={themes} onChange={handleChange} />
|
|
76
59
|
<Form.Text className="text-muted">
|
|
77
60
|
Select a color theme for the editor
|
|
78
61
|
</Form.Text>
|
|
79
62
|
</Form.Group>
|
|
80
63
|
<Form.Group className="mb-3" controlId="formFork">
|
|
81
64
|
<Form.Label>Auto-Save Time</Form.Label>
|
|
82
|
-
<Select
|
|
83
|
-
classvalue="themes"
|
|
84
|
-
options={saveTimes}
|
|
85
|
-
onChange={handleSaveTimeChange}
|
|
86
|
-
/>
|
|
65
|
+
<Select options={saveTimes} onChange={handleSaveTimeChange} />
|
|
87
66
|
<Form.Text className="text-muted">Select an auto save time</Form.Text>
|
|
88
67
|
</Form.Group>
|
|
89
68
|
</Modal.Body>
|