mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
editor/Editor: use hooks to configure editor
This fixes the issue of the editor not being updated when properties change.
This commit is contained in:
+152
-75
@@ -6,14 +6,19 @@ import { ContextMenu2, ResizeSensor2 } from '@blueprintjs/popover2';
|
||||
import { I18n, 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, { useState } from 'react';
|
||||
import MonacoEditor, { monaco } from 'react-monaco-editor';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import MonacoEditor, {
|
||||
ChangeHandler,
|
||||
EditorDidMount,
|
||||
EditorWillUnmount,
|
||||
monaco,
|
||||
} from 'react-monaco-editor';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTernaryDarkMode } from 'usehooks-ts';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { fileStorageWriteFile } from '../fileStorage/actions';
|
||||
import { compile } from '../mpy/actions';
|
||||
import { settingsToggleShowDocs } from '../settings/actions';
|
||||
import { useSettingIsShowDocsEnabled } from '../settings/hooks';
|
||||
import { isMacOS } from '../utils/os';
|
||||
import { I18nId } from './i18n';
|
||||
import * as pybricksMicroPython from './pybricksMicroPython';
|
||||
@@ -61,7 +66,12 @@ monaco.editor.defineTheme(
|
||||
const xcodeId = 'xcode';
|
||||
monaco.editor.defineTheme(xcodeId, xcodeTheme as monaco.editor.IStandaloneThemeData);
|
||||
|
||||
type EditorContextMenuProps = { editor: EditorType; i18n: I18n };
|
||||
type EditorContextMenuProps = {
|
||||
/** The editor. */
|
||||
editor?: monaco.editor.IStandaloneCodeEditor;
|
||||
/** Translation context. */
|
||||
i18n: I18n;
|
||||
};
|
||||
|
||||
const EditorContextMenu: React.VoidFunctionComponent<EditorContextMenuProps> = ({
|
||||
editor,
|
||||
@@ -133,17 +143,150 @@ const EditorContextMenu: React.VoidFunctionComponent<EditorContextMenuProps> = (
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper around useEffect() hook that uses {@link maybeEditor}.
|
||||
* @param maybeEditor The editor or undefined if the editor is not mounted.
|
||||
* @param callback The callback to call when editor is defined and when {@link deps} change.
|
||||
* @param deps Additional dependencies used in the {@link callback}.
|
||||
*/
|
||||
function useEditor(
|
||||
maybeEditor: monaco.editor.IStandaloneCodeEditor | undefined,
|
||||
callback: (
|
||||
editor: monaco.editor.IStandaloneCodeEditor,
|
||||
) => ReturnType<React.EffectCallback>,
|
||||
deps: React.DependencyList,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!maybeEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callback(maybeEditor);
|
||||
}, [maybeEditor, ...deps]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for adding actions to the editor.
|
||||
* @param maybeEditor The editor or undefined if the editor is not mounted.
|
||||
* @param createAction A callback to create a new action.
|
||||
* @param deps Additional dependencies used in {@link createAction}.
|
||||
*/
|
||||
function useEditorAction(
|
||||
maybeEditor: monaco.editor.IStandaloneCodeEditor | undefined,
|
||||
createAction: () => monaco.editor.IActionDescriptor,
|
||||
deps: React.DependencyList,
|
||||
): void {
|
||||
useEditor(
|
||||
maybeEditor,
|
||||
(editor) => {
|
||||
const subscription = editor.addAction(createAction());
|
||||
return () => subscription.dispose();
|
||||
},
|
||||
deps,
|
||||
);
|
||||
}
|
||||
|
||||
type EditorProps = { onEditorChanged?: (editor: EditorType) => void };
|
||||
|
||||
const Editor: React.VoidFunctionComponent<EditorProps> = ({ onEditorChanged }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [editor, setEditor] = useState<EditorType>(null);
|
||||
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor>();
|
||||
const { toggleIsSettingShowDocsEnabled } = useSettingIsShowDocsEnabled();
|
||||
const { isDarkMode } = useTernaryDarkMode();
|
||||
|
||||
// istanbul ignore next: babel-loader rewrites this line
|
||||
const [i18n] = useI18n();
|
||||
|
||||
const options = useMemo<monaco.editor.IStandaloneEditorConstructionOptions>(
|
||||
() => ({
|
||||
fontSize: 18,
|
||||
minimap: { enabled: false },
|
||||
contextmenu: false,
|
||||
rulers: [80],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEditor(
|
||||
editor,
|
||||
(editor) => {
|
||||
const contrib = new UntitledHintContribution(
|
||||
editor,
|
||||
i18n.translate(I18nId.Placeholder),
|
||||
);
|
||||
return () => contrib.dispose();
|
||||
},
|
||||
[i18n],
|
||||
);
|
||||
|
||||
useEditorAction(
|
||||
editor,
|
||||
() => ({
|
||||
id: 'pybricks.action.toggleDocs',
|
||||
label: i18n.translate(I18nId.ToggleDocs),
|
||||
run: () => toggleIsSettingShowDocsEnabled(),
|
||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
|
||||
}),
|
||||
[i18n, toggleIsSettingShowDocsEnabled],
|
||||
);
|
||||
|
||||
useEditorAction(
|
||||
editor,
|
||||
() => ({
|
||||
id: 'pybricks.action.check',
|
||||
label: i18n.translate(I18nId.Check),
|
||||
// REVISIT: the compile options here might need to be changed - hopefully there is
|
||||
// one setting that works for all hub types for cases where we aren't connected.
|
||||
run: (e) => {
|
||||
dispatch(compile(e.getValue(), []));
|
||||
},
|
||||
keybindings: [monaco.KeyCode.F2],
|
||||
}),
|
||||
[i18n, dispatch],
|
||||
);
|
||||
|
||||
useEditorAction(
|
||||
editor,
|
||||
() => ({
|
||||
id: 'pybricks.action.save',
|
||||
label: 'Unused',
|
||||
run: () => {
|
||||
// We already automatically save the file after every change,
|
||||
// so CTRL+S is ignored
|
||||
console.debug('Ctrl-S ignored');
|
||||
},
|
||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleEditorDidMount = useCallback<EditorDidMount>(
|
||||
(editor) => {
|
||||
editor.focus();
|
||||
setEditor(editor);
|
||||
|
||||
if (onEditorChanged) {
|
||||
onEditorChanged(editor);
|
||||
}
|
||||
},
|
||||
[onEditorChanged, setEditor],
|
||||
);
|
||||
|
||||
const handleEditorWillUnmount = useCallback<EditorWillUnmount>(() => {
|
||||
if (onEditorChanged) {
|
||||
onEditorChanged(null);
|
||||
}
|
||||
|
||||
setEditor(undefined);
|
||||
}, [onEditorChanged, setEditor]);
|
||||
|
||||
const handleChange = useCallback<ChangeHandler>(
|
||||
// REVISIT: need to ensure we have exclusive access to file
|
||||
(v) => dispatch(fileStorageWriteFile('main.py', v)),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<ResizeSensor2 onResize={() => editor?.layout()}>
|
||||
<ContextMenu2
|
||||
@@ -157,76 +300,10 @@ const Editor: React.VoidFunctionComponent<EditorProps> = ({ onEditorChanged }) =
|
||||
<MonacoEditor
|
||||
language={pybricksMicroPythonId}
|
||||
theme={isDarkMode ? tomorrowNightEightiesId : xcodeId}
|
||||
width="100%"
|
||||
height="100%"
|
||||
options={{
|
||||
fontSize: 18,
|
||||
minimap: { enabled: false },
|
||||
contextmenu: false,
|
||||
rulers: [80],
|
||||
}}
|
||||
editorDidMount={(editor) => {
|
||||
const subscriptions = new Array<IDisposable>();
|
||||
// FIXME: editor does not respond to changes in i18n
|
||||
subscriptions.push(
|
||||
new UntitledHintContribution(
|
||||
editor,
|
||||
i18n.translate(I18nId.Placeholder),
|
||||
),
|
||||
);
|
||||
subscriptions.push(
|
||||
editor.addAction({
|
||||
id: 'pybricks.action.toggleDocs',
|
||||
label: i18n.translate(I18nId.ToggleDocs),
|
||||
run: () => {
|
||||
// we have to use dispatch here instead of
|
||||
// toggleIsSettingShowDocsEnabled since this
|
||||
// isn't updated on state changes
|
||||
dispatch(settingsToggleShowDocs());
|
||||
},
|
||||
keybindings: [
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD,
|
||||
],
|
||||
}),
|
||||
);
|
||||
subscriptions.push(
|
||||
editor.addAction({
|
||||
id: 'pybricks.action.check',
|
||||
label: i18n.translate(I18nId.Check),
|
||||
// REVISIT: the compile options here might need to be changed - hopefully there is
|
||||
// one setting that works for all hub types for cases where we aren't connected.
|
||||
run: (e) => {
|
||||
dispatch(compile(e.getValue(), []));
|
||||
},
|
||||
keybindings: [monaco.KeyCode.F2],
|
||||
}),
|
||||
);
|
||||
subscriptions.push(
|
||||
editor.addAction({
|
||||
id: 'pybricks.action.save',
|
||||
label: 'Unused',
|
||||
run: () => {
|
||||
// We already automatically save the file
|
||||
// to local storage after every change, so
|
||||
// CTRL+S is ignored
|
||||
console.debug('Ctrl-S ignored');
|
||||
},
|
||||
keybindings: [
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
|
||||
],
|
||||
}),
|
||||
);
|
||||
editor.onDidDispose(() =>
|
||||
subscriptions.forEach((s) => s.dispose()),
|
||||
);
|
||||
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))}
|
||||
options={options}
|
||||
editorDidMount={handleEditorDidMount}
|
||||
editorWillUnmount={handleEditorWillUnmount}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</ContextMenu2>
|
||||
</ResizeSensor2>
|
||||
|
||||
Reference in New Issue
Block a user