diff --git a/client/package.json b/client/package.json index 2015ea3..a39cfd1 100644 --- a/client/package.json +++ b/client/package.json @@ -100,6 +100,26 @@ "command": "liquidjava.showView", "title": "Show View", "category": "LiquidJava" + }, + { + "command": "liquidjava.start", + "title": "Start", + "category": "LiquidJava" + }, + { + "command": "liquidjava.stop", + "title": "Stop", + "category": "LiquidJava" + }, + { + "command": "liquidjava.restart", + "title": "Restart", + "category": "LiquidJava" + }, + { + "command": "liquidjava.verify", + "title": "Verify", + "category": "LiquidJava" } ], "menus": {}, diff --git a/client/server/language-server-liquidjava.jar b/client/server/language-server-liquidjava.jar index 5bc6a37..b29d285 100644 Binary files a/client/server/language-server-liquidjava.jar and b/client/server/language-server-liquidjava.jar differ diff --git a/client/src/extension.ts b/client/src/extension.ts index 74bc4e6..37ddd39 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -8,7 +8,7 @@ import { registerStatusBar, updateStatusBar } from "./services/status-bar"; import { registerWebview } from "./services/webview"; import { registerHover } from "./services/hover"; import { registerEvents } from "./services/events"; -import { runLanguageServer } from "./lsp/server"; +import { runLanguageServer, stopLanguageServer } from "./lsp/server"; import { runClient, stopClient } from "./lsp/client"; /** @@ -26,12 +26,35 @@ export async function activate(context: vscode.ExtensionContext) { extension.logger.client.info("Activating LiquidJava extension..."); await applyItalicOverlay(); + await startExtension(context); +} + +/** + * Deactivates the LiquidJava extension + */ +export async function deactivate() { + extension.logger?.client.info("Deactivating LiquidJava extension..."); + await stopClient("Extension was deactivated"); + await stopLanguageServer(); +} + +/** + * Starts the LiquidJava language server and client + * @param context The extension context + */ +export async function startExtension(context: vscode.ExtensionContext) { + // check if already running + if (extension.client || extension.serverProcess) { + extension.logger.client.info("LiquidJava is already running"); + return; + } + extension.logger.client.info("Starting LiquidJava..."); // find java executable path const javaExecutablePath = findJavaExecutable(); if (!javaExecutablePath) { vscode.window.showErrorMessage("LiquidJava - Java Runtime Not Found in JAVA_HOME or PATH"); - extension.logger.client.error("Java Runtime not found in JAVA_HOME or PATH - Not activating extension"); + extension.logger.client.error("Java Runtime not found in JAVA_HOME or PATH"); updateStatusBar("stopped"); return; } @@ -47,9 +70,33 @@ export async function activate(context: vscode.ExtensionContext) { } /** - * Deactivates the LiquidJava extension + * Stops the LiquidJava language server and client */ -export async function deactivate() { - extension.logger?.client.info("Deactivating LiquidJava extension..."); - await stopClient("Extension was deactivated"); +export async function stopExtension() { + if (!extension.client && !extension.serverProcess) { + extension.logger?.client.info("LiquidJava is not running"); + return; + } + extension.logger?.client.info("Stopping LiquidJava..."); + await stopClient("Extension stop command"); + await stopLanguageServer(); } + +/** + * Restarts the LiquidJava language server and client + * @param context The extension context + */ +export async function restartExtension(context: vscode.ExtensionContext) { + extension.logger?.client.info("Restarting LiquidJava..."); + + // stop if running + if (extension.client || extension.serverProcess) { + await stopExtension(); + // ensure clean shutdown + await new Promise(resolve => setTimeout(resolve, 500)); + } + + // start again + await startExtension(context); +} + diff --git a/client/src/lsp/client.ts b/client/src/lsp/client.ts index 8672e25..8153a08 100644 --- a/client/src/lsp/client.ts +++ b/client/src/lsp/client.ts @@ -1,10 +1,9 @@ import * as vscode from 'vscode'; -import { LanguageClient, LanguageClientOptions, ServerOptions, State } from 'vscode-languageclient/node'; +import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; import { connectToPort } from '../utils/utils'; -import { killProcess } from '../utils/utils'; import { extension } from '../state'; import { updateStatusBar } from '../services/status-bar'; -import { handleLJDiagnostics } from '../services/webview'; +import { handleLJDiagnostics } from '../services/diagnostics'; import { onActiveFileChange } from '../services/events'; import type { LJDiagnostic } from "../types/diagnostics"; @@ -32,16 +31,8 @@ export async function runClient(context: vscode.ExtensionContext, port: number) documentSelector: [{ language: "java" }], }; extension.client = new LanguageClient("liquidJavaServer", "LiquidJava Server", serverOptions, clientOptions); - extension.client.onDidChangeState((e) => { - if (e.newState === State.Stopped) { - stopClient("Client stopped"); - } - }); - context.subscriptions.push(extension.client); // client teardown - context.subscriptions.push({ - dispose: () => stopClient("Client was disposed"), // server teardown - }); + context.subscriptions.push(extension.client); // disposed on deactivation try { await extension.client.start(); @@ -100,8 +91,4 @@ export async function stopClient(reason: string) { } finally { extension.socket = undefined; } - - // kill server process - await killProcess(extension.serverProcess); - extension.serverProcess = undefined; } \ No newline at end of file diff --git a/client/src/lsp/server.ts b/client/src/lsp/server.ts index c92ab57..96ed551 100644 --- a/client/src/lsp/server.ts +++ b/client/src/lsp/server.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as child_process from 'child_process'; import * as path from 'path'; -import { getAvailablePort } from '../utils/utils'; +import { getAvailablePort, killProcess } from '../utils/utils'; import { extension } from '../state'; import { DEBUG_MODE, DEBUG_PORT, SERVER_JAR } from '../utils/constants'; @@ -40,7 +40,16 @@ export async function runLanguageServer(context: vscode.ExtensionContext, javaEx }); extension.serverProcess.on("close", (code) => { extension.logger.server.info(`Process exited with code ${code}`); - extension.client?.stop(); + extension.serverProcess = undefined; }); return port; +} + +/** + * Stops the LiquidJava language server + * @returns A promise that resolves when the server is stopped + */ +export async function stopLanguageServer() { + await killProcess(extension.serverProcess); + extension.serverProcess = undefined; } \ No newline at end of file diff --git a/client/src/services/commands.ts b/client/src/services/commands.ts index 675c8e7..fd5df16 100644 --- a/client/src/services/commands.ts +++ b/client/src/services/commands.ts @@ -1,20 +1,54 @@ import * as vscode from "vscode"; +import { startExtension, stopExtension, restartExtension } from "../extension"; +import { verify } from "./diagnostics"; + +const commandIcons: Record = { + "liquidjava.showLogs": "$(output)", + "liquidjava.showView": "$(window)", + "liquidjava.start": "$(play)", + "liquidjava.stop": "$(debug-stop)", + "liquidjava.restart": "$(debug-restart)", + "liquidjava.verify": "$(check)", +} + +const commandHandlers: Record Promise> = { + "liquidjava.start": async (context) => await startExtension(context), + "liquidjava.stop": async () => await stopExtension(), + "liquidjava.restart": async (context) => await restartExtension(context), + "liquidjava.verify": async () => await verify(), +} /** - * Initializes the command palette for the extension + * Registers all commands for the LiquidJava extension * @param context The extension context */ export function registerCommands(context: vscode.ExtensionContext) { + const packageJson = context.extension.packageJSON; + const commands = (packageJson.contributes?.commands || []) as vscode.Command[]; + + // register commands + commands.forEach(cmd => { + const handler = commandHandlers[cmd.command]; + if (handler) { + context.subscriptions.push( + vscode.commands.registerCommand(cmd.command, () => handler(context)) + ); + } + }); + + // register command to show all commands context.subscriptions.push( vscode.commands.registerCommand("liquidjava.showCommands", async () => { - const commands = [ - { label: "$(output) Show Logs", command: "liquidjava.showLogs" }, - { label: "$(window) Show View", command: "liquidjava.showView" }, - // TODO: add more commands here, e.g., start, stop, restart, verify, etc. - ]; + const quickPickItems = commands + .filter(cmd => cmd.command !== "liquidjava.showCommands") + .map(cmd => ({ + label: `${commandIcons[cmd.command] || "$(symbol-misc)"} ${cmd.title}`, + command: cmd.command, + })); + const placeHolder = "Select a LiquidJava Command"; - const selected = await vscode.window.showQuickPick(commands, { placeHolder }); + const selected = await vscode.window.showQuickPick(quickPickItems, { placeHolder }); if (selected) vscode.commands.executeCommand(selected.command); }) - ); + ); } \ No newline at end of file diff --git a/client/src/services/diagnostics.ts b/client/src/services/diagnostics.ts new file mode 100644 index 0000000..e8a406d --- /dev/null +++ b/client/src/services/diagnostics.ts @@ -0,0 +1,38 @@ +import * as vscode from "vscode"; +import { extension } from "../state"; +import { LJDiagnostic } from "../types/diagnostics"; +import { StatusBarState, updateStatusBar } from "./status-bar"; + +/** + * Handles LiquidJava diagnostics received from the language server + * @param diagnostics The array of diagnostics received + */ +export function handleLJDiagnostics(diagnostics: LJDiagnostic[]) { + const containsError = diagnostics.some(d => d.category === "error"); + const statusBarState: StatusBarState = containsError ? "failed" : "passed"; + updateStatusBar(statusBarState); + extension.webview?.sendMessage({ type: "diagnostics", diagnostics }); + extension.diagnostics = diagnostics; +} + +/** + * Triggers the LiquidJava verification manually + */ +export async function verify() { + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.languageId !== "java") { + vscode.window.showWarningMessage("LiquidJava: No Java file is currently open"); + return; + } + + if (!extension.client) { + vscode.window.showWarningMessage("LiquidJava: Extension is not running. Use 'LiquidJava: Start' first."); + return; + } + + const uri = editor.document.uri.toString(); + extension.logger?.client.info("Verify command — checking diagnostics"); + updateStatusBar("loading"); + + extension.client.sendNotification("liquidjava/verify", { uri }); +} \ No newline at end of file diff --git a/client/src/services/events.ts b/client/src/services/events.ts index 5dbc715..ec1dc71 100644 --- a/client/src/services/events.ts +++ b/client/src/services/events.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { extension } from '../state'; -import { updateStateMachine } from './webview'; +import { updateStateMachine } from './state-machine'; /** * Initializes file system event listeners diff --git a/client/src/services/state-machine.ts b/client/src/services/state-machine.ts new file mode 100644 index 0000000..cfa7c26 --- /dev/null +++ b/client/src/services/state-machine.ts @@ -0,0 +1,13 @@ +import * as vscode from "vscode"; +import { extension } from "../state"; +import { StateMachine } from "../types/fsm"; + +/** + * Requests the state machine for the given document from the language server + * @param document The text document + */ +export async function updateStateMachine(document: vscode.TextDocument) { + const sm: StateMachine = await extension.client?.sendRequest("liquidjava/fsm", { uri: document.uri.toString() }); + extension.webview?.sendMessage({ type: "fsm", sm }); + extension.stateMachine = sm; +} diff --git a/client/src/services/status-bar.ts b/client/src/services/status-bar.ts index 0bf5cc3..dbcd1c1 100644 --- a/client/src/services/status-bar.ts +++ b/client/src/services/status-bar.ts @@ -3,13 +3,26 @@ import { extension } from "../state"; export type StatusBarState = "loading" | "stopped" | "passed" | "failed"; + const icons = { + loading: "$(sync~spin)", + stopped: "$(circle-slash)", + passed: "$(check)", + failed: "$(x)", +}; + +const statusText = { + loading: "Loading", + stopped: "Stopped", + passed: "Verification passed", + failed: "Verification failed", +}; + /** * Initializes the status bar for the extension * @param context The extension context */ export function registerStatusBar(context: vscode.ExtensionContext) { extension.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - extension.statusBar.tooltip = "LiquidJava Commands"; extension.statusBar.command = "liquidjava.showCommands"; updateStatusBar("loading"); extension.statusBar.show(); @@ -21,13 +34,8 @@ export function registerStatusBar(context: vscode.ExtensionContext) { * @param state The current state ("loading", "stopped", "passed", "failed") */ export function updateStatusBar(state: StatusBarState) { - const icons = { - loading: "$(sync~spin)", - stopped: "$(circle-slash)", - passed: "$(check)", - failed: "$(x)", - }; const color = state === "stopped" ? "errorForeground" : "statusBar.foreground"; extension.statusBar.color = new vscode.ThemeColor(color); extension.statusBar.text = icons[state] + " LiquidJava"; + extension.statusBar.tooltip = statusText[state]; } \ No newline at end of file diff --git a/client/src/services/webview.ts b/client/src/services/webview.ts index 633fee1..6391677 100644 --- a/client/src/services/webview.ts +++ b/client/src/services/webview.ts @@ -1,9 +1,6 @@ import * as vscode from "vscode"; import { LiquidJavaWebviewProvider } from "../webview/provider"; import { extension } from "../state"; -import { StatusBarState, updateStatusBar } from "./status-bar"; -import type { StateMachine } from "../types/fsm"; -import type { LJDiagnostic } from "../types/diagnostics"; /** * Initializes the webview panel for the extension @@ -34,26 +31,3 @@ export function registerWebview(context: vscode.ExtensionContext) { }) ); } - - -/** - * Handles LiquidJava diagnostics received from the language server - * @param diagnostics The array of diagnostics received - */ -export function handleLJDiagnostics(diagnostics: LJDiagnostic[]) { - const containsError = diagnostics.some(d => d.category === "error"); - const statusBarState: StatusBarState = containsError ? "failed" : "passed"; - updateStatusBar(statusBarState); - extension.webview?.sendMessage({ type: "diagnostics", diagnostics }); - extension.diagnostics = diagnostics; -} - -/** - * Requests the state machine for the given document from the language server - * @param document The text document - */ -export async function updateStateMachine(document: vscode.TextDocument) { - const sm: StateMachine = await extension.client?.sendRequest("liquidjava/fsm", { uri: document.uri.toString() }); - extension.webview?.sendMessage({ type: "fsm", sm }); - extension.stateMachine = sm; -} diff --git a/server/src/main/java/LJLanguageServer.java b/server/src/main/java/LJLanguageServer.java index 2a15fb1..5ca7ebf 100644 --- a/server/src/main/java/LJLanguageServer.java +++ b/server/src/main/java/LJLanguageServer.java @@ -88,4 +88,9 @@ public CompletableFuture fsm(Uri uri) { return StateMachineParser.parse(uri.uri()); }); } + + @JsonNotification("liquidjava/verify") + public void verify(Uri uri) { + diagnosticsService.generateDiagnostics(uri.uri()); + } }