vzcode 1.3.0 → 1.4.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,7 +20,7 @@
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-BnoPNwcv.js"></script>
23
+ <script type="module" crossorigin src="/assets/index-wJsFLYuD.js"></script>
24
24
  <link rel="stylesheet" crossorigin href="/assets/index-ClPo_hzs.css">
25
25
  </head>
26
26
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -107,7 +107,7 @@
107
107
  "@codemirror/lang-markdown": "^6.2.5",
108
108
  "@codemirror/state": "^6.4.1",
109
109
  "@codemirror/theme-one-dark": "^6.1.2",
110
- "@codemirror/view": "^6.28.0",
110
+ "@codemirror/view": "^6.28.1",
111
111
  "@lezer/highlight": "^1.2.0",
112
112
  "@replit/codemirror-indentation-markers": "^6.5.2",
113
113
  "@replit/codemirror-interact": "^6.3.1",
@@ -136,9 +136,8 @@
136
136
  "json0-ot-diff": "^1.1.2",
137
137
  "ngrok": "^4.3.3",
138
138
  "open": "^10.1.0",
139
- "openai": "^4.50.0",
139
+ "openai": "^4.51.0",
140
140
  "prettier-plugin-svelte": "^3.2.4",
141
- "rainbowbrackets": "^2.0.2",
142
141
  "react": "^18.3.1",
143
142
  "react-bootstrap": "^2.10.2",
144
143
  "react-dom": "^18.3.1",
@@ -154,9 +153,9 @@
154
153
  "@vitejs/plugin-react": "^4.3.1",
155
154
  "concurrently": "^8.2.2",
156
155
  "prettier": "^3.3.2",
157
- "sass": "^1.77.4",
156
+ "sass": "^1.77.5",
158
157
  "typescript": "^5.4.5",
159
- "vite": "^5.2.13",
158
+ "vite": "^5.3.1",
160
159
  "vitest": "^1.6.0"
161
160
  },
162
161
  "optionalDependencies": {
@@ -39,7 +39,7 @@ import { typeScriptLinter } from './typeScriptLinter';
39
39
  import { keymap } from '@codemirror/view';
40
40
  import { basicSetup } from './basicSetup';
41
41
  import { InteractRule } from '@replit/codemirror-interact';
42
- import rainbowBrackets from 'rainbowbrackets';
42
+ import rainbowBrackets from '../CodeEditor/rainbowBrackets';
43
43
 
44
44
  // Feature flag to enable TypeScript completions & TypeScript Linter.
45
45
  const enableTypeScriptCompletions = true;
@@ -35,9 +35,10 @@ export const CodeEditor = ({
35
35
  editorNoLongerWantsFocus,
36
36
  typeScriptWorker,
37
37
  theme,
38
+ codeEditorRef,
38
39
  } = useContext(VZCodeContext);
39
40
 
40
- const ref = useRef<HTMLDivElement>(null);
41
+ const ref = codeEditorRef;
41
42
 
42
43
  // Set `doc.data.isInteracting` to `true` when the user is interacting
43
44
  // via interactive code widgets (e.g. Alt+drag), and `false` when they are not.
@@ -138,5 +139,11 @@ export const CodeEditor = ({
138
139
  editorNoLongerWantsFocus,
139
140
  ]);
140
141
 
141
- return <div className="vz-code-editor" ref={ref}></div>;
142
+ return (
143
+ <div
144
+ className="vz-code-editor"
145
+ ref={ref}
146
+ tabIndex={-1}
147
+ ></div>
148
+ );
142
149
  };
@@ -27,6 +27,9 @@ export const json1PresenceDisplay = ({
27
27
  // See https://codemirror.net/6/docs/ref/#view.Decoration
28
28
  decorations: RangeSet<Decoration>;
29
29
 
30
+ //Added variable for cursor position
31
+ cursorPosition = {};
32
+
30
33
  constructor(view: EditorView) {
31
34
  // Initialize decorations to empty array so CodeMirror doesn't crash.
32
35
  this.decorations = RangeSet.of([]);
@@ -36,8 +39,18 @@ export const json1PresenceDisplay = ({
36
39
  // * Values are presence objects as defined by ot-json1-presence
37
40
  const presenceState = {};
38
41
 
42
+ // Add the scroll event listener
43
+ //This runs for the arrow key scrolling, it should result in the users scrolling to eachother's location.
44
+ view.dom.addEventListener('scroll', () => {
45
+ this.scrollToCursor(view);
46
+ });
39
47
  // Receive remote presence changes.
40
48
  docPresence.on('receive', (id, presence) => {
49
+ if (debug)
50
+ console.log(
51
+ `Received presence for id ${id}`,
52
+ presence,
53
+ ); // Debug statement
41
54
  // If presence === null, the user has disconnected / exited
42
55
  // We also check if the presence is for the current file or not.
43
56
  if (presence && pathMatches(path, presence)) {
@@ -97,6 +110,13 @@ export const json1PresenceDisplay = ({
97
110
  }),
98
111
  });
99
112
  }
113
+ if (view.state.doc.length >= from) {
114
+ // Ensure position is valid
115
+ this.cursorPosition[id] = from; // Store the cursor position, important to run if we cant get the regular scroll to work
116
+ // console.log(`Stored cursor position for id ${id}: ${from}`); // Debug statement
117
+ } else {
118
+ // console.warn(`Invalid cursor position for id ${id}: ${from}`); // Debug statement
119
+ }
100
120
  }
101
121
 
102
122
  this.decorations = Decoration.set(
@@ -116,8 +136,21 @@ export const json1PresenceDisplay = ({
116
136
  annotations: [presenceAnnotation.of(true)],
117
137
  });
118
138
  }, 0);
139
+ //call to scroll to the cursor of the other user
140
+ this.scrollToCursor(view);
119
141
  });
120
142
  }
143
+ // Method to scroll the view to keep the cursor in view
144
+ scrollToCursor(view) {
145
+ for (const id in this.cursorPosition) {
146
+ //getting the cursor position of the other cursor
147
+ const cursorPos = this.cursorPosition[id];
148
+ view.dispatch({
149
+ //if the other person's cursor has jumped off screen, we will follow it by scrolling there directly.
150
+ effects: EditorView.scrollIntoView(cursorPos),
151
+ });
152
+ }
153
+ }
121
154
  },
