vzcode 0.5.0 → 0.7.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 +20 -5
  2. package/dist/assets/index-ccd52a89.css +1 -0
  3. package/dist/assets/index-d2d6b59b.js +89 -0
  4. package/dist/index.html +17 -3
  5. package/package.json +26 -21
  6. package/src/client/{App.jsx → App.tsx} +75 -120
  7. package/src/client/CodeEditor/getOrCreateEditor.ts +113 -0
  8. package/src/client/CodeEditor/index.tsx +50 -0
  9. package/src/client/{json1PresenceDisplay.js → CodeEditor/json1PresenceDisplay.js} +2 -2
  10. package/src/client/CodeEditor/style.scss +11 -0
  11. package/src/client/Settings.tsx +76 -0
  12. package/src/client/Sidebar/DirectoryListing.tsx +77 -0
  13. package/src/client/Sidebar/{File.jsx → FileListing.tsx} +14 -4
  14. package/src/client/Sidebar/Listing.tsx +44 -0
  15. package/src/client/Sidebar/index.tsx +81 -0
  16. package/src/client/Sidebar/styles.scss +84 -0
  17. package/src/client/TabList/index.tsx +82 -0
  18. package/src/client/TabList/style.scss +32 -0
  19. package/src/client/{getFileTree.js → getFileTree.ts} +6 -3
  20. package/src/client/main.jsx +1 -1
  21. package/src/client/{sortFileTree.js → sortFileTree.ts} +7 -3
  22. package/src/client/style.scss +49 -0
  23. package/src/client/themes.ts +31 -0
  24. package/src/client/{Sidebar/useOpenDirectories.js → useOpenDirectories.ts} +18 -14
  25. package/src/client/useTabsState.ts +51 -0
  26. package/src/server/index.js +0 -0
  27. package/src/types.ts +69 -0
  28. package/dist/assets/index-29b6a606.css +0 -1
  29. package/dist/assets/index-523ecad7.js +0 -89
  30. package/src/client/CodeEditor.jsx +0 -31
  31. package/src/client/Sidebar/Directory.jsx +0 -62
  32. package/src/client/Sidebar/FileOrDirectory.jsx +0 -33
  33. package/src/client/Sidebar/index.jsx +0 -64
  34. package/src/client/Sidebar/styles.css +0 -88
  35. package/src/client/getOrCreateEditor.js +0 -84
  36. package/src/client/settings.jsx +0 -141
  37. package/src/client/style.css +0 -120
  38. /package/src/client/{json1PresenceBroadcast.js → CodeEditor/json1PresenceBroadcast.js} +0 -0
@@ -0,0 +1,77 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { Item } from './Item';
3
+ import { Listing } from './Listing';
4
+ import { DirectoryArrowSVG } from './DirectoryArrowSVG';
5
+ import { FileTree, FileTreeFile } from '../../types';
6
+
7
+ export const DirectoryListing = ({
8
+ name,
9
+ path,
10
+ children,
11
+ handleRenameFileClick,
12
+ handleDeleteFileClick,
13
+ handleFileClick,
14
+ isDirectoryOpen,
15
+ toggleDirectory,
16
+ }: {
17
+ name: string;
18
+ path: string;
19
+ children: Array<FileTree | FileTreeFile>;
20
+ handleRenameFileClick: (fileId: string) => void;
21
+ handleDeleteFileClick: (fileId: string, event: React.MouseEvent) => void;
22
+ handleFileClick: (fileId: string) => void;
23
+ isDirectoryOpen: (path: string) => boolean;
24
+ toggleDirectory: (path: string) => void;
25
+ }) => {
26
+ const handleClick = useCallback(() => {
27
+ toggleDirectory(path);
28
+ }, [toggleDirectory]);
29
+
30
+ const handleDeleteClick = useCallback(() => {
31
+ console.log('TODO handleDeleteDirectoryClick');
32
+ }, []);
33
+
34
+ const handleRenameClick = useCallback(() => {
35
+ console.log('TODO handleRenameDirectoryClick');
36
+ }, []);
37
+
38
+ const isOpen = useMemo(() => isDirectoryOpen(path), [isDirectoryOpen, path]);
39
+
40
+ return (
41
+ <>
42
+ <Item
43
+ name={name}
44
+ handleClick={handleClick}
45
+ handleDeleteClick={handleDeleteClick}
46
+ handleRenameClick={handleRenameClick}
47
+ >
48
+ <div
49
+ className="arrow-wrapper"
50
+ style={{ transform: `rotate(${isOpen ? 90 : 0}deg)` }}
51
+ >
52
+ <DirectoryArrowSVG />
53
+ </div>
54
+ {name}
55
+ </Item>
56
+ {children && isOpen ? (
57
+ <div className="indentation">
58
+ {children.map((entity) => {
59
+ const { fileId } = entity as FileTreeFile;
60
+ const { path } = entity as FileTree;
61
+ return (
62
+ <Listing
63
+ entity={entity}
64
+ key={fileId || path}
65
+ handleRenameFileClick={handleRenameFileClick}
66
+ handleDeleteFileClick={handleDeleteFileClick}
67
+ handleFileClick={handleFileClick}
68
+ isDirectoryOpen={isDirectoryOpen}
69
+ toggleDirectory={toggleDirectory}
70
+ />
71
+ );
72
+ })}
73
+ </div>
74
+ ) : null}
75
+ </>
76
+ );
77
+ };
@@ -1,20 +1,30 @@
1
1
  import { useCallback } from 'react';
