vzcode 0.16.0 → 0.18.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 +47 -39
- package/dist/assets/index-0053b846.css +1 -0
- package/dist/assets/{index-0818f26e.js → index-24635cd2.js} +37 -37
- package/dist/index.html +15 -5
- package/package.json +2 -2
- package/src/client/App.tsx +99 -54
- package/src/client/CodeEditor/getOrCreateEditor.ts +33 -9
- package/src/client/CodeEditor/index.tsx +15 -11
- package/src/client/CodeEditor/json1PresenceBroadcast.js +10 -3
- package/src/client/CodeEditor/json1PresenceDisplay.js +7 -2
- package/src/client/CodeEditor/widgets.ts +5 -1
- package/src/client/Resizer/index.tsx +96 -0
- package/src/client/Resizer/styles.scss +29 -0
- package/src/client/Settings.tsx +29 -21
- package/src/client/Sidebar/DirectoryArrowSVG.jsx +4 -1
- package/src/client/Sidebar/DirectoryListing.tsx +17 -5
- package/src/client/Sidebar/FileListing.tsx +4 -1
- package/src/client/Sidebar/Listing.tsx +9 -2
- package/src/client/Sidebar/index.tsx +30 -8
- package/src/client/Sidebar/styles.scss +0 -1
- package/src/client/SplitPaneResizeContext.tsx +63 -0
- package/src/client/TabList/index.tsx +7 -2
- package/src/client/diff.js +7 -1
- package/src/client/featureFlags.js +2 -1
- package/src/client/sortFileTree.ts +3 -1
- package/src/client/style.scss +18 -12
- package/src/client/themes.ts +10 -8
- package/src/client/useDynamicTheme.ts +5 -2
- package/src/client/useOpenDirectories.ts +11 -8
- package/src/client/usePrettier/index.ts +77 -61
- package/src/client/usePrettier/worker.ts +4 -1
- package/src/client/useTabsState.ts +10 -3
- package/src/randomId.js +14 -1
- package/src/server/computeInitialDocument.js +18 -5
- package/src/server/index.js +7 -2
- package/src/types.ts +11 -4
- package/dist/assets/index-ccd52a89.css +0 -1
package/dist/index.html
CHANGED
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0"
|
|
8
|
+
/>
|
|
6
9
|
<title>VZCode</title>
|
|
7
|
-
<link
|
|
8
|
-
|
|
10
|
+
<link
|
|
11
|
+
rel="preconnect"
|
|
12
|
+
href="https://fonts.googleapis.com"
|
|
13
|
+
/>
|
|
14
|
+
<link
|
|
15
|
+
rel="preconnect"
|
|
16
|
+
href="https://fonts.gstatic.com"
|
|
17
|
+
crossorigin
|
|
18
|
+
/>
|
|
9
19
|
<link
|
|
10
20
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
11
21
|
rel="stylesheet"
|
|
@@ -18,8 +28,8 @@
|
|
|
18
28
|
href="https://unpkg.com/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
|
19
29
|
rel="stylesheet"
|
|
20
30
|
/>
|
|
21
|
-
<script type="module" crossorigin src="/assets/index-
|
|
22
|
-
<link rel="stylesheet" href="/assets/index-
|
|
31
|
+
<script type="module" crossorigin src="/assets/index-24635cd2.js"></script>
|
|
32
|
+
<link rel="stylesheet" href="/assets/index-0053b846.css">
|
|
23
33
|
</head>
|
|
24
34
|
<body>
|
|
25
35
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -54,7 +54,7 @@
|
|
|
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.13",
|
|
58
58
|
"codemirror": "^6.0.1",
|
|
59
59
|
"codemirror-ot": "^4.1.0",
|
|
60
60
|
"d3-array": "^3.2.4",
|
package/src/client/App.tsx
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useEffect,
|
|
4
|
+
useCallback,
|
|
5
|
+
useRef,
|
|
6
|
+
} from 'react';
|
|
2
7
|
import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
|
|
3
8
|
import { json1Presence } from '../ot';
|
|
4
9
|
import { randomId } from '../randomId';
|
|
@@ -6,15 +11,27 @@ import { CodeEditor } from './CodeEditor';
|
|
|
6
11
|
import { diff } from './diff';
|
|
7
12
|
import { Settings } from './Settings';
|
|
8
13
|
import { Sidebar } from './Sidebar';
|
|
9
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
FileId,
|
|
16
|
+
Files,
|
|
17
|
+
ShareDBDoc,
|
|
18
|
+
VZCodeContent,
|
|
19
|
+
} from '../types';
|
|
10
20
|
import { TabList } from './TabList';
|
|
11
21
|
import { useOpenDirectories } from './useOpenDirectories';
|
|
12
22
|
import { useTabsState } from './useTabsState';
|
|
13
23
|
import { ThemeLabel, defaultTheme } from './themes';
|
|
14
|
-
import './style.scss';
|
|
15
|
-
import { usePrettier } from './usePrettier';
|
|
16
24
|
import { useEditorCache } from './useEditorCache';
|
|
17
25
|
import { useDynamicTheme } from './useDynamicTheme';
|
|
26
|
+
import { usePrettier } from './usePrettier';
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
import PrettierWorker from './usePrettier/worker?worker';
|
|
29
|
+
import { SplitPaneResizeProvider } from './SplitPaneResizeContext';
|
|
30
|
+
import { Resizer } from './Resizer';
|
|
31
|
+
import './style.scss';
|
|
32
|
+
|
|
33
|
+
// Instantiate the Prettier worker.
|
|
34
|
+
const prettierWorker = new PrettierWorker();
|
|
18
35
|
|
|
19
36
|
// Register our custom JSON1 OT type that supports presence.
|
|
20
37
|
// See https://github.com/vizhub-core/json1-presence
|
|
@@ -23,8 +40,13 @@ ShareDBClient.types.register(json1Presence.type);
|
|
|
23
40
|
// Establish the singleton ShareDB connection over WebSockets.
|
|
24
41
|
// TODO consider using reconnecting WebSocket
|
|
25
42
|
const { Connection } = ShareDBClient;
|
|
26
|
-
const wsProtocol =
|
|
27
|
-
|
|
43
|
+
const wsProtocol =
|
|
44
|
+
window.location.protocol === 'https:'
|
|
45
|
+
? 'wss://'
|
|
46
|
+
: 'ws://';
|
|
47
|
+
const socket = new WebSocket(
|
|
48
|
+
wsProtocol + window.location.host + '/ws',
|
|
49
|
+
);
|
|
28
50
|
const connection = new Connection(socket);
|
|
29
51
|
|
|
30
52
|
function App() {
|
|
@@ -33,6 +55,7 @@ function App() {
|
|
|
33
55
|
useState<ShareDBDoc<VZCodeContent> | null>(null);
|
|
34
56
|
|
|
35
57
|
// A helper function to submit operations to the ShareDB document
|
|
58
|
+
|
|
36
59
|
const submitOperation: (
|
|
37
60
|
next: (content: VZCodeContent) => VZCodeContent,
|
|
38
61
|
) => void = useCallback(
|
|
@@ -47,7 +70,7 @@ function App() {
|
|
|
47
70
|
);
|
|
48
71
|
|
|
49
72
|
// Auto-run Pretter after local changes.
|
|
50
|
-
usePrettier(shareDBDoc, submitOperation);
|
|
73
|
+
usePrettier(shareDBDoc, submitOperation, prettierWorker);
|
|
51
74
|
|
|
52
75
|
// Local ShareDB presence, for broadcasting our cursor position
|
|
53
76
|
// so other clients can see it.
|
|
@@ -61,7 +84,8 @@ function App() {
|
|
|
61
84
|
// The `doc.data` part of the ShareDB document,
|
|
62
85
|
// updated on each change to decouple rendering from ShareDB.
|
|
63
86
|
// Starts out as `null` until the document is loaded.
|
|
64
|
-
const [content, setContent] =
|
|
87
|
+
const [content, setContent] =
|
|
88
|
+
useState<VZCodeContent | null>(null);
|
|
65
89
|
|
|
66
90
|
// Set up the connection to ShareDB.
|
|
67
91
|
// TODO move this logic to a hook called `useShareDB`
|
|
@@ -94,7 +118,10 @@ function App() {
|
|
|
94
118
|
|
|
95
119
|
// Set up presence.
|
|
96
120
|
// See https://github.com/share/sharedb/blob/master/examples/rich-text-presence/client.js#L53
|
|
97
|
-
const docPresence = connection.getDocPresence(
|
|
121
|
+
const docPresence = connection.getDocPresence(
|
|
122
|
+
collection,
|
|
123
|
+
id,
|
|
124
|
+
);
|
|
98
125
|
|
|
99
126
|
// Subscribe to receive remote presence updates.
|
|
100
127
|
docPresence.subscribe(function (error) {
|
|
@@ -118,7 +145,8 @@ function App() {
|
|
|
118
145
|
|
|
119
146
|
// The id of the currently open file tab.
|
|
120
147
|
// TODO make this a URL param
|
|
121
|
-
const [activeFileId, setActiveFileId] =
|
|
148
|
+
const [activeFileId, setActiveFileId] =
|
|
149
|
+
useState<FileId | null>(null);
|
|
122
150
|
|
|
123
151
|
// The ordered list of tabs.
|
|
124
152
|
// TODO make this a URL param
|
|
@@ -134,7 +162,8 @@ function App() {
|
|
|
134
162
|
|
|
135
163
|
// The current theme.
|
|
136
164
|
// TODO persist this in local storage
|
|
137
|
-
const [theme, setTheme] =
|
|
165
|
+
const [theme, setTheme] =
|
|
166
|
+
useState<ThemeLabel>(defaultTheme);
|
|
138
167
|
|
|
139
168
|
// Cache of CodeMirror editors by file id.
|
|
140
169
|
const editorCache = useEditorCache();
|
|
@@ -143,10 +172,12 @@ function App() {
|
|
|
143
172
|
useDynamicTheme(editorCache, theme);
|
|
144
173
|
|
|
145
174
|
// True to show the settings modal.
|
|
146
|
-
const [isSettingsOpen, setIsSettingsOpen] =
|
|
175
|
+
const [isSettingsOpen, setIsSettingsOpen] =
|
|
176
|
+
useState(false);
|
|
147
177
|
|
|
148
178
|
// The set of open directories.
|
|
149
|
-
const { isDirectoryOpen, toggleDirectory } =
|
|
179
|
+
const { isDirectoryOpen, toggleDirectory } =
|
|
180
|
+
useOpenDirectories();
|
|
150
181
|
|
|
151
182
|
// TODO move this logic to a hook called `useFileCRUD`
|
|
152
183
|
// Include
|
|
@@ -159,7 +190,10 @@ function App() {
|
|
|
159
190
|
if (name) {
|
|
160
191
|
submitOperation((document) => ({
|
|
161
192
|
...document,
|
|
162
|
-
files: {
|
|
193
|
+
files: {
|
|
194
|
+
...document.files,
|
|
195
|
+
[randomId()]: { name, text: '' },
|
|
196
|
+
},
|
|
163
197
|
}));
|
|
164
198
|
}
|
|
165
199
|
}, [submitOperation]);
|
|
@@ -219,7 +253,10 @@ function App() {
|
|
|
219
253
|
const handleInteract = useCallback(() => {
|
|
220
254
|
// Set `isInteracting: true` if not already set.
|
|
221
255
|
if (!interactTimeoutRef.current) {
|
|
222
|
-
submitOperation((document) => ({
|
|
256
|
+
submitOperation((document) => ({
|
|
257
|
+
...document,
|
|
258
|
+
isInteracting: true,
|
|
259
|
+
}));
|
|
223
260
|
} else {
|
|
224
261
|
clearTimeout(interactTimeoutRef.current);
|
|
225
262
|
}
|
|
@@ -227,54 +264,62 @@ function App() {
|
|
|
227
264
|
// Set `isInteracting: false` after a delay.
|
|
228
265
|
interactTimeoutRef.current = setTimeout(() => {
|
|
229
266
|
interactTimeoutRef.current = null;
|
|
230
|
-
submitOperation((document) => ({
|
|
267
|
+
submitOperation((document) => ({
|
|
268
|
+
...document,
|
|
269
|
+
isInteracting: false,
|
|
270
|
+
}));
|
|
231
271
|
}, 800);
|
|
232
272
|
}, [submitOperation]);
|
|
233
273
|
|
|
234
274
|
// Isolate the files object from the document.
|
|
235
|
-
const files: Files | null = content
|
|
275
|
+
const files: Files | null = content
|
|
276
|
+
? content.files
|
|
277
|
+
: null;
|
|
236
278
|
|
|
237
279
|
return (
|
|
238
|
-
<
|
|
239
|
-
<div className="
|
|
240
|
-
<
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
setTheme={setTheme}
|
|
255
|
-
/>
|
|
256
|
-
</div>
|
|
257
|
-
<div className="right">
|
|
258
|
-
<TabList
|
|
259
|
-
files={files}
|
|
260
|
-
tabList={tabList}
|
|
261
|
-
activeFileId={activeFileId}
|
|
262
|
-
setActiveFileId={setActiveFileId}
|
|
263
|
-
closeTab={closeTab}
|
|
264
|
-
/>
|
|
265
|
-
{content && activeFileId ? (
|
|
266
|
-
<CodeEditor
|
|
267
|
-
shareDBDoc={shareDBDoc}
|
|
268
|
-
localPresence={localPresence}
|
|
269
|
-
docPresence={docPresence}
|
|
270
|
-
activeFileId={activeFileId}
|
|
280
|
+
<SplitPaneResizeProvider>
|
|
281
|
+
<div className="app">
|
|
282
|
+
<div className="left">
|
|
283
|
+
<Sidebar
|
|
284
|
+
createFile={createFile}
|
|
285
|
+
files={files}
|
|
286
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
287
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
288
|
+
handleFileClick={openTab}
|
|
289
|
+
setIsSettingsOpen={setIsSettingsOpen}
|
|
290
|
+
isDirectoryOpen={isDirectoryOpen}
|
|
291
|
+
toggleDirectory={toggleDirectory}
|
|
292
|
+
/>
|
|
293
|
+
<Settings
|
|
294
|
+
show={isSettingsOpen}
|
|
295
|
+
onClose={handleSettingsClose}
|
|
271
296
|
theme={theme}
|
|
272
|
-
|
|
273
|
-
|
|
297
|
+
setTheme={setTheme}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
<div className="right">
|
|
301
|
+
<TabList
|
|
302
|
+
files={files}
|
|
303
|
+
tabList={tabList}
|
|
304
|
+
activeFileId={activeFileId}
|
|
305
|
+
setActiveFileId={setActiveFileId}
|
|
306
|
+
closeTab={closeTab}
|
|
274
307
|
/>
|
|
275
|
-
|
|
308
|
+
{content && activeFileId ? (
|
|
309
|
+
<CodeEditor
|
|
310
|
+
shareDBDoc={shareDBDoc}
|
|
311
|
+
localPresence={localPresence}
|
|
312
|
+
docPresence={docPresence}
|
|
313
|
+
activeFileId={activeFileId}
|
|
314
|
+
theme={theme}
|
|
315
|
+
onInteract={handleInteract}
|
|
316
|
+
editorCache={editorCache}
|
|
317
|
+
/>
|
|
318
|
+
) : null}
|
|
319
|
+
</div>
|
|
320
|
+
<Resizer />
|
|
276
321
|
</div>
|
|
277
|
-
</
|
|
322
|
+
</SplitPaneResizeProvider>
|
|
278
323
|
);
|
|
279
324
|
}
|
|
280
325
|
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { EditorView, basicSetup } from 'codemirror';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Compartment,
|
|
4
|
+
EditorState,
|
|
5
|
+
} from '@codemirror/state';
|
|
3
6
|
import { javascript } from '@codemirror/lang-javascript';
|
|
4
7
|
import { markdown } from '@codemirror/lang-markdown';
|
|
5
8
|
import { html } from '@codemirror/lang-html';
|
|
6
9
|
import { css } from '@codemirror/lang-css';
|
|
7
10
|
import { json1Sync } from 'codemirror-ot';
|
|
8
11
|
import { json1Presence, textUnicode } from '../../ot';
|
|
9
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
FileId,
|
|
14
|
+
ShareDBDoc,
|
|
15
|
+
VZCodeContent,
|
|
16
|
+
} from '../../types';
|
|
10
17
|
import { json1PresenceBroadcast } from './json1PresenceBroadcast';
|
|
11
18
|
import { json1PresenceDisplay } from './json1PresenceDisplay';
|
|
12
19
|
import { widgets } from './widgets';
|
|
13
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
EditorCache,
|
|
22
|
+
EditorCacheValue,
|
|
23
|
+
} from '../useEditorCache';
|
|
14
24
|
import { ThemeLabel, themeOptionsByLabel } from '../themes';
|
|
15
25
|
|
|
16
26
|
// Language extensions for CodeMirror.
|
|
@@ -104,11 +114,18 @@ export const getOrCreateEditor = ({
|
|
|
104
114
|
|
|
105
115
|
// Deals with broadcasting changes in cursor location and selection.
|
|
106
116
|
if (localPresence)
|
|
107
|
-
extensions.push(
|
|
117
|
+
extensions.push(
|
|
118
|
+
json1PresenceBroadcast({
|
|
119
|
+
path: textPath,
|
|
120
|
+
localPresence,
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
108
123
|
|
|
109
124
|
// Deals with receiving the broadcas from other clients and displaying them.
|
|
110
125
|
if (docPresence)
|
|
111
|
-
extensions.push(
|
|
126
|
+
extensions.push(
|
|
127
|
+
json1PresenceDisplay({ path: textPath, docPresence }),
|
|
128
|
+
);
|
|
112
129
|
|
|
113
130
|
// This is the "basic setup" for CodeMirror,
|
|
114
131
|
// which actually adds a ton on functionality.
|
|
@@ -118,7 +135,9 @@ export const getOrCreateEditor = ({
|
|
|
118
135
|
extensions.push(basicSetup);
|
|
119
136
|
|
|
120
137
|
// This supports dynamic changing of the theme.
|
|
121
|
-
extensions.push(
|
|
138
|
+
extensions.push(
|
|
139
|
+
themeCompartment.of(themeOptionsByLabel[theme].value),
|
|
140
|
+
);
|
|
122
141
|
|
|
123
142
|
// TODO handle dynamic changing of the file extension.
|
|
124
143
|
// TODO handle dynamic file extensions by making
|
|
@@ -131,15 +150,20 @@ export const getOrCreateEditor = ({
|
|
|
131
150
|
return languageFunc ? languageFunc() : undefined;
|
|
132
151
|
};
|
|
133
152
|
|
|
134
|
-
const languageExtension =
|
|
153
|
+
const languageExtension =
|
|
154
|
+
getLanguageExtension(fileExtension);
|
|
135
155
|
if (languageExtension) {
|
|
136
|
-
extensions.push(
|
|
156
|
+
extensions.push(
|
|
157
|
+
languageCompartment.of(languageExtension),
|
|
158
|
+
);
|
|
137
159
|
} else {
|
|
138
160
|
// Not sure if this case even works.
|
|
139
161
|
// TODO manually test this case by creating a file
|
|
140
162
|
// that has no extension, opening it up,
|
|
141
163
|
// and then adding an extension.
|
|
142
|
-
console.warn(
|
|
164
|
+
console.warn(
|
|
165
|
+
`No language extension for file extension: ${fileExtension}`,
|
|
166
|
+
);
|
|
143
167
|
// We still need to push the compartment,
|
|
144
168
|
// otherwise the compartment won't work when
|
|
145
169
|
// a file extension _is_ added later on.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { useRef, useLayoutEffect } from 'react';
|
|
2
2
|
import { FileId } from '../../types';
|
|
3
3
|
import { ThemeLabel, defaultTheme } from '../themes';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
EditorCache,
|
|
6
|
+
EditorCacheValue,
|
|
7
|
+
} from '../useEditorCache';
|
|
5
8
|
import { getOrCreateEditor } from './getOrCreateEditor';
|
|
6
9
|
import './style.scss';
|
|
7
10
|
|
|
@@ -40,16 +43,17 @@ export const CodeEditor = ({
|
|
|
40
43
|
|
|
41
44
|
// Get the editor corresponding to the active file.
|
|
42
45
|
// Looks in `editorCache` first, and if not found, creates a new editor.
|
|
43
|
-
const editorCacheValue: EditorCacheValue =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
const editorCacheValue: EditorCacheValue =
|
|
47
|
+
getOrCreateEditor({
|
|
48
|
+
fileId: activeFileId,
|
|
49
|
+
shareDBDoc,
|
|
50
|
+
filesPath,
|
|
51
|
+
localPresence,
|
|
52
|
+
docPresence,
|
|
53
|
+
theme,
|
|
54
|
+
onInteract,
|
|
55
|
+
editorCache,
|
|
56
|
+
});
|
|
53
57
|
|
|
54
58
|
// Add the editor to the DOM.
|
|
55
59
|
ref.current.appendChild(editorCacheValue.editor.dom);
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { EditorView } from 'codemirror';
|
|
2
2
|
|
|
3
3
|
// Deals with broadcasting changes in cursor location and selection.
|
|
4
|
-
export const json1PresenceBroadcast = ({
|
|
4
|
+
export const json1PresenceBroadcast = ({
|
|
5
|
+
path,
|
|
6
|
+
localPresence,
|
|
7
|
+
}) =>
|
|
5
8
|
// See https://discuss.codemirror.net/t/codemirror-6-proper-way-to-listen-for-changes/2395/10
|
|
6
9
|
EditorView.updateListener.of((viewUpdate) => {
|
|
7
10
|
// If this update modified the cursor / selection,
|
|
@@ -11,11 +14,15 @@ export const json1PresenceBroadcast = ({ path, localPresence }) =>
|
|
|
11
14
|
// Unfortunately JSON1 with presence does not yet
|
|
12
15
|
// support multiple selections.
|
|
13
16
|
// See https://github.com/ottypes/json1/pull/25#issuecomment-1459616521
|
|
14
|
-
const selection =
|
|
17
|
+
const selection =
|
|
18
|
+
viewUpdate.state.selection.ranges[0];
|
|
15
19
|
const { from, to } = selection;
|
|
16
20
|
|
|
17
21
|
// Translate this into the form expected by json1Presence.
|
|
18
|
-
const presence = {
|
|
22
|
+
const presence = {
|
|
23
|
+
start: [...path, from],
|
|
24
|
+
end: [...path, to],
|
|
25
|
+
};
|
|
19
26
|
|
|
20
27
|
// Broadcast presence to remote clients!
|
|
21
28
|
// See https://github.com/share/sharedb/blob/master/examples/rich-text-presence/client.js#L71
|
|
@@ -14,7 +14,10 @@ import { Annotation, RangeSet } from '@codemirror/state';
|
|
|
14
14
|
// * https://codemirror.net/examples/decoration/
|
|
15
15
|
// * https://github.com/share/sharedb/blob/master/examples/rich-text-presence/client.js
|
|
16
16
|
// * https://share.github.io/sharedb/presence
|
|
17
|
-
export const json1PresenceDisplay = ({
|
|
17
|
+
export const json1PresenceDisplay = ({
|
|
18
|
+
path,
|
|
19
|
+
docPresence,
|
|
20
|
+
}) => [
|
|
18
21
|
ViewPlugin.fromClass(
|
|
19
22
|
class {
|
|
20
23
|
constructor(view) {
|
|
@@ -67,7 +70,9 @@ export const json1PresenceDisplay = ({ path, docPresence }) => [
|
|
|
67
70
|
// Set timeout so that the current CodeMirror update finishes
|
|
68
71
|
// before the next ones that render presence begin.
|
|
69
72
|
setTimeout(() => {
|
|
70
|
-
view.dispatch({
|
|
73
|
+
view.dispatch({
|
|
74
|
+
annotations: [presenceAnnotation.of(true)],
|
|
75
|
+
});
|
|
71
76
|
}, 0);
|
|
72
77
|
});
|
|
73
78
|
}
|
|
@@ -8,7 +8,11 @@ import interact from '@replit/codemirror-interact';
|
|
|
8
8
|
// Inspired by:
|
|
9
9
|
// https://github.com/replit/codemirror-interact/blob/master/dev/index.ts
|
|
10
10
|
// `onInteract` is called when the user interacts with a widget.
|
|
11
|
-
export const widgets = ({
|
|
11
|
+
export const widgets = ({
|
|
12
|
+
onInteract,
|
|
13
|
+
}: {
|
|
14
|
+
onInteract?: () => void;
|
|
15
|
+
}) =>
|
|
12
16
|
interact({
|
|
13
17
|
rules: [
|
|
14
18
|
// a rule for a number dragger
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The split pane resizer component.
|
|
2
|
+
// This is what users drag to resize the split pane.
|
|
3
|
+
import {
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useCallback,
|
|
7
|
+
useRef,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
import { SplitPaneResizeContext } from '../SplitPaneResizeContext';
|
|
11
|
+
import './styles.scss';
|
|
12
|
+
|
|
13
|
+
// This is the width of the resizer interaction surface.
|
|
14
|
+
// On this surface, the cursor changes to a resize cursor.
|
|
15
|
+
const resizerInteractionSurfaceWidth = 20;
|
|
16
|
+
|
|
17
|
+
// Expand while dragging to make it easier to hit.
|
|
18
|
+
// Without this, various sidebar elements get hovered on and change style.
|
|
19
|
+
const resizerInteractionSurfaceWidthWhileDragging = 200;
|
|
20
|
+
|
|
21
|
+
// This is the width of the resizer visible area.
|
|
22
|
+
// This is the part of the resizer that is visible to the user.
|
|
23
|
+
const resizerThumbWidth = 4;
|
|
24
|
+
|
|
25
|
+
export const Resizer = () => {
|
|
26
|
+
const {
|
|
27
|
+
codeEditorWidth,
|
|
28
|
+
moveSplitPane,
|
|
29
|
+
isDragging,
|
|
30
|
+
setIsDragging,
|
|
31
|
+
} = useContext(SplitPaneResizeContext);
|
|
32
|
+
const previousClientX = useRef<number>(0);
|
|
33
|
+
|
|
34
|
+
const onMouseDown = useCallback(
|
|
35
|
+
(event: React.MouseEvent) => {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
previousClientX.current = event.clientX;
|
|
38
|
+
setIsDragging(true);
|
|
39
|
+
document.body.style.cursor = 'col-resize';
|
|
40
|
+
},
|
|
41
|
+
[setIsDragging],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const onMouseMove = useCallback(
|
|
45
|
+
(event: MouseEvent) => {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
const movementClientX =
|
|
48
|
+
event.clientX - previousClientX.current;
|
|
49
|
+
previousClientX.current = event.clientX;
|
|
50
|
+
moveSplitPane(movementClientX);
|
|
51
|
+
},
|
|
52
|
+
[moveSplitPane],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const onMouseUp = useCallback(() => {
|
|
56
|
+
setIsDragging(false);
|
|
57
|
+
document.body.style.cursor = 'default';
|
|
58
|
+
}, [setIsDragging]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (isDragging) {
|
|
62
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
63
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
64
|
+
return () => {
|
|
65
|
+
document.removeEventListener(
|
|
66
|
+
'mousemove',
|
|
67
|
+
onMouseMove,
|
|
68
|
+
);
|
|
69
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}, [isDragging, onMouseMove, onMouseUp]);
|
|
73
|
+
|
|
74
|
+
const resizerWidth = isDragging
|
|
75
|
+
? resizerInteractionSurfaceWidthWhileDragging
|
|
76
|
+
: resizerInteractionSurfaceWidth;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
className="vz-resizer"
|
|
81
|
+
onMouseDown={onMouseDown}
|
|
82
|
+
style={{
|
|
83
|
+
left: codeEditorWidth - resizerWidth / 2,
|
|
84
|
+
width: resizerWidth,
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
className="vz-resizer-thumb"
|
|
89
|
+
style={{
|
|
90
|
+
left: resizerWidth / 2 - resizerThumbWidth / 2,
|
|
91
|
+
width: resizerThumbWidth,
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.vz-resizer {
|
|
2
|
+
position: absolute;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
bottom: 0;
|
|
6
|
+
|
|
7
|
+
// Useful for debugging:
|
|
8
|
+
// background-color: #ffd0007c;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.vz-resizer:hover {
|
|
12
|
+
cursor: col-resize;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.vz-resizer .vz-resizer-thumb {
|
|
16
|
+
position: absolute;
|
|
17
|
+
top: 0;
|
|
18
|
+
bottom: 0;
|
|
19
|
+
|
|
20
|
+
// Transition the background color after a delay.
|
|
21
|
+
// First value is the duration of the transition,
|
|
22
|
+
// second value is the delay before the transition starts.
|
|
23
|
+
// Tuned to roughly match VSCode's resizer.
|
|
24
|
+
transition: background-color 0.15s 0.25s;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.vz-resizer:hover .vz-resizer-thumb {
|
|
28
|
+
background-color: #0078d4;
|
|
29
|
+
}
|