vzcode 0.17.0 → 0.19.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 (38) hide show
  1. package/README.md +47 -39
  2. package/dist/assets/index-0053b846.css +1 -0
  3. package/dist/assets/index-34134182.js +118 -0
  4. package/dist/index.html +15 -5
  5. package/package.json +4 -4
  6. package/src/client/App.tsx +92 -52
  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 +77 -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 +76 -57
  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-ab3ae13e.js +0 -115
  38. 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-ab3ae13e.js"></script>
22
- <link rel="stylesheet" href="/assets/index-ccd52a89.css">
31
+ <script type="module" crossorigin src="/assets/index-34134182.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.17.0",
3
+ "version": "0.19.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",
@@ -68,8 +68,8 @@
68
68
  "react-bootstrap": "^2.8.0",
69
69
  "react-dom": "^18.2.0",
70
70
  "react-select": "^5.7.4",
71
- "sharedb": "^4.0.0",
72
- "sharedb-client-browser": "^4.3.0",
71
+ "sharedb": "^4.0.1",
72
+ "sharedb-client-browser": "^4.3.1",
73
73
  "ot-json1": "^1.0.2",
74
74
  "ws": "^8.13.0"
75
75
  },
@@ -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,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 { 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
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 = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
32
- 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
+ );
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] = useState<VZCodeContent | null>(null);
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(collection, id);
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] = useState<FileId>(null);
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] = useState<ThemeLabel>(defaultTheme);
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] = useState(false);
175
+ const [isSettingsOpen, setIsSettingsOpen] =
176
+ useState(false);
152
177
 
153
178
  // The set of open directories.
154
- const { isDirectoryOpen, toggleDirectory } = useOpenDirectories();
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: { ...document.files, [randomId()]: { name, text: '' } },
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) => ({ ...document, isInteracting: true }));
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) => ({ ...document, isInteracting: false }));
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 ? content.files : null;
275
+ const files: Files | null = content
276
+ ? content.files
277
+ : null;
241
278
 
242
279
  return (
243
- <div className="app">
244
- <div className="left">
245
- <Sidebar
246
- createFile={createFile}
247
- files={files}
248
- handleRenameFileClick={handleRenameFileClick}
249
- handleDeleteFileClick={handleDeleteFileClick}
250
- handleFileClick={openTab}
251
- setIsSettingsOpen={setIsSettingsOpen}
252
- isDirectoryOpen={isDirectoryOpen}
253
- toggleDirectory={toggleDirectory}
254
- />
255
- <Settings
256
- show={isSettingsOpen}
257
- onClose={handleSettingsClose}
258
- theme={theme}
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
- onInteract={handleInteract}
278
- 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}
279
307
  />
280
- ) : 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 />
281
321
  </div>
282
- </div>
322
+ </SplitPaneResizeProvider>
283
323
  );
284
324
  }
285
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
+ }