2
2
  import { Item } from './Item';
3
+ import { FileId } from '../../types';
3
4
 
4
- export const File = ({
5
+ export const FileListing = ({
5
6
  name,
6
7
  fileId,
7
8
  handleRenameFileClick,
8
9
  handleDeleteFileClick,
9
10
  handleFileClick,
11
+ }: {
12
+ name: string;
13
+ fileId: FileId;
14
+ handleRenameFileClick: (fileId: FileId) => void;
15
+ handleDeleteFileClick: (fileId: FileId, event: React.MouseEvent) => void;
16
+ handleFileClick: (fileId: FileId) => void;
10
17
  }) => {
11
18
  const handleClick = useCallback(() => {
12
19
  handleFileClick(fileId);
13
20
  }, [fileId, handleFileClick]);
14
21
 
15
- const handleDeleteClick = useCallback(() => {
16
- handleDeleteFileClick(fileId);
17
- }, [fileId]);
22
+ const handleDeleteClick = useCallback(
23
+ (event) => {
24
+ handleDeleteFileClick(fileId, event);
25
+ },
26
+ [fileId],
27
+ );
18
28
 
19
29
  const handleRenameClick = useCallback(() => {
20
30
  handleRenameFileClick(fileId);
@@ -0,0 +1,44 @@
1
+ import { DirectoryListing } from './DirectoryListing';
2
+ import { FileId, FileTree, FileTreeFile } from '../../types';
3
+ import { FileListing } from './FileListing';
4
+
5
+ // A "Listing" is a "FileListing" or a "DirectoryListing"
6
+ // that appears in the Sidebar.
7
+ export const Listing = ({
8
+ entity,
9
+ handleRenameFileClick,
10
+ handleDeleteFileClick,
11
+ handleFileClick,
12
+ isDirectoryOpen,
13
+ toggleDirectory,
14
+ }: {
15
+ entity: FileTree | FileTreeFile;
16
+ handleRenameFileClick: (fileId: FileId) => void;
17
+ handleDeleteFileClick: (fileId: FileId, event: React.MouseEvent) => void;
18
+ handleFileClick: (fileId: FileId) => void;
19
+ isDirectoryOpen: (path: string) => boolean;
20
+ toggleDirectory: (path: string) => void;
21
+ }) => {
22
+ const { name, file, fileId } = entity as FileTreeFile;
23
+ const { path, children } = entity as FileTree;
24
+ return file ? (
25
+ <FileListing
26
+ fileId={fileId}
27
+ name={name}
28
+ handleRenameFileClick={handleRenameFileClick}
29
+ handleDeleteFileClick={handleDeleteFileClick}
30
+ handleFileClick={handleFileClick}
31
+ />
32
+ ) : (
33
+ <DirectoryListing
34
+ name={name}
35
+ path={path}
36
+ children={children}
37
+ handleRenameFileClick={handleRenameFileClick}
38
+ handleDeleteFileClick={handleDeleteFileClick}
39
+ handleFileClick={handleFileClick}
40
+ isDirectoryOpen={isDirectoryOpen}
41
+ toggleDirectory={toggleDirectory}
42
+ />
43
+ );
44
+ };
@@ -0,0 +1,81 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { getFileTree } from '../getFileTree';
3
+ import { sortFileTree } from '../sortFileTree';
4
+ import { disableSettings } from '../featureFlags';
5
+ import { Listing } from './Listing';
6
+
7
+ import './styles.scss';
8
+ import { FileId, FileTree, FileTreeFile, Files } from '../../types';
9
+
10
+ export const Sidebar = ({
11
+ createFile,
12
+ files,
13
+ handleRenameFileClick,
14
+ handleDeleteFileClick,
15
+ handleFileClick,
16
+ setIsSettingsOpen,
17
+ isDirectoryOpen,
18
+ toggleDirectory,
19
+ }: {
20
+ files: Files;
21
+ createFile?: () => void;
22
+ handleRenameFileClick?: (fileId: FileId) => void;
23
+ handleDeleteFileClick?: (fileId: FileId, event: React.MouseEvent) => void;
24
+ handleFileClick?: (fileId: FileId) => void;
25
+ setIsSettingsOpen?: (isSettingsOpen: boolean) => void;
26
+ isDirectoryOpen?: (path: string) => boolean;
27
+ toggleDirectory?: (path: string) => void;
28
+ }) => {
29
+ const fileTree = useMemo(
30
+ () => (files ? sortFileTree(getFileTree(files)) : null),
31
+ [files],
32
+ );
33
+
34
+ const handleSettingsClick = useCallback(() => {
35
+ setIsSettingsOpen(true);
36
+ }, []);
37
+
38
+ return (
39
+ <div className="vz-sidebar">
40
+ <div className="files">
41
+ <div className="full-box">
42
+ <div>
43
+ <a className="link-name" href="#">
44
+ Files
45
+ </a>
46
+ </div>
47
+ <div>
48
+ <i
49
+ className="bx bxs-file-plus new-btn"
50
+ style={{ color: '#dbdde1' }}
51
+ onClick={createFile}
52
+ ></i>
53
+ </div>
54
+ </div>
55
+ {fileTree
56
+ ? fileTree.children.map((entity) => {
57
+ const { fileId } = entity as FileTreeFile;
58
+ const { path } = entity as FileTree;
59
+ const key = fileId ? fileId : path;
60
+ return (
61
+ <Listing
62
+ entity={entity}
63
+ key={key}
64
+ handleRenameFileClick={handleRenameFileClick}
65
+ handleDeleteFileClick={handleDeleteFileClick}
66
+ handleFileClick={handleFileClick}
67
+ isDirectoryOpen={isDirectoryOpen}
68
+ toggleDirectory={toggleDirectory}
69
+ />
70
+ );
71
+ })
72
+ : null}
73
+ </div>
74
+ {disableSettings ? null : (
75
+ <div className="settings" onClick={handleSettingsClick}>
76
+ Settings
77
+ </div>
78
+ )}
79
+ </div>
80
+ );
81
+ };
@@ -0,0 +1,84 @@
1
+ .vz-sidebar {
2
+ height: 100%;
3
+ width: 200px;
4
+ background: rgb(54, 65, 89);
5
+ display: flex;
6
+ flex-direction: column;
7
+ justify-content: space-between;
8
+
9
+ .full-box {
10
+ padding: 7px;
11
+ display: flex;
12
+ flex-direction: row;
13
+ align-items: center;
14
+ justify-content: space-between;
15
+ }
16
+
17
+ .files {
18
+ overflow: auto;
19
+ }
20
+
21
+ .file-or-directory {
22
+ display: flex;
23
+ flex-direction: row;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ cursor: pointer;
27
+ margin: 0 10px;
28
+ color: rgb(219, 221, 225);
29
+ border-radius: 5px;
30
+ }
31
+
32
+ .link-name {
33
+ margin-top: 30px;
34
+ margin-left: 10px;
35
+ color: rgb(154, 160, 172);
36
+ font-size: 12px;
37
+ text-decoration: none;
38
+ }
39
+
40
+ .new-btn {
41
+ justify-content: right;
42
+ margin-right: 10px;
43
+ cursor: pointer;
44
+ }
45
+
46
+ .utils {
47
+ display: flex;
48
+ }
49
+ .utilities {
50
+ margin-right: 3px;
51
+ }
52
+
53
+ .file-or-directory:hover,
54
+ .settings:hover {
55
+ background-color: rgb(74, 84, 105);
56
+ }
57
+
58
+ .name {
59
+ display: flex;
60
+ text-overflow: ellipsis;
61
+ white-space: nowrap;
62
+ overflow: hidden;
63
+ }
64
+
65
+ .indentation {
66
+ margin-left: 20px;
67
+ }
68
+
69
+ .arrow-wrapper {
70
+ margin-right: 4px;
71
+ }
72
+
73
+ .settings {
74
+ height: 30px;
75
+ color: rgb(219, 221, 225);
76
+ cursor: pointer;
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ padding: 14px 0;
81
+ margin: 10px;
82
+ border-radius: 5px;
83
+ }
84
+ }
@@ -0,0 +1,82 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { FileId, Files } from '../../types';
3
+ import './style.scss';
4
+
5
+ // Supports adding the file's containing folder to the tab name
6
+ const fileNameSplit = (fileName) => {
7
+ const split = fileName.split('/');
8
+ if (split.length === 1) return split[split.length - 1];
9
+ return split[split.length - 2] + '/' + split[split.length - 1];
10
+ };
11
+
12
+ const Tab = ({
13
+ fileId,
14
+ isActive,
15
+ setActiveFileId,
16
+ closeTab,
17
+ fileName,
18
+ }: {
19
+ fileId: FileId;
20
+ isActive: boolean;
21
+ setActiveFileId: (fileId: FileId) => void;
22
+ closeTab: (fileId: FileId) => void;
23
+ fileName: string;
24
+ }) => {
25
+ const handleCloseClick = useCallback(
26
+ (event: React.MouseEvent) => {
27
+ // Stop propagation so that the outer listener doesn't fire.
28
+ event.stopPropagation();
29
+
30
+ closeTab(fileId);
31
+ },
32
+ [closeTab, fileId],
33
+ );
34
+
35
+ const tabName = useMemo(() => fileNameSplit(fileName), [fileName]);
36
+
37
+ return (
38
+ <div
39
+ className={isActive ? 'tab active' : 'tab'}
40
+ onClick={() => {
41
+ setActiveFileId(fileId);
42
+ }}
43
+ >
44
+ {tabName}
45
+ <div
46
+ className={isActive ? 'bx bx-x tab-close' : ''}
47
+ onClick={handleCloseClick}
48
+ ></div>
49
+ </div>
50
+ );
51
+ };
52
+
53
+ // Displays the list of tabs above the code editor.
54
+ export const TabList = ({
55
+ files,
56
+ tabList,
57
+ activeFileId,
58
+ setActiveFileId,
59
+ closeTab,
60
+ }: {
61
+ files: Files;
62
+ tabList: FileId[];
63
+ activeFileId: FileId;
64
+ setActiveFileId: (fileId: FileId) => void;
65
+ closeTab: (fileId: FileId) => void;
66
+ }) => {
67
+ return (
68
+ <div className="vz-tab-list">
69
+ {files &&
70
+ tabList.map((fileId: FileId) => (
71
+ <Tab
72
+ key={fileId}
73
+ fileId={fileId}
74
+ isActive={fileId === activeFileId}
75
+ setActiveFileId={setActiveFileId}
76
+ closeTab={closeTab}
77
+ fileName={files[fileId].name}
78
+ />
79
+ ))}
80
+ </div>
81
+ );
82
+ };
@@ -0,0 +1,32 @@
1
+ .vz-tab-list {
2
+ display: flex;
3
+ background-color: rgb(37, 37, 38);
4
+
5
+ .tab {
6
+ background: rgb(45, 45, 45);
7
+ color: #fff;
8
+ font-family: var(--vzcode-font-family);
9
+ align-items: center;
10
+ display: flex;
11
+ padding-top: 2px;
12
+ padding-left: 10px;
13
+ padding-right: 10px;
14
+ border-top-left-radius: 10px;
15
+ border-top-right-radius: 10px;
16
+ margin-top: 5px;
17
+ margin-left: 2px;
18
+ margin-right: 2px;
19
+ cursor: pointer;
20
+
21
+ .tab-close {
22
+ margin-left: 5px;
23
+ }
24
+ .tab-close:hover {
25
+ border-radius: 30%;
26
+ background-color: rgb(37, 37, 38);
27
+ }
28
+ }
29
+ .tab.active {
30
+ background-color: rgb(41, 44, 52);
31
+ }
32
+ }
@@ -1,7 +1,10 @@
1
1
  // Inspired by
2
+
3
+ import { Files, FileTree } from '../types';
4
+
2
5
  // https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/FileTree/getFileTree.js
3
- export const getFileTree = (files) => {
4
- const tree = { name: 'files' };
6
+ export const getFileTree = (files: Files): FileTree => {
7
+ const tree: FileTree = { name: 'files' };
5
8
  for (const fileId of Object.keys(files)) {
6
9
  const file = files[fileId];
7
10
  const path = file.name.split('/');
@@ -11,7 +14,7 @@ export const getFileTree = (files) => {
11
14
  // Walk the path.
12
15
  for (let i = 0; i < n - 1; i++) {
13
16
  // Search for an existing child.
14
- let child;
17
+ let child: FileTree | undefined;
15
18
  const name = path[i];
16
19
  if (node.children) {
17
20
  for (let j = 0; j < node.children.length; j++) {
@@ -5,5 +5,5 @@ import App from './App';
5
5
  ReactDOM.createRoot(document.getElementById('root')).render(
6
6
  <React.StrictMode>
7
7
  <App />
8
- </React.StrictMode>
8
+ </React.StrictMode>,
9
9
  );
@@ -1,12 +1,16 @@
1
1
  // Recursively sorts the file tree in-place.
2
2
  // Sorts first to group directories before files.
3
3
  // Sorts second alphabetically.
4
+
5
+ import { FileTree, FileTreeFile } from '../types';
6
+
4
7
  // From https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/FileTree/sortFileTree.js
5
- export const sortFileTree = (fileTree) => {
8
+ export const sortFileTree = (fileTree: FileTree) => {
6
9
  if (fileTree.children) {
7
10
  fileTree.children.sort((a, b) => {
8
- const aIsFile = a.file ? 1 : 0;
9
- const bIsFile = b.file ? 1 : 0;
11
+ const aIsFile = (a as FileTreeFile).file ? 1 : 0;
12
+ const bIsFile = (b as FileTreeFile).file ? 1 : 0;
13
+ // Directories before files.
10
14
  return aIsFile - bIsFile || a.name.localeCompare(b.name);
11
15
  });
12
16
  fileTree.children.forEach(sortFileTree);
@@ -0,0 +1,49 @@
1
+ :root {
2
+ --vzcode-font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
3
+ Liberation Mono, monospace;
4
+ }
5
+
6
+ /* width */
7
+ ::-webkit-scrollbar {
8
+ width: 10px;
9
+ height: 10px;
10
+ }
11
+
12
+ /* Track */
13
+ ::-webkit-scrollbar-track {
14
+ background: rgb(41, 44, 52);
15
+ }
16
+
17
+ /* Handle */
18
+ ::-webkit-scrollbar-thumb {
19
+ background: rgb(37, 37, 38);
20
+ }
21
+
22
+ /* Handle on hover */
23
+ ::-webkit-scrollbar-thumb:hover {
24
+ background: #555;
25
+ }
26
+
27
+ html,
28
+ body,
29
+ #root,
30
+ .app {
31
+ height: 100%;
32
+ }
33
+ .app {
34
+ /* TODO use a variable for this */
35
+ background-color: rgb(41, 44, 52);
36
+
37
+ font-family: 'Poppins', sans-serif;
38
+ display: flex;
39
+
40
+ .left {
41
+ display: flex;
42
+ }
43
+ .right {
44
+ display: flex;
45
+ flex: 1;
46
+ flex-direction: column;
47
+ min-width: 0;
48
+ }
49
+ }
@@ -0,0 +1,31 @@
1
+ import { oneDark } from '@codemirror/theme-one-dark';
2
+ import { okaidia } from '@uiw/codemirror-theme-okaidia';
3
+ import { abcdef } from '@uiw/codemirror-theme-abcdef';
4
+ import { dracula } from '@uiw/codemirror-theme-dracula';
5
+ import { eclipse } from '@uiw/codemirror-theme-eclipse';
6
+ import { githubDark } from '@uiw/codemirror-theme-github';
7
+ import { material } from '@uiw/codemirror-theme-material';
8
+ import { nord } from '@uiw/codemirror-theme-nord';
9
+ import { xcodeLight } from '@uiw/codemirror-theme-xcode';
10
+
11
+ // CodeMirror themes
12
+ export const themes = [
13
+ { value: abcdef, label: 'abcdef' },
14
+ { value: dracula, label: 'darcula' },
15
+ { value: eclipse, label: 'eclipse' },
16
+ { value: material, label: 'material' },
17
+ { value: nord, label: 'nord' },
18
+ { value: oneDark, label: 'oneDark' },
19
+ { value: okaidia, label: 'okaidia' },
20
+ { value: githubDark, label: 'github' },
21
+ { value: xcodeLight, label: 'xcode' },
22
+ ];
23
+
24
+ // Map theme labels to theme values
25
+ export const themesByLabel = themes.reduce((acc, theme) => {
26
+ acc[theme.label] = theme.value;
27
+ return acc;
28
+ }, {});
29
+
30
+ // The default theme
31
+ export const defaultTheme = 'oneDark';
@@ -17,23 +17,27 @@ import { useState, useCallback } from 'react';
17
17
 
18
18
  // Inspired by
19
19
  // https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/useOpenDirectories.js
20
- export const useOpenDirectories = (activeFile) => {
21
- //const [openDirectories, setOpenDirectories] = useState(
22
- // initialOpenDirectories(activeFile)
23
- //);
24
- const [openDirectories, setOpenDirectories] = useState({});
20
+ export const useOpenDirectories = () => {
21
+ // The set of open directories by path/
22
+ const [openDirectories, setOpenDirectories] = useState<Set<string>>(
23
+ new Set(),
24
+ );
25
+
26
+ // Whether a directory is open.
27
+ const isDirectoryOpen: (path: string) => boolean = useCallback(
28
+ (path) => openDirectories.has(path),
29
+ [openDirectories],
30
+ );
25
31
 
26
- // TODO consider useReducer to avoid event listener churn
32
+ // Toggle whether a directory is open.
27
33
  const toggleDirectory = useCallback(
28
34
  (path) => {
29
- console.log(openDirectories[path], 'here');
30
- setOpenDirectories(
31
- Object.assign({}, openDirectories, {
32
- [path]: !openDirectories[path],
33
- })
34
- );
35
+ const newOpenDirectories = new Set(openDirectories);
36
+ newOpenDirectories[isDirectoryOpen(path) ? 'delete' : 'add'](path);
37
+ setOpenDirectories(newOpenDirectories);
35
38
  },
36
- [openDirectories]
39
+ [openDirectories, isDirectoryOpen],
37
40
  );
38
- return { openDirectories, toggleDirectory };
41
+
42
+ return { isDirectoryOpen, toggleDirectory };
39
43
  };
@@ -0,0 +1,51 @@
1
+ import { useCallback } from 'react';
2
+ import { FileId } from '../types';
3
+
4
+ // Encapsulates the state and logic for managing the list of open tabs.
5
+ export const useTabsState = (
6
+ activeFileId: FileId,
7
+ setActiveFileId: (fileId: FileId | null) => void,
8
+ tabList: Array<FileId>,
9
+ setTabList: (tabList: Array<FileId>) => void,
10
+ ) => {
11
+ // Called when a tab is closed.
12
+ const closeTab = useCallback(
13
+ (fileIdToRemove: FileId) => {
14
+ const i = tabList.findIndex((fileId) => fileId === fileIdToRemove);
15
+
16
+ // Support calling `closeTab` on a fileId that is not open,
17
+ // such as when a file is deleted..
18
+ if (i !== -1) {
19
+ // Remove the tab from the tab list.
20
+ const newTabList = [...tabList.slice(0, i), ...tabList.slice(i + 1)];
21
+ setTabList(newTabList);
22
+
23
+ // If we are closing the active file,
24
+ if (activeFileId === fileIdToRemove) {
25
+ // set the new active file to the next tab over,
26
+ if (newTabList.length > 0) {
27
+ setActiveFileId(i === 0 ? newTabList[i] : newTabList[i - 1]);
28
+ } else {
29
+ // or clear out the active file
30
+ // if we've closed the last tab.
31
+ setActiveFileId(null);
32
+ }
33
+ }
34
+ }
35
+ },
36
+ [tabList, activeFileId],
37
+ );
38
+
39
+ // Called when a tab is opened.
40
+ const openTab = useCallback(
41
+ (fileId: FileId) => {
42
+ setActiveFileId(fileId);
43
+ if (!tabList.includes(fileId)) {
44
+ setTabList([...tabList, fileId]);
45
+ }
46
+ },
47
+ [tabList],
48
+ );
49
+
50
+ return { tabList, closeTab, openTab };
51
+ };
File without changes