Skip to content

Shorten editor tab file paths #2383

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/commons/editor/EditorContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Editor, { EditorProps, EditorTabStateProps } from './Editor';
import EditorTabContainer from './tabs/EditorTabContainer';

type OwnProps = {
baseFilePath?: string;
isFolderModeEnabled: boolean;
activeEditorTabIndex: number | null;
setActiveEditorTabIndex: (activeEditorTabIndex: number | null) => void;
Expand Down Expand Up @@ -60,6 +61,7 @@ const createSourcecastEditorTab =

const EditorContainer: React.FC<EditorContainerProps> = (props: EditorContainerProps) => {
const {
baseFilePath,
isFolderModeEnabled,
activeEditorTabIndex,
setActiveEditorTabIndex,
Expand Down Expand Up @@ -87,6 +89,7 @@ const EditorContainer: React.FC<EditorContainerProps> = (props: EditorContainerP
<div className="editor-container">
{isFolderModeEnabled && (
<EditorTabContainer
baseFilePath={baseFilePath ?? ''}
activeEditorTabIndex={activeEditorTabIndex}
filePaths={filePaths}
setActiveEditorTabIndex={setActiveEditorTabIndex}
Expand Down
15 changes: 15 additions & 0 deletions src/commons/editor/__tests__/tabs/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getShortestUniqueFilePaths } from '../../tabs/utils';

describe('getShortestUniqueFilePaths', () => {
it('returns the shortest unique file paths', () => {
const filePaths = ['/dir/dir1/a.js', '/dir/dir2/a.js', '/dir/dir1/b.js'];
const shortenedFilePaths = getShortestUniqueFilePaths(filePaths);
expect(shortenedFilePaths).toEqual(['/dir1/a.js', '/dir2/a.js', '/b.js']);
});

it('works even when the number of path segments in a file path is less than the number of iterations', () => {
const filePaths = ['/a.js', '/dir/dir2/a.js'];
const shortenedFilePaths = getShortestUniqueFilePaths(filePaths);
expect(shortenedFilePaths).toEqual(['/a.js', '/dir2/a.js']);
});
});
16 changes: 13 additions & 3 deletions src/commons/editor/tabs/EditorTabContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import React from 'react';

import EditorTab from './EditorTab';
import { getShortestUniqueFilePaths } from './utils';

export type EditorTabContainerProps = {
baseFilePath: string;
filePaths: string[];
activeEditorTabIndex: number;
setActiveEditorTabIndex: (activeEditorTabIndex: number | null) => void;
removeEditorTabByIndex: (editorTabIndex: number) => void;
};

const EditorTabContainer: React.FC<EditorTabContainerProps> = (props: EditorTabContainerProps) => {
const { filePaths, activeEditorTabIndex, setActiveEditorTabIndex, removeEditorTabByIndex } =
props;
const {
baseFilePath,
filePaths,
activeEditorTabIndex,
setActiveEditorTabIndex,
removeEditorTabByIndex
} = props;

const handleHorizontalScroll = (e: React.WheelEvent<HTMLDivElement>) => {
e.currentTarget.scrollTo({
left: e.currentTarget.scrollLeft + e.deltaY
});
};

const relativeFilePaths = filePaths.map(filePath => filePath.replace(baseFilePath, ''));
const shortenedFilePaths = getShortestUniqueFilePaths(relativeFilePaths);

return (
<div className="editor-tab-container" onWheel={handleHorizontalScroll}>
{filePaths.map((filePath, index) => (
{shortenedFilePaths.map((filePath, index) => (
<EditorTab
key={index}
filePath={filePath}
Expand Down
78 changes: 78 additions & 0 deletions src/commons/editor/tabs/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Returns the shortest file paths that is uniquely identifiable among
* all open editor tabs. This is similar to how most code editors available
* handle the displaying of file names.
*
* For example, if there are 3 open editor tabs where the file names are
* exactly the same, we would need to display more of the file path to be
* able to distinguish which editor tab corresponds to which file. Given
* the following absolute file paths:
* - /a.js
* - /dir1/a.js
* - /dir1/dir2/a.js
* The shortest unique file paths will be:
* - /a.js
* - /dir1/a.js
* - /dir2/a.js
*
* @param originalFilePaths The file paths to be shortened.
*/
export const getShortestUniqueFilePaths = (originalFilePaths: string[]): string[] => {
// Store the unique shortened file paths as a mapping from the original file paths
// to the shortened file paths. This is necessary because the output of this function
// must preserve the original ordering of file paths.
const originalToUniqueShortenedFilePaths: Record<string, string> = {};
// Split each original file path into path segments and store the mapping from file
// path to path segments for O(1) lookup. Since we only deal with the BrowserFS file
// system, the path separator will always be '/'.
const filePathSegments: Record<string, string[]> = originalFilePaths.reduce(
(segments, filePath) => ({
...segments,
// It is necessary to remove empty segments to deal with the very first '/' in
// file paths.
[filePath]: filePath.split('/').filter(segment => segment !== '')
}),
{}
);

for (
let numOfPathSegments = 1;
// Keep looping while some original file paths have yet to be shortened.
Object.keys(originalToUniqueShortenedFilePaths).length < originalFilePaths.length;
numOfPathSegments++
) {
// Based on the number of path segments for the iteration, we construct the
// shortened file path. We then store the mapping from the shortened file path
// to any original file path which transforms into it.
const shortenedToOriginalFilePaths: Record<string, string[]> = Object.entries(
filePathSegments
).reduce((filePaths, [originalFilePath, filePathSegments]) => {
// Note that if there are fewer path segments than the number being sliced,
// all of the path segments will be returned without error.
const shortenedFilePath = '/' + filePathSegments.slice(-numOfPathSegments).join('/');
return {
...filePaths,
[shortenedFilePath]: (filePaths[shortenedFilePath] ?? []).concat(originalFilePath)
};
}, {});
// Each shortened file path that only has a single corresponding original file
// path is added to the unique shortened file paths record and their entry in
// the file path segments record is removed to prevent further processing.
Object.entries(shortenedToOriginalFilePaths).forEach(
([shortenedFilePath, originalFilePaths]) => {
if (originalFilePaths.length > 1) {
return;
}

const originalFilePath = originalFilePaths[0];
originalToUniqueShortenedFilePaths[originalFilePath] = shortenedFilePath;
// Remove the file path's segments from the next iteration.
delete filePathSegments[originalFilePath];
}
);
}

// Finally, we retrieve the unique shortened file paths while preserving the ordering
// of file paths.
return originalFilePaths.map(filePath => originalToUniqueShortenedFilePaths[filePath]);
};
3 changes: 2 additions & 1 deletion src/pages/fileSystem/createInBrowserFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { ApiError } from 'browserfs/dist/node/core/api_error';
import { Store } from 'redux';

import { setInBrowserFileSystem } from '../../commons/fileSystem/FileSystemActions';
import { BASE_PLAYGROUND_FILE_PATH } from '../playground/Playground';

export const createInBrowserFileSystem = (store: Store) => {
configure(
{
fs: 'MountableFileSystem',
options: {
'/playground': {
[BASE_PLAYGROUND_FILE_PATH]: {
fs: 'IndexedDB',
options: {
storeName: 'playground'
Expand Down
10 changes: 9 additions & 1 deletion src/pages/playground/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ export async function handleHash(hash: string, props: PlaygroundProps) {
}
}

export const BASE_PLAYGROUND_FILE_PATH = '/playground';

const Playground: React.FC<PlaygroundProps> = ({ workspaceLocation = 'playground', ...props }) => {
const { isSicpEditor } = props;
const { isMobileBreakpoint } = useResponsive();
Expand Down Expand Up @@ -817,6 +819,7 @@ const Playground: React.FC<PlaygroundProps> = ({ workspaceLocation = 'playground
const editorContainerProps: NormalEditorContainerProps = {
..._.pick(props, 'editorSessionId', 'isEditorAutorun'),
editorVariant: 'normal',
baseFilePath: BASE_PLAYGROUND_FILE_PATH,
isFolderModeEnabled,
activeEditorTabIndex,
setActiveEditorTabIndex,
Expand Down Expand Up @@ -888,7 +891,12 @@ const Playground: React.FC<PlaygroundProps> = ({ workspaceLocation = 'playground
? [
{
label: 'Folder',
body: <FileSystemView workspaceLocation="playground" basePath="/playground" />,
body: (
<FileSystemView
workspaceLocation="playground"
basePath={BASE_PLAYGROUND_FILE_PATH}
/>
),
iconName: IconNames.FOLDER_CLOSE,
id: SideContentType.folder
}
Expand Down