vzcode 0.17.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-ab3ae13e.js → index-24635cd2.js} +37 -37
- package/dist/index.html +15 -5
- package/package.json +2 -2
- package/src/client/App.tsx +92 -52
- 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 +76 -57
- 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,17 +11,24 @@ 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
24
|
import { useEditorCache } from './useEditorCache';
|
|
16
25
|
import { useDynamicTheme } from './useDynamicTheme';
|
|
17
26
|
import { usePrettier } from './usePrettier';
|
|
18
27
|
// @ts-ignore
|
|
19
28
|
import PrettierWorker from './usePrettier/worker?worker';
|
|
29
|
+
import { SplitPaneResizeProvider } from './SplitPaneResizeContext';
|
|
30
|
+
import { Resizer } from './Resizer';
|
|
31
|
+
import './style.scss';
|
|
20
32
|
|
|
21
33
|
// Instantiate the Prettier worker.
|
|
22
34
|
const prettierWorker = new PrettierWorker();
|
|
@@ -28,8 +40,13 @@ ShareDBClient.types.register(json1Presence.type);
|
|
|
28
40
|
// Establish the singleton ShareDB connection over WebSockets.
|
|
29
41
|
// TODO consider using reconnecting WebSocket
|
|
30
42
|
const { Connection } = ShareDBClient;
|
|
31
|
-
const wsProtocol =
|
|
32
|
-
|
|
43
|
+
const wsProtocol =
|
|
44
|
+
window.location.protocol === 'https:'
|
|
45
|
+
? 'wss://'
|
|
46
|
+
: 'ws://';
|
|
47
|
+
const socket = new WebSocket(
|
|
48
|
+
wsProtocol + window.location.host + '/ws',
|
|
49
|
+
);
|
|
33
50
|
const connection = new Connection(socket);
|
|
34
51
|
|
|
35
52
|
function App() {
|
|
@@ -38,6 +55,7 @@ function App() {
|
|
|
38
55
|
useState<ShareDBDoc<VZCodeContent> | null>(null);
|
|
39
56
|
|
|
40
57
|
// A helper function to submit operations to the ShareDB document
|
|
58
|
+
|
|
41
59
|
const submitOperation: (
|
|
42
60
|
next: (content: VZCodeContent) => VZCodeContent,
|
|
43
61
|
) => void = useCallback(
|
|
@@ -66,7 +84,8 @@ function App() {
|
|
|
66
84
|
// The `doc.data` part of the ShareDB document,
|
|
67
85
|
// updated on each change to decouple rendering from ShareDB.
|
|
68
86
|
// Starts out as `null` until the document is loaded.
|
|
69
|
-
const [content, setContent] =
|
|
87
|
+
const [content, setContent] =
|
|
88
|
+
useState<VZCodeContent | null>(null);
|
|
70
89
|
|
|
71
90
|
// Set up the connection to ShareDB.
|
|
72
91
|
// TODO move this logic to a hook called `useShareDB`
|
|
@@ -99,7 +118,10 @@ function App() {
|
|
|
99
118
|
|
|
100
119
|
// Set up presence.
|
|
101
120
|
// See https://github.com/share/sharedb/blob/master/examples/rich-text-presence/client.js#L53
|
|
102
|
-
const docPresence = connection.getDocPresence(
|
|
121
|
+
const docPresence = connection.getDocPresence(
|
|
122
|
+
collection,
|
|
123
|
+
id,
|
|
124
|
+
);
|
|
103
125
|
|
|
104
126
|
// Subscribe to receive remote presence updates.
|
|
105
127
|
docPresence.subscribe(function (error) {
|
|
@@ -123,7 +145,8 @@ function App() {
|
|
|
123
145
|
|
|
124
146
|
// The id of the currently open file tab.
|
|
125
147
|
// TODO make this a URL param
|
|
126
|
-
const [activeFileId, setActiveFileId] =
|
|
148
|
+
const [activeFileId, setActiveFileId] =
|
|
149
|
+
useState<FileId | null>(null);
|
|
127
150
|
|
|
128
151
|
// The ordered list of tabs.
|
|
129
152
|
// TODO make this a URL param
|
|
@@ -139,7 +162,8 @@ function App() {
|
|
|
139
162
|
|
|
140
163
|
// The current theme.
|
|
141
164
|
// TODO persist this in local storage
|
|
142
|
-
const [theme, setTheme] =
|
|
165
|
+
const [theme, setTheme] =
|
|
166
|
+
useState<ThemeLabel>(defaultTheme);
|
|
143
167
|
|
|
144
168
|
// Cache of CodeMirror editors by file id.
|
|
145
169
|
const editorCache = useEditorCache();
|
|
@@ -148,10 +172,12 @@ function App() {
|
|
|
148
172
|
useDynamicTheme(editorCache, theme);
|
|
149
173
|
|
|
150
174
|
// True to show the settings modal.
|
|
151
|
-
const [isSettingsOpen, setIsSettingsOpen] =
|
|
175
|
+
const [isSettingsOpen, setIsSettingsOpen] =
|
|
176
|
+
useState(false);
|
|
152
177
|
|
|
153
178
|
// The set of open directories.
|
|
154
|
-
const { isDirectoryOpen, toggleDirectory } =
|
|
179
|
+
const { isDirectoryOpen, toggleDirectory } =
|
|
180
|
+
useOpenDirectories();
|
|
155
181
|
|
|
156
182
|
// TODO move this logic to a hook called `useFileCRUD`
|
|
157
183
|
// Include
|
|
@@ -164,7 +190,10 @@ function App() {
|
|
|
164
190
|
if (name) {
|
|
165
191
|
submitOperation((document) => ({
|
|
166
192
|
...document,
|
|
167
|
-
files: {
|
|
193
|
+
files: {
|
|
194
|
+
...document.files,
|
|
195
|
+
[randomId()]: { name, text: '' },
|
|
196
|
+
},
|
|
168
197
|
}));
|
|
169
198
|
}
|
|
170
199
|
}, [submitOperation]);
|
|
@@ -224,7 +253,10 @@ function App() {
|
|
|
224
253
|
const handleInteract = useCallback(() => {
|
|
225
254
|
// Set `isInteracting: true` if not already set.
|
|
226
255
|
if (!interactTimeoutRef.current) {
|
|
227
|
-
submitOperation((document) => ({
|
|
256
|
+
submitOperation((document) => ({
|
|
257
|
+
...document,
|
|
258
|
+
isInteracting: true,
|
|
259
|
+
}));
|
|
228
260
|
} else {
|
|
229
261
|
clearTimeout(interactTimeoutRef.current);
|
|
230
262
|
}
|
|
@@ -232,54 +264,62 @@ function App() {
|
|
|
232
264
|
// Set `isInteracting: false` after a delay.
|
|
233
265
|
interactTimeoutRef.current = setTimeout(() => {
|
|
234
266
|
interactTimeoutRef.current = null;
|
|
235
|
-
submitOperation((document) => ({
|
|
267
|
+
submitOperation((document) => ({
|
|
268
|
+
...document,
|
|
269
|
+
isInteracting: false,
|
|
270
|
+
}));
|
|
236
271
|
}, 800);
|
|
237
272
|
}, [submitOperation]);
|
|
238
273
|
|
|
239
274
|
// Isolate the files object from the document.
|
|
240
|
-
const files: Files | null = content
|
|
275
|
+
const files: Files | null = content
|
|
276
|
+
? content.files
|
|
277
|
+
: null;
|
|
241
278
|
|
|
242
279
|
return (
|
|
243
|
-
<
|
|
244
|
-
<div className="
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
setTheme={setTheme}
|
|
260
|
-
/>
|
|
261
|
-
</div>
|
|
262
|
-
<div className="right">
|
|
263
|
-
<TabList
|
|
264
|
-
files={files}
|
|
265
|
-
tabList={tabList}
|
|
266
|
-
activeFileId={activeFileId}
|
|
267
|
-
setActiveFileId={setActiveFileId}
|
|
268
|
-
closeTab={closeTab}
|
|
269
|
-
/>
|
|
270
|
-
{content && activeFileId ? (
|
|
271
|
-
<CodeEditor
|
|
272
|
-
shareDBDoc={shareDBDoc}
|
|
273
|
-
localPresence={localPresence}
|
|
274
|
-
docPresence={docPresence}
|
|
275
|
-
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}
|
|
276
296
|
theme={theme}
|
|
277
|
-
|
|
278
|
-
|
|
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}
|
|
279
307
|
/>
|
|
280
|
-
|
|
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 />
|
|
281
321
|
</div>
|
|
282
|
-
</
|
|
322
|
+
</SplitPaneResizeProvider>
|
|
283
323
|
);
|
|
284
324
|
}
|
|
285
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
|
+
}
|