From c158e4f0e2e6f6a9547b730686d9a5d31cfe81cd Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 26 Mar 2022 13:42:32 -0500 Subject: [PATCH] editor: add editorGetValue() saga This is another step towards removing the 'editor' context. --- src/editor/actions.ts | 20 ++++++++++++++++ src/editor/sagas.ts | 48 +++++++++++++++++++++++++++++++++++--- src/firmware/sagas.test.ts | 6 ----- src/firmware/sagas.ts | 12 ++-------- src/hub/sagas.test.ts | 32 +++++++++++++------------ src/hub/sagas.ts | 12 +++------- 6 files changed, 87 insertions(+), 43 deletions(-) diff --git a/src/editor/actions.ts b/src/editor/actions.ts index 382458ee..ff0de423 100644 --- a/src/editor/actions.ts +++ b/src/editor/actions.ts @@ -7,3 +7,23 @@ import { createAction } from '../actions'; export const editorDidCreate = createAction(() => ({ type: 'editor.action.didCreate', })); + +/** + * Action that requests getting the current contents of the editor. + * @param id A unique identifier for this request. + */ +export const editorGetValueRequest = createAction((id: number) => ({ + type: 'editor,action.getCurrentScriptRequest', + id, +})); + +/** + * Action that responds to {@link editorGetCurrentScriptRequest}. + * @param id The id that matches {@link editorGetCurrentScriptRequest}. + * @param value The current editor contents. + */ +export const editorGetValueResponse = createAction((id: number, value: string) => ({ + type: 'editor,action.getCurrentScriptResponse', + id, + value, +})); diff --git a/src/editor/sagas.ts b/src/editor/sagas.ts index 620e58dc..40a1dc48 100644 --- a/src/editor/sagas.ts +++ b/src/editor/sagas.ts @@ -3,7 +3,16 @@ import { monaco } from 'react-monaco-editor'; import { eventChannel } from 'redux-saga'; -import { fork, put, race, select, take, takeEvery } from 'typed-redux-saga/macro'; +import { + SagaGenerator, + fork, + getContext, + put, + race, + select, + take, + takeEvery, +} from 'typed-redux-saga/macro'; import { fileStorageDidFailToReadFile, fileStorageDidInitialize, @@ -11,7 +20,40 @@ import { fileStorageReadFile, } from '../fileStorage/actions'; import { RootState } from '../reducers'; -import { editorDidCreate } from './actions'; +import { + editorDidCreate, + editorGetValueRequest, + editorGetValueResponse, +} from './actions'; + +/** + * Saga that gets the current value from the editor. + * @returns The value. + * @throws Error if editor.isReady state is false. + */ +export function* editorGetValue(): SagaGenerator { + const nextMessageId = yield* getContext<() => number>('nextMessageId'); + + const isReady = yield* select((s: RootState) => s.editor.isReady); + + if (!isReady) { + throw new Error('editorGetValue() called before editor.isReady'); + } + + const request = yield* put(editorGetValueRequest(nextMessageId())); + const response = yield* take( + editorGetValueResponse.when((a) => a.id === request.id), + ); + + return response.value; +} + +function* handleEditorGetValueRequest( + editor: monaco.editor.ICodeEditor, + action: ReturnType, +): Generator { + yield* put(editorGetValueResponse(action.id, editor.getValue())); +} function* handleDidCreateEditor(editor: monaco.editor.ICodeEditor): Generator { // first, we need to be sure that file storage is ready @@ -42,7 +84,7 @@ function* handleDidCreateEditor(editor: monaco.editor.ICodeEditor): Generator { editor.setValue(succeeded.fileContents); } - // TODO: subscribe to actions that act on the editor + yield* takeEvery(editorGetValueRequest, handleEditorGetValueRequest, editor); yield* put(editorDidCreate()); } diff --git a/src/firmware/sagas.test.ts b/src/firmware/sagas.test.ts index aaaf19fc..98ab7dd0 100644 --- a/src/firmware/sagas.test.ts +++ b/src/firmware/sagas.test.ts @@ -9,7 +9,6 @@ import { import { mock } from 'jest-mock-extended'; import JSZip from 'jszip'; import { AsyncSaga } from '../../test'; -import { EditorType } from '../editor/Editor'; import { BootloaderConnectionFailureReason, checksumRequest, @@ -1730,12 +1729,7 @@ describe('flashFirmware', () => { new Response(await zip.generateAsync({ type: 'blob' })), ); - const editor = mock({ - getValue: () => 'print("test")', - }); - const saga = new AsyncSaga(flashFirmware, { - editor, nextMessageId: createCountFunc(), }); diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts index e06df85a..12b49676 100644 --- a/src/firmware/sagas.ts +++ b/src/firmware/sagas.ts @@ -25,7 +25,7 @@ import { take, takeEvery, } from 'typed-redux-saga/macro'; -import { EditorType } from '../editor/Editor'; +import { editorGetValue } from '../editor/sagas'; import { checksumRequest, checksumResponse, @@ -286,15 +286,7 @@ function* handleFlashFirmware(action: ReturnType): Generat let program: string | undefined = undefined; if (action.flashCurrentProgram) { - const editor = yield* getContext('editor'); - - // istanbul ignore if: it is a bug to dispatch this action with no current editor - if (editor === null) { - console.error('flashFirmware: No current editor'); - return; - } - - program = editor.getValue(); + program = yield* editorGetValue(); } if (action.data !== null) { diff --git a/src/hub/sagas.test.ts b/src/hub/sagas.test.ts index 05c7e729..7833b197 100644 --- a/src/hub/sagas.test.ts +++ b/src/hub/sagas.test.ts @@ -1,14 +1,13 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020-2022 The Pybricks Authors -import { mock } from 'jest-mock-extended'; -import { monaco } from 'react-monaco-editor'; import { AsyncSaga } from '../../test'; import { didWrite, write } from '../ble-nordic-uart-service/actions'; import { didSendCommand, sendStopUserProgramCommand, } from '../ble-pybricks-service/actions'; +import { editorGetValueRequest, editorGetValueResponse } from '../editor/actions'; import { compile, didCompile } from '../mpy/actions'; import { createCountFunc } from '../utils/iter'; import { @@ -26,17 +25,23 @@ jest.mock('react-monaco-editor'); describe('downloadAndRun', () => { test('no errors', async () => { - const mockEditor = mock(); const saga = new AsyncSaga(hub, { - editor: mockEditor, nextMessageId: createCountFunc(), }); + saga.updateState({ editor: { isReady: true } }); + saga.put(downloadAndRun()); - // first, it tries to compile the program in the current editor + // first, it gets the value from the current editor + const editorValueAction = await saga.take(); + expect(editorValueAction).toEqual(editorGetValueRequest(0)); + + saga.put(editorGetValueResponse(0, '')); + + // then it tries to compile the program in the current editor const compileAction = await saga.take(); - expect(compile.matches(compileAction)).toBeTruthy(); + expect(compileAction).toEqual(compile('', ['-mno-unicode'])); saga.put(didCompile(new Uint8Array(30))); // then it notifies that loading has begun @@ -45,9 +50,8 @@ describe('downloadAndRun', () => { // first message is the length const writeAction = await saga.take(); - expect(writeAction).toBeTruthy(); - expect((writeAction as ReturnType).value.length).toBe(4); - saga.put(didWrite(0)); + expect(writeAction).toEqual(write(1, new Uint8Array([30, 0, 0, 0]))); + saga.put(didWrite(1)); saga.put(checksum(30)); // then progress is updated @@ -56,9 +60,8 @@ describe('downloadAndRun', () => { // then the first chunk of 20 bytes const writeAction2 = await saga.take(); - expect(write.matches(writeAction2)).toBeTruthy(); - expect((writeAction2 as ReturnType).value.length).toBe(20); - saga.put(didWrite(1)); + expect(writeAction2).toEqual(write(2, new Uint8Array(20))); + saga.put(didWrite(2)); saga.put(checksum(0)); // then progress is updated @@ -67,9 +70,8 @@ describe('downloadAndRun', () => { // then last chunk const writeAction3 = await saga.take(); - expect(write.matches(writeAction3)).toBeTruthy(); - expect((writeAction3 as ReturnType).value.length).toBe(10); - saga.put(didWrite(2)); + expect(writeAction3).toEqual(write(3, new Uint8Array(10))); + saga.put(didWrite(3)); saga.put(checksum(0)); // Then a status message saying that we are done diff --git a/src/hub/sagas.ts b/src/hub/sagas.ts index 8e65954a..d4e3d2e4 100644 --- a/src/hub/sagas.ts +++ b/src/hub/sagas.ts @@ -19,7 +19,7 @@ import { sendStopUserProgramCommand, } from '../ble-pybricks-service/actions'; import { didConnect } from '../ble/actions'; -import { EditorType } from '../editor/Editor'; +import { editorGetValue } from '../editor/sagas'; import { compile, didCompile, didFailToCompile } from '../mpy/actions'; import { defined } from '../utils'; import { xor8 } from '../utils/math'; @@ -47,16 +47,10 @@ function* waitForWrite(id: number): SagaGenerator<{ } function* handleDownloadAndRun(): Generator { - const editor = yield* getContext('editor'); + const script = yield* editorGetValue(); - // istanbul ignore next: it is a bug to dispatch this action with no current editor - if (editor === null) { - console.error('downloadAndRun: No current editor'); - return; - } - - const script = editor.getValue(); yield* put(compile(script, ['-mno-unicode'])); + const { mpy, mpyFail } = yield* race({ mpy: take(didCompile), mpyFail: take(didFailToCompile),