diff --git a/src/ScriptEditor/ui/ScriptEditorRoot.tsx b/src/ScriptEditor/ui/ScriptEditorRoot.tsx index db00ea9d8..294d6a7a0 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