diff --git a/src/editor/Editor.test.tsx b/src/editor/Editor.test.tsx index eb8fc371..66d31141 100644 --- a/src/editor/Editor.test.tsx +++ b/src/editor/Editor.test.tsx @@ -6,28 +6,43 @@ import { RenderResult, cleanup, fireEvent, waitFor } from '@testing-library/reac import userEvent from '@testing-library/user-event'; import React from 'react'; import { monaco } from 'react-monaco-editor'; -import { testRender } from '../../test'; +import { testRender, uuid } from '../../test'; +import { FileMetadata } from '../fileStorage'; +import { useFileStorageMetadata, useFileStoragePath } from '../fileStorage/hooks'; import { defined } from '../utils'; import Editor from './Editor'; import { editorActivateFile, editorCloseFile } from './actions'; +const testFile: FileMetadata = { + uuid: uuid(0), + path: 'test.file', + sha256: '', + viewState: null, +}; + describe('Editor', () => { describe('tabs', () => { it('should dispatch activate action when tab is clicked', async () => { + jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]); + jest.mocked(useFileStoragePath).mockReturnValue(testFile.path); + const [editor, dispatch] = testRender(, { - editor: { openFiles: ['test.file'] }, + editor: { openFileUuids: [testFile.uuid] }, }); userEvent.click(editor.getByRole('tab', { name: 'test.file' })); - expect(dispatch).toHaveBeenCalledWith(editorActivateFile('test.file')); + expect(dispatch).toHaveBeenCalledWith(editorActivateFile(testFile.uuid)); }); it.each(['enter', 'space'])( 'should dispatch activate action when % button is pressed', async (button) => { + jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]); + jest.mocked(useFileStoragePath).mockReturnValue(testFile.path); + const [editor, dispatch] = testRender(, { - editor: { openFiles: ['test.file'] }, + editor: { openFileUuids: [testFile.uuid] }, }); userEvent.type( @@ -35,28 +50,36 @@ describe('Editor', () => { `{${button}}`, ); - expect(dispatch).toHaveBeenCalledWith(editorActivateFile('test.file')); + expect(dispatch).toHaveBeenCalledWith( + editorActivateFile(testFile.uuid), + ); }, ); it('should dispatch close action when close button is clicked', async () => { + jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]); + jest.mocked(useFileStoragePath).mockReturnValue(testFile.path); + const [editor, dispatch] = testRender(, { - editor: { openFiles: ['test.file'] }, + editor: { openFileUuids: [testFile.uuid] }, }); userEvent.click(editor.getByRole('button', { name: 'Close test.file' })); - expect(dispatch).toHaveBeenCalledWith(editorCloseFile('test.file')); + expect(dispatch).toHaveBeenCalledWith(editorCloseFile(testFile.uuid)); }); it('should dispatch close action when delete button is pressed', async () => { + jest.mocked(useFileStorageMetadata).mockReturnValue([testFile]); + jest.mocked(useFileStoragePath).mockReturnValue(testFile.path); + const [editor, dispatch] = testRender(, { - editor: { openFiles: ['test.file'] }, + editor: { openFileUuids: [testFile.uuid] }, }); userEvent.type(editor.getByRole('tab', { name: 'test.file' }), '{delete}'); - expect(dispatch).toHaveBeenCalledWith(editorCloseFile('test.file')); + expect(dispatch).toHaveBeenCalledWith(editorCloseFile(testFile.uuid)); }); }); diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index bc012945..23374c31 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -18,7 +18,7 @@ import { import { ContextMenu2, ResizeSensor2 } from '@blueprintjs/popover2'; import tomorrowNightEightiesTheme from 'monaco-themes/themes/Tomorrow-Night-Eighties.json'; import xcodeTheme from 'monaco-themes/themes/Xcode_default.json'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useId } from 'react-aria'; import MonacoEditor, { EditorDidMount, @@ -28,6 +28,8 @@ import MonacoEditor, { import { useDispatch } from 'react-redux'; import { useTernaryDarkMode } from 'usehooks-ts'; import { IDisposable } from 'xterm'; +import { UUID } from '../fileStorage'; +import { useFileStoragePath } from '../fileStorage/hooks'; import { compile } from '../mpy/actions'; import { useSelector } from '../reducers'; import { useSettingIsShowDocsEnabled } from '../settings/hooks'; @@ -173,20 +175,77 @@ const EditorContextMenu: React.VoidFunctionComponent = ( ); }; +type FileNameProps = { + /** The DOM ID. */ + id: string; + /** The file UUID. */ + uuid: UUID; + /** Called when the file name changes. */ + onNameChanged: () => void; +}; + +const TabLabel: React.VoidFunctionComponent = ({ + id, + uuid, + onNameChanged, +}) => { + const fileName = useFileStoragePath(uuid); + + useEffect(() => { + onNameChanged?.(); + }, [fileName, onNameChanged]); + + return ( + + {fileName} + + ); +}; + +type TabCloseButtonProps = { + /** The file UUID. */ + uuid: UUID; +}; + +const TabCloseButton: React.VoidFunctionComponent = ({ uuid }) => { + const fileName = useFileStoragePath(uuid) ?? ''; + const dispatch = useDispatch(); + const i18n = useI18n(); + + return ( +