first pass at monaco.

This commit is contained in:
Olivier Gagnon 2021-08-20 01:21:37 -04:00
parent 567c5dc230
commit 73ec97db87
11 changed files with 345 additions and 15 deletions

@ -47,7 +47,6 @@
#script-editor-filename-wrapper {
background-color: #555;
margin-left: 6px;
margin-right: 0;
padding-left: 6px;
width: 100%;

80
package-lock.json generated

@ -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",

@ -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",

1
src/Fconf/Fconf.d.ts vendored Normal file

@ -0,0 +1 @@
export declare function parseFconfSettings(config: string): void;

@ -13,4 +13,5 @@ export interface IEngine {
loadMissionContent: () => void;
loadResleevingContent: () => void;
loadStockMarketContent: () => void;
loadTerminalContent: () => void;
}

3
src/Monaco/ui/Config.ts Normal file

@ -0,0 +1,3 @@
export interface Config {
theme: string;
}

@ -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<Config>(props.config);
function save() {
props.save(config);
removePopup(props.id);
}
function setTheme(event: React.ChangeEvent<HTMLSelectElement>): void {
setConfig(old => {
old.theme = event.target.value;
return old;
});
}
return (<>
<select className="dropdown" onChange={setTheme} defaultValue={config.theme}>
<option value="vs-dark">vs-dark</option>
<option value="light">light</option>
</select>
<br />
<StdButton text={"Save"} onClick={save} />
</>);
}

204
src/Monaco/ui/Root.tsx Normal file

@ -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<string>(props.code);
const [ram, setRAM] = useState('');
const [config, setConfig] = useState<Config>({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<HTMLInputElement>): 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<void>(() => undefined);
}
const id = setInterval(() => {
console.log(code);
updateRAM();
}, 10000);
return () => clearInterval(id);
}, []);
return (<div id="script-editor-wrapper">
<div id="script-editor-filename-wrapper">
<p id="script-editor-filename-tag"> <strong style={{backgroundColor:'#555'}}>Script name: </strong></p>
<input id="script-editor-filename" type="text" maxLength={100} tabIndex={1} value={filename} onChange={onFilenameChange} />
<StdButton text={"config"} onClick={openConfig} />
</div>
<Editor
loading={<p>Loading script editor!</p>}
height="80%"
defaultLanguage="javascript"
defaultValue={code}
value={code}
onChange={updateCode}
theme="vs-dark"
options={config}
/>
<div id="script-editor-buttons-wrapper">
<StdButton text={"Beautify"} onClick={beautify} />
<p id="script-editor-status-text" style={{display:"inline-block", margin:"10px"}}>{ram}</p>
<button className="std-button" style={{display: "inline-block"}} onClick={save}>Save & Close (Ctrl/Cmd + b)</button>
<a className="std-button" style={{display: "inline-block"}} target="_blank" href="https://bitburner.readthedocs.io/en/latest/index.html">Netscript Documentation</a>
</div>
</div>);
}

@ -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() {

@ -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(
<ScriptEditorRoot filename={filename} code={code} player={Player} engine={this} />,
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);

@ -125,11 +125,8 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
<input id="script-editor-filename" type="text" maxlength="100" tabindex="1" />
</div>
<div id="ace-editor"></div>
<form id="codemirror-form-wrapper"><textarea id="codemirror-editor"></textarea></form>
<div id="codemirror-vim-command-display-wrapper">
Key Buffer: <span id="codemirror-vim-command-display"></span>
</div>
<div id="monaco-editor"></div>
<div id="script-editor-buttons-wrapper"></div> <!-- Buttons below script editor -->
</div> <!-- End wrapper -->
@ -175,6 +172,14 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
<fieldset id="script-editor-option-flex4-fieldset"></fieldset>
</div> <!-- End script editor options panel -->
<!-- TODO(hydroflame): remove this once Monaco is implemented -->
<div id="ace-editor" style="display: none"></div>
<form id="codemirror-form-wrapper" style="display: none"><textarea id="codemirror-editor"></textarea></form>
<div id="codemirror-vim-command-display-wrapper" style="display: none">
Key Buffer: <span id="codemirror-vim-command-display"></span>
</div>
</div>
<!-- Terminal page -->