From edf48390e646626a4496fb166c3e8ca8dac951b9 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 18 Mar 2021 18:44:01 +0000 Subject: [PATCH 01/15] Test the fs layer and add file versions. Strip out the file size stuff for now. If we want it we want it to be a different event. https://github.com/microbit-foundation/python-editor-next/issues/21 --- src/App.tsx | 5 +- src/editor/EditorArea.tsx | 11 +- src/editor/EditorContainer.tsx | 7 +- src/editor/NonMainFileNotice.tsx | 7 +- src/files/FileRow.tsx | 5 +- src/files/FilesArea.tsx | 2 +- src/fs/fs-util.ts | 32 ++++ src/fs/fs.test.ts | 188 +++++++++++++++++++ src/fs/fs.ts | 269 ++++++++++++---------------- src/fs/micropython.ts | 33 ++++ src/fs/storage.ts | 96 ++++++---- src/project/HelpMenu.tsx | 2 +- src/project/Project.tsx | 2 +- src/project/ProjectNameEditable.tsx | 2 +- src/project/project-actions.tsx | 38 +++- src/project/project-hooks.tsx | 24 ++- src/workbench/LeftPanel.tsx | 12 +- src/workbench/Workbench.tsx | 34 +++- 18 files changed, 532 insertions(+), 237 deletions(-) create mode 100644 src/fs/fs.test.ts create mode 100644 src/fs/micropython.ts diff --git a/src/App.tsx b/src/App.tsx index 3c20e60e2..060abb5bb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,10 +17,13 @@ import Project from "./project/Project"; import ProjectDropTarget from "./project/ProjectDropTarget"; import { LoggingContext } from "./logging/logging-hooks"; import { DefaultLogging } from "./logging/default"; +import { fetchMicroPython } from "./fs/micropython"; const logging = new DefaultLogging(); const device = new MicrobitWebUSBConnection({ logging }); -const fs = new FileSystem(); +const fs = new FileSystem(logging, fetchMicroPython); +// If this fails then we retry on access. +fs.initializeInBackground(); const App = () => { useEffect(() => { diff --git a/src/editor/EditorArea.tsx b/src/editor/EditorArea.tsx index 4aed7585c..192159962 100644 --- a/src/editor/EditorArea.tsx +++ b/src/editor/EditorArea.tsx @@ -1,11 +1,12 @@ import { Box, Flex } from "@chakra-ui/react"; import { MAIN_FILE } from "../fs/fs"; +import { FileVersion } from "../fs/storage"; import EditorContainer from "./EditorContainer"; import NonMainFileNotice from "./NonMainFileNotice"; import ZoomControls from "./ZoomControls"; interface EditorAreaProps { - filename: string; + file: FileVersion; onSelectedFileChanged: (filename: string) => void; } @@ -13,13 +14,13 @@ interface EditorAreaProps { * Wrapper for the editor that integrates it with the app settings * and wires it to the currently open file. */ -const EditorArea = ({ filename, onSelectedFileChanged }: EditorAreaProps) => { - const isMainFile = filename === MAIN_FILE; +const EditorArea = ({ file, onSelectedFileChanged }: EditorAreaProps) => { + const isMainFile = file.name === MAIN_FILE; return ( {!isMainFile && ( )} @@ -34,7 +35,7 @@ const EditorArea = ({ filename, onSelectedFileChanged }: EditorAreaProps) => { pr={5} zIndex={1} /> - + ); diff --git a/src/editor/EditorContainer.tsx b/src/editor/EditorContainer.tsx index 5d5c6ff27..16ada8bfa 100644 --- a/src/editor/EditorContainer.tsx +++ b/src/editor/EditorContainer.tsx @@ -1,18 +1,19 @@ +import { FileVersion } from "../fs/storage"; import { useProjectFileText } from "../project/project-hooks"; import { useSettings } from "../settings/settings"; import Editor from "./codemirror/CodeMirror"; interface EditorContainerProps { - filename: string; + file: FileVersion; } /** * Container for the editor that integrates it with the app settings * and wires it to the currently open file. */ -const EditorContainer = ({ filename }: EditorContainerProps) => { +const EditorContainer = ({ file }: EditorContainerProps) => { const [{ fontSize, highlightCodeStructure }] = useSettings(); - const [defaultValue, onFileChange] = useProjectFileText(filename); + const [defaultValue, onFileChange] = useProjectFileText(file.name); return typeof defaultValue === "undefined" ? null : ( void; } @@ -13,14 +14,14 @@ interface NonMainFileNoticeProps extends BoxProps { * We offer an additional route back to editing the main document. */ const NonMainFileNotice = ({ - filename, + file, onSelectedFileChanged, ...props }: NonMainFileNoticeProps) => { return ( - Editing {filename}. + Editing {file.name}. + + ); +}; + +export default FileInputButton; diff --git a/src/files/FilesArea.tsx b/src/files/FilesArea.tsx index 58651d17d..dbdf2eb73 100644 --- a/src/files/FilesArea.tsx +++ b/src/files/FilesArea.tsx @@ -1,6 +1,7 @@ import { List, ListItem, VStack } from "@chakra-ui/react"; import OpenButton from "../project/OpenButton"; import { useProject } from "../project/project-hooks"; +import UploadButton from "../project/UploadButton"; import FileRow from "./FileRow"; interface FilesProps { @@ -25,7 +26,8 @@ const FilesArea = ({ onSelectedFileChanged }: FilesProps) => { ))} - + Open a project + Upload a file ); }; diff --git a/src/project/OpenButton.tsx b/src/project/OpenButton.tsx index ce5849382..5d3bc0dd2 100644 --- a/src/project/OpenButton.tsx +++ b/src/project/OpenButton.tsx @@ -1,58 +1,25 @@ -import { Button, ButtonProps, Input } from "@chakra-ui/react"; -import React, { useCallback, useRef } from "react"; -import { RiFolderOpenLine } from "react-icons/ri"; +import { ButtonProps } from "@chakra-ui/react"; +import React from "react"; +import FileInputButton from "../common/FileInputButton"; import { useProjectActions } from "./project-hooks"; -interface OpenButtonProps extends ButtonProps { - text?: string; -} +interface OpenButtonProps extends ButtonProps {} /** * Open HEX button, with an associated input field. */ -const OpenButton = ({ text = "Open", ...props }: OpenButtonProps) => { +const OpenButton = ({ children, ...props }: OpenButtonProps) => { const actions = useProjectActions(); - const ref = useRef(null); - - const handleChooseFile = useCallback(() => { - ref.current && ref.current.click(); - }, []); - - const handleOpenFile = useCallback( - async (e: React.ChangeEvent) => { - const files = e.target.files; - console.log(files); - if (files) { - const file = files.item(0); - // Clear the input so we're triggered if the user opens the same file again. - ref.current!.value = ""; - if (file) { - await actions.open(file); - } - } - }, - [actions] - ); - return ( - <> - - - + + {children} + ); }; diff --git a/src/project/UploadButton.tsx b/src/project/UploadButton.tsx new file mode 100644 index 000000000..dcb60a203 --- /dev/null +++ b/src/project/UploadButton.tsx @@ -0,0 +1,28 @@ +import { ButtonProps } from "@chakra-ui/react"; +import React from "react"; +import { RiFileUploadLine } from "react-icons/ri"; +import FileInputButton from "../common/FileInputButton"; +import { useProjectActions } from "./project-hooks"; + +interface OpenButtonProps extends ButtonProps {} + +/** + * Upload button, with an associated input field. + * + * This adds or updates files in the file system rather than switching project. + */ +const UploadButton = ({ children, ...props }: OpenButtonProps) => { + const actions = useProjectActions(); + return ( + } + {...props} + > + {children} + + ); +}; + +export default UploadButton; diff --git a/src/project/project-actions.tsx b/src/project/project-actions.tsx index a137d924b..f60c7397f 100644 --- a/src/project/project-actions.tsx +++ b/src/project/project-actions.tsx @@ -25,6 +25,9 @@ class HexGenerationError extends Error {} * * These actions all perform their own error handling and * give appropriate feedback to the user if they fail. + * + * Functions all use arrow functions so they can be directly + * used as callbacks. */ export class ProjectActions { constructor( @@ -148,7 +151,7 @@ export class ProjectActions { } }; - async addOrUpdateFile(file: File): Promise { + addOrUpdateFile = async (file: File): Promise => { // TODO: Consider special-casing Python, modules or hex files? // At least modules make sense here. Perhaps this should // be the only way to add a module. @@ -163,7 +166,7 @@ export class ProjectActions { } catch (e) { this.actionFeedback.unexpectedError(e); } - } + }; /** * Flash the device. @@ -246,7 +249,9 @@ export class ProjectActions { filename === MAIN_FILE ? `${projectName}.py` : filename; try { const content = await this.fs.read(filename); - const blob = new Blob([content.data], { type: "application/octet-stream" }); + const blob = new Blob([content.data], { + type: "application/octet-stream", + }); saveAs(blob, downloadName); } catch (e) { this.actionFeedback.unexpectedError(e); From 32f727673d303eecfc5e7fd7c3c87a7eb66ad7e9 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sat, 20 Mar 2021 14:34:22 +0000 Subject: [PATCH 14/15] Fix icon. --- src/common/FileInputButton.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/common/FileInputButton.tsx b/src/common/FileInputButton.tsx index 85e72b1cf..deba375af 100644 --- a/src/common/FileInputButton.tsx +++ b/src/common/FileInputButton.tsx @@ -55,11 +55,7 @@ const FileInputButton = ({ onChange={handleOpenFile} ref={ref} /> - From 7dcea3947c0e86dd022fe8d4fe70112ba93551a1 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sat, 20 Mar 2021 14:52:13 +0000 Subject: [PATCH 15/15] Missing test + fixes. --- src/fs/fs.test.ts | 10 ++++++++++ src/fs/fs.ts | 2 +- src/project/UploadButton.tsx | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/fs/fs.test.ts b/src/fs/fs.test.ts index a88b1af7e..9362afc22 100644 --- a/src/fs/fs.test.ts +++ b/src/fs/fs.test.ts @@ -105,6 +105,16 @@ describe("Filesystem", () => { ); }); + it("can remove files", async () => { + await ufs.write(MAIN_FILE, "hey", VersionAction.INCREMENT); + expect(events[0].files).toEqual([{ name: MAIN_FILE, version: 1 }]); + + await ufs.remove(MAIN_FILE); + + expect(events[1].files).toEqual([]); + expect(await ufs.exists(MAIN_FILE)).toEqual(false); + }); + it("can replace project with a Python file", async () => { await ufs.initialize(); diff --git a/src/fs/fs.ts b/src/fs/fs.ts index ccdf309c9..493e8e6a0 100644 --- a/src/fs/fs.ts +++ b/src/fs/fs.ts @@ -172,7 +172,7 @@ export class FileSystem extends EventEmitter { this.fs.write(filename, content); } if (versionAction === VersionAction.INCREMENT) { - this.notify(); + return this.notify(); } else { // Nothing can have changed, don't needlessly change the identity of our file objects. } diff --git a/src/project/UploadButton.tsx b/src/project/UploadButton.tsx index dcb60a203..514d9a59c 100644 --- a/src/project/UploadButton.tsx +++ b/src/project/UploadButton.tsx @@ -15,7 +15,7 @@ const UploadButton = ({ children, ...props }: OpenButtonProps) => { const actions = useProjectActions(); return ( } {...props}