vzcode 0.51.0 → 0.53.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/assets/{index-jYtAJn79.css → index-7PozlhVS.css} +1 -1
- package/dist/assets/index-wDsdDSEj.js +147 -0
- package/dist/assets/worker-y75r0vyN.js +232 -0
- package/dist/index.html +2 -2
- package/package.json +14 -13
- package/src/client/CodeEditor/getOrCreateEditor.ts +5 -3
- package/src/client/TabList/Tab.tsx +2 -2
- package/src/client/VZCodeContext.tsx +11 -19
- package/src/client/tabsSearchParameters.test.ts +54 -78
- package/src/client/tabsSearchParameters.ts +121 -60
- package/src/client/useURLSync.ts +134 -0
- package/src/client/vzReducer/createInitialState.ts +4 -11
- package/src/client/vzReducer/index.ts +3 -3
- package/dist/assets/index-yk4vJ5U-.js +0 -147
- package/dist/assets/worker-fboacSft.js +0 -234
package/dist/index.html
CHANGED
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css"
|
|
25
25
|
rel="stylesheet"
|
|
26
26
|
/>
|
|
27
|
-
<script type="module" crossorigin src="/assets/index-
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
27
|
+
<script type="module" crossorigin src="/assets/index-wDsdDSEj.js"></script>
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/index-7PozlhVS.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -63,18 +63,19 @@
|
|
|
63
63
|
},
|
|
64
64
|
"homepage": "https://github.com/vizhub-core/vzcode#readme",
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@codemirror/autocomplete": "^6.
|
|
66
|
+
"@codemirror/autocomplete": "^6.12.0",
|
|
67
67
|
"@codemirror/lang-css": "^6.2.1",
|
|
68
68
|
"@codemirror/lang-html": "^6.4.7",
|
|
69
69
|
"@codemirror/lang-javascript": "^6.2.1",
|
|
70
70
|
"@codemirror/lang-json": "^6.0.1",
|
|
71
|
-
"@codemirror/lang-markdown": "^6.2.
|
|
71
|
+
"@codemirror/lang-markdown": "^6.2.4",
|
|
72
72
|
"@codemirror/state": "^6.4.0",
|
|
73
73
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
74
74
|
"@codemirror/view": "^6.23.0",
|
|
75
75
|
"@lezer/highlight": "^1.2.0",
|
|
76
76
|
"@replit/codemirror-indentation-markers": "^6.5.0",
|
|
77
77
|
"@replit/codemirror-interact": "^6.3.0",
|
|
78
|
+
"@replit/codemirror-lang-svelte": "^6.0.0",
|
|
78
79
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
|
79
80
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
80
81
|
"@typescript/vfs": "^1.5.0",
|
|
@@ -93,30 +94,30 @@
|
|
|
93
94
|
"color-hash": "^2.0.2",
|
|
94
95
|
"d3-array": "^3.2.4",
|
|
95
96
|
"diff-match-patch": "^1.0.5",
|
|
96
|
-
"dotenv": "^16.3.
|
|
97
|
+
"dotenv": "^16.3.2",
|
|
97
98
|
"express": "^4.18.2",
|
|
98
99
|
"ignore": "^5.3.0",
|
|
99
100
|
"json0-ot-diff": "^1.1.2",
|
|
100
101
|
"ngrok": "^4.3.3",
|
|
101
102
|
"open": "^10.0.3",
|
|
102
|
-
"openai": "^4.
|
|
103
|
+
"openai": "^4.25.0",
|
|
103
104
|
"react": "^18.2.0",
|
|
104
|
-
"react-bootstrap": "^2.
|
|
105
|
+
"react-bootstrap": "^2.10.0",
|
|
105
106
|
"react-dom": "^18.2.0",
|
|
106
|
-
"react-router-dom": "^6.21.
|
|
107
|
+
"react-router-dom": "^6.21.3",
|
|
107
108
|
"sharedb": "^4.1.2",
|
|
108
109
|
"sharedb-client-browser": "^4.4.0",
|
|
109
|
-
"vizhub-ui": "^3.
|
|
110
|
+
"vizhub-ui": "^3.18.0",
|
|
110
111
|
"ws": "^8.16.0"
|
|
111
112
|
},
|
|
112
113
|
"devDependencies": {
|
|
113
|
-
"@types/react": "^18.2.
|
|
114
|
+
"@types/react": "^18.2.48",
|
|
114
115
|
"@types/react-dom": "^18.2.18",
|
|
115
116
|
"@vitejs/plugin-react": "^4.2.1",
|
|
116
|
-
"prettier": "^3.
|
|
117
|
-
"sass": "^1.
|
|
117
|
+
"prettier": "^3.2.4",
|
|
118
|
+
"sass": "^1.70.0",
|
|
118
119
|
"typescript": "^5.3.3",
|
|
119
|
-
"vite": "^5.0.
|
|
120
|
-
"vitest": "^1.1
|
|
120
|
+
"vite": "^5.0.12",
|
|
121
|
+
"vitest": "^1.2.1"
|
|
121
122
|
}
|
|
122
123
|
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
EditorState,
|
|
5
5
|
} from '@codemirror/state';
|
|
6
6
|
import { javascript } from '@codemirror/lang-javascript';
|
|
7
|
+
import { svelte } from '@replit/codemirror-lang-svelte';
|
|
7
8
|
import { json } from '@codemirror/lang-json';
|
|
8
9
|
import { markdown } from '@codemirror/lang-markdown';
|
|
9
10
|
import { html } from '@codemirror/lang-html';
|
|
@@ -60,6 +61,7 @@ const languageExtensions = {
|
|
|
60
61
|
html,
|
|
61
62
|
css,
|
|
62
63
|
md: markdown,
|
|
64
|
+
svelte,
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
// Gets a value at a path in an object.
|
|
@@ -195,9 +197,9 @@ export const getOrCreateEditor = ({
|
|
|
195
197
|
// TODO manually test this case by creating a file
|
|
196
198
|
// that has no extension, opening it up,
|
|
197
199
|
// and then adding an extension.
|
|
198
|
-
console.warn(
|
|
199
|
-
|
|
200
|
-
);
|
|
200
|
+
// console.warn(
|
|
201
|
+
// `No language extension for file extension: ${fileExtension}`,
|
|
202
|
+
// );
|
|
201
203
|
// We still need to push the compartment,
|
|
202
204
|
// otherwise the compartment won't work when
|
|
203
205
|
// a file extension _is_ added later on.
|
|
@@ -21,7 +21,7 @@ export const Tab = ({
|
|
|
21
21
|
fileName,
|
|
22
22
|
}: {
|
|
23
23
|
fileId: FileId;
|
|
24
|
-
isTransient
|
|
24
|
+
isTransient?: boolean;
|
|
25
25
|
isActive: boolean;
|
|
26
26
|
setActiveFileId: (fileId: FileId) => void;
|
|
27
27
|
openTab: (tabState: TabState) => void;
|
|
@@ -49,7 +49,7 @@ export const Tab = ({
|
|
|
49
49
|
|
|
50
50
|
// Double-clicking a transient tab makes it a persistent tab.
|
|
51
51
|
const handleDoubleClick = useCallback(() => {
|
|
52
|
-
openTab({ fileId
|
|
52
|
+
openTab({ fileId });
|
|
53
53
|
}, [fileId, openTab]);
|
|
54
54
|
|
|
55
55
|
return (
|
|
@@ -24,10 +24,7 @@ import {
|
|
|
24
24
|
EditorCache,
|
|
25
25
|
useEditorCache,
|
|
26
26
|
} from './useEditorCache';
|
|
27
|
-
import {
|
|
28
|
-
useInitialTabs,
|
|
29
|
-
usePersistTabs,
|
|
30
|
-
} from './tabsSearchParameters';
|
|
27
|
+
import { useURLSync } from './useURLSync';
|
|
31
28
|
|
|
32
29
|
// This context centralizes all the "smart" logic
|
|
33
30
|
// to do with the application state. This includes
|
|
@@ -146,22 +143,11 @@ export const VZCodeProvider = ({
|
|
|
146
143
|
typeScriptWorker,
|
|
147
144
|
});
|
|
148
145
|
|
|
149
|
-
// Get the initial open tabs from the URL
|
|
150
|
-
const {
|
|
151
|
-
tabList: initialTabList,
|
|
152
|
-
activeFileId: initialActiveFileId,
|
|
153
|
-
} = useInitialTabs();
|
|
154
|
-
|
|
155
146
|
// Set up the reducer that manages much of the application state.
|
|
156
147
|
// See https://react.dev/reference/react/useReducer
|
|
157
148
|
const [state, dispatch] = useReducer(
|
|
158
149
|
vzReducer,
|
|
159
|
-
{
|
|
160
|
-
defaultTheme,
|
|
161
|
-
initialUsername,
|
|
162
|
-
initialTabList,
|
|
163
|
-
initialActiveFileId,
|
|
164
|
-
},
|
|
150
|
+
{ defaultTheme, initialUsername },
|
|
165
151
|
createInitialState,
|
|
166
152
|
);
|
|
167
153
|
|
|
@@ -175,9 +161,6 @@ export const VZCodeProvider = ({
|
|
|
175
161
|
username,
|
|
176
162
|
} = state;
|
|
177
163
|
|
|
178
|
-
// Sync tab state to the URL
|
|
179
|
-
usePersistTabs({ activeFileId, tabList });
|
|
180
|
-
|
|
181
164
|
// Functions for dispatching actions to the reducer.
|
|
182
165
|
const {
|
|
183
166
|
setActiveFileId,
|
|
@@ -190,6 +173,15 @@ export const VZCodeProvider = ({
|
|
|
190
173
|
setUsername,
|
|
191
174
|
} = useActions(dispatch);
|
|
192
175
|
|
|
176
|
+
// Sync tab state to the URL.
|
|
177
|
+
useURLSync({
|
|
178
|
+
content,
|
|
179
|
+
openTab,
|
|
180
|
+
setActiveFileId,
|
|
181
|
+
tabList,
|
|
182
|
+
activeFileId,
|
|
183
|
+
});
|
|
184
|
+
|
|
193
185
|
// The set of open directories.
|
|
194
186
|
// TODO move this into reducer/useActions
|
|
195
187
|
const { isDirectoryOpen, toggleDirectory } =
|
|
@@ -1,102 +1,78 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
encodeTabs,
|
|
4
4
|
decodeTabs,
|
|
5
|
-
|
|
5
|
+
delimiter,
|
|
6
6
|
} from './tabsSearchParameters';
|
|
7
|
+
import { VZCodeContent } from '../types';
|
|
7
8
|
|
|
8
9
|
describe('tabsSearchParameters', () => {
|
|
9
10
|
test.each([
|
|
10
11
|
{
|
|
11
12
|
name: 'empty',
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
tabStateParams: {},
|
|
14
|
+
tabList: [],
|
|
15
|
+
activeFileId: null,
|
|
14
16
|
},
|
|
15
17
|
{
|
|
16
|
-
name: '
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
tabList: [{ fileId: '123', isTransient: false }],
|
|
20
|
-
activeFileId: '123',
|
|
18
|
+
name: 'single active tab',
|
|
19
|
+
tabStateParams: {
|
|
20
|
+
file: 'index.html',
|
|
21
21
|
},
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: 'transient tab',
|
|
25
|
-
search: '456=ta',
|
|
26
|
-
state: {
|
|
27
|
-
tabList: [{ fileId: '456', isTransient: true }],
|
|
28
|
-
activeFileId: '456',
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'no active file',
|
|
33
|
-
search: '678=p',
|
|
34
|
-
state: {
|
|
35
|
-
tabList: [{ fileId: '678', isTransient: false }],
|
|
36
|
-
activeFileId: null,
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
name: 'three tabs',
|
|
41
|
-
search: '111=p&222=ta&333=p',
|
|
42
|
-
state: {
|
|
43
|
-
tabList: [
|
|
44
|
-
{ fileId: '111', isTransient: false },
|
|
45
|
-
{ fileId: '222', isTransient: true },
|
|
46
|
-
{ fileId: '333', isTransient: false },
|
|
47
|
-
],
|
|
48
|
-
activeFileId: '222',
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
])('round trip: $name', ({ search, state }) => {
|
|
52
|
-
const decoded = decodeTabs(new URLSearchParams(search));
|
|
53
|
-
expect(decoded).toStrictEqual(state);
|
|
54
|
-
const encoded = encodeTabs(state).toString();
|
|
55
|
-
expect(encoded).toBe(search);
|
|
56
|
-
});
|
|
57
22
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
name: 'empty value',
|
|
61
|
-
search: 'abc',
|
|
62
|
-
correctedSearch: 'abc=p',
|
|
63
|
-
state: {
|
|
64
|
-
tabList: [{ fileId: 'abc', isTransient: false }],
|
|
65
|
-
activeFileId: null,
|
|
66
|
-
},
|
|
23
|
+
tabList: [{ fileId: '789' }],
|
|
24
|
+
activeFileId: '789',
|
|
67
25
|
},
|
|
68
26
|
{
|
|
69
|
-
name: '
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
tabList: [{ fileId: '890', isTransient: true }],
|
|
74
|
-
activeFileId: '890',
|
|
27
|
+
name: 'multiple tabs, active tab first',
|
|
28
|
+
tabStateParams: {
|
|
29
|
+
file: 'index.html',
|
|
30
|
+
tabs: `index.html${delimiter}README.md${delimiter}index.js`,
|
|
75
31
|
},
|
|
32
|
+
tabList: [
|
|
33
|
+
{ fileId: '789' },
|
|
34
|
+
{ fileId: '456' },
|
|
35
|
+
{ fileId: '123' },
|
|
36
|
+
],
|
|
37
|
+
activeFileId: '789',
|
|
76
38
|
},
|
|
77
39
|
{
|
|
78
|
-
name: '
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
tabList: [{ fileId: '321', isTransient: false }],
|
|
83
|
-
activeFileId: '321',
|
|
40
|
+
name: 'multiple tabs, active tab in middle',
|
|
41
|
+
tabStateParams: {
|
|
42
|
+
file: 'README.md',
|
|
43
|
+
tabs: `index.html${delimiter}README.md${delimiter}index.js`,
|
|
84
44
|
},
|
|
45
|
+
tabList: [
|
|
46
|
+
{ fileId: '789' },
|
|
47
|
+
{ fileId: '456' },
|
|
48
|
+
{ fileId: '123' },
|
|
49
|
+
],
|
|
50
|
+
activeFileId: '456',
|
|
85
51
|
},
|
|
86
52
|
])(
|
|
87
|
-
'
|
|
88
|
-
({
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
53
|
+
'round trip: $name',
|
|
54
|
+
({ tabStateParams, tabList, activeFileId }) => {
|
|
55
|
+
// Fake Content object
|
|
56
|
+
const content: VZCodeContent = {
|
|
57
|
+
files: {
|
|
58
|
+
'123': { name: 'index.js', text: 'abc' },
|
|
59
|
+
'456': { name: 'README.md', text: 'def' },
|
|
60
|
+
'789': { name: 'index.html', text: 'ghi' },
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const decoded = decodeTabs({
|
|
65
|
+
tabStateParams,
|
|
66
|
+
content,
|
|
67
|
+
});
|
|
68
|
+
const expected = { tabList, activeFileId };
|
|
69
|
+
expect(decoded).toStrictEqual(expected);
|
|
70
|
+
const encoded = encodeTabs({
|
|
71
|
+
tabList,
|
|
72
|
+
activeFileId,
|
|
73
|
+
content,
|
|
74
|
+
});
|
|
75
|
+
expect(encoded).toEqual(tabStateParams);
|
|
100
76
|
},
|
|
101
77
|
);
|
|
102
78
|
});
|
|
@@ -1,76 +1,137 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
useNavigate,
|
|
4
|
-
useSearchParams,
|
|
5
|
-
} from 'react-router-dom';
|
|
6
|
-
import { FileId } from '../types';
|
|
1
|
+
import { FileId, VZCodeContent } from '../types';
|
|
7
2
|
import { TabState } from './vzReducer';
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
4
|
+
// The delimiter used to separate file names in the `tabs` parameter.
|
|
5
|
+
// We need a character that is both URL-safe (does not get escaped in URLs)
|
|
6
|
+
// and is not typically found in file names, especially for JavaScript and CSS files.
|
|
7
|
+
//
|
|
8
|
+
// The following characters are not URL-safe as they get converted to escaped sequences in URLs,
|
|
9
|
+
// leading to less readable URLs:
|
|
10
|
+
// - `+` becomes `%2B`
|
|
11
|
+
// - `|` becomes `%7C`
|
|
12
|
+
// - `;` becomes `%3B`
|
|
13
|
+
// - `:` becomes `%3A`
|
|
14
|
+
// - `,` becomes `%2C`
|
|
15
|
+
// - `!` becomes `%21`
|
|
16
|
+
// - `/` becomes `%2F`
|
|
17
|
+
//
|
|
18
|
+
// The following characters, while URL-safe, are commonly used in file names,
|
|
19
|
+
// hence using them could lead to conflicts or misinterpretation:
|
|
20
|
+
// - `.` commonly used as a dot separator in file names and extensions
|
|
21
|
+
// - ` ` (space) often found in file names but can lead to issues in URLs
|
|
22
|
+
// - `-` (hyphen) frequently used in file and folder names for readability
|
|
23
|
+
// - `_` (underscore) also commonly used in file and folder names
|
|
24
|
+
//
|
|
25
|
+
// Considering the above constraints, we need a character that is both URL-safe
|
|
26
|
+
// and not typically used in file names. The following character meets these criteria:
|
|
27
|
+
// - `~` is URL-safe (remains unescaped in URLs) and is not a standard character
|
|
28
|
+
// in file names for JavaScript and CSS files, making it a suitable choice for a delimiter.
|
|
29
|
+
export const delimiter = '~';
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
// Gets the file id of a file with the given name.
|
|
32
|
+
// Returns null if not found.
|
|
33
|
+
const getFileId = (
|
|
34
|
+
content: VZCodeContent,
|
|
35
|
+
fileName: string,
|
|
36
|
+
): string | null => {
|
|
37
|
+
if (content && content.files) {
|
|
38
|
+
for (const fileId of Object.keys(content.files)) {
|
|
39
|
+
const file = content.files[fileId];
|
|
40
|
+
if (file.name === fileName) {
|
|
41
|
+
return fileId;
|
|
42
|
+
}
|
|
28
43
|
}
|
|
29
|
-
searchParams.set(tab.fileId, tabState);
|
|
30
44
|
}
|
|
31
|
-
return
|
|
45
|
+
return null;
|
|
32
46
|
};
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
activeFileId = fileId;
|
|
48
|
+
// Gets the file name of a file with the given id,
|
|
49
|
+
// guard against failure cases.
|
|
50
|
+
// Returns null if not found.
|
|
51
|
+
const getFileName = (
|
|
52
|
+
content: VZCodeContent,
|
|
53
|
+
fileId: string,
|
|
54
|
+
): string | null => {
|
|
55
|
+
if (content && content.files) {
|
|
56
|
+
const file = content.files[fileId];
|
|
57
|
+
if (file) {
|
|
58
|
+
return file.name;
|
|
46
59
|
}
|
|
47
|
-
tabList.push({ fileId, isTransient });
|
|
48
60
|
}
|
|
49
|
-
return
|
|
61
|
+
return null;
|
|
50
62
|
};
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
// An object representing the search parameters.
|
|
65
|
+
export type TabStateParams = {
|
|
66
|
+
file?: string;
|
|
67
|
+
tabs?: string;
|
|
68
|
+
};
|
|
54
69
|
|
|
55
|
-
|
|
70
|
+
// Decodes the tab list and active file from the search parameters.
|
|
71
|
+
export const decodeTabs = ({
|
|
72
|
+
tabStateParams,
|
|
73
|
+
content,
|
|
74
|
+
}: {
|
|
75
|
+
tabStateParams: TabStateParams;
|
|
76
|
+
content: VZCodeContent;
|
|
77
|
+
}): {
|
|
78
|
+
tabList: Array<TabState>;
|
|
79
|
+
activeFileId: FileId;
|
|
80
|
+
} => {
|
|
81
|
+
const { file, tabs } = tabStateParams;
|
|
82
|
+
const tabList: TabState[] = [];
|
|
83
|
+
|
|
84
|
+
// Decode active file
|
|
85
|
+
const activeFileId = getFileId(content, file);
|
|
86
|
+
|
|
87
|
+
// Decode tab list
|
|
88
|
+
if (tabs) {
|
|
89
|
+
tabs.split(delimiter).forEach((fileName) => {
|
|
90
|
+
const fileId = getFileId(content, fileName);
|
|
91
|
+
if (fileId) {
|
|
92
|
+
tabList.push({ fileId });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
} else if (activeFileId) {
|
|
96
|
+
// If there's only an active file without a tab list
|
|
97
|
+
tabList.push({ fileId: activeFileId });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { tabList, activeFileId };
|
|
56
101
|
};
|
|
57
102
|
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
// Encodes the tab list and active file into the search parameters.
|
|
104
|
+
export const encodeTabs = ({
|
|
60
105
|
tabList,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
activeFileId,
|
|
107
|
+
content,
|
|
108
|
+
}: {
|
|
109
|
+
tabList: Array<TabState>;
|
|
110
|
+
activeFileId: FileId | null;
|
|
111
|
+
content: VZCodeContent;
|
|
112
|
+
}): TabStateParams => {
|
|
113
|
+
// Get the file name of the active file
|
|
114
|
+
const activeFileName = getFileName(content, activeFileId);
|
|
115
|
+
|
|
116
|
+
const params: TabStateParams = {};
|
|
117
|
+
|
|
118
|
+
// If that's missing, no need to proceed (should never happen).
|
|
119
|
+
if (activeFileName) {
|
|
120
|
+
// In any case we set the `file` parameter.
|
|
121
|
+
params.file = activeFileName;
|
|
122
|
+
|
|
123
|
+
// We only need to encode the `tabs` parameter
|
|
124
|
+
// if there's more than one tab.
|
|
125
|
+
if (tabList.length > 1) {
|
|
126
|
+
// Set tab list
|
|
127
|
+
const tabs = tabList
|
|
128
|
+
.map((tabstate: TabState) =>
|
|
129
|
+
getFileName(content, tabstate.fileId),
|
|
130
|
+
)
|
|
131
|
+
.join(delimiter);
|
|
132
|
+
params.tabs = tabs;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return params;
|
|
76
137
|
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useSearchParams } from 'react-router-dom';
|
|
2
|
+
import { FileId, VZCodeContent } from '../types';
|
|
3
|
+
import { TabState } from './vzReducer';
|
|
4
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
TabStateParams,
|
|
7
|
+
decodeTabs,
|
|
8
|
+
encodeTabs,
|
|
9
|
+
} from './tabsSearchParameters';
|
|
10
|
+
|
|
11
|
+
// Synchronizes the tab state with the URL parameters.
|
|
12
|
+
export const useURLSync = ({
|
|
13
|
+
content,
|
|
14
|
+
openTab,
|
|
15
|
+
setActiveFileId,
|
|
16
|
+
tabList,
|
|
17
|
+
activeFileId,
|
|
18
|
+
}: {
|
|
19
|
+
content: VZCodeContent | null;
|
|
20
|
+
openTab: (tabState: TabState) => void;
|
|
21
|
+
setActiveFileId: (activeFileId: FileId) => void;
|
|
22
|
+
tabList: Array<TabState>;
|
|
23
|
+
activeFileId: FileId | null;
|
|
24
|
+
}) => {
|
|
25
|
+
// Use React router to get and set the search parameters.
|
|
26
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
27
|
+
|
|
28
|
+
// Extract the tab state parameters from the search parameters.
|
|
29
|
+
const tabStateParams: TabStateParams = useMemo(
|
|
30
|
+
() => ({
|
|
31
|
+
file: searchParams.get('file'),
|
|
32
|
+
tabs: searchParams.get('tabs'),
|
|
33
|
+
}),
|
|
34
|
+
[searchParams],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// `true` after the state is updated based on the
|
|
38
|
+
// initial search parameters from the URL.
|
|
39
|
+
const isInitialized = useRef(false);
|
|
40
|
+
|
|
41
|
+
// Initialize the state based on the search parameters.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
// Do nothing if the state has already been initialized.
|
|
44
|
+
if (isInitialized.current) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Wait for the content to be loaded.
|
|
49
|
+
if (!content) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Decode the search parameters.
|
|
54
|
+
const { tabList, activeFileId } = decodeTabs({
|
|
55
|
+
tabStateParams,
|
|
56
|
+
content,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Mark the state as initialized.
|
|
60
|
+
isInitialized.current = true;
|
|
61
|
+
|
|
62
|
+
// Update the state.
|
|
63
|
+
setActiveFileId(activeFileId);
|
|
64
|
+
tabList.forEach(openTab);
|
|
65
|
+
}, [
|
|
66
|
+
content,
|
|
67
|
+
searchParams,
|
|
68
|
+
setActiveFileId,
|
|
69
|
+
openTab,
|
|
70
|
+
isInitialized,
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
// Track content in a ref so that we can use it in the
|
|
74
|
+
// effect below without triggering the effect when
|
|
75
|
+
// the content changes.
|
|
76
|
+
const contentRef = useRef(content);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
contentRef.current = content;
|
|
79
|
+
}, [content]);
|
|
80
|
+
|
|
81
|
+
// Update the URL to match the current state whenever
|
|
82
|
+
// the active file or tab list changes.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
// Safety check to make sure the content is loaded.
|
|
85
|
+
// Should never happen.
|
|
86
|
+
if (!contentRef.current) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const newTabStateParams: TabStateParams = encodeTabs({
|
|
91
|
+
tabList,
|
|
92
|
+
activeFileId,
|
|
93
|
+
content: contentRef.current,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
tabStateParams.file !== newTabStateParams.file ||
|
|
98
|
+
tabStateParams.tabs !== newTabStateParams.tabs
|
|
99
|
+
) {
|
|
100
|
+
// Update the URL if the search parameters have changed.
|
|
101
|
+
setSearchParams(
|
|
102
|
+
(oldSearchParams: URLSearchParams) => {
|
|
103
|
+
// Create a copy of the old search params
|
|
104
|
+
const updatedSearchParams = new URLSearchParams(
|
|
105
|
+
oldSearchParams,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Set the 'file' parameter
|
|
109
|
+
if (newTabStateParams.file) {
|
|
110
|
+
updatedSearchParams.set(
|
|
111
|
+
'file',
|
|
112
|
+
newTabStateParams.file,
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
updatedSearchParams.delete('file'); // Remove 'file' if it's not present in newTabStateParams
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Set the 'tabs' parameter
|
|
119
|
+
if (newTabStateParams.tabs) {
|
|
120
|
+
updatedSearchParams.set(
|
|
121
|
+
'tabs',
|
|
122
|
+
newTabStateParams.tabs,
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
updatedSearchParams.delete('tabs'); // Remove 'tabs' if it's not present in newTabStateParams
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Return the updated search params
|
|
129
|
+
return updatedSearchParams;
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}, [searchParams, tabList, activeFileId]);
|
|
134
|
+
};
|