Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down
Binary file modified client/server/language-server-liquidjava.jar
Binary file not shown.
59 changes: 53 additions & 6 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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;
}
Expand All @@ -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);
}

19 changes: 3 additions & 16 deletions client/src/lsp/client.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -100,8 +91,4 @@ export async function stopClient(reason: string) {
} finally {
extension.socket = undefined;
}

// kill server process
await killProcess(extension.serverProcess);
extension.serverProcess = undefined;
}
13 changes: 11 additions & 2 deletions client/src/lsp/server.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
}
50 changes: 42 additions & 8 deletions client/src/services/commands.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,54 @@
import * as vscode from "vscode";
import { startExtension, stopExtension, restartExtension } from "../extension";
import { verify } from "./diagnostics";

const commandIcons: Record<string, string> = {
"liquidjava.showLogs": "$(output)",
"liquidjava.showView": "$(window)",
"liquidjava.start": "$(play)",
"liquidjava.stop": "$(debug-stop)",
"liquidjava.restart": "$(debug-restart)",
"liquidjava.verify": "$(check)",
}

const commandHandlers: Record<string, (context: vscode.ExtensionContext) => Promise<void>> = {
"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);
})
);
);
}
38 changes: 38 additions & 0 deletions client/src/services/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
2 changes: 1 addition & 1 deletion client/src/services/events.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
13 changes: 13 additions & 0 deletions client/src/services/state-machine.ts
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 15 additions & 7 deletions client/src/services/status-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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];
}
26 changes: 0 additions & 26 deletions client/src/services/webview.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
}
5 changes: 5 additions & 0 deletions server/src/main/java/LJLanguageServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,9 @@ public CompletableFuture<StateMachine> fsm(Uri uri) {
return StateMachineParser.parse(uri.uri());
});
}

@JsonNotification("liquidjava/verify")
public void verify(Uri uri) {
diagnosticsService.generateDiagnostics(uri.uri());
}
}