vzcode 0.79.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -20,8 +20,8 @@
20
20
  href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
21
21
  rel="stylesheet"
22
22
  />
23
- <script type="module" crossorigin src="/assets/index-DZXcjW4O.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-WtPPom-d.css">
23
+ <script type="module" crossorigin src="/assets/index-CnwdGSLW.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-J8-kRnnE.css">
25
25
  </head>
26
26
  <body>
27
27
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "0.79.0",
3
+ "version": "0.81.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -79,15 +79,15 @@
79
79
  "@replit/codemirror-vscode-keymap": "^6.0.2",
80
80
  "@teamwork/websocket-json-stream": "^2.0.0",
81
81
  "@typescript/vfs": "^1.5.0",
82
- "@uiw/codemirror-theme-abcdef": "^4.21.24",
83
- "@uiw/codemirror-theme-dracula": "^4.21.24",
84
- "@uiw/codemirror-theme-eclipse": "^4.21.24",
85
- "@uiw/codemirror-theme-github": "^4.21.24",
86
- "@uiw/codemirror-theme-material": "^4.21.24",
87
- "@uiw/codemirror-theme-nord": "^4.21.24",
88
- "@uiw/codemirror-theme-okaidia": "^4.21.24",
89
- "@uiw/codemirror-theme-xcode": "^4.21.24",
90
- "@uiw/codemirror-themes": "^4.21.24",
82
+ "@uiw/codemirror-theme-abcdef": "^4.21.25",
83
+ "@uiw/codemirror-theme-dracula": "^4.21.25",
84
+ "@uiw/codemirror-theme-eclipse": "^4.21.25",
85
+ "@uiw/codemirror-theme-github": "^4.21.25",
86
+ "@uiw/codemirror-theme-material": "^4.21.25",
87
+ "@uiw/codemirror-theme-nord": "^4.21.25",
88
+ "@uiw/codemirror-theme-okaidia": "^4.21.25",
89
+ "@uiw/codemirror-theme-xcode": "^4.21.25",
90
+ "@uiw/codemirror-themes": "^4.21.25",
91
91
  "body-parser": "^1.20.2",
92
92
  "codemirror": "^6.0.1",
93
93
  "codemirror-ot": "^4.4.0",
@@ -95,7 +95,7 @@
95
95
  "d3-array": "^3.2.4",
96
96
  "diff-match-patch": "^1.0.5",
97
97
  "dotenv": "^16.4.5",
98
- "express": "^4.19.1",
98
+ "express": "^4.19.2",
99
99
  "ignore": "^5.3.1",
100
100
  "json0-ot-diff": "^1.1.2",
101
101
  "ngrok": "^4.3.3",
@@ -112,14 +112,14 @@
112
112
  "ws": "^8.16.0"
113
113
  },
114
114
  "devDependencies": {
115
- "@types/react": "^18.2.67",
115
+ "@types/react": "^18.2.71",
116
116
  "@types/react-dom": "^18.2.22",
117
117
  "@vitejs/plugin-react": "^4.2.1",
118
118
  "concurrently": "^8.2.2",
119
119
  "prettier": "^3.2.5",
120
120
  "sass": "^1.72.0",
121
121
  "typescript": "^5.4.3",
122
- "vite": "^5.2.3",
122
+ "vite": "^5.2.6",
123
123
  "vitest": "^1.4.0"
124
124
  }
125
125
  }
@@ -1,12 +1,6 @@
1
1
  import { useCallback, useContext, useState } from 'react';
2
- import {
3
- FileId,
4
- ShareDBDoc,
5
- VZCodeContent,
6
- } from '../../../types';
7
2
  import { OverlayTrigger, Tooltip } from '../../bootstrap';
