Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion editors/code/common/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,29 @@
* (`kind` on component nodes, `kind`/`isOutput` on ports) and are shared between
* the extension host and the webview.
*/
import type { SNode, SPort } from "sprotty-protocol";
import type { SEdge, SNode, SPort } from "sprotty-protocol";

export interface ComponentSNode extends SNode {
kind: string
}

/** A declared port of the diagrammed topology, drawn on the diagram boundary. */
export interface BoundarySNode extends SNode {
kind: string,
isOutput: boolean, // Outputs are pinned to the last ELK layer, inputs to the first.
}

/** A collapsed imported topology; activating it opens that topology's diagram. */
export interface TopologySNode extends SNode {
qualifiedName: string
}

export interface FppSEdge extends SEdge {
implicit?: boolean, // Ends on a collapsed topology's boundary rather than a rendered port.
detail?: string, // Hover text.
count?: number, // Wires the edge stands for; more than one makes it a bus. Absent means 1.
}

export interface PortSNode extends SPort {
kind: string,
isOutput: boolean, // Store output info here for ELK layout config. Input ports are positioned west, and output ports are positioned east.
Expand Down
22 changes: 22 additions & 0 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@
"icon": "$(eye)",
"category": "FPP Diagram"
},
{
"command": "fpp.diagram.toggle-collapsed-subtopologies",
"title": "FPP: Toggle Collapsed Subtopologies",
"icon": "$(group-by-ref-type)",
"category": "FPP Diagram"
},
{
"command": "fpp.diagram.toggle-bus-wires",
"title": "FPP: Toggle Bus Wires",
"icon": "$(combine)",
"category": "FPP Diagram"
},
{
"command": "fpp.stateMachine.fit",
"title": "FPP: Fit State Machine Diagram",
Expand Down Expand Up @@ -178,6 +190,16 @@
"when": "activeWebviewPanelId == 'fppDiagrams'",
"group": "navigation"
},
{
"command": "fpp.diagram.toggle-collapsed-subtopologies",
"when": "activeWebviewPanelId == 'fppDiagrams'",
"group": "navigation"
},
{
"command": "fpp.diagram.toggle-bus-wires",
"when": "activeWebviewPanelId == 'fppDiagrams'",
"group": "navigation"
},
{
"command": "fpp.stateMachine.fit",
"when": "activeWebviewPanelId == 'fppStateMachineDiagram'",
Expand Down
10 changes: 10 additions & 0 deletions editors/code/src/diagram/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ export function registerDiagramSupport(
!webviewPanelManager.diagramConfig.hideUnusedPorts;
void webviewPanelManager.updateDiagram();
}),
vscode.commands.registerCommand("fpp.diagram.toggle-collapsed-subtopologies", () => {
webviewPanelManager.diagramConfig.collapseSubtopologies =
!webviewPanelManager.diagramConfig.collapseSubtopologies;
void webviewPanelManager.updateDiagram();
}),
vscode.commands.registerCommand("fpp.diagram.toggle-bus-wires", () => {
webviewPanelManager.diagramConfig.bundleEdges =
!webviewPanelManager.diagramConfig.bundleEdges;
void webviewPanelManager.updateDiagram();
}),
vscode.commands.registerCommand("fpp.stateMachine.fit", () => mermaidPanel.fit()),
vscode.commands.registerCommand("fpp.stateMachine.export", () => mermaidPanel.export()),
vscode.commands.registerCommand("fpp.stateMachine.view-source", () => mermaidPanel.viewSource()),
Expand Down
23 changes: 22 additions & 1 deletion editors/code/src/diagram/layout-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LayoutOptions } from "elkjs";
import { DefaultLayoutConfigurator } from "sprotty-elk";
import { SGraph, SEdge, SNode, SLabel } from 'sprotty-protocol';
import { SModelIndex } from "sprotty-protocol";
import { PortSNode } from "../../common/models";
import { BoundarySNode, PortSNode } from "../../common/models";

