mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-26 11:10:58 +02:00
638 lines
22 KiB
TypeScript
638 lines
22 KiB
TypeScript
import type { ContentFilePath } from "../../Paths/ContentFile";
|
|
|
|
import React, { useEffect, useRef } from "react";
|
|
import * as monaco from "monaco-editor";
|
|
|
|
import type * as acorn from "acorn";
|
|
import * as walk from "acorn-walk";
|
|
import { extendAcornWalkForTypeScriptNodes } from "../../ThirdParty/acorn-typescript-walk";
|
|
import { extend as extendAcornWalkForJsxNodes } from "acorn-jsx-walk";
|
|
|
|
import { Editor } from "./Editor";
|
|
|
|
import { Router } from "../../ui/GameRoot";
|
|
import { Page } from "../../ui/Router";
|
|
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
|
import { checkInfiniteLoop } from "../../Script/RamCalculations";
|
|
|
|
import { Settings } from "../../Settings/Settings";
|
|
import { iTutorialNextStep, ITutorial, iTutorialSteps } from "../../InteractiveTutorial";
|
|
import { debounce } from "lodash";
|
|
import { saveObject } from "../../SaveObject";
|
|
import { GetServer } from "../../Server/AllServers";
|
|
|
|
import { PromptEvent } from "../../ui/React/PromptManager";
|
|
|
|
import { useRerender } from "../../ui/React/hooks";
|
|
|
|
import { isUnsavedFile, getServerCode, makeModel } from "./utils";
|
|
import { OpenScript } from "./OpenScript";
|
|
import { Tabs } from "./Tabs";
|
|
import { Toolbar } from "./Toolbar";
|
|
import { NoOpenScripts } from "./NoOpenScripts";
|
|
import { ScriptEditorContextProvider, useScriptEditorContext } from "./ScriptEditorContext";
|
|
import { useVimEditor } from "./useVimEditor";
|
|
import { useCallback } from "react";
|
|
import { type AST, getFileType, getModuleScript, parseAST } from "../../utils/ScriptTransformer";
|
|
import { RamCalculationErrorCode } from "../../Script/RamCalculationErrorCodes";
|
|
import { hasScriptExtension, isLegacyScript, type ScriptFilePath } from "../../Paths/ScriptFilePath";
|
|
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
|
import type { BaseServer } from "../../Server/BaseServer";
|
|
import {
|
|
convertKeyboardEventToKeyCombination,
|
|
CurrentKeyBindings,
|
|
determineKeyBindingTypes,
|
|
ScriptEditorAction,
|
|
} from "../../utils/KeyBindingUtils";
|
|
import { SpecialServers } from "../../Server/data/SpecialServers";
|
|
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
|
import { ToastVariant } from "@enums";
|
|
import { createRunningScriptInstance, startWorkerScript } from "../../NetscriptWorker";
|
|
import type { PositiveInteger } from "../../types";
|
|
|
|
// Extend acorn-walk to support TypeScript nodes.
|
|
extendAcornWalkForTypeScriptNodes(walk.base);
|
|
|
|
// Extend acorn-walk to support JSX nodes.
|
|
extendAcornWalkForJsxNodes(walk.base);
|
|
|
|
type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
|
|
|
|
interface IProps {
|
|
// Map of filename -> code
|
|
files: Map<ContentFilePath, string>;
|
|
hostname: string;
|
|
vim: boolean;
|
|
}
|
|
const openScripts: OpenScript[] = [];
|
|
let currentScript: OpenScript | null = null;
|
|
|
|
function Root(props: IProps): React.ReactElement {
|
|
const rerender = useRerender();
|
|
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
|
|
|
|
// This is the workaround for a bug in monaco-editor: https://github.com/microsoft/monaco-editor/issues/4455
|
|
const removeOutlineOfEditor = useCallback(() => {
|
|
if (!editorRef.current) {
|
|
return;
|
|
}
|
|
const containerDomNode = editorRef.current.getContainerDomNode();
|
|
const elements = containerDomNode.getElementsByClassName("monaco-editor");
|
|
if (elements.length === 0) {
|
|
return;
|
|
}
|
|
const editorElement = elements[0];
|
|
(editorElement as HTMLElement).style.outline = "none";
|
|
}, [editorRef]);
|
|
|
|
/**
|
|
* The TypeScript compiler needs time to perform type-checking, so in some edge cases, the editor shows the 2792 error
|
|
* ("Cannot find module") even after we created the required models. For example, let's say "ts.ts" script imports
|
|
* "sum" function from "sum.js". The flow is like this:
|
|
* - The player opens "ts.ts". The editor opens with a model for "ts.ts".
|
|
* - TSC starts performing type-checking. This action is asynchronous.
|
|
* - makeModelsForImports is called to dynamically create models for imported modules. We create a model for "sum.js".
|
|
* After this model is created, it's synced to both language workers (check "onDidCreateModel" code in
|
|
* src\ScriptEditor\ScriptEditor.ts).
|
|
* - Before the model of "sum.js" is synced properly, TSC finishes typechecking. At this point, it cannot find
|
|
* relevant data of "sum.js", so it thinks that "sum.js" is not loaded.
|
|
* - The editor shows an error marker at the import code of "sum.js".
|
|
*
|
|
* The error markers will disappear when the player edits the code (the model is updated when the code is changed), so
|
|
* this is not a big problem. Nonetheless, we will still work around this problem to minimize the chance of showing
|
|
* wrong error markers. In order to do that, we check error markers after a short delay (2 seconds); if there is a
|
|
* false-positive error marker, we will reload the model. Reloading the model will force the type-checking to run
|
|
* again.
|
|
*/
|
|
const reloadModelOfCurrentScript = debounce(() => {
|
|
if (!currentScript || !editorRef.current) {
|
|
return;
|
|
}
|
|
const markers = monaco.editor.getModelMarkers({
|
|
resource: currentScript.model.uri,
|
|
});
|
|
let needToReloadModel = false;
|
|
for (const marker of markers) {
|
|
// 2792: "Cannot find module" error
|
|
if (marker.code !== "2792") {
|
|
continue;
|
|
}
|
|
needToReloadModel = true;
|
|
break;
|
|
}
|
|
if (needToReloadModel) {
|
|
const currentModel = editorRef.current.getModel();
|
|
// Save the current cursor position. The position resets when the model is changed.
|
|
const currentPosition = editorRef.current.getPosition();
|
|
// Reload the model.
|
|
currentModel?.setValue(currentModel.getValue());
|
|
// Restore the saved position.
|
|
if (currentPosition) {
|
|
editorRef.current.setPosition(currentPosition);
|
|
}
|
|
}
|
|
}, 2000);
|
|
|
|
function makeModelsForImports(ast: AST, server: BaseServer): void {
|
|
if (!currentScript) {
|
|
return;
|
|
}
|
|
// Skipping processing if the current file is not a script or it's a legacy script.
|
|
if (!hasScriptExtension(currentScript.path) || isLegacyScript(currentScript.path)) {
|
|
return;
|
|
}
|
|
// Dynamically load imported scripts.
|
|
walk.simple(
|
|
ast as acorn.Node, // Pretend that ast is an acorn node
|
|
{
|
|
ImportDeclaration: (node: acorn.ImportDeclaration) => {
|
|
if (typeof node.source.value !== "string" || !currentScript) {
|
|
return;
|
|
}
|
|
const importedScript = getModuleScript(
|
|
node.source.value,
|
|
currentScript.path as ScriptFilePath,
|
|
server.scripts,
|
|
);
|
|
/**
|
|
* We use openScripts to store all opened files when the player opens them in the editor. When they edit code,
|
|
* the changed code is in openScripts, regardless of whether they save it. When the player switches from the
|
|
* editor tab to another tab, all models are disposed, so the next time they open the editor, this function
|
|
* will load imported scripts. However, if the player did not save their code, loaded scripts would not
|
|
* contain changed code. Therefore, for each loaded script, we need to check if it is in openScripts. If it
|
|
* is, we use the script content in openScripts.
|
|
*/
|
|
let code = importedScript.code;
|
|
for (const openScript of openScripts) {
|
|
if (openScript.hostname !== importedScript.server || openScript.path !== importedScript.filename) {
|
|
continue;
|
|
}
|
|
code = openScript.code;
|
|
}
|
|
makeModel(importedScript.server, importedScript.filename, code);
|
|
},
|
|
},
|
|
);
|
|
// Reload the model to force the type-checking to run again.
|
|
reloadModelOfCurrentScript();
|
|
}
|
|
|
|
const { options, showRAMError, updateRAM, startUpdatingRAM, finishUpdatingRAM } = useScriptEditorContext();
|
|
|
|
let decorations: monaco.editor.IEditorDecorationsCollection | undefined;
|
|
|
|
const beautify = useCallback(async (): Promise<void> => {
|
|
const action = editorRef.current?.getAction("editor.action.formatDocument");
|
|
if (action == null) {
|
|
return;
|
|
}
|
|
return action.run().catch((error) => console.error(error));
|
|
}, []);
|
|
|
|
const save = useCallback(async () => {
|
|
if (currentScript === null) {
|
|
console.error("currentScript is null when it shouldn't be. Unable to save script");
|
|
return;
|
|
}
|
|
|
|
const preSave = options.beautifyOnSave ? beautify : () => Promise.resolve();
|
|
|
|
// this is duplicate code with saving later.
|
|
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalEditScript) {
|
|
//Make sure filename + code properly follow tutorial
|
|
if (currentScript.path !== "n00dles.js") {
|
|
dialogBoxCreate("Don't change the script name for now.");
|
|
return;
|
|
}
|
|
const cleanCode = currentScript.code.replace(/\s/g, "");
|
|
const expectedCleanCode = `/**@param{NS}ns*/exportasyncfunctionmain(ns){while(true){awaitns.hack("n00dles");}}`;
|
|
if (!cleanCode.includes(expectedCleanCode)) {
|
|
dialogBoxCreate("Please copy and paste the code from the tutorial!");
|
|
return;
|
|
}
|
|
|
|
//Save the script
|
|
await preSave();
|
|
saveScript(currentScript);
|
|
Router.toPage(Page.Terminal);
|
|
|
|
iTutorialNextStep();
|
|
|
|
return;
|
|
}
|
|
await preSave();
|
|
saveScript(currentScript);
|
|
rerender();
|
|
}, [rerender, options.beautifyOnSave, beautify]);
|
|
|
|
const run = useCallback(async () => {
|
|
if (currentScript === null) {
|
|
return;
|
|
}
|
|
// Check if "currentScript" is a script. It may be a text file.
|
|
if (!hasScriptExtension(currentScript.path)) {
|
|
dialogBoxCreate(`Cannot run ${currentScript.path}. It is not a script.`);
|
|
return;
|
|
}
|
|
// Check if the current script's server is valid.
|
|
const server = GetServer(currentScript.hostname);
|
|
if (server === null) {
|
|
return;
|
|
}
|
|
|
|
// Always save before doing anything else.
|
|
await save();
|
|
|
|
const result = createRunningScriptInstance(
|
|
server,
|
|
currentScript.path,
|
|
{ threads: 1 as PositiveInteger, temporary: false, preventDuplicates: false },
|
|
[],
|
|
);
|
|
if (!result.success) {
|
|
dialogBoxCreate(result.message);
|
|
return;
|
|
}
|
|
startWorkerScript(result.runningScript, server);
|
|
}, [save]);
|
|
|
|
useEffect(() => {
|
|
async function keydown(event: KeyboardEvent) {
|
|
if (Settings.DisableHotkeys) {
|
|
return;
|
|
}
|
|
const keyBindingTypes = determineKeyBindingTypes(CurrentKeyBindings, convertKeyboardEventToKeyCombination(event));
|
|
if (keyBindingTypes.has(ScriptEditorAction.Save)) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
await save();
|
|
}
|
|
if (keyBindingTypes.has(ScriptEditorAction.GoToTerminal)) {
|
|
event.preventDefault();
|
|
Router.toPage(Page.Terminal);
|
|
}
|
|
if (keyBindingTypes.has(ScriptEditorAction.Run)) {
|
|
event.preventDefault();
|
|
await run();
|
|
}
|
|
}
|
|
const listener = (event: KeyboardEvent) => {
|
|
keydown(event).catch((error) => console.error(error));
|
|
};
|
|
document.addEventListener("keydown", listener);
|
|
return () => document.removeEventListener("keydown", listener);
|
|
}, [save, run]);
|
|
|
|
function infLoop(ast: AST, code: string): void {
|
|
if (editorRef.current === null || currentScript === null || isLegacyScript(currentScript.path)) {
|
|
return;
|
|
}
|
|
if (!decorations) {
|
|
decorations = editorRef.current.createDecorationsCollection();
|
|
}
|
|
const possibleLines = checkInfiniteLoop(ast, code);
|
|
if (possibleLines.length !== 0) {
|
|
decorations.set(
|
|
possibleLines.map((awaitWarning) => ({
|
|
range: {
|
|
startLineNumber: awaitWarning,
|
|
startColumn: 1,
|
|
endLineNumber: awaitWarning,
|
|
endColumn: 10,
|
|
},
|
|
options: {
|
|
isWholeLine: true,
|
|
glyphMarginClassName: "myGlyphMarginClass",
|
|
glyphMarginHoverMessage: {
|
|
value:
|
|
"Possible infinite loop, await something. If this is a false positive, use `// @ignore-infinite` to suppress.",
|
|
},
|
|
},
|
|
})),
|
|
);
|
|
} else {
|
|
decorations.clear();
|
|
}
|
|
}
|
|
|
|
const debouncedCodeParsing = debounce((newCode: string) => {
|
|
let server;
|
|
if (!currentScript || !hasScriptExtension(currentScript.path)) {
|
|
showRAMError();
|
|
return;
|
|
}
|
|
if (!(server = GetServer(currentScript.hostname))) {
|
|
showRAMError({
|
|
errorCode: RamCalculationErrorCode.InvalidServer,
|
|
errorMessage: `Server ${currentScript.hostname} does not exist`,
|
|
});
|
|
return;
|
|
}
|
|
let ast;
|
|
try {
|
|
ast = parseAST(currentScript.path, currentScript.hostname, newCode, getFileType(currentScript.path));
|
|
makeModelsForImports(ast, server);
|
|
} catch (error) {
|
|
showRAMError({
|
|
errorCode: RamCalculationErrorCode.SyntaxError,
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
});
|
|
return;
|
|
}
|
|
infLoop(ast, newCode);
|
|
updateRAM(ast, currentScript.path, server);
|
|
finishUpdatingRAM();
|
|
}, 300);
|
|
|
|
const parseCode = (newCode: string) => {
|
|
startUpdatingRAM();
|
|
debouncedCodeParsing(newCode);
|
|
};
|
|
|
|
// When the editor is mounted
|
|
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.
|
|
editorRef.current = editor;
|
|
|
|
// Open current script. This happens when the player switch tabs and open the editor tab.
|
|
if (props.files.size === 0 && currentScript !== null) {
|
|
currentScript.regenerateModel();
|
|
editorRef.current.setModel(currentScript.model);
|
|
editorRef.current.setPosition(currentScript.lastPosition);
|
|
editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber);
|
|
parseCode(currentScript.code);
|
|
editorRef.current.focus();
|
|
return;
|
|
}
|
|
|
|
// This happens when the player opens scripts by using nano/vim.
|
|
for (const [filename, code] of props.files) {
|
|
// Check if file is already opened
|
|
const openScript = openScripts.find((script) => script.path === filename && script.hostname === props.hostname);
|
|
if (openScript) {
|
|
// Script is already opened
|
|
if (openScript.model === undefined || openScript.model === null || openScript.model.isDisposed()) {
|
|
openScript.regenerateModel();
|
|
}
|
|
|
|
currentScript = openScript;
|
|
editorRef.current.setModel(openScript.model);
|
|
editorRef.current.setPosition(openScript.lastPosition);
|
|
editorRef.current.revealLineInCenter(openScript.lastPosition.lineNumber);
|
|
parseCode(openScript.code);
|
|
} else {
|
|
// Open script
|
|
const newScript = new OpenScript(
|
|
filename,
|
|
code,
|
|
props.hostname,
|
|
new monaco.Position(0, 0),
|
|
makeModel(props.hostname, filename, code),
|
|
props.vim,
|
|
);
|
|
openScripts.push(newScript);
|
|
currentScript = newScript;
|
|
editorRef.current.setModel(newScript.model);
|
|
parseCode(newScript.code);
|
|
}
|
|
}
|
|
|
|
editorRef.current.focus();
|
|
}
|
|
|
|
// When the code is updated within the editor
|
|
function updateCode(newCode?: string): void {
|
|
if (newCode === undefined) return;
|
|
// parseCode includes ram check and infinite loop detection
|
|
parseCode(newCode);
|
|
if (editorRef.current === null) return;
|
|
const newPos = editorRef.current.getPosition();
|
|
if (newPos === null) return;
|
|
if (currentScript !== null) {
|
|
currentScript.code = newCode;
|
|
currentScript.lastPosition = newPos;
|
|
}
|
|
}
|
|
|
|
function saveScript(scriptToSave: OpenScript): void {
|
|
const server = GetServer(scriptToSave.hostname);
|
|
if (!server) {
|
|
dialogBoxCreate(`Server ${scriptToSave.hostname} does not exist.`);
|
|
return;
|
|
}
|
|
// Show a warning message if the file is on a non-home server.
|
|
if (scriptToSave.hostname !== SpecialServers.Home) {
|
|
SnackbarEvents.emit("You saved a file on a non-home server!", ToastVariant.WARNING, 3000);
|
|
}
|
|
// This server helper already handles overwriting, etc.
|
|
server.writeToContentFile(scriptToSave.path, scriptToSave.code);
|
|
if (Settings.SaveGameOnFileSave) {
|
|
saveObject.saveGame().catch((error) => exceptionAlert(error));
|
|
}
|
|
}
|
|
|
|
function currentTabIndex(): number | undefined {
|
|
if (currentScript) return openScripts.findIndex((openScript) => currentScript === openScript);
|
|
return undefined;
|
|
}
|
|
|
|
function onTabClick(index: number): void {
|
|
if (currentScript !== null) {
|
|
// Save the current position of the cursor.
|
|
const currentPosition = editorRef.current?.getPosition();
|
|
if (currentPosition) {
|
|
currentScript.lastPosition = currentPosition;
|
|
}
|
|
// Save currentScript to openScripts
|
|
const curIndex = currentTabIndex();
|
|
if (curIndex !== undefined) {
|
|
openScripts[curIndex] = currentScript;
|
|
}
|
|
}
|
|
|
|
currentScript = openScripts[index];
|
|
|
|
if (editorRef.current !== null && openScripts[index] !== null) {
|
|
if (!currentScript.model || currentScript.model.isDisposed()) {
|
|
currentScript.regenerateModel();
|
|
}
|
|
editorRef.current.setModel(currentScript.model);
|
|
editorRef.current.setPosition(currentScript.lastPosition);
|
|
editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber);
|
|
parseCode(currentScript.code);
|
|
editorRef.current.focus();
|
|
}
|
|
removeOutlineOfEditor();
|
|
}
|
|
|
|
function onTabClose(index: number): void {
|
|
// See if the script on the server is up to date
|
|
const closingScript = openScripts[index];
|
|
const savedScriptCode = closingScript.code;
|
|
const wasCurrentScript = openScripts[index] === currentScript;
|
|
|
|
if (isUnsavedFile(openScripts, index)) {
|
|
PromptEvent.emit({
|
|
txt: `Do you want to save changes to ${closingScript.path} on ${closingScript.hostname}?`,
|
|
resolve: (result: boolean | string) => {
|
|
if (result) {
|
|
// Save changes
|
|
closingScript.code = savedScriptCode;
|
|
saveScript(closingScript);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
//unmounting the editor will dispose all, doesnt hurt to dispose on close aswell
|
|
closingScript.model.dispose();
|
|
openScripts.splice(index, 1);
|
|
if (openScripts.length === 0) {
|
|
currentScript = null;
|
|
Router.toPage(Page.Terminal);
|
|
return;
|
|
}
|
|
|
|
// Change current script if we closed it
|
|
if (wasCurrentScript) {
|
|
//Keep the same index unless we were on the last script
|
|
const indexOffset = openScripts.length === index ? -1 : 0;
|
|
currentScript = openScripts[index + indexOffset];
|
|
if (editorRef.current !== null) {
|
|
if (!currentScript.model || currentScript.model.isDisposed()) {
|
|
currentScript.regenerateModel();
|
|
}
|
|
editorRef.current.setModel(currentScript.model);
|
|
editorRef.current.setPosition(currentScript.lastPosition);
|
|
editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber);
|
|
parseCode(currentScript.code);
|
|
editorRef.current.focus();
|
|
}
|
|
}
|
|
rerender();
|
|
removeOutlineOfEditor();
|
|
}
|
|
|
|
function onTabUpdate(index: number): void {
|
|
const openScript = openScripts[index];
|
|
const serverScriptCode = getServerCode(openScripts, index);
|
|
if (serverScriptCode === null) return;
|
|
|
|
if (openScript.code !== serverScriptCode) {
|
|
PromptEvent.emit({
|
|
txt:
|
|
"Do you want to overwrite the current editor content with the contents of " +
|
|
openScript.path +
|
|
" on the server? This cannot be undone.",
|
|
resolve: (result: boolean | string) => {
|
|
if (result) {
|
|
// Save changes
|
|
openScript.code = serverScriptCode;
|
|
|
|
// Switch to target tab
|
|
onTabClick(index);
|
|
|
|
if (editorRef.current !== null && openScript !== null) {
|
|
if (openScript.model === undefined || openScript.model.isDisposed()) {
|
|
openScript.regenerateModel();
|
|
}
|
|
editorRef.current.setModel(openScript.model);
|
|
|
|
editorRef.current.setValue(openScript.code);
|
|
parseCode(openScript.code);
|
|
editorRef.current.focus();
|
|
}
|
|
}
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
function onOpenNextTab(step: number): void {
|
|
// Go to the next tab (to the right). Wraps around when at the rightmost tab
|
|
const currIndex = currentTabIndex();
|
|
if (currIndex !== undefined) {
|
|
const nextIndex = (currIndex + step) % openScripts.length;
|
|
onTabClick(nextIndex);
|
|
}
|
|
}
|
|
|
|
function onOpenPreviousTab(step: number): void {
|
|
// Go to the previous tab (to the left). Wraps around when at the leftmost tab
|
|
const currIndex = currentTabIndex();
|
|
if (currIndex !== undefined) {
|
|
let nextIndex = currIndex - step;
|
|
while (nextIndex < 0) {
|
|
nextIndex += openScripts.length;
|
|
}
|
|
onTabClick(nextIndex);
|
|
}
|
|
}
|
|
|
|
function onUnmountEditor() {
|
|
if (!currentScript) {
|
|
return;
|
|
}
|
|
// Save the current position of the cursor.
|
|
const currentPosition = editorRef.current?.getPosition();
|
|
if (currentPosition) {
|
|
currentScript.lastPosition = currentPosition;
|
|
}
|
|
}
|
|
|
|
const { statusBarRef } = useVimEditor({
|
|
editor: editorRef.current,
|
|
vim: currentScript !== null ? currentScript.vimMode : props.vim,
|
|
onSave: save,
|
|
onOpenNextTab,
|
|
onOpenPreviousTab,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (currentScript !== null) {
|
|
const tabIndex = currentTabIndex();
|
|
if (typeof tabIndex === "number") onTabClick(tabIndex);
|
|
parseCode(currentScript.code);
|
|
}
|
|
// disable eslint because we want to run this only once on mount
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
style={{
|
|
display: currentScript !== null ? "flex" : "none",
|
|
height: "100%",
|
|
width: "100%",
|
|
flexDirection: "column",
|
|
}}
|
|
>
|
|
<Tabs
|
|
scripts={openScripts}
|
|
currentScript={currentScript}
|
|
onTabClick={onTabClick}
|
|
onTabClose={onTabClose}
|
|
onTabUpdate={onTabUpdate}
|
|
/>
|
|
<div style={{ flex: "0 0 5px" }} />
|
|
<Editor onMount={onMount} onChange={updateCode} onUnmount={onUnmountEditor} />
|
|
|
|
{statusBarRef.current}
|
|
|
|
<Toolbar onSave={save} onRun={run} editor={editorRef.current} onBeautify={beautify} />
|
|
</div>
|
|
{!currentScript && <NoOpenScripts />}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Called every time script editor is opened
|
|
export function ScriptEditorRoot(props: IProps) {
|
|
return (
|
|
<ScriptEditorContextProvider>
|
|
<Root {...props} />
|
|
</ScriptEditorContextProvider>
|
|
);
|
|
}
|