diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java index daf47a62..9734f319 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -6,6 +6,7 @@ import java.util.List; public record BaselineServerDescription( String name, String version, + String frontendLanguageId, boolean hoverSupported, List semanticTokenTypes, List visualThemes, @@ -14,6 +15,7 @@ public record BaselineServerDescription( public BaselineServerDescription { name = requireText(name, "name"); version = requireText(version, "version"); + frontendLanguageId = requireText(frontendLanguageId, "frontendLanguageId"); semanticTokenTypes = List.copyOf(Objects.requireNonNull(semanticTokenTypes, "semanticTokenTypes")); visualThemes = List.copyOf(Objects.requireNonNull(visualThemes, "visualThemes")); activeVisualThemeId = requireText(activeVisualThemeId, "activeVisualThemeId"); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java index 787aa02c..534c3ffa 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -36,6 +36,7 @@ public record CompilerLanguageServiceBridge() implements LanguageServiceBridge { return new BaselineServerDescription( "Prometeu Studio LSP", "0.1.0", + context.languageId(), true, PBSDefinitions.PBS.getSemanticPresentation().semanticKeys(), PBSDefinitions.PBS.getSemanticPresentation().themes().stream().map(this::mapVisualTheme).toList(), diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java index 28d8cf8d..aba2dd5f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -5,7 +5,9 @@ import p.studio.lsp.messages.*; import java.util.ArrayList; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { @Override @@ -26,6 +28,12 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); semanticTokens.setFull(true); capabilities.setSemanticTokensProvider(semanticTokens); + capabilities.setExperimental(Map.of( + "prometeuVisualThemes", + Map.of( + "frontendLanguageId", description.frontendLanguageId(), + "activeVisualThemeId", description.activeVisualThemeId(), + "visualThemes", description.visualThemes().stream().map(this::mapVisualThemePayload).toList()))); final InitializeResult result = new InitializeResult(capabilities); final ServerInfo serverInfo = new ServerInfo(); @@ -122,4 +130,25 @@ public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { case ERROR -> DiagnosticSeverity.Error; }; } + + private Map mapVisualThemePayload(final BaselineVisualTheme theme) { + final Map payload = new LinkedHashMap<>(); + payload.put("themeId", theme.themeId()); + payload.put("displayName", theme.displayName()); + payload.put("editorPalette", Map.of( + "baseForeground", theme.editorPalette().baseForeground(), + "selectionBackground", theme.editorPalette().selectionBackground(), + "activeHighlightBackground", theme.editorPalette().activeHighlightBackground(), + "lineNumberForeground", theme.editorPalette().lineNumberForeground(), + "statusChipBackground", theme.editorPalette().statusChipBackground(), + "statusChipBorder", theme.editorPalette().statusChipBorder(), + "statusChipForeground", theme.editorPalette().statusChipForeground())); + payload.put("tokenStyles", theme.tokenStyles().stream().map(tokenStyle -> Map.of( + "semanticKey", tokenStyle.semanticKey(), + "foreground", tokenStyle.foreground(), + "italic", tokenStyle.italic(), + "bold", tokenStyle.bold(), + "underline", tokenStyle.underline())).toList()); + return payload; + } } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java index 05e009cc..e613bb54 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -62,6 +62,7 @@ class CompilerLanguageServiceBridgeTest { final var description = bridge.describeServer(new LspProjectContext("main", "pbs", Path.of("."))); + assertEquals("pbs", description.frontendLanguageId()); assertEquals("pbs-default", description.activeVisualThemeId()); assertEquals(1, description.visualThemes().size()); assertEquals("PBS Default", description.visualThemes().getFirst().displayName()); diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java index 10e8e769..f59b745c 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -55,6 +55,7 @@ class PrometeuLanguageServerTest { assertSame(expected, result); assertEquals(1, bridge.describeServerCalls); assertSame(bridge.serverDescription, mapper.lastServerDescription); + assertEquals("pbs", bridge.serverDescription.frontendLanguageId()); } @Test @@ -85,6 +86,7 @@ class PrometeuLanguageServerTest { private final BaselineServerDescription serverDescription = new BaselineServerDescription( "demo", "1", + "pbs", true, List.of(), List.of(new BaselineVisualTheme( diff --git a/tools/vscode-extension/out/extension.js b/tools/vscode-extension/out/extension.js new file mode 100644 index 00000000..bd27beb0 --- /dev/null +++ b/tools/vscode-extension/out/extension.js @@ -0,0 +1,292 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.activate = activate; +exports.deactivate = deactivate; +const net = __importStar(require("node:net")); +const vscode = __importStar(require("vscode")); +const node_1 = require("vscode-languageclient/node"); +let client; +let output; +let reconnectTimer; +let reconnectContext; +let stoppingClient = false; +let lastAppliedThemeSignature; +const RECONNECT_DELAY_MS = 2000; +function activate(context) { + reconnectContext = context; + output = vscode.window.createOutputChannel("Prometeu LSP"); + output.appendLine("Prometeu VS Code extension activated."); + const connectCommand = vscode.commands.registerCommand("prometeu.connectStudioLsp", async () => { + await startClient(context); + }); + const restartCommand = vscode.commands.registerCommand("prometeu.restartStudioLsp", async () => { + await stopClient(); + await startClient(context); + }); + context.subscriptions.push(output, connectCommand, restartCommand); + void startClient(context); +} +async function deactivate() { + await stopClient(); +} +async function startClient(context) { + clearReconnectTimer(); + if (client) { + output.appendLine("Prometeu LSP client is already running."); + return; + } + const config = vscode.workspace.getConfiguration("prometeu.studio"); + const host = config.get("host", "127.0.0.1"); + const port = config.get("port", 7775); + output.show(true); + output.appendLine(`Connecting to Prometeu Studio LSP at ${host}:${port}...`); + const serverOptions = async () => { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ host, port }); + let settled = false; + const connectTimeout = setTimeout(() => { + if (settled) { + return; + } + settled = true; + socket.destroy(); + reject(new Error("Connection timed out.")); + }, 3000); + socket.once("connect", () => { + if (settled) { + return; + } + settled = true; + clearTimeout(connectTimeout); + output.appendLine("TCP connection established with Prometeu Studio."); + resolve({ + reader: socket, + writer: socket + }); + }); + socket.once("error", (error) => { + if (settled) { + return; + } + settled = true; + clearTimeout(connectTimeout); + reject(error); + }); + }); + }; + const clientOptions = { + documentSelector: [ + { + scheme: "file", + language: "pbs" + } + ], + synchronize: { + fileEvents: [ + vscode.workspace.createFileSystemWatcher("**/*.pbs") + ] + }, + initializationOptions: { + clientKind: "vscode", + prometeuClientVersion: context.extension.packageJSON.version + }, + outputChannel: output, + errorHandler: { + error: (error) => { + output.appendLine(`Prometeu LSP transport error: ${error.message}`); + return { action: node_1.ErrorAction.Continue }; + }, + closed: () => { + output.appendLine("Prometeu LSP transport closed."); + return { action: node_1.CloseAction.DoNotRestart }; + } + } + }; + const nextClient = new node_1.LanguageClient("prometeuStudioLsp", "Prometeu Studio LSP", serverOptions, clientOptions); + client = nextClient; + nextClient.onDidChangeState((event) => { + output.appendLine(`Prometeu LSP state changed: ${node_1.State[event.oldState]} -> ${node_1.State[event.newState]}`); + if (event.newState !== node_1.State.Stopped) { + return; + } + if (client === nextClient) { + client = undefined; + } + if (!stoppingClient) { + scheduleReconnect(); + } + }); + try { + await nextClient.start(); + await applyPrometeuVisualTheme(nextClient.initializeResult); + output.appendLine("Prometeu Studio LSP client started."); + vscode.window.showInformationMessage("Connected to Prometeu Studio LSP."); + } + catch (error) { + if (client === nextClient) { + client = undefined; + } + const message = error instanceof Error ? error.message : String(error); + output.appendLine(`Failed to start Prometeu LSP client: ${message}`); + vscode.window.showErrorMessage(`Could not connect to Prometeu Studio LSP: ${message}`); + scheduleReconnect(); + } +} +async function stopClient() { + clearReconnectTimer(); + if (!client) { + return; + } + const currentClient = client; + client = undefined; + stoppingClient = true; + output.appendLine("Stopping Prometeu Studio LSP client..."); + try { + await currentClient.stop(); + output.appendLine("Prometeu Studio LSP client stopped."); + } + catch (error) { + const message = error instanceof Error ? error.message : String(error); + output.appendLine(`Failed to stop Prometeu LSP client cleanly: ${message}`); + } + finally { + stoppingClient = false; + } +} +function scheduleReconnect() { + if (!reconnectContext) { + return; + } + if (reconnectTimer || client) { + return; + } + output.appendLine(`Scheduling Prometeu LSP reconnect in ${RECONNECT_DELAY_MS}ms...`); + reconnectTimer = setTimeout(() => { + reconnectTimer = undefined; + void startClient(reconnectContext); + }, RECONNECT_DELAY_MS); +} +function clearReconnectTimer() { + if (!reconnectTimer) { + return; + } + clearTimeout(reconnectTimer); + reconnectTimer = undefined; +} +async function applyPrometeuVisualTheme(initializeResult) { + const payload = extractPrometeuVisualThemesPayload(initializeResult); + if (!payload) { + output.appendLine("Prometeu visual theme payload not present in initialize result."); + return; + } + const activeTheme = payload.visualThemes.find((theme) => theme.themeId === payload.activeVisualThemeId); + if (!activeTheme) { + output.appendLine(`Prometeu visual theme '${payload.activeVisualThemeId}' was not declared by the server.`); + return; + } + const themeSignature = JSON.stringify({ + languageId: payload.frontendLanguageId, + themeId: activeTheme.themeId, + editorPalette: activeTheme.editorPalette, + tokenStyles: activeTheme.tokenStyles + }); + if (themeSignature === lastAppliedThemeSignature) { + output.appendLine(`Prometeu visual theme '${activeTheme.themeId}' is already applied.`); + return; + } + const configurationTarget = vscode.workspace.workspaceFolders?.length + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.Global; + const configuration = vscode.workspace.getConfiguration(); + const existingSemanticCustomizations = configuration.get("editor.semanticTokenColorCustomizations") ?? {}; + const semanticRules = { + ...(readObject(existingSemanticCustomizations.rules)), + ...buildSemanticTokenRules(payload.frontendLanguageId, activeTheme) + }; + await configuration.update("editor.semanticTokenColorCustomizations", { + ...existingSemanticCustomizations, + enabled: true, + rules: semanticRules + }, configurationTarget); + const existingWorkbenchColors = configuration.get("workbench.colorCustomizations") ?? {}; + await configuration.update("workbench.colorCustomizations", { + ...existingWorkbenchColors, + "editor.foreground": activeTheme.editorPalette.baseForeground, + "editor.selectionBackground": activeTheme.editorPalette.selectionBackground, + "editorLineNumber.foreground": activeTheme.editorPalette.lineNumberForeground + }, configurationTarget); + lastAppliedThemeSignature = themeSignature; + output.appendLine(`Applied Prometeu visual theme '${activeTheme.themeId}' for language '${payload.frontendLanguageId}'.`); +} +function extractPrometeuVisualThemesPayload(initializeResult) { + if (!initializeResult || typeof initializeResult !== "object") { + return undefined; + } + const capabilities = readObject(initializeResult.capabilities); + const experimental = readObject(capabilities.experimental); + const payload = readObject(experimental.prometeuVisualThemes); + if (typeof payload.frontendLanguageId !== "string" || typeof payload.activeVisualThemeId !== "string") { + return undefined; + } + if (!Array.isArray(payload.visualThemes)) { + return undefined; + } + return payload; +} +function buildSemanticTokenRules(languageId, theme) { + const rules = {}; + for (const tokenStyle of theme.tokenStyles) { + const selector = `${tokenStyle.semanticKey}:${languageId}`; + if (!tokenStyle.italic && !tokenStyle.bold && !tokenStyle.underline) { + rules[selector] = tokenStyle.foreground; + continue; + } + rules[selector] = { + foreground: tokenStyle.foreground, + ...(tokenStyle.italic ? { italic: true } : {}), + ...(tokenStyle.bold ? { bold: true } : {}), + ...(tokenStyle.underline ? { underline: true } : {}) + }; + } + return rules; +} +function readObject(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return {}; + } + return value; +} +//# sourceMappingURL=extension.js.map diff --git a/tools/vscode-extension/package.json b/tools/vscode-extension/package.json index 425b945d..f370837e 100644 --- a/tools/vscode-extension/package.json +++ b/tools/vscode-extension/package.json @@ -148,41 +148,7 @@ } ], "configurationDefaults": { - "editor.semanticHighlighting.enabled": true, - "editor.semanticTokenColorCustomizations": { - "enabled": true, - "rules": { - "pbs-keyword:pbs": "#569cd6", - "pbs-lifecycle:pbs": "#ef50c0", - "pbs-function:pbs": "#f2c14e", - "pbs-method:pbs": "#f2c14e", - "pbs-constructor:pbs": "#ecdcaa", - "pbs-struct:pbs": "#4ec9b0", - "pbs-contract:pbs": "#78dce8", - "pbs-host:pbs": "#b7a2fa", - "pbs-builtin-type:pbs": "#8be9fd", - "pbs-service:pbs": "#b7a2fa", - "pbs-error:pbs": "#ff5b5b", - "pbs-enum:pbs": "#56cfe1", - "pbs-callback:pbs": "#b7a2fa", - "pbs-global:pbs": { - "foreground": "#f790fc", - "italic": true - }, - "pbs-const:pbs": "#f78c6c", - "pbs-implements:pbs": "#a1c181", - "pbs-string:pbs": "#00c088", - "pbs-number:pbs": "#ff90b0", - "pbs-comment:pbs": { - "foreground": "#c090b0", - "italic": true - }, - "pbs-literal:pbs": "#4fc1ff", - "pbs-operator:pbs": "#d4d4d4", - "pbs-punctuation:pbs": "#d4d4d4", - "pbs-identifier:pbs": "#d4d4d4" - } - } + "editor.semanticHighlighting.enabled": true } }, "scripts": { diff --git a/tools/vscode-extension/src/extension.ts b/tools/vscode-extension/src/extension.ts index 82e99904..19d7a1b5 100644 --- a/tools/vscode-extension/src/extension.ts +++ b/tools/vscode-extension/src/extension.ts @@ -16,9 +16,41 @@ let output: vscode.OutputChannel; let reconnectTimer: NodeJS.Timeout | undefined; let reconnectContext: vscode.ExtensionContext | undefined; let stoppingClient = false; +let lastAppliedThemeSignature: string | undefined; const RECONNECT_DELAY_MS = 2000; +type PrometeuTokenStyle = { + semanticKey: string; + foreground: string; + italic: boolean; + bold: boolean; + underline: boolean; +}; + +type PrometeuEditorPalette = { + baseForeground: string; + selectionBackground: string; + activeHighlightBackground: string; + lineNumberForeground: string; + statusChipBackground: string; + statusChipBorder: string; + statusChipForeground: string; +}; + +type PrometeuVisualTheme = { + themeId: string; + displayName: string; + editorPalette: PrometeuEditorPalette; + tokenStyles: PrometeuTokenStyle[]; +}; + +type PrometeuVisualThemesPayload = { + frontendLanguageId: string; + activeVisualThemeId: string; + visualThemes: PrometeuVisualTheme[]; +}; + export function activate(context: vscode.ExtensionContext): void { reconnectContext = context; output = vscode.window.createOutputChannel("Prometeu LSP"); @@ -160,6 +192,7 @@ async function startClient(context: vscode.ExtensionContext): Promise { try { await nextClient.start(); + await applyPrometeuVisualTheme(nextClient.initializeResult); output.appendLine("Prometeu Studio LSP client started."); vscode.window.showInformationMessage("Connected to Prometeu Studio LSP."); @@ -220,3 +253,111 @@ function clearReconnectTimer(): void { clearTimeout(reconnectTimer); reconnectTimer = undefined; } + +async function applyPrometeuVisualTheme(initializeResult: unknown): Promise { + const payload = extractPrometeuVisualThemesPayload(initializeResult); + if (!payload) { + output.appendLine("Prometeu visual theme payload not present in initialize result."); + return; + } + + const activeTheme = payload.visualThemes.find((theme) => theme.themeId === payload.activeVisualThemeId); + if (!activeTheme) { + output.appendLine(`Prometeu visual theme '${payload.activeVisualThemeId}' was not declared by the server.`); + return; + } + + const themeSignature = JSON.stringify({ + languageId: payload.frontendLanguageId, + themeId: activeTheme.themeId, + editorPalette: activeTheme.editorPalette, + tokenStyles: activeTheme.tokenStyles + }); + if (themeSignature === lastAppliedThemeSignature) { + output.appendLine(`Prometeu visual theme '${activeTheme.themeId}' is already applied.`); + return; + } + + const configurationTarget = vscode.workspace.workspaceFolders?.length + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.Global; + const configuration = vscode.workspace.getConfiguration(); + + const existingSemanticCustomizations = configuration.get>("editor.semanticTokenColorCustomizations") ?? {}; + const semanticRules = { + ...(readObject(existingSemanticCustomizations.rules)), + ...buildSemanticTokenRules(payload.frontendLanguageId, activeTheme) + }; + + await configuration.update( + "editor.semanticTokenColorCustomizations", + { + ...existingSemanticCustomizations, + enabled: true, + rules: semanticRules + }, + configurationTarget + ); + + const existingWorkbenchColors = configuration.get>("workbench.colorCustomizations") ?? {}; + await configuration.update( + "workbench.colorCustomizations", + { + ...existingWorkbenchColors, + "editor.foreground": activeTheme.editorPalette.baseForeground, + "editor.selectionBackground": activeTheme.editorPalette.selectionBackground, + "editorLineNumber.foreground": activeTheme.editorPalette.lineNumberForeground + }, + configurationTarget + ); + + lastAppliedThemeSignature = themeSignature; + output.appendLine(`Applied Prometeu visual theme '${activeTheme.themeId}' for language '${payload.frontendLanguageId}'.`); +} + +function extractPrometeuVisualThemesPayload(initializeResult: unknown): PrometeuVisualThemesPayload | undefined { + if (!initializeResult || typeof initializeResult !== "object") { + return undefined; + } + + const capabilities = readObject((initializeResult as { capabilities?: unknown }).capabilities); + const experimental = readObject(capabilities.experimental); + const payload = readObject(experimental.prometeuVisualThemes); + + if (typeof payload.frontendLanguageId !== "string" || typeof payload.activeVisualThemeId !== "string") { + return undefined; + } + if (!Array.isArray(payload.visualThemes)) { + return undefined; + } + + return payload as unknown as PrometeuVisualThemesPayload; +} + +function buildSemanticTokenRules( + languageId: string, + theme: PrometeuVisualTheme +): Record { + const rules: Record = {}; + for (const tokenStyle of theme.tokenStyles) { + const selector = `${tokenStyle.semanticKey}:${languageId}`; + if (!tokenStyle.italic && !tokenStyle.bold && !tokenStyle.underline) { + rules[selector] = tokenStyle.foreground; + continue; + } + rules[selector] = { + foreground: tokenStyle.foreground, + ...(tokenStyle.italic ? { italic: true } : {}), + ...(tokenStyle.bold ? { bold: true } : {}), + ...(tokenStyle.underline ? { underline: true } : {}) + }; + } + return rules; +} + +function readObject(value: unknown): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return {}; + } + return value as Record; +}