feat: integrate Monaco Editor with tabs, NWScript syntax, and session persistence
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
import { useRef, useCallback } from "react";
|
||||
import { Editor as ReactMonacoEditor, type OnMount } from "@monaco-editor/react";
|
||||
import type { editor } from "monaco-editor";
|
||||
|
||||
interface MonacoEditorProps {
|
||||
filePath: string;
|
||||
content: string;
|
||||
language?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
let nwscriptRegistered = false;
|
||||
|
||||
function registerNWScript(monaco: Parameters<OnMount>[1]) {
|
||||
if (nwscriptRegistered) return;
|
||||
nwscriptRegistered = true;
|
||||
|
||||
monaco.languages.register({ id: "nwscript", extensions: [".nss"] });
|
||||
|
||||
monaco.languages.setMonarchTokensProvider("nwscript", {
|
||||
keywords: [
|
||||
"void",
|
||||
"int",
|
||||
"float",
|
||||
"string",
|
||||
"object",
|
||||
"effect",
|
||||
"itemproperty",
|
||||
"location",
|
||||
"vector",
|
||||
"action",
|
||||
"talent",
|
||||
"event",
|
||||
"struct",
|
||||
"if",
|
||||
"else",
|
||||
"while",
|
||||
"for",
|
||||
"do",
|
||||
"switch",
|
||||
"case",
|
||||
"default",
|
||||
"break",
|
||||
"continue",
|
||||
"return",
|
||||
"const",
|
||||
],
|
||||
|
||||
constants: ["TRUE", "FALSE", "OBJECT_SELF", "OBJECT_INVALID"],
|
||||
|
||||
typeKeywords: [
|
||||
"void",
|
||||
"int",
|
||||
"float",
|
||||
"string",
|
||||
"object",
|
||||
"effect",
|
||||
"itemproperty",
|
||||
"location",
|
||||
"vector",
|
||||
"action",
|
||||
"talent",
|
||||
"event",
|
||||
"struct",
|
||||
],
|
||||
|
||||
operators: [
|
||||
"=",
|
||||
">",
|
||||
"<",
|
||||
"!",
|
||||
"~",
|
||||
"?",
|
||||
":",
|
||||
"==",
|
||||
"<=",
|
||||
">=",
|
||||
"!=",
|
||||
"&&",
|
||||
"||",
|
||||
"++",
|
||||
"--",
|
||||
"+",
|
||||
"-",
|
||||
"*",
|
||||
"/",
|
||||
"&",
|
||||
"|",
|
||||
"^",
|
||||
"%",
|
||||
"<<",
|
||||
">>",
|
||||
"+=",
|
||||
"-=",
|
||||
"*=",
|
||||
"/=",
|
||||
"&=",
|
||||
"|=",
|
||||
"^=",
|
||||
"%=",
|
||||
],
|
||||
|
||||
symbols: /[=><!~?:&|+\-*/^%]+/,
|
||||
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/#include\b/, "keyword.preprocessor"],
|
||||
[/#define\b/, "keyword.preprocessor"],
|
||||
|
||||
[
|
||||
/[a-zA-Z_]\w*/,
|
||||
{
|
||||
cases: {
|
||||
"@constants": "constant",
|
||||
"@typeKeywords": "type",
|
||||
"@keywords": "keyword",
|
||||
"@default": "identifier",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
{ include: "@whitespace" },
|
||||
|
||||
[/[{}()[\]]/, "@brackets"],
|
||||
[/[;,.]/, "delimiter"],
|
||||
|
||||
[
|
||||
/@symbols/,
|
||||
{
|
||||
cases: {
|
||||
"@operators": "operator",
|
||||
"@default": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
[/\d*\.\d+([eE][-+]?\d+)?[fF]?/, "number.float"],
|
||||
[/0[xX][0-9a-fA-F]+/, "number.hex"],
|
||||
[/\d+/, "number"],
|
||||
|
||||
[/"([^"\\]|\\.)*$/, "string.invalid"],
|
||||
[/"/, { token: "string.quote", next: "@string" }],
|
||||
],
|
||||
|
||||
string: [
|
||||
[/[^\\"]+/, "string"],
|
||||
[/\\./, "string.escape"],
|
||||
[/"/, { token: "string.quote", next: "@pop" }],
|
||||
],
|
||||
|
||||
whitespace: [
|
||||
[/[ \t\r\n]+/, "white"],
|
||||
[/\/\*/, "comment", "@comment"],
|
||||
[/\/\/.*$/, "comment"],
|
||||
],
|
||||
|
||||
comment: [
|
||||
[/[^/*]+/, "comment"],
|
||||
[/\*\//, "comment", "@pop"],
|
||||
[/[/*]/, "comment"],
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function defineForgeTheme(monaco: Parameters<OnMount>[1]) {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const bg = style.getPropertyValue("--forge-bg").trim() || "#121212";
|
||||
const surface = style.getPropertyValue("--forge-surface").trim() || "#1e1e2e";
|
||||
const accent = style.getPropertyValue("--forge-accent").trim() || "#946200";
|
||||
const text = style.getPropertyValue("--forge-text").trim() || "#f2f2f2";
|
||||
const textSecondary =
|
||||
style.getPropertyValue("--forge-text-secondary").trim() || "#888888";
|
||||
const border = style.getPropertyValue("--forge-border").trim() || "#2e2e3e";
|
||||
|
||||
monaco.editor.defineTheme("forge-dark", {
|
||||
base: "vs-dark",
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: "keyword", foreground: "C586C0" },
|
||||
{ token: "keyword.preprocessor", foreground: "569CD6" },
|
||||
{ token: "type", foreground: "4EC9B0" },
|
||||
{ token: "constant", foreground: "4FC1FF" },
|
||||
{ token: "string", foreground: "CE9178" },
|
||||
{ token: "comment", foreground: "6A9955" },
|
||||
{ token: "number", foreground: "B5CEA8" },
|
||||
{ token: "operator", foreground: "D4D4D4" },
|
||||
],
|
||||
colors: {
|
||||
"editor.background": bg,
|
||||
"editor.foreground": text,
|
||||
"editorCursor.foreground": accent,
|
||||
"editor.lineHighlightBackground": surface,
|
||||
"editorLineNumber.foreground": textSecondary,
|
||||
"editorLineNumber.activeForeground": text,
|
||||
"editor.selectionBackground": "#264f7840",
|
||||
"editorWidget.background": surface,
|
||||
"editorWidget.border": border,
|
||||
"editorSuggestWidget.background": surface,
|
||||
"editorSuggestWidget.border": border,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function languageFromPath(filePath: string): string {
|
||||
const ext = filePath.split(".").pop()?.toLowerCase();
|
||||
const map: Record<string, string> = {
|
||||
nss: "nwscript",
|
||||
json: "json",
|
||||
sql: "sql",
|
||||
css: "css",
|
||||
html: "html",
|
||||
xml: "xml",
|
||||
js: "javascript",
|
||||
ts: "typescript",
|
||||
md: "markdown",
|
||||
txt: "plaintext",
|
||||
"2da": "plaintext",
|
||||
};
|
||||
return map[ext ?? ""] ?? "plaintext";
|
||||
}
|
||||
|
||||
export function MonacoEditor({
|
||||
filePath,
|
||||
content,
|
||||
language,
|
||||
onChange,
|
||||
}: MonacoEditorProps) {
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||
|
||||
const handleMount: OnMount = useCallback(
|
||||
(editorInstance, monaco) => {
|
||||
editorRef.current = editorInstance;
|
||||
registerNWScript(monaco);
|
||||
defineForgeTheme(monaco);
|
||||
monaco.editor.setTheme("forge-dark");
|
||||
|
||||
const model = editorInstance.getModel();
|
||||
if (model) {
|
||||
const lang = language ?? languageFromPath(filePath);
|
||||
monaco.editor.setModelLanguage(model, lang);
|
||||
}
|
||||
},
|
||||
[filePath, language],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string | undefined) => {
|
||||
if (value !== undefined) {
|
||||
onChange?.(value);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactMonacoEditor
|
||||
value={content}
|
||||
language={language ?? languageFromPath(filePath)}
|
||||
theme="vs-dark"
|
||||
onChange={handleChange}
|
||||
onMount={handleMount}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
lineNumbers: "on",
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
tabSize: 4,
|
||||
insertSpaces: true,
|
||||
wordWrap: "off",
|
||||
renderWhitespace: "selection",
|
||||
bracketPairColorization: { enabled: true },
|
||||
padding: { top: 8 },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user