vzcode 0.15.0 → 0.17.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 +98 -130
- package/dist/assets/index-ab3ae13e.js +115 -0
- package/dist/assets/worker-d5f0575a.js +237 -0
- package/dist/index.html +1 -1
- package/package.json +5 -5
- package/src/client/App.tsx +61 -35
- package/src/client/CodeEditor/getOrCreateEditor.ts +100 -42
- package/src/client/CodeEditor/index.tsx +22 -10
- package/src/client/Settings.tsx +17 -3
- package/src/client/main.jsx +3 -3
- package/src/client/themes.ts +30 -7
- package/src/client/useDynamicTheme.ts +26 -0
- package/src/client/useEditorCache.ts +21 -0
- package/src/client/usePrettier/index.ts +2 -5
- package/src/client/vizhubTheme.js +1 -0
- package/dist/assets/index-176d73ed.js +0 -115
- package/dist/assets/worker-aa634fce.js +0 -237
package/dist/index.html
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
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-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-ab3ae13e.js"></script>
|
|
22
22
|
<link rel="stylesheet" href="/assets/index-ccd52a89.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
"homepage": "https://github.com/vizhub-core/vzcode#readme",
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@codemirror/lang-css": "^6.2.1",
|
|
50
|
-
"@codemirror/lang-html": "^6.4.
|
|
51
|
-
"@codemirror/lang-javascript": "^6.2.
|
|
50
|
+
"@codemirror/lang-html": "^6.4.6",
|
|
51
|
+
"@codemirror/lang-javascript": "^6.2.1",
|
|
52
52
|
"@codemirror/lang-markdown": "^6.2.0",
|
|
53
53
|
"@codemirror/state": "^6.2.1",
|
|
54
54
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
55
55
|
"@replit/codemirror-interact": "^6.3.0",
|
|
56
56
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
57
|
-
"@uiw/codemirror-themes-all": "^4.21.
|
|
57
|
+
"@uiw/codemirror-themes-all": "^4.21.12",
|
|
58
58
|
"codemirror": "^6.0.1",
|
|
59
59
|
"codemirror-ot": "^4.1.0",
|
|
60
60
|
"d3-array": "^3.2.4",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"@types/react": "^18.2.21",
|
|
78
78
|
"@types/react-dom": "^18.2.7",
|
|
79
79
|
"@vitejs/plugin-react": "^4.0.4",
|
|
80
|
-
"prettier": "^3.0.
|
|
80
|
+
"prettier": "^3.0.3",
|
|
81
81
|
"sass": "^1.66.1",
|
|
82
82
|
"typescript": "^5.2.2",
|
|
83
83
|
"vite": "^4.4.9",
|
package/src/client/App.tsx
CHANGED
|
@@ -10,9 +10,16 @@ import { FileId, Files, ShareDBDoc, VZCodeContent } from '../types';
|
|
|
10
10
|
import { TabList } from './TabList';
|
|
11
11
|
import { useOpenDirectories } from './useOpenDirectories';
|
|
12
12
|
import { useTabsState } from './useTabsState';
|
|
13
|
-
import { defaultTheme } from './themes';
|
|
13
|
+
import { ThemeLabel, defaultTheme } from './themes';
|
|
14
14
|
import './style.scss';
|
|
15
|
+
import { useEditorCache } from './useEditorCache';
|
|
16
|
+
import { useDynamicTheme } from './useDynamicTheme';
|
|
15
17
|
import { usePrettier } from './usePrettier';
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
import PrettierWorker from './usePrettier/worker?worker';
|
|
20
|
+
|
|
21
|
+
// Instantiate the Prettier worker.
|
|
22
|
+
const prettierWorker = new PrettierWorker();
|
|
16
23
|
|
|
17
24
|
// Register our custom JSON1 OT type that supports presence.
|
|
18
25
|
// See https://github.com/vizhub-core/json1-presence
|
|
@@ -45,7 +52,7 @@ function App() {
|
|
|
45
52
|
);
|
|
46
53
|
|
|
47
54
|
// Auto-run Pretter after local changes.
|
|
48
|
-
usePrettier(shareDBDoc, submitOperation);
|
|
55
|
+
usePrettier(shareDBDoc, submitOperation, prettierWorker);
|
|
49
56
|
|
|
50
57
|
// Local ShareDB presence, for broadcasting our cursor position
|
|
51
58
|
// so other clients can see it.
|
|
@@ -61,30 +68,8 @@ function App() {
|
|
|
61
68
|
// Starts out as `null` until the document is loaded.
|
|
62
69
|
const [content, setContent] = useState<VZCodeContent | null>(null);
|
|
63
70
|
|
|
64
|
-
// The id of the currently open file tab.
|
|
65
|
-
const [activeFileId, setActiveFileId] = useState<FileId>(null);
|
|
66
|
-
|
|
67
|
-
// The ordered list of tabs.
|
|
68
|
-
const [tabList, setTabList] = useState<Array<FileId>>([]);
|
|
69
|
-
|
|
70
|
-
// Logic for opening and closing tabs.
|
|
71
|
-
const { closeTab, openTab } = useTabsState(
|
|
72
|
-
activeFileId,
|
|
73
|
-
setActiveFileId,
|
|
74
|
-
tabList,
|
|
75
|
-
setTabList,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// The current theme.
|
|
79
|
-
const [theme, setTheme] = useState<string>(defaultTheme);
|
|
80
|
-
|
|
81
|
-
// True to show the settings modal.
|
|
82
|
-
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
83
|
-
|
|
84
|
-
// The set of open directories.
|
|
85
|
-
const { isDirectoryOpen, toggleDirectory } = useOpenDirectories();
|
|
86
|
-
|
|
87
71
|
// Set up the connection to ShareDB.
|
|
72
|
+
// TODO move this logic to a hook called `useShareDB`
|
|
88
73
|
useEffect(() => {
|
|
89
74
|
// Since there is only ever a single document,
|
|
90
75
|
// these things are pretty arbitrary.
|
|
@@ -136,6 +121,54 @@ function App() {
|
|
|
136
121
|
};
|
|
137
122
|
}, []);
|
|
138
123
|
|
|
124
|
+
// The id of the currently open file tab.
|
|
125
|
+
// TODO make this a URL param
|
|
126
|
+
const [activeFileId, setActiveFileId] = useState<FileId>(null);
|
|
127
|
+
|
|
128
|
+
// The ordered list of tabs.
|
|
129
|
+
// TODO make this a URL param
|
|
130
|
+
const [tabList, setTabList] = useState<Array<FileId>>([]);
|
|
131
|
+
|
|
132
|
+
// Logic for opening and closing tabs.
|
|
133
|
+
const { closeTab, openTab } = useTabsState(
|
|
134
|
+
activeFileId,
|
|
135
|
+
setActiveFileId,
|
|
136
|
+
tabList,
|
|
137
|
+
setTabList,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// The current theme.
|
|
141
|
+
// TODO persist this in local storage
|
|
142
|
+
const [theme, setTheme] = useState<ThemeLabel>(defaultTheme);
|
|
143
|
+
|
|
144
|
+
// Cache of CodeMirror editors by file id.
|
|
145
|
+
const editorCache = useEditorCache();
|
|
146
|
+
|
|
147
|
+
// Handle dynamic theme changes.
|
|
148
|
+
useDynamicTheme(editorCache, theme);
|
|
149
|
+
|
|
150
|
+
// True to show the settings modal.
|
|
151
|
+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
152
|
+
|
|
153
|
+
// The set of open directories.
|
|
154
|
+
const { isDirectoryOpen, toggleDirectory } = useOpenDirectories();
|
|
155
|
+
|
|
156
|
+
// TODO move this logic to a hook called `useFileCRUD`
|
|
157
|
+
// Include
|
|
158
|
+
// - `createFile`
|
|
159
|
+
// - `handleRenameFileClick`
|
|
160
|
+
// - `deleteFile`
|
|
161
|
+
// - `handleDeleteFileClick`
|
|
162
|
+
const createFile = useCallback(() => {
|
|
163
|
+
const name = prompt('Enter new file name');
|
|
164
|
+
if (name) {
|
|
165
|
+
submitOperation((document) => ({
|
|
166
|
+
...document,
|
|
167
|
+
files: { ...document.files, [randomId()]: { name, text: '' } },
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
}, [submitOperation]);
|
|
171
|
+
|
|
139
172
|
// Called when a file in the sidebar is double-clicked.
|
|
140
173
|
const handleRenameFileClick = useCallback(
|
|
141
174
|
(fileId: FileId) => {
|
|
@@ -170,16 +203,6 @@ function App() {
|
|
|
170
203
|
[submitOperation, closeTab],
|
|
171
204
|
);
|
|
172
205
|
|
|
173
|
-
const createFile = useCallback(() => {
|
|
174
|
-
const name = prompt('Enter new file name');
|
|
175
|
-
if (name) {
|
|
176
|
-
submitOperation((document) => ({
|
|
177
|
-
...document,
|
|
178
|
-
files: { ...document.files, [randomId()]: { name, text: '' } },
|
|
179
|
-
}));
|
|
180
|
-
}
|
|
181
|
-
}, [submitOperation]);
|
|
182
|
-
|
|
183
206
|
// TODO prompt the user "Are you sure?"
|
|
184
207
|
const handleDeleteFileClick = useCallback(
|
|
185
208
|
(fileId: FileId, event: React.MouseEvent) => {
|
|
@@ -213,6 +236,7 @@ function App() {
|
|
|
213
236
|
}, 800);
|
|
214
237
|
}, [submitOperation]);
|
|
215
238
|
|
|
239
|
+
// Isolate the files object from the document.
|
|
216
240
|
const files: Files | null = content ? content.files : null;
|
|
217
241
|
|
|
218
242
|
return (
|
|
@@ -231,6 +255,7 @@ function App() {
|
|
|
231
255
|
<Settings
|
|
232
256
|
show={isSettingsOpen}
|
|
233
257
|
onClose={handleSettingsClose}
|
|
258
|
+
theme={theme}
|
|
234
259
|
setTheme={setTheme}
|
|
235
260
|
/>
|
|
236
261
|
</div>
|
|
@@ -250,6 +275,7 @@ function App() {
|
|
|
250
275
|
activeFileId={activeFileId}
|
|
251
276
|
theme={theme}
|
|
252
277
|
onInteract={handleInteract}
|
|
278
|
+
editorCache={editorCache}
|
|
253
279
|
/>
|
|
254
280
|
) : null}
|
|
255
281
|
</div>
|
|
@@ -6,17 +6,26 @@ import { html } from '@codemirror/lang-html';
|
|
|
6
6
|
import { css } from '@codemirror/lang-css';
|
|
7
7
|
import { json1Sync } from 'codemirror-ot';
|
|
8
8
|
import { json1Presence, textUnicode } from '../../ot';
|
|
9
|
-
import { FileId, ShareDBDoc } from '../../types';
|
|
9
|
+
import { FileId, ShareDBDoc, VZCodeContent } from '../../types';
|
|
10
10
|
import { json1PresenceBroadcast } from './json1PresenceBroadcast';
|
|
11
11
|
import { json1PresenceDisplay } from './json1PresenceDisplay';
|
|
12
12
|
import { widgets } from './widgets';
|
|
13
|
+
import { EditorCache, EditorCacheValue } from '../useEditorCache';
|
|
14
|
+
import { ThemeLabel, themeOptionsByLabel } from '../themes';
|
|
13
15
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
// Language extensions for CodeMirror.
|
|
17
|
+
// Keys are file extensions.
|
|
18
|
+
// Values are CodeMirror extensions.
|
|
19
|
+
// TODO consider moving this to a separate file.
|
|
20
|
+
const languageExtensions = {
|
|
21
|
+
js: javascript,
|
|
22
|
+
json: javascript,
|
|
23
|
+
jsx: javascript,
|
|
24
|
+
ts: javascript,
|
|
25
|
+
html: html,
|
|
26
|
+
css: css,
|
|
27
|
+
md: markdown,
|
|
28
|
+
};
|
|
20
29
|
|
|
21
30
|
// Gets a value at a path in an object.
|
|
22
31
|
// e.g. getAtPath({a: {b: 1}}, ['a', 'b']) === 1
|
|
@@ -29,7 +38,8 @@ const getAtPath = (obj, path) => {
|
|
|
29
38
|
return current;
|
|
30
39
|
};
|
|
31
40
|
|
|
32
|
-
// Gets or creates
|
|
41
|
+
// Gets or creates an `editorCache` entry for the given file id.
|
|
42
|
+
// Looks in `editorCache` first, and if not found, creates a new editor.
|
|
33
43
|
export const getOrCreateEditor = ({
|
|
34
44
|
fileId,
|
|
35
45
|
shareDBDoc,
|
|
@@ -38,27 +48,46 @@ export const getOrCreateEditor = ({
|
|
|
38
48
|
docPresence,
|
|
39
49
|
theme,
|
|
40
50
|
onInteract,
|
|
51
|
+
editorCache,
|
|
41
52
|
}: {
|
|
42
53
|
fileId: FileId;
|
|
43
|
-
|
|
54
|
+
|
|
55
|
+
// The ShareDB document that contains the file.
|
|
56
|
+
// Used when the editor is created for:
|
|
57
|
+
// * Figuring out the initial text
|
|
58
|
+
// * Figuring out the initial language extension
|
|
59
|
+
// based on the file extension.
|
|
60
|
+
// It's also passed into the `json1Sync` extension,
|
|
61
|
+
// which is used for multiplayer editing.
|
|
62
|
+
shareDBDoc: ShareDBDoc<VZCodeContent>;
|
|
63
|
+
|
|
44
64
|
filesPath: string[];
|
|
45
65
|
localPresence: any;
|
|
46
66
|
docPresence: any;
|
|
47
|
-
theme:
|
|
67
|
+
theme: ThemeLabel;
|
|
48
68
|
onInteract?: () => void;
|
|
49
|
-
|
|
69
|
+
editorCache: EditorCache;
|
|
70
|
+
}): EditorCacheValue => {
|
|
71
|
+
// Cache hit
|
|
72
|
+
if (editorCache.has(fileId)) {
|
|
73
|
+
return editorCache.get(fileId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Cache miss
|
|
77
|
+
|
|
78
|
+
// Compute `text` and `fileExtension` from the ShareDB document.
|
|
50
79
|
const data = shareDBDoc.data;
|
|
51
80
|
const textPath = [...filesPath, fileId, 'text'];
|
|
52
81
|
const namePath = [...filesPath, fileId, 'name'];
|
|
53
|
-
|
|
54
82
|
const text = getAtPath(data, textPath);
|
|
55
83
|
const name = getAtPath(data, namePath);
|
|
56
|
-
|
|
57
84
|
const fileExtension = name.split('.').pop();
|
|
58
85
|
|
|
59
|
-
//
|
|
60
|
-
|
|
86
|
+
// Create a compartment for the theme so that it can be changed dynamically.
|
|
87
|
+
// Inspired by: https://github.com/craftzdog/cm6-themes/blob/main/example/index.ts
|
|
88
|
+
let themeCompartment = new Compartment();
|
|
61
89
|
|
|
90
|
+
// The CodeMirror extensions to use.
|
|
62
91
|
const extensions = [];
|
|
63
92
|
|
|
64
93
|
// This plugin implements multiplayer editing,
|
|
@@ -81,38 +110,67 @@ export const getOrCreateEditor = ({
|
|
|
81
110
|
if (docPresence)
|
|
82
111
|
extensions.push(json1PresenceDisplay({ path: textPath, docPresence }));
|
|
83
112
|
|
|
113
|
+
// This is the "basic setup" for CodeMirror,
|
|
114
|
+
// which actually adds a ton on functionality.
|
|
115
|
+
// TODO vet this functionality, and determine how much
|
|
116
|
+
// we want to replace with
|
|
117
|
+
// https://github.com/vizhub-core/vzcode/issues/134
|
|
84
118
|
extensions.push(basicSetup);
|
|
85
|
-
|
|
119
|
+
|
|
120
|
+
// This supports dynamic changing of the theme.
|
|
121
|
+
extensions.push(themeCompartment.of(themeOptionsByLabel[theme].value));
|
|
86
122
|
|
|
87
123
|
// TODO handle dynamic changing of the file extension.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
extensions.push(
|
|
124
|
+
// TODO handle dynamic file extensions by making
|
|
125
|
+
// the CodeMirror language extension dynamic
|
|
126
|
+
// using a Compartment.
|
|
127
|
+
const languageCompartment = new Compartment();
|
|
128
|
+
// See https://github.com/vizhub-core/vzcode/issues/55
|
|
129
|
+
const getLanguageExtension = (fileExtension) => {
|
|
130
|
+
const languageFunc = languageExtensions[fileExtension];
|
|
131
|
+
return languageFunc ? languageFunc() : undefined;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const languageExtension = getLanguageExtension(fileExtension);
|
|
135
|
+
if (languageExtension) {
|
|
136
|
+
extensions.push(languageCompartment.of(languageExtension));
|
|
137
|
+
} else {
|
|
138
|
+
// Not sure if this case even works.
|
|
139
|
+
// TODO manually test this case by creating a file
|
|
140
|
+
// that has no extension, opening it up,
|
|
141
|
+
// and then adding an extension.
|
|
142
|
+
console.warn(`No language extension for file extension: ${fileExtension}`);
|
|
143
|
+
// We still need to push the compartment,
|
|
144
|
+
// otherwise the compartment won't work when
|
|
145
|
+
// a file extension _is_ added later on.
|
|
146
|
+
extensions.push(languageCompartment.of([]));
|
|
101
147
|
}
|
|
102
148
|
|
|
149
|
+
// Add interactive widgets.
|
|
150
|
+
// Includes the Alt+drag functionality for numbers.
|
|
151
|
+
// Calls `onInteract` when one of those widgets is interacted with.
|
|
152
|
+
// This can be used to trigger a transition to throttled mode
|
|
153
|
+
// for hot reloading.
|
|
154
|
+
// TODO consider leveraging the new `dragEnd` event handler.
|
|
155
|
+
// and removing the `onInteract` callback, replacing it with
|
|
156
|
+
// `onInteractStart` and `onInteractEnd`.
|
|
157
|
+
// That may be tricky for one-off interactions though, like
|
|
158
|
+
// the boolean checkboxes. The color pickers are also tricky,
|
|
159
|
+
// as they would also need to be able to handle `onInteractEnd`.
|
|
160
|
+
// See https://github.com/replit/codemirror-interact/issues/14
|
|
103
161
|
extensions.push(widgets({ onInteract }));
|
|
104
162
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
163
|
+
const editor = new EditorView({
|
|
164
|
+
state: EditorState.create({
|
|
165
|
+
doc: text,
|
|
166
|
+
extensions,
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const editorCacheValue = { editor, themeCompartment };
|
|
171
|
+
|
|
172
|
+
// Populate the cache.
|
|
173
|
+
editorCache.set(fileId, editorCacheValue);
|
|
174
|
+
|
|
175
|
+
return editorCacheValue;
|
|
118
176
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useRef, useLayoutEffect } from 'react';
|
|
2
2
|
import { FileId } from '../../types';
|
|
3
|
-
import {
|
|
3
|
+
import { ThemeLabel, defaultTheme } from '../themes';
|
|
4
|
+
import { EditorCache, EditorCacheValue } from '../useEditorCache';
|
|
4
5
|
import { getOrCreateEditor } from './getOrCreateEditor';
|
|
5
6
|
import './style.scss';
|
|
6
7
|
|
|
@@ -12,40 +13,51 @@ export const CodeEditor = ({
|
|
|
12
13
|
theme = defaultTheme,
|
|
13
14
|
filesPath = ['files'],
|
|
14
15
|
onInteract,
|
|
16
|
+
editorCache,
|
|
15
17
|
}: {
|
|
16
18
|
activeFileId: FileId;
|
|
17
19
|
shareDBDoc: any;
|
|
18
20
|
localPresence?: any;
|
|
19
21
|
docPresence?: any;
|
|
20
|
-
theme?:
|
|
22
|
+
theme?: ThemeLabel;
|
|
21
23
|
|
|
22
24
|
// The path of the files object in the ShareDB document.
|
|
23
|
-
// Defaults to
|
|
25
|
+
// Defaults to `files` if not provided.
|
|
24
26
|
filesPath?: string[];
|
|
25
27
|
onInteract?: () => void;
|
|
28
|
+
editorCache: EditorCache;
|
|
26
29
|
}) => {
|
|
27
30
|
const ref = useRef<HTMLDivElement>(null);
|
|
28
31
|
|
|
32
|
+
// Every time the active file switches from one file to another,
|
|
33
|
+
// the editor corresponding to the old file is removed from the DOM,
|
|
34
|
+
// and the editor corresponding to the new file is added to the DOM.
|
|
35
|
+
|
|
29
36
|
useLayoutEffect(() => {
|
|
37
|
+
// Guard against cases where page is still loading.
|
|
30
38
|
if (!ref.current) return;
|
|
31
39
|
if (!shareDBDoc) return;
|
|
32
40
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
const
|
|
41
|
+
// Get the editor corresponding to the active file.
|
|
42
|
+
// Looks in `editorCache` first, and if not found, creates a new editor.
|
|
43
|
+
const editorCacheValue: EditorCacheValue = getOrCreateEditor({
|
|
36
44
|
fileId: activeFileId,
|
|
37
45
|
shareDBDoc,
|
|
38
46
|
filesPath,
|
|
39
47
|
localPresence,
|
|
40
48
|
docPresence,
|
|
41
|
-
|
|
42
|
-
theme: themesByLabel[theme],
|
|
49
|
+
theme,
|
|
43
50
|
onInteract,
|
|
51
|
+
editorCache,
|
|
44
52
|
});
|
|
45
|
-
|
|
53
|
+
|
|
54
|
+
// Add the editor to the DOM.
|
|
55
|
+
ref.current.appendChild(editorCacheValue.editor.dom);
|
|
46
56
|
|
|
47
57
|
return () => {
|
|
48
|
-
|
|
58
|
+
// Remove the old editor from the DOM.
|
|
59
|
+
// This happens every time `activeFileId` changes.
|
|
60
|
+
ref.current.removeChild(editorCacheValue.editor.dom);
|
|
49
61
|
};
|
|
50
62
|
}, [shareDBDoc, activeFileId]);
|
|
51
63
|
|
package/src/client/Settings.tsx
CHANGED
|
@@ -3,7 +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 { themes } from './themes';
|
|
6
|
+
import { ThemeLabel, themeOptionsByLabel, themes } from './themes';
|
|
7
7
|
|
|
8
8
|
const saveTimes = [
|
|
9
9
|
{ value: 1, label: '1 second' },
|
|
@@ -12,7 +12,17 @@ const saveTimes = [
|
|
|
12
12
|
{ value: 30, label: '30 seconds' },
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
-
export const Settings = ({
|
|
15
|
+
export const Settings = ({
|
|
16
|
+
show,
|
|
17
|
+
onClose,
|
|
18
|
+
setTheme,
|
|
19
|
+
theme,
|
|
20
|
+
}: {
|
|
21
|
+
show: boolean;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
setTheme: (theme: ThemeLabel) => void;
|
|
24
|
+
theme: ThemeLabel;
|
|
25
|
+
}) => {
|
|
16
26
|
const handleChange = useCallback((selectedOption) => {
|
|
17
27
|
setTheme(selectedOption.label);
|
|
18
28
|
// TODO make this work.
|
|
@@ -55,7 +65,11 @@ export const Settings = ({ show, onClose, setTheme }) => {
|
|
|
55
65
|
</Form.Group>
|
|
56
66
|
<Form.Group className="mb-3" controlId="formFork">
|
|
57
67
|
<Form.Label>Theme</Form.Label>
|
|
58
|
-
<Select
|
|
68
|
+
<Select
|
|
69
|
+
options={themes}
|
|
70
|
+
onChange={handleChange}
|
|
71
|
+
value={themeOptionsByLabel[theme]}
|
|
72
|
+
/>
|
|
59
73
|
<Form.Text className="text-muted">
|
|
60
74
|
Select a color theme for the editor
|
|
61
75
|
</Form.Text>
|
package/src/client/main.jsx
CHANGED
package/src/client/themes.ts
CHANGED
|
@@ -7,11 +7,29 @@ import { githubDark } from '@uiw/codemirror-theme-github';
|
|
|
7
7
|
import { material } from '@uiw/codemirror-theme-material';
|
|
8
8
|
import { nord } from '@uiw/codemirror-theme-nord';
|
|
9
9
|
import { xcodeLight } from '@uiw/codemirror-theme-xcode';
|
|
10
|
+
// import { vizhub } from './vizhub-theme';
|
|
10
11
|
|
|
11
12
|
// CodeMirror themes
|
|
12
|
-
export
|
|
13
|
+
export type ThemeLabel =
|
|
14
|
+
| 'abcdef'
|
|
15
|
+
| 'dracula'
|
|
16
|
+
| 'eclipse'
|
|
17
|
+
| 'material'
|
|
18
|
+
| 'nord'
|
|
19
|
+
| 'oneDark'
|
|
20
|
+
| 'okaidia'
|
|
21
|
+
| 'github'
|
|
22
|
+
| 'xcode';
|
|
23
|
+
// | 'vizhub'
|
|
24
|
+
|
|
25
|
+
export type ThemeOption = {
|
|
26
|
+
value: any;
|
|
27
|
+
label: ThemeLabel;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const themes: Array<ThemeOption> = [
|
|
13
31
|
{ value: abcdef, label: 'abcdef' },
|
|
14
|
-
{ value: dracula, label: '
|
|
32
|
+
{ value: dracula, label: 'dracula' },
|
|
15
33
|
{ value: eclipse, label: 'eclipse' },
|
|
16
34
|
{ value: material, label: 'material' },
|
|
17
35
|
{ value: nord, label: 'nord' },
|
|
@@ -19,13 +37,18 @@ export const themes = [
|
|
|
19
37
|
{ value: okaidia, label: 'okaidia' },
|
|
20
38
|
{ value: githubDark, label: 'github' },
|
|
21
39
|
{ value: xcodeLight, label: 'xcode' },
|
|
40
|
+
// { value: vizhub, label: 'vizhub' },
|
|
22
41
|
];
|
|
23
42
|
|
|
24
43
|
// Map theme labels to theme values
|
|
25
|
-
export const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
export const themeOptionsByLabel: Record<ThemeLabel, ThemeOption> =
|
|
45
|
+
themes.reduce(
|
|
46
|
+
(acc, themeOption: ThemeOption) => {
|
|
47
|
+
acc[themeOption.label] = themeOption;
|
|
48
|
+
return acc;
|
|
49
|
+
},
|
|
50
|
+
{} as Record<ThemeLabel, any>,
|
|
51
|
+
);
|
|
29
52
|
|
|
30
53
|
// The default theme
|
|
31
|
-
export const defaultTheme = 'oneDark';
|
|
54
|
+
export const defaultTheme: ThemeLabel = 'oneDark';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { EditorCache } from './useEditorCache';
|
|
3
|
+
import { ThemeLabel, themeOptionsByLabel } from './themes';
|
|
4
|
+
|
|
5
|
+
export const useDynamicTheme = (
|
|
6
|
+
editorCache: EditorCache,
|
|
7
|
+
theme: ThemeLabel,
|
|
8
|
+
) => {
|
|
9
|
+
const isFirstRun = useRef(true);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
// Skip the first run, since the theme is already initialized.
|
|
12
|
+
if (isFirstRun.current) {
|
|
13
|
+
isFirstRun.current = false;
|
|
14
|
+
} else {
|
|
15
|
+
// When the user changes the theme, update the theme of all editors.
|
|
16
|
+
for (const editorCacheValue of Array.from(editorCache.values())) {
|
|
17
|
+
const { editor, themeCompartment } = editorCacheValue;
|
|
18
|
+
editor.dispatch({
|
|
19
|
+
effects: themeCompartment.reconfigure([
|
|
20
|
+
themeOptionsByLabel[theme].value,
|
|
21
|
+
]),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}, [theme, editorCache]);
|
|
26
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { FileId } from '../types';
|
|
3
|
+
|
|
4
|
+
export type EditorCacheValue = {
|
|
5
|
+
editor: any;
|
|
6
|
+
themeCompartment: any;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type EditorCache = Map<FileId, EditorCacheValue>;
|
|
10
|
+
|
|
11
|
+
export const useEditorCache = () => {
|
|
12
|
+
// Singleton cache of CodeMirror instances
|
|
13
|
+
// These are created, but never destroyed.
|
|
14
|
+
// TODO consider invalidating this cache at some point.
|
|
15
|
+
// Right now these editor instances just keep accumulating,
|
|
16
|
+
// and they all retain their connections to ShareDB.
|
|
17
|
+
// This has not been an issue so far, but it could become one.
|
|
18
|
+
const editorCacheRef = useRef<EditorCache>(new Map());
|
|
19
|
+
|
|
20
|
+
return editorCacheRef.current;
|
|
21
|
+
};
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { JSONOp } from 'ot-json1';
|
|
2
2
|
import { FileId, ShareDBDoc, VZCodeContent } from '../../types';
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
|
-
// @ts-ignore
|
|
5
|
-
import PrettierWorker from './worker?worker';
|
|
6
4
|
|
|
7
5
|
// The time in milliseconds by which auto-saving is debounced.
|
|
8
6
|
const autoPrettierDebounceTimeMS = 1200;
|
|
@@ -20,7 +18,8 @@ const extension = (fileName: string) => {
|
|
|
20
18
|
|
|
21
19
|
export const usePrettier = (
|
|
22
20
|
shareDBDoc: ShareDBDoc<VZCodeContent> | null,
|
|
23
|
-
submitOperation,
|
|
21
|
+
submitOperation: (next: (content: VZCodeContent) => VZCodeContent) => void,
|
|
22
|
+
prettierWorker: Worker,
|
|
24
23
|
) => {
|
|
25
24
|
// The set of files that have been modified
|
|
26
25
|
// since the last Prettier run.
|
|
@@ -36,8 +35,6 @@ export const usePrettier = (
|
|
|
36
35
|
return;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
const prettierWorker = new PrettierWorker();
|
|
40
|
-
|
|
41
38
|
// Get the message that comes back from the worker.
|
|
42
39
|
// This is the prettified text for a specific file.
|
|
43
40
|
const handleMessage = (event) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// TODO
|