From 0d96c1fcd79de74e18005368eebf7a19a379580e Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 18 May 2023 16:50:26 -0500 Subject: [PATCH] editor: fix replacing file when open in editor When we import/replace a file that is open in an editor, instead of modifying the file in storage, we need to modify the file in the editor. This allows the modification to be pushed on the undo stack so that the user can undo the change in case it wrote over any of their recent changes. Issue: https://github.com/pybricks/support/issues/975 --- CHANGELOG.md | 6 ++++++ src/editor/actions.ts | 13 ++++++++++++- src/editor/sagas.ts | 33 ++++++++++++++++++++++++++++++++- src/explorer/sagas.ts | 28 +++++++++++++++++++--------- 4 files changed, 69 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4acec657..597487121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ## [Unreleased] +### Fixed +- Fixed importing/replacing file when file is open in editor ([support#975]). + +[support#975]: https://github.com/pybricks/support/issues/975 + + ## [2.2.0-beta.4] - 2023-05-16 ### Added diff --git a/src/editor/actions.ts b/src/editor/actions.ts index 1db7854e1..497eedf2c 100644 --- a/src/editor/actions.ts +++ b/src/editor/actions.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors +// Copyright (c) 2022-2023 The Pybricks Authors import { createAction } from '../actions'; import { UUID } from '../fileStorage'; @@ -121,3 +121,14 @@ export const editorGoto = createAction((uuid: UUID, line: number) => ({ uuid, line, })); + +/** + * Requests to replace the value a file in the editor. + * @param uuid The file UUID. + * @param value The new value for the contents of the file. + */ +export const editorReplaceFile = createAction((uuid: UUID, value: string) => ({ + type: 'editor.action.replaceFile', + uuid, + value, +})); diff --git a/src/editor/sagas.ts b/src/editor/sagas.ts index 5af3827ec..52ffb39b4 100644 --- a/src/editor/sagas.ts +++ b/src/editor/sagas.ts @@ -3,7 +3,7 @@ import type { DatabaseChangeType, IDatabaseChange } from 'dexie-observable/api'; import * as monaco from 'monaco-editor'; -import { EventChannel, buffers, eventChannel } from 'redux-saga'; +import { EventChannel, Task, buffers, eventChannel } from 'redux-saga'; import { call, cancelled, @@ -62,6 +62,7 @@ import { editorGetValueResponse, editorGoto, editorOpenFile, + editorReplaceFile, } from './actions'; import { EditorError } from './error'; import { ActiveFileHistoryManager, OpenFileManager } from './lib'; @@ -92,6 +93,28 @@ function* handleModelDidChange( } } +function handleReplaceFile( + editor: monaco.editor.ICodeEditor, + model: monaco.editor.ITextModel, + action: ReturnType, +) { + // model might not be open in editor, but it doesn't hurt to do this in the + // open editor + editor.pushUndoStop(); + // pushEditOperations says it expects a cursorComputer, but doesn't seem to need one. + model.pushEditOperations( + [], + [ + { + range: model.getFullModelRange(), + text: action.value, + }, + ], + undefined as unknown as monaco.editor.ICursorStateComputer, + ); + editor.pushUndoStop(); +} + function* handleEditorOpenFile( editor: monaco.editor.ICodeEditor, openFiles: OpenFileManager, @@ -161,6 +184,14 @@ function* handleEditorOpenFile( // https://github.com/redux-saga/redux-saga/issues/620#issuecomment-259161095 yield* fork(handleModelDidChange, 1000, didChangeModelChan, model); + const replaceFileTask: Task = yield* takeEvery( + editorReplaceFile.when((a) => a.uuid === action.uuid), + handleReplaceFile, + editor, + model, + ); + defer.push(() => replaceFileTask.cancel()); + openFiles.add(action.uuid, model, didLoad.viewState); defer.push(() => openFiles.remove(action.uuid)); diff --git a/src/explorer/sagas.ts b/src/explorer/sagas.ts index 1da7ed89a..d1008cfbe 100644 --- a/src/explorer/sagas.ts +++ b/src/explorer/sagas.ts @@ -20,6 +20,7 @@ import { editorDidActivateFile, editorDidCloseFile, editorDidFailToActivateFile, + editorReplaceFile, } from '../editor/actions'; import { EditorError } from '../editor/error'; import { getPybricksMicroPythonFileTemplate } from '../editor/pybricksMicroPython'; @@ -243,17 +244,26 @@ function* importPythonFile( fileName = accepted.newName; } - yield* put(fileStorageWriteFile(fileName, sourceFileContents)); + const existingFileInfo = existingFiles.find((x) => x.path === fileName); + const openFileUuids = yield* select((s: RootState) => s.editor.openFileUuids); - const { didFailToWrite } = yield* race({ - didWrite: take(fileStorageDidWriteFile.when((a) => a.path === fileName)), - didFailToWrite: take( - fileStorageDidFailToWriteFile.when((a) => a.path === fileName), - ), - }); + // If the file is open, modify contents in the editor so preserve undo + // history, otherwise write directly to storage. + if (existingFileInfo && openFileUuids.includes(existingFileInfo.uuid)) { + yield* put(editorReplaceFile(existingFileInfo.uuid, sourceFileContents)); + } else { + yield* put(fileStorageWriteFile(fileName, sourceFileContents)); - if (didFailToWrite) { - throw didFailToWrite.error; + const { didFailToWrite } = yield* race({ + didWrite: take(fileStorageDidWriteFile.when((a) => a.path === fileName)), + didFailToWrite: take( + fileStorageDidFailToWriteFile.when((a) => a.path === fileName), + ), + }); + + if (didFailToWrite) { + throw didFailToWrite.error; + } } }