diff --git a/css/scripteditor.scss b/css/scripteditor.scss index 56e04a6b9..1fe1412fe 100644 --- a/css/scripteditor.scss +++ b/css/scripteditor.scss @@ -47,7 +47,6 @@ #script-editor-filename-wrapper { background-color: #555; - margin-left: 6px; margin-right: 0; padding-left: 6px; width: 100%; diff --git a/package-lock.json b/package-lock.json index abb8e7733..67d2fa071 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "bitburner", - "version": "0.52.0", + "version": "0.52.5", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "0.52.0", + "version": "0.52.5", "hasInstallScript": true, "license": "SEE LICENSE IN license.txt", "dependencies": { "@material-ui/core": "^4.11.3", + "@monaco-editor/react": "^4.2.2", + "@types/js-beautify": "^1.13.2", "@types/numeral": "0.0.25", "@types/react": "^16.8.6", "@types/react-dom": "^16.8.2", @@ -688,6 +690,31 @@ "node": ">=8.0.0" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.1.1.tgz", + "integrity": "sha512-mkT4r4xDjIyOG9o9M6rJDSzEIeonwF80sYErxEvAAL4LncFVdcbNli8Qv6NDqF6nyv6sunuKkDzo4iFjxPL+uQ==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.2.2.tgz", + "integrity": "sha512-yDDct+f/mZ946tJEXudvmMC8zXDygkELNujpJGjqJ0gS3WePZmS/IwBBsuJ8JyKQQC3Dy/+Ivg1sSpW+UvCv9g==", + "dependencies": { + "@monaco-editor/loader": "^1.1.1", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -751,6 +778,11 @@ "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", "dev": true }, + "node_modules/@types/js-beautify": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/js-beautify/-/js-beautify-1.13.2.tgz", + "integrity": "sha512-crV/441NhrynLIclg94i1wV6nX/6rU9ByUyn4muCrsL0HPd3nBzrt6kpQ9MQOB+HeYgLcRARteNJcbnYkp5OwA==" + }, "node_modules/@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", @@ -10432,6 +10464,12 @@ "yarn": "*" } }, + "node_modules/monaco-editor": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.27.0.tgz", + "integrity": "sha512-UhwP78Wb8w0ZSYoKXQNTV/0CHObp6NS3nCt51QfKE6sKyBo5PBsvuDOHoI2ooBakc6uIwByRLHVeT7+yXQe2fQ==", + "peer": true + }, "node_modules/move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -15040,6 +15078,11 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/state-toggle": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz", @@ -19336,6 +19379,23 @@ "react-is": "^16.8.0 || ^17.0.0" } }, + "@monaco-editor/loader": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.1.1.tgz", + "integrity": "sha512-mkT4r4xDjIyOG9o9M6rJDSzEIeonwF80sYErxEvAAL4LncFVdcbNli8Qv6NDqF6nyv6sunuKkDzo4iFjxPL+uQ==", + "requires": { + "state-local": "^1.0.6" + } + }, + "@monaco-editor/react": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.2.2.tgz", + "integrity": "sha512-yDDct+f/mZ946tJEXudvmMC8zXDygkELNujpJGjqJ0gS3WePZmS/IwBBsuJ8JyKQQC3Dy/+Ivg1sSpW+UvCv9g==", + "requires": { + "@monaco-editor/loader": "^1.1.1", + "prop-types": "^15.7.2" + } + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -19386,6 +19446,11 @@ "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", "dev": true }, + "@types/js-beautify": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/js-beautify/-/js-beautify-1.13.2.tgz", + "integrity": "sha512-crV/441NhrynLIclg94i1wV6nX/6rU9ByUyn4muCrsL0HPd3nBzrt6kpQ9MQOB+HeYgLcRARteNJcbnYkp5OwA==" + }, "@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", @@ -27520,6 +27585,12 @@ } } }, + "monaco-editor": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.27.0.tgz", + "integrity": "sha512-UhwP78Wb8w0ZSYoKXQNTV/0CHObp6NS3nCt51QfKE6sKyBo5PBsvuDOHoI2ooBakc6uIwByRLHVeT7+yXQe2fQ==", + "peer": true + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -31433,6 +31504,11 @@ "safe-buffer": "^5.1.1" } }, + "state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "state-toggle": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz", diff --git a/package.json b/package.json index bbcaf2b8d..fda0a0d6f 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ }, "dependencies": { "@material-ui/core": "^4.11.3", + "@monaco-editor/react": "^4.2.2", + "@types/js-beautify": "^1.13.2", "@types/numeral": "0.0.25", "@types/react": "^16.8.6", "@types/react-dom": "^16.8.2", diff --git a/src/Fconf/Fconf.d.ts b/src/Fconf/Fconf.d.ts new file mode 100644 index 000000000..01f636bfd --- /dev/null +++ b/src/Fconf/Fconf.d.ts @@ -0,0 +1 @@ +export declare function parseFconfSettings(config: string): void; \ No newline at end of file diff --git a/src/IEngine.ts b/src/IEngine.ts index d95b89fed..a2961b683 100644 --- a/src/IEngine.ts +++ b/src/IEngine.ts @@ -13,4 +13,5 @@ export interface IEngine { loadMissionContent: () => void; loadResleevingContent: () => void; loadStockMarketContent: () => void; + loadTerminalContent: () => void; } diff --git a/src/Monaco/ui/Config.ts b/src/Monaco/ui/Config.ts new file mode 100644 index 000000000..de5530a69 --- /dev/null +++ b/src/Monaco/ui/Config.ts @@ -0,0 +1,3 @@ +export interface Config { + theme: string; +} \ No newline at end of file diff --git a/src/Monaco/ui/ConfigPopup.tsx b/src/Monaco/ui/ConfigPopup.tsx new file mode 100644 index 000000000..d9b72cc62 --- /dev/null +++ b/src/Monaco/ui/ConfigPopup.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; +import { Config } from "./Config"; +import { StdButton } from "../../ui/React/StdButton"; +import { removePopup } from "../../ui/React/createPopup"; + +interface IProps { + id: string; + config: Config; + save: (config: Config) => void; +} + +export function ConfigPopup(props: IProps): React.ReactElement { + const [config, setConfig] = useState(props.config); + function save() { + props.save(config); + removePopup(props.id); + } + + function setTheme(event: React.ChangeEvent): void { + setConfig(old => { + old.theme = event.target.value; + return old; + }); + } + + return (<> + +
+ + ); +} \ No newline at end of file diff --git a/src/Monaco/ui/Root.tsx b/src/Monaco/ui/Root.tsx new file mode 100644 index 000000000..6f99ab61a --- /dev/null +++ b/src/Monaco/ui/Root.tsx @@ -0,0 +1,204 @@ +import React, { useState, useEffect } from 'react'; +import { StdButton } from "../../ui/React/StdButton"; +import Editor from "@monaco-editor/react"; +import { createPopup } from "../../ui/React/createPopup"; +import { ConfigPopup } from "./ConfigPopup"; +import { Config } from "./Config"; +import { js_beautify as beautifyCode } from 'js-beautify'; +import { isValidFilePath } from "../../Terminal/DirectoryHelpers"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { IEngine } from "../../IEngine"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { parseFconfSettings } from "../../Fconf/Fconf"; +import { isScriptFilename } from "../../Script/ScriptHelpersTS"; +import { Script } from "../../Script/Script"; +import { TextFile } from "../../TextFile"; +import { calculateRamUsage } from "../../Script/RamCalculations"; +import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes"; +import { numeralWrapper } from "../../ui/numeralFormat"; + +interface IProps { + filename: string; + code: string; + player: IPlayer; + engine: IEngine; +}; + +// How to load function definition in monaco +// https://github.com/Microsoft/monaco-editor/issues/1415 +// https://microsoft.github.io/monaco-editor/api/modules/monaco.languages.html +// https://www.npmjs.com/package/@monaco-editor/react#development-playground + +export function Root(props: IProps): React.ReactElement { + const [filename, setFilename] = useState(props.filename); + const [code, setCode] = useState(props.code); + const [ram, setRAM] = useState(''); + const [config, setConfig] = useState({theme: 'vs-dark'}); + + function save(): void { + //CursorPositions.saveCursor(filename, cursor); + + // TODO(hydroflame): re-enable the tutorial. + // if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) { + // //Make sure filename + code properly follow tutorial + // if (filename !== "n00dles.script") { + // dialogBoxCreate("Leave the script name as 'n00dles'!"); + // return; + // } + // code = code.replace(/\s/g, ""); + // if (code.indexOf("while(true){hack('n00dles');}") == -1) { + // dialogBoxCreate("Please copy and paste the code from the tutorial!"); + // return; + // } + + // //Save the script + // let s = Player.getCurrentServer(); + // for (var i = 0; i < s.scripts.length; i++) { + // if (filename == s.scripts[i].filename) { + // s.scripts[i].saveScript(getCurrentEditor().getCode(), Player.currentServer, Player.getCurrentServer().scripts); + // Engine.loadTerminalContent(); + // return iTutorialNextStep(); + // } + // } + + // // If the current script does NOT exist, create a new one + // let script = new Script(); + // script.saveScript(getCurrentEditor().getCode(), Player.currentServer, Player.getCurrentServer().scripts); + // s.scripts.push(script); + + // return iTutorialNextStep(); + // } + + if (filename == "") { + dialogBoxCreate("You must specify a filename!"); + return; + } + + if (filename !== ".fconf" && !isValidFilePath(filename)) { + dialogBoxCreate("Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension."); + return; + } + + const s = props.player.getCurrentServer(); + if (filename === ".fconf") { + try { + parseFconfSettings(code); + } catch(e) { + dialogBoxCreate(`Invalid .fconf file: ${e}`); + return; + } + } else if (isScriptFilename(filename)) { + //If the current script already exists on the server, overwrite it + for (let i = 0; i < s.scripts.length; i++) { + if (filename == s.scripts[i].filename) { + s.scripts[i].saveScript(code, props.player.currentServer, props.player.getCurrentServer().scripts); + props.engine.loadTerminalContent(); + return; + } + } + + //If the current script does NOT exist, create a new one + const script = new Script(); + script.saveScript(code, props.player.currentServer, props.player.getCurrentServer().scripts); + s.scripts.push(script); + } else if (filename.endsWith(".txt")) { + for (let i = 0; i < s.textFiles.length; ++i) { + if (s.textFiles[i].fn === filename) { + s.textFiles[i].write(code); + props.engine.loadTerminalContent(); + return; + } + } + const textFile = new TextFile(filename, code); + s.textFiles.push(textFile); + } else { + dialogBoxCreate("Invalid filename. Must be either a script (.script) or " + + " or text file (.txt)") + return; + } + props.engine.loadTerminalContent(); + } + + function beautify(): void { + setCode(code => beautifyCode(code, { + indent_size: 4, + brace_style: "preserve-inline", + })); + } + + function onFilenameChange(event: React.ChangeEvent): void { + setFilename(event.target.value); + } + + function openConfig(): void { + const id="script-editor-config-options-popup"; + createPopup(id, ConfigPopup, { + id: id, + config: {theme: config.theme}, + save: (config: Config) => setConfig(config), + }); + } + + function updateCode(newCode?: string): void { + if(newCode === undefined) return; + + setCode(newCode); + } + + useEffect(() => { + async function updateRAM() { + const codeCopy = code.repeat(1); + const ramUsage = await calculateRamUsage(codeCopy, props.player.getCurrentServer().scripts); + if (ramUsage > 0) { + console.log(ramUsage); + setRAM("RAM: " + numeralWrapper.formatRAM(ramUsage)); + return; + } + switch (ramUsage) { + case RamCalculationErrorCode.ImportError: { + setRAM("RAM: Import Error"); + break; + } + case RamCalculationErrorCode.URLImportError: { + setRAM("RAM: HTTP Import Error"); + break; + } + case RamCalculationErrorCode.SyntaxError: + default: { + setRAM("RAM: Syntax Error"); + break; + } + } + return new Promise(() => undefined); + } + const id = setInterval(() => { + console.log(code); + updateRAM(); + }, 10000); + return () => clearInterval(id); + }, []); + + return (
+
+

