Skip to content

Commit 69b6860

Browse files
Label a named state set's diagram with its name (#178)
Brings [liquidjava-fsm#17](liquid-java/liquidjava-fsm#17) to the extension, for the verifier's named state sets and states written as values of their sets (liquid-java/liquidjava#498, #510, #511; design in liquid-java/liquidjava#502). **Depends on a liquidjava-fsm 0.2.3 release** of liquidjava-fsm#17: until it is on Maven Central, `server` does not build in CI. ## What changes - `server/pom.xml`: liquidjava-fsm 0.2.2 → 0.2.3, which reads `phase(this) == open`, `open == phase(this)` and `phase(this) != open` in `from`/`to` as the states they name, and reports each diagram's `stateSetName`. - `StateMachineDTO`: passes `stateSetName` to the client (the `liquidjava/fsm` request returns the fsm records, so it gets it as is). - State Machine view: a named state set's diagram is titled with its name, also when it is the class's only state set, so it can be matched with the error message (`doorState(d) == closed`). A state set without a name keeps its states as the title, as now. ## Checked, no change needed The verifier sends the same shapes to the IDE as before (`git diff fbc5e9f6 origin/main`): `StateErrorContext` still names states by their names, and hints are unchanged. Only the text of a state error's message, `expected` and `found` changes, to `doorState(d) == closed` for named sets. The client renders those strings as they come, with the generic highlighter, and nothing in the client parses state names out of message text: the diagram marks and the summary line use `errorContext`. The verification view shows the new form as is (last screenshot). ## Screenshots Headless VS Code with this branch, liquidjava-fsm#17 installed locally as 0.2.3 and the verifier built from liquid-java/liquidjava main, on this spec (`d.lock(); d.open();` as the error): ```java @stateset(name = "doorState", value = {"closed", "opened"}) @stateset(name = "lockState", value = {"unlocked", "locked"}) public class Door { @StateRefinement(to = "doorState(this) == closed && lockState(this) == unlocked") public Door() {} @StateRefinement(from = "doorState(this) == closed && lockState(this) == unlocked", to = "doorState(this) == opened") public void open() {} @StateRefinement(from = "doorState(this) == opened", to = "doorState(this) == closed") public void close() {} @StateRefinement(from = "doorState(this) == closed && lockState(this) != locked", to = "lockState(this) == locked") public void lock() {} @StateRefinement(from = "lockState(this) == locked", to = "lockState(this) == unlocked") public void unlock() {} } ``` Before (main, fsm 0.2.2), the diagrams are wrong: `open` and `lock` go from every state with the whole `from` as a condition, `close` and `unlock` are missing, the summary only names `lockState`, and the titles are the states. After, the diagrams match the spec, the conditions keep the form the spec wrote, and the titles are the set names: | | Before | After | |---|---|---| | Show all | <img src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/178/before-all.png" width="380"> | <img src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/178/after-all.png" width="380"> | | Focus on error | <img src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/178/before-focus.png" width="380"> | <img src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/178/after-focus.png" width="380"> | The verifier's message for the same error (no change here; shown for context): <img src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/178/verification.png" width="380"> ## Not in this PR - The verifier bump: main of liquid-java/liquidjava is not released yet. When it is, `DiagnosticConverterTest` needs the new `StateRefinementError` constructor argument (`display`, from #511) and the `RefinementError` one (`aliases`); the server code itself compiles against it. - The walkthrough and README examples stay in the shorthand until the extension ships a verifier that accepts named sets. ## Tests - client: `vitest run` 124 passed (one new test, fails without the change), typecheck and lint clean - server: `mvn test` 35 passed, against liquidjava-fsm#17 installed as 0.2.3 in an isolated local Maven repository 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 123cda0 commit 69b6860

5 files changed

Lines changed: 24 additions & 7 deletions

File tree

‎client/src/types/fsm.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export type LJStateMachine = {
55
className: string;
66
// index of the state set among the class's @StateSet annotations, in declaration order
77
stateSet?: number;
8+
// the name given to the state set, as the verifier's messages use it, e.g. phase(this) == open; null if it has none
9+
stateSetName?: string | null;
810
initialTransitions: { to: string; toCondition?: string | null; constructorSignature?: string | null }[];
911
// methods that put an object in an unknown state (a field, a parameter or a getter's result) into a state
1012
entryTransitions?: { to: string; label: string; toCondition?: string | null }[];

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,15 @@ describe('state machine view', () => {
325325
expect(document.querySelectorAll('#diagram-wrapper')).toHaveLength(1);
326326
});
327327

328+
it('labels a named state set with its name, even when it is the only one', () => {
329+
renderStateMachineView(document.body, [{ ...tiling, stateSetName: 'tiling' }, compression], ['tiling diagram', 'compression diagram'], 'TB', false);
330+
expect(Array.from(document.querySelectorAll('.diagram-state-set-title'), node => node.textContent))
331+
.toEqual(['tiling', 'startCompression \u00B7 compressionExplicit']);
332+
333+
renderStateMachineView(document.body, [{ ...tiling, stateSetName: 'tiling' }], [diagram], 'TB', false);
334+
expect(document.querySelector('.diagram-state-set-title')?.textContent).toBe('tiling');
335+
});
336+
328337
it('keeps a single state set without a state set label', () => {
329338
renderStateMachineView(document.body, [tiling], [diagram], 'TB', false);
330339

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function renderStateMachineViewHtml(sms: LJStateMachine[], diagrams: string[], o
6767
` : ''}
6868
<div class="diagram-viewport${focusOnError && errorFocuses.length > 0 ? ' diagram-focused' : ''}">
6969
<div id="diagram-wrapper" class="diagram-wrapper">
70-
${sms.length === 1
70+
${sms.length === 1 && !sms[0].stateSetName
7171
? `<pre class="mermaid">${diagrams[0]}</pre>`
7272
: sms.map((sm, i) => renderStateSetDiagram(sm, diagrams[i], focusOnError && errorFocuses.length > 0)).join('')}
7373
</div>
@@ -79,12 +79,14 @@ function renderStateMachineViewHtml(sms: LJStateMachine[], diagrams: string[], o
7979
`;
8080
}
8181

82-
// the diagram of one state set, labelled with its states, and dimmed when focusing on an error of another state set
82+
// the diagram of one state set, labelled with its name, or its states if it has none,
83+
// and dimmed when focusing on an error of another state set
8384
function renderStateSetDiagram(sm: LJStateMachine, diagram: string, dimUnrelated: boolean): string {
8485
const dimmed = dimUnrelated && !sm.errorContext;
86+
const title = sm.stateSetName ?? sm.states.join(' \u00B7 ');
8587
return /*html*/`
8688
<div class="diagram-state-set${dimmed ? ' diagram-state-set-dimmed' : ''}">
87-
<h3 class="diagram-state-set-title">${sm.states.map(state => escapeHtml(state)).join(' \u00B7 ')}</h3>
89+
<h3 class="diagram-state-set-title">${escapeHtml(title)}</h3>
8890
<pre class="mermaid">${diagram}</pre>
8991
</div>
9092
`;

‎server/src/main/java/dtos/fsm/StateMachineDTO.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
* DTO for serializing the state machine diagram of one state set, with the state error shown on it
1313
*
1414
* @param stateSet the index of the state set among the class's StateSet annotations
15+
* @param stateSetName the name given to the state set, or null if it has none
1516
* @param entryTransitions the methods that put an object in an unknown state into a state of this state set
1617
* @param errorContext the state error on this diagram, or null if the call does not involve this state set
1718
*/
1819
public record StateMachineDTO(
1920
String className,
2021
int stateSet,
22+
String stateSetName,
2123
List<String> states,
2224
List<StateMachineTransition> transitions,
2325
List<StateMachineInitialTransition> initialTransitions,
@@ -26,7 +28,7 @@ public record StateMachineDTO(
2628

2729
public static StateMachineDTO from(StateMachine sm, StateErrorContext context) {
2830
if (sm == null) return null;
29-
return new StateMachineDTO(sm.className(), sm.stateSet(), sm.states(), sm.transitions(),
31+
return new StateMachineDTO(sm.className(), sm.stateSet(), sm.stateSetName(), sm.states(), sm.transitions(),
3032
sm.initialTransitions(), sm.entryTransitions(), StateErrorContextDTO.from(context, sm.states(), sm.stateSet()));
3133
}
3234

‎server/src/test/java/dtos/fsm/StateMachineDTOTest.java‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,14 @@ void showsTheErrorOnEveryStateSetWhenNoneHasTheCalledMethod() {
5353
}
5454

5555
@Test
56-
void keepsTheEntryTransitions() {
57-
StateMachine sm = new StateMachine("example.Edit", 0, List.of("start", "endDone"),
56+
void keepsTheStateSetNameAndEntryTransitions() {
57+
StateMachine sm = new StateMachine("example.Edit", 0, "phase", List.of("start", "endDone"),
5858
List.of(new StateMachineTransition("start", "endDone", "end")),
5959
List.of(new StateMachineInitialTransition("start")),
6060
List.of(new StateMachineEntryTransition("endDone", "canUndo", "return")));
61-
assertEquals(sm.entryTransitions(), StateMachineDTO.from(sm, null).entryTransitions());
61+
StateMachineDTO dto = StateMachineDTO.from(sm, null);
62+
assertEquals("phase", dto.stateSetName());
63+
assertEquals(sm.entryTransitions(), dto.entryTransitions());
6264
}
6365

6466
@Test

0 commit comments

Comments
 (0)