Files
bitburner-src/src/ScriptEditor/ScriptEditor.ts

140 lines
6.8 KiB
TypeScript

import type { ContentFilePath } from "../Paths/ContentFile";
import { EventEmitter } from "../utils/EventEmitter";
import * as monaco from "monaco-editor";
import { loadThemes, makeTheme } from "./ui/themes";
import { Settings } from "../Settings/Settings";
import { NetscriptExtra } from "../NetscriptFunctions/Extra";
import * as enums from "../Enums";
import { ns } from "../NetscriptFunctions";
import { isLegacyScript } from "../Paths/ScriptFilePath";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
/** Event emitter used for tracking when changes have been made to a content file. */
export const fileEditEvents = new EventEmitter<[hostname: string, filename: ContentFilePath]>();
export class ScriptEditor {
// TODO: This will store info about currently open scripts.
// Among other things, this will allow informing the script editor of changes made elsewhere, even if the script editor is not being rendered.
// openScripts: OpenScript[] = [];
// Currently, this object is only used for initialization.
isInitialized = false;
initialize() {
if (this.isInitialized) return;
this.isInitialized = true;
// populate API keys for adding tokenization
const apiKeys: string[] = [];
const api = { args: [], pid: 1, enums, ...ns };
const hiddenAPI = NetscriptExtra();
function populate(apiLayer: object = api) {
for (const [apiKey, apiValue] of Object.entries(apiLayer)) {
if (apiLayer === api && apiKey in hiddenAPI) continue;
apiKeys.push(apiKey);
if (typeof apiValue === "object") {
populate(apiValue as object);
}
}
}
populate();
// Add api keys to tokenization
(async function () {
// We have to improve the default js language otherwise theme sucks
const jsLanguage = monaco.languages.getLanguages().find((l) => l.id === "javascript");
if (!jsLanguage) {
return;
}
const loader = await jsLanguage.loader();
// replaced the bare tokens with regexes surrounded by \b, e.g. \b{token}\b which matches a word-break on either side
// this prevents the highlighter from highlighting pieces of variables that start with a reserved token name
loader.language.tokenizer.root.unshift([new RegExp("\\bns\\b"), { token: "ns" }]);
for (const symbol of apiKeys)
loader.language.tokenizer.root.unshift([new RegExp(`\\b${symbol}\\b`), { token: "netscriptfunction" }]);
const otherKeywords = ["let", "const", "var", "function", "arguments"];
const otherKeyvars = ["true", "false", "null", "undefined"];
otherKeywords.forEach((k) =>
loader.language.tokenizer.root.unshift([new RegExp(`\\b${k}\\b`), { token: "otherkeywords" }]),
);
otherKeyvars.forEach((k) =>
loader.language.tokenizer.root.unshift([new RegExp(`\\b${k}\\b`), { token: "otherkeyvars" }]),
);
loader.language.tokenizer.root.unshift([new RegExp("\\bthis\\b"), { token: "this" }]);
})().catch((e) => exceptionAlert(e));
for (const [language, languageDefaults, getLanguageWorker] of [
["javascript", monaco.languages.typescript.javascriptDefaults, monaco.languages.typescript.getJavaScriptWorker],
["typescript", monaco.languages.typescript.typescriptDefaults, monaco.languages.typescript.getTypeScriptWorker],
] as const) {
languageDefaults.setCompilerOptions({
...languageDefaults.getCompilerOptions(),
// We allow direct importing of `.ts`/`.tsx` files, so tell the typescript language server that.
allowImportingTsExtensions: true,
// We use file-at-a-time transpiler. See https://www.typescriptlang.org/tsconfig/#isolatedModules
isolatedModules: true,
// We use the classic (i.e. `React.createElement`:) react runtime.
jsx: monaco.languages.typescript.JsxEmit.React,
// We define `React` and `ReactDOM` as globals. Don't mark using them as errors.
allowUmdGlobalAccess: true,
// Enable strict typechecking.
// Note that checking in javascript is disabled by default but can be enabled via `// @ts-check`.
// This enables strictNullChecks, which impacts reported types, even in javascript.
strict: true,
noImplicitAny: language === "typescript",
noImplicitReturns: true,
// Allow processing of javascript files, for handling cross-language imports.
allowJs: true,
});
languageDefaults.setDiagnosticsOptions({
...languageDefaults.getDiagnosticsOptions(),
// Show semantic errors, even in javascript.
// Note that this will only happen if checking is enabled in javascript (e.g. by `// @ts-check`)
noSemanticValidation: false,
// Ignore these errors in the editor:
diagnosticCodesToIgnore: [
// We define `React` and `ReactDOM` as globals. Don't mark using them as errors.
// Even though we set allowUmdGlobalAccess, it still shows a warning (instead of an error).
// - 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.(2686)
2686,
],
});
// Sync all javascript and typescript text models to both language servers.
//
// `monaco.languages.typescript.get{Java,Type}ScriptWorker` returns a promise that
// fires with a function that takes a list of `monaco.Uri`s and sync's them with the
// worker. (It also returns the worker, but we don't care about that.) However, it
// returns a reject promise if the language worker is not loaded yet, so we wait to
// call it until the language gets loaded.
const languageWorker = new Promise<(...uris: monaco.Uri[]) => unknown>((resolve) =>
monaco.languages.onLanguage(language, () => {
getLanguageWorker()
.then(resolve)
.catch((error) => exceptionAlert(error));
}),
);
// Whenever a model is created, arrange for it to be synced to the language server.
monaco.editor.onDidCreateModel((model) => {
if (language === "typescript" && isLegacyScript(model.uri.path)) {
// Don't sync legacy scripts to typescript worker.
return;
}
if (["javascript", "typescript"].includes(model.getLanguageId())) {
languageWorker.then((resolve) => resolve(model.uri)).catch((error) => exceptionAlert(error));
}
});
}
monaco.languages.json.jsonDefaults.setModeConfiguration({
...monaco.languages.json.jsonDefaults.modeConfiguration,
//completion should be disabled because the
//json language server tries to load a schema by default
completionItems: false,
});
// Load themes
loadThemes(monaco.editor.defineTheme);
monaco.editor.defineTheme("customTheme", makeTheme(Settings.EditorTheme));
}
}
export const scriptEditor = new ScriptEditor();