diff --git a/client/src/lsp/client.ts b/client/src/lsp/client.ts index dbefecf4..99aa0c9f 100644 --- a/client/src/lsp/client.ts +++ b/client/src/lsp/client.ts @@ -3,7 +3,7 @@ import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-lan import { connectToPort } from '../utils/utils'; import { extension } from '../state'; import { updateStatusBar } from '../services/status-bar'; -import { handleLJDiagnostics } from '../services/diagnostics'; +import { handleLJDiagnostics, handleLJFailure } from '../services/diagnostics'; import { onActiveFileChange } from '../services/events'; import type { LJDiagnostic } from "../types/diagnostics"; import { LJContext } from '../types/context'; @@ -42,6 +42,10 @@ export async function runClient(context: vscode.ExtensionContext, port: number) handleLJDiagnostics(diagnostics); }); + extension.client.onNotification("liquidjava/failure", () => { + handleLJFailure(); + }); + extension.client.onNotification("liquidjava/context", (context: LJContext) => { handleContext(context); }); diff --git a/client/src/services/diagnostics.ts b/client/src/services/diagnostics.ts index 0f79831b..57422227 100644 --- a/client/src/services/diagnostics.ts +++ b/client/src/services/diagnostics.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; -import { extension } from "../state"; +import { extension, ExtensionStatus } from "../state"; import { LJDiagnostic } from "../types/diagnostics"; -import { StatusBarState, updateStatusBar } from "./status-bar"; +import { updateStatusBar } from "./status-bar"; import { updateErrorAtCursor } from "./context"; import { refreshCodeLenses } from "./codelens"; @@ -11,7 +11,7 @@ import { refreshCodeLenses } from "./codelens"; */ export function handleLJDiagnostics(diagnostics: LJDiagnostic[]) { const containsError = diagnostics.some(d => d.category === "error"); - const statusBarState: StatusBarState = containsError ? "failed" : "passed"; + const statusBarState: ExtensionStatus = containsError ? "failed" : "passed"; updateStatusBar(statusBarState); extension.diagnostics = diagnostics; refreshCodeLenses(); @@ -21,6 +21,19 @@ export function handleLJDiagnostics(diagnostics: LJDiagnostic[]) { extension.webview?.sendMessage({ type: "context", context: extension.context, errorAtCursor: extension.errorAtCursor }); } +/** + * Handles LiquidJava verifier crashes received from the language server + */ +export function handleLJFailure() { + extension.diagnostics = []; + extension.errorAtCursor = undefined; + refreshCodeLenses(); + extension.webview?.sendMessage({ type: "diagnostics", diagnostics: [] }); + if (extension.context) + extension.webview?.sendMessage({ type: "context", context: extension.context, errorAtCursor: extension.errorAtCursor }); + updateStatusBar("crashed"); +} + /** * Triggers the LiquidJava verification manually */ diff --git a/client/src/services/status-bar.ts b/client/src/services/status-bar.ts index ae4eaf7a..5b19d51d 100644 --- a/client/src/services/status-bar.ts +++ b/client/src/services/status-bar.ts @@ -1,13 +1,12 @@ import * as vscode from "vscode"; import { ExtensionStatus, extension } from "../state"; -export type StatusBarState = ExtensionStatus; - - const icons = { +const icons = { loading: "$(sync~spin)", stopped: "$(circle-slash)", passed: "$(check)", failed: "$(x)", + crashed: "$(x)", }; const statusText = { @@ -15,6 +14,7 @@ const statusText = { stopped: "Stopped", passed: "Verification passed", failed: "Verification failed", + crashed: "Crashed", }; /** @@ -31,15 +31,15 @@ export function registerStatusBar(context: vscode.ExtensionContext) { /** * Updates the status bar with the current state - * @param status The current status ("loading", "stopped", "passed", "failed") + * @param status The current status ("loading", "stopped", "passed", "failed", "crashed") * @param notifyWebview Whether the webview should reflect this status update. */ -export function updateStatusBar(status: StatusBarState, notifyWebview = status !== "loading") { +export function updateStatusBar(status: ExtensionStatus, notifyWebview = status !== "loading") { if (notifyWebview) { extension.status = status; extension.webview?.sendMessage({ type: "status", status }); } - const color = status === "stopped" ? "errorForeground" : "statusBar.foreground"; + const color = status === "stopped" || status === "crashed" ? "errorForeground" : "statusBar.foreground"; if (!extension.statusBar) return; extension.statusBar.color = new vscode.ThemeColor(color); extension.statusBar.text = icons[status] + " LiquidJava"; diff --git a/client/src/services/webview.ts b/client/src/services/webview.ts index b83b9ed3..0300adf4 100644 --- a/client/src/services/webview.ts +++ b/client/src/services/webview.ts @@ -25,11 +25,11 @@ export function registerWebview(context: vscode.ExtensionContext) { context.subscriptions.push( extension.webview.onDidReceiveMessage(message => { if (message.type === "ready") { - if (extension.status) extension.webview?.sendMessage({ type: "status", status: extension.status }); if (extension.file) extension.webview?.sendMessage({ type: "file", file: extension.file }); if (extension.diagnostics) extension.webview?.sendMessage({ type: "diagnostics", diagnostics: extension.diagnostics }); if (extension.context) extension.webview?.sendMessage({ type: "context", context: extension.context , errorAtCursor: extension.errorAtCursor }); if (extension.stateMachine) extension.webview?.sendMessage({ type: "fsm", sm: extension.stateMachine }); + if (extension.status) extension.webview?.sendMessage({ type: "status", status: extension.status }); } }) ); diff --git a/client/src/state.ts b/client/src/state.ts index 9c9ffc4f..f47d5103 100644 --- a/client/src/state.ts +++ b/client/src/state.ts @@ -8,7 +8,7 @@ import type { LJDiagnostic, RefinementMismatchError } from "./types/diagnostics" import type { LJStateMachine } from "./types/fsm"; import { LJContext, Range } from "./types/context"; -export type ExtensionStatus = "loading" | "stopped" | "passed" | "failed"; +export type ExtensionStatus = "loading" | "stopped" | "passed" | "failed" | "crashed"; export class ExtensionState { // server/client state diff --git a/client/src/webview/script.ts b/client/src/webview/script.ts index d03d1742..ed4e7a8e 100644 --- a/client/src/webview/script.ts +++ b/client/src/webview/script.ts @@ -304,9 +304,9 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window) * Updates the webview content based on the current state */ function updateView() { - if (status === 'stopped') { + if (status === 'stopped' || status === 'crashed') { currentDiagram = ''; - root.innerHTML = renderStopped(); + root.innerHTML = renderStopped(status); return; } if (status === 'loading') { diff --git a/client/src/webview/styles.ts b/client/src/webview/styles.ts index c1148ceb..bbaead14 100644 --- a/client/src/webview/styles.ts +++ b/client/src/webview/styles.ts @@ -437,6 +437,7 @@ export function getStyles(): string { } .stopped-view { display: flex; + position: relative; min-height: calc(100vh - 2rem); flex-direction: column; align-items: center; diff --git a/client/src/webview/views/stopped.ts b/client/src/webview/views/stopped.ts index 7f4227a1..7f228aa8 100644 --- a/client/src/webview/views/stopped.ts +++ b/client/src/webview/views/stopped.ts @@ -1,9 +1,23 @@ -export function renderStopped(): string { +type StoppedViewStatus = "stopped" | "crashed"; + +const stoppedViewContent: Record = { + stopped: { + title: "LiquidJava Not Running", + message: /*html*/`To use LiquidJava, run LiquidJava: Start from the command palette.`, + }, + crashed: { + title: "LiquidJava Crashed", + message: /*html*/`LiquidJava could not verify this project.
Check for Java compilation errors.

To inspect the problem, run LiquidJava: Show Logs from the command palette.`, + }, +}; + +export function renderStopped(status: StoppedViewStatus): string { + const { title, message } = stoppedViewContent[status]; return /*html*/`
-

LiquidJava Not Running

-

To use LiquidJava, run LiquidJava: Start from the command palette

+

${title}

+

${message}

`; } diff --git a/server/src/main/java/LJDiagnosticsHandler.java b/server/src/main/java/LJDiagnosticsHandler.java index ae98ce71..d4617fd1 100644 --- a/server/src/main/java/LJDiagnosticsHandler.java +++ b/server/src/main/java/LJDiagnosticsHandler.java @@ -14,7 +14,6 @@ import liquidjava.api.CommandLineLauncher; import liquidjava.diagnostics.Diagnostics; import liquidjava.diagnostics.LJDiagnostic; -import liquidjava.diagnostics.errors.CustomError; import liquidjava.diagnostics.errors.LJError; import liquidjava.diagnostics.warnings.LJWarning; import spoon.reflect.cu.SourcePosition; @@ -29,28 +28,23 @@ public class LJDiagnosticsHandler { * @param path the file path * @return LJDiagnostics */ - public static LJDiagnostics getLJDiagnostics(String path) { + public static LJDiagnostics getLJDiagnostics(String path) throws Exception { List errors = new ArrayList<>(); List warnings = new ArrayList<>(); - try { - CommandLineLauncher.cmdArgs.lspMode = true; - CommandLineLauncher.launch(path); - Diagnostics diagnostics = Diagnostics.getInstance(); - if (diagnostics.foundWarning()) { - warnings.addAll(diagnostics.getWarnings()); - } - if (diagnostics.foundError()) { - System.out.println("Failed verification"); - errors.addAll(diagnostics.getErrors()); - } else { - System.out.println("Passed verification"); - } - return new LJDiagnostics(errors, warnings); - } catch (Exception e) { - e.printStackTrace(); - errors.add(new CustomError("LiquidJava verification failed, check for Java errors")); - return new LJDiagnostics(errors, warnings); + + CommandLineLauncher.cmdArgs.lspMode = true; + CommandLineLauncher.launch(path); + Diagnostics diagnostics = Diagnostics.getInstance(); + if (diagnostics.foundWarning()) { + warnings.addAll(diagnostics.getWarnings()); + } + if (diagnostics.foundError()) { + System.out.println("Failed verification"); + errors.addAll(diagnostics.getErrors()); + } else { + System.out.println("Passed verification"); } + return new LJDiagnostics(errors, warnings); } /** diff --git a/server/src/main/java/LJDiagnosticsService.java b/server/src/main/java/LJDiagnosticsService.java index a76483b6..290be084 100644 --- a/server/src/main/java/LJDiagnosticsService.java +++ b/server/src/main/java/LJDiagnosticsService.java @@ -1,6 +1,8 @@ import java.io.File; import java.net.URI; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -27,6 +29,7 @@ public class LJDiagnosticsService implements TextDocumentService, WorkspaceServi private LJLanguageClient client; private String workspaceRoot; + private final Set publishedDiagnosticUris = new HashSet<>(); private final ExecutorService diagnosticsExecutor = Executors.newSingleThreadExecutor(r -> { Thread thread = new Thread(r, "liquidjava-diagnostics"); thread.setDaemon(true); @@ -61,14 +64,26 @@ public void sendDiagnosticsNotification(List diagnostics) { */ public void generateDiagnostics(String uri) { String path = PathUtils.extractBasePath(uri); - LJDiagnostics ljDiagnostics = LJDiagnosticsHandler.getLJDiagnostics(path); - List nativeDiagnostics = LJDiagnosticsHandler.getNativeDiagnostics(ljDiagnostics, uri); - nativeDiagnostics.forEach(params -> { - this.client.publishDiagnostics(params); - }); - List diagnostics = Stream.concat(ljDiagnostics.errors().stream(), ljDiagnostics.warnings().stream()).collect(Collectors.toList()); - sendDiagnosticsNotification(diagnostics); - this.client.sendContext(ContextHistoryConverter.convertToDTO(ContextHistory.getInstance())); + clearPublishedDiagnostics(uri); + + try { + LJDiagnostics ljDiagnostics = LJDiagnosticsHandler.getLJDiagnostics(path); + List nativeDiagnostics = LJDiagnosticsHandler.getNativeDiagnostics(ljDiagnostics, uri); + nativeDiagnostics.forEach(params -> { + this.client.publishDiagnostics(params); + if (!params.getDiagnostics().isEmpty()) { + publishedDiagnosticUris.add(params.getUri()); + } + }); + List diagnostics = Stream.concat(ljDiagnostics.errors().stream(), ljDiagnostics.warnings().stream()).collect(Collectors.toList()); + sendDiagnosticsNotification(diagnostics); + this.client.sendContext(ContextHistoryConverter.convertToDTO(ContextHistory.getInstance())); + } catch (Exception e) { + System.err.println("LiquidJava verification crashed while checking: " + path); + e.printStackTrace(System.err); + clearPublishedDiagnostics(uri); + this.client.sendFailure(); + } } /** @@ -93,10 +108,18 @@ public void shutdown() { */ public void clearDiagnostic(String uri) { this.client.publishDiagnostics(LJDiagnosticsHandler.getEmptyDiagnostics(uri)); + publishedDiagnosticUris.remove(uri); // TODO: fix consistency between native and custom diagnostics // sendDiagnosticsNotification(List.of()); } + private void clearPublishedDiagnostics(String uri) { + Set urisToClear = new HashSet<>(publishedDiagnosticUris); + urisToClear.add(uri); + urisToClear.forEach(clearUri -> this.client.publishDiagnostics(LJDiagnosticsHandler.getEmptyDiagnostics(clearUri))); + publishedDiagnosticUris.clear(); + } + /** * Checks diagnostics when a document is opened * @param params diff --git a/server/src/main/java/LJLanguageClient.java b/server/src/main/java/LJLanguageClient.java index 3a19d4c2..fe194f3e 100644 --- a/server/src/main/java/LJLanguageClient.java +++ b/server/src/main/java/LJLanguageClient.java @@ -18,6 +18,12 @@ public interface LJLanguageClient extends LanguageClient { @JsonNotification("liquidjava/diagnostics") void sendDiagnostics(List diagnostics); + /** + * Sends a verifier failure notification to the client + */ + @JsonNotification("liquidjava/failure") + void sendFailure(); + /** * Sends the context history to the client * @param contextHistory the context history to send