editor: add editorGetValue() saga

This is another step towards removing the 'editor' context.
This commit is contained in:
David Lechner
2022-03-26 13:44:55 -05:00
parent 79d5b06136
commit c158e4f0e2
6 changed files with 87 additions and 43 deletions
+20
View File
@@ -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,
}));
+45 -3
View File
@@ -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<string> {
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<typeof editorGetValueRequest>,
): 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());
}
-6
View File
@@ -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<EditorType>({
getValue: () => 'print("test")',
});
const saga = new AsyncSaga(flashFirmware, {
editor,
nextMessageId: createCountFunc(),
});
+2 -10
View File
@@ -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<typeof flashFirmware>): Generat
let program: string | undefined = undefined;
if (action.flashCurrentProgram) {
const editor = yield* getContext<EditorType>('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) {
+17 -15
View File
@@ -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<monaco.editor.ICodeEditor>();
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<typeof write>).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<typeof write>).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<typeof write>).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
+3 -9
View File
@@ -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<EditorType>('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),