vzcode 0.15.0 → 0.16.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-0818f26e.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 +55 -34
- 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/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-0818f26e.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.16.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,11 @@ 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
15
|
import { usePrettier } from './usePrettier';
|
|
16
|
+
import { useEditorCache } from './useEditorCache';
|
|
17
|
+
import { useDynamicTheme } from './useDynamicTheme';
|
|
16
18
|
|
|
17
19
|
// Register our custom JSON1 OT type that supports presence.
|
|
18
20
|
// See https://github.com/vizhub-core/json1-presence
|
|
@@ -61,30 +63,8 @@ function App() {
|
|
|
61
63
|
// Starts out as `null` until the document is loaded.
|
|
62
64
|
const [content, setContent] = useState<VZCodeContent | null>(null);
|
|
63
65
|
|
|
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
66
|
// Set up the connection to ShareDB.
|
|
67
|
+
// TODO move this logic to a hook called `useShareDB`
|
|
88
68
|
useEffect(() => {
|
|
89
69
|
// Since there is only ever a single document,
|
|
90
70
|
// these things are pretty arbitrary.
|
|
@@ -136,6 +116,54 @@ function App() {
|
|
|
136
116
|
};
|
|
137
117
|
}, []);
|
|
138
118
|
|
|
119
|
+
// The id of the currently open file tab.
|
|
120
|
+
// TODO make this a URL param
|
|
121
|
+
const [activeFileId, setActiveFileId] = useState<FileId>(null);
|
|
122
|
+
|
|
123
|
+
// The ordered list of tabs.
|
|
124
|
+
// TODO make this a URL param
|
|
125
|
+
const [tabList, setTabList] = useState<Array<FileId>>([]);
|
|
126
|
+
|
|
127
|
+
// Logic for opening and closing tabs.
|
|
128
|
+
const { closeTab, openTab } = useTabsState(
|
|
129
|
+
activeFileId,
|
|
130
|
+
setActiveFileId,
|
|
131
|
+
tabList,
|
|
132
|
+
setTabList,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// The current theme.
|
|
136
|
+
// TODO persist this in local storage
|
|
137
|
+
const [theme, setTheme] = useState<ThemeLabel>(defaultTheme);
|
|
138
|
+
|
|
139
|
+
// Cache of CodeMirror editors by file id.
|
|
140
|
+
const editorCache = useEditorCache();
|
|
141
|
+
|
|
142
|
+
// Handle dynamic theme changes.
|
|
143
|
+
useDynamicTheme(editorCache, theme);
|
|
144
|
+
|
|
145
|
+
// True to show the settings modal.
|
|
146
|
+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
147
|
+
|
|
148
|
+
// The set of open directories.
|
|
149
|
+
const { isDirectoryOpen, toggleDirectory } = useOpenDirectories();
|
|
150
|
+
|
|
151
|
+
// TODO move this logic to a hook called `useFileCRUD`
|
|
152
|
+
// Include
|
|
153
|
+
// - `createFile`
|
|
154
|
+
// - `handleRenameFileClick`
|
|
155
|
+
// - `deleteFile`
|
|
156
|
+
// - `handleDeleteFileClick`
|
|
157
|
+
const createFile = useCallback(() => {
|
|
158
|
+
const name = prompt('Enter new file name');
|
|
159
|
+
if (name) {
|
|
160
|
+
submitOperation((document) => ({
|
|
161
|
+
...document,
|
|
162
|
+
files: { ...document.files, [randomId()]: { name, text: '' } },
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
}, [submitOperation]);
|
|
166
|
+
|
|
139
167
|
// Called when a file in the sidebar is double-clicked.
|
|
140
168
|
const handleRenameFileClick = useCallback(
|
|
141
169
|
(fileId: FileId) => {
|
|
@@ -170,16 +198,6 @@ function App() {
|
|
|
170
198
|
[submitOperation, closeTab],
|
|
171
199
|
);
|
|
172
200
|
|
|
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
201
|
// TODO prompt the user "Are you sure?"
|
|
184
202
|
const handleDeleteFileClick = useCallback(
|
|
185
203
|
(fileId: FileId, event: React.MouseEvent) => {
|
|
@@ -213,6 +231,7 @@ function App() {
|
|
|
213
231
|
}, 800);
|
|
214
232
|
}, [submitOperation]);
|
|
215
233
|
|
|
234
|
+
// Isolate the files object from the document.
|
|
216
235
|
const files: Files | null = content ? content.files : null;
|
|
217
236
|
|
|
218
237
|
return (
|
|
@@ -231,6 +250,7 @@ function App() {
|
|
|
231
250
|
<Settings
|
|
232
251
|
show={isSettingsOpen}
|
|
233
252
|
onClose={handleSettingsClose}
|
|
253
|
+
theme={theme}
|
|
234
254
|
setTheme={setTheme}
|
|
235
255
|
/>
|
|
236
256
|
</div>
|
|
@@ -250,6 +270,7 @@ function App() {
|
|
|
250
270
|
activeFileId={activeFileId}
|
|
251
271
|
theme={theme}
|
|
252
272
|
onInteract={handleInteract}
|
|
273
|
+
editorCache={editorCache}
|
|
253
274
|
/>
|
|
254
275
|
) : null}
|
|
255
276
|
</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
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// TODO
|