/**
* The kinds of diagram the extension can request from the language server.
Expand All @@ -21,6 +21,8 @@ export class FppDiagramConfig extends DefaultLayoutConfigurator {

// Stateful diagram options
public hideUnusedPorts = true; // By default, hide unused ports.
public collapseSubtopologies = false; // By default, flatten imported topologies into the diagram.
public bundleEdges = true; // By default, draw parallel wires as one counted bus.
public currentDiagramType: DiagramType | undefined; // Current diagram type
public fullyQualifiedName: string = ""; // Fully qualified name of the element currently displayed

Expand Down Expand Up @@ -57,8 +59,13 @@ export class FppDiagramConfig extends DefaultLayoutConfigurator {
'elk.layered.selfLoopDistribution': 'EQUALLY',
};
}
// Boundary ports are pinned to the first/last layer, which only lines
// them up along the diagram's edges if ELK lays the graph out as one
// piece rather than packing each connected component separately.
const hasBoundary = sgraph.children.some(c => c.type === 'node:boundary');
return {
'elk.algorithm': 'layered',
...(hasBoundary ? { 'elk.separateConnectedComponents': 'false' } : {}),
// Apply some spacing at the graph level to ensure the layered algorithm picks it up.
'elk.spacing.labelPortHorizontal': '5',
'elk.spacing.portPort': '10',
Expand Down Expand Up @@ -103,11 +110,25 @@ export class FppDiagramConfig extends DefaultLayoutConfigurator {
'elk.padding': '[top=8,left=10,bottom=8,right=10]',
};
}
if (snode.type === 'node:boundary') {
// A boundary port of the diagrammed topology: pin inputs to the first
// layer and outputs to the last so they sit on the diagram's edge.
const isOutput = (snode as BoundarySNode).isOutput;
return {
'elk.layered.layering.layerConstraint': isOutput ? 'LAST_SEPARATE' : 'FIRST_SEPARATE',
'elk.nodeLabels.placement': 'INSIDE, H_CENTER, V_CENTER',
'elk.nodeSize.constraints': 'NODE_LABELS, MINIMUM_SIZE',
'elk.nodeSize.minimum': '(40, 22)',
'elk.padding': '[top=4,left=14,bottom=4,right=14]',
};
}
return {
"elk.nodeLabels.placement": "INSIDE, H_CENTER, V_CENTER",
"elk.portLabels.nextToPortIfPossible": 'true',
'elk.portConstraints': 'FIXED_SIDE', // So that elk.port.side can take effect.
"elk.nodeSize.constraints": "PORTS, PORT_LABELS, NODE_LABELS, MINIMUM_SIZE",
// Collapsed topologies stand in for several components; keep them boxy.
...(snode.type === 'node:topology' ? { 'elk.nodeSize.minimum': '(180, 90)' } : {}),
};
}

Expand Down
18 changes: 17 additions & 1 deletion editors/code/src/diagram/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
* rendering) is unchanged.
*/
import { createWebviewPanel, SprottyDiagramIdentifier, WebviewEndpoint, WebviewPanelManager, WebviewPanelManagerOptions } from "sprotty-vscode";
import { RequestModelAction, ComputedBoundsAction, UpdateModelAction, FitToScreenAction, SGraph, RequestBoundsAction, applyBounds, SelectAllAction } from 'sprotty-protocol';
import { RequestModelAction, ComputedBoundsAction, UpdateModelAction, FitToScreenAction, SGraph, RequestBoundsAction, applyBounds, SelectAllAction, OpenAction } from 'sprotty-protocol';
import * as vscode from "vscode";
import { LanguageClient } from "vscode-languageclient/node";
import { ElkLayoutEngine } from "sprotty-elk/lib/elk-layout";
import ELK from 'elkjs/lib/elk-api.js';
import { FppLayoutEngine } from "./layout";
import { FppDiagramConfig, DiagramType } from "./layout-config";
import * as lsp_ext from "../lsp_ext";
import { TopologySNode } from "../../common/models";

