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.
Files changed (37) hide show
  1. package/README.md +47 -39
  2. package/dist/assets/index-0053b846.css +1 -0
  3. package/dist/assets/{index-0818f26e.js → index-24635cd2.js} +37 -37
  4. package/dist/index.html +15 -5
  5. package/package.json +2 -2
  6. package/src/client/App.tsx +99 -54
  7. package/src/client/CodeEditor/getOrCreateEditor.ts +33 -9
  8. package/src/client/CodeEditor/index.tsx +15 -11
  9. package/src/client/CodeEditor/json1PresenceBroadcast.js +10 -3
  10. package/src/client/CodeEditor/json1PresenceDisplay.js +7 -2
  11. package/src/client/CodeEditor/widgets.ts +5 -1
  12. package/src/client/Resizer/index.tsx +96 -0
  13. package/src/client/Resizer/styles.scss +29 -0
  14. package/src/client/Settings.tsx +29 -21
  15. package/src/client/Sidebar/DirectoryArrowSVG.jsx +4 -1
  16. package/src/client/Sidebar/DirectoryListing.tsx +17 -5
  17. package/src/client/Sidebar/FileListing.tsx +4 -1
  18. package/src/client/Sidebar/Listing.tsx +9 -2
  19. package/src/client/Sidebar/index.tsx +30 -8
  20. package/src/client/Sidebar/styles.scss +0 -1
  21. package/src/client/SplitPaneResizeContext.tsx +63 -0
  22. package/src/client/TabList/index.tsx +7 -2
  23. package/src/client/diff.js +7 -1
  24. package/src/client/featureFlags.js +2 -1
  25. package/src/client/sortFileTree.ts +3 -1
  26. package/src/client/style.scss +18 -12
  27. package/src/client/themes.ts +10 -8
  28. package/src/client/useDynamicTheme.ts +5 -2
  29. package/src/client/useOpenDirectories.ts +11 -8
  30. package/src/client/usePrettier/index.ts +77 -61
  31. package/src/client/usePrettier/worker.ts +4 -1
  32. package/src/client/useTabsState.ts +10 -3
  33. package/src/randomId.js +14 -1
  34. package/src/server/computeInitialDocument.js +18 -5
  35. package/src/server/index.js +7 -2
  36. package/src/types.ts +11 -4
  37. 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 name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0"
8
+ />
6
9
  <title>VZCode</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com" />
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
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-0818f26e.js"></script>
22
- <link rel="stylesheet" href="/assets/index-ccd52a89.css">
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.16.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.12",
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",
@@ -1,4 +1,9 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
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 { FileId, Files, ShareDBDoc, VZCodeContent } from '../types';
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 = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
27
- const socket = new WebSocket(wsProtocol + window.location.host + '/ws');
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] = useState<VZCodeContent | null>(null);
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(collection, id);
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] = useState<FileId>(null);
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] = useState<ThemeLabel>(defaultTheme);
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] = useState(false);
175
+ const [isSettingsOpen, setIsSettingsOpen] =
176
+ useState(false);
147
177
 
148
178
  // The set of open directories.
149
- const { isDirectoryOpen, toggleDirectory } = useOpenDirectories();
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: { ...document.files, [randomId()]: { name, text: '' } },
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) => ({ ...document, isInteracting: true }));
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) => ({ ...document, isInteracting: false }));
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 ? content.files : null;
275
+ const files: Files | null = content
276
+ ? content.files
277
+ : null;
236
278
 
237
279
  return (
238
- <div className="app">
239
- <div className="left">
240
- <Sidebar
241
- createFile={createFile}
242
- files={files}
243
- handleRenameFileClick={handleRenameFileClick}
244
- handleDeleteFileClick={handleDeleteFileClick}
245
- handleFileClick={openTab}
246
- setIsSettingsOpen={setIsSettingsOpen}
247
- isDirectoryOpen={isDirectoryOpen}
248
- toggleDirectory={toggleDirectory}
249
- />
250
- <Settings
251
- show={isSettingsOpen}
252
- onClose={handleSettingsClose}
253
- theme={theme}
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
- onInteract={handleInteract}
273
- editorCache={editorCache}
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
- ) : null}
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
- </div>
322
+ </SplitPaneResizeProvider>
278
323
  );
279
324
  }
280
325
 
@@ -1,16 +1,26 @@
1
1
  import { EditorView, basicSetup } from 'codemirror';
2
- import { Compartment, EditorState } from '@codemirror/state';
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 { FileId, ShareDBDoc, VZCodeContent } from '../../types';
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 { EditorCache, EditorCacheValue } from '../useEditorCache';
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(json1PresenceBroadcast({ path: textPath, localPresence }));
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(json1PresenceDisplay({ path: textPath, docPresence }));
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(themeCompartment.of(themeOptionsByLabel[theme].value));
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 = getLanguageExtension(fileExtension);
153
+ const languageExtension =
154
+ getLanguageExtension(fileExtension);
135
155
  if (languageExtension) {
136
- extensions.push(languageCompartment.of(languageExtension));
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(`No language extension for file extension: ${fileExtension}`);
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 { EditorCache, EditorCacheValue } from '../useEditorCache';
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 = getOrCreateEditor({
44
- fileId: activeFileId,
45
- shareDBDoc,
46
- filesPath,
47
- localPresence,
48
- docPresence,
49
- theme,
50
- onInteract,
51
- editorCache,
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 = ({ path, localPresence }) =>
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 = viewUpdate.state.selection.ranges[0];
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 = { start: [...path, from], end: [...path, to] };
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 = ({ path, docPresence }) => [
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({ annotations: [presenceAnnotation.of(true)] });
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 = ({ onInteract }: { onInteract?: () => void }) =>
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
+ }