EDITOR: useVimEditor uses Material UI (#1332)

This commit is contained in:
G4mingJon4s 2024-06-06 03:30:03 +02:00 committed by GitHub
parent cf48d666f5
commit 463d4cdb1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 212 additions and 20 deletions

@ -373,7 +373,7 @@ function Root(props: IProps): React.ReactElement {
} }
} }
const { VimStatus } = useVimEditor({ const { statusBarRef } = useVimEditor({
editor: editorRef.current, editor: editorRef.current,
vim: options.vim, vim: options.vim,
onSave: save, onSave: save,
@ -411,7 +411,7 @@ function Root(props: IProps): React.ReactElement {
<div style={{ flex: "0 0 5px" }} /> <div style={{ flex: "0 0 5px" }} />
<Editor onMount={onMount} onChange={updateCode} onUnmount={onUnmountEditor} /> <Editor onMount={onMount} onChange={updateCode} onUnmount={onUnmountEditor} />
{VimStatus} {statusBarRef.current}
<Toolbar onSave={save} editor={editorRef.current} /> <Toolbar onSave={save} editor={editorRef.current} />
</div> </div>

@ -0,0 +1,199 @@
import { Input, Typography } from "@mui/material";
import { styled } from "@mui/material/styles";
import type { editor } from "monaco-editor";
import React from "react";
type IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
const StatusBarContainer = styled("div")({
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
height: 36,
marginLeft: 4,
marginRight: 4,
});
const StatusBarLeft = styled("div")({
display: "flex",
flexDirection: "row",
alignItems: "center",
});
// This Class is injected into the monaco-vim initVimMode function to override the status bar.
export class StatusBar {
// modeInfoNode is used to display the mode in the status bar.
// notifNode is used to display notifications in the status bar.
// secInfoNode is used to display the input box in the status bar.
// keyInfoNode is used to display the operator and count in the status bar.
// editor is kept to focus when closing the input box.
// sanitizer is weird.
node: React.MutableRefObject<React.ReactElement | null>;
editor: IStandaloneCodeEditor;
mode = "--NORMAL--";
showInput = false;
inputValue = "";
buffer = "";
rerender: () => void;
onCloseHandler: ((query: string) => void) | null;
onKeyDownHandler: ((e: React.KeyboardEvent, query: string, close: () => void) => void) | null;
onKeyUpHandler: ((e: React.KeyboardEvent, query: string, close: () => void) => void) | null;
// node is used to setup the status bar. However, we use it to forward the resulting status bar to the outside.
// sanitizer is weird, so we use it to forward a rerender hook.
constructor(
node: React.MutableRefObject<React.ReactElement | null>,
editor: IStandaloneCodeEditor,
rerender: () => void,
) {
this.node = node;
this.editor = editor;
this.rerender = rerender;
this.onCloseHandler = null;
this.onKeyDownHandler = null;
this.onKeyUpHandler = null;
this.update();
}
// this is used to change the mode shown in the status bar.
setMode(ev: { mode: string; subMode?: string }) {
if (ev.mode === "visual") {
if (ev.subMode === "linewise") {
this.mode = "--VISUAL LINE--";
} else if (ev.subMode === "blockwise") {
this.mode = "--VISUAL BLOCK--";
} else {
this.mode = "--VISUAL--";
}
} else {
this.mode = `--${ev.mode.toUpperCase()}--`;
}
this.update();
}
// this is used to set the current operator, like d, r, etc or the count, like 5j, 3w, etc.
setKeyBuffer(key: string) {
this.buffer = key;
this.update();
}
// this is used to set the input box.
setSec(
// this is the created HTML element from monaco-vim. We're not going to use it, so it is marked as unused.
__text: HTMLElement,
// this is used to close the input box and set the cursor back to the line in the monaco-editor. query is the text in the input box.
onClose: ((query: string) => void) | null,
options: {
// This handles ESC, Backspace when input is empty, CTRL-C and CTRL-[. query is the text in the input box. close is a function that closes the input box. e is the key event.
onKeyDown: ((e: React.KeyboardEvent, query: string, close: () => void) => void) | undefined;
// This handles all other key events. query is the text in the input box. close is a function that closes the input box. e is the key event.
onKeyUp: ((e: React.KeyboardEvent, query: string, close: () => void) => void) | undefined;
// this is a default value for the input box. The box should be empty if this is not set.
value: string | undefined;
},
) {
this.onCloseHandler = onClose;
this.onKeyDownHandler = options.onKeyDown ?? null;
this.onKeyUpHandler = options.onKeyUp ?? null;
this.inputValue = options.value || "";
this.showInput = true;
this.update();
}
// this is used to toggle showing the status bar.
toggleVisibility(toggle: boolean) {
if (toggle) {
this.node.current = this.StatusBar();
} else {
this.node.current = null;
}
}
// this is used to close the input box.
closeInput = () => {
this.showInput = false;
this.update();
this.editor.focus();
};
// this is used to clean up the status bar on unmount.
clear = () => {
this.node.current = null;
};
// this is used to show notifications. In game, these won't be rendered, so the function is empty.
showNotification(__text: HTMLElement) {
return;
}
update = () => {
this.node.current = this.StatusBar();
this.rerender();
};
keyUp = (e: React.KeyboardEvent) => {
if (this.onKeyUpHandler !== null) {
this.onKeyUpHandler(e, this.inputValue, this.closeInput);
} else {
// if the player somehow gets stuck here, they can also press enter to close the input box.
if (e.key === "Enter") this.closeInput();
}
};
keyDown = (e: React.KeyboardEvent) => {
if (this.onKeyDownHandler !== null) {
this.onKeyDownHandler(e, this.inputValue, this.closeInput);
}
// this handles pressing escape in the input box.
if (e.key === "Escape") {
e.stopPropagation();
this.closeInput();
}
// this handles pressing enter in the input box.
if (e.key === "Enter" && this.onCloseHandler !== null) {
e.stopPropagation();
e.preventDefault();
this.onCloseHandler(this.inputValue);
this.closeInput();
}
};
handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
this.inputValue = e.target.value;
this.update();
};
StatusBar(): React.ReactElement {
return (
<StatusBarContainer>
<StatusBarLeft>
<Typography sx={{ mr: 4 }}>{this.mode}</Typography>
{this.showInput && (
<Input
value={this.inputValue}
onChange={this.handleInput}
onKeyUp={this.keyUp}
onKeyDown={this.keyDown}
autoFocus={true}
/>
)}
{!this.showInput && <div />}
</StatusBarLeft>
<Typography>{this.buffer}</Typography>
</StatusBarContainer>
);
}
}

@ -4,10 +4,10 @@ import * as MonacoVim from "monaco-vim";
import type { editor } from "monaco-editor"; import type { editor } from "monaco-editor";
type IStandaloneCodeEditor = editor.IStandaloneCodeEditor; type IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
import Box from "@mui/material/Box";
import { Router } from "../../ui/GameRoot"; import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router"; import { Page } from "../../ui/Router";
import { StatusBar } from "./StatusBar";
import { useRerender } from "../../ui/React/hooks";
interface IProps { interface IProps {
vim: boolean; vim: boolean;
@ -21,7 +21,8 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
// monaco-vim does not have types, so this is an any // monaco-vim does not have types, so this is an any
const [vimEditor, setVimEditor] = useState<any>(null); const [vimEditor, setVimEditor] = useState<any>(null);
const vimStatusRef = useRef<HTMLElement>(null); const statusBarRef = useRef<React.ReactElement | null>(null);
const rerender = useRerender();
const actionsRef = useRef({ save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab }); const actionsRef = useRef({ save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab });
actionsRef.current = { save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab }; actionsRef.current = { save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab };
@ -31,7 +32,7 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
if (vim && editor && !vimEditor) { if (vim && editor && !vimEditor) {
// Using try/catch because MonacoVim does not have types. // Using try/catch because MonacoVim does not have types.
try { try {
setVimEditor(MonacoVim.initVimMode(editor, vimStatusRef.current)); setVimEditor(MonacoVim.initVimMode(editor, statusBarRef, StatusBar, rerender));
MonacoVim.VimMode.Vim.defineEx("write", "w", function () { MonacoVim.VimMode.Vim.defineEx("write", "w", function () {
// your own implementation on what you want to do when :w is pressed // your own implementation on what you want to do when :w is pressed
actionsRef.current.save(); actionsRef.current.save();
@ -40,6 +41,10 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
Router.toPage(Page.Terminal); Router.toPage(Page.Terminal);
}); });
// Remove any macro recording, since it isn't supported.
MonacoVim.VimMode.Vim.mapCommand("q", "", "", null, { context: "normal" });
MonacoVim.VimMode.Vim.mapCommand("@", "", "", null, { context: "normal" });
const saveNQuit = (): void => { const saveNQuit = (): void => {
actionsRef.current.save(); actionsRef.current.save();
Router.toPage(Page.Terminal); Router.toPage(Page.Terminal);
@ -72,19 +77,7 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
return () => { return () => {
vimEditor?.dispose(); vimEditor?.dispose();
}; };
}, [vim, editor, vimEditor]); }, [vim, editor, vimEditor, rerender]);
const VimStatus = ( return { statusBarRef };
<Box
ref={vimStatusRef}
className="vim-display"
display="flex"
flexGrow="0"
flexDirection="row"
sx={{ p: 1 }}
alignItems="center"
/>
);
return { VimStatus };
} }