From 2265f797c45656c5699cb85463701365ccb76989 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 17 Dec 2021 07:36:33 -0600 Subject: [PATCH] Added the ability to open multiple script files at a time Fixed a bug where ram would stay the same when switching between scripts Fixed bug where updating ram could cause the GUI to crash The editor will now go to the last edited line when switching tabs. Tidied up the script editor UI --- src/ScriptEditor/ui/ScriptEditorRoot.tsx | 357 ++++++++++++++++------- 1 file changed, 259 insertions(+), 98 deletions(-) diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index db00ea9d8..4d0a97e18 100644 --- a/src/ScriptEditor/ui/ScriptEditorRoot.tsx +++ b/src/ScriptEditor/ui/ScriptEditorRoot.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect, useRef, useMemo } from "react"; -import Editor from "@monaco-editor/react"; +import Editor, { Monaco } from "@monaco-editor/react"; import * as monaco from "monaco-editor"; type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor; +type ITextModel = monaco.editor.ITextModel; import { OptionsModal } from "./OptionsModal"; import { Options } from "./Options"; import { isValidFilePath } from "../../Terminal/DirectoryHelpers"; @@ -34,6 +35,10 @@ import IconButton from "@mui/material/IconButton"; import SettingsIcon from "@mui/icons-material/Settings"; import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts"; +import { cssNumber } from "cypress/types/jquery"; +import { buttonBaseClasses } from "@mui/material"; +import { fromPairs } from "cypress/types/lodash"; +import { StringMatcher } from "cypress/types/net-stubbing"; let symbolsLoaded = false; let symbols: string[] = []; @@ -80,22 +85,32 @@ interface IProps { // https://github.com/threehams/typescript-error-guide/blob/master/stories/components/Editor.tsx#L11-L39 // https://blog.checklyhq.com/customizing-monaco/ -// These variables are used to reload a script when it's clicked on. Because we -// won't have references to the old script. -let lastFilename = ""; -let lastCode = ""; -let hostname = ""; -let lastPosition: monaco.Position | null = null; +// Holds all the data for a open script +class openScript { + fileName: string; + code: string; + hostname: string; + lastPosition: monaco.Position; + model: ITextModel; + + constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) { + this.fileName = fileName; + this.code = code; + this.hostname = hostname; + this.lastPosition = lastPosition; + this.model = model; + } +} + +const openScripts = new Array(); // Holds all open scripts +let currentScript = {} as openScript; // Script currently being viewed export function Root(props: IProps): React.ReactElement { const editorRef = useRef(null); - const [filename, setFilename] = useState(props.filename ? props.filename : lastFilename); - const [code, setCode] = useState(props.filename ? props.code : lastCode); + const monacoRef = useRef(null); + const [filename, setFilename] = useState(props.filename); + const [code, setCode] = useState(props.code); const [decorations, setDecorations] = useState([]); - hostname = props.filename ? props.hostname : hostname; - if (hostname === "") { - hostname = props.player.getCurrentServer().hostname; - } const [ram, setRAM] = useState("RAM: ???"); const [updatingRam, setUpdatingRam] = useState(false); const [optionsOpen, setOptionsOpen] = useState(false); @@ -105,6 +120,8 @@ export function Root(props: IProps): React.ReactElement { fontSize: Settings.MonacoFontSize, }); + + const debouncedSetRAM = useMemo( () => debounce((s) => { @@ -114,54 +131,21 @@ export function Root(props: IProps): React.ReactElement { [], ); - // store the last known state in case we need to restart without nano. - useEffect(() => { - if (props.filename === undefined) return; - lastFilename = props.filename; - lastCode = props.code; - lastPosition = null; - }, []); - function save(): void { - if (editorRef.current !== null) { - const position = editorRef.current.getPosition(); - if (position !== null) { - CursorPositions.saveCursor(filename, { - row: position.lineNumber, - column: position.column, - }); - } - } - lastPosition = null; - // this is duplicate code with saving later. if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) { //Make sure filename + code properly follow tutorial - if (filename !== "n00dles.script") { + if (currentScript.fileName !== "n00dles.script") { dialogBoxCreate("Leave the script name as 'n00dles.script'!"); return; } - if (code.replace(/\s/g, "").indexOf("while(true){hack('n00dles');}") == -1) { + if (currentScript.code.replace(/\s/g, "").indexOf("while(true){hack('n00dles');}") == -1) { dialogBoxCreate("Please copy and paste the code from the tutorial!"); return; } //Save the script - const server = GetServer(hostname); - if (server === null) throw new Error("Server should not be null but it is."); - let found = false; - for (let i = 0; i < server.scripts.length; i++) { - if (filename == server.scripts[i].filename) { - server.scripts[i].saveScript(filename, code, hostname, server.scripts); - found = true; - } - } - - if (!found) { - const script = new Script(); - script.saveScript(filename, code, hostname, server.scripts); - server.scripts.push(script); - } + saveScript(currentScript); iTutorialNextStep(); @@ -169,45 +153,43 @@ export function Root(props: IProps): React.ReactElement { return; } - if (filename == "") { + if (currentScript.fileName == "") { dialogBoxCreate("You must specify a filename!"); return; } - if (!isValidFilePath(filename)) { + if (!isValidFilePath(currentScript.fileName)) { dialogBoxCreate( "Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.", ); return; } - const server = GetServer(hostname); + const server = GetServer(currentScript.hostname); if (server === null) throw new Error("Server should not be null but it is."); - if (isScriptFilename(filename)) { + if (isScriptFilename(currentScript.fileName)) { //If the current script already exists on the server, overwrite it for (let i = 0; i < server.scripts.length; i++) { - if (filename == server.scripts[i].filename) { - server.scripts[i].saveScript(filename, code, props.player.currentServer, server.scripts); + if (currentScript.fileName == server.scripts[i].filename) { + server.scripts[i].saveScript(currentScript.fileName, currentScript.code, props.player.currentServer, server.scripts); if (Settings.SaveGameOnFileSave) saveObject.saveGame(); - props.router.toTerminal(); return; } } //If the current script does NOT exist, create a new one const script = new Script(); - script.saveScript(filename, code, props.player.currentServer, server.scripts); + script.saveScript(currentScript.fileName, currentScript.code, props.player.currentServer, server.scripts); server.scripts.push(script); - } else if (filename.endsWith(".txt")) { + } else if (currentScript.fileName.endsWith(".txt")) { for (let i = 0; i < server.textFiles.length; ++i) { - if (server.textFiles[i].fn === filename) { - server.textFiles[i].write(code); + if (server.textFiles[i].fn === currentScript.fileName) { + server.textFiles[i].write(currentScript.code); if (Settings.SaveGameOnFileSave) saveObject.saveGame(); - props.router.toTerminal(); return; } } - const textFile = new TextFile(filename, code); + const textFile = new TextFile(currentScript.fileName, currentScript.code); server.textFiles.push(textFile); } else { dialogBoxCreate("Invalid filename. Must be either a script (.script, .js, or .ns) or " + " or text file (.txt)"); @@ -215,7 +197,6 @@ export function Root(props: IProps): React.ReactElement { } if (Settings.SaveGameOnFileSave) saveObject.saveGame(); - props.router.toTerminal(); } function beautify(): void { @@ -223,14 +204,9 @@ export function Root(props: IProps): React.ReactElement { editorRef.current.getAction("editor.action.formatDocument").run(); } - function onFilenameChange(event: React.ChangeEvent): void { - lastFilename = event.target.value; - setFilename(event.target.value); - } - function infLoop(newCode: string): void { if (editorRef.current === null) return; - if (!filename.endsWith(".ns") && !filename.endsWith(".js")) return; + if (!currentScript.fileName.endsWith(".ns") && !currentScript.fileName.endsWith(".js")) return; const awaitWarning = checkInfiniteLoop(newCode); if (awaitWarning !== -1) { const newDecorations = editorRef.current.deltaDecorations(decorations, [ @@ -259,20 +235,18 @@ export function Root(props: IProps): React.ReactElement { function updateCode(newCode?: string): void { if (newCode === undefined) return; - lastCode = newCode; - setCode(newCode); updateRAM(newCode); + currentScript.code = newCode; try { if (editorRef.current !== null) { - lastPosition = editorRef.current.getPosition(); infLoop(newCode); } - } catch (err) {} + } catch (err) { } } // calculate it once the first time the file is loaded. useEffect(() => { - updateRAM(code); + updateRAM(currentScript.code); }, []); async function updateRAM(newCode: string): Promise { @@ -314,8 +288,64 @@ export function Root(props: IProps): React.ReactElement { return () => document.removeEventListener("keydown", maybeSave); }); - function onMount(editor: IStandaloneCodeEditor): void { + // Generates a new model for the script + function regenerateModel(script: openScript) { + if (monacoRef.current !== null) { + script.model = monacoRef.current.editor.createModel(script.code, 'javascript'); + } + } + + // Sets the currently viewed script + function setCurrentScript(script: openScript) { + // Update last position + if (editorRef.current !== null) { + if (currentScript !== null) { + var currentPosition = editorRef.current.getPosition(); + if (currentPosition !== null) { + currentScript.lastPosition = currentPosition; + } + } + + editorRef.current.setModel(script.model); + currentScript = script; + editorRef.current.setPosition(currentScript.lastPosition); + editorRef.current.revealLine(currentScript.lastPosition.lineNumber); + updateRAM(currentScript.code); + } + } + + // Gets a currently opened script + function getOpenedScript(fileName: string, hostname: string) { + for (const script of openScripts) { + if (script.fileName === fileName && script.hostname === hostname) { + return script; + } + } + + return null; + } + + function saveScript(script: openScript) { + const server = GetServer(script.hostname); + if (server === null) throw new Error("Server should not be null but it is."); + let found = false; + for (let i = 0; i < server.scripts.length; i++) { + if (script.fileName == server.scripts[i].filename) { + server.scripts[i].saveScript(script.fileName, script.code, script.hostname, server.scripts); + found = true; + } + } + + if (!found) { + const newScript = new Script(); + newScript.saveScript(script.fileName, script.code, script.hostname, server.scripts); + server.scripts.push(newScript); + } + } + + function onMount(editor: IStandaloneCodeEditor, monaco: Monaco): void { editorRef.current = editor; + monacoRef.current = monaco; if (editorRef.current === null) return; const position = CursorPositions.getCursor(filename); if (position.row !== -1) @@ -323,12 +353,44 @@ export function Root(props: IProps): React.ReactElement { lineNumber: position.row, column: position.column, }); - else if (lastPosition !== null) - editorRef.current.setPosition({ - lineNumber: lastPosition.lineNumber, - column: lastPosition.column + 1, - }); editorRef.current.focus(); + + const script = getOpenedScript(filename, props.player.getCurrentServer().hostname); + + // Check if script is already opened, if so switch to that model + if (script !== null) { + if (script.model.isDisposed()) { + regenerateModel(script); + } + + setCurrentScript(script); + } else { + if (filename !== undefined) { + // Create new model + if (monacoRef.current !== null) { + var newScript = new openScript(filename, code, props.player.getCurrentServer().hostname, new monaco.Position(0, 0), monacoRef.current.editor.createModel(code, 'javascript')); + setCurrentScript(newScript); + openScripts.push(newScript); + } + } else { + // Script Editor was opened by the sidebar button + if (currentScript.model !== undefined) { + if (currentScript.model.isDisposed()) { + // Create new model, old one was disposed of + regenerateModel(currentScript); + } + + setCurrentScript(currentScript); + } else { + // Create a new temporary file + if (monacoRef.current !== null) { + var newScript = new openScript('NewFile.ns', '', props.player.getCurrentServer().hostname, new monaco.Position(0, 0), monacoRef.current.editor.createModel('', 'javascript')); + setCurrentScript(newScript); + openScripts.push(newScript); + } + } + } + } } function beforeMount(monaco: any): void { @@ -368,26 +430,119 @@ export function Root(props: IProps): React.ReactElement { monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts"); loadThemes(monaco); } + + // Change tab highlight from old tab to new tab + function changeTabButtonColor(oldButtonFileName: string, oldButtonHostname: string, newButtonFileName: string, newButtonHostname: string) { + const oldTabButton = document.getElementById('tabButton' + oldButtonFileName + oldButtonHostname); + if (oldTabButton !== null) { + oldTabButton.style.backgroundColor = ''; + } + + const oldTabCloseButton = document.getElementById('tabCloseButton' + oldButtonFileName + oldButtonHostname); + if (oldTabCloseButton !== null) { + oldTabCloseButton.style.backgroundColor = ''; + } + + const newTabButton = document.getElementById('tabButton' + newButtonFileName + newButtonHostname); + if (newTabButton !== null) { + newTabButton.style.backgroundColor = '#173b2d'; + } + + const newTabCloseButton = document.getElementById('tabCloseButton' + newButtonFileName + newButtonHostname); + if (newTabCloseButton !== null) { + newTabCloseButton.style.backgroundColor = '#173b2d'; + } + } + + // Called when a script tab was clicked + function onTabButtonClick(e: React.MouseEvent) { + const valSplit = e.currentTarget.value.split(':'); + const fileName = valSplit[0]; + const hostname = valSplit[1]; + + // Change tab highlight from old tab to new tab + changeTabButtonColor(currentScript.fileName, currentScript.hostname, fileName, hostname) + + + // Update current script + const clickedScript = getOpenedScript(fileName, hostname); + + if (clickedScript !== null) { + if (clickedScript.model.isDisposed()) { + regenerateModel(clickedScript); + } + + setCurrentScript(clickedScript); + } + } + + // Called when a script tab close button was clicked + function onCloseButtonClick(e: React.MouseEvent) { + const valSplit = e.currentTarget.value.split(':'); + const fileName = valSplit[0]; + const hostname = valSplit[1]; + + const scriptToClose = getOpenedScript(fileName, hostname); + + // Save and remove script from openScripts + if (scriptToClose !== null) { + saveScript(scriptToClose); + + openScripts.splice(openScripts.indexOf(scriptToClose), 1); + } + + if (openScripts.length === 0) { + // No other scripts are open, create a new temporary file + if (monacoRef.current !== null) { + const newScript = new openScript("NewFile.ns", '', props.player.getCurrentServer().hostname, new monacoRef.current.Position(0, 0), monacoRef.current.editor.createModel('', 'javascript')); + + setCurrentScript(newScript) + openScripts.push(newScript); + + // Create new tab button for temporary file + const element = (
) + + // Modify button for temp file + var parent = e.currentTarget.parentElement; + if (parent !== null) { + (parent.children[0] as HTMLButtonElement).value = 'NewFile.ns:home'; + (parent.children[0] as HTMLButtonElement).textContent = 'NewFile.ns'; + e.currentTarget.value = 'NewFile.ns:home'; + } + } + } else { + if (openScripts[0].model.isDisposed()) { + regenerateModel(openScripts[0]); + } + + changeTabButtonColor(currentScript.fileName, currentScript.hostname, openScripts[0].fileName, openScripts[0].hostname); + + setCurrentScript(openScripts[0]); + } + } + + // Generate a button for each open script + const scriptButtons = []; + for (let i = 0; i < openScripts.length; i++) { + if (openScripts[i].fileName !== '') { + const fileName2 = openScripts[i].fileName; + const hostname = openScripts[i].hostname; + if (openScripts[i].fileName === currentScript.fileName && openScripts[i].hostname === currentScript.hostname) { + // Set special background color for current script tab button + scriptButtons.push(
) + } else { + scriptButtons.push(
) + } + } + } + // 370px 71%, 725px 85.1%, 1085px 90%, 1300px 91.7% // fuck around in desmos until you find a function const p = 11000 / -window.innerHeight + 100; return ( <> - {hostname}:~/ }} - /> - setOptionsOpen(true)}> - <> - - options - - + {scriptButtons} - + {ram} - - + + {" "} Documentation:{" "} @@ -417,6 +572,12 @@ export function Root(props: IProps): React.ReactElement { Full + setOptionsOpen(true)}> + <> + + options + + ); -} +} \ No newline at end of file