Script name:

+ + +
+ Loading script editor!

} + height="80%" + defaultLanguage="javascript" + defaultValue={code} + value={code} + onChange={updateCode} + theme="vs-dark" + options={config} + /> +
+ +

{ram}

+ + Netscript Documentation +
+
); +} \ No newline at end of file diff --git a/src/Terminal.jsx b/src/Terminal.jsx index aae32f0a9..26c465eaa 100644 --- a/src/Terminal.jsx +++ b/src/Terminal.jsx @@ -100,6 +100,7 @@ import * as JSZip from "jszip"; import * as FileSaver from "file-saver"; import * as libarg from 'arg'; import React from "react"; +import ReactDOM from 'react-dom'; function postNetburnerText() { diff --git a/src/engine.jsx b/src/engine.jsx index 284127d25..9da5821e2 100644 --- a/src/engine.jsx +++ b/src/engine.jsx @@ -71,6 +71,7 @@ import { scriptEditorInit, updateScriptEditorContent, } from "./Script/ScriptHelpers"; +import { Root as ScriptEditorRoot } from "./Monaco/ui/Root"; import { initForeignServers, AllServers } from "./Server/AllServers"; import { Settings } from "./Settings/Settings"; import { updateSourceFileFlags } from "./SourceFile/SourceFileFlags"; @@ -260,14 +261,16 @@ const Engine = { loadScriptEditorContent: function(filename = "", code = "") { Engine.hideAllContent(); Engine.Display.scriptEditorContent.style.display = "block"; - try { - getCurrentEditor().openScript(filename, code); - } catch(e) { - exceptionAlert(e); - } - - updateScriptEditorContent(); routing.navigateTo(Page.ScriptEditor); + + + const monaco = document.getElementById('monaco-editor'); + //https://www.npmjs.com/package/@monaco-editor/react#development-playground + ReactDOM.render( + , + Engine.Display.scriptEditorContent, + ); + MainMenuLinks.ScriptEditor.classList.add("active"); }, @@ -504,6 +507,7 @@ const Engine = { Engine.Display.terminalContent.style.display = "none"; Engine.Display.characterContent.style.display = "none"; Engine.Display.scriptEditorContent.style.display = "none"; + ReactDOM.unmountComponentAtNode(Engine.Display.scriptEditorContent); Engine.Display.activeScriptsContent.style.display = "none"; ReactDOM.unmountComponentAtNode(Engine.Display.activeScriptsContent); diff --git a/src/index.html b/src/index.html index 6a1fd5f9d..4bc0c086a 100644 --- a/src/index.html +++ b/src/index.html @@ -125,11 +125,8 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %> -
-
-
- Key Buffer: -
+
+
@@ -175,6 +172,14 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
+ + + + + +