- {VimStatus}
+ {statusBarRef.current}
diff --git a/src/ScriptEditor/ui/StatusBar.tsx b/src/ScriptEditor/ui/StatusBar.tsx
new file mode 100644
index 000000000..521ad6096
--- /dev/null
+++ b/src/ScriptEditor/ui/StatusBar.tsx
@@ -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;
+ 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,
+ 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) => {
+ this.inputValue = e.target.value;
+
+ this.update();
+ };
+
+ StatusBar(): React.ReactElement {
+ return (
+
+
+ {this.mode}
+ {this.showInput && (
+
+ )}
+ {!this.showInput && }
+
+ {this.buffer}
+
+ );
+ }
+}
diff --git a/src/ScriptEditor/ui/useVimEditor.tsx b/src/ScriptEditor/ui/useVimEditor.tsx
index 3157457b3..4ad98b809 100644
--- a/src/ScriptEditor/ui/useVimEditor.tsx
+++ b/src/ScriptEditor/ui/useVimEditor.tsx
@@ -4,10 +4,10 @@ import * as MonacoVim from "monaco-vim";
import type { editor } from "monaco-editor";
type IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
-import Box from "@mui/material/Box";
-
import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router";
+import { StatusBar } from "./StatusBar";
+import { useRerender } from "../../ui/React/hooks";
interface IProps {
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
const [vimEditor, setVimEditor] = useState(null);
- const vimStatusRef = useRef(null);
+ const statusBarRef = useRef(null);
+ const rerender = useRerender();
const actionsRef = useRef({ 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) {
// Using try/catch because MonacoVim does not have types.
try {
- setVimEditor(MonacoVim.initVimMode(editor, vimStatusRef.current));
+ setVimEditor(MonacoVim.initVimMode(editor, statusBarRef, StatusBar, rerender));
MonacoVim.VimMode.Vim.defineEx("write", "w", function () {
// your own implementation on what you want to do when :w is pressed
actionsRef.current.save();
@@ -40,6 +41,10 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
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 => {
actionsRef.current.save();
Router.toPage(Page.Terminal);
@@ -72,19 +77,7 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
return () => {
vimEditor?.dispose();
};
- }, [vim, editor, vimEditor]);
+ }, [vim, editor, vimEditor, rerender]);
- const VimStatus = (
-
- );
-
- return { VimStatus };
+ return { statusBarRef };
}