8
- import { EditorCache } from '../../useEditorCache';
9
- import { TabState } from '../../vzReducer';
3
+ import { VZCodeContext } from '../../VZCodeContext';
10
4
  import {
11
5
  RequestId,
12
6
  generateRequestId,
@@ -14,30 +8,24 @@ import {
14
8
  import { SparklesSVG } from '../../Icons';
15
9
  import { startAIAssist } from '../startAIAssist';
16
10
  import { Spinner } from '../Spinner';
17
- import { VZCodeContext } from '../../VZCodeContext';
18
11
  import './style.scss';
19
12
 
20
13
  const enableStopGeneration = false;
21
14
 
22
15
  export const AIAssistWidget = ({
23
- activeFileId,
24
- shareDBDoc,
25
- editorCache,
26
- tabList,
27
16
  aiAssistEndpoint,
28
17
  aiAssistOptions,
29
18
  aiAssistTooltipText = 'Start AI Assist',
30
19
  aiAssistClickOverride,
31
20
  }: {
32
- activeFileId: FileId;
33
- shareDBDoc: ShareDBDoc<VZCodeContent>;
34
- editorCache: EditorCache;
35
- tabList: Array<TabState>;
36
21
  aiAssistEndpoint: string;
37
22
  aiAssistOptions: { [key: string]: string };
38
23
  aiAssistTooltipText?: string;
39
24
  aiAssistClickOverride?: () => void;
40
25
  }) => {
26
+ const { shareDBDoc, activeFileId, tabList, editorCache } =
27
+ useContext(VZCodeContext);
28
+
41
29
  // The stream ID of the most recent request.
42
30
  // * If `null`, no request has been made yet.
43
31
  // * If non-null, a request has been made, and the
@@ -39,6 +39,7 @@ import { typeScriptCompletions } from './typeScriptCompletions';
39
39
  import { typeScriptLinter } from './typeScriptLinter';
40
40
  import { keymap } from '@codemirror/view';
41
41
  import { basicSetup } from './basicSetup';
42
+ import { InteractRule } from '@replit/codemirror-interact';
42
43
 
43
44
  // Feature flag to enable TypeScript completions & TypeScript Linter.
44
45
  const enableTypeScriptCompletions = true;
@@ -89,6 +90,7 @@ export const getOrCreateEditor = ({
89
90
  editorCache,
90
91
  usernameRef,
91
92
  typeScriptWorker,
93
+ customInteractRules,
92
94
  }: {
93
95
  fileId: FileId;
94
96
 
@@ -116,6 +118,7 @@ export const getOrCreateEditor = ({
116
118
  usernameRef: React.MutableRefObject<Username>;
117
119
  aiAssistEndpoint?: string;
118
120
  typeScriptWorker: Worker;
121
+ customInteractRules?: Array<InteractRule>;
119
122
  }): EditorCacheValue => {
120
123
  // Cache hit
121
124
  if (editorCache.has(fileId)) {
@@ -234,7 +237,9 @@ export const getOrCreateEditor = ({
234
237
  // the boolean checkboxes. The color pickers are also tricky,
235
238
  // as they would also need to be able to handle `onInteractEnd`.
236
239
  // See https://github.com/replit/codemirror-interact/issues/14
237
- extensions.push(widgets({ onInteract }));
240
+ extensions.push(
241
+ widgets({ onInteract, customInteractRules }),
242
+ );
238
243
 
239
244
  extensions.push(highlightWidgets);
240
245
 
@@ -10,11 +10,16 @@ import { EditorCacheValue } from '../useEditorCache';
10
10
  import { getOrCreateEditor } from './getOrCreateEditor';
11
11
  import { VZCodeContext } from '../VZCodeContext';
12
12
  import './style.scss';
13
+ import { InteractRule } from '@replit/codemirror-interact';
13
14
 
14
15
  // The path in the ShareDB document where the files live.
15
16
  const filesPath = ['files'];
16
17
 
17
- export const CodeEditor = () => {
18
+ export const CodeEditor = ({
19
+ customInteractRules,
20
+ }: {
21
+ customInteractRules?: Array<InteractRule>;
22
+ }) => {
18
23
  const {
19
24
  activeFileId,
20
25
  shareDBDoc,
@@ -79,6 +84,7 @@ export const CodeEditor = () => {
79
84
  editorCache,
80
85
  usernameRef,
81
86
  typeScriptWorker,
87
+ customInteractRules,
82
88
  }),
83
89
  [
84
90
  activeFileId,
@@ -1,7 +1,9 @@
1
1
  // TODO move back to this way of importing when this PR is merged:
2
2
  // https://github.com/replit/codemirror-interact/pull/19/files
3
3
  // import interact from '@replit/codemirror-interact';
4
- import interact from './codemirror-interact';
4
+ import interact, {
5
+ InteractRule,
6
+ } from './codemirror-interact';
5
7
 
6
8
  import {
7
9
  ViewPlugin,
@@ -11,6 +13,9 @@ import {
11
13
  import { Extension, RangeSet } from '@codemirror/state';
12
14
  import { EditorView } from 'codemirror';
13
15
 
16
+ // Regular expression for hex colors.
17
+ const colorRegex = /#[0-9A-Fa-f]{6}/g;
18
+
14
19
  // Interactive code widgets.
15
20
  // * Number dragger
16
21
  // * Boolean toggler
@@ -21,181 +26,181 @@ import { EditorView } from 'codemirror';
21
26
  // `onInteract` is called when the user interacts with a widget.
22
27
  export const widgets = ({
23
28
  onInteract,
29
+ customInteractRules,
24
30
  }: {
25
31
  onInteract?: () => void;
26
- }) =>
27
- interact({
28
- rules: [
29
- // hex color picker
30
- // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L71
31
- // Extended to support single and double quotes
32
- {
33
- regexp: /["']\#([0-9]|[A-F]|[a-f]){6}["']/g,
34
- cursor: 'pointer',
35
- onClick(text, setText, e) {
36
- const res =
37
- /(?<quote>["'])(?<hex>\#([0-9]|[A-F]|[a-f]){6})\k<quote>/.exec(
38
- text,
32
+ customInteractRules?: Array<InteractRule>;
33
+ }) => {
34
+ const rules: Array<InteractRule> = [
35
+ // hex color picker
36
+ // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L71
37
+ // Works without quotes to support CSS.
38
+ {
39
+ regexp: colorRegex,
40
+ cursor: 'pointer',
41
+ onClick(
42
+ text: string,
43
+ setText: (newText: string) => void,
44
+ ) {
45
+ const startingColor: string = text;
46
+
47
+ const sel: HTMLInputElement =
48
+ document.createElement('input');
49
+ sel.type = 'color';
50
+ sel.value = startingColor.toLowerCase();
51
+
52
+ // valueIsUpper maintains the style of the user's code. It keeps the case of a-f the same case as the original.
53
+ const valueIsUpper: boolean =
54
+ startingColor.toUpperCase() === startingColor;
55
+
56
+ const updateHex = (e: Event) => {
57
+ const el: HTMLInputElement =
58
+ e.target as HTMLInputElement;
59
+ if (el.value) {
60
+ setText(
61
+ valueIsUpper
62
+ ? el.value.toUpperCase()
63
+ : el.value,
39
64
  );
40
- const startingColor = res.groups?.hex;
41
- const quoteType = res.groups?.quote;
42
-
43
- const sel = document.createElement('input');
44
- sel.type = 'color';
45
-
46
- sel.value = startingColor.toLowerCase();
47
-
48
- //valueIsUpper maintains style of user's code. It keeps the case of a-f the same case as the original."
49
- const valueIsUpper =
50
- startingColor.toUpperCase() === startingColor;
51
-
52
- const updateHex = (e: Event) => {
53
- const el = e.target as HTMLInputElement;
54
- if (onInteract) onInteract();
55
- if (el.value) {
56
- setText(
57
- `${quoteType}${
58
- valueIsUpper
59
- ? el.value.toUpperCase()
60
- : el.value
61
- }${quoteType}`,
62
- );
63
- }
64
- };
65
- sel.addEventListener('input', updateHex);
66
- sel.click();
67
- },
65
+ }
66
+ };
67
+ sel.addEventListener('input', updateHex);
68
+ sel.click();
68
69
  },
70
+ },
69
71
 
70
- // a rule for a number dragger
71
- {
72
- // the regexp matching the value
73
- regexp: /(?<!\#)-?\b\d+\.?\d*\b/g,
74
- // set cursor to "ew-resize" on hover
75
- cursor: 'ew-resize',
76
- // change number value based on mouse X movement on drag
77
- onDrag: (text, setText, e) => {
78
- if (onInteract) onInteract();
79
- const newVal = Number(text) + e.movementX;
80
- if (isNaN(newVal)) return;
81
- setText(newVal.toString());
82
- },
72
+ // a rule for a number dragger
73
+ {
74
+ // the regexp matching the value
75
+ regexp: /(?<!\#)-?\b\d+\.?\d*\b/g,
76
+ // set cursor to "ew-resize" on hover
77
+ cursor: 'ew-resize',
78
+ // change number value based on mouse X movement on drag
79
+ onDrag: (text, setText, e) => {
80
+ if (onInteract) onInteract();
81
+ const newVal = Number(text) + e.movementX;
82
+ if (isNaN(newVal)) return;
83
+ setText(newVal.toString());
83
84
  },
84
- // bool toggler
85
- {
86
- regexp: /true|false/g,
87
- cursor: 'pointer',
88
- onClick: (text, setText) => {
89
- if (onInteract) onInteract();
90
- switch (text) {
91
- case 'true':
92
- return setText('false');
93
- case 'false':
94
- return setText('true');
95
- }
96
- },
85
+ },
86
+ // bool toggler
87
+ {
88
+ regexp: /true|false/g,
89
+ cursor: 'pointer',
90
+ onClick: (text, setText) => {
91
+ if (onInteract) onInteract();
92
+ switch (text) {
93
+ case 'true':
94
+ return setText('false');
95
+ case 'false':
96
+ return setText('true');
97
+ }
97
98
  },
98
- // vec2 slider
99
- // Inspired by: https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L61
100
- {
101
- regexp:
102
- /vec2\(-?\b\d+\.?\d*\b\s*(,\s*-?\b\d+\.?\d*\b)?\)/g,
103
- cursor: 'move',
104
-
105
- onDrag: (text, setText, e) => {
106
- const res =
107
- /vec2\((?<x>-?\b\d+\.?\d*\b)\s*(,\s*(?<y>-?\b\d+\.?\d*\b))?\)/.exec(
108
- text,
109
- );
110
- const x = Number(res?.groups?.x);
111
- let y = Number(res?.groups?.y);
112
- if (isNaN(x)) return;
113
- if (isNaN(y)) y = x;
114
- setText(
115
- `vec2(${x + e.movementX}, ${y - e.movementY})`,
99
+ },
100
+ // vec2 slider
101
+ // Inspired by: https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L61
102
+ {
103
+ regexp:
104
+ /vec2\(-?\b\d+\.?\d*\b\s*(,\s*-?\b\d+\.?\d*\b)?\)/g,
105
+ cursor: 'move',
106
+
107
+ onDrag: (text, setText, e) => {
108
+ const res =
109
+ /vec2\((?<x>-?\b\d+\.?\d*\b)\s*(,\s*(?<y>-?\b\d+\.?\d*\b))?\)/.exec(
110
+ text,
116
111
  );
117
- },
112
+ const x = Number(res?.groups?.x);
113
+ let y = Number(res?.groups?.y);
114
+ if (isNaN(x)) return;
115
+ if (isNaN(y)) y = x;
116
+ setText(
117
+ `vec2(${x + e.movementX}, ${y - e.movementY})`,
118
+ );
118
119
  },
119
- // rgb color picker
120
- // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L71
121
- //TODO: create color picker for hsl colors
122
- {
123
- regexp: /rgb\(.*\)/g,
124
- cursor: 'pointer',
125
- onClick: (text, setText, e) => {
126
- const res =
127
- /rgb\((?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s*(?<b>\d+)\)/.exec(
128
- text,
129
- );
130
- const r = Number(res?.groups?.r);
131
- const g = Number(res?.groups?.g);
132
- const b = Number(res?.groups?.b);
133
-
134
- //sel will open the color picker when sel.click is called.
135
- const sel = document.createElement('input');
136
- sel.type = 'color';
137
-
138
- if (!isNaN(r + g + b))
139
- sel.value = rgb2Hex(r, g, b);
140
-
141
- const updateRGB = (e: Event) => {
142
- const el = e.target as HTMLInputElement;
143
- if (onInteract) onInteract();
144
-
145
- if (el.value) {
146
- const [r, g, b] = hex2RGB(el.value);
147
- setText(`rgb(${r}, ${g}, ${b})`);
148
- }
149
- sel.removeEventListener('change', updateRGB);
150
- };
120
+ },
121
+ // rgb color picker
122
+ // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L71
123
+ //TODO: create color picker for hsl colors
124
+ {
125
+ regexp: /rgb\(.*\)/g,
126
+ cursor: 'pointer',
127
+ onClick: (text, setText, e) => {
128
+ const res =
129
+ /rgb\((?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s*(?<b>\d+)\)/.exec(
130
+ text,
131
+ );
132
+ const r = Number(res?.groups?.r);
133
+ const g = Number(res?.groups?.g);
134
+ const b = Number(res?.groups?.b);
151
135
 
152
- sel.addEventListener('change', updateRGB);
153
- sel.click();
154
- },
136
+ //sel will open the color picker when sel.click is called.
137
+ const sel = document.createElement('input');
138
+ sel.type = 'color';
139
+
140
+ if (!isNaN(r + g + b)) sel.value = rgb2Hex(r, g, b);
141
+
142
+ const updateRGB = (e: Event) => {
143
+ const el = e.target as HTMLInputElement;
144
+ if (onInteract) onInteract();
145
+
146
+ if (el.value) {
147
+ const [r, g, b] = hex2RGB(el.value);
148
+ setText(`rgb(${r}, ${g}, ${b})`);
149
+ }
150
+ sel.removeEventListener('change', updateRGB);
151
+ };
152
+
153
+ sel.addEventListener('change', updateRGB);
154
+ sel.click();
155
155
  },
156
- // url clicker
157
- {
158
- regexp: /https?:\/\/[^ "]+/g,
159
- cursor: 'pointer',
160
- onClick: (text) => {
161
- window.open(text);
162
- },
156
+ },
157
+ // url clicker
158
+ {
159
+ regexp: /https?:\/\/[^ ")]+/g,
160
+ cursor: 'pointer',
161
+ onClick: (text) => {
162
+ window.open(text);
163
163
  },
164
+ },
164
165
 
165
- //Set rotation to the angle between the x-axis and a line from the word "rotate" to the mouse pointer (while dragging).
166
- //The rotation is in range (-180,180]
167
- {
168
- regexp: /rotate\(-?\d*\.?\d*\)/g,
169
- cursor: 'move',
170
- onDragStart(text, setText, e) {
171
- rotationOrigin = { x: e.clientX, y: e.clientY };
172
- },
166
+ //Set rotation to the angle between the x-axis and a line from the word "rotate" to the mouse pointer (while dragging).
167
+ //The rotation is in range (-180,180]
168
+ {
169
+ regexp: /rotate\(-?\d*\.?\d*\)/g,
170
+ cursor: 'move',
171
+ onDragStart(text, setText, e) {
172
+ rotationOrigin = { x: e.clientX, y: e.clientY };
173
+ },
173
174
 
174
- onDrag(text, setText, e) {
175
- if (rotationOrigin == null) return;
176
- const rotationDegree = Math.round(
177
- (Math.atan2(
178
- rotationOrigin.y - e.clientY,
179
- e.clientX - rotationOrigin.x,
180
- ) *
181
- 180) /
182
- Math.PI,
183
- );
184
- //Calculate the angle between the x axis and a line from where the user first clicks to the current location of the mouse.
185
- setText(`rotate(${rotationDegree})`);
186
- const updateDragEvent = new CustomEvent(
187
- 'updateRotateDrag',
188
- { detail: rotationDegree },
189
- );
175
+ onDrag(text, setText, e) {
176
+ if (rotationOrigin == null) return;
177
+ const rotationDegree = Math.round(
178
+ (Math.atan2(
179
+ rotationOrigin.y - e.clientY,
180
+ e.clientX - rotationOrigin.x,
181
+ ) *
182
+ 180) /
183
+ Math.PI,
184
+ );
185
+ //Calculate the angle between the x axis and a line from where the user first clicks to the current location of the mouse.
186
+ setText(`rotate(${rotationDegree})`);
187
+ const updateDragEvent = new CustomEvent(
188
+ 'updateRotateDrag',
189
+ { detail: rotationDegree },
190
+ );
190
191
 
191
- document.dispatchEvent(updateDragEvent);
192
- },
193
- onDragEnd(text, setText) {
194
- rotationOrigin = null;
195
- },
192
+ document.dispatchEvent(updateDragEvent);
193
+ },
194
+ onDragEnd() {
195
+ rotationOrigin = null;
196
196
  },
197
- ],
198
- });
197
+ },
198
+ ];
199
+ if (customInteractRules) {
200
+ rules.push(...customInteractRules);
201
+ }
202
+ return interact({ rules });
203
+ };
199
204
 
200
205
  let rotationOrigin: { x: number; y: number } = null;
201
206
 
@@ -244,7 +249,8 @@ export const colorsInTextPlugin: Extension = [
244
249
  }
245
250
  const hexColorOccurences = line.value.matchAll(
246
251
  // /\"\#([0-9]|[A-F]|[a-f]){6}\"/g,
247
- /["']\#([0-9]|[A-F]|[a-f]){6}["']/g,
252
+ // /["']\#([0-9]|[A-F]|[a-f]){6}["']/g,
253
+ colorRegex,
248
254
  );
249
255
  let hexOccurance = hexColorOccurences.next();
250
256
  while (!hexOccurance.done) {
@@ -260,9 +266,9 @@ export const colorsInTextPlugin: Extension = [
260
266
  return Decoration.set(
261
267
  colorInfos.map((colorInfo) => {
262
268
  return {
263
- // 9 is the length of the hex color string including quotation marks
264
- from: colorInfo.index + 9,
265
- to: colorInfo.index + 9,
269
+ // 7 is the length of the hex color string
270
+ from: colorInfo.index + 7,
271
+ to: colorInfo.index + 7,
266
272
  value: Decoration.widget({
267
273
  side: -1,
268
274
  widget: new ColorWidget(colorInfo[0]),
@@ -288,10 +294,10 @@ class ColorWidget extends WidgetType {
288
294
  return widget.color === this.color;
289
295
  }
290
296
 
291
- toDOM(view: EditorView): HTMLElement {
297
+ toDOM(): HTMLElement {
292
298
  const parent = document.createElement('div');
293
299
  const size = 20;
294
- const innerSize = 16;
300
+ const innerSize = 14;
295
301
 
296
302
  parent.setAttribute(
297
303
  'style',
@@ -311,7 +317,7 @@ class ColorWidget extends WidgetType {
311
317
  'fill',
312
318
  this.color.replace(/["']/g, ''),
313
319
  );
314
- colorCircle.setAttributeNS(null, 'stroke', 'black');
320
+ // colorCircle.setAttributeNS(null, 'stroke', 'black');
315
321
  colorCircle.setAttributeNS(
316
322
  null,
317
323
  'r',
@@ -1,26 +1,20 @@
1
- import type { TabState } from '../vzReducer';
2
- import type { FileId, Files } from '../../types';
1
+ import { useContext } from 'react';
2
+ import { VZCodeContext } from '../VZCodeContext';
3
+ import { TabState } from '../vzReducer';
3
4
  import { Tab } from './Tab';
4
5
  import './style.scss';
5
6
 
6
7
  // Displays the list of tabs above the code editor.
7
- export const TabList = ({
8
- files,
9
- tabList,
10
- activeFileId,
11
- setActiveFileId,
12
- openTab,
13
- closeTabs,
14
- createFile,
15
- }: {
16
- files: Files;
17
- tabList: Array<TabState>;
18
- activeFileId: FileId;
19
- setActiveFileId: (fileId: FileId) => void;
20
- openTab: (tabState: TabState) => void;
21
- closeTabs: (fileIds: FileId[]) => void;
22
- createFile: (fileName: string) => void;
23
- }) => {
8
+ export const TabList = () => {
9
+ const {
10
+ files,
11
+ activeFileId,
12
+ setActiveFileId,
13
+ tabList,
14
+ openTab,
15
+ closeTabs,
16
+ } = useContext(VZCodeContext);
17
+
24
18
  return (
25
19
  <div className="vz-tab-list">
26
20
  {files &&
@@ -5,7 +5,9 @@ import {
5
5
  useState,
6
6
  } from 'react';
7
7
  import {
8
+ FileId,
8
9
  Files,
10
+ ItemId,
9
11
  ShareDBDoc,
10
12
  SubmitOperation,
11
13
  Username,
@@ -111,6 +113,9 @@ export type VZCodeContextValue = {
111
113
  runCodeRef: React.MutableRefObject<null | (() => void)>;
112
114
 
113
115
  connected: boolean;
116
+
117
+ hoveredItemId: ItemId | null;
118
+ setHoveredItemId: (itemId: ItemId | null) => void;
114
119
  };
115
120
 
116
121
  export const VZCodeProvider = ({
@@ -275,6 +280,10 @@ export const VZCodeProvider = ({
275
280
  runCodeRef,
276
281
  });
277
282
 
283
+ // Track the currently hovered file id.
284
+ const [hoveredItemId, setHoveredItemId] =
285
+ useState<ItemId | null>(null);
286
+
278
287
  // The value provided by this context.
279
288
  const value: VZCodeContextValue = {
280
289
  content,
@@ -328,6 +337,9 @@ export const VZCodeProvider = ({
328
337
  runCodeRef,
329
338
 
330
339
  connected,
340
+
341
+ hoveredItemId,
342
+ setHoveredItemId,
331
343
  };
332
344
 
333
345
  return (