/** A provider of the current language client, since the client is recreated on restart. */
export type ClientProvider = () => LanguageClient | undefined;
Expand Down Expand Up @@ -76,6 +77,8 @@ export class FppWebviewPanelManager extends WebviewPanelManager {
kind: kind as unknown as lsp_ext.DiagramKind,
name: this.diagramConfig.fullyQualifiedName,
hideUnusedPorts: this.diagramConfig.hideUnusedPorts,
topologyView: this.diagramConfig.collapseSubtopologies ? "collapsed" : "flattened",
bundleEdges: this.diagramConfig.bundleEdges,
});
return model as SGraph;
} catch (e) {
Expand All @@ -91,6 +94,7 @@ export class FppWebviewPanelManager extends WebviewPanelManager {
const activeWebview = super.createEndpoint(identifier);
this.addRequestModelHandler(activeWebview);
this.addComputedBoundsHandler(activeWebview);
this.addOpenHandler(activeWebview);
return activeWebview;
}

Expand Down Expand Up @@ -155,6 +159,18 @@ export class FppWebviewPanelManager extends WebviewPanelManager {
endpoint.addActionHandler(ComputedBoundsAction.KIND, handler);
}

/** Double-clicking a collapsed topology node (`OpenAction`) shows that topology's diagram in the same panel. */
protected addOpenHandler(endpoint: WebviewEndpoint) {
const handler = async (action: OpenAction) => {
const element = this.sGraph?.children.find(c => c.id === action.elementId);
if (element?.type !== 'node:topology') {
return;
}
await this.displayDiagram(DiagramType.topology, (element as TopologySNode).qualifiedName);
};
endpoint.addActionHandler(OpenAction.KIND, handler);
}

/**************************************************************************/
/* Handlers for sending messages to webview upon user's actions in editor */
/**************************************************************************/
Expand Down
12 changes: 12 additions & 0 deletions editors/code/src/lsp_ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ export type DiagramParams = {
* full flattened executed action sequence. Defaults to `"uml"` when omitted.
*/
transitionActionMode?: "uml" | "flattened",
/**
* How imported topologies are drawn (topology and connection-group diagrams
* only). `"flattened"` makes every component instance a node; `"collapsed"`
* draws each directly imported topology as one node with its declared
* ports. Defaults to `"flattened"` when omitted.
*/
topologyView?: "flattened" | "collapsed",
/**
* When true, parallel wires between the same two elements are bundled into
* one bus edge carrying a wire count. Defaults to false when omitted.
*/
bundleEdges?: boolean,
};

/**
Expand Down
47 changes: 47 additions & 0 deletions editors/code/webview/css/diagram.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,53 @@ tspan {
stroke: #fe92a8;
}

/* A collapsed imported topology: dashed outline marks a boundary, not a component. */
.topology-node {
fill: var(--vscode-editor-inactiveSelectionBackground);
stroke: var(--vscode-foreground);
stroke-width: 1.5;
stroke-dasharray: 8 4;
}

/* A declared port of the diagrammed topology, on the diagram boundary. */
.boundary-port {
stroke: var(--vscode-foreground);
stroke-width: 1;
}

.boundary-input {
fill: var(--vscode-charts-blue);
}

.boundary-output {
fill: var(--vscode-charts-orange);
}

/* An implicit connection: it crosses a collapsed topology's boundary rather than
ending on a rendered port. */
.sprotty-edge path.edge-implicit {
stroke-dasharray: 5 4;
opacity: 0.85;
}

/* A bus: several parallel wires drawn as one fat wire with a slash and a count. */
.sprotty-edge path.edge-bus {
stroke-width: 3px;
}

.sprotty-edge path.edge-bus.edge-implicit {
stroke-dasharray: 8 5;
}

.sprotty-edge .edge-bus-slash {
stroke-width: 1.5px;
}

/* A collapsed topology opens on double-click. */
.topology-node {
cursor: pointer;
}

.port-sync {
/* Red expresses the notion of "caution", because the component is synchronous. */
fill: #fe92a8;
Expand Down
8 changes: 6 additions & 2 deletions editors/code/webview/src/di.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ import 'sprotty/css/sprotty.css';
import '../css/diagram.css';

import { Container, ContainerModule } from 'inversify';
import { boundsFeature, configureModelElement, configureViewerOptions, editFeature, layoutableChildFeature, loadDefaultModules, moveFeature, overrideViewerOptions, SEdgeImpl, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SNodeImpl, SPortImpl, SRoutingHandleImpl, SRoutingHandleView } from 'sprotty';
import { ArrowEdgeView, ChoiceNodeView, ComponentNodeView, InitialNodeView, MultiLineLabelView, RectanglePortView, StateNodeView } from './views';
import { boundsFeature, configureModelElement, configureViewerOptions, editFeature, layoutableChildFeature, loadDefaultModules, moveFeature, openFeature, overrideViewerOptions, SEdgeImpl, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SNodeImpl, SPortImpl, SRoutingHandleImpl, SRoutingHandleView } from 'sprotty';
import { ArrowEdgeView, BoundaryNodeView, ChoiceNodeView, ComponentNodeView, InitialNodeView, MultiLineLabelView, RectanglePortView, StateNodeView, TopologyNodeView } from './views';

const myModule = new ContainerModule((bind, unbind, isBound, rebind) => {
const context = { bind, unbind, isBound, rebind };
configureModelElement(context, 'graph', SGraphImpl, SGraphView);

// Component / topology (port-based) diagrams.
configureModelElement(context, 'node:component', SNodeImpl, ComponentNodeView, { enable: [layoutableChildFeature], disable: [moveFeature] });
// Collapsed topology view: imported topologies as one node (double-click opens it), own ports on the boundary.
configureModelElement(context, 'node:topology', SNodeImpl, TopologyNodeView, { enable: [layoutableChildFeature, openFeature], disable: [moveFeature] });
configureModelElement(context, 'node:boundary', SNodeImpl, BoundaryNodeView, { enable: [layoutableChildFeature], disable: [moveFeature] });
configureModelElement(context, 'label:node:component', SLabelImpl, SLabelView);
configureModelElement(context, 'port', SPortImpl, RectanglePortView, { disable: [boundsFeature] }); // By default, boundsFeature is enabled for SPort. We want fixed-size ports so this feature needs to be disabled.
configureModelElement(context, 'label:port', SLabelImpl, SLabelView);
configureModelElement(context, 'edge', SEdgeImpl, ArrowEdgeView, { disable: [editFeature] });
configureModelElement(context, 'label:edge', SLabelImpl, SLabelView);

// State machine diagrams.
configureModelElement(context, 'node:state', SNodeImpl, StateNodeView, { enable: [layoutableChildFeature], disable: [moveFeature] });
Expand Down
Loading
Loading