122
155
  {
123
156
  decorations: (v) => v.decorations,
@@ -0,0 +1,110 @@
1
+ import {
2
+ EditorView,
3
+ Decoration,
4
+ ViewPlugin,
5
+ } from '@codemirror/view';
6
+
7
+ function generateColors() {
8
+ return ['red', 'orange', 'yellow', 'green'];
9
+ }
10
+
11
+ const rainbowBracketsPlugin = ViewPlugin.fromClass(
12
+ class {
13
+ decorations;
14
+
15
+ constructor(view) {
16
+ this.decorations = this.getBracketDecorations(view);
17
+ }
18
+
19
+ update(update) {
20
+ if (
21
+ update.docChanged ||
22
+ update.selectionSet ||
23
+ update.viewportChanged
24
+ ) {
25
+ this.decorations = this.getBracketDecorations(
26
+ update.view,
27
+ );
28
+ }
29
+ }
30
+
31
+ getBracketDecorations(view) {
32
+ const { doc } = view.state;
33
+ const decorations = [];
34
+ const stack = [];
35
+ const colors = generateColors();
36
+
37
+ for (let pos = 0; pos < doc.length; pos += 1) {
38
+ const char = doc.sliceString(pos, pos + 1);
39
+ if (char === '(' || char === '[' || char === '{') {
40
+ stack.push({ type: char, from: pos });
41
+ } else if (
42
+ char === ')' ||
43
+ char === ']' ||
44
+ char === '}'
45
+ ) {
46
+ const open = stack.pop();
47
+ if (
48
+ open &&
49
+ open.type === this.getMatchingBracket(char)
50
+ ) {
51
+ const color =
52
+ colors[stack.length % colors.length];
53
+ decorations.push(
54
+ Decoration.mark({
55
+ class: `rainbow-bracket-${color}`,
56
+ }).range(open.from, open.from + 1),
57
+ Decoration.mark({
58
+ class: `rainbow-bracket-${color}`,
59
+ }).range(pos, pos + 1),
60
+ );
61
+ }
62
+ }
63
+ }
64
+
65
+ decorations.sort(
66
+ (a, b) =>
67
+ a.from - b.from || a.startSide - b.startSide,
68
+ );
69
+
70
+ return Decoration.set(decorations);
71
+ }
72
+
73
+ // eslint-disable-next-line class-methods-use-this
74
+ getMatchingBracket(closingBracket) {
75
+ switch (closingBracket) {
76
+ case ')':
77
+ return '(';
78
+ case ']':
79
+ return '[';
80
+ case '}':
81
+ return '{';
82
+ default:
83
+ return null;
84
+ }
85
+ }
86
+ },
87
+ {
88
+ decorations: (v) => v.decorations,
89
+ },
90
+ );
91
+
92
+ export default function rainbowBrackets() {
93
+ return [
94
+ rainbowBracketsPlugin,
95
+ EditorView.baseTheme({
96
+ '.rainbow-bracket-red': { color: 'white' },
97
+ '.rainbow-bracket-red > span': { color: 'white' },
98
+ '.rainbow-bracket-orange': { color: '#FFFFB2' },
99
+ '.rainbow-bracket-orange > span': {
100
+ color: '#FFFFB2',
101
+ },
102
+ '.rainbow-bracket-yellow': { color: '#FFB2DA' },
103
+ '.rainbow-bracket-yellow > span': {
104
+ color: '#FFB2DA',
105
+ },
106
+ '.rainbow-bracket-green': { color: '#B2FFB2' },
107
+ '.rainbow-bracket-green > span': { color: '#B2FFB2' },
108
+ }),
109
+ ];
110
+ }
@@ -129,6 +129,8 @@ export type VZCodeContextValue = {
129
129
 
130
130
  sidebarRef: React.RefObject<HTMLDivElement>;
131
131
 
132
+ codeEditorRef: React.RefObject<HTMLDivElement>;
133
+
132
134
  connected: boolean;
133
135
 
134
136
  hoveredItemId: ItemId | null;
@@ -179,6 +181,8 @@ export const VZCodeProvider = ({
179
181
 
180
182
  const sidebarRef = useRef(null);
181
183
 
184
+ const codeEditorRef = useRef(null);
185
+
182
186
  // The error message shows either:
183
187
  // * `prettierError` - errors from Prettier, client-side only
184
188
  // * `codeError` - errors from an external source, such as
@@ -323,6 +327,7 @@ export const VZCodeProvider = ({
323
327
  runCodeRef,
324
328
  sidebarRef,
325
329
  editorCache,
330
+ codeEditorRef,
326
331
  });
327
332
 
328
333
  // Track the currently hovered file id.
@@ -390,6 +395,7 @@ export const VZCodeProvider = ({
390
395
  runPrettierRef,
391
396
  runCodeRef,
392
397
  sidebarRef,
398
+ codeEditorRef,
393
399
 
394
400
  connected,
395
401
 
@@ -26,7 +26,25 @@ export type ThemeOption = {
26
26
  value: any;
27
27
  label: ThemeLabel;
28
28
  };
29
-
29
+ // add light theme brackets to eclipse theme
30
+ // const modifiedEclipse = {
31
+ // ...eclipse,
32
+ // '.rainbow-bracket-red > span': {
33
+ // color: 'white',
34
+ // },
35
+ // '.rainbow-bracket-orange > span': {
36
+ // //yellow
37
+ // color: '#FFFFB2',
38
+ // },
39
+ // '.rainbow-bracket-yellow > span': {
40
+ // // orange
41
+ // color: '#FFB2DA',
42
+ // },
43
+ // '.rainbow-bracket-green > span': {
44
+ // // green
45
+ // color: '#B2FFB2',
46
+ // }
47
+ // };
30
48
  export const themes: Array<ThemeOption> = [
31
49
  { value: abcdef, label: 'abcdef' },
32
50
  { value: dracula, label: 'dracula' },
@@ -190,33 +190,6 @@ function vizhubThemeInit(
190
190
  backgroundColor: 'transparent',
191
191
  outline: '1px solid #ff2222',
192
192
  },
193
- '.rainbow-bracket-red > span': {
194
- color: 'white',
195
- },
196
- '.rainbow-bracket-orange > span': {
197
- // red
198
- color: '#FFB2B2',
199
- },
200
- '.rainbow-bracket-yellow > span': {
201
- // orange
202
- color: '#FFB2DA',
203
- },
204
- '.rainbow-bracket-green > span': {
205
- // yellow
206
- color: '#FFFFB2',
207
- },
208
- '.rainbow-bracket-blue > span': {
209
- // green
210
- color: '#B2FFB2',
211
- },
212
- '.rainbow-bracket-indigo > span': {
213
- // blue
214
- color: '#B2FFFF',
215
- },
216
- '.rainbow-bracket-violet > span': {
217
- // purple
218
- color: '#DAB2FF',
219
- }
220
193
  },
221
194
  { dark: true },
222
195
  ),
@@ -1,14 +1,22 @@
1
1
  import { useEffect } from 'react';
2
2
  import { shouldTriggerRun } from './shouldTriggerRun';
3
- import { EditorView } from 'codemirror';
4
3
  import { syntaxTree } from '@codemirror/language';
5
- import { SyntaxNode } from '@lezer/common';
6
- import {
7
- EditorState,
8
- SelectionRange,
9
- } from '@codemirror/state';
4
+ import { SyntaxNode, SyntaxNodeRef } from '@lezer/common';
5
+ import { EditorView } from '@codemirror/view';
6
+ import { EditorState } from '@codemirror/state';
10
7
 
11
- // Nesting types for the specific language
8
+ /*
9
+ The following is a helpful resource for the following Code Mirror Syntax Tree methods below
10
+ https://lezer.codemirror.net/docs/ref/#common
11
+ */
12
+
13
+ // Store the element in the current editor DOM to highlight, indicating a potential jump to definition is possible
14
+ let activeJumpingElement: HTMLSpanElement = null;
15
+
16
+ // Store the syntax node representing the destination within the syntax tree
17
+ let definingNode: SyntaxNode = null;
18
+
19
+ // Example nesting types for the specific language to find level in the syntax tree
12
20
  const nestingTypes = new Set<string>([
13
21
  'FunctionDeclaration',
14
22
  'ClassDeclaration',
@@ -22,9 +30,10 @@ const nestingTypes = new Set<string>([
22
30
  'CatchClause',
23
31
  'WithStatement',
24
32
  'ArrowFunction',
33
+ 'ImportGroup',
25
34
  ]);
26
35
 
27
- // Declaration types for the specific language
36
+ // Example declaration types for the specific language to find definitions in the syntax tree
28
37
  const declarationTypes = new Set<string>([
29
38
  'VariableDeclaration',
30
39
  'FunctionDeclaration',
@@ -46,10 +55,10 @@ const declarationTypes = new Set<string>([
46
55
  function getIdentifierContext(
47
56
  identifier: SyntaxNode,
48
57
  ): number {
49
- let levels = 0;
50
58
  let current: SyntaxNode = identifier;
59
+ let levels: number = 0;
51
60
 
52
- // Traverse up the tree to find the total depth
61
+ // Traverse up the tree to find the total depth, only counting valid nesting types
53
62
  while (current && current.type) {
54
63
  const parentType: string = current.type.name;
55
64
 
@@ -63,19 +72,18 @@ function getIdentifierContext(
63
72
  return levels;
64
73
  }
65
74
 
66
- function jumpToDefinition(editor: EditorView) {
67
- // Use current editor state and selection range to find a potential definition
75
+ function jumpToDefinition(
76
+ editor: EditorView,
77
+ node: SyntaxNode,
78
+ ): SyntaxNode {
68
79
  const state: EditorState = editor.state;
69
- const location: SelectionRange = state.selection.main;
70
80
  const definitions: Array<{
71
81
  identifier: SyntaxNode;
72
82
  context: number;
73
83
  }> = [];
74
84
 
75
- // Fetch the current syntax node information using cursor position
76
- const identifier: SyntaxNode = syntaxTree(
77
- state,
78
- ).resolveInner(location.from, -1);
85
+ // From an identifier in the syntax tree, fetch the name and context to find closest defining syntax node
86
+ const identifier: SyntaxNode = node;
79
87
  const identifierName: string = state.doc.sliceString(
80
88
  identifier.from,
81
89
  identifier.to,
@@ -84,13 +92,15 @@ function jumpToDefinition(editor: EditorView) {
84
92
 
85
93
  if (identifier) {
86
94
  syntaxTree(state).iterate({
87
- enter(tree) {
95
+ enter(tree: SyntaxNodeRef) {
88
96
  // Traverse syntax tree to find positions of respective identifier definitions within context
89
97
  if (
90
98
  declarationTypes.has(tree.name) ||
91
99
  nestingTypes.has(tree.name)
92
100
  ) {
93
101
  const parent: SyntaxNode = tree.node;
102
+
103
+ // Fetch a host of potential identifiers in an attempt to find the defining syntax node
94
104
  const children: Array<SyntaxNode> = [
95
105
  ...parent.getChildren('VariableDefinition'),
96
106
  ...parent.getChildren('PropertyDefinition'),
@@ -104,9 +114,10 @@ function jumpToDefinition(editor: EditorView) {
104
114
  child.to,
105
115
  );
106
116
 
117
+ // Ensure no jump to self by comparing syntax node id's
107
118
  if (
108
119
  name === identifierName &&
109
- child !== identifier
120
+ child.type.id !== identifier.type.id
110
121
  ) {
111
122
  definitions.push({
112
123
  identifier: child,
@@ -132,21 +143,10 @@ function jumpToDefinition(editor: EditorView) {
132
143
  }
133
144
  }
134
145
 
135
- // Set the cursor position to the valid declaration in current scope
136
- editor.dispatch({
137
- selection: {
138
- anchor: closestDefinition.from,
139
- head: closestDefinition.to,
140
- },
141
- scrollIntoView: true,
142
- effects: EditorView.scrollIntoView(
143
- closestDefinition.from,
144
- {
145
- y: 'center',
146
- },
147
- ),
148
- });
146
+ return closestDefinition;
149
147
  }
148
+
149
+ return null;
150
150
  }
151
151
  }
152
152
 
@@ -158,6 +158,7 @@ function jumpToDefinition(editor: EditorView) {
158
158
  // * Alt-PageUp: Change the active tab to the previous one
159
159
  // * Alt-PageDown: Change the active tab to the next one
160
160
  // * Ctrl-s or Shift-Enter: Run the code and format it with Prettier
161
+ // * Ctrl-Click: Jump to closest definition for a potential identifier
161
162
  export const useKeyboardShortcuts = ({
162
163
  closeTabs,
163
164
  activeFileId,
@@ -168,6 +169,7 @@ export const useKeyboardShortcuts = ({
168
169
  runCodeRef,
169
170
  sidebarRef,
170
171
  editorCache,
172
+ codeEditorRef,
171
173
  }) => {
172
174
  useEffect(() => {
173
175
  const handleKeyPress = (event: KeyboardEvent) => {
@@ -189,18 +191,11 @@ export const useKeyboardShortcuts = ({
189
191
  return;
190
192
  }
191
193
 
192
- if (event.ctrlKey) {
193
- const editor: EditorView =
194
- editorCache.get(activeFileId).editor;
195
- const jumpToDefinitionHandler = () => {
196
- jumpToDefinition(editor);
197
- };
198
-
199
- // CTRL + Click: Jump to relative definition
194
+ if (event.ctrlKey === true) {
195
+ // On holding CTRL key, search for a potential definition jump using mouse location
200
196
  document.addEventListener(
201
- 'mousedown',
202
- jumpToDefinitionHandler,
203
- { once: true },
197
+ 'mouseover',
198
+ handleMouseOver,
204
199
  );
205
200
  }
206
201
 
@@ -231,23 +226,149 @@ export const useKeyboardShortcuts = ({
231
226
  setActiveFileRight();
232
227
  return;
233
228
  }
234
- }
235
229
 
236
- if (event.ctrlKey == true) {
237
- if (event.key === '0') {
230
+ if (event.key === '1') {
238
231
  if (sidebarRef.current) {
239
232
  sidebarRef.current.focus();
240
233
  }
241
234
  }
235
+
236
+ if (event.key === '2') {
237
+ if (codeEditorRef.current) {
238
+ codeEditorRef.current.focus();
239
+ }
240
+ }
241
+ }
242
+ };
243
+
244
+ const resetActiveJumpingElement = (): void => {
245
+ if (activeJumpingElement) {
246
+ activeJumpingElement.style.cursor = 'initial';
247
+ activeJumpingElement.style.textDecoration = 'none';
248
+ activeJumpingElement = definingNode = null;
249
+ }
250
+
251
+ document.removeEventListener(
252
+ 'mouseover',
253
+ handleMouseOver,
254
+ );
255
+ document.removeEventListener(
256
+ 'mousedown',
257
+ jumpToDefinitionHandler,
258
+ );
259
+ };
260
+
261
+ const jumpToDefinitionHandler = (
262
+ event: MouseEvent,
263
+ ): void => {
264
+ // Ensure the current destination node is defined and current cursor position matches highlighted element
265
+ if (
266
+ !definingNode ||
267
+ (event.target as HTMLSpanElement) !==
268
+ activeJumpingElement
269
+ ) {
270
+ return;
271
+ }
272
+
273
+ // Move current cursor and center view in the editor to destination node
274
+ const editor: EditorView =
275
+ editorCache.get(activeFileId).editor;
276
+ const closestDefinition: SyntaxNode = definingNode;
277
+
278
+ editor.dispatch({
279
+ selection: {
280
+ anchor: closestDefinition.from,
281
+ head: closestDefinition.to,
282
+ },
283
+ scrollIntoView: true,
284
+ effects: EditorView.scrollIntoView(
285
+ closestDefinition.from,
286
+ {
287
+ y: 'center',
288
+ },
289
+ ),
290
+ });
291
+
292
+ resetActiveJumpingElement();
293
+ };
294
+
295
+ const handleKeyRelease = (event: KeyboardEvent) => {
296
+ // On releasing CTRL key, reset all active definition jumping elements and listeners
297
+ if (!event.ctrlKey) {
298
+ resetActiveJumpingElement();
299
+ }
300
+ };
301
+
302
+ const handleMouseOver = (event: MouseEvent) => {
303
+ const editor: EditorView =
304
+ editorCache.get(activeFileId).editor;
305
+ const tree = syntaxTree(editor.state);
306
+ const element = event.target as HTMLSpanElement;
307
+
308
+ // Ensure the identifier element can be found and is within the current editor DOM
309
+ if (
310
+ element == null ||
311
+ !editor.dom.contains(element)
312
+ ) {
313
+ return;
314
+ }
315
+
316
+ const position: number = editor.posAtDOM(element);
317
+ const identifier: SyntaxNode = tree.resolveInner(
318
+ position,
319
+ 1,
320
+ );
321
+
322
+ // All valid identifiers must be span elements to find a potential defining jump in editor
323
+ if (
324
+ identifier &&
325
+ element instanceof HTMLSpanElement
326
+ ) {
327
+ const potentialJump: SyntaxNode = jumpToDefinition(
328
+ editor,
329
+ identifier,
330
+ );
331
+
332
+ // Only allowing to jump to other definition nodes
333
+ if (
334
+ potentialJump &&
335
+ identifier.type.id !== potentialJump.type.id
336
+ ) {
337
+ if (activeJumpingElement) {
338
+ activeJumpingElement.style.cursor = 'initial';
339
+ activeJumpingElement.style.textDecoration =
340
+ 'none';
341
+ }
342
+
343
+ activeJumpingElement = element;
344
+ definingNode = potentialJump;
345
+
346
+ activeJumpingElement.style.cursor = 'pointer';
347
+ activeJumpingElement.style.textDecoration =
348
+ 'underline';
349
+
350
+ // CTRL + Click: Jump to relative definition, which is removed on CTRL key release
351
+ document.addEventListener(
352
+ 'mousedown',
353
+ jumpToDefinitionHandler,
354
+ { once: true },
355
+ );
356
+ }
242
357
  }
243
358
  };
244
359
 
245
360
  document.addEventListener('keydown', handleKeyPress);
361
+ document.addEventListener('keyup', handleKeyRelease);
362
+
246
363
  return () => {
247
364
  document.removeEventListener(
248
365
  'keydown',
249
366
  handleKeyPress,
250
367
  );
368
+ document.removeEventListener(
369
+ 'keyup',
370
+ handleKeyRelease,
371
+ );
251
372
  };
252
373
  }, [
253
374
  handleOpenCreateFileModal,