Skip to content

Commit 93ec868

Browse files
authored
Shorten editor tab file paths (#2383)
* Shorten editor tab file paths * Remove empty file path segments * Add test cases for getting the shortened unique file paths * Exclude the workspace base path from being displayed in editor tab * Add comments & better name variables in getShortestUniqueFilePaths * Fix value of default base file path We need to pass in an empty string instead of '/' because otherwise, the first slash of file paths would be removed and the result would not be file paths.
1 parent ca356c9 commit 93ec868

File tree

6 files changed

+120
-5
lines changed

6 files changed

+120
-5
lines changed

src/commons/editor/EditorContainer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Editor, { EditorProps, EditorTabStateProps } from './Editor';
1313
import EditorTabContainer from './tabs/EditorTabContainer';
1414

1515
type OwnProps = {
16+
baseFilePath?: string;
1617
isFolderModeEnabled: boolean;
1718
activeEditorTabIndex: number | null;
1819
setActiveEditorTabIndex: (activeEditorTabIndex: number | null) => void;
@@ -60,6 +61,7 @@ const createSourcecastEditorTab =
6061

6162
const EditorContainer: React.FC<EditorContainerProps> = (props: EditorContainerProps) => {
6263
const {
64+
baseFilePath,
6365
isFolderModeEnabled,
6466
activeEditorTabIndex,
6567
setActiveEditorTabIndex,
@@ -87,6 +89,7 @@ const EditorContainer: React.FC<EditorContainerProps> = (props: EditorContainerP
8789
<div className="editor-container">
8890
{isFolderModeEnabled && (
8991
<EditorTabContainer
92+
baseFilePath={baseFilePath ?? ''}
9093
activeEditorTabIndex={activeEditorTabIndex}
9194
filePaths={filePaths}
9295
setActiveEditorTabIndex={setActiveEditorTabIndex}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getShortestUniqueFilePaths } from '../../tabs/utils';
2+
3+
describe('getShortestUniqueFilePaths', () => {
4+
it('returns the shortest unique file paths', () => {
5+
const filePaths = ['/dir/dir1/a.js', '/dir/dir2/a.js', '/dir/dir1/b.js'];
6+
const shortenedFilePaths = getShortestUniqueFilePaths(filePaths);
7+
expect(shortenedFilePaths).toEqual(['/dir1/a.js', '/dir2/a.js', '/b.js']);
8+
});
9+
10+
it('works even when the number of path segments in a file path is less than the number of iterations', () => {
11+
const filePaths = ['/a.js', '/dir/dir2/a.js'];
12+
const shortenedFilePaths = getShortestUniqueFilePaths(filePaths);
13+
expect(shortenedFilePaths).toEqual(['/a.js', '/dir2/a.js']);
14+
});
15+
});

src/commons/editor/tabs/EditorTabContainer.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
import React from 'react';
22

33
import EditorTab from './EditorTab';
4+
import { getShortestUniqueFilePaths } from './utils';
45

56
export type EditorTabContainerProps = {
7+
baseFilePath: string;
68
filePaths: string[];
79
activeEditorTabIndex: number;
810
setActiveEditorTabIndex: (activeEditorTabIndex: number | null) => void;
911
removeEditorTabByIndex: (editorTabIndex: number) => void;
1012
};
1113

1214
const EditorTabContainer: React.FC<EditorTabContainerProps> = (props: EditorTabContainerProps) => {
13-
const { filePaths, activeEditorTabIndex, setActiveEditorTabIndex, removeEditorTabByIndex } =
14-
props;
15+
const {
16+
baseFilePath,
17+
filePaths,
18+
activeEditorTabIndex,
19+
setActiveEditorTabIndex,
20+
removeEditorTabByIndex
21+
} = props;
1522

1623
const handleHorizontalScroll = (e: React.WheelEvent<HTMLDivElement>) => {
1724
e.currentTarget.scrollTo({
1825
left: e.currentTarget.scrollLeft + e.deltaY
1926
});
2027
};
2128

29+
const relativeFilePaths = filePaths.map(filePath => filePath.replace(baseFilePath, ''));
30+
const shortenedFilePaths = getShortestUniqueFilePaths(relativeFilePaths);
31+
2232
return (
2333
<div className="editor-tab-container" onWheel={handleHorizontalScroll}>
24-
{filePaths.map((filePath, index) => (
34+
{shortenedFilePaths.map((filePath, index) => (
2535
<EditorTab
2636
key={index}
2737
filePath={filePath}

src/commons/editor/tabs/utils.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Returns the shortest file paths that is uniquely identifiable among
3+
* all open editor tabs. This is similar to how most code editors available
4+
* handle the displaying of file names.
5+
*
6+
* For example, if there are 3 open editor tabs where the file names are
7+
* exactly the same, we would need to display more of the file path to be
8+
* able to distinguish which editor tab corresponds to which file. Given
9+
* the following absolute file paths:
10+
* - /a.js
11+
* - /dir1/a.js
12+
* - /dir1/dir2/a.js
13+
* The shortest unique file paths will be:
14+
* - /a.js
15+
* - /dir1/a.js
16+
* - /dir2/a.js
17+
*
18+
* @param originalFilePaths The file paths to be shortened.
19+
*/
20+
export const getShortestUniqueFilePaths = (originalFilePaths: string[]): string[] => {
21+
// Store the unique shortened file paths as a mapping from the original file paths
22+
// to the shortened file paths. This is necessary because the output of this function
23+
// must preserve the original ordering of file paths.
24+
const originalToUniqueShortenedFilePaths: Record<string, string> = {};
25+
// Split each original file path into path segments and store the mapping from file
26+
// path to path segments for O(1) lookup. Since we only deal with the BrowserFS file
27+
// system, the path separator will always be '/'.
28+
const filePathSegments: Record<string, string[]> = originalFilePaths.reduce(
29+
(segments, filePath) => ({
30+
...segments,
31+
// It is necessary to remove empty segments to deal with the very first '/' in
32+
// file paths.
33+
[filePath]: filePath.split('/').filter(segment => segment !== '')
34+
}),
35+
{}
36+
);
37+
38+
for (
39+
let numOfPathSegments = 1;
40+
// Keep looping while some original file paths have yet to be shortened.
41+
Object.keys(originalToUniqueShortenedFilePaths).length < originalFilePaths.length;
42+
numOfPathSegments++
43+
) {
44+
// Based on the number of path segments for the iteration, we construct the
45+
// shortened file path. We then store the mapping from the shortened file path
46+
// to any original file path which transforms into it.
47+
const shortenedToOriginalFilePaths: Record<string, string[]> = Object.entries(
48+
filePathSegments
49+
).reduce((filePaths, [originalFilePath, filePathSegments]) => {
50+
// Note that if there are fewer path segments than the number being sliced,
51+
// all of the path segments will be returned without error.
52+
const shortenedFilePath = '/' + filePathSegments.slice(-numOfPathSegments).join('/');
53+
return {
54+
...filePaths,
55+
[shortenedFilePath]: (filePaths[shortenedFilePath] ?? []).concat(originalFilePath)
56+
};
57+
}, {});
58+
// Each shortened file path that only has a single corresponding original file
59+
// path is added to the unique shortened file paths record and their entry in
60+
// the file path segments record is removed to prevent further processing.
61+
Object.entries(shortenedToOriginalFilePaths).forEach(
62+
([shortenedFilePath, originalFilePaths]) => {
63+
if (originalFilePaths.length > 1) {
64+
return;
65+
}
66+
67+
const originalFilePath = originalFilePaths[0];
68+
originalToUniqueShortenedFilePaths[originalFilePath] = shortenedFilePath;
69+
// Remove the file path's segments from the next iteration.
70+
delete filePathSegments[originalFilePath];
71+
}
72+
);
73+
}
74+
75+
// Finally, we retrieve the unique shortened file paths while preserving the ordering
76+
// of file paths.
77+
return originalFilePaths.map(filePath => originalToUniqueShortenedFilePaths[filePath]);
78+
};

src/pages/fileSystem/createInBrowserFileSystem.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { ApiError } from 'browserfs/dist/node/core/api_error';
33
import { Store } from 'redux';
44

55
import { setInBrowserFileSystem } from '../../commons/fileSystem/FileSystemActions';
6+
import { BASE_PLAYGROUND_FILE_PATH } from '../playground/Playground';
67

78
export const createInBrowserFileSystem = (store: Store) => {
89
configure(
910
{
1011
fs: 'MountableFileSystem',
1112
options: {
12-
'/playground': {
13+
[BASE_PLAYGROUND_FILE_PATH]: {
1314
fs: 'IndexedDB',
1415
options: {
1516
storeName: 'playground'

src/pages/playground/Playground.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ export async function handleHash(hash: string, props: PlaygroundProps) {
207207
}
208208
}
209209

210+
export const BASE_PLAYGROUND_FILE_PATH = '/playground';
211+
210212
const Playground: React.FC<PlaygroundProps> = ({ workspaceLocation = 'playground', ...props }) => {
211213
const { isSicpEditor } = props;
212214
const { isMobileBreakpoint } = useResponsive();
@@ -817,6 +819,7 @@ const Playground: React.FC<PlaygroundProps> = ({ workspaceLocation = 'playground
817819
const editorContainerProps: NormalEditorContainerProps = {
818820
..._.pick(props, 'editorSessionId', 'isEditorAutorun'),
819821
editorVariant: 'normal',
822+
baseFilePath: BASE_PLAYGROUND_FILE_PATH,
820823
isFolderModeEnabled,
821824
activeEditorTabIndex,
822825
setActiveEditorTabIndex,
@@ -888,7 +891,12 @@ const Playground: React.FC<PlaygroundProps> = ({ workspaceLocation = 'playground
888891
? [
889892
{
890893
label: 'Folder',
891-
body: <FileSystemView workspaceLocation="playground" basePath="/playground" />,
894+
body: (
895+
<FileSystemView
896+
workspaceLocation="playground"
897+
basePath={BASE_PLAYGROUND_FILE_PATH}
898+
/>
899+
),
892900
iconName: IconNames.FOLDER_CLOSE,
893901
id: SideContentType.folder
894902
}

0 commit comments

Comments
 (0)