From c75f3720f54678b764ae44a11fc68180d98e9ec8 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 12 Mar 2022 18:29:56 -0600 Subject: [PATCH] editor: drop EditorContext We want to rely less on directly accessing the editor object. --- src/app/App.tsx | 122 ++++++++++++++++++--------------------- src/app/actions.ts | 6 ++ src/app/reducers.test.ts | 1 + src/app/reducers.ts | 11 ++++ src/editor/Editor.tsx | 69 +++++++++++----------- src/hub/RunButton.tsx | 7 +-- 6 files changed, 112 insertions(+), 104 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index f020eae1..c48d7c37 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -2,10 +2,10 @@ // Copyright (c) 2020-2022 The Pybricks Authors import { Classes } from '@blueprintjs/core'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; import SplitterLayout from 'react-splitter-layout'; -import Editor, { EditorContext, EditorContextType, EditorType } from '../editor/Editor'; +import Editor, { EditorType } from '../editor/Editor'; import Explorer from '../explorer/Explorer'; import { useSelector } from '../reducers'; import { toggleBoolean } from '../settings/actions'; @@ -17,6 +17,7 @@ import { isMacOS } from '../utils/os'; import 'react-splitter-layout/lib/index.css'; import './app.scss'; +import { appEditor } from './actions'; const Docs: React.FunctionComponent = (_props) => { const dispatch = useDispatch(); @@ -131,21 +132,7 @@ const App: React.VoidFunctionComponent = ({ onEditorChanged }) => { const darkMode = useSelector((s): boolean => s.settings.darkMode); const showDocs = useSelector((s): boolean => s.settings.showDocs); const [isDragging, setIsDragging] = useState(false); - const [editor, setEditor] = useState(null); - - const editorContext = useMemo( - () => ({ - editor, - setEditor: (editor) => { - setEditor(editor); - - if (onEditorChanged) { - onEditorChanged(editor); - } - }, - }), - [editor], - ); + const dispatch = useDispatch(); // darkMode class has to be applied to body element, otherwise it won't // affect portals @@ -160,56 +147,61 @@ const App: React.VoidFunctionComponent = ({ onEditorChanged }) => { }, [darkMode]); return ( - -
- - setIsDragging(true)} - onDragEnd={(): void => setIsDragging(false)} - percentage={true} - secondaryInitialSize={Number( - localStorage.getItem('app-docs-split') || 30, - )} - onSecondaryPaneSizeChange={(value): void => - localStorage.setItem('app-docs-split', String(value)) - } - > -
-
- -
-
- - localStorage.setItem( - 'app-terminal-split', - String(value), - ) - } - > - -
- -
-
-
+
+ + setIsDragging(true)} + onDragEnd={(): void => setIsDragging(false)} + percentage={true} + secondaryInitialSize={Number( + localStorage.getItem('app-docs-split') || 30, + )} + onSecondaryPaneSizeChange={(value): void => + localStorage.setItem('app-docs-split', String(value)) + } + > +
+
+
-
- {isDragging &&
} - +
+ + localStorage.setItem( + 'app-terminal-split', + String(value), + ) + } + > + { + dispatch(appEditor(editor !== null)); + if (onEditorChanged) { + onEditorChanged(editor); + } + }} + /> +
+ +
+
- - -
- +
+
+ {isDragging &&
} + +
+ + +
); }; diff --git a/src/app/actions.ts b/src/app/actions.ts index 2be6281d..312ca0a6 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -48,3 +48,9 @@ export const didInstall = createAction(() => ({ export const didStart = createAction(() => ({ type: 'app.action.didStart', })); + +/** Temporary action for transitioning editor context. Do no use in new code. */ +export const appEditor = createAction((hasEditor: boolean) => ({ + type: 'app.action.editor', + hasEditor, +})); diff --git a/src/app/reducers.test.ts b/src/app/reducers.test.ts index 603a333a..f6bc4577 100644 --- a/src/app/reducers.test.ts +++ b/src/app/reducers.test.ts @@ -22,6 +22,7 @@ test('initial state', () => { expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(` Object { "checkingForUpdate": false, + "hasEditor": false, "hasUnresolvedInstallPrompt": false, "isServiceWorkerRegistered": false, "promptingInstall": false, diff --git a/src/app/reducers.ts b/src/app/reducers.ts index 7b450fb4..f8d385ce 100644 --- a/src/app/reducers.ts +++ b/src/app/reducers.ts @@ -13,6 +13,7 @@ import { appDidCheckForUpdate, appDidReceiveBeforeInstallPrompt, appDidResolveInstallPrompt, + appEditor, appShowInstallPrompt, didInstall, } from './actions'; @@ -91,6 +92,15 @@ const readyForOfflineUse: Reducer = (state = false, action) => { return state; }; +/** Temporary reducer for transitioning editor context. Do no use in new code. */ +const hasEditor: Reducer = (state = false, action) => { + if (appEditor.matches(action)) { + return action.hasEditor; + } + + return state; +}; + export default combineReducers({ isServiceWorkerRegistered, checkingForUpdate, @@ -98,4 +108,5 @@ export default combineReducers({ hasUnresolvedInstallPrompt, promptingInstall, readyForOfflineUse, + hasEditor, }); diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index 975fc6f9..bb277ad5 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -2,15 +2,11 @@ // Copyright (c) 2020-2022 The Pybricks Authors import { Menu, MenuDivider, MenuItem } from '@blueprintjs/core'; -import { - ContextMenu2, - ContextMenu2ContentProps, - ResizeSensor2, -} from '@blueprintjs/popover2'; +import { ContextMenu2, ResizeSensor2 } from '@blueprintjs/popover2'; import { useI18n } from '@shopify/react-i18n'; import tomorrowNightEightiesTheme from 'monaco-themes/themes/Tomorrow-Night-Eighties.json'; import xcodeTheme from 'monaco-themes/themes/Xcode_default.json'; -import React, { createContext, useContext, useRef } from 'react'; +import React, { useState } from 'react'; import MonacoEditor, { monaco } from 'react-monaco-editor'; import { useDispatch } from 'react-redux'; import { IDisposable } from 'xterm'; @@ -32,22 +28,6 @@ import './editor.scss'; */ export type EditorType = monaco.editor.ICodeEditor | null; -/** - * The type for the value of EditorContext. - */ -export type EditorContextType = { - editor: EditorType; - setEditor: (editor: EditorType) => void; -}; - -/** - * Editor context for getting access to the current editor. - */ -export const EditorContext = createContext({ - editor: null, - setEditor: () => undefined, -}); - const pybricksMicroPythonId = 'pybricks-micropython'; monaco.languages.register({ id: pybricksMicroPythonId }); @@ -83,11 +63,22 @@ monaco.editor.defineTheme( const xcodeId = 'xcode'; monaco.editor.defineTheme(xcodeId, xcodeTheme as monaco.editor.IStandaloneThemeData); -const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => { - const { editor } = useContext(EditorContext); +type EditorContextMenuProps = { editor: EditorType }; +const EditorContextMenu: React.VoidFunctionComponent = ({ + editor, +}) => { const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en }); + const hasEditor = editor !== null; + + const selection = editor?.getSelection(); + const hasSelection = selection && !selection.isEmpty(); + + const model = editor?.getModel(); + const canUndo = model && model.canUndo(); + const canRedo = model && model.canRedo(); + return ( { text={i18n.translate(EditorStringId.Copy)} icon="duplicate" label={isMacOS() ? 'Cmd-C' : 'Ctrl-C'} - disabled={!editor?.getSelection() || editor?.getSelection()?.isEmpty()} + disabled={!hasSelection} /> { @@ -108,6 +99,7 @@ const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => { text={i18n.translate(EditorStringId.Paste)} icon="clipboard" label={isMacOS() ? 'Cmd-V' : 'Ctrl-V'} + disabled={!hasEditor} /> { @@ -117,6 +109,7 @@ const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => { text={i18n.translate(EditorStringId.SelectAll)} icon="blank" label={isMacOS() ? 'Cmd-A' : 'Ctrl-A'} + disabled={!hasEditor} /> { text={i18n.translate(EditorStringId.Undo)} icon="undo" label={isMacOS() ? 'Cmd-Z' : 'Ctrl-Z'} - disabled={!editor?.getModel()?.canUndo()} + disabled={!canUndo} /> { @@ -137,30 +130,33 @@ const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => { text={i18n.translate(EditorStringId.Redo)} icon="redo" label={isMacOS() ? 'Cmd-Shift-Z' : 'Ctrl-Shift-Z'} - disabled={!editor?.getModel()?.canRedo()} + disabled={!canRedo} /> ); }; -const Editor: React.FunctionComponent = (_props) => { - const editorRef = useRef(null); - const dispatch = useDispatch(); - const { setEditor } = useContext(EditorContext); +type EditorProps = { onEditorChanged: (editor: EditorType) => void }; +const Editor: React.VoidFunctionComponent = ({ onEditorChanged }) => { + const dispatch = useDispatch(); + + const [editor, setEditor] = useState(null); const darkMode = useSelector((s) => s.settings.darkMode); const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en }); return ( - editorRef?.current?.editor?.layout()}> + editor?.layout()}> editorRef.current?.editor?.focus() }} + // NB: we have to create a new context menu each time it is + // shown in order to get some state, like canUndo and canRedo + // that don't have events to monitor changes. + content={() => } + popoverProps={{ onClosed: () => editor?.focus() }} > { ); editor.focus(); setEditor(editor); + if (onEditorChanged) { + onEditorChanged(editor); + } }} // REVIST: need to ensure we have exclusive access to file onChange={(v) => dispatch(fileStorageWriteFile('main.py', v))} diff --git a/src/hub/RunButton.tsx b/src/hub/RunButton.tsx index e8d1b8a8..3473914c 100644 --- a/src/hub/RunButton.tsx +++ b/src/hub/RunButton.tsx @@ -1,9 +1,8 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020-2022 The Pybricks Authors -import React, { useContext } from 'react'; +import React from 'react'; import { useDispatch } from 'react-redux'; -import { EditorContext } from '../editor/Editor'; import { useSelector } from '../reducers'; import ActionButton, { ActionButtonProps } from '../toolbar/ActionButton'; import { TooltipId } from '../toolbar/i18n'; @@ -17,9 +16,9 @@ const RunButton: React.VoidFunctionComponent = ({ id, keyboardShortcut, }) => { - const { editor } = useContext(EditorContext); const downloadProgress = useSelector((s) => s.hub.downloadProgress); const runtime = useSelector((s) => s.hub.runtime); + const hasEditor = useSelector((s) => s.app.hasEditor); const dispatch = useDispatch(); @@ -30,7 +29,7 @@ const RunButton: React.VoidFunctionComponent = ({ tooltip={TooltipId.Run} progressTooltip={TooltipId.RunProgress} icon={runIcon} - enabled={editor !== null && runtime === HubRuntimeState.Idle} + enabled={hasEditor && runtime === HubRuntimeState.Idle} showProgress={runtime === HubRuntimeState.Loading} progress={downloadProgress === null ? undefined : downloadProgress} onAction={() => dispatch(downloadAndRun())}