vzcode 0.14.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/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-176d73ed.js"></script>
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.14.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.5",
51
- "@codemirror/lang-javascript": "^6.2.0",
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.11",
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",
@@ -70,14 +70,14 @@
70
70
  "react-select": "^5.7.4",
71
71
  "sharedb": "^4.0.0",
72
72
  "sharedb-client-browser": "^4.3.0",
73
+ "ot-json1": "^1.0.2",
73
74
  "ws": "^8.13.0"
74
75
  },
75
76
  "devDependencies": {
76
77
  "@types/react": "^18.2.21",
77
78
  "@types/react-dom": "^18.2.7",
78
79
  "@vitejs/plugin-react": "^4.0.4",
79
- "ot-json1": "^1.0.2",
80
- "prettier": "^3.0.2",
80
+ "prettier": "^3.0.3",
81
81
  "sass": "^1.66.1",
82
82
  "typescript": "^5.2.2",
83
83
  "vite": "^4.4.9",
@@ -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
- // Singleton cache of CodeMirror instances
15
- // These are created, but never destroyed.
16
- // TODO consider invalidating this cache at some point.
17
- // * Keys are file ids
18
- // * Values are CodeMirror instances (TODO add CodeMirror type)
19
- const editorCache = new Map<FileId, any>();
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 a CodeMirror editor for the given file id.
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
- shareDBDoc: ShareDBDoc<any>;
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: any;
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
- // TODO revisit if this is the right approach for dynamic themes
60
- let themeSet = new Compartment();
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
- extensions.push(themeSet.of(theme));
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
- if (
89
- fileExtension === 'js' ||
90
- fileExtension === 'json' ||
91
- fileExtension === 'jsx' ||
92
- fileExtension === 'ts'
93
- ) {
94
- extensions.push(javascript());
95
- } else if (fileExtension === 'html') {
96
- extensions.push(html());
97
- } else if (fileExtension === 'css') {
98
- extensions.push(css());
99
- } else if (fileExtension === 'md') {
100
- extensions.push(markdown());
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
- let editor = editorCache.get(fileId);
106
- if (!editor) {
107
- editor = new EditorView({
108
- state: EditorState.create({
109
- doc: text,
110
- extensions,
111
- }),
112
- });
113
-
114
- // Populate the cache.
115
- editorCache.set(fileId, editor);
116
- }
117
- return editor;
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 { defaultTheme, themesByLabel } from '../themes';
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?: string;
22
+ theme?: ThemeLabel;
21
23
 
22
24
  // The path of the files object in the ShareDB document.
23
- // Defaults to the root of the document.
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
- // `getOrCreateEditor` gets called
34
- // every time the active file changes.
35
- const editor = getOrCreateEditor({
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
- // TODO refactor this, make dynamic themes work
42
- theme: themesByLabel[theme],
49
+ theme,
43
50
  onInteract,
51
+ editorCache,
44
52
  });
45
- ref.current.appendChild(editor.dom);
53
+
54
+ // Add the editor to the DOM.
55
+ ref.current.appendChild(editorCacheValue.editor.dom);
46
56
 
47
57
  return () => {
48
- ref.current.removeChild(editor.dom);
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
 
@@ -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 = ({ show, onClose, setTheme }) => {
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 options={themes} onChange={handleChange} />
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>
@@ -3,7 +3,7 @@ import * as ReactDOM from 'react-dom/client';
3
3
  import App from './App';
4
4
 
5
5
  ReactDOM.createRoot(document.getElementById('root')).render(
6
- <React.StrictMode>
7
- <App />
8
- </React.StrictMode>,
6
+ // <React.StrictMode>
7
+ <App />,
8
+ // </React.StrictMode>,
9
9
  );
@@ -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 const themes = [
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: 'darcula' },
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 themesByLabel = themes.reduce((acc, theme) => {
26
- acc[theme.label] = theme.value;
27
- return acc;
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