Skip to content

Commit 7b6a3bd

Browse files
Keep the stepper and View error when a state error cannot be marked, and put State Machine next to Verification (#161)
## Description Based on main after #159. It also carries the tab order from #162, which was merged into the #159 branch after #159 had been merged into main, so it never reached main. **Tab order.** The tabs are now **Verification · State Machine · Context**, since Verification and State Machine link to each other through **Show in state machine** and **View error**. The walkthrough steps and the README follow the same order. Screenshots and tests are in #162. **Errors without marks:** When the verifier cannot tell which states allow a call, the diagram has no marks and the error summary was not shown. With it went the `‹ 1 of 3 ›` stepper and **View error**: the tab showed a badge but no way to step through the errors or go back to them. This happens in two cases: - the allowed states depend on the call's arguments, so the verifier reports no accepted states; - the verifier finds no state machine for the call. In the C-stars exercise `E_OSS_A1_09_PlayfieldWindow`, `setUndecorated` is a `java.awt.Frame` method, but the `@StateSet` is declared on `java.awt.Window`, so the error has no state context. The summary row is now shown for every shown state error. Without marks, it says `counter.open() is rejected here` when the verifier names the call, or `The call at line 49 is rejected` otherwise. The legend and the Show all toggle stay hidden, since nothing is marked or dimmed. ## Screenshots `E_OSS_A1_09_PlayfieldWindow` from the C-stars proposal in building-examples-liquidjava. **Before** (#159): the badge says 1 state error, but nothing in the tab shows it. <img width="560" alt="State Machine tab without an error summary" src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/state-error-without-context/before.png" /> **After:** the summary and View error are shown. <img width="560" alt="State Machine tab with The call at line 49 is rejected and View error" src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/state-error-without-context/after.png" /> **View error** goes back to the error in the Verification tab. <img width="560" alt="Verification tab with the error outlined" src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/state-error-without-context/view-error.png" /> ## Tests Run on this branch, rebased on main (verifier 0.0.36, liquidjava-fsm 0.2.0): - `npm run lint` and `npm run test:typecheck`: clean. - `npm run test:unit`: 94 passed. New tests: - `fsm.spec.ts`: stepper, View error and text without accepted states, without an error context, and without a position; - `script.spec.ts`: stepping through errors without an error context and going back to one, and the tab order. - `npm test` (VS Code): 9 + 1 passing, including the walkthrough test with the new step order. - Server `mvn test`: 27 passing. Local smoke test: passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0730892 commit 7b6a3bd

8 files changed

Lines changed: 88 additions & 14 deletions

File tree

‎client/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ A repository with LiquidJava examples is available at [liquidjava-examples](http
4040

4141
### Getting started in VS Code
4242

43-
The **Get Started with LiquidJava** walkthrough introduces LiquidJava and its interactive tutorial, includes copyable Maven/Gradle annotation dependencies, then explains verification and its status indicator, diagnostics, context, state machines, the Command Palette, and logs. It also links to the [interactive tutorial](https://liquid-java.github.io/liquidjava-interactive-tutorial/). It opens once on installation, following VS Code’s walkthrough preferences. You can dismiss it at any time and reopen it with **LiquidJava: Show Walkthrough** from the Command Palette or **LiquidJava: Show Commands**. VS Code saves its progress across workspaces.
43+
The **Get Started with LiquidJava** walkthrough introduces LiquidJava and its interactive tutorial, includes copyable Maven/Gradle annotation dependencies, then explains verification and its status indicator, diagnostics, state machines, context, the Command Palette, and logs. It also links to the [interactive tutorial](https://liquid-java.github.io/liquidjava-interactive-tutorial/). It opens once on installation, following VS Code’s walkthrough preferences. You can dismiss it at any time and reopen it with **LiquidJava: Show Walkthrough** from the Command Palette or **LiquidJava: Show Commands**. VS Code saves its progress across workspaces.
4444

4545
### What are Liquid Types?
4646

‎client/package.json‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,22 +227,22 @@
227227
]
228228
},
229229
{
230-
"id": "context",
231-
"title": "Inspect context",
232-
"description": "The **Context** tab shows **Variables**, **Aliases**, and **Ghosts** after verification. Expand or collapse each section.\nLocal variables are filtered by the cursor: only declarations before it and in enclosing scopes appear. Select a range to inspect declarations that overlap it; global variables are also included.\nClick a variable to reveal its source. Variables relevant to a nearby error are highlighted alongside the failing refinement.\n[Open LiquidJava view](command:liquidjava.showView)",
230+
"id": "stateMachine",
231+
"title": "Explore state machines",
232+
"description": "The **State Machine** tab shows object states and method transitions declared with ``@StateSet`` and ``@StateRefinement``.\nPan the diagram; use **Zoom In**, **Zoom Out**, **Reset Zoom**, and **Toggle Orientation** to inspect it.\nUse **Expand Conditions** or **Collapse Conditions** to show or hide preconditions and postconditions. **Copy Mermaid Source** copies the diagram definition.\nFrom 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.\n[Open LiquidJava view](command:liquidjava.showView)",
233233
"media": {
234-
"markdown": "media/walkthrough/context.md"
234+
"markdown": "media/walkthrough/state-machine.md"
235235
},
236236
"completionEvents": [
237237
"onStepSelected"
238238
]
239239
},
240240
{
241-
"id": "stateMachine",
242-
"title": "Explore state machines",
243-
"description": "The **State Machine** tab shows object states and method transitions declared with ``@StateSet`` and ``@StateRefinement``.\nPan the diagram; use **Zoom In**, **Zoom Out**, **Reset Zoom**, and **Toggle Orientation** to inspect it.\nUse **Expand Conditions** or **Collapse Conditions** to show or hide preconditions and postconditions. **Copy Mermaid Source** copies the diagram definition.\nFrom 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.\n[Open LiquidJava view](command:liquidjava.showView)",
241+
"id": "context",
242+
"title": "Inspect context",
243+
"description": "The **Context** tab shows **Variables**, **Aliases**, and **Ghosts** after verification. Expand or collapse each section.\nLocal variables are filtered by the cursor: only declarations before it and in enclosing scopes appear. Select a range to inspect declarations that overlap it; global variables are also included.\nClick a variable to reveal its source. Variables relevant to a nearby error are highlighted alongside the failing refinement.\n[Open LiquidJava view](command:liquidjava.showView)",
244244
"media": {
245-
"markdown": "media/walkthrough/state-machine.md"
245+
"markdown": "media/walkthrough/context.md"
246246
},
247247
"completionEvents": [
248248
"onStepSelected"

‎client/src/test/walkthrough.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ suite('LiquidJava walkthrough', () => {
2525
assert.ok(walkthrough);
2626
assert.equal(walkthrough.title, 'Get Started with LiquidJava');
2727
assert.deepEqual(walkthrough.steps.map((step: { id: string }) => step.id),
28-
['tutorial', 'annotations', 'verification', 'commands', 'diagnostics', 'context', 'stateMachine', 'logs']);
28+
['tutorial', 'annotations', 'verification', 'commands', 'diagnostics', 'stateMachine', 'context', 'logs']);
2929
assert.equal(walkthrough.steps[0].title, 'Learn LiquidJava');
3030
const commands = await vscode.commands.getCommands(true);
3131
assert.ok(commands.includes('liquidjava.showWalkthrough'));

‎client/src/webview/script.spec.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ describe('webview script', () => {
127127
receive({ type: 'status', status: diagnostics.some(d => d.category === 'error') ? 'failed' : 'passed' });
128128
}
129129

130+
it('puts the State Machine tab next to Verification, which link to each other', () => {
131+
showDiagnostics([]);
132+
133+
expect(Array.from(root.querySelectorAll('.nav-tab'), tab => tab.getAttribute('data-tab'))).toEqual(['diagnostics', 'fsm', 'context']);
134+
});
135+
130136
it('posts ready and renders the initial loading view', () => {
131137
expect(postMessage.mock.calls).toEqual([[{ type: 'ready' }]]);
132138
expect(root.querySelector('h2')?.textContent).toBe('Verification Pending');
@@ -309,6 +315,23 @@ describe('webview script', () => {
309315
expect(root.querySelectorAll('.diagnostic-state-machine-btn')).toHaveLength(1);
310316
});
311317

318+
it('steps through state errors the verifier could not mark on the diagram', () => {
319+
showDiagnostics([
320+
stateError('First state error', { ...errorMachine('First', 'close'), errorContext: null }, 4),
321+
stateError('Second state error', { ...errorMachine('Second', 'reset'), errorContext: null }, 9),
322+
]);
323+
click('[data-tab="fsm"]');
324+
expect(root.querySelector('.diagram-error-text')?.textContent).toBe('The call at line 5 is rejected');
325+
expect(root.querySelector('.state-error-position')?.textContent).toBe('1 of 2');
326+
327+
click('.state-error-step-btn[data-step="1"]');
328+
expect(root.querySelector('.diagram-title')?.textContent).toBe('Second');
329+
expect(root.querySelector('.diagram-error-text')?.textContent).toBe('The call at line 10 is rejected');
330+
331+
click('#state-error-reveal-btn');
332+
expect(root.querySelector('.nav-tab.selected')?.getAttribute('data-tab')).toBe('diagnostics');
333+
});
334+
312335
it('follows the state error at the cursor without overriding a later choice', () => {
313336
const first = stateError('First state error', errorMachine('First', 'close'), 4);
314337
const second = stateError('Second state error', errorMachine('Second', 'reset'), 9);

‎client/src/webview/script.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
430430
? createMermaidDiagram({ ...displayedStateMachine, errorContext: null }, diagramOrientation, showDiagramConditions)
431431
: diagram;
432432
renderStateMachineView(root, displayedStateMachine, diagram, diagramOrientation, showDiagramConditions, focusOnError,
433-
{ index: shownIndex, count: stateErrors.length });
433+
{ index: shownIndex, count: stateErrors.length, line: getStateErrorLine(shownError) });
434434
renderedStateMachineView = getStateMachineViewKey();
435435
if (displayedStateMachine) renderMermaidDiagram(document, window);
436436
break;
@@ -441,6 +441,12 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
441441
}
442442
}
443443

444+
// the 1-based line of the shown error, null without a position, undefined when no error is shown
445+
function getStateErrorLine(error: StateRefinementError | undefined): number | null | undefined {
446+
if (!error) return undefined;
447+
return error.position ? error.position.lineStart + 1 : null;
448+
}
449+
444450
function getCurrentStateErrors(): StateRefinementError[] {
445451
return getStateErrors(diagnostics, currentFile);
446452
}

‎client/src/webview/views/fsm/fsm.spec.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,29 @@ describe('state machine view', () => {
196196
expect(document.querySelector('#diagram-focus-btn')).toBeNull();
197197
});
198198

199+
it.each([
200+
['without the accepted states', {
201+
calledMethod: 'open', receiver: 'counter', possibleStates: ['Closed'],
202+
acceptedStates: [], validStates: [], invalidStates: [],
203+
}, 'counter.open() is rejected here'],
204+
['without an error context', null, 'The call at line 12 is rejected'],
205+
])('keeps the stepper and View error for a shown error %s', (_, errorContext, text) => {
206+
renderStateMachineView(document.body, machine({ errorContext }), diagram, 'TB', false, true, { index: 1, count: 2, line: 12 });
207+
208+
expect(document.querySelector('.diagram-error-text')?.textContent).toBe(text);
209+
expect(document.querySelector('.state-error-position')?.textContent).toBe('2 of 2');
210+
expect(button('state-error-reveal-btn')).not.toBeNull();
211+
// nothing is marked, so there is nothing to explain or dim
212+
expect(document.querySelector('.diagram-error-legend')).toBeNull();
213+
expect(document.querySelector('#diagram-focus-btn')).toBeNull();
214+
});
215+
216+
it('says the call is rejected without a line when the shown error has no position', () => {
217+
renderStateMachineView(document.body, machine(), diagram, 'TB', false, true, { index: -1, count: 0, line: null });
218+
219+
expect(document.querySelector('.diagram-error-text')?.textContent).toBe('The call is rejected');
220+
});
221+
199222
it('clips the panned diagram below the error summary and legend', () => {
200223
renderStateMachineView(document.body, machine({
201224
errorContext: {

‎client/src/webview/views/fsm/fsm.ts‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { renderMainHeader } from "../sections";
1111
export type StateErrorNavigation = {
1212
index: number;
1313
count: number;
14+
// the 1-based line of the shown error, if any error is shown
15+
line?: number | null;
1416
};
1517

1618
const NO_STATE_ERROR_NAVIGATION: StateErrorNavigation = { index: -1, count: 0 };
@@ -48,7 +50,9 @@ function renderStateMachineViewHtml(sm: LJStateMachine | undefined, diagram: str
4850
${renderCodiconButton("copy", { id: "copy-diagram-btn", className: "diagram-control-btn", title: "Copy Mermaid Source" })}
4951
</div>
5052
</div>
51-
${errorFocus ? renderStateErrorSummary(errorFocus, stateErrors) + renderStateErrorLegend(errorFocus, focusOnError) : ''}
53+
${errorFocus
54+
? renderStateErrorSummary(getStateErrorSummary(errorFocus), stateErrors) + renderStateErrorLegend(errorFocus, focusOnError)
55+
: isStateErrorShown(stateErrors) ? renderStateErrorSummary(getUnmarkedStateErrorSummary(sm, stateErrors), stateErrors) : ''}
5256
${showConditions && hasConditionExpansions ? /*html*/`
5357
<div class="diagram-condition-legend">
5458
<span class="diagram-condition-legend-pre">Precondition</span>
@@ -67,7 +71,11 @@ function renderStateMachineViewHtml(sm: LJStateMachine | undefined, diagram: str
6771
`;
6872
}
6973

70-
function renderStateErrorSummary(focus: StateErrorFocus, stateErrors: StateErrorNavigation): string {
74+
function isStateErrorShown(stateErrors: StateErrorNavigation): boolean {
75+
return stateErrors.index >= 0 || stateErrors.line !== undefined;
76+
}
77+
78+
function getStateErrorSummary(focus: StateErrorFocus): string {
7179
const call = getRejectedCallText(focus);
7280
const receiver = focus.receiver ? `<code>${escapeHtml(focus.receiver)}</code>` : 'the receiver';
7381
const rejected = `${call ? `<code>${escapeHtml(call)}</code>` : 'The call'} is rejected`;
@@ -81,6 +89,20 @@ function renderStateErrorSummary(focus: StateErrorFocus, stateErrors: StateError
8189
summary = `${rejected} here`;
8290
}
8391
if (allowedStates.length > 0) summary += ` \u00B7 allowed in state ${renderStates(allowedStates)}`;
92+
return summary;
93+
}
94+
95+
// the shown error cannot be marked on the diagram when the verifier could not tell which states allow the call
96+
function getUnmarkedStateErrorSummary(sm: LJStateMachine, stateErrors: StateErrorNavigation): string {
97+
const context = sm.errorContext;
98+
if (context?.calledMethod) {
99+
const call = `${context.receiver ? `${context.receiver}.` : ''}${context.calledMethod}()`;
100+
return `<code>${escapeHtml(call)}</code> is rejected here`;
101+
}
102+
return stateErrors.line ? `The call at line ${stateErrors.line} is rejected` : 'The call is rejected';
103+
}
104+
105+
function renderStateErrorSummary(summary: string, stateErrors: StateErrorNavigation): string {
84106
return /*html*/`
85107
<div class="diagram-error-summary">
86108
<p class="diagram-error-text">${summary}</p>

‎client/src/webview/views/sections.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ export function renderNav(selectedTab: NavTab, stateErrorCount = 0): string {
141141
<nav>
142142
<ul>
143143
<li><button class="nav-tab ${selectedTab === 'diagnostics' ? 'selected' : ''}" data-tab="diagnostics">Verification</button></li>
144-
<li><button class="nav-tab ${selectedTab === 'context' ? 'selected' : ''}" data-tab="context">Context</button></li>
145144
<li><button class="nav-tab ${selectedTab === 'fsm' ? 'selected' : ''}" data-tab="fsm">State Machine${stateErrorBadge}</button></li>
145+
<li><button class="nav-tab ${selectedTab === 'context' ? 'selected' : ''}" data-tab="context">Context</button></li>
146146
</ul>
147147
</nav>
148148
`;

0 commit comments

Comments
 (0)