Skip to content

Commit 7e17de0

Browse files
Show one state machine diagram per state set
The State Machine view stacks the diagram of each state set of a class, such as the tiling and compression states of ImageWriteParam, under the class name, each labelled with its states. A class with one state set looks as before. A state error is drawn on the diagrams of the state sets it involves, which come first, and focusing on it dims the other diagrams. The summary lists the rejected states of every state set and the allowed states each one needs. Copy Mermaid Source copies every diagram. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 636a489 commit 7e17de0

20 files changed

Lines changed: 264 additions & 91 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
The **State Machine** tab shows object states and method transitions declared with `@StateSet` and `@StateRefinement`.
1+
The **State Machine** tab shows object states and method transitions declared with `@StateSet` and `@StateRefinement`. A class with several `@StateSet` annotations has one diagram per state set, one below the other.
22
Pan the diagram; use **Zoom In**, **Zoom Out**, **Reset Zoom**, and **Toggle Orientation** to inspect it.
33
Use **Expand Conditions** or **Collapse Conditions** to show or hide preconditions and postconditions. **Copy Mermaid Source** copies the diagram definition.
4-
From a state error, choose **Show in state machine** to see the related states and call. The tab opens on the first state error of the file, keeps the one you choose, and follows the error at the cursor; step through several with the arrows, and use **View error** to go back to it.
4+
From a state error, choose **Show in state machine** to see the related states and call, on the diagrams of the state sets the call involves. The tab opens on the first state error of the file, keeps the one you choose, and follows the error at the cursor; step through several with the arrows, and use **View error** to go back to it.

