diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index 202599703..1bb31320c 100644 --- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx +++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx @@ -373,7 +373,7 @@ function Root(props: IProps): React.ReactElement { } } - const { VimStatus } = useVimEditor({ + const { statusBarRef } = useVimEditor({ editor: editorRef.current, vim: options.vim, onSave: save, @@ -411,7 +411,7 @@ function Root(props: IProps): React.ReactElement {
- {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 }; }