Skip to content

Commit 9a42f7a

Browse files
Open the State Machine view on the first state error of the file
Count only the state errors of the current file, keep the focus toggle with the legend apart from View error, and move Show in state machine to the top of the error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 31edcee commit 9a42f7a

10 files changed

Lines changed: 109 additions & 99 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
The **State Machine** tab shows object states and method transitions declared with `@StateSet` and `@StateRefinement`.
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 keeps the last state error shown 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. 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/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@
240240
{
241241
"id": "stateMachine",
242242
"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 keeps the last state error shown 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)",
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)",
244244
"media": {
245245
"markdown": "media/walkthrough/state-machine.md"
246246
},

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

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ const stateMachine: LJStateMachine = {
5757
errorContext: null,
5858
};
5959

60-
function stateError(title: string, sm: LJStateMachine, line = position.lineStart): LJDiagnostic {
61-
const errorPosition = { ...position, lineStart: line, lineEnd: line };
60+
function stateError(title: string, sm: LJStateMachine, line = position.lineStart, file = currentFile): LJDiagnostic {
61+
const errorPosition = { ...position, file, lineStart: line, lineEnd: line };
6262
return {
63-
title, message: `${title} message`, hint: null, file: currentFile, position: errorPosition,
63+
title, message: `${title} message`, hint: null, file, position: errorPosition,
6464
category: 'error', type: 'state-refinement-error', translationTable: {}, expected: 'Open(counter)',
6565
found: { implication: null as never, origin: null, simplification: 'Closed(counter)' },
6666
customMessage: '', declarationPosition: null, stateMachine: sm,
@@ -285,16 +285,28 @@ describe('webview script', () => {
285285
expect(root.querySelector('.diagram-title')?.textContent).toBe('First');
286286
});
287287

288-
it('points to the state errors of the file until one is shown', () => {
289-
showDiagnostics([stateError('State refinement error', errorMachine('Counter', 'close'))]);
288+
it('opens on the first state error of the file, focused', () => {
289+
showDiagnostics([
290+
stateError('Second state error', errorMachine('Second', 'reset'), 9),
291+
stateError('First state error', errorMachine('First', 'close'), 4),
292+
]);
290293
receive({ type: 'fsm', sm: stateMachine });
291294
click('[data-tab="fsm"]');
292-
expect(root.querySelector('.diagram-error-text')?.textContent).toBe('1 state error found');
293-
expect(root.querySelector('.mermaid')?.textContent).not.toContain('ljState');
294295

295-
click('#state-error-show-btn');
296+
expect(root.querySelector('.diagram-title')?.textContent).toBe('First');
297+
expect(root.querySelector('.state-error-position')?.textContent).toBe('1 of 2');
296298
expect(root.querySelector('#diagram-focus-btn')?.textContent?.trim()).toBe('Show all');
297-
expect(root.querySelector('.mermaid')?.textContent).toContain('class Broken ljStateDimmed');
299+
expect(root.querySelector('.mermaid')?.textContent).toContain('ljStateDimmed');
300+
});
301+
302+
it('counts only the state errors of the current file, as listed in the Verification tab', () => {
303+
showDiagnostics([
304+
stateError('State refinement error', errorMachine('Counter', 'close'), 4),
305+
stateError('Other file error', errorMachine('Counter', 'close'), 2, otherFile),
306+
]);
307+
308+
expect(root.querySelector('[data-tab="fsm"] .nav-badge')?.textContent).toBe('1');
309+
expect(root.querySelectorAll('.diagnostic-state-machine-btn')).toHaveLength(1);
298310
});
299311

300312
it('follows the state error at the cursor without overriding a later choice', () => {
@@ -368,6 +380,21 @@ describe('webview script', () => {
368380

369381
expect(root.querySelector('.diagnostic-state-machine-btn')?.textContent?.trim()).toBe('Show in state machine');
370382
expect(root.querySelector('.diagnostic-actions .diagnostic-state-machine-btn')).toBeNull();
383+
// right below the error message, before its details
384+
const firstSection = root.querySelector('.error-item .section')!;
385+
expect(root.querySelector('.diagnostic-state-machine-btn')!.compareDocumentPosition(firstSection)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
386+
});
387+
388+
it('opens on the first state error of a newly opened file', () => {
389+
showDiagnostics([
390+
stateError('Main error', errorMachine('Main', 'close'), 4),
391+
stateError('Other file error', errorMachine('Other', 'close'), 2, otherFile),
392+
]);
393+
click('.diagnostic-state-machine-btn');
394+
expect(root.querySelector('.diagram-title')?.textContent).toBe('Main');
395+
396+
receive({ type: 'file', file: otherFile });
397+
expect(root.querySelector('.diagram-title')?.textContent).toBe('Other');
371398
});
372399

373400
it('toggles between file and workspace errors by clicking scope buttons', () => {

‎client/src/webview/script.ts‎

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
4141
let status: ExtensionStatus = 'loading';
4242
let diagramOrientation: "LR" | "TB" = "TB";
4343
let showDiagramConditions = false;
44-
let focusDiagramOnError = false;
44+
let focusDiagramOnError = true;
4545
let currentDiagram: string = '';
4646
let renderedStateMachineView: string | undefined;
4747
let revealTimeout: ReturnType<typeof setTimeout> | undefined;
@@ -156,23 +156,16 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
156156
e.stopPropagation();
157157
if ((stateErrorStepButton as HTMLButtonElement).disabled) return;
158158
const errors = getCurrentStateErrors();
159-
const index = getShownStateErrorIndex(errors) + parseInt(stateErrorStepButton.getAttribute('data-step') || '0', 10);
159+
const index = getShownStateError(errors).index + parseInt(stateErrorStepButton.getAttribute('data-step') || '0', 10);
160160
if (errors[index]) showStateError(errors[index]);
161161
return;
162162
}
163163

164-
// show the first state error of the current file
165-
if (target.closest?.('#state-error-show-btn')) {
166-
e.stopPropagation();
167-
const [first] = getCurrentStateErrors();
168-
if (first) showStateError(first);
169-
return;
170-
}
171-
172164
// go back to the shown state error in the Verification tab
173165
if (target.closest?.('#state-error-reveal-btn')) {
174166
e.stopPropagation();
175-
const revealTarget = selectedStateError && getDiagnosticRevealTarget(selectedStateError);
167+
const { error } = getShownStateError(getCurrentStateErrors());
168+
const revealTarget = error && getDiagnosticRevealTarget(error);
176169
if (revealTarget) revealDiagnostic(revealTarget);
177170
return;
178171
}
@@ -378,6 +371,8 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
378371
}
379372
case 'file':
380373
currentFile = msg.file;
374+
// the view shows the first error of the new file instead
375+
if (selectedStateError && selectedStateError.file.toLowerCase() !== currentFile?.toLowerCase()) selectedStateError = undefined;
381376
if (diagnostics && !showAllDiagnostics && selectedTab === 'diagnostics') updateView();
382377
stateMachine = undefined;
383378
if (selectedTab === 'fsm') updateStateMachineViewIfChanged();
@@ -426,8 +421,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
426421
: renderLoading();
427422
break;
428423
case 'fsm': {
429-
const shownIndex = getShownStateErrorIndex(stateErrors);
430-
const shownError = stateErrors[shownIndex];
424+
const { error: shownError, index: shownIndex } = getShownStateError(stateErrors);
431425
const displayedStateMachine = shownError?.stateMachine ?? stateMachine;
432426
const focusOnError = focusDiagramOnError && !!shownError;
433427
const diagram = createMermaidDiagram(displayedStateMachine, diagramOrientation, showDiagramConditions, focusOnError);
@@ -448,13 +442,17 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
448442
}
449443

450444
function getCurrentStateErrors(): StateRefinementError[] {
451-
return getStateErrors(diagnostics, currentFile, stateMachine);
445+
return getStateErrors(diagnostics, currentFile);
452446
}
453447

454-
function getShownStateErrorIndex(errors: StateRefinementError[]): number {
455-
if (!selectedStateError) return -1;
448+
/**
449+
* Gets the state error the view shows and its index among the state errors of the current file
450+
* This is the selected error, which is outside them when chosen from another file, or else the first one
451+
*/
452+
function getShownStateError(errors: StateRefinementError[]): { error: StateRefinementError | undefined; index: number } {
453+
if (!selectedStateError) return { error: errors[0], index: errors.length > 0 ? 0 : -1 };
456454
const key = getStateErrorKey(selectedStateError);
457-
return errors.findIndex(error => getStateErrorKey(error) === key);
455+
return { error: selectedStateError, index: errors.findIndex(error => getStateErrorKey(error) === key) };
458456
}
459457

460458
function findStateErrorAt(target: DiagnosticRevealTarget): StateRefinementError | undefined {
@@ -497,7 +495,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
497495

498496
function getStateMachineViewKey(): string {
499497
const stateErrors = getCurrentStateErrors();
500-
const shownError = stateErrors[getShownStateErrorIndex(stateErrors)];
498+
const { error: shownError } = getShownStateError(stateErrors);
501499
return JSON.stringify([shownError?.stateMachine ?? stateMachine ?? null, shownError ? getStateErrorKey(shownError) : null, stateErrors.length]);
502500
}
503501

‎client/src/webview/state-errors.spec.ts‎

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,11 @@ describe('state errors', () => {
4444
it('lists the errors of the current file in source order', () => {
4545
const later = stateError(20);
4646
const earlier = stateError(10);
47-
const otherFile = stateError(5, { file: otherCallerFile, stateMachine: machine('java.util.ArrayList') });
47+
const otherFile = stateError(5, { file: otherCallerFile });
4848

49-
expect(getStateErrors([later, otherFile, earlier], callerFile, undefined)).toEqual([earlier, later]);
50-
expect(getStateErrors([later], undefined, undefined)).toEqual([]);
51-
});
52-
53-
it('adds the errors on the state machine of the current file from other files after its own', () => {
54-
const ownError = stateError(30, { file: refinementsFile, stateMachine: machine('java.util.ArrayList') });
55-
const callerError = stateError(10);
56-
const otherCallerError = stateError(4, { file: otherCallerFile });
57-
const otherMachine = stateError(2, { file: otherCallerFile, stateMachine: machine('java.util.ArrayList') });
58-
59-
expect(getStateErrors([callerError, otherMachine, otherCallerError, ownError], refinementsFile, machine()))
60-
.toEqual([ownError, otherCallerError, callerError]);
49+
expect(getStateErrors([later, otherFile, earlier], callerFile)).toEqual([earlier, later]);
50+
expect(getStateErrors([later, otherFile], refinementsFile)).toEqual([]);
51+
expect(getStateErrors([later], undefined)).toEqual([]);
6152
});
6253

6354
it('finds the selected error again after verification', () => {

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

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
11
import type { LJDiagnostic, StateRefinementError } from "../types/diagnostics";
2-
import type { LJStateMachine } from "../types/fsm";
32
import { getDiagnosticRevealTarget, getDiagnosticRevealTargetKey } from "./diagnostic-reveal";
43

54
/**
6-
* Gets the state errors that the State Machine view can show for the current file
7-
* These are the state errors reported in the file and, since errors are usually reported in the caller file,
8-
* the ones on the state machine declared in the file, with the errors of the file first in source order
5+
* Gets the state errors reported in the current file, in source order
6+
* These are the errors the State Machine view steps through, one for each Show in state machine button of the file
97
* @param diagnostics All diagnostics in the workspace
108
* @param currentFile The file open in the editor
11-
* @param sm The state machine declared in the current file
129
*/
13-
export function getStateErrors(diagnostics: LJDiagnostic[] | undefined, currentFile: string | undefined, sm: LJStateMachine | undefined): StateRefinementError[] {
14-
const isCurrentFile = (error: StateRefinementError) => !!currentFile && error.file.toLowerCase() === currentFile.toLowerCase();
10+
export function getStateErrors(diagnostics: LJDiagnostic[] | undefined, currentFile: string | undefined): StateRefinementError[] {
11+
if (!currentFile) return [];
1512
return getAllStateErrors(diagnostics)
16-
.filter(error => isCurrentFile(error) || error.stateMachine?.className === sm?.className)
13+
.filter(error => error.file.toLowerCase() === currentFile.toLowerCase())
1714
.map((error, index) => ({ error, index }))
18-
.sort((a, b) => Number(isCurrentFile(b.error)) - Number(isCurrentFile(a.error))
19-
|| a.error.file.localeCompare(b.error.file)
20-
|| (a.error.position?.lineStart ?? 0) - (b.error.position?.lineStart ?? 0)
15+
.sort((a, b) => (a.error.position?.lineStart ?? 0) - (b.error.position?.lineStart ?? 0)
2116
|| (a.error.position?.colStart ?? 0) - (b.error.position?.colStart ?? 0)
2217
|| a.index - b.index)
2318
.map(({ error }) => error);

‎client/src/webview/styles.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ export function getStyles(): string {
252252
display: inline-flex;
253253
align-items: center;
254254
gap: 0.4rem;
255-
margin: 0.75rem 0 0.25rem;
255+
margin: 0 0 1rem;
256256
padding: 0.25rem 0.6rem;
257257
color: var(--vscode-button-secondaryForeground);
258258
background-color: var(--vscode-button-secondaryBackground);
@@ -1095,6 +1095,7 @@ export function getStyles(): string {
10951095
}
10961096
.diagram-focus-btn {
10971097
flex-shrink: 0;
1098+
margin-left: auto;
10981099
}
10991100
.diagram-error-actions {
11001101
display: flex;
@@ -1124,6 +1125,7 @@ export function getStyles(): string {
11241125
.diagram-error-legend {
11251126
display: flex;
11261127
flex-wrap: wrap;
1128+
align-items: center;
11271129
gap: 0.25rem 1rem;
11281130
margin-bottom: 0.75rem;
11291131
font-size: 0.8rem;

‎client/src/webview/views/diagnostics/errors.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ export function renderErrors(errors: LJError[]): string {
2525
${renderDiagnosticContextAction(error)}
2626
${renderCopyDiagnosticButton('error', index)}
2727
</div>
28-
${renderError(error)}
29-
${renderDiagnosticStateMachineAction(error, index)}
28+
${renderError(error, renderDiagnosticStateMachineAction(error, index))}
3029
</li>
3130
`;
3231
}).join("")}
@@ -67,13 +66,18 @@ const errorContentRenderers: ErrorRendererMap = {
6766
'custom-error': (_: CustomError) => "",
6867
};
6968

70-
export function renderError(error: LJError): string {
69+
/**
70+
* Renders the error content
71+
* @param error The error to render
72+
* @param headerActions Actions shown below the error message
73+
*/
74+
export function renderError(error: LJError, headerActions = ''): string {
7175
const message = error.type === 'refinement-error' || error.type === 'state-refinement-error' ? error.customMessage : error.message;
7276
const header = renderDiagnosticHeader(error.title, message || '');
7377
const content = (errorContentRenderers[error.type] as (error: LJError) => string)?.(error) || '';
7478
const hint = renderHint(error.hint);
7579
const location = renderLocation(error);
76-
return /*html*/`${header}${content}${hint}${location}`;
80+
return /*html*/`${header}${headerActions}${content}${hint}${location}`;
7781
}
7882

7983
function renderDiagnosticContextAction(error: LJError): string {

0 commit comments

Comments
 (0)