From 818865462e1e407b3c2b270382829e0a196e3c98 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 31 May 2022 17:21:29 -0500 Subject: [PATCH] editor: handle files by uuid This changes things up a bit so that files are handled by uuid instead of path. The allows for files to be renamed without breaking things. Also some improvements with persistence of the view state for each file is made. --- src/editor/Editor.test.tsx | 41 +++++-- src/editor/Editor.tsx | 116 ++++++++++++++----- src/editor/actions.ts | 65 +++++------ src/editor/lib.test.ts | 76 +++++++------ src/editor/lib.ts | 91 +++++++++------ src/editor/reducers.test.ts | 23 ++-- src/editor/reducers.ts | 21 ++-- src/editor/sagas.test.ts | 87 ++++++++------ src/editor/sagas.ts | 195 +++++++++++++++++++++++++------- src/error-log/sagas.ts | 11 ++ src/explorer/Explorer.test.tsx | 17 ++- src/explorer/Explorer.tsx | 11 +- src/explorer/actions.ts | 17 ++- src/explorer/sagas.test.ts | 24 ++-- src/explorer/sagas.ts | 18 +-- src/fileStorage/actions.ts | 79 ++++++++++++- src/fileStorage/index.ts | 5 +- src/fileStorage/sagas.test.ts | 46 ++++---- src/fileStorage/sagas.ts | 108 +++++++++++++++++- src/notifications/sagas.test.ts | 6 +- 20 files changed, 762 insertions(+), 295 deletions(-) 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 ( +