vzcode 1.12.0 → 1.14.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
@@ -20,8 +20,8 @@
20
20
  href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
21
21
  rel="stylesheet"
22
22
  />
23
- <script type="module" crossorigin src="/assets/index-4cYZN5Uf.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-BKRpkJER.css">
23
+ <script type="module" crossorigin src="/assets/index-DGNI0PAo.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-Wxh-Jaj6.css">
25
25
  </head>
26
26
  <body>
27
27
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -111,7 +111,7 @@
111
111
  "@codemirror/lang-markdown": "^6.2.5",
112
112
  "@codemirror/state": "^6.4.1",
113
113
  "@codemirror/theme-one-dark": "^6.1.2",
114
- "@codemirror/view": "^6.28.6",
114
+ "@codemirror/view": "^6.29.1",
115
115
  "@lezer/highlight": "^1.2.0",
116
116
  "@replit/codemirror-indentation-markers": "^6.5.3",
117
117
  "@replit/codemirror-interact": "^6.3.1",
@@ -140,14 +140,14 @@
140
140
  "json0-ot-diff": "^1.1.2",
141
141
  "ngrok": "^4.3.3",
142
142
  "open": "^10.1.0",
143
- "openai": "^4.52.7",
143
+ "openai": "^4.53.2",
144
144
  "prettier-plugin-svelte": "^3.2.6",
145
145
  "react": "^18.3.1",
146
146
  "react-bootstrap": "^2.10.4",
147
147
  "react-dom": "^18.3.1",
148
- "react-router-dom": "^6.25.1",
149
- "sharedb": "^5.0.2",
150
- "sharedb-client-browser": "^5.0.2",
148
+ "react-router-dom": "^6.26.0",
149
+ "sharedb": "^5.0.3",
150
+ "sharedb-client-browser": "^5.0.3",
151
151
  "vizhub-ui": "^3.23.0",
152
152
  "ws": "^8.18.0"
153
153
  },
@@ -159,12 +159,12 @@
159
159
  "cross-env": "^7.0.3",
160
160
  "prettier": "^3.3.3",
161
161
  "sass": "^1.77.8",
162
- "typescript": "^5.5.3",
163
- "vite": "^5.3.4",
164
- "vitest": "^2.0.3"
162
+ "typescript": "^5.5.4",
163
+ "vite": "^5.3.5",
164
+ "vitest": "^2.0.5"
165
165
  },
166
166
  "optionalDependencies": {
167
- "@rollup/rollup-darwin-arm64": "^4.18.1",
168
- "@rollup/rollup-win32-x64-msvc": "^4.18.1"
167
+ "@rollup/rollup-darwin-arm64": "^4.19.2",
168
+ "@rollup/rollup-win32-x64-msvc": "^4.19.2"
169
169
  }
170
170
  }
