mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
editor: drop EditorContext
We want to rely less on directly accessing the editor object.
This commit is contained in:
+57
-65
@@ -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<AppProps> = ({ 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<EditorType>(null);
|
||||
|
||||
const editorContext = useMemo<EditorContextType>(
|
||||
() => ({
|
||||
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<AppProps> = ({ onEditorChanged }) => {
|
||||
}, [darkMode]);
|
||||
|
||||
return (
|
||||
<EditorContext.Provider value={editorContext}>
|
||||
<div className="pb-app h-100 w-100 p-absolute">
|
||||
<Toolbar />
|
||||
<SplitterLayout
|
||||
customClassName={`pb-app-body ${
|
||||
showDocs ? 'pb-show-docs' : 'pb-hide-docs'
|
||||
}`}
|
||||
onDragStart={(): void => 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))
|
||||
}
|
||||
>
|
||||
<div className="h-100 w-100" style={{ display: 'flex' }}>
|
||||
<div style={{ display: 'inline-block', width: 250 }}>
|
||||
<Explorer />
|
||||
</div>
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
<SplitterLayout
|
||||
vertical={true}
|
||||
percentage={true}
|
||||
secondaryInitialSize={Number(
|
||||
localStorage.getItem('app-terminal-split') || 30,
|
||||
)}
|
||||
onSecondaryPaneSizeChange={(value): void =>
|
||||
localStorage.setItem(
|
||||
'app-terminal-split',
|
||||
String(value),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Editor />
|
||||
<div className="pb-app-terminal-padding h-100">
|
||||
<Terminal />
|
||||
</div>
|
||||
</SplitterLayout>
|
||||
</div>
|
||||
<div className="pb-app h-100 w-100 p-absolute">
|
||||
<Toolbar />
|
||||
<SplitterLayout
|
||||
customClassName={`pb-app-body ${
|
||||
showDocs ? 'pb-show-docs' : 'pb-hide-docs'
|
||||
}`}
|
||||
onDragStart={(): void => 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))
|
||||
}
|
||||
>
|
||||
<div className="h-100 w-100" style={{ display: 'flex' }}>
|
||||
<div style={{ display: 'inline-block', width: 250 }}>
|
||||
<Explorer />
|
||||
</div>
|
||||
<div className="h-100 w-100">
|
||||
{isDragging && <div className="h-100 w-100 p-absolute" />}
|
||||
<Docs />
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
<SplitterLayout
|
||||
vertical={true}
|
||||
percentage={true}
|
||||
secondaryInitialSize={Number(
|
||||
localStorage.getItem('app-terminal-split') || 30,
|
||||
)}
|
||||
onSecondaryPaneSizeChange={(value): void =>
|
||||
localStorage.setItem(
|
||||
'app-terminal-split',
|
||||
String(value),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Editor
|
||||
onEditorChanged={(editor) => {
|
||||
dispatch(appEditor(editor !== null));
|
||||
if (onEditorChanged) {
|
||||
onEditorChanged(editor);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="pb-app-terminal-padding h-100">
|
||||
<Terminal />
|
||||
</div>
|
||||
</SplitterLayout>
|
||||
</div>
|
||||
</SplitterLayout>
|
||||
<StatusBar />
|
||||
</div>
|
||||
</EditorContext.Provider>
|
||||
</div>
|
||||
<div className="h-100 w-100">
|
||||
{isDragging && <div className="h-100 w-100 p-absolute" />}
|
||||
<Docs />
|
||||
</div>
|
||||
</SplitterLayout>
|
||||
<StatusBar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -22,6 +22,7 @@ test('initial state', () => {
|
||||
expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"checkingForUpdate": false,
|
||||
"hasEditor": false,
|
||||
"hasUnresolvedInstallPrompt": false,
|
||||
"isServiceWorkerRegistered": false,
|
||||
"promptingInstall": false,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
appDidCheckForUpdate,
|
||||
appDidReceiveBeforeInstallPrompt,
|
||||
appDidResolveInstallPrompt,
|
||||
appEditor,
|
||||
appShowInstallPrompt,
|
||||
didInstall,
|
||||
} from './actions';
|
||||
@@ -91,6 +92,15 @@ const readyForOfflineUse: Reducer<boolean> = (state = false, action) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
/** Temporary reducer for transitioning editor context. Do no use in new code. */
|
||||
const hasEditor: Reducer<boolean> = (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,
|
||||
});
|
||||
|
||||
+34
-35
@@ -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<EditorContextType>({
|
||||
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<EditorContextMenuProps> = ({
|
||||
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 (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
@@ -98,7 +89,7 @@ const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => {
|
||||
text={i18n.translate(EditorStringId.Copy)}
|
||||
icon="duplicate"
|
||||
label={isMacOS() ? 'Cmd-C' : 'Ctrl-C'}
|
||||
disabled={!editor?.getSelection() || editor?.getSelection()?.isEmpty()}
|
||||
disabled={!hasSelection}
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
@@ -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}
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
@@ -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}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
@@ -127,7 +120,7 @@ const contextMenu = (_props: ContextMenu2ContentProps): JSX.Element => {
|
||||
text={i18n.translate(EditorStringId.Undo)}
|
||||
icon="undo"
|
||||
label={isMacOS() ? 'Cmd-Z' : 'Ctrl-Z'}
|
||||
disabled={!editor?.getModel()?.canUndo()}
|
||||
disabled={!canUndo}
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
@@ -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}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const Editor: React.FunctionComponent = (_props) => {
|
||||
const editorRef = useRef<MonacoEditor>(null);
|
||||
const dispatch = useDispatch();
|
||||
const { setEditor } = useContext(EditorContext);
|
||||
type EditorProps = { onEditorChanged: (editor: EditorType) => void };
|
||||
|
||||
const Editor: React.VoidFunctionComponent<EditorProps> = ({ onEditorChanged }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [editor, setEditor] = useState<EditorType>(null);
|
||||
const darkMode = useSelector((s) => s.settings.darkMode);
|
||||
|
||||
const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en });
|
||||
|
||||
return (
|
||||
<ResizeSensor2 onResize={() => editorRef?.current?.editor?.layout()}>
|
||||
<ResizeSensor2 onResize={() => editor?.layout()}>
|
||||
<ContextMenu2
|
||||
className="h-100"
|
||||
content={contextMenu}
|
||||
popoverProps={{ onClosed: () => 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={() => <EditorContextMenu editor={editor} />}
|
||||
popoverProps={{ onClosed: () => editor?.focus() }}
|
||||
>
|
||||
<MonacoEditor
|
||||
ref={editorRef}
|
||||
language={pybricksMicroPythonId}
|
||||
theme={darkMode ? tomorrowNightEightiesId : xcodeId}
|
||||
width="100%"
|
||||
@@ -224,6 +220,9 @@ const Editor: React.FunctionComponent = (_props) => {
|
||||
);
|
||||
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))}
|
||||
|
||||
@@ -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<RunButtonProps> = ({
|
||||
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<RunButtonProps> = ({
|
||||
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())}
|
||||
|
||||
Reference in New Issue
Block a user