diff --git a/src/commons/editor/EditorContainer.tsx b/src/commons/editor/EditorContainer.tsx index 8938864f19..c59b8f06e0 100644 --- a/src/commons/editor/EditorContainer.tsx +++ b/src/commons/editor/EditorContainer.tsx @@ -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; @@ -60,6 +61,7 @@ const createSourcecastEditorTab = const EditorContainer: React.FC = (props: EditorContainerProps) => { const { + baseFilePath, isFolderModeEnabled, activeEditorTabIndex, setActiveEditorTabIndex, @@ -87,6 +89,7 @@ const EditorContainer: React.FC = (props: EditorContainerP
{isFolderModeEnabled && ( { + 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']); + }); +}); diff --git a/src/commons/editor/tabs/EditorTabContainer.tsx b/src/commons/editor/tabs/EditorTabContainer.tsx index 52bc06cd58..b3c2fbe2f1 100644 --- a/src/commons/editor/tabs/EditorTabContainer.tsx +++ b/src/commons/editor/tabs/EditorTabContainer.tsx @@ -1,8 +1,10 @@ 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; @@ -10,8 +12,13 @@ export type EditorTabContainerProps = { }; const EditorTabContainer: React.FC = (props: EditorTabContainerProps) => { - const { filePaths, activeEditorTabIndex, setActiveEditorTabIndex, removeEditorTabByIndex } = - props; + const { + baseFilePath, + filePaths, + activeEditorTabIndex, + setActiveEditorTabIndex, + removeEditorTabByIndex + } = props; const handleHorizontalScroll = (e: React.WheelEvent) => { e.currentTarget.scrollTo({ @@ -19,9 +26,12 @@ const EditorTabContainer: React.FC = (props: EditorTabC }); }; + const relativeFilePaths = filePaths.map(filePath => filePath.replace(baseFilePath, '')); + const shortenedFilePaths = getShortestUniqueFilePaths(relativeFilePaths); + return (
- {filePaths.map((filePath, index) => ( + {shortenedFilePaths.map((filePath, index) => ( { + // 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 = {}; + // 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 = 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 = 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]); +}; diff --git a/src/pages/fileSystem/createInBrowserFileSystem.ts b/src/pages/fileSystem/createInBrowserFileSystem.ts index 8ad27ee8a2..7d61360c99 100644 --- a/src/pages/fileSystem/createInBrowserFileSystem.ts +++ b/src/pages/fileSystem/createInBrowserFileSystem.ts @@ -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' diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index df6f6372f5..f142d3aac8 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -207,6 +207,8 @@ export async function handleHash(hash: string, props: PlaygroundProps) { } } +export const BASE_PLAYGROUND_FILE_PATH = '/playground'; + const Playground: React.FC = ({ workspaceLocation = 'playground', ...props }) => { const { isSicpEditor } = props; const { isMobileBreakpoint } = useResponsive(); @@ -817,6 +819,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground const editorContainerProps: NormalEditorContainerProps = { ..._.pick(props, 'editorSessionId', 'isEditorAutorun'), editorVariant: 'normal', + baseFilePath: BASE_PLAYGROUND_FILE_PATH, isFolderModeEnabled, activeEditorTabIndex, setActiveEditorTabIndex, @@ -888,7 +891,12 @@ const Playground: React.FC = ({ workspaceLocation = 'playground ? [ { label: 'Folder', - body: , + body: ( + + ), iconName: IconNames.FOLDER_CLOSE, id: SideContentType.folder }