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:
David Lechner
2022-03-25 17:49:56 -05:00
parent 2be1c26f82
commit 3c48d115d7
+152 -75
View File
@@ -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>