f851d8b8f2
Electron desktop application for Neverwinter Nights module development. Clone, edit, build, and run a complete Layonara NWNX server with only Docker required. - React 19 + Vite frontend with Monaco editor and NWScript LSP - Node.js + Express backend managing Docker sibling containers - Electron shell with Docker availability check and auto-setup - Builder image auto-builds on first use from bundled Dockerfile - Cross-platform: Windows (.exe), macOS (.dmg), Linux (.AppImage) - Gitea Actions CI for automated release builds
212 lines
7.3 KiB
TypeScript
212 lines
7.3 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
|
import { LogLevel } from "@codingame/monaco-vscode-api";
|
|
import {
|
|
MonacoEditorReactComp,
|
|
} from "@typefox/monaco-editor-react";
|
|
import type { MonacoVscodeApiConfig } from "monaco-languageclient/vscodeApiWrapper";
|
|
import type { EditorAppConfig, TextContents } from "monaco-languageclient/editorApp";
|
|
import type { LanguageClientConfig } from "monaco-languageclient/lcwrapper";
|
|
import { configureDefaultWorkerFactory } from "monaco-languageclient/workerFactory";
|
|
import "../../nwscript-extension/index.js";
|
|
|
|
function getVscodeApiConfig(): MonacoVscodeApiConfig {
|
|
return {
|
|
$type: "extended",
|
|
viewsConfig: {
|
|
$type: "EditorService",
|
|
},
|
|
userConfiguration: {
|
|
json: JSON.stringify({
|
|
"workbench.colorTheme": "Default Dark Modern",
|
|
"workbench.colorCustomizations": {
|
|
"editor.background": "#231e17",
|
|
"editor.foreground": "#ece8e3",
|
|
"editor.lineHighlightBackground": "#302a2040",
|
|
"editor.selectionBackground": "#3d3018",
|
|
"editor.selectionHighlightBackground": "#3d301860",
|
|
"editor.inactiveSelectionBackground": "#3d301850",
|
|
"editor.findMatchBackground": "#b07a0040",
|
|
"editor.findMatchHighlightBackground": "#b07a0025",
|
|
"editor.hoverHighlightBackground": "#3d301830",
|
|
"editorCursor.foreground": "#b07a00",
|
|
"editorWhitespace.foreground": "#4a403550",
|
|
"editorIndentGuide.background": "#4a403530",
|
|
"editorIndentGuide.activeBackground": "#4a403580",
|
|
"editorLineNumber.foreground": "#a69f9650",
|
|
"editorLineNumber.activeForeground": "#ece8e3",
|
|
"editorBracketMatch.background": "#3d301840",
|
|
"editorBracketMatch.border": "#b07a0080",
|
|
"editorOverviewRuler.border": "#4a4035",
|
|
"editorGutter.background": "#231e17",
|
|
"editorWidget.background": "#3b3328",
|
|
"editorWidget.foreground": "#ece8e3",
|
|
"editorWidget.border": "#4a4035",
|
|
"editorSuggestWidget.background": "#3b3328",
|
|
"editorSuggestWidget.border": "#4a4035",
|
|
"editorSuggestWidget.foreground": "#ece8e3",
|
|
"editorSuggestWidget.highlightForeground": "#b07a00",
|
|
"editorSuggestWidget.selectedBackground": "#3d3018",
|
|
"editorHoverWidget.background": "#3b3328",
|
|
"editorHoverWidget.border": "#4a4035",
|
|
"editorGroupHeader.tabsBackground": "#302a20",
|
|
"editorGroupHeader.tabsBorder": "#4a4035",
|
|
"editorGroup.border": "#4a4035",
|
|
"tab.activeBackground": "#231e17",
|
|
"tab.activeForeground": "#ece8e3",
|
|
"tab.activeBorderTop": "#b07a00",
|
|
"tab.inactiveBackground": "#302a20",
|
|
"tab.inactiveForeground": "#a69f96",
|
|
"tab.border": "#4a4035",
|
|
"input.background": "#231e17",
|
|
"input.foreground": "#ece8e3",
|
|
"input.border": "#4a4035",
|
|
"input.placeholderForeground": "#a69f9680",
|
|
"inputOption.activeBorder": "#b07a00",
|
|
"dropdown.background": "#3b3328",
|
|
"dropdown.foreground": "#ece8e3",
|
|
"dropdown.border": "#4a4035",
|
|
"list.activeSelectionBackground": "#3d3018",
|
|
"list.activeSelectionForeground": "#ece8e3",
|
|
"list.inactiveSelectionBackground": "#3d301880",
|
|
"list.hoverBackground": "#302a2080",
|
|
"list.highlightForeground": "#b07a00",
|
|
"scrollbarSlider.background": "#4a403540",
|
|
"scrollbarSlider.hoverBackground": "#4a403580",
|
|
"scrollbarSlider.activeBackground": "#4a4035a0",
|
|
"focusBorder": "#b07a0080",
|
|
"peekView.border": "#b07a00",
|
|
"peekViewEditor.background": "#231e17",
|
|
"peekViewResult.background": "#302a20",
|
|
"peekViewTitle.background": "#302a20",
|
|
"minimap.selectionHighlight": "#3d3018",
|
|
"minimap.findMatchHighlight": "#b07a0060",
|
|
},
|
|
"editor.fontSize": 14,
|
|
"editor.fontFamily": "'JetBrains Mono Variable', 'Fira Code', monospace",
|
|
"editor.tabSize": 4,
|
|
"editor.insertSpaces": true,
|
|
"editor.minimap.enabled": false,
|
|
"editor.scrollBeyondLastLine": false,
|
|
"editor.wordWrap": "off",
|
|
"editor.renderWhitespace": "selection",
|
|
"editor.bracketPairColorization.enabled": true,
|
|
"editor.padding.top": 8,
|
|
"editor.lineNumbers": "on",
|
|
"editor.guides.bracketPairsHorizontal": "active",
|
|
"editor.wordBasedSuggestions": "off",
|
|
"editor.quickSuggestions": true,
|
|
"editor.parameterHints.enabled": true,
|
|
}),
|
|
},
|
|
monacoWorkerFactory: configureDefaultWorkerFactory,
|
|
};
|
|
}
|
|
|
|
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",
|
|
cfg: "ini",
|
|
sh: "shellscript",
|
|
};
|
|
return map[ext ?? ""] ?? "plaintext";
|
|
}
|
|
|
|
interface MonacoEditorProps {
|
|
filePath: string;
|
|
content: string;
|
|
language?: string;
|
|
onChange?: (value: string) => void;
|
|
workspacePath?: string;
|
|
}
|
|
|
|
export function MonacoEditor({
|
|
filePath,
|
|
content,
|
|
language,
|
|
onChange,
|
|
workspacePath,
|
|
}: MonacoEditorProps) {
|
|
const resolvedLang = language ?? languageFromPath(filePath);
|
|
const isNwscript = resolvedLang === "nwscript";
|
|
|
|
const fileUri = workspacePath
|
|
? `file://${workspacePath}/repos/nwn-module/${filePath}`
|
|
: `file:///workspace/repos/nwn-module/${filePath}`;
|
|
|
|
const editorAppConfig = useMemo<EditorAppConfig>(
|
|
() => ({
|
|
codeResources: {
|
|
modified: {
|
|
text: content,
|
|
uri: fileUri,
|
|
enforceLanguageId: resolvedLang,
|
|
},
|
|
},
|
|
editorOptions: {
|
|
automaticLayout: true,
|
|
},
|
|
}),
|
|
[filePath],
|
|
);
|
|
|
|
const languageClientConfig = useMemo<LanguageClientConfig | undefined>(() => {
|
|
if (!isNwscript) return undefined;
|
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
return {
|
|
languageId: "nwscript",
|
|
connection: {
|
|
options: {
|
|
$type: "WebSocketUrl" as const,
|
|
url: `${protocol}//${window.location.host}/ws/lsp`,
|
|
},
|
|
},
|
|
clientOptions: {
|
|
documentSelector: ["nwscript"],
|
|
workspaceFolder: workspacePath
|
|
? {
|
|
index: 0,
|
|
name: "nwn-module",
|
|
uri: { scheme: "file", path: `${workspacePath}/repos/nwn-module` } as any,
|
|
}
|
|
: undefined,
|
|
},
|
|
};
|
|
}, [isNwscript, workspacePath]);
|
|
|
|
const handleTextChanged = useCallback(
|
|
(textChanges: TextContents) => {
|
|
if (textChanges.modified !== undefined) {
|
|
onChange?.(textChanges.modified);
|
|
}
|
|
},
|
|
[onChange],
|
|
);
|
|
|
|
const handleError = useCallback((error: Error) => {
|
|
console.error("[MonacoEditor]", error.message, error.stack);
|
|
}, []);
|
|
|
|
return (
|
|
<MonacoEditorReactComp
|
|
style={{ width: "100%", height: "100%" }}
|
|
vscodeApiConfig={getVscodeApiConfig()}
|
|
editorAppConfig={editorAppConfig}
|
|
languageClientConfig={languageClientConfig}
|
|
onTextChanged={handleTextChanged}
|
|
onError={handleError}
|
|
logLevel={LogLevel.Warning}
|
|
/>
|
|
);
|
|
}
|