‎client/src/services/codelens.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function createDiagnosticCodeLenses(diagnostic: LJDiagnostic, file: string): vsc
4040
command: "liquidjava.showView",
4141
arguments: [targetDiagnostic]
4242
})];
43-
if (diagnostic.type === 'state-refinement-error' && diagnostic.stateMachine) {
43+
if (diagnostic.type === 'state-refinement-error' && diagnostic.stateMachines.length > 0) {
4444
codeLenses.push(new vscode.CodeLens(range, {
4545
title: "Show in state machine",
4646
command: "liquidjava.showView",

‎client/src/services/events.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function registerEvents(context: vscode.ExtensionContext) {
3636
*/
3737
export async function onActiveFileChange(editor: vscode.TextEditor) {
3838
extension.file = normalizeFilePath(editor.document.uri.fsPath);
39-
extension.stateMachine = undefined;
39+
extension.stateMachines = undefined;
4040
extension.webview?.sendMessage({ type: "file", file: extension.file });
4141
await updateStateMachine(editor.document);
4242
handleContextUpdate(editor.selection);

‎client/src/services/state-machine.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { LJStateMachine } from "../types/fsm";
44
import { normalizeFilePath } from "../utils/utils";
55

66
/**
7-
* Requests the state machine for the given document from the language server
7+
* Requests the state machines for the given document from the language server, one per state set
88
* @param document The text document
99
*/
1010
export async function updateStateMachine(document: vscode.TextDocument) {
1111
const file = normalizeFilePath(document.uri.fsPath);
12-
const sm = await extension.client?.sendRequest<LJStateMachine | null>("liquidjava/fsm", { uri: document.uri.toString() });
12+
const sms = await extension.client?.sendRequest<LJStateMachine[] | null>("liquidjava/fsm", { uri: document.uri.toString() });
1313
if (file !== extension.file) return;
1414

15-
extension.stateMachine = sm;
16-
extension.webview?.sendMessage({ type: "fsm", sm });
15+
extension.stateMachines = sms;
16+
extension.webview?.sendMessage({ type: "fsm", sms });
1717
}

‎client/src/services/webview.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function registerWebview(context: vscode.ExtensionContext) {
3939
if (extension.file) extension.webview?.sendMessage({ type: "file", file: extension.file });
4040
if (extension.diagnostics) extension.webview?.sendMessage({ type: "diagnostics", diagnostics: extension.diagnostics });
4141
if (extension.context) extension.webview?.sendMessage({ type: "context", context: extension.context , errorAtCursor: extension.errorAtCursor });
42-
if (extension.stateMachine !== undefined) extension.webview?.sendMessage({ type: "fsm", sm: extension.stateMachine });
42+
if (extension.stateMachines !== undefined) extension.webview?.sendMessage({ type: "fsm", sms: extension.stateMachines });
4343
if (extension.status) extension.webview?.sendMessage({ type: "status", status: extension.status });
4444
if (pendingDiagnosticReveal) {
4545
extension.webview?.sendMessage({ type: "revealDiagnostic", ...pendingDiagnosticReveal });

‎client/src/state.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class ExtensionState {
2727
diagnostics?: LJDiagnostic[];
2828
readonly diagnosticsEmitter = new vscode.EventEmitter<LJDiagnostic[]>();
2929
readonly failureEmitter = new vscode.EventEmitter<void>();
30-
stateMachine?: LJStateMachine | null;
30+
stateMachines?: LJStateMachine[] | null;
3131
context?: LJContext;
3232
currentSelection?: Range;
3333
errorAtCursor?: RefinementMismatchError;

‎client/src/test/state-errors.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ suite('Bundled LiquidJava state machine errors', () => {
4747
await ready;
4848

4949
const machine = nextWebviewMessage('the refinements file state machine', event =>
50-
event.direction === 'toWebview' && event.message.type === 'fsm' && event.message.sm
51-
? event.message.sm as LJStateMachine : undefined);
50+
event.direction === 'toWebview' && event.message.type === 'fsm' && event.message.sms?.length
51+
? (event.message.sms as LJStateMachine[])[0] : undefined);
5252
const stateError = nextWebviewMessage('the state error diagnostics', event =>
5353
event.direction === 'toWebview' && event.message.type === 'diagnostics'
5454
? (event.message.diagnostics as LJDiagnostic[]).find(isStateError) : undefined);
@@ -58,9 +58,9 @@ suite('Bundled LiquidJava state machine errors', () => {
5858

5959
const [sm, error] = await Promise.all([machine, stateError]);
6060
assert.equal(sm.className, 'javax.swing.tree.DefaultMutableTreeNode');
61-
assert.ok(error.stateMachine, 'the state error must carry its state machine');
62-
assert.equal(error.stateMachine.className, sm.className, 'errors are matched to the machine by class');
63-
assert.ok(error.stateMachine.errorContext, 'the state error must carry its error context');
61+
assert.equal(error.stateMachines.length, 1, 'the state error must carry the state machine of each state set');
62+
assert.equal(error.stateMachines[0].className, sm.className, 'errors are matched to the machine by class');
63+
assert.ok(error.stateMachines[0].errorContext, 'the state error must carry its error context');
6464
assert.ok(error.position && sameFile(error.position.file ?? error.file, callerUri));
6565
assert.ok(error.declarationPosition?.file && sameFile(error.declarationPosition.file, refinementsUri));
6666

‎client/src/types/diagnostics.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ export type StateRefinementError = BaseDiagnostic & {
9090
found: VCSimplificationResult;
9191
customMessage: string;
9292
declarationPosition: SourcePosition | null;
93-
stateMachine: LJStateMachine | null;
93+
// one state machine per state set of the receiver's class
94+
stateMachines: LJStateMachine[];
9495
}
9596

9697
export type ArgumentMismatchError = BaseDiagnostic & {

‎client/src/types/fsm.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
// Type definitions used for representing finite state machines
22

3+
// The state machine of one state set of a class
34
export type LJStateMachine = {
45
className: string;
6+
// index of the state set among the class's @StateSet annotations, in declaration order
7+
stateSet?: number;
58
initialTransitions: { to: string; toCondition?: string | null; constructorSignature?: string | null }[];
69
states: string[];
710
transitions: { from: string; to: string; label: string; fromCondition?: string | null; toCondition?: string | null }[];
8-
errorContext: LJStateMachineErrorContext | null;
11+
// the state error on this state set, null if the call does not involve it
12+
errorContext?: LJStateMachineErrorContext | null;
913
};
1014

1115
// The verifier's states of a rejected call, on the diagram's states

‎client/src/webview/diagram-focus.spec.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { afterEach, describe, expect, it, vi } from 'vitest';
22
import type { LJStateMachine, LJStateMachineErrorContext } from '../types/fsm';
33
import { createMermaidDiagram, renderMermaidDiagram } from './diagram';
4-
import { getRelatedStates, getStateErrorFocus } from './diagram-focus';
4+
import { getRelatedStates, getStateErrorFocus, getStateErrorFocuses, orderStateMachines } from './diagram-focus';
55

66
// DefaultMutableTreeNode from the state error focus issue:
77
// root.add(...); root.removeAllChildren(); root.getFirstChild(); // state error
@@ -250,3 +250,19 @@ describe('rendered state error focus', () => {
250250
expect(dimmed('g.label.lj-dimmed')).toEqual(['edge0', 'edge1', 'edge12']);
251251
});
252252
});
253+
254+
describe('state errors on several state sets', () => {
255+
const tiling: LJStateMachine = { ...treeNode(null), className: 'ImageWriteParam', stateSet: 0 };
256+
const compression: LJStateMachine = { ...treeNode(), className: 'ImageWriteParam', stateSet: 1 };
257+
const other: LJStateMachine = { ...treeNode(null), className: 'ImageWriteParam', stateSet: 2 };
258+
259+
it('puts the state sets with the error first, keeping the declaration order otherwise', () => {
260+
expect(orderStateMachines([tiling, compression, other])).toEqual([compression, tiling, other]);
261+
expect(orderStateMachines([tiling, other])).toEqual([tiling, other]);
262+
});
263+
264+
it('focuses on the error of each state set that has it', () => {
265+
expect(getStateErrorFocuses([tiling, compression, other])).toEqual([getStateErrorFocus(compression)]);
266+
expect(getStateErrorFocuses([tiling, other])).toEqual([]);
267+
});
268+
});

0 commit comments

Comments
 (0)