@@ -8,6 +8,7 @@ import {
8
8
  import { SparklesSVG } from '../../Icons';
9
9
  import { startAIAssist } from '../startAIAssist';
10
10
  import { Spinner } from '../Spinner';
11
+ import { editorCacheKey } from '../../useEditorCache';
11
12
  import './style.scss';
12
13
 
13
14
  const enableStopGeneration = false;
@@ -23,8 +24,15 @@ export const AIAssistWidget = ({
23
24
  aiAssistTooltipText?: string;
24
25
  aiAssistClickOverride?: () => void;
25
26
  }) => {
26
- const { shareDBDoc, activeFileId, tabList, editorCache } =
27
- useContext(VZCodeContext);
27
+ const {
28
+ shareDBDoc,
29
+ activeFileId,
30
+ activePaneId,
31
+ tabList,
32
+ editorCache,
33
+ runPrettierRef,
34
+ runCodeRef,
35
+ } = useContext(VZCodeContext);
28
36
 
29
37
  // The stream ID of the most recent request.
30
38
  // * If `null`, no request has been made yet.
@@ -33,9 +41,6 @@ export const AIAssistWidget = ({
33
41
  const [aiStreamId, setAiStreamId] =
34
42
  useState<RequestId | null>(null);
35
43
 
36
- const { runPrettierRef, runCodeRef } =
37
- useContext(VZCodeContext);
38
-
39
44
  const handleClick = useCallback(async () => {
40
45
  const isCurrentlyGenerationg = aiStreamId !== null;
41
46
  if (isCurrentlyGenerationg) {
@@ -46,7 +51,9 @@ export const AIAssistWidget = ({
46
51
 
47
52
  // Wait for generation to finish.
48
53
  await startAIAssist({
49
- view: editorCache.get(activeFileId).editor,
54
+ view: editorCache.get(
55
+ editorCacheKey(activeFileId, activePaneId),
56
+ ).editor,
50
57
  shareDBDoc,
51
58
  fileId: activeFileId,
52
59
  tabList,
@@ -89,7 +96,7 @@ export const AIAssistWidget = ({
89
96
  : currentAIStreamId,
90
97
  );
91
98
  }
92
- }, [activeFileId, aiStreamId]);
99
+ }, [activeFileId, activePaneId, aiStreamId]);
93
100
 
94
101
  const showWidget = enableStopGeneration
95
102
  ? true
@@ -18,6 +18,7 @@ import { Diagnostic, linter } from '@codemirror/lint';
18
18
  import { json1Presence, textUnicode } from '../../ot';
19
19
  import {
20
20
  FileId,
21
+ PaneId,
21
22
  ShareDBDoc,
22
23
  TabState,
23
24
  Username,
@@ -32,6 +33,7 @@ import {
32
33
  } from './InteractiveWidgets';
33
34
  import {
34
35
  EditorCache,
36
+ editorCacheKey,
35
37
  EditorCacheValue,
36
38
  } from '../useEditorCache';
37
39
  import { ThemeLabel, themeOptionsByLabel } from '../themes';
@@ -102,6 +104,7 @@ const getAtPath = (obj, path) => {
102
104
  // Gets or creates an `editorCache` entry for the given file id.
103
105
  // Looks in `editorCache` first, and if not found, creates a new editor.
104
106
  export const getOrCreateEditor = ({
107
+ paneId = 'root',
105
108
  fileId,
106
109
  shareDBDoc,
107
110
  content,
@@ -118,6 +121,8 @@ export const getOrCreateEditor = ({
118
121
  enableAutoFollowRef,
119
122
  openTab,
120
123
  }: {
124
+ // TODO pass this in from the outside
125
+ paneId?: PaneId;
121
126
  fileId: FileId;
122
127
 
123
128
  // The ShareDB document that contains the file.
@@ -153,8 +158,11 @@ export const getOrCreateEditor = ({
153
158
  openTab: (tabState: TabState) => void;
154
159
  }): EditorCacheValue => {
155
160
  // Cache hit
156
- if (editorCache.has(fileId)) {
157
- return editorCache.get(fileId);
161
+
162
+ const cacheKey = editorCacheKey(fileId, paneId);
163
+
164
+ if (editorCache.has(cacheKey)) {
165
+ return editorCache.get(cacheKey);
158
166
  }
159
167
 
160
168
  // Cache miss
@@ -385,7 +393,7 @@ export const getOrCreateEditor = ({
385
393
  const editorCacheValue = { editor, themeCompartment };
386
394
 
387
395
  // Populate the cache.
388
- editorCache.set(fileId, editorCacheValue);
396
+ editorCache.set(cacheKey, editorCacheValue);
389
397
 
390
398
  return editorCacheValue;
391
399
  };
@@ -99,6 +99,7 @@ export const CodeEditor = ({
99
99
  allowGlobals,
100
100
  enableAutoFollowRef,
101
101
  openTab,
102
+ // splitPaneView,
102
103
  }),
103
104
  [
104
105
  activeFileId,
@@ -121,6 +122,7 @@ export const CodeEditor = ({
121
122
  // Guard against cases where page is still loading.
122
123
  if (!codeEditorRef.current) return;
123
124
  if (!content) return;
125
+ // if (openSplitPane) return;
124
126
 
125
127
  // Add the editor and apply the prior scroll position to the DOM.
126
128
  codeEditorRef.current.appendChild(
@@ -0,0 +1,34 @@
1
+ export const SplitEditorSVG = () => {
2
+ return (
3
+ <svg
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width="24"
6
+ height="24"
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ >
10
+ <rect
11
+ x="5"
12
+ y="4"
13
+ width="7.5"
14
+ height="16"
15
+ rx="1"
16
+ ry="1"
17
+ stroke="currentColor"
18
+ strokeWidth="1.5"
19
+ fill="none"
20
+ />
21
+ <rect
22
+ x="12"
23
+ y="4"
24
+ width="7.5"
25
+ height="16"
26
+ rx="1"
27
+ ry="1"
28
+ stroke="currentColor"
29
+ strokeWidth="1.5"
30
+ fill="none"
31
+ />
32
+ </svg>
33
+ );
34
+ };
@@ -1,9 +1,14 @@
1
1
  import { useCallback, useContext, useState } from 'react';
2
2
  import { PlaySVG } from '../Icons';
3
+ import { SplitEditorSVG } from '../Icons/SplitEditorSVG';
3
4
  import { VZCodeContext } from '../VZCodeContext';
4
5
  import { OverlayTrigger, Tooltip } from '../bootstrap';
6
+
5
7
  import './style.scss';
6
8
 
9
+ // Feature flag for split pane feature (WIP)
10
+ const enableSplitPane = false;
11
+
7
12
  export const RunCodeWidget = ({
8
13
  runCodeWidgetTooltipText = (
9
14
  <>
@@ -14,7 +19,7 @@ export const RunCodeWidget = ({
14
19
  }: {
15
20
  runCodeWidgetTooltipText?: JSX.Element;
16
21
  }) => {
17
- const { runCodeRef, runPrettierRef } =
22
+ const { runCodeRef, runPrettierRef, splitCurrentPane } =
18
23
  useContext(VZCodeContext);
19
24
  const [isRunning, setIsRunning] = useState(false);
20
25
 
@@ -37,23 +42,50 @@ export const RunCodeWidget = ({
37
42
  setTimeout(() => setIsRunning(false), 1000);
38
43
  }, []);
39
44
 
45
+ const handleSplitEditor = useCallback(() => {
46
+ console.log('Split Editor');
47
+ console.log(splitCurrentPane);
48
+ splitCurrentPane();
49
+ }, [splitCurrentPane]);
50
+
40
51
  return (
41
- <div className="vz-code-run-code-widget">
42
- <OverlayTrigger
43
- placement="left"
44
- overlay={
45
- <Tooltip id="run-code-widget-tooltip">
46
- {runCodeWidgetTooltipText}
47
- </Tooltip>
48
- }
49
- >
50
- <i
51
- className={`icon-button icon-button-dark ${isRunning ? 'rotate-icon' : ''}`}
52
- onClick={handleClick}
52
+ <div>
53
+ <div className="vz-code-split-view-widget">
54
+ {enableSplitPane && (
55
+ <OverlayTrigger
56
+ placement="bottom"
57
+ overlay={
58
+ <Tooltip id="fork-file-tooltip">
59
+ Split Editor
60
+ </Tooltip>
61
+ }
62
+ >
63
+ <i
64
+ onClick={handleSplitEditor}
65
+ className="icon-button icon-button-dark"
66
+ >
67
+ <SplitEditorSVG />
68
+ </i>
69
+ </OverlayTrigger>
70
+ )}
71
+ </div>
72
+ <div className="vz-code-run-code-widget">
73
+ <OverlayTrigger
74
+ placement="bottom"
75
+ overlay={
76
+ <Tooltip id="run-code-widget-tooltip">
77
+ {runCodeWidgetTooltipText}
78
+ </Tooltip>
79
+ }
53
80
  >
54
- <PlaySVG />
55
- </i>
56
- </OverlayTrigger>
81
+ <i
82
+ className={`icon-button icon-button-dark ${isRunning ? 'rotate-icon' : ''}`}
83
+ onClick={handleClick}
84
+ >
85
+ <PlaySVG />
86
+ </i>
87
+ </OverlayTrigger>
88
+ </div>
57
89
  </div>
58
90
  );
59
91
  };
@@ -3,11 +3,16 @@
3
3
  right: 16px;
4
4
  top: 9px;
5
5
  color: var(--vh-color-neutral-04);
6
-
7
6
  .rotate-icon {
8
7
  animation: spin 1s linear;
9
8
  }
10
9
  }
10
+ .vz-code-split-view-widget {
11
+ position: absolute;
12
+ right: 51px;
13
+ top: 9px;
14
+ color: var(--vh-color-neutral-04);
15
+ }
11
16
 
12
17
  @keyframes spin {
13
18
  from {
@@ -15,6 +15,9 @@ import {
15
15
  SearchResults,
16
16
  SearchFileVisibility,
17
17
  TabState,
18
+ PresenceIndicator,
19
+ Pane,
20
+ PaneId,
18
21
  } from '../types';
19
22
  import { usePrettier } from './usePrettier';
20
23
  import { useTypeScript } from './useTypeScript';
@@ -74,6 +77,8 @@ export type VZCodeContextValue = {
74
77
  setActiveFileLeft: () => void;
75
78
  setActiveFileRight: () => void;
76
79
 
80
+ activePaneId: PaneId;
81
+
77
82
  tabList: Array<TabState>;
78
83
  openTab: (tabState: TabState) => void;
79
84
  closeTabs: (fileIds: string[]) => void;
@@ -146,6 +151,13 @@ export type VZCodeContextValue = {
146
151
 
147
152
  enableAutoFollow: boolean;
148
153
  toggleAutoFollow: () => void;
154
+
155
+ updatePresenceIndicator: (
156
+ presenceIndicator: PresenceIndicator,
157
+ ) => void;
158
+
159
+ sidebarPresenceIndicators: Array<PresenceIndicator>;
160
+ splitCurrentPane: () => void;
149
161
  };
150
162
 
151
163
  export const VZCodeProvider = ({
@@ -223,8 +235,11 @@ export const VZCodeProvider = ({
223
235
  );
224
236
 
225
237
  // Unpack state.
238
+ // print the state object to see what it contains
239
+ // console.log('state: ', state);
226
240
  const {
227
241
  pane,
242
+ activePaneId,
228
243
  theme,
229
244
  search,
230
245
  isSearchOpen,
@@ -233,6 +248,7 @@ export const VZCodeProvider = ({
233
248
  editorWantsFocus,
234
249
  username,
235
250
  enableAutoFollow,
251
+ sidebarPresenceIndicators,
236
252
  } = state;
237
253
 
238
254
  // TODO support splitPane type
@@ -262,6 +278,8 @@ export const VZCodeProvider = ({
262
278
  editorNoLongerWantsFocus,
263
279
  setUsername,
264
280
  toggleAutoFollow,
281
+ updatePresenceIndicator,
282
+ splitCurrentPane,
265
283
  } = useActions(dispatch);
266
284
 
267
285
  // Sync tab state to the URL.
@@ -382,6 +400,8 @@ export const VZCodeProvider = ({
382
400
  setActiveFileLeft,
383
401
  setActiveFileRight,
384
402
 
403
+ activePaneId,
404
+
385
405
  tabList,
386
406
  openTab,
387
407
  closeTabs,
@@ -439,6 +459,9 @@ export const VZCodeProvider = ({
439
459
 
440
460
  enableAutoFollow,
441
461
  toggleAutoFollow,
462
+ updatePresenceIndicator,
463
+ sidebarPresenceIndicators,
464
+ splitCurrentPane,
442
465
  };
443
466
 
444
467
  return (
@@ -23,6 +23,7 @@ export const DirectoryListing = ({
23
23
  deleteDirectory,
24
24
  isDirectoryOpen,
25
25
  toggleDirectory,
26
+ sidebarPresenceIndicators,
26
27
  } = useContext(VZCodeContext);
27
28
 
28
29
  const handleClick = useCallback(() => {
@@ -63,7 +64,7 @@ export const DirectoryListing = ({
63
64
  >
64
65
  <DirectoryArrowSVG />
65
66
  </div>
66
- {name}
67
+ <div className="name">{name}</div>
67
68
  </Item>
68
69
  {children && isOpen ? (
69
70
  <div className="indentation">
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useContext } from 'react';
2
2
  import { Item } from './Item';
3
- import { FileId } from '../../types';
3
+ import { FileId, PresenceIndicator } from '../../types';
4
4
  import {
5
5
  FileSVG,
6
6
  JavaScriptSVG,
@@ -46,12 +46,14 @@ export const FileListing = ({
46
46
  handleFileClick,
47
47
  handleFileDoubleClick,
48
48
  isActive,
49
+ presence,
49
50
  }: {
50
51
  name: string;
51
52
  fileId: FileId;
52
53
  handleFileClick: (fileId: FileId) => void;
53
54
  handleFileDoubleClick: (fileId: FileId) => void;
54
55
  isActive: boolean;
56
+ presence: PresenceIndicator[];
55
57
  }) => {
56
58
  const { renameFile, deleteFile } =
57
59
  useContext(VZCodeContext);
@@ -85,8 +87,26 @@ export const FileListing = ({
85
87
  handleRenameClick={handleRenameClick}
86
88
  isActive={isActive}
87
89
  >
88
- <i className="file-icon">{getExtensionIcon(name)}</i>
89
- {name}
90
+ <div className="name">
91
+ {/* Render presence indicators to the left of the file name */}
92
+ {presence.length > 0 && (
93
+ <div className="presence-indicators">
94
+ {presence.map((indicator, index) => (
95
+ <div
96
+ key={index}
97
+ className="presence-indicator"
98
+ >
99
+ {indicator.username[0]}{' '}
100
+ {/* Display the first letter of the username */}
101
+ </div>
102
+ ))}
103
+ </div>
104
+ )}
105
+ <i className="file-icon">
106
+ {getExtensionIcon(name)}
107
+ </i>
108
+ {name}
109
+ </div>
90
110
  </Item>
91
111
  );
92
112
  };
@@ -13,9 +13,9 @@ import { DeleteConfirmationModal } from './DeleteConfirmationModal';
13
13
  import { VZCodeContext } from '../VZCodeContext';
14
14
  import { ItemId } from '../../types';
15
15
  import { getExtensionIcon } from './FileListing';
16
+ import { SplitEditorSVG } from '../Icons/SplitEditorSVG';
17
+ import { SplitPaneResizeProvider } from '../SplitPaneResizeContext';
16
18
 
17
- // TODO support renaming directories
18
- // See https://github.com/vizhub-core/vzcode/issues/103
19
19
  const enableRenameDirectory = true;
20
20
 
21
21
  // A file or directory in the sidebar.
@@ -19,10 +19,16 @@ export const Listing = ({
19
19
  handleFileClick: (fileId: FileId) => void;
20
20
  handleFileDoubleClick: (fileId: FileId) => void;
21
21
  }) => {
22
- const { activeFileId } = useContext(VZCodeContext);
22
+ const { activeFileId, sidebarPresenceIndicators } =
23
+ useContext(VZCodeContext);
23
24
  const { name, file, fileId } = entity as FileTreeFile;
24
25
  const { path, children } = entity as FileTree;
25
26
 
27
+ const presence =
28
+ sidebarPresenceIndicators?.filter(
29
+ (indicator) => indicator.fileId === fileId,
30
+ ) || [];
31
+
26
32
  return file ? (
27
33
  <FileListing
28
34
  fileId={fileId}
@@ -30,6 +36,7 @@ export const Listing = ({
30
36
  handleFileClick={handleFileClick}
31
37
  handleFileDoubleClick={handleFileDoubleClick}
32
38
  isActive={fileId === activeFileId}
39
+ presence={presence}
33
40
  />
34
41
  ) : (
35
42
  <DirectoryListing
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  useCallback,
3
3
  useContext,
4
+ useEffect,
4
5
  useMemo,
5
6
  useState,
6
7
  } from 'react';
@@ -8,6 +9,9 @@ import {
8
9
  FileId,
9
10
  FileTree,
10
11
  FileTreeFile,
12
+ Presence,
13
+ PresenceId,
14
+ PresenceIndicator,
11
15
  } from '../../types';
12
16
  import { Tooltip, OverlayTrigger } from '../bootstrap';
13
17
  import { Search } from './Search';
@@ -117,6 +121,9 @@ export const VZSidebar = ({
117
121
  sidebarRef,
118
122
  enableAutoFollow,
119
123
  toggleAutoFollow,
124
+ docPresence,
125
+ updatePresenceIndicator,
126
+ sidebarPresenceIndicators,
120
127
  } = useContext(VZCodeContext);
121
128
 
122
129
  const fileTree = useMemo(
@@ -164,6 +171,42 @@ export const VZSidebar = ({
164
171
  handleDrop,
165
172
  } = useDragAndDrop();
166
173
 
174
+ // Track presence of remote users across files
175
+ // so that they can be displayed in the sidebar.
176
+ useEffect(() => {
177
+ docPresence;
178
+ }, []);
179
+
180
+ // Track the presence indicators for display in sidebar
181
+ useEffect(() => {
182
+ if (docPresence) {
183
+ const handleReceive = (
184
+ presenceId: PresenceId,
185
+ update: Presence,
186
+ ) => {
187
+ const presenceIndicator: PresenceIndicator = {
188
+ username: update.username,
189
+ fileId: update.start[1] as FileId,
190
+ };
191
+
192
+ // console.log('Got presence!');
193
+ // // console.log({presenceId,update})
194
+ // console.log(
195
+ // JSON.stringify(presenceIndicator, null, 2),
196
+ // );
197
+
198
+ updatePresenceIndicator(presenceIndicator);
199
+ };
200
+
201
+ docPresence.on('receive', handleReceive);
202
+ return () => {
203
+ docPresence.off('receive', handleReceive);
204
+ };
205
+ }
206
+ }, [docPresence]);
207
+
208
+ // console.log(sidebarPresenceIndicators);
209
+
167
210
  return (
168
211
  <div
169
212
  className="vz-sidebar"
@@ -253,4 +253,29 @@
253
253
  }
254
254
  }
255
255
  }
256
+ .name {
257
+ display: flex;
258
+ align-items: center;
259
+ white-space: nowrap;
260
+ overflow: hidden;
261
+ gap: 6px;
262
+ }
263
+
264
+ .presence-indicators {
265
+ display: flex;
266
+ gap: 4px;
267
+ margin-right: 6px; /* Space between the indicators and the file name */
268
+ }
269
+
270
+ .presence-indicator {
271
+ width: 16px;
272
+ height: 16px;
273
+ border-radius: 50%;
274
+ display: flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ font-size: 10px;
278
+ background-color: #ff5733;
279
+ color: #ffffff;
280
+ }
256
281
  }
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
2
2
  import { ThemeLabel } from './themes';
3
3
  import {
4
4
  FileId,
5
+ PresenceIndicator,
5
6
  SearchFileVisibility,
6
7
  ShareDBDoc,
7
8
  TabState,
@@ -172,6 +173,22 @@ export const useActions = (
172
173
  });
173
174
  }, [dispatch]);
174
175
 
176
+ const updatePresenceIndicator = useCallback(
177
+ (presenceIndicator: PresenceIndicator) => {
178
+ dispatch({
179
+ type: 'update_presence_indicator',
180
+ presenceIndicator,
181
+ });
182
+ },
183
+ [dispatch],
184
+ );
185
+
186
+ const splitCurrentPane = useCallback(() => {
187
+ dispatch({
188
+ type: 'split_current_pane',
189
+ });
190
+ }, [dispatch]);
191
+
175
192
  return {
176
193
  setActiveFileId,
177
194
  setActiveFileLeft,
@@ -191,5 +208,7 @@ export const useActions = (
191
208
  editorNoLongerWantsFocus,
192
209
  setUsername,
193
210
  toggleAutoFollow,
211
+ updatePresenceIndicator,
212
+ splitCurrentPane,
194
213
  };
195
214
  };
@@ -1,5 +1,5 @@
1
1
  import { useRef } from 'react';
2
- import { FileId } from '../types';
2
+ import { FileId, PaneId } from '../types';
3
3
  import { EditorView } from 'codemirror';
4
4
 
5
5
  export type EditorCacheValue = {
@@ -8,7 +8,17 @@ export type EditorCacheValue = {
8
8
  scrollPosition?: number;
9
9
  };
10
10
 
11
- export type EditorCache = Map<FileId, EditorCacheValue>;
11
+ export type EditorCacheKey = string;
12
+
13
+ export const editorCacheKey = (
14
+ fileId: FileId,
15
+ paneId: PaneId,
16
+ ): EditorCacheKey => fileId + '|' + paneId;
17
+
18
+ export type EditorCache = Map<
19
+ EditorCacheKey,
20
+ EditorCacheValue
21
+ >;
12
22
 
13
23
  export const useEditorCache = () => {
14
24
  // Singleton cache of CodeMirror instances