Skip to content

Commit 4b063e9

Browse files
authored
Complex State Transitions in DFA (#86)
1 parent 5f8c9f5 commit 4b063e9

19 files changed

Lines changed: 546 additions & 84 deletions

client/src/types/fsm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
export type LJStateMachine = {
44
className: string;
5-
initialStates: string[];
5+
initialTransitions: { to: string; postCond?: string | null }[];
66
states: string[];
7-
transitions: { from: string; to: string; label: string }[];
7+
transitions: { from: string; to: string; label: string; preCond?: string | null; postCond?: string | null }[];
88
};

client/src/webview/diagram.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let startY = 0;
2121
* @param sm
2222
* @returns Mermaid diagram string
2323
*/
24-
export function createMermaidDiagram(sm: LJStateMachine | undefined, orientation: "LR" | "TB"): string {
24+
export function createMermaidDiagram(sm: LJStateMachine | undefined, orientation: "LR" | "TB", showConditions = false): string {
2525
if (!sm) return '';
2626

2727
const lines: string[] = [];
@@ -33,17 +33,19 @@ export function createMermaidDiagram(sm: LJStateMachine | undefined, orientation
3333
lines.push('stateDiagram-v2');
3434
lines.push(` direction ${orientation}`);
3535

36-
// initial states
37-
sm.initialStates.forEach(state => {
38-
lines.push(` [*] --> ${state}`);
36+
// initial transitions
37+
sm.initialTransitions.forEach(transition => {
38+
const label = getInitialTransitionLabel(transition.postCond, showConditions);
39+
lines.push(` [*] --> ${transition.to}${label ? ` : ${label}` : ''}`);
3940
});
4041

4142
// group transitions by from/to states and merge labels
4243
const transitionMap = new Map<string, string[]>();
4344
sm.transitions.forEach(transition => {
45+
const label = getTransitionLabel(transition.label, transition.preCond, transition.postCond, showConditions);
4446
const key = `${transition.from}|${transition.to}`;
4547
if (!transitionMap.has(key)) transitionMap.set(key, []);
46-
transitionMap.get(key)?.push(transition.label);
48+
transitionMap.get(key)?.push(label);
4749
});
4850

4951
// add transitions
@@ -56,6 +58,33 @@ export function createMermaidDiagram(sm: LJStateMachine | undefined, orientation
5658
return lines.join('\n');
5759
}
5860

61+
function getTransitionLabel(label: string, preCond?: string | null, postCond?: string | null, showConditions = false): string {
62+
if (!showConditions) {
63+
return escapeMermaidLabel(label);
64+
}
65+
66+
return [
67+
getConditionLabel('pre', preCond),
68+
escapeMermaidLabel(label),
69+
getConditionLabel('post', postCond)
70+
].filter(Boolean).join('<br/>');
71+
}
72+
73+
function getInitialTransitionLabel(postCond?: string | null, showConditions = false): string {
74+
return showConditions ? getConditionLabel('post', postCond) : '';
75+
}
76+
77+
function getConditionLabel(kind: 'pre' | 'post', cond?: string | null): string {
78+
if (!cond) {
79+
return '';
80+
}
81+
return `<span class="state-cond state-cond-${kind}">${escapeMermaidLabel(cond)}</span>`;
82+
}
83+
84+
function escapeMermaidLabel(label: string): string {
85+
return label.replace(/&/g, '&amp;').replace(/"/g, '\\"').replace(/</g, '&lt;').replace(/>/g, '&gt;');
86+
}
87+
5988
/**
6089
* Renders Mermaid diagrams in the document
6190
* @param document The document object

client/src/webview/script.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
3737
let selectedTab: NavTab = 'diagnostics';
3838
let status: ExtensionStatus = 'loading';
3939
let diagramOrientation: "LR" | "TB" = "TB";
40+
let showDiagramConditions = false;
4041
let currentDiagram: string = '';
4142
let revealTimeout: ReturnType<typeof setTimeout> | undefined;
4243
const contextSectionState: ContextSectionState = {
@@ -150,6 +151,15 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
150151
return;
151152
}
152153

154+
// toggle diagram conditions
155+
if (target.id === 'diagram-conditions-btn') {
156+
e.stopPropagation();
157+
if ((target as HTMLButtonElement).disabled) return;
158+
showDiagramConditions = !showDiagramConditions;
159+
updateView();
160+
return;
161+
}
162+
153163
// zoom in
154164
if (target.id === 'zoom-in-btn') {
155165
e.stopPropagation();
@@ -267,6 +277,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
267277
break;
268278
case 'fsm':
269279
stateMachine = msg.sm as LJStateMachine;
280+
showDiagramConditions = false;
270281
if (selectedTab === 'fsm') updateView();
271282
break;
272283
case 'context':
@@ -302,9 +313,9 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
302313
: renderLoading();
303314
break;
304315
case 'fsm': {
305-
const diagram = createMermaidDiagram(stateMachine, diagramOrientation);
316+
const diagram = createMermaidDiagram(stateMachine, diagramOrientation, showDiagramConditions);
306317
currentDiagram = diagram;
307-
root.innerHTML = renderStateMachineView(stateMachine, diagram, diagramOrientation);
318+
root.innerHTML = renderStateMachineView(stateMachine, diagram, diagramOrientation, showDiagramConditions);
308319
if (stateMachine) renderMermaidDiagram(document, window);
309320
break;
310321
}

client/src/webview/styles.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export function getStyles(): string {
3838
--lj-token-boolean: var(--vscode-debugTokenExpression-boolean, var(--vscode-symbolIcon-booleanForeground, var(--vscode-editor-foreground)));
3939
--lj-token-identifier: var(--vscode-debugTokenExpression-name, var(--vscode-editor-foreground));
4040
--lj-token-punctuation: var(--vscode-editor-foreground);
41+
--lj-state-cond-pre: var(--lj-token-function);
42+
--lj-state-cond-post: var(--lj-token-type);
4143
}
4244
body.vscode-light {
4345
--lj-token-keyword: #0000FF;
@@ -650,9 +652,12 @@ export function getStyles(): string {
650652
background: none;
651653
opacity: 1;
652654
}
655+
.diagram-control-btn.active {
656+
opacity: 1;
657+
}
653658
.diagram-control-btn:disabled {
654659
cursor: default;
655-
opacity: 0.8;
660+
opacity: 0.35;
656661
}
657662
.mermaid .statediagramTitleText {
658663
font-size: 30px!important;
@@ -681,6 +686,18 @@ export function getStyles(): string {
681686
color: var(--vscode-foreground) !important;
682687
background: var(--vscode-editor-background) !important;
683688
}
689+
.diagram-container .mermaid .edgeLabel .state-cond {
690+
color: var(--vscode-descriptionForeground) !important;
691+
display: inline-block !important;
692+
font-size: 0.82em !important;
693+
line-height: 1.2 !important;
694+
}
695+
.diagram-container .mermaid .edgeLabel .state-cond-pre {
696+
color: var(--lj-state-cond-pre) !important;
697+
}
698+
.diagram-container .mermaid .edgeLabel .state-cond-post {
699+
color: var(--lj-state-cond-post) !important;
700+
}
684701
.diagram-container .mermaid svg rect,
685702
.diagram-container .mermaid svg circle,
686703
.diagram-container .mermaid svg ellipse,

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import type { LJStateMachine } from "../../../types/fsm";
22
import { renderMainHeader } from "../sections";
33

4-
export function renderStateMachineView(sm: LJStateMachine | undefined, diagram: string, orientation: "LR" | "TB"): string {
4+
export function renderStateMachineView(sm: LJStateMachine | undefined, diagram: string, orientation: "LR" | "TB", showConditions: boolean): string {
5+
const initialStateNames = sm ? [...new Set(sm.initialTransitions.map(transition => transition.to))] : [];
6+
const hasConditionExpansions = sm
7+
? sm.initialTransitions.some(transition => !!transition.postCond)
8+
|| sm.transitions.some(transition => !!transition.preCond || !!transition.postCond)
9+
: false;
10+
const conditionToggleLabel = showConditions ? 'Collapse Conditions' : 'Expand Conditions';
11+
const conditionToggleIcon = showConditions ? '⊟' : '⊞';
12+
513
return /*html*/`
614
<div>
715
${renderMainHeader("", 'fsm')}
@@ -13,6 +21,7 @@ export function renderStateMachineView(sm: LJStateMachine | undefined, diagram:
1321
<button id="zoom-out-btn" class="diagram-control-btn" title="Zoom Out">-</button>
1422
<button id="zoom-reset-btn" class="diagram-control-btn" title="Reset Zoom">⟲</button>
1523
<button id="diagram-orientation-btn" class="diagram-control-btn" title="Rotate Diagram">${orientation === "TB" ? "↓" : "→"}</button>
24+
<button id="diagram-conditions-btn" class="diagram-control-btn${showConditions ? ' active' : ''}" title="${conditionToggleLabel}" aria-label="${conditionToggleLabel}" aria-pressed="${showConditions ? 'true' : 'false'}" ${hasConditionExpansions ? '' : 'disabled'}>${conditionToggleIcon}</button>
1625
<button id="copy-diagram-btn" class="diagram-control-btn" title="Copy Mermaid Source">⎘</button>
1726
</div>
1827
<div id="diagram-wrapper" class="diagram-wrapper">
@@ -21,9 +30,9 @@ export function renderStateMachineView(sm: LJStateMachine | undefined, diagram:
2130
</div>
2231
<div>
2332
<p><strong>States:</strong> ${sm.states.join(', ')}</p>
24-
<p><strong>Initial state${sm.initialStates.length > 1 ? 's' : ''}:</strong> ${sm.initialStates.join(', ')}</p>
33+
<p><strong>Initial state${initialStateNames.length > 1 ? 's' : ''}:</strong> ${initialStateNames.join(', ')}</p>
2534
<p><strong>Number of states:</strong> ${sm.states.length}</p>
26-
<p><strong>Number of transitions:</strong> ${sm.transitions.length + 1}</p>
35+
<p><strong>Number of transitions:</strong> ${sm.transitions.length + sm.initialTransitions.length}</p>
2736
</div>
2837
</div>`
2938
: 'No state machine available for the current file'}

server/src/main/java/fsm/StateMachine.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
public record StateMachine(
99
String className,
10-
List<String> initialStates,
1110
List<String> states,
12-
List<StateMachineTransition> transitions
11+
List<StateMachineTransition> transitions,
12+
List<StateMachineInitialTransition> initialTransitions
1313
) { }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package fsm;
2+
3+
/**
4+
* Represents an initial transition in a state machine
5+
*/
6+
public record StateMachineInitialTransition(String to, String postCond) {
7+
8+
public StateMachineInitialTransition(String to) {
9+
this(to, null);
10+
}
11+
}

0 commit comments

Comments
 (0)