bitburner-src/src/ScriptEditor/ui/ScriptEditorRoot.tsx

833 lines
29 KiB
TypeScript
Raw Normal View History

2021-12-20 12:59:09 +01:00
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2021-10-11 22:59:37 +02:00
import React, { useState, useEffect, useRef, useMemo } from "react";
import Editor, { Monaco } from "@monaco-editor/react";
2021-08-20 07:57:32 +02:00
import * as monaco from "monaco-editor";
2021-08-27 01:14:56 +02:00
type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
type ITextModel = monaco.editor.ITextModel;
2021-09-25 05:36:28 +02:00
import { OptionsModal } from "./OptionsModal";
import { Options } from "./Options";
2021-08-20 07:21:37 +02:00
import { isValidFilePath } from "../../Terminal/DirectoryHelpers";
import { IPlayer } from "../../PersonObjects/IPlayer";
2021-09-17 08:58:02 +02:00
import { IRouter } from "../../ui/Router";
2021-09-25 20:42:57 +02:00
import { dialogBoxCreate } from "../../ui/React/DialogBox";
2021-09-24 22:34:21 +02:00
import { isScriptFilename } from "../../Script/isScriptFilename";
2021-08-20 07:21:37 +02:00
import { Script } from "../../Script/Script";
import { TextFile } from "../../TextFile";
2021-10-29 05:04:26 +02:00
import { calculateRamUsage, checkInfiniteLoop } from "../../Script/RamCalculations";
2021-08-20 07:21:37 +02:00
import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes";
import { numeralWrapper } from "../../ui/numeralFormat";
2021-12-20 16:48:32 +01:00
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
2021-10-28 05:19:19 +02:00
2021-08-21 06:17:26 +02:00
import { NetscriptFunctions } from "../../NetscriptFunctions";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { Settings } from "../../Settings/Settings";
2021-09-09 05:47:34 +02:00
import { iTutorialNextStep, ITutorial, iTutorialSteps } from "../../InteractiveTutorial";
2021-10-11 22:59:37 +02:00
import { debounce } from "lodash";
2021-10-11 23:57:17 +02:00
import { saveObject } from "../../SaveObject";
2021-10-12 16:56:19 +02:00
import { loadThemes } from "./themes";
import { GetServer } from "../../Server/AllServers";
2021-08-21 06:17:26 +02:00
2021-09-25 05:36:28 +02:00
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 IconButton from "@mui/material/IconButton";
import SettingsIcon from "@mui/icons-material/Settings";
2021-12-20 07:48:03 +01:00
import { PromptEvent } from "../../ui/React/PromptManager";
2021-09-25 05:36:28 +02:00
2021-10-30 18:34:14 +02:00
import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts";
2021-10-28 05:19:19 +02:00
2021-12-20 07:48:03 +01:00
interface IProps {
// Map of filename -> code
files: Record<string, string>;
2021-12-20 07:48:03 +01:00
hostname: string;
player: IPlayer;
router: IRouter;
2021-12-17 18:48:34 +01:00
vim: boolean;
2021-12-20 07:48:03 +01:00
}
// TODO: try to removve global symbols
2021-10-10 04:59:06 +02:00
let symbolsLoaded = false;
2021-08-21 06:17:26 +02:00
let symbols: string[] = [];
2021-10-05 03:06:55 +02:00
export function SetupTextEditor(): void {
2021-09-05 01:09:30 +02:00
const ns = NetscriptFunctions({} as WorkerScript);
2021-08-21 06:17:26 +02:00
2021-12-20 07:48:03 +01:00
// Populates symbols for text editor
2021-09-05 01:09:30 +02:00
function populate(ns: any): string[] {
let symbols: string[] = [];
const keys = Object.keys(ns);
for (const key of keys) {
if (typeof ns[key] === "object") {
symbols.push(key);
symbols = symbols.concat(populate(ns[key]));
}
if (typeof ns[key] === "function") {
symbols.push(key);
}
2021-08-21 06:17:26 +02:00
}
2021-12-20 07:48:03 +01:00
2021-09-05 01:09:30 +02:00
return symbols;
}
2021-12-20 07:48:03 +01:00
2021-09-05 01:09:30 +02:00
symbols = populate(ns);
2021-10-27 21:16:16 +02:00
const exclude = ["heart", "break", "exploit", "bypass", "corporation", "alterReality"];
symbols = symbols.filter((symbol: string) => !exclude.includes(symbol)).sort();
2021-10-05 03:06:55 +02:00
}
2021-08-20 07:21:37 +02:00
// Holds all the data for a open script
2021-12-20 07:48:03 +01:00
class OpenScript {
fileName: string;
2021-09-05 01:09:30 +02:00
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;
}
2021-08-27 01:14:56 +02:00
}
2021-08-20 07:21:37 +02:00
2021-12-29 08:04:24 +01:00
let openScripts: OpenScript[] = [];
let currentScript: OpenScript | null = null;
2021-12-20 07:48:03 +01:00
// Called every time script editor is opened
2021-08-20 07:21:37 +02:00
export function Root(props: IProps): React.ReactElement {
2021-12-29 08:04:24 +01:00
const setRerender = useState(false)[1];
function rerender(): void {
setRerender((o) => !o);
}
2021-09-05 01:09:30 +02:00
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<Monaco | null>(null);
2021-12-20 16:44:17 +01:00
const vimStatusRef = useRef<HTMLElement>(null);
const [vimEditor, setVimEditor] = useState<any>(null);
const [editor, setEditor] = useState<IStandaloneCodeEditor | null>(null);
2021-08-21 07:54:39 +02:00
2021-09-05 01:09:30 +02:00
const [ram, setRAM] = useState("RAM: ???");
2021-10-11 22:59:37 +02:00
const [updatingRam, setUpdatingRam] = useState(false);
2021-12-20 07:48:03 +01:00
const [decorations, setDecorations] = useState<string[]>([]);
2021-09-25 05:36:28 +02:00
const [optionsOpen, setOptionsOpen] = useState(false);
2021-09-05 01:09:30 +02:00
const [options, setOptions] = useState<Options>({
theme: Settings.MonacoTheme,
insertSpaces: Settings.MonacoInsertSpaces,
2021-10-05 03:06:55 +02:00
fontSize: Settings.MonacoFontSize,
2021-12-20 16:44:17 +01:00
vim: props.vim || Settings.MonacoVim,
2021-09-05 01:09:30 +02:00
});
// Prevent Crash if script is open on deleted server
openScripts = openScripts.filter((script) => {
return GetServer(script.hostname) !== null;
})
if (currentScript && (GetServer(currentScript.hostname) === null)) {
currentScript = openScripts[0];
}
const [dimensions, setDimensions] = useState({
2021-12-20 20:05:22 +01:00
height: window.innerHeight,
width: window.innerWidth,
});
useEffect(() => {
2021-12-20 20:05:22 +01:00
const debouncedHandleResize = debounce(function handleResize() {
setDimensions({
height: window.innerHeight,
width: window.innerWidth,
});
}, 250);
2021-12-20 20:05:22 +01:00
window.addEventListener("resize", debouncedHandleResize);
2021-12-20 20:05:22 +01:00
return () => {
window.removeEventListener("resize", debouncedHandleResize);
};
}, []);
2021-12-20 07:48:03 +01:00
useEffect(() => {
if (currentScript !== null) {
updateRAM(currentScript.code);
}
}, []);
useEffect(() => {
2021-12-29 08:04:24 +01:00
function keydown(event: KeyboardEvent): void {
2021-12-20 07:48:03 +01:00
if (Settings.DisableHotkeys) return;
//Ctrl + b
2021-12-29 08:04:24 +01:00
if (event.code == "KeyB" && (event.ctrlKey || event.metaKey)) {
2021-12-20 07:48:03 +01:00
event.preventDefault();
2021-12-29 08:04:24 +01:00
props.router.toTerminal();
2021-12-20 07:48:03 +01:00
}
2021-12-20 16:48:32 +01:00
// CTRL/CMD + S
2021-12-29 08:04:24 +01:00
if (event.code == "KeyS" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
event.stopPropagation();
save();
}
2021-12-20 07:48:03 +01:00
}
2021-12-29 08:04:24 +01:00
document.addEventListener("keydown", keydown);
return () => document.removeEventListener("keydown", keydown);
2021-12-20 07:48:03 +01:00
});
useEffect(() => {
// setup monaco-vim
if (options.vim && editor && !vimEditor) {
try {
// This library is not typed
// @ts-expect-error
window.require(["monaco-vim"], function (MonacoVim: any) {
setVimEditor(MonacoVim.initVimMode(editor, vimStatusRef.current));
MonacoVim.VimMode.Vim.defineEx("write", "w", function () {
// your own implementation on what you want to do when :w is pressed
save();
});
MonacoVim.VimMode.Vim.defineEx("quit", "q", function () {
save();
});
editor.focus();
});
} catch {}
} else if (!options.vim) {
// Whem vim mode is disabled
vimEditor?.dispose();
setVimEditor(null);
}
return () => {
vimEditor?.dispose();
};
}, [options, editorRef, editor, vimEditor]);
2021-12-20 07:48:03 +01:00
// Generates a new model for the script
function regenerateModel(script: OpenScript): void {
if (monacoRef.current !== null) {
2021-12-21 18:00:12 +01:00
script.model = monacoRef.current.editor.createModel(
script.code,
script.fileName.endsWith(".txt") ? "plaintext" : "javascript",
);
2021-12-20 07:48:03 +01:00
}
}
2021-10-11 22:59:37 +02:00
const debouncedSetRAM = useMemo(
() =>
debounce((s) => {
setRAM(s);
setUpdatingRam(false);
}, 300),
[],
);
2021-12-20 07:48:03 +01:00
async function updateRAM(newCode: string): Promise<void> {
2021-12-21 00:21:22 +01:00
if (currentScript != null && currentScript.fileName.endsWith(".txt")) {
debouncedSetRAM("");
return;
}
2021-12-20 07:48:03 +01:00
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);
}
2021-08-20 07:21:37 +02:00
2021-12-20 07:48:03 +01:00
// 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/
2021-12-20 07:48:03 +01:00
// Before the editor is mounted
function beforeMount(monaco: any): void {
if (symbolsLoaded) return;
// Setup monaco auto completion
symbolsLoaded = true;
monaco.languages.registerCompletionItemProvider("javascript", {
provideCompletionItems: () => {
const suggestions = [];
for (const symbol of symbols) {
suggestions.push({
label: symbol,
kind: monaco.languages.CompletionItemKind.Function,
insertText: symbol,
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
});
}
return { suggestions: suggestions };
},
});
(async function () {
// We have to improve the default js language otherwise theme sucks
const l = await monaco.languages
.getLanguages()
.find((l: any) => l.id === "javascript")
.loader();
l.language.tokenizer.root.unshift(["ns", { token: "ns" }]);
for (const symbol of symbols) l.language.tokenizer.root.unshift([symbol, { token: "netscriptfunction" }]);
const otherKeywords = ["let", "const", "var", "function"];
const otherKeyvars = ["true", "false", "null", "undefined"];
otherKeywords.forEach((k) => l.language.tokenizer.root.unshift([k, { token: "otherkeywords" }]));
otherKeyvars.forEach((k) => l.language.tokenizer.root.unshift([k, { token: "otherkeyvars" }]));
l.language.tokenizer.root.unshift(["this", { token: "this" }]);
})();
const source = (libSource + "").replace(/export /g, "");
monaco.languages.typescript.javascriptDefaults.addExtraLib(source, "netscript.d.ts");
monaco.languages.typescript.typescriptDefaults.addExtraLib(source, "netscript.d.ts");
loadThemes(monaco);
}
// When the editor is mounted
2021-12-20 12:59:09 +01:00
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);
2021-12-20 07:48:03 +01:00
editorRef.current = editor;
monacoRef.current = monaco;
if (editorRef.current === null || monacoRef.current === null) return;
2021-12-21 19:41:11 +01:00
if (!props.files && 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();
return;
}
2021-12-21 18:00:12 +01:00
if (props.files) {
const files = Object.entries(props.files);
2021-12-21 18:00:12 +01:00
if (!files.length) {
editorRef.current.focus();
return;
}
2021-12-20 07:48:03 +01:00
2021-12-21 18:00:12 +01:00
for (const [filename, code] of files) {
// Check if file is already opened
const openScript = openScripts.find(
(script) => script.fileName === filename && script.hostname === props.hostname,
2021-12-20 16:48:32 +01:00
);
2021-12-21 18:00:12 +01:00
if (openScript) {
// Script is already opened
if (openScript.model === undefined || openScript.model === null || openScript.model.isDisposed()) {
regenerateModel(openScript);
}
2021-12-29 08:04:24 +01:00
currentScript = openScript;
2021-12-21 18:00:12 +01:00
editorRef.current.setModel(openScript.model);
editorRef.current.setPosition(openScript.lastPosition);
editorRef.current.revealLineInCenter(openScript.lastPosition.lineNumber);
updateRAM(openScript.code);
} else {
// Open script
const newScript = new OpenScript(
filename,
code,
props.hostname,
new monacoRef.current.Position(0, 0),
monacoRef.current.editor.createModel(code, filename.endsWith(".txt") ? "plaintext" : "javascript"),
);
2021-12-29 08:04:24 +01:00
openScripts.push(newScript);
currentScript = { ...newScript };
2021-12-21 18:00:12 +01:00
editorRef.current.setModel(newScript.model);
updateRAM(newScript.code);
}
2021-12-20 07:48:03 +01:00
}
}
editorRef.current.focus();
2021-12-20 07:48:03 +01:00
}
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
2021-12-20 12:59:09 +01:00
function updateCode(newCode?: string): void {
2021-12-20 07:48:03 +01:00
if (newCode === undefined) return;
updateRAM(newCode);
2021-12-29 08:04:24 +01:00
if (editorRef.current === null) return;
const newPos = editorRef.current.getPosition();
if (newPos === null) return;
if (currentScript !== null) {
currentScript = { ...currentScript, code: newCode, lastPosition: newPos };
const curIndex = openScripts.findIndex(
(script) =>
currentScript !== null &&
script.fileName === currentScript.fileName &&
script.hostname === currentScript.hostname,
);
const newArr = [...openScripts];
const tempScript = currentScript;
tempScript.code = newCode;
newArr[curIndex] = tempScript;
openScripts = [...newArr];
2021-09-05 01:09:30 +02:00
}
2021-12-29 08:04:24 +01:00
try {
infLoop(newCode);
} catch (err) {}
2021-12-20 07:48:03 +01:00
}
2021-08-23 08:09:49 +02:00
2021-12-20 07:48:03 +01:00
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();
}
2021-09-05 01:09:30 +02:00
function save(): void {
2021-12-20 07:48:03 +01:00
if (currentScript === null) {
2021-12-29 08:04:24 +01:00
console.error("currentScript is null when it shouldn't be. Unable to save script");
2021-12-20 07:48:03 +01:00
return;
}
2021-09-19 06:46:39 +02:00
// this is duplicate code with saving later.
2021-09-09 05:47:34 +02:00
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) {
2021-09-05 01:09:30 +02:00
//Make sure filename + code properly follow tutorial
if (currentScript.fileName !== "n00dles.script") {
dialogBoxCreate("Leave the script name as 'n00dles.script'!");
2021-09-05 01:09:30 +02:00
return;
}
if (currentScript.code.replace(/\s/g, "").indexOf("while(true){hack('n00dles');}") == -1) {
2021-09-05 01:09:30 +02:00
dialogBoxCreate("Please copy and paste the code from the tutorial!");
return;
}
//Save the script
saveScript(currentScript);
2021-09-19 06:46:39 +02:00
iTutorialNextStep();
2021-08-20 07:21:37 +02:00
2021-09-19 06:46:39 +02:00
return;
2021-09-05 01:09:30 +02:00
}
2021-08-20 07:21:37 +02:00
if (currentScript.fileName == "") {
2021-09-05 01:09:30 +02:00
dialogBoxCreate("You must specify a filename!");
return;
}
2021-08-20 07:21:37 +02:00
if (!isValidFilePath(currentScript.fileName)) {
2021-09-05 01:09:30 +02:00
dialogBoxCreate(
"Script filename can contain only alphanumerics, hyphens, and underscores, and must end with an extension.",
);
return;
}
2021-08-20 07:21:37 +02:00
const server = GetServer(currentScript.hostname);
2021-09-09 05:47:34 +02:00
if (server === null) throw new Error("Server should not be null but it is.");
if (isScriptFilename(currentScript.fileName)) {
2021-09-05 01:09:30 +02:00
//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) {
2021-12-18 22:26:50 +01:00
server.scripts[i].saveScript(
currentScript.fileName,
currentScript.code,
props.player.currentServer,
server.scripts,
);
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
2021-09-05 01:09:30 +02:00
return;
2021-08-20 07:21:37 +02:00
}
2021-09-05 01:09:30 +02:00
}
2021-08-20 07:21:37 +02:00
2021-09-05 01:09:30 +02:00
//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);
2021-09-05 01:09:30 +02:00
server.scripts.push(script);
} else if (currentScript.fileName.endsWith(".txt")) {
2021-09-05 01:09:30 +02:00
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();
2021-09-05 01:09:30 +02:00
return;
2021-08-20 07:21:37 +02:00
}
2021-09-05 01:09:30 +02:00
}
const textFile = new TextFile(currentScript.fileName, currentScript.code);
2021-09-05 01:09:30 +02:00
server.textFiles.push(textFile);
} else {
2021-09-09 05:47:34 +02:00
dialogBoxCreate("Invalid filename. Must be either a script (.script, .js, or .ns) or " + " or text file (.txt)");
2021-09-05 01:09:30 +02:00
return;
}
2021-10-11 23:57:17 +02:00
if (Settings.SaveGameOnFileSave) saveObject.saveGame();
2021-09-05 01:09:30 +02:00
}
2021-08-20 07:21:37 +02:00
2021-12-20 12:59:09 +01:00
function reorder(list: Array<OpenScript>, startIndex: number, endIndex: number): OpenScript[] {
2021-12-20 07:48:03 +01:00
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
2021-08-20 07:21:37 +02:00
2021-12-20 07:48:03 +01:00
return result;
2021-09-05 01:09:30 +02:00
}
2021-08-20 07:21:37 +02:00
2021-12-20 16:48:32 +01:00
function onDragEnd(result: any): void {
2021-12-20 07:48:03 +01:00
// Dropped outside of the list
if (!result.destination) {
2021-12-20 16:48:32 +01:00
result;
2021-09-05 01:09:30 +02:00
return;
2021-10-29 07:23:15 +02:00
}
2021-12-20 07:48:03 +01:00
const items = reorder(openScripts, result.source.index, result.destination.index);
2021-08-20 07:21:37 +02:00
2021-12-29 08:04:24 +01:00
openScripts = items;
}
2021-10-11 22:59:37 +02:00
2021-12-20 12:59:09 +01:00
function onTabClick(index: number): void {
2021-12-20 07:48:03 +01:00
if (currentScript !== null) {
// Save currentScript to openScripts
2021-12-20 16:48:32 +01:00
const curIndex = openScripts.findIndex(
2021-12-29 08:04:24 +01:00
(script) =>
currentScript !== null &&
script.fileName === currentScript.fileName &&
script.hostname === currentScript.hostname,
2021-12-20 16:48:32 +01:00
);
2021-12-20 07:48:03 +01:00
openScripts[curIndex] = currentScript;
2021-08-20 07:21:37 +02:00
}
2021-12-29 08:04:24 +01:00
currentScript = { ...openScripts[index] };
2021-12-20 07:48:03 +01:00
if (editorRef.current !== null && openScripts[index] !== null) {
if (openScripts[index].model === undefined || openScripts[index].model.isDisposed()) {
regenerateModel(openScripts[index]);
2021-09-05 01:09:30 +02:00
}
2021-12-20 07:48:03 +01:00
editorRef.current.setModel(openScripts[index].model);
2021-12-20 07:48:03 +01:00
editorRef.current.setPosition(openScripts[index].lastPosition);
editorRef.current.revealLineInCenter(openScripts[index].lastPosition.lineNumber);
updateRAM(openScripts[index].code);
editorRef.current.focus();
2021-08-20 07:21:37 +02:00
}
2021-09-05 01:09:30 +02:00
}
2021-08-20 07:21:37 +02:00
2021-12-29 08:04:24 +01:00
function onTabClose(index: number): void {
2021-12-20 07:48:03 +01:00
// See if the script on the server is up to date
2021-12-20 12:59:09 +01:00
const closingScript = openScripts[index];
2021-12-29 08:04:24 +01:00
const savedScriptIndex = openScripts.findIndex(
2021-12-20 16:48:32 +01:00
(script) => script.fileName === closingScript.fileName && script.hostname === closingScript.hostname,
);
let savedScriptCode = "";
2021-12-20 07:48:03 +01:00
if (savedScriptIndex !== -1) {
2021-12-29 08:04:24 +01:00
savedScriptCode = openScripts[savedScriptIndex].code;
2021-08-20 07:57:32 +02:00
}
2021-12-29 08:04:24 +01:00
const server = GetServer(closingScript.hostname);
if (server === null) throw new Error(`Server '${closingScript.hostname}' should not be null, but it is.`);
2021-08-20 07:57:32 +02:00
2021-12-29 08:04:24 +01:00
const serverScriptIndex = server.scripts.findIndex((script) => script.filename === closingScript.fileName);
if (serverScriptIndex === -1 || savedScriptCode !== server.scripts[serverScriptIndex as number].code) {
2021-12-20 07:48:03 +01:00
PromptEvent.emit({
2021-12-20 16:48:32 +01:00
txt: "Do you want to save changes to " + closingScript.fileName + "?",
2021-12-20 07:48:03 +01:00
resolve: (result: boolean) => {
if (result) {
// Save changes
closingScript.code = savedScriptCode;
saveScript(closingScript);
2021-12-20 07:48:03 +01:00
}
2021-12-20 16:48:32 +01:00
},
});
}
2021-12-20 07:48:03 +01:00
if (openScripts.length > 1) {
2021-12-29 08:04:24 +01:00
openScripts = openScripts.filter((value, i) => i !== index);
2021-12-20 07:48:03 +01:00
let indexOffset = -1;
if (openScripts[index + indexOffset] === undefined) {
indexOffset = 1;
2021-12-29 08:04:24 +01:00
if (openScripts[index + indexOffset] === undefined) {
indexOffset = 0;
}
}
2021-08-20 07:57:32 +02:00
2021-12-20 07:48:03 +01:00
// Change current script if we closed it
2021-12-29 08:04:24 +01:00
currentScript = openScripts[index + indexOffset];
2021-12-20 07:48:03 +01:00
if (editorRef.current !== null) {
2021-12-20 16:48:32 +01:00
if (
openScripts[index + indexOffset].model === undefined ||
openScripts[index + indexOffset].model === null ||
openScripts[index + indexOffset].model.isDisposed()
) {
2021-12-20 07:48:03 +01:00
regenerateModel(openScripts[index + indexOffset]);
2021-08-21 07:54:39 +02:00
}
2021-12-20 07:48:03 +01:00
editorRef.current.setModel(openScripts[index + indexOffset].model);
editorRef.current.setPosition(openScripts[index + indexOffset].lastPosition);
2021-12-20 16:48:32 +01:00
editorRef.current.revealLineInCenter(openScripts[index + indexOffset].lastPosition.lineNumber);
editorRef.current.focus();
}
2021-12-29 08:04:24 +01:00
rerender();
2021-12-20 07:48:03 +01:00
} else {
// No more scripts are open
2021-12-29 08:04:24 +01:00
openScripts = [];
currentScript = null;
props.router.toTerminal();
}
2021-09-05 01:09:30 +02:00
}
2021-12-29 08:04:24 +01:00
function dirty(index: number): string {
const openScript = openScripts[index];
const server = GetServer(openScript.hostname);
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName);
if (serverScript === undefined) return " *";
return serverScript.code !== openScript.code ? " *" : "";
}
// Toolbars are roughly 112px:
// 8px body margin top
// 38.5px filename tabs
// 5px padding for top of editor
// 44px bottom tool bar + 16px margin
// + vim bar 34px
2021-12-21 18:00:12 +01:00
const editorHeight = dimensions.height - (130 + (options.vim ? 34 : 0));
2021-09-05 01:09:30 +02:00
return (
2021-09-17 08:04:44 +02:00
<>
2021-12-20 16:48:32 +01:00
<div style={{ display: currentScript !== null ? "block" : "none", height: "100%", width: "100%" }}>
2021-12-20 07:48:03 +01:00
<DragDropContext onDragEnd={onDragEnd}>
2021-12-20 16:48:32 +01:00
<Droppable droppableId="tabs" direction="horizontal">
2021-12-20 07:48:03 +01:00
{(provided, snapshot) => (
<Box
maxWidth="1640px"
display="flex"
flexDirection="row"
alignItems="center"
whiteSpace="nowrap"
ref={provided.innerRef}
{...provided.droppableProps}
2021-12-20 16:48:32 +01:00
style={{
backgroundColor: snapshot.isDraggingOver ? "#1F2022" : Settings.theme.backgroundprimary,
overflowX: "scroll",
}}
2021-12-20 07:48:03 +01:00
>
{openScripts.map(({ fileName, hostname }, index) => (
2021-12-20 16:48:32 +01:00
<Draggable
key={fileName + hostname}
draggableId={fileName + hostname}
index={index}
disableInteractiveElementBlocking={true}
>
2021-12-20 12:59:09 +01:00
{(provided) => (
2021-12-20 07:48:03 +01:00
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...provided.draggableProps.style,
2021-12-20 16:48:32 +01:00
marginRight: "5px",
flexShrink: 0,
2021-12-20 07:48:03 +01:00
}}
>
<Button
onClick={() => onTabClick(index)}
2021-12-20 16:48:32 +01:00
style={{
background:
currentScript?.fileName === openScripts[index].fileName
? Settings.theme.secondarydark
: "",
}}
2021-12-20 07:48:03 +01:00
>
2021-12-29 08:04:24 +01:00
{hostname}:~/{fileName} {dirty(index)}
2021-12-20 07:48:03 +01:00
</Button>
<Button
onClick={() => onTabClose(index)}
2021-12-20 16:48:32 +01:00
style={{
maxWidth: "20px",
minWidth: "20px",
background:
currentScript?.fileName === openScripts[index].fileName
? Settings.theme.secondarydark
: "",
}}
2021-12-20 07:48:03 +01:00
>
x
</Button>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</Box>
)}
</Droppable>
</DragDropContext>
2021-12-20 16:48:32 +01:00
<div style={{ paddingBottom: "5px" }} />
2021-12-20 07:48:03 +01:00
<Editor
beforeMount={beforeMount}
onMount={onMount}
loading={<Typography>Loading script editor!</Typography>}
height={`${editorHeight}px`}
2021-12-20 07:48:03 +01:00
defaultLanguage="javascript"
2021-12-20 16:48:32 +01:00
defaultValue={""}
2021-12-20 07:48:03 +01:00
onChange={updateCode}
theme={options.theme}
options={{ ...options, glyphMargin: true }}
/>
2021-12-20 16:48:32 +01:00
<Box
ref={vimStatusRef}
className="monaco-editor"
display="flex"
flexDirection="row"
sx={{ p: 1 }}
alignItems="center"
></Box>
2021-12-20 07:48:03 +01:00
<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>
2021-12-29 08:04:24 +01:00
<Button onClick={save}>Save All (Ctrl/Cmd + s)</Button>
<Button onClick={props.router.toTerminal}>Close (Ctrl/Cmd + b)</Button>
2021-12-20 07:48:03 +01:00
<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,
2021-12-20 16:45:44 +01:00
vim: Settings.MonacoVim,
2021-12-20 07:48:03 +01:00
}}
save={(options: Options) => {
setOptions(options);
Settings.MonacoTheme = options.theme;
Settings.MonacoInsertSpaces = options.insertSpaces;
Settings.MonacoFontSize = options.fontSize;
2021-12-20 16:45:44 +01:00
Settings.MonacoVim = options.vim;
2021-12-20 07:48:03 +01:00
}}
/>
2021-12-20 07:48:03 +01:00
</div>
2021-12-20 16:48:32 +01:00
<div
style={{
display: currentScript !== null ? "none" : "flex",
height: "100%",
width: "100%",
justifyContent: "center",
alignItems: "center",
}}
>
2021-12-29 08:04:24 +01:00
<span style={{ color: Settings.theme.primary, fontSize: "20px", textAlign: "center" }}>
<Typography variant="h4">No open files</Typography>
<Typography variant="h5">
Use `nano FILENAME` in
<br />
the terminal to open files
</Typography>
</span>
2021-12-20 07:48:03 +01:00
</div>
2021-09-17 08:04:44 +02:00
</>
2021-12-20 16:48:32 +01:00
);
}