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; + } } }