Merge branch 'dev' into feat/add-vim-mode

This commit is contained in:
Billy Vong
2021-12-20 10:36:58 -05:00
75 changed files with 2507 additions and 1362 deletions
+19 -3
View File
@@ -2290,6 +2290,22 @@ export interface Hacknet {
*/
spendHashes(upgName: string, upgTarget?: string): boolean;
/**
* Get the list of hash upgrades
* @remarks
* RAM cost: 0 GB
*
* This function is only applicable for Hacknet Servers (the upgraded version of a Hacknet Node).
*
* Returns the list of all available hash upgrades that can be used in the spendHashes function.
* @example
* ```ts
* const upgrades = hacknet.getHashUpgrades(); // ["Sell for Money","Sell for Corporation Funds",...]
* ```
* @returns An array containing the available upgrades
*/
getHashUpgrades(): string[];
/**
* Get the level of a hash upgrade.
* @remarks
@@ -4198,7 +4214,7 @@ export interface NS extends Singularity {
* @remarks
* RAM cost: 0.05 GB
*
* Runs the NUKE.exe program on the target server. NUKE.exe must exist on your home computer.
* Running NUKE.exe on a target server gives you root access which means you can executes scripts on said server. NUKE.exe must exist on your home computer.
*
* @example
* ```ts
@@ -4362,7 +4378,7 @@ export interface NS extends Singularity {
* @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument numThreads must be filled in with a value.
* @returns Returns the PID of a successfully started script, and 0 otherwise.
*/
exec(script: string, host: string, numThreads?: number, ...args: string[]): number;
exec(script: string, host: string, numThreads?: number, ...args: Array<string | number | boolean>): number;
/**
* Terminate current script and start another in 10s.
@@ -5242,7 +5258,7 @@ export interface NS extends Singularity {
* @param format - Formatter.
* @returns Formated number.
*/
nFormat(n: number, format: string): number;
nFormat(n: number, format: string): string;
/**
* Format time to readable string
+543 -304
View File
@@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
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";
@@ -15,7 +17,7 @@ import { TextFile } from "../../TextFile";
import { calculateRamUsage, checkInfiniteLoop } from "../../Script/RamCalculations";
import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes";
import { numeralWrapper } from "../../ui/numeralFormat";
import { CursorPositions } from "../CursorPositions";
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import { NetscriptFunctions } from "../../NetscriptFunctions";
import { WorkerScript } from "../../Netscript/WorkerScript";
@@ -30,17 +32,28 @@ import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import SettingsIcon from "@mui/icons-material/Settings";
import { PromptEvent } from "../../ui/React/PromptManager";
import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts";
interface IProps {
filename: string;
code: string;
hostname: string;
player: IPlayer;
router: IRouter;
}
// TODO: try to removve global symbols
let symbolsLoaded = false;
let symbols: string[] = [];
export function SetupTextEditor(): void {
const ns = NetscriptFunctions({} as WorkerScript);
// Populates symbols for text editor
function populate(ns: any): string[] {
let symbols: string[] = [];
const keys = Object.keys(ns);
@@ -53,41 +66,48 @@ export function SetupTextEditor(): void {
symbols.push(key);
}
}
return symbols;
}
symbols = populate(ns);
const exclude = ["heart", "break", "exploit", "bypass", "corporation", "alterReality"];
symbols = symbols.filter((symbol: string) => !exclude.includes(symbol)).sort();
}
interface IProps {
filename: string;
// Holds all the data for a open script
class OpenScript {
fileName: string;
code: string;
hostname: string;
player: IPlayer;
router: IRouter;
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;
}
}
/*
// Called every time script editor is opened
export function Root(props: IProps): React.ReactElement {
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<Monaco | null>(null);
*/
const [openScripts, setOpenScripts] = useState<OpenScript[]>(
window.localStorage.getItem('scriptEditorOpenScripts') !== null ? JSON.parse(window.localStorage.getItem('scriptEditorOpenScripts')!) : []
);
// 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
// https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages
// 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;
const [currentScript, setCurrentScript] = useState<OpenScript | null>(
window.localStorage.getItem('scriptEditorCurrentScript') !== null ? JSON.parse(window.localStorage.getItem('scriptEditorCurrentScript')!) : null
);
<<<<<<< HEAD
export function Root(props: IProps): React.ReactElement {
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
const vimStatusRef = useRef<HTMLElement>(null);
@@ -100,8 +120,12 @@ export function Root(props: IProps): React.ReactElement {
if (hostname === "") {
hostname = props.player.getCurrentServer().hostname;
}
=======
>>>>>>> dev
const [ram, setRAM] = useState("RAM: ???");
const [updatingRam, setUpdatingRam] = useState(false);
const [decorations, setDecorations] = useState<string[]>([]);
const [optionsOpen, setOptionsOpen] = useState(false);
const [options, setOptions] = useState<Options>({
theme: Settings.MonacoTheme,
@@ -110,202 +134,26 @@ export function Root(props: IProps): React.ReactElement {
vim: Settings.MonacoVim,
});
const debouncedSetRAM = useMemo(
() =>
debounce((s) => {
setRAM(s);
setUpdatingRam(false);
}, 300),
[],
);
// 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;
}, []);
// Save currentScript
window.localStorage.setItem('scriptEditorCurrentScript', JSON.stringify(currentScript, (key, value) => {
if (key == 'model') return undefined;
return value;
}));
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;
// Save openScripts
window.localStorage.setItem('scriptEditorOpenScripts', JSON.stringify(openScripts, (key, value) => {
if (key == 'model') return undefined;
return value;
}))
}, [currentScript, openScripts])
// 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") {
dialogBoxCreate("Leave the script name as 'n00dles.script'!");
return;
}
if (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);
}
iTutorialNextStep();
props.router.toTerminal();
return;
}
if (filename == "") {
dialogBoxCreate("You must specify a filename!");
return;
}
if (!isValidFilePath(filename)) {
dialogBoxCreate(
"Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.",
);
return;
}
const server = GetServer(hostname);
if (server === null) throw new Error("Server should not be null but it is.");
if (isScriptFilename(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 (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);
server.scripts.push(script);
} else if (filename.endsWith(".txt")) {
for (let i = 0; i < server.textFiles.length; ++i) {
if (server.textFiles[i].fn === filename) {
server.textFiles[i].write(code);
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
return;
}
}
const textFile = new TextFile(filename, code);
server.textFiles.push(textFile);
} else {
dialogBoxCreate("Invalid filename. Must be either a script (.script, .js, or .ns) or " + " or text file (.txt)");
return;
}
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
}
function beautify(): void {
if (editorRef.current === null) return;
editorRef.current.getAction("editor.action.formatDocument").run();
}
function onFilenameChange(event: React.ChangeEvent<HTMLInputElement>): 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;
const awaitWarning = checkInfiniteLoop(newCode);
if (awaitWarning !== -1) {
const newDecorations = editorRef.current.deltaDecorations(decorations, [
{
range: {
startLineNumber: awaitWarning,
startColumn: 1,
endLineNumber: awaitWarning,
endColumn: 10,
},
options: {
isWholeLine: true,
glyphMarginClassName: "myGlyphMarginClass",
glyphMarginHoverMessage: {
value: "Possible infinite loop, await something.",
},
},
},
]);
setDecorations(newDecorations);
} else {
const newDecorations = editorRef.current.deltaDecorations(decorations, []);
setDecorations(newDecorations);
}
}
function updateCode(newCode?: string): void {
if (newCode === undefined) return;
lastCode = newCode;
setCode(newCode);
updateRAM(newCode);
try {
if (editorRef.current !== null) {
lastPosition = editorRef.current.getPosition();
infLoop(newCode);
}
} catch (err) {}
}
// calculate it once the first time the file is loaded.
useEffect(() => {
updateRAM(code);
if (currentScript !== null) {
updateRAM(currentScript.code);
}
}, []);
async function updateRAM(newCode: string): Promise<void> {
setUpdatingRam(true);
const codeCopy = newCode + "";
const ramUsage = await calculateRamUsage(codeCopy, props.player.getCurrentServer().scripts);
if (ramUsage > 0) {
debouncedSetRAM("RAM: " + numeralWrapper.formatRAM(ramUsage));
return;
}
switch (ramUsage) {
case RamCalculationErrorCode.ImportError: {
debouncedSetRAM("RAM: Import Error");
break;
}
case RamCalculationErrorCode.URLImportError: {
debouncedSetRAM("RAM: HTTP Import Error");
break;
}
case RamCalculationErrorCode.SyntaxError:
default: {
debouncedSetRAM("RAM: Syntax Error");
break;
}
}
return new Promise<void>(() => undefined);
}
useEffect(() => {
function maybeSave(event: KeyboardEvent): void {
if (Settings.DisableHotkeys) return;
@@ -314,6 +162,13 @@ export function Root(props: IProps): React.ReactElement {
event.preventDefault();
save();
}
// CTRL/CMD + S
if (event.code == `KeyS` && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
event.stopPropagation();
save();
}
}
document.addEventListener("keydown", maybeSave);
return () => document.removeEventListener("keydown", maybeSave);
@@ -348,29 +203,65 @@ export function Root(props: IProps): React.ReactElement {
};
}, [options, editorRef, editor, vimEditor]);
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;
if (editorRef.current === null) return;
const position = CursorPositions.getCursor(filename);
if (position.row !== -1)
editorRef.current.setPosition({
lineNumber: position.row,
column: position.column,
});
else if (lastPosition !== null)
editorRef.current.setPosition({
lineNumber: lastPosition.lineNumber,
column: lastPosition.column + 1,
});
editorRef.current.focus();
// Generates a new model for the script
function regenerateModel(script: OpenScript): void {
if (monacoRef.current !== null) {
script.model = monacoRef.current.editor.createModel(script.code, "javascript");
}
}
const debouncedSetRAM = useMemo(
() =>
debounce((s) => {
setRAM(s);
setUpdatingRam(false);
}, 300),
[],
);
async function updateRAM(newCode: string): Promise<void> {
setUpdatingRam(true);
const codeCopy = newCode + "";
const ramUsage = await calculateRamUsage(codeCopy, props.player.getCurrentServer().scripts);
if (ramUsage > 0) {
debouncedSetRAM("RAM: " + numeralWrapper.formatRAM(ramUsage));
return;
}
switch (ramUsage) {
case RamCalculationErrorCode.ImportError: {
debouncedSetRAM("RAM: Import Error");
break;
}
case RamCalculationErrorCode.URLImportError: {
debouncedSetRAM("RAM: HTTP Import Error");
break;
}
case RamCalculationErrorCode.SyntaxError:
default: {
debouncedSetRAM("RAM: Syntax Error");
break;
}
}
return new Promise<void>(() => undefined);
}
// Formats the code
function beautify(): void {
if (editorRef.current === null) return;
editorRef.current.getAction("editor.action.formatDocument").run();
}
// 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
// https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages
// https://github.com/threehams/typescript-error-guide/blob/master/stories/components/Editor.tsx#L11-L39
// https://blog.checklyhq.com/customizing-monaco/
// Before the editor is mounted
function beforeMount(monaco: any): void {
if (symbolsLoaded) return;
// Setup monaco auto completion
symbolsLoaded = true;
monaco.languages.registerCompletionItemProvider("javascript", {
provideCompletionItems: () => {
@@ -386,6 +277,7 @@ export function Root(props: IProps): React.ReactElement {
return { suggestions: suggestions };
},
});
(async function () {
// We have to improve the default js language otherwise theme sucks
const l = await monaco.languages
@@ -407,6 +299,319 @@ export function Root(props: IProps): React.ReactElement {
loadThemes(monaco);
}
// When the editor is mounted
function onMount(editor: IStandaloneCodeEditor, monaco: Monaco): 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;
monacoRef.current = monaco;
if (editorRef.current === null || monacoRef.current === null) return;
if (props.filename) {
// Check if file is already opened
const openScriptIndex = openScripts.findIndex(script => script.fileName === props.filename && script.hostname === props.hostname);
if (openScriptIndex !== -1) {
// Script is already opened
if (openScripts[openScriptIndex].model === undefined || openScripts[openScriptIndex].model === null || openScripts[openScriptIndex].model.isDisposed()) {
regenerateModel(openScripts[openScriptIndex]);
}
setCurrentScript(openScripts[openScriptIndex]);
editorRef.current.setModel(openScripts[openScriptIndex].model);
editorRef.current.setPosition(openScripts[openScriptIndex].lastPosition);
editorRef.current.revealLineInCenter(openScripts[openScriptIndex].lastPosition.lineNumber);
updateRAM(openScripts[openScriptIndex].code);
} else {
// Open script
const newScript = new OpenScript(props.filename, props.code, props.hostname, new monacoRef.current.Position(0, 0), monacoRef.current.editor.createModel(props.code, 'javascript'));
setOpenScripts(oldArray => [...oldArray, newScript]);
setCurrentScript({ ...newScript });
editorRef.current.setModel(newScript.model);
updateRAM(newScript.code);
}
} else if (currentScript !== null) {
// Open currentscript
regenerateModel(currentScript);
editorRef.current.setModel(currentScript.model);
editorRef.current.setPosition(currentScript.lastPosition);
editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber);
updateRAM(currentScript.code);
}
editorRef.current.focus();
}
function infLoop(newCode: string): void {
if (editorRef.current === null || currentScript === null) return;
if (!currentScript.fileName.endsWith(".ns") && !currentScript.fileName.endsWith(".js")) return;
const awaitWarning = checkInfiniteLoop(newCode);
if (awaitWarning !== -1) {
const newDecorations = editorRef.current.deltaDecorations(decorations, [
{
range: {
startLineNumber: awaitWarning,
startColumn: 1,
endLineNumber: awaitWarning,
endColumn: 10,
},
options: {
isWholeLine: true,
glyphMarginClassName: "myGlyphMarginClass",
glyphMarginHoverMessage: {
value: "Possible infinite loop, await something.",
},
},
},
]);
setDecorations(newDecorations);
} else {
const newDecorations = editorRef.current.deltaDecorations(decorations, []);
setDecorations(newDecorations);
}
}
// When the code is updated within the editor
function updateCode(newCode?: string): void {
if (newCode === undefined) return;
updateRAM(newCode);
if (editorRef.current !== null) {
const newPos = editorRef.current.getPosition();
if (newPos === null) return;
setCurrentScript(oldScript => ({ ...oldScript!, code: newCode, lastPosition: newPos! }))
if (currentScript !== null) {
const curIndex = openScripts.findIndex(script => script.fileName === currentScript.fileName && script.hostname === currentScript.hostname);
const newArr = [...openScripts];
const tempScript = currentScript;
tempScript.code = newCode;
newArr[curIndex] = tempScript;
setOpenScripts([...newArr]);
}
try {
infLoop(newCode);
} catch (err) { }
}
}
function saveScript(scriptToSave: OpenScript): void {
const server = GetServer(scriptToSave.hostname);
if (server === null) throw new Error("Server should not be null but it is.");
if (isScriptFilename(scriptToSave.fileName)) {
//If the current script already exists on the server, overwrite it
for (let i = 0; i < server.scripts.length; i++) {
if (scriptToSave.fileName == server.scripts[i].filename) {
server.scripts[i].saveScript(
scriptToSave.fileName,
scriptToSave.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(scriptToSave.fileName, scriptToSave.code, props.player.currentServer, server.scripts);
server.scripts.push(script);
} else if (scriptToSave.fileName.endsWith(".txt")) {
for (let i = 0; i < server.textFiles.length; ++i) {
if (server.textFiles[i].fn === scriptToSave.fileName) {
server.textFiles[i].write(scriptToSave.code);
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
return;
}
}
const textFile = new TextFile(scriptToSave.fileName, scriptToSave.code);
server.textFiles.push(textFile);
} else {
dialogBoxCreate("Invalid filename. Must be either a script (.script, .js, or .ns) or " + " or text file (.txt)");
return;
}
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
}
function save(): void {
if (currentScript === null) {
console.log("currentScript is null when it shouldn't be. Unabel to save script");
return;
}
// this is duplicate code with saving later.
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) {
//Make sure filename + code properly follow tutorial
if (currentScript.fileName !== "n00dles.script") {
dialogBoxCreate("Leave the script name as 'n00dles.script'!");
return;
}
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
saveScript(currentScript);
iTutorialNextStep();
props.router.toTerminal();
return;
}
if (currentScript.fileName == "") {
dialogBoxCreate("You must specify a filename!");
return;
}
if (!isValidFilePath(currentScript.fileName)) {
dialogBoxCreate(
"Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.",
);
return;
}
const server = GetServer(currentScript.hostname);
if (server === null) throw new Error("Server should not be null but it is.");
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 (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(currentScript.fileName, currentScript.code, props.player.currentServer, server.scripts);
server.scripts.push(script);
} else if (currentScript.fileName.endsWith(".txt")) {
for (let i = 0; i < server.textFiles.length; ++i) {
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(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)");
return;
}
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
props.router.toTerminal();
}
function reorder(list: Array<OpenScript>, startIndex: number, endIndex: number): OpenScript[] {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
}
function onDragEnd(result: any): void{
// Dropped outside of the list
if (!result.destination) {
result
return;
}
const items = reorder(openScripts, result.source.index, result.destination.index);
setOpenScripts(items);
}
function onTabClick(index: number): void {
if (currentScript !== null) {
// Save currentScript to openScripts
const curIndex = openScripts.findIndex(script => script.fileName === currentScript.fileName && script.hostname === currentScript.hostname);
openScripts[curIndex] = currentScript;
}
setCurrentScript({ ...openScripts[index] });
if (editorRef.current !== null && openScripts[index] !== null) {
if (openScripts[index].model === undefined || openScripts[index].model.isDisposed()) {
regenerateModel(openScripts[index]);
}
editorRef.current.setModel(openScripts[index].model);
editorRef.current.setPosition(openScripts[index].lastPosition);
editorRef.current.revealLineInCenter(openScripts[index].lastPosition.lineNumber);
updateRAM(openScripts[index].code);
editorRef.current.focus();
}
}
async function onTabClose(index: number): Promise<void> {
// See if the script on the server is up to date
const closingScript = openScripts[index];
const savedOpenScripts: Array<OpenScript> = JSON.parse(window.localStorage.getItem('scriptEditorOpenScripts')!);
const savedScriptIndex = savedOpenScripts.findIndex(script => script.fileName === closingScript.fileName && script.hostname === closingScript.hostname);
let savedScriptCode = '';
if (savedScriptIndex !== -1) {
savedScriptCode = savedOpenScripts[savedScriptIndex].code;
}
const serverScriptIndex = GetServer(closingScript.hostname)?.scripts.findIndex(script => script.filename === closingScript.fileName);
if (serverScriptIndex === -1 || savedScriptCode !== GetServer(closingScript.hostname)?.scripts[serverScriptIndex as number].code) {
PromptEvent.emit({
txt: 'Do you want to save changes to ' + closingScript.fileName + '?',
resolve: (result: boolean) => {
if (result) {
// Save changes
closingScript.code = savedScriptCode;
saveScript(closingScript);
}
}
})
}
if (openScripts.length > 1) {
setOpenScripts(oldScripts => oldScripts.filter((value, i) => i !== index));
let indexOffset = -1;
if (openScripts[index + indexOffset] === undefined) {
indexOffset = 1;
}
// Change current script if we closed it
setCurrentScript(openScripts[index + indexOffset]);
if (editorRef.current !== null) {
if (openScripts[index + indexOffset].model === undefined || openScripts[index + indexOffset].model === null || openScripts[index + indexOffset].model.isDisposed()) {
regenerateModel(openScripts[index + indexOffset]);
}
editorRef.current.setModel(openScripts[index + indexOffset].model);
editorRef.current.setPosition(openScripts[index + indexOffset].lastPosition);
editorRef.current.revealLineInCenter(openScripts[index + indexOffset].lastPosition.lineNumber)
editorRef.current.focus();
}
} else {
// No more scripts are open
setOpenScripts([]);
setCurrentScript(null);
}
}
// TODO: Make this responsive to window resizes
// Toolbars are roughly 108px + vim bar 34px
// Get percentage of space that toolbars represent and the rest should be the
@@ -415,78 +620,112 @@ export function Root(props: IProps): React.ReactElement {
return (
<>
<Box display="flex" flexDirection="row" alignItems="center">
<TextField
placeholder="filename"
type="text"
tabIndex={1}
value={filename}
onChange={onFilenameChange}
InputProps={{ startAdornment: <Typography>{hostname}:~/</Typography> }}
<div style={{ display: currentScript !== null ? 'block' : 'none', height: '100%', width: '100%' }}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId='tabs' direction='horizontal'>
{(provided, snapshot) => (
<Box
maxWidth="1640px"
display="flex"
flexDirection="row"
alignItems="center"
whiteSpace="nowrap"
ref={provided.innerRef}
{...provided.droppableProps}
style={{ backgroundColor: snapshot.isDraggingOver ? '#1F2022' : Settings.theme.backgroundprimary, overflowX: 'scroll' }}
>
{openScripts.map(({ fileName, hostname }, index) => (
<Draggable key={fileName + hostname} draggableId={fileName + hostname} index={index} disableInteractiveElementBlocking={true}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...provided.draggableProps.style,
marginRight: '5px',
flexShrink: 0
}}
>
<Button
id={"tabButton" + fileName + hostname}
onClick={() => onTabClick(index)}
style={{ background: currentScript?.fileName === openScripts[index].fileName ? Settings.theme.secondarydark : '' }}
>
{hostname}:~/{fileName}
</Button>
<Button
id={"tabCloseButton" + fileName + hostname}
onClick={() => onTabClose(index)}
style={{ maxWidth: "20px", minWidth: "20px", background: currentScript?.fileName === openScripts[index].fileName ? Settings.theme.secondarydark : '' }}
>
x
</Button>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</Box>
)}
</Droppable>
</DragDropContext>
<div style={{ paddingBottom: '5px' }} />
<Editor
beforeMount={beforeMount}
onMount={onMount}
loading={<Typography>Loading script editor!</Typography>}
height={`${editorHeight}%`}
defaultLanguage="javascript"
defaultValue={''}
onChange={updateCode}
theme={options.theme}
options={{ ...options, glyphMargin: true }}
/>
<IconButton onClick={() => setOptionsOpen(true)}>
<>
<SettingsIcon />
options
</>
</IconButton>
</Box>
<Editor
beforeMount={beforeMount}
onMount={onMount}
loading={<Typography>Loading script editor!</Typography>}
height={`${editorHeight}%`}
defaultLanguage="javascript"
defaultValue={code}
onChange={updateCode}
theme={options.theme}
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">
<Button onClick={beautify}>Beautify</Button>
<Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}>
{ram}
</Typography>
<Button onClick={save}>Save & Close (Ctrl/Cmd + b)</Button>
<Typography sx={{ mx: 1 }}>
{" "}
Documentation:{" "}
<Link target="_blank" href="https://bitburner.readthedocs.io/en/latest/index.html">
Basic
</Link>{" "}
|
<Link target="_blank" href="https://github.com/danielyxie/bitburner/blob/dev/markdown/bitburner.ns.md">
Full
</Link>
</Typography>
</Box>
<OptionsModal
open={optionsOpen}
onClose={() => setOptionsOpen(false)}
options={{
theme: Settings.MonacoTheme,
insertSpaces: Settings.MonacoInsertSpaces,
fontSize: Settings.MonacoFontSize,
vim: Settings.MonacoVim,
}}
save={(options: Options) => {
setOptions(options);
Settings.MonacoTheme = options.theme;
Settings.MonacoInsertSpaces = options.insertSpaces;
Settings.MonacoFontSize = options.fontSize;
Settings.MonacoVim = options.vim;
}}
/>
<Box display="flex" flexDirection="row" sx={{ m: 1 }} alignItems="center">
<Button onClick={beautify}>Beautify</Button>
<Typography color={updatingRam ? "secondary" : "primary"} sx={{ mx: 1 }}>
{ram}
</Typography>
<Button onClick={save}>Save & Close (Ctrl/Cmd + s)</Button>
<Typography sx={{ mx: 1 }}>
{" "}
Documentation:{" "}
<Link target="_blank" href="https://bitburner.readthedocs.io/en/latest/index.html">
Basic
</Link>{" "}
|
<Link target="_blank" href="https://github.com/danielyxie/bitburner/blob/dev/markdown/bitburner.ns.md">
Full
</Link>
</Typography>
<IconButton style={{ marginLeft: "auto" }} onClick={() => setOptionsOpen(true)}>
<>
<SettingsIcon />
options
</>
</IconButton>
</Box>
<OptionsModal
open={optionsOpen}
onClose={() => setOptionsOpen(false)}
options={{
theme: Settings.MonacoTheme,
insertSpaces: Settings.MonacoInsertSpaces,
fontSize: Settings.MonacoFontSize,
}}
save={(options: Options) => {
setOptions(options);
Settings.MonacoTheme = options.theme;
Settings.MonacoInsertSpaces = options.insertSpaces;
Settings.MonacoFontSize = options.fontSize;
}}
/>
</div>
<div style={{ display: currentScript !== null ? 'none' : 'flex', height: '100%', width: '100%', justifyContent: 'center', alignItems: 'center' }}>
<p style={{ color: Settings.theme.primary, fontSize: '20px', textAlign: 'center' }}><h1>No open files</h1><h5>Use "nano [File Name]" in the terminal to open files</h5></p>
</div>
</>
);
)
}