id={deleteButtonId}
icon="trash"
tooltip={i18n.translate(I18nId.TreeItemDeleteTooltip, { fileName })}
- onClick={() => dispatch(explorerDeleteFile(fileName))}
+ onClick={() =>
+ dispatch(explorerDeleteFile(fileName, item.index as UUID))
+ }
/>
@@ -272,7 +275,7 @@ const renderTreeContainer: typeof renderers.renderTreeContainer = (props) => {
const handleDeleteKeyDown = useCallback(() => {
if (focusedItem !== undefined) {
const fileName = environment.getItemTitle(environment.items[focusedItem]);
- dispatch(explorerDeleteFile(fileName));
+ dispatch(explorerDeleteFile(fileName, focusedItem as UUID));
}
}, [environment]);
@@ -389,7 +392,9 @@ const FileTree: React.VoidFunctionComponent = () => {
canRename={false} // we implement our own rename handler
onFocusItem={(item) => setFocusedItem(item.index)}
onPrimaryAction={(item) =>
- dispatch(explorerUserActivateFile(item.data.fileName))
+ dispatch(
+ explorerUserActivateFile(item.data.fileName, item.index as UUID),
+ )
}
>
diff --git a/src/explorer/actions.ts b/src/explorer/actions.ts
index 488a9266..b4fc76dd 100644
--- a/src/explorer/actions.ts
+++ b/src/explorer/actions.ts
@@ -2,6 +2,7 @@
// Copyright (c) 2022 The Pybricks Authors
import { createAction } from '../actions';
+import { UUID } from '../fileStorage';
/**
* Request to archive (download) all files in the store.
*/
@@ -74,11 +75,15 @@ export const explorerDidFailToCreateNewFile = createAction((error: Error) => ({
/**
* Request to activate a file (open or bring to foreground if already open).
* @param fileName The file name.
+ * @param uuid The file metadata UUID.
*/
-export const explorerUserActivateFile = createAction((fileName: string) => ({
- type: 'explorer.user.action.activateFile',
- fileName,
-}));
+export const explorerUserActivateFile = createAction(
+ (fileName: string, uuid: UUID) => ({
+ type: 'explorer.user.action.activateFile',
+ fileName,
+ uuid,
+ }),
+);
/**
* Indicates that {@link explorerUserActivateFile} completed.
@@ -177,10 +182,12 @@ export const explorerDidFailToExportFile = createAction(
/**
* Action that requests to delete a file.
* @param fileName The file name.
+ * @param uuid The file metadata UUID.
*/
-export const explorerDeleteFile = createAction((fileName: string) => ({
+export const explorerDeleteFile = createAction((fileName: string, uuid: UUID) => ({
type: 'explorer.action.deleteFile',
fileName,
+ uuid,
}));
/**
diff --git a/src/explorer/sagas.test.ts b/src/explorer/sagas.test.ts
index 293ce5cb..66de7c1f 100644
--- a/src/explorer/sagas.test.ts
+++ b/src/explorer/sagas.test.ts
@@ -4,7 +4,7 @@
import * as browserFsAccess from 'browser-fs-access';
import { FileWithHandle } from 'browser-fs-access';
import { mock } from 'jest-mock-extended';
-import { AsyncSaga } from '../../test';
+import { AsyncSaga, uuid } from '../../test';
import { alertsShowAlert } from '../alerts/actions';
import {
editorActivateFile,
@@ -174,7 +174,7 @@ describe('handleExplorerImportFiles', () => {
fileStorageWriteFile(testFileName, testFileContents),
);
- saga.put(fileStorageDidWriteFile(testFileName));
+ saga.put(fileStorageDidWriteFile(testFileName, uuid(0)));
await expect(saga.take()).resolves.toEqual(explorerDidImportFiles());
@@ -237,9 +237,9 @@ describe('handleExplorerCreateNewFile', () => {
}
`);
- saga.put(fileStorageDidWriteFile('test.py'));
+ saga.put(fileStorageDidWriteFile('test.py', uuid(0)));
- await expect(saga.take()).resolves.toEqual(editorActivateFile('test.py'));
+ await expect(saga.take()).resolves.toEqual(editorActivateFile(uuid(0)));
await expect(saga.take()).resolves.toEqual(explorerDidCreateNewFile());
});
@@ -255,14 +255,14 @@ describe('handleExplorerActivateFile', () => {
beforeEach(async () => {
saga = new AsyncSaga(explorer);
- saga.put(explorerUserActivateFile('test.file'));
+ saga.put(explorerUserActivateFile('test.file', uuid(0)));
- await expect(saga.take()).resolves.toEqual(editorActivateFile('test.file'));
+ await expect(saga.take()).resolves.toEqual(editorActivateFile(uuid(0)));
});
it('should alert file in use error', async () => {
const testError = new EditorError('FileInUse', 'test error');
- saga.put(editorDidFailToActivateFile('test.file', testError));
+ saga.put(editorDidFailToActivateFile(uuid(0), testError));
await expect(saga.take()).resolves.toEqual(
alertsShowAlert('explorer', 'fileInUse', { fileName: 'test.file' }),
@@ -274,7 +274,7 @@ describe('handleExplorerActivateFile', () => {
it('should alert unexpected error', async () => {
const testError = new Error('test error');
- saga.put(editorDidFailToActivateFile('test.file', testError));
+ saga.put(editorDidFailToActivateFile(uuid(0), testError));
await expect(saga.take()).resolves.toEqual(
alertsShowAlert('alerts', 'unexpectedError', { error: testError }),
@@ -285,7 +285,7 @@ describe('handleExplorerActivateFile', () => {
});
it('should notify success', async () => {
- saga.put(editorDidActivateFile('test.file'));
+ saga.put(editorDidActivateFile(uuid(0)));
await expect(saga.take()).resolves.toEqual(
explorerUserDidActivateFile('test.file'),
@@ -455,7 +455,7 @@ describe('handleExplorerDeleteFile', () => {
beforeEach(async () => {
saga = new AsyncSaga(explorer);
- saga.put(explorerDeleteFile(testFile));
+ saga.put(explorerDeleteFile(testFile, uuid(0)));
await expect(saga.take()).resolves.toEqual(deleteFileAlertShow(testFile));
});
@@ -476,8 +476,8 @@ describe('handleExplorerDeleteFile', () => {
saga.put(deleteFileAlertDidAccept());
// should close the editor first
- await expect(saga.take()).resolves.toEqual(editorCloseFile(testFile));
- saga.put(editorDidCloseFile(testFile));
+ await expect(saga.take()).resolves.toEqual(editorCloseFile(uuid(0)));
+ saga.put(editorDidCloseFile(uuid(0)));
// then delete the file
await expect(saga.take()).resolves.toEqual(fileStorageDeleteFile(testFile));
diff --git a/src/explorer/sagas.ts b/src/explorer/sagas.ts
index e44eddfb..3cacef3a 100644
--- a/src/explorer/sagas.ts
+++ b/src/explorer/sagas.ts
@@ -214,7 +214,7 @@ function* handleExplorerCreateNewFile(): Generator {
),
);
- const { didFailToWrite } = yield* race({
+ const { didWrite, didFailToWrite } = yield* race({
didWrite: take(fileStorageDidWriteFile.when((a) => a.path === fileName)),
didFailToWrite: take(
fileStorageDidFailToWriteFile.when((a) => a.path === fileName),
@@ -225,7 +225,9 @@ function* handleExplorerCreateNewFile(): Generator {
throw didFailToWrite.error;
}
- yield* put(editorActivateFile(fileName));
+ defined(didWrite);
+
+ yield* put(editorActivateFile(didWrite.uuid));
yield* put(explorerDidCreateNewFile());
} catch (err) {
@@ -240,14 +242,12 @@ function* handleExplorerCreateNewFile(): Generator {
function* handleExplorerActivateFile(
action: ReturnType,
): Generator {
- yield* put(editorActivateFile(action.fileName));
+ yield* put(editorActivateFile(action.uuid));
const { didFailToActivate } = yield* race({
- didActivate: take(
- editorDidActivateFile.when((a) => a.fileName === action.fileName),
- ),
+ didActivate: take(editorDidActivateFile.when((a) => a.uuid === action.uuid)),
didFailToActivate: take(
- editorDidFailToActivateFile.when((a) => a.fileName === action.fileName),
+ editorDidFailToActivateFile.when((a) => a.uuid === action.uuid),
),
});
@@ -406,8 +406,8 @@ function* handleExplorerDeleteFile(action: ReturnType
// at this point we know the user accepted
// have to close editor before deleting, otherwise we get "in use" error
- yield* put(editorCloseFile(action.fileName));
- yield* take(editorDidCloseFile.when((a) => a.fileName === action.fileName));
+ yield* put(editorCloseFile(action.uuid));
+ yield* take(editorDidCloseFile.when((a) => a.uuid === action.uuid));
yield* put(fileStorageDeleteFile(action.fileName));
diff --git a/src/fileStorage/actions.ts b/src/fileStorage/actions.ts
index eaa1da98..7fabb305 100644
--- a/src/fileStorage/actions.ts
+++ b/src/fileStorage/actions.ts
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
+import type { monaco } from 'react-monaco-editor';
import { createAction } from '../actions';
-import { FileMetadata } from '.';
+import { FileMetadata, UUID } from '.';
/** File open modes. */
export type FileOpenMode = 'r' | 'w';
@@ -48,11 +49,13 @@ export const fileStorageOpen = createAction(
/**
* Action that indicates that {@link fileStorageOpen} succeeded.
* @param path The file path.
+ * @param uuid The UUID of the file metadata.
* @param fd The file descriptor.
*/
-export const fileStorageDidOpen = createAction((path: string, fd: FD) => ({
+export const fileStorageDidOpen = createAction((path: string, uuid: UUID, fd: FD) => ({
type: 'fileStorage.action.DidOpen',
path,
+ uuid,
fd,
}));
@@ -199,10 +202,12 @@ export const fileStorageWriteFile = createAction((path: string, contents: string
/**
* Indicates that {@link fileStorageWriteFile} succeeded.
* @param path: The file path.
+ * @param uuid: The UUID of the file metadata.
*/
-export const fileStorageDidWriteFile = createAction((path: string) => ({
+export const fileStorageDidWriteFile = createAction((path: string, uuid: UUID) => ({
type: 'fileStorage.action.didWriteFile',
path,
+ uuid,
}));
/**
@@ -341,3 +346,71 @@ export const fileStorageDidFailToDumpAllFiles = createAction((error: Error) => (
type: 'fileStorage.action.didFailToDumpAllFiles',
error,
}));
+
+export const fileStorageLoadTextFile = createAction((uuid: UUID) => ({
+ type: 'fileStorage.action.loadTextFile',
+ uuid,
+}));
+
+export const fileStorageDidLoadTextFile = createAction(
+ (
+ uuid: UUID,
+ value: string,
+ viewState: monaco.editor.ICodeEditorViewState | null,
+ ) => ({
+ type: 'fileStorage.action.didLoadTextFile',
+ uuid,
+ value,
+ viewState,
+ }),
+);
+
+export const fileStorageDidFailToLoadTextFile = createAction(
+ (uuid: UUID, error: Error) => ({
+ type: 'fileStorage.action.didFailToLoadTextFile',
+ uuid,
+ error,
+ }),
+);
+
+export const fileStorageStoreTextFileValue = createAction(
+ (uuid: UUID, value: string) => ({
+ type: 'fileStorage.action.storeTextFileValue',
+ uuid,
+ value,
+ }),
+);
+
+export const fileStorageDidStoreTextFileValue = createAction((uuid: UUID) => ({
+ type: 'fileStorage.action.didStoreTextFileValue',
+ uuid,
+}));
+
+export const fileStorageDidFailToStoreTextFileValue = createAction(
+ (uuid: UUID, error: Error) => ({
+ type: 'fileStorage.action.didFailToStoreTextFileValue',
+ uuid,
+ error,
+ }),
+);
+
+export const fileStorageStoreTextFileViewState = createAction(
+ (uuid: UUID, viewState: monaco.editor.ICodeEditorViewState | null) => ({
+ type: 'fileStorage.action.storeTextFileViewState',
+ uuid,
+ viewState,
+ }),
+);
+
+export const fileStorageDidStoreTextFileViewState = createAction((uuid: UUID) => ({
+ type: 'fileStorage.action.didStoreTextFileViewState',
+ uuid,
+}));
+
+export const fileStorageDidFailToStoreTextFileViewState = createAction(
+ (uuid: UUID, error: Error) => ({
+ type: 'fileStorage.action.didFailToStoreTextFileViewState',
+ uuid,
+ error,
+ }),
+);
diff --git a/src/fileStorage/index.ts b/src/fileStorage/index.ts
index 78efdf8e..f066cead 100644
--- a/src/fileStorage/index.ts
+++ b/src/fileStorage/index.ts
@@ -3,6 +3,7 @@
import 'dexie-observable';
import Dexie, { Table } from 'dexie';
+import type { monaco } from 'react-monaco-editor';
/** Type to avoid mixing UUID with regular string. */
export type UUID = string & { _uuidBrand: undefined };
@@ -19,6 +20,8 @@ export type FileMetadata = Readonly<{
path: string;
/** The SHA256 hash of the file contents. */
sha256: string;
+ /** The text editor view state. */
+ viewState: monaco.editor.ICodeEditorViewState | null;
}>;
/**
@@ -44,7 +47,7 @@ export class FileStorageDb extends Dexie {
constructor(databaseName: string) {
super(databaseName);
this.version(1).stores({
- metadata: '$$uuid, &path, sha256',
+ metadata: '$$uuid, &path, sha256, viewState',
_contents: 'path, contents',
});
}
diff --git a/src/fileStorage/sagas.test.ts b/src/fileStorage/sagas.test.ts
index 60d94b12..84390ef4 100644
--- a/src/fileStorage/sagas.test.ts
+++ b/src/fileStorage/sagas.test.ts
@@ -78,6 +78,7 @@ async function setUpTestFile(saga: AsyncSaga): Promise<[FileMetadata, string]> {
uuid: testFileId,
path: testFilePath,
sha256: testFileContentsSha256,
+ viewState: null,
};
saga.put(fileStorageOpen(testFilePath, 'w', true));
@@ -118,7 +119,12 @@ describe('initialize', () => {
// new storage backend
await expect(saga.take()).resolves.toEqual(
fileStorageDidInitialize([
- { uuid: uuid(0), path: 'main.py', sha256: oldProgramContentsSha256 },
+ {
+ uuid: uuid(0),
+ path: 'main.py',
+ sha256: oldProgramContentsSha256,
+ viewState: null,
+ },
]),
);
expect(localStorage.getItem(oldProgramKey)).toBeNull();
@@ -170,7 +176,7 @@ describe('open', () => {
saga.put(fileStorageOpen('test.file', 'w', true));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 0 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 0 as FD),
);
});
@@ -203,7 +209,7 @@ describe('open', () => {
saga.put(fileStorageOpen('test.file', mode, false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
});
});
@@ -214,13 +220,13 @@ describe('open', () => {
saga.put(fileStorageOpen('test.file', 'r', false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
saga.put(fileStorageOpen('test.file', 'r', false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 2 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 2 as FD),
);
});
@@ -230,7 +236,7 @@ describe('open', () => {
saga.put(fileStorageOpen('test.file', 'r', false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
saga.put(fileStorageOpen('test.file', 'w', false));
@@ -247,7 +253,7 @@ describe('open', () => {
saga.put(fileStorageOpen('test.file', 'w', true));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 0 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 0 as FD),
);
saga.put(fileStorageClose(0 as FD));
@@ -279,7 +285,7 @@ describe('read', () => {
saga.put(fileStorageOpen('test.file', mode, false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
saga.put(fileStorageRead(1 as FD));
@@ -297,7 +303,7 @@ describe('read', () => {
saga.put(fileStorageOpen('test.file', mode, false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
saga.put(fileStorageClose(1 as FD));
@@ -334,7 +340,7 @@ describe('write', () => {
saga.put(fileStorageOpen('test.file', 'w', false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
saga.put(fileStorageWrite(1 as FD, 'new contents'));
@@ -348,7 +354,7 @@ describe('write', () => {
saga.put(fileStorageOpen('test.file', 'r', false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
saga.put(fileStorageWrite(1 as FD, 'new contents'));
@@ -368,7 +374,7 @@ describe('write', () => {
saga.put(fileStorageOpen('test.file', mode, false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
saga.put(fileStorageClose(1 as FD));
@@ -416,7 +422,7 @@ describe('readFile', () => {
describe('should open file', () => {
beforeEach(async () => {
- saga.put(fileStorageDidOpen('test.file', 0 as FD));
+ saga.put(fileStorageDidOpen('test.file', uuid(0), 0 as FD));
await expect(saga.take()).resolves.toEqual(fileStorageRead(0 as FD));
});
@@ -483,7 +489,7 @@ describe('writeFile', () => {
describe('should open file', () => {
beforeEach(async () => {
- saga.put(fileStorageDidOpen('test.file', 0 as FD));
+ saga.put(fileStorageDidOpen('test.file', uuid(0), 0 as FD));
await expect(saga.take()).resolves.toEqual(
fileStorageWrite(0 as FD, contents),
@@ -515,7 +521,7 @@ describe('writeFile', () => {
saga.put(fileStorageDidClose(0 as FD));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidWriteFile('test.file'),
+ fileStorageDidWriteFile('test.file', uuid(0)),
);
});
});
@@ -549,7 +555,7 @@ describe('copyFile', () => {
it('should fail if new file is open', async () => {
saga.put(fileStorageOpen('new.file', 'w', true));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('new.file', 1 as FD),
+ fileStorageDidOpen('new.file', uuid(1), 1 as FD),
);
saga.put(fileStorageCopyFile('test.file', 'new.file'));
@@ -565,7 +571,7 @@ describe('copyFile', () => {
it('should fail if new file exists', async () => {
saga.put(fileStorageOpen('new.file', 'w', true));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('new.file', 1 as FD),
+ fileStorageDidOpen('new.file', uuid(1), 1 as FD),
);
saga.put(fileStorageClose(1 as FD));
@@ -617,7 +623,7 @@ describe('deleteFile', () => {
saga.put(fileStorageOpen('test.file', 'r', false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
saga.put(fileStorageDeleteFile('test.file'));
@@ -669,7 +675,7 @@ describe('renameFile', () => {
saga.put(fileStorageOpen('test.file', 'r', false));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen('test.file', 1 as FD),
+ fileStorageDidOpen('test.file', uuid(0), 1 as FD),
);
saga.put(fileStorageRenameFile('test.file', newPath));
@@ -688,7 +694,7 @@ describe('renameFile', () => {
saga.put(fileStorageOpen(newPath, 'w', true));
await expect(saga.take()).resolves.toEqual(
- fileStorageDidOpen(newPath, 1 as FD),
+ fileStorageDidOpen(newPath, uuid(1), 1 as FD),
);
saga.put(fileStorageRenameFile('test.file', newPath));
diff --git a/src/fileStorage/sagas.ts b/src/fileStorage/sagas.ts
index c4db64aa..859a5be7 100644
--- a/src/fileStorage/sagas.ts
+++ b/src/fileStorage/sagas.ts
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
+import Dexie from 'dexie';
import {
call,
fork,
@@ -27,24 +28,33 @@ import {
fileStorageDidFailToDeleteFile,
fileStorageDidFailToDumpAllFiles,
fileStorageDidFailToInitialize,
+ fileStorageDidFailToLoadTextFile,
fileStorageDidFailToOpen,
fileStorageDidFailToRead,
fileStorageDidFailToReadFile,
fileStorageDidFailToRenameFile,
+ fileStorageDidFailToStoreTextFileValue,
+ fileStorageDidFailToStoreTextFileViewState,
fileStorageDidFailToWrite,
fileStorageDidFailToWriteFile,
fileStorageDidInitialize,
+ fileStorageDidLoadTextFile,
fileStorageDidOpen,
fileStorageDidRead,
fileStorageDidReadFile,
fileStorageDidRenameFile,
+ fileStorageDidStoreTextFileValue,
+ fileStorageDidStoreTextFileViewState,
fileStorageDidWrite,
fileStorageDidWriteFile,
fileStorageDumpAllFiles,
+ fileStorageLoadTextFile,
fileStorageOpen,
fileStorageRead,
fileStorageReadFile,
fileStorageRenameFile,
+ fileStorageStoreTextFileValue,
+ fileStorageStoreTextFileViewState,
fileStorageWrite,
fileStorageWriteFile,
} from './actions';
@@ -115,6 +125,7 @@ function* handleOpen(
const key = await db.metadata.add((>{
path: action.path,
sha256,
+ viewState: null,
}) as FileMetadata);
await db._contents.put({ path: action.path, contents });
@@ -131,7 +142,7 @@ function* handleOpen(
openFds.set(fd, { mode: action.mode, uuid });
- yield* put(fileStorageDidOpen(action.path, fd));
+ yield* put(fileStorageDidOpen(action.path, uuid, fd));
yield* take(fileStorageClose.when((a) => a.fd === fd));
@@ -353,7 +364,7 @@ function* handleWriteFile(action: ReturnType): Gene
yield* take(fileStorageDidClose.when((a) => a.fd === didOpen.fd));
}
- yield* put(fileStorageDidWriteFile(action.path));
+ yield* put(fileStorageDidWriteFile(action.path, didOpen.uuid));
} catch (err) {
yield* put(fileStorageDidFailToWriteFile(action.path, ensureError(err)));
}
@@ -571,6 +582,91 @@ function* handleDumpAllFiles(db: FileStorageDb): Generator {
}
}
+function* handleLoadTextFile(
+ db: FileStorageDb,
+ action: ReturnType,
+): Generator {
+ try {
+ const { value, viewState } = yield* call(() =>
+ db.transaction('r', db.metadata, db._contents, async () => {
+ const metadata = await db.metadata.get(action.uuid);
+
+ if (!metadata) {
+ throw new Error(`file with uuid '${action.uuid}' not found`);
+ }
+
+ const content = await db._contents.get(metadata.path);
+
+ if (!content) {
+ throw new Error(`content for file '${metadata.path}' not found`);
+ }
+
+ return { value: content.contents, viewState: metadata.viewState };
+ }),
+ );
+
+ yield* put(fileStorageDidLoadTextFile(action.uuid, value, viewState));
+ } catch (err) {
+ yield* put(fileStorageDidFailToLoadTextFile(action.uuid, ensureError(err)));
+ }
+}
+
+function* handleStoreTextFileValue(
+ db: FileStorageDb,
+ action: ReturnType,
+): Generator {
+ try {
+ yield* call(() =>
+ db.transaction('rw', db.metadata, db._contents, async () => {
+ const metadata = await db.metadata.get(action.uuid);
+
+ if (!metadata) {
+ throw new Error(`file with uuid '${action.uuid}' not found`);
+ }
+
+ const sha256 = await Dexie.waitFor(sha256Digest(action.value));
+
+ await db.metadata.update(metadata.uuid, { sha256 });
+
+ await db._contents.put({ path: metadata.path, contents: action.value });
+ }),
+ );
+
+ yield* put(fileStorageDidStoreTextFileValue(action.uuid));
+ } catch (err) {
+ yield* put(
+ fileStorageDidFailToStoreTextFileValue(action.uuid, ensureError(err)),
+ );
+ }
+}
+
+function* handleStoreTextFileViewState(
+ db: FileStorageDb,
+ action: ReturnType,
+): Generator {
+ try {
+ yield* call(() =>
+ db.transaction('rw', db.metadata, async () => {
+ const metadata = await db.metadata.get(action.uuid);
+
+ if (!metadata) {
+ throw new Error(`file with uuid '${action.uuid}' not found`);
+ }
+
+ await db.metadata.update(metadata.uuid, {
+ viewState: action.viewState,
+ });
+ }),
+ );
+
+ yield* put(fileStorageDidStoreTextFileViewState(action.uuid));
+ } catch (err) {
+ yield* put(
+ fileStorageDidFailToStoreTextFileViewState(action.uuid, ensureError(err)),
+ );
+ }
+}
+
/**
* Initializes the storage backend.
*/
@@ -595,6 +691,7 @@ function* initialize(): Generator {
await db.metadata.add((>{
path: 'main.py',
sha256,
+ viewState: null,
}) as FileMetadata);
await db._contents.add({ path: 'main.py', contents: oldProgram });
@@ -622,6 +719,13 @@ function* initialize(): Generator {
yield* takeEvery(fileStorageDeleteFile, handleDeleteFile, db);
yield* takeEvery(fileStorageRenameFile, handleRenameFile, db);
yield* takeEvery(fileStorageDumpAllFiles, handleDumpAllFiles, db);
+ yield* takeEvery(fileStorageLoadTextFile, handleLoadTextFile, db);
+ yield* takeEvery(fileStorageStoreTextFileValue, handleStoreTextFileValue, db);
+ yield* takeEvery(
+ fileStorageStoreTextFileViewState,
+ handleStoreTextFileViewState,
+ db,
+ );
const files = yield* call(() => db.metadata.toArray());
diff --git a/src/notifications/sagas.test.ts b/src/notifications/sagas.test.ts
index cb5381bb..9b84ff99 100644
--- a/src/notifications/sagas.test.ts
+++ b/src/notifications/sagas.test.ts
@@ -9,7 +9,7 @@ import {
} from '@pybricks/firmware';
import { I18nManager } from '@shopify/react-i18n';
import { AnyAction } from 'redux';
-import { AsyncSaga } from '../../test';
+import { AsyncSaga, uuid } from '../../test';
import { appDidCheckForUpdate } from '../app/actions';
import { bleDIServiceDidReceiveFirmwareRevision } from '../ble-device-info-service/actions';
import {
@@ -17,6 +17,7 @@ import {
didFailToConnect as bleDidFailToConnect,
} from '../ble/actions';
import { editorDidFailToOpenFile } from '../editor/actions';
+import { EditorError } from '../editor/error';
import {
explorerDidFailToArchiveAllFiles,
explorerDidFailToCreateNewFile,
@@ -116,7 +117,7 @@ test.each([
explorerDidFailToDuplicateFile('test.file', new Error('test error')),
explorerDidFailToExportFile('test.file', new Error('test error')),
explorerDidFailToDeleteFile('test.file', new Error('test error')),
- editorDidFailToOpenFile('test.file', new Error('test error')),
+ editorDidFailToOpenFile(uuid(0), new Error('test error')),
])('actions that should show notification: %o', async (action: AnyAction) => {
const { toaster, saga } = createTestToasterSaga();
@@ -151,6 +152,7 @@ test.each([
'test.file',
new DOMException('test message', 'AbortError'),
),
+ editorDidFailToOpenFile(uuid(0), new EditorError('FileInUse', 'test error')),
])('actions that should not show a notification: %o', async (action: AnyAction) => {
const { toaster, saga } = createTestToasterSaga();