feat: Add vim mode to script editor

This adds an option to turn on vim mode using the `monaco-vim` library.
This commit is contained in:
Billy Vong 2021-12-17 12:34:00 -05:00
parent 3436873373
commit fb5d374279
7 changed files with 78 additions and 9 deletions

7
dist/ext/monaco-vim.js vendored Normal file

File diff suppressed because one or more lines are too long

@ -17,12 +17,13 @@
<link rel="stylesheet" data-name="vs/editor/editor.main" href="dist/ext/monaco-editor/min/vs/editor/editor.main.css"/> <link rel="stylesheet" data-name="vs/editor/editor.main" href="dist/ext/monaco-editor/min/vs/editor/editor.main.css"/>
<script> <script>
var require = { paths: { vs: "dist/ext/monaco-editor/min/vs" } }; var require = { paths: { vs: "dist/ext/monaco-editor/min/vs", "monaco-vim": "dist/ext/monaco-vim" } };
</script> </script>
<script src="dist/ext/monaco-editor/min/vs/loader.js"></script> <script src="dist/ext/monaco-editor/min/vs/loader.js"></script>
<script src="dist/ext/monaco-editor/min/vs/editor/editor.main.nls.js"></script> <script src="dist/ext/monaco-editor/min/vs/editor/editor.main.nls.js"></script>
<script src="dist/ext/monaco-editor/min/vs/editor/editor.main.js"></script> <script src="dist/ext/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="dist/ext/monaco-vim.js"></script>
<!-- Google Analytics --> <!-- Google Analytics -->
<script> <script>

@ -2,4 +2,5 @@ export interface Options {
theme: string; theme: string;
insertSpaces: boolean; insertSpaces: boolean;
fontSize: number; fontSize: number;
vim: boolean;
} }

@ -21,12 +21,14 @@ export function OptionsModal(props: IProps): React.ReactElement {
const [theme, setTheme] = useState(props.options.theme); const [theme, setTheme] = useState(props.options.theme);
const [insertSpaces, setInsertSpaces] = useState(props.options.insertSpaces); const [insertSpaces, setInsertSpaces] = useState(props.options.insertSpaces);
const [fontSize, setFontSize] = useState(props.options.fontSize); const [fontSize, setFontSize] = useState(props.options.fontSize);
const [vim, setVim] = useState(props.options.vim);
function save(): void { function save(): void {
props.save({ props.save({
theme: theme, theme,
insertSpaces: insertSpaces, insertSpaces,
fontSize: fontSize, fontSize,
vim,
}); });
props.onClose(); props.onClose();
} }
@ -54,6 +56,12 @@ export function OptionsModal(props: IProps): React.ReactElement {
<Typography>Use whitespace over tabs: </Typography> <Typography>Use whitespace over tabs: </Typography>
<Switch onChange={(event) => setInsertSpaces(event.target.checked)} checked={insertSpaces} /> <Switch onChange={(event) => setInsertSpaces(event.target.checked)} checked={insertSpaces} />
</Box> </Box>
<Box display="flex" flexDirection="row" alignItems="center">
<Typography>Enable vim mode: </Typography>
<Switch onChange={(event) => setVim(event.target.checked)} checked={vim} />
</Box>
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
<TextField type="number" label="Font size" value={fontSize} onChange={onFontChange} /> <TextField type="number" label="Font size" value={fontSize} onChange={onFontChange} />
</Box> </Box>

@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef, useMemo } from "react"; import React, { useState, useEffect, useRef, useMemo } from "react";
import Editor from "@monaco-editor/react"; import Editor from "@monaco-editor/react";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor; type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
import { OptionsModal } from "./OptionsModal"; import { OptionsModal } from "./OptionsModal";
import { Options } from "./Options"; import { Options } from "./Options";
@ -86,9 +87,13 @@ let lastFilename = "";
let lastCode = ""; let lastCode = "";
let hostname = ""; let hostname = "";
let lastPosition: monaco.Position | null = null; let lastPosition: monaco.Position | null = null;
// let vimEditor: any | null = null;
export function Root(props: IProps): React.ReactElement { export function Root(props: IProps): React.ReactElement {
const editorRef = useRef<IStandaloneCodeEditor | null>(null); const editorRef = useRef<IStandaloneCodeEditor | null>(null);
const vimStatusRef = useRef<HTMLElement>(null);
const [vimEditor, setVimEditor] = useState<any>(null);
const [editor, setEditor] = useState<IStandaloneCodeEditor|null>(null);
const [filename, setFilename] = useState(props.filename ? props.filename : lastFilename); const [filename, setFilename] = useState(props.filename ? props.filename : lastFilename);
const [code, setCode] = useState<string>(props.filename ? props.code : lastCode); const [code, setCode] = useState<string>(props.filename ? props.code : lastCode);
const [decorations, setDecorations] = useState<string[]>([]); const [decorations, setDecorations] = useState<string[]>([]);
@ -103,6 +108,7 @@ export function Root(props: IProps): React.ReactElement {
theme: Settings.MonacoTheme, theme: Settings.MonacoTheme,
insertSpaces: Settings.MonacoInsertSpaces, insertSpaces: Settings.MonacoInsertSpaces,
fontSize: Settings.MonacoFontSize, fontSize: Settings.MonacoFontSize,
vim: Settings.MonacoVim,
}); });
const debouncedSetRAM = useMemo( const debouncedSetRAM = useMemo(
@ -314,7 +320,39 @@ export function Root(props: IProps): React.ReactElement {
return () => document.removeEventListener("keydown", maybeSave); return () => document.removeEventListener("keydown", maybeSave);
}); });
useEffect(() => {
// setup monaco-vim
if (options.vim && editor && !vimEditor) {
try {
// This library is not typed
// @ts-expect-error
window.require(["monaco-vim"], function (MonacoVim: any) {
setVimEditor(MonacoVim.initVimMode(editor, vimStatusRef.current));
MonacoVim.VimMode.Vim.defineEx('write', 'w', function() {
// your own implementation on what you want to do when :w is pressed
save();
});
MonacoVim.VimMode.Vim.defineEx('quit', 'q', function() {
save();
});
editor.focus();
});
} catch {}
} else if (!options.vim) {
// Whem vim mode is disabled
vimEditor?.dispose();
setVimEditor(null);
}
return () => {
vimEditor?.dispose();
}
}, [ options, editorRef, editor, vimEditor])
function onMount(editor: IStandaloneCodeEditor): void { function onMount(editor: IStandaloneCodeEditor): void {
// Required when switching between site navigation (e.g. from Script Editor -> Terminal and back)
// the `useEffect()` for vim mode is called before editor is mounted.
setEditor(editor);
editorRef.current = editor; editorRef.current = editor;
if (editorRef.current === null) return; if (editorRef.current === null) return;
const position = CursorPositions.getCursor(filename); const position = CursorPositions.getCursor(filename);
@ -328,6 +366,7 @@ export function Root(props: IProps): React.ReactElement {
lineNumber: lastPosition.lineNumber, lineNumber: lastPosition.lineNumber,
column: lastPosition.column + 1, column: lastPosition.column + 1,
}); });
editorRef.current.focus(); editorRef.current.focus();
} }
@ -368,9 +407,13 @@ export function Root(props: IProps): React.ReactElement {
monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts"); monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts");
loadThemes(monaco); loadThemes(monaco);
} }
// 370px 71%, 725px 85.1%, 1085px 90%, 1300px 91.7%
// fuck around in desmos until you find a function // TODO: Make this responsive to window resizes
const p = 11000 / -window.innerHeight + 100; // Toolbars are roughly 108px + vim bar 34px
// Get percentage of space that toolbars represent and the rest should be the
// editor
const editorHeight = 100 - (((108 + (options.vim ? 34 : 0)) / window.innerHeight) * 100);
return ( return (
<> <>
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
@ -393,13 +436,16 @@ export function Root(props: IProps): React.ReactElement {
beforeMount={beforeMount} beforeMount={beforeMount}
onMount={onMount} onMount={onMount}
loading={<Typography>Loading script editor!</Typography>} loading={<Typography>Loading script editor!</Typography>}
height={p + "%"} height={`${editorHeight}%`}
defaultLanguage="javascript" defaultLanguage="javascript"
defaultValue={code} defaultValue={code}
onChange={updateCode} onChange={updateCode}
theme={options.theme} theme={options.theme}
options={{ ...options, glyphMargin: true }} options={{ ...options, glyphMargin: true }}
/> />
<Box ref={vimStatusRef} className="monaco-editor" display="flex" flexDirection="row" sx={{ p: 1 }} alignItems="center"></Box>
<Box display="flex" flexDirection="row" sx={{ m: 1 }} alignItems="center"> <Box display="flex" flexDirection="row" sx={{ m: 1 }} alignItems="center">
<Button onClick={beautify}>Beautify</Button> <Button onClick={beautify}>Beautify</Button>
<Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}> <Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}>
@ -425,12 +471,14 @@ export function Root(props: IProps): React.ReactElement {
theme: Settings.MonacoTheme, theme: Settings.MonacoTheme,
insertSpaces: Settings.MonacoInsertSpaces, insertSpaces: Settings.MonacoInsertSpaces,
fontSize: Settings.MonacoFontSize, fontSize: Settings.MonacoFontSize,
vim: Settings.MonacoVim,
}} }}
save={(options: Options) => { save={(options: Options) => {
setOptions(options); setOptions(options);
Settings.MonacoTheme = options.theme; Settings.MonacoTheme = options.theme;
Settings.MonacoInsertSpaces = options.insertSpaces; Settings.MonacoInsertSpaces = options.insertSpaces;
Settings.MonacoFontSize = options.fontSize; Settings.MonacoFontSize = options.fontSize;
Settings.MonacoVim = options.vim;
}} }}
/> />
</> </>

@ -163,6 +163,8 @@ interface ISettings extends IDefaultSettings {
MonacoInsertSpaces: boolean; MonacoInsertSpaces: boolean;
MonacoFontSize: number; MonacoFontSize: number;
MonacoVim: boolean;
} }
export const defaultSettings: IDefaultSettings = { export const defaultSettings: IDefaultSettings = {
@ -254,6 +256,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
MonacoTheme: "monokai", MonacoTheme: "monokai",
MonacoInsertSpaces: false, MonacoInsertSpaces: false,
MonacoFontSize: 20, MonacoFontSize: 20,
MonacoVim: false,
theme: { theme: {
primarylight: defaultSettings.theme.primarylight, primarylight: defaultSettings.theme.primarylight,

@ -21,12 +21,13 @@
/> />
<script> <script>
var require = { paths: { vs: "dist/ext/monaco-editor/min/vs" } }; var require = { paths: { vs: "dist/ext/monaco-editor/min/vs", "monaco-vim": "dist/ext/monaco-vim" } };
</script> </script>
<script src="dist/ext/monaco-editor/min/vs/loader.js"></script> <script src="dist/ext/monaco-editor/min/vs/loader.js"></script>
<script src="dist/ext/monaco-editor/min/vs/editor/editor.main.nls.js"></script> <script src="dist/ext/monaco-editor/min/vs/editor/editor.main.nls.js"></script>
<script src="dist/ext/monaco-editor/min/vs/editor/editor.main.js"></script> <script src="dist/ext/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="dist/ext/monaco-vim.js"></script>
<!-- Google Analytics --> <!-- Google Analytics -->
<script> <script>