feat: integrate monaco-languageclient v10 with NWScript LSP
Replace hand-rolled LSP client (lspClient.ts, useLspClient.ts) with monaco-languageclient v10 extended mode using @typefox/monaco-editor-react. NWScript TextMate grammar from the LSP submodule provides syntax highlighting. Full LSP features: completion, hover, diagnostics, go-to-definition, signature help — all wired through WebSocket to the nwscript-language-server. LSP server patches: fix workspaceFolders null assertion crash, handle missing workspace/configuration gracefully, derive rootPath from rootUri when null, guard tokenizer getRawTokenContent against undefined tokens. Backend fixes: WebSocket routing changed to noServer mode so /ws, /ws/lsp, and /ws/terminal/* don't conflict. TLK index loaded at startup (41,927 entries from nwn-haks/layonara.tlk.json). Workspace routes get proper try/catch. writeConfig creates parent directories. setupClone ensures workspace structure. Frontend: GffEditor and AreaEditor rewritten with inline styles and TLK resolution for CExoLocString fields. EditorTabs rewritten with lucide icons. Tab content hydrates from API on refresh. Setup wizard gets friendly error messages. SimpleEditor/SimpleDiffEditor for non-LSP editor uses. Vite config updated for monaco-vscode-api compatibility.
This commit is contained in:
@@ -4,7 +4,7 @@ import { createServer } from "http";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { initWebSocket, getClientCount } from "./services/ws.service.js";
|
||||
import { initWebSocket, getClientCount, handleUpgrade as handleEventUpgrade } from "./services/ws.service.js";
|
||||
import workspaceRouter from "./routes/workspace.js";
|
||||
import dockerRouter from "./routes/docker.js";
|
||||
import editorRouter from "./routes/editor.js";
|
||||
@@ -17,6 +17,8 @@ import reposRouter from "./routes/repos.js";
|
||||
import { attachWebSocket, createTerminalSession } from "./services/terminal.service.js";
|
||||
import { attachLspWebSocket } from "./services/lsp.service.js";
|
||||
import { startUpstreamPolling } from "./services/git.service.js";
|
||||
import { loadTlkIndex } from "./nwscript/tlk-index.js";
|
||||
import { getRepoPath } from "./services/workspace.service.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const app = express();
|
||||
@@ -72,6 +74,12 @@ server.on("upgrade", (request, socket, head) => {
|
||||
attachWebSocket(sessionId, ws);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/ws") {
|
||||
handleEventUpgrade(request, socket, head);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -79,4 +87,6 @@ const PORT = parseInt(process.env.PORT || "3000", 10);
|
||||
server.listen(PORT, "0.0.0.0", () => {
|
||||
console.log(`Layonara Forge listening on http://0.0.0.0:${PORT}`);
|
||||
startUpstreamPolling();
|
||||
const tlkPath = getRepoPath("nwn-haks", "layonara.tlk.json");
|
||||
loadTlkIndex(tlkPath).then(() => console.log(`TLK index loaded`)).catch(() => {});
|
||||
});
|
||||
|
||||
@@ -8,11 +8,12 @@ export async function loadTlkIndex(tlkJsonPath: string): Promise<void> {
|
||||
try {
|
||||
const raw = await fs.readFile(tlkJsonPath, "utf-8");
|
||||
const data = JSON.parse(raw);
|
||||
if (Array.isArray(data)) {
|
||||
for (const entry of data) {
|
||||
if (entry.id !== undefined && entry.value !== undefined) {
|
||||
tlkStrings.set(Number(entry.id), String(entry.value));
|
||||
}
|
||||
const entries = Array.isArray(data) ? data : Array.isArray(data.entries) ? data.entries : [];
|
||||
for (const entry of entries) {
|
||||
const id = entry.id ?? entry.index;
|
||||
const text = entry.text ?? entry.value;
|
||||
if (id !== undefined && text !== undefined) {
|
||||
tlkStrings.set(Number(id), String(text));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -9,21 +9,36 @@ import {
|
||||
const router = Router();
|
||||
|
||||
router.get("/config", async (_req, res) => {
|
||||
const config = await readConfig();
|
||||
const sanitized = { ...config, githubPat: config.githubPat ? "***" : undefined };
|
||||
res.json(sanitized);
|
||||
try {
|
||||
const config = await readConfig();
|
||||
const sanitized = { ...config, githubPat: config.githubPat ? "***" : undefined };
|
||||
res.json(sanitized);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "Failed to read config";
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/config", async (req, res) => {
|
||||
const current = await readConfig();
|
||||
const updated = { ...current, ...req.body };
|
||||
await writeConfig(updated);
|
||||
res.json({ ok: true });
|
||||
try {
|
||||
const current = await readConfig();
|
||||
const updated = { ...current, ...req.body };
|
||||
await writeConfig(updated);
|
||||
res.json({ ok: true });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "Failed to save config";
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/init", async (_req, res) => {
|
||||
await ensureWorkspaceStructure();
|
||||
res.json({ ok: true, path: getWorkspacePath() });
|
||||
try {
|
||||
await ensureWorkspaceStructure();
|
||||
res.json({ ok: true, path: getWorkspacePath() });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "Failed to initialize workspace";
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { simpleGit, SimpleGit } from "simple-git";
|
||||
import fs from "fs/promises";
|
||||
import { REPOS, GIT_PROVIDER_URL, type RepoName } from "../config/repos.js";
|
||||
import { getRepoPath, readConfig } from "./workspace.service.js";
|
||||
import { getRepoPath, readConfig, ensureWorkspaceStructure } from "./workspace.service.js";
|
||||
import { broadcast } from "./ws.service.js";
|
||||
|
||||
function git(repoPath: string): SimpleGit {
|
||||
@@ -128,6 +128,7 @@ function getUpstreamUrl(owner: string, repo: string, provider: string, token?: s
|
||||
}
|
||||
|
||||
export async function setupClone(repoName: RepoName) {
|
||||
await ensureWorkspaceStructure();
|
||||
const config = await readConfig();
|
||||
const pat = config.githubPat;
|
||||
if (!pat) throw new Error("Git provider token not configured");
|
||||
|
||||
@@ -2,6 +2,7 @@ import { spawn, ChildProcess } from "child_process";
|
||||
import { WebSocket } from "ws";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { getRepoPath } from "./workspace.service.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -21,7 +22,9 @@ export function startLspServer(): ChildProcess {
|
||||
}
|
||||
|
||||
const serverPath = getLspServerPath();
|
||||
const cwd = getRepoPath("nwn-module");
|
||||
lspProcess = spawn("node", [serverPath, "--stdio"], {
|
||||
cwd,
|
||||
env: { ...process.env },
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ export async function readConfig(): Promise<ForgeConfig> {
|
||||
|
||||
export async function writeConfig(config: ForgeConfig): Promise<void> {
|
||||
const configPath = path.join(WORKSPACE_PATH, "config", "forge.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ const clients = new Set<WebSocket>();
|
||||
|
||||
let wss: WebSocketServer;
|
||||
|
||||
export function initWebSocket(server: Server): WebSocketServer {
|
||||
wss = new WebSocketServer({ server, path: "/ws" });
|
||||
export function initWebSocket(_server: Server): WebSocketServer {
|
||||
wss = new WebSocketServer({ noServer: true });
|
||||
|
||||
wss.on("connection", (ws) => {
|
||||
clients.add(ws);
|
||||
@@ -26,6 +26,12 @@ export function initWebSocket(server: Server): WebSocketServer {
|
||||
return wss;
|
||||
}
|
||||
|
||||
export function handleUpgrade(request: import("http").IncomingMessage, socket: import("stream").Duplex, head: Buffer): void {
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit("connection", ws, request);
|
||||
});
|
||||
}
|
||||
|
||||
export function broadcast(type: EventType, action: string, data: unknown): void {
|
||||
const event: ForgeEvent = {
|
||||
type,
|
||||
|
||||
Reference in New Issue
Block a user