diff --git a/client/src/services/events.ts b/client/src/services/events.ts index 3596a10..e13a1e6 100644 --- a/client/src/services/events.ts +++ b/client/src/services/events.ts @@ -36,6 +36,7 @@ export function registerEvents(context: vscode.ExtensionContext) { */ export async function onActiveFileChange(editor: vscode.TextEditor) { extension.file = normalizeFilePath(editor.document.uri.fsPath); + extension.stateMachine = undefined; extension.webview?.sendMessage({ type: "file", file: extension.file }); await updateStateMachine(editor.document); handleContextUpdate(editor.selection); @@ -70,4 +71,4 @@ function handleContextUpdate(selection: vscode.Selection) { extension.context.allVars = allVars; updateErrorAtCursor(); extension.webview?.sendMessage({ type: "context", context: extension.context, errorAtCursor: extension.errorAtCursor }); -} \ No newline at end of file +} diff --git a/client/src/services/state-machine.ts b/client/src/services/state-machine.ts index 9969f2e..6532025 100644 --- a/client/src/services/state-machine.ts +++ b/client/src/services/state-machine.ts @@ -1,16 +1,17 @@ import * as vscode from "vscode"; import { extension } from "../state"; import { LJStateMachine } from "../types/fsm"; +import { normalizeFilePath } from "../utils/utils"; /** * 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 = await extension.client?.sendRequest("liquidjava/fsm", { uri: document.uri.toString() }); + const file = normalizeFilePath(document.uri.fsPath); + const sm = await extension.client?.sendRequest("liquidjava/fsm", { uri: document.uri.toString() }); + if (file !== extension.file) return; - // dont update diagram if it hasnt changed to a new one - if (!sm || JSON.stringify(sm) === JSON.stringify(extension.stateMachine)) return; extension.stateMachine = sm; extension.webview?.sendMessage({ type: "fsm", sm }); } diff --git a/client/src/services/webview.ts b/client/src/services/webview.ts index 787f656..23a5740 100644 --- a/client/src/services/webview.ts +++ b/client/src/services/webview.ts @@ -36,7 +36,7 @@ export function registerWebview(context: vscode.ExtensionContext) { 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.stateMachine !== undefined) extension.webview?.sendMessage({ type: "fsm", sm: extension.stateMachine }); if (extension.status) extension.webview?.sendMessage({ type: "status", status: extension.status }); if (pendingDiagnosticReveal) { extension.webview?.sendMessage({ type: "revealDiagnostic", diagnostic: pendingDiagnosticReveal }); diff --git a/client/src/state.ts b/client/src/state.ts index f47d510..2d8cdb0 100644 --- a/client/src/state.ts +++ b/client/src/state.ts @@ -25,7 +25,7 @@ export class ExtensionState { // application state file?: string; diagnostics?: LJDiagnostic[]; - stateMachine?: LJStateMachine; + stateMachine?: LJStateMachine | null; context?: LJContext; currentSelection?: Range; errorAtCursor?: RefinementMismatchError; diff --git a/client/src/types/diagnostics.ts b/client/src/types/diagnostics.ts index 1ed3f32..8b38924 100644 --- a/client/src/types/diagnostics.ts +++ b/client/src/types/diagnostics.ts @@ -1,4 +1,5 @@ import type { Range } from './context'; +import type { LJStateMachine } from './fsm'; import type { VCSimplificationResult } from './vc-implications'; // Type definitions used for LiquidJava diagnostics @@ -62,6 +63,7 @@ export type RefinementError = BaseDiagnostic & { found: VCSimplificationResult; customMessage: string; counterexample: string; + declarationPosition: SourcePosition | null; } export type StateConflictError = BaseDiagnostic & { @@ -78,6 +80,8 @@ export type StateRefinementError = BaseDiagnostic & { expected: string; found: VCSimplificationResult; customMessage: string; + declarationPosition: SourcePosition | null; + stateMachine: LJStateMachine | null; } export type ArgumentMismatchError = BaseDiagnostic & { diff --git a/client/src/webview/script.ts b/client/src/webview/script.ts index afc428c..bcc23a7 100644 --- a/client/src/webview/script.ts +++ b/client/src/webview/script.ts @@ -30,7 +30,8 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window) let diagnostics: LJDiagnostic[] | undefined; let showAllDiagnostics = false; let currentFile: string; - let stateMachine: LJStateMachine; + let stateMachine: LJStateMachine | undefined; + let diagnosticStateMachine: LJStateMachine | undefined; let context: LJContext; let errorAtCursor: RefinementMismatchError; let selectedTab: NavTab = 'diagnostics'; @@ -131,6 +132,24 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window) return; } + const diagnosticStateMachineButton = target.closest?.('.diagnostic-state-machine-btn'); + if (diagnosticStateMachineButton) { + e.preventDefault(); + e.stopPropagation(); + + const errorIndex = parseInt(diagnosticStateMachineButton.getAttribute('data-error-index') || '-1', 10); + const diagnostic = getDisplayDiagnostics(diagnostics ?? [], showAllDiagnostics, currentFile) + .filter(d => d.category === 'error')[errorIndex]; + if (diagnostic?.type !== 'state-refinement-error' || !diagnostic.stateMachine) return; + + selectedTab = 'fsm'; + diagnosticStateMachine = diagnostic.stateMachine; + showDiagramConditions = false; + currentDiagram = ''; + updateView(); + return; + } + // VC implication simplification step buttons const vcImplicationStepButton = target.closest?.('.vc-step-btn'); if (vcImplicationStepButton) { @@ -237,6 +256,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window) const tab = target.getAttribute('data-tab') as NavTab; if (tab && tab !== selectedTab) { vscode.postMessage({ type: 'highlight', range: null }); + if (tab === 'fsm') diagnosticStateMachine = undefined; selectedTab = tab; updateView(); } @@ -309,11 +329,15 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window) case 'file': currentFile = msg.file; if (diagnostics && !showAllDiagnostics && selectedTab === 'diagnostics') updateView(); + diagnosticStateMachine = undefined; + stateMachine = undefined; + currentDiagram = ''; + if (selectedTab === 'fsm') updateView(); break; case 'fsm': - stateMachine = msg.sm as LJStateMachine; + stateMachine = (msg.sm as LJStateMachine | null) ?? undefined; showDiagramConditions = false; - if (selectedTab === 'fsm') updateView(); + if (selectedTab === 'fsm' && !diagnosticStateMachine) updateView(); break; case 'context': context = msg.context as LJContext; @@ -348,10 +372,11 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window) : renderLoading(); break; case 'fsm': { - const diagram = createMermaidDiagram(stateMachine, diagramOrientation, showDiagramConditions); + const displayedStateMachine = diagnosticStateMachine ?? stateMachine; + const diagram = createMermaidDiagram(displayedStateMachine, diagramOrientation, showDiagramConditions); currentDiagram = diagram; - renderStateMachineView(root, stateMachine, diagram, diagramOrientation, showDiagramConditions); - if (stateMachine) renderMermaidDiagram(document, window); + renderStateMachineView(root, displayedStateMachine, diagram, diagramOrientation, showDiagramConditions); + if (displayedStateMachine) renderMermaidDiagram(document, window); break; } case 'context': diff --git a/client/src/webview/styles.ts b/client/src/webview/styles.ts index 769ab1f..da36826 100644 --- a/client/src/webview/styles.ts +++ b/client/src/webview/styles.ts @@ -176,10 +176,16 @@ export function getStyles(): string { line-height: 1; pointer-events: none; } - .copy-diagnostic-btn, - .diagnostic-context-btn { + .diagnostic-actions { position: absolute; top: 0.5rem; + right: 0.5rem; + display: flex; + gap: 0.25rem; + } + .copy-diagnostic-btn, + .diagnostic-context-btn, + .diagnostic-state-machine-btn { display: inline-flex; align-items: center; justify-content: center; @@ -193,20 +199,16 @@ export function getStyles(): string { opacity: 0.65; transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease, transform 0.16s ease; } - .copy-diagnostic-btn { - right: 0.5rem; - } - .diagnostic-context-btn { - right: 2.5rem; - } .copy-diagnostic-btn:hover, - .diagnostic-context-btn:hover { + .diagnostic-context-btn:hover, + .diagnostic-state-machine-btn:hover { background: var(--vscode-editor-background); border-color: var(--vscode-widget-border); opacity: 1; } .copy-diagnostic-btn:disabled, - .diagnostic-context-btn:disabled { + .diagnostic-context-btn:disabled, + .diagnostic-state-machine-btn:disabled { opacity: 0.8; cursor: default; } diff --git a/client/src/webview/views/diagnostics/diagnostics.ts b/client/src/webview/views/diagnostics/diagnostics.ts index 5035baf..65a637c 100644 --- a/client/src/webview/views/diagnostics/diagnostics.ts +++ b/client/src/webview/views/diagnostics/diagnostics.ts @@ -71,7 +71,7 @@ export async function copyDiagnosticToClipboard(button: any, displayDiagnostics: } export function formatDiagnosticForClipboard(diagnostic: LJDiagnostic): string { - const skippedFields = new Set(['category', 'type', 'translationTable', 'position', 'file']); + const skippedFields = new Set(['category', 'type', 'translationTable', 'position', 'file', 'declarationPosition', 'stateMachine']); const lines: string[] = []; Object.entries(diagnostic).forEach(([key, value]) => { diff --git a/client/src/webview/views/diagnostics/errors.ts b/client/src/webview/views/diagnostics/errors.ts index ac48c74..e3acde6 100644 --- a/client/src/webview/views/diagnostics/errors.ts +++ b/client/src/webview/views/diagnostics/errors.ts @@ -1,4 +1,4 @@ -import { renderDiagnosticDataAttributes, renderExpressionSection, renderDiagnosticHeader, renderCustomSection, renderLocation, renderDiagnosticContextButton } from "../sections"; +import { renderDiagnosticDataAttributes, renderExpressionSection, renderDiagnosticHeader, renderCustomSection, renderLocation, renderDiagnosticContextButton, renderDiagnosticStateMachineButton } from "../sections"; import { renderCounterexample } from "./counterexample"; import { renderVCImplication } from "./vc-implications"; import type { @@ -21,8 +21,11 @@ export function renderErrors(errors: LJError[]): string { ${errors.map((error, index) => { return /*html*/`
  • - ${renderDiagnosticContextAction(error)} - ${renderCopyDiagnosticButton('error', index)} +
    + ${renderDiagnosticStateMachineAction(error, index)} + ${renderDiagnosticContextAction(error)} + ${renderCopyDiagnosticButton('error', index)} +
    ${renderError(error)}
  • `; @@ -74,3 +77,8 @@ function renderDiagnosticContextAction(error: LJError): string { if (error.type !== 'refinement-error' && error.type !== 'state-refinement-error') return ""; return renderDiagnosticContextButton(error.position); } + +function renderDiagnosticStateMachineAction(error: LJError, index: number): string { + if (error.type !== 'state-refinement-error' || !error.stateMachine) return ""; + return renderDiagnosticStateMachineButton(index); +} diff --git a/client/src/webview/views/diagnostics/warnings.ts b/client/src/webview/views/diagnostics/warnings.ts index a5dbc86..b9a7caa 100644 --- a/client/src/webview/views/diagnostics/warnings.ts +++ b/client/src/webview/views/diagnostics/warnings.ts @@ -7,7 +7,9 @@ export function renderWarnings(warnings: LJWarning[]): string {