From f402a01dfcb1bb3ca6a8a3944bdd8d3fb24f6edb Mon Sep 17 00:00:00 2001 From: Alexander Vogt Date: Fri, 15 Aug 2025 08:28:51 +0200 Subject: [PATCH] highlight ports with invalid behaviour --- .../dfdElements/AssignmentLanguage.ts | 46 ++++++++----------- .../dfdElements/behaviorRefactorer.ts | 7 +-- src/features/dfdElements/elementStyles.css | 4 +- src/features/dfdElements/outputPortEditUi.ts | 2 +- src/features/dfdElements/ports.tsx | 33 +++++++++++-- src/index.ts | 2 + 6 files changed, 57 insertions(+), 37 deletions(-) diff --git a/src/features/dfdElements/AssignmentLanguage.ts b/src/features/dfdElements/AssignmentLanguage.ts index be0c723..6751570 100644 --- a/src/features/dfdElements/AssignmentLanguage.ts +++ b/src/features/dfdElements/AssignmentLanguage.ts @@ -7,7 +7,7 @@ import { Token, WordCompletion, } from "../constraintMenu/AutoCompletion"; -import { SModelElementImpl, SModelRootImpl, SParentElementImpl, SPortImpl } from "sprotty"; +import { SModelElementImpl, SParentElementImpl, SPortImpl } from "sprotty"; import { LabelTypeRegistry } from "../labels/labelTypeRegistry"; import { DfdNodeImpl } from "./nodes"; @@ -148,14 +148,14 @@ export class ReplaceAutoCompleteTree extends AutoCompleteTree { export namespace TreeBuilder { export function buildTree( - model: SModelRootImpl, labelTypeRegistry: LabelTypeRegistry, + port?: SPortImpl, ): AutoCompleteNode[] { return [ buildSetOrUnsetStatement(labelTypeRegistry, "set"), buildSetOrUnsetStatement(labelTypeRegistry, "unset"), - buildForwardStatement(model), - buildAssignStatement(labelTypeRegistry, model), + buildForwardStatement(port), + buildAssignStatement(labelTypeRegistry, port), ]; } @@ -173,9 +173,9 @@ export namespace TreeBuilder { }; } - function buildForwardStatement(model: SModelRootImpl) { + function buildForwardStatement(port?: SPortImpl) { const inputNode: AutoCompleteNode = { - word: new InputListWord(model), + word: new InputListWord(port), children: [], }; return { @@ -186,20 +186,20 @@ export namespace TreeBuilder { function buildAssignStatement( labelTypeRegistry: LabelTypeRegistry, - model: SModelRootImpl, + port?: SPortImpl, ): AutoCompleteNode { const fromNode: AutoCompleteNode = { word: new ConstantWord("from"), children: [ { - word: new InputListWord(model), + word: new InputListWord(port), children: [], }, ], }; const ifNode: AutoCompleteNode = { word: new ConstantWord("if"), - children: buildCondition(model, labelTypeRegistry, fromNode), + children: buildCondition(labelTypeRegistry, fromNode, port), }; return { word: new ConstantWord("assign"), @@ -212,7 +212,7 @@ export namespace TreeBuilder { }; } - function buildCondition(model: SModelRootImpl, labelTypeRegistry: LabelTypeRegistry, nextNode: AutoCompleteNode) { + function buildCondition(labelTypeRegistry: LabelTypeRegistry, nextNode: AutoCompleteNode, port?: SPortImpl) { const connectors: AutoCompleteNode[] = ["&&", "||"].map((o) => ({ word: new ConstantWord(o), children: [], @@ -221,7 +221,7 @@ export namespace TreeBuilder { const expressors: AutoCompleteNode[] = [ new ConstantWord("TRUE"), new ConstantWord("FALSE"), - new InputLabelWord(model, labelTypeRegistry), + new InputLabelWord(labelTypeRegistry, port), ].map((e) => ({ word: e, children: [...connectors, nextNode], @@ -236,20 +236,14 @@ export namespace TreeBuilder { } abstract class InputAwareWord { - constructor(private model: SModelRootImpl) {} + constructor(private port?: SPortImpl) {} protected getAvailableInputs(): string[] { - const selectedPorts = this.getSelectedPorts(this.model); - if (selectedPorts.length === 0) { - return []; - } - return selectedPorts.flatMap((port) => { - const parent = port.parent; - if (!(parent instanceof DfdNodeImpl)) { - return []; - } + const parent = this.port?.parent; + if (parent && parent instanceof DfdNodeImpl) { return parent.getAvailableInputs().filter((input) => input !== undefined) as string[]; - }); + } + return []; } private getSelectedPorts(node: SModelElementImpl): SPortImpl[] { @@ -386,8 +380,8 @@ class InputWord extends InputAwareWord implements ReplaceableAbstractWord { class InputListWord implements ReplaceableAbstractWord { private inputWord: InputWord; - constructor(model: SModelRootImpl) { - this.inputWord = new InputWord(model); + constructor(port?: SPortImpl) { + this.inputWord = new InputWord(port); } completionOptions(word: string): WordCompletion[] { @@ -426,8 +420,8 @@ class InputLabelWord implements ReplaceableAbstractWord { private inputWord: InputWord; private labelWord: LabelWord; - constructor(model: SModelRootImpl, labelTypeRegistry: LabelTypeRegistry) { - this.inputWord = new InputWord(model); + constructor(labelTypeRegistry: LabelTypeRegistry, port?: SPortImpl) { + this.inputWord = new InputWord(port); this.labelWord = new LabelWord(labelTypeRegistry); } diff --git a/src/features/dfdElements/behaviorRefactorer.ts b/src/features/dfdElements/behaviorRefactorer.ts index 48025aa..a957c7f 100644 --- a/src/features/dfdElements/behaviorRefactorer.ts +++ b/src/features/dfdElements/behaviorRefactorer.ts @@ -10,7 +10,6 @@ import { SEdgeImpl, SLabelImpl, SModelElementImpl, - SModelRootImpl, SParentElementImpl, TYPES, } from "sprotty"; @@ -87,8 +86,8 @@ export class DFDBehaviorRefactorer { this.logger.log(this, "Changed labels", changedLabels); const model = await this.commandStack.executeAll([]); - const tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(model, this.registry)); this.traverseDfdOutputPorts(model, (port) => { + const tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(this.registry, port)); this.renameLabelsForPort(port, changedLabels, tree); }); @@ -118,7 +117,6 @@ export class DFDBehaviorRefactorer { port: DfdInputPortImpl, oldLabelText: string, newLabelText: string, - root: SModelRootImpl, ): Map { label.text = oldLabelText; const oldInputName = port.getName(); @@ -131,7 +129,7 @@ export class DFDBehaviorRefactorer { return behaviorChanges; } - const tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(root, this.registry)); + const tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(this.registry, port)); node.children.forEach((child) => { if (!(child instanceof DfdOutputPortImpl)) { @@ -209,7 +207,6 @@ export class RefactorInputNameInDFDBehaviorCommand extends Command { port, oldInputName, newInputName, - context.root, ); behaviorChanges.forEach((updatedBehavior, id) => { const port = context.root.index.getById(id); diff --git a/src/features/dfdElements/elementStyles.css b/src/features/dfdElements/elementStyles.css index d472ff5..91c2d74 100644 --- a/src/features/dfdElements/elementStyles.css +++ b/src/features/dfdElements/elementStyles.css @@ -72,8 +72,8 @@ /* Ports */ .sprotty-port rect { - stroke: var(--color-foreground); - fill: color-mix(in srgb, var(--color-primary), var(--color-background) 25%); + stroke: var(--port-border, var(--color-foreground)); + fill: color-mix(in srgb, var(--port-color, var(--color-primary)), var(--color-background) 25%); stroke-width: 0.5; } diff --git a/src/features/dfdElements/outputPortEditUi.ts b/src/features/dfdElements/outputPortEditUi.ts index 8fc4ea4..87f6c4c 100644 --- a/src/features/dfdElements/outputPortEditUi.ts +++ b/src/features/dfdElements/outputPortEditUi.ts @@ -318,7 +318,7 @@ export class OutputPortEditUI extends AbstractUIExtension implements Switchable readOnly: this.editorModeController?.isReadOnly() ?? false, }); - this.tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(root, this.labelTypeRegistry)); + this.tree = new ReplaceAutoCompleteTree(TreeBuilder.buildTree(this.labelTypeRegistry, this.port)); // Validation of loaded behavior text. this.validateBehavior(); diff --git a/src/features/dfdElements/ports.tsx b/src/features/dfdElements/ports.tsx index 1ae8dc8..369b816 100644 --- a/src/features/dfdElements/ports.tsx +++ b/src/features/dfdElements/ports.tsx @@ -13,8 +13,11 @@ import { } from "sprotty"; import { Bounds, SPort } from "sprotty-protocol"; import { injectable } from "inversify"; -import { VNode } from "snabbdom"; +import { VNode, VNodeStyle } from "snabbdom"; import { ArrowEdgeImpl } from "./edges"; +import { AutoCompleteTree } from "../constraintMenu/AutoCompletion"; +import { TreeBuilder } from "./AssignmentLanguage"; +import { labelTypeRegistry } from "../.."; const defaultPortFeatures = [...SPortImpl.DEFAULT_FEATURES, moveFeature, deletableFeature]; const portSize = 7; @@ -94,6 +97,12 @@ export class DfdOutputPortImpl extends SPortImpl { static readonly DEFAULT_FEATURES = [...defaultPortFeatures, withEditLabelFeature]; behavior: string = ""; + private tree: AutoCompleteTree; + + constructor() { + super(); + this.tree = new AutoCompleteTree(TreeBuilder.buildTree(labelTypeRegistry, this)); + } override get bounds(): Bounds { return { @@ -117,11 +126,29 @@ export class DfdOutputPortImpl extends SPortImpl { // Only allow edges from this port outwards return role === "source"; } + + /** + * Generates the per-node inline style object for the view. + */ + geViewStyleObject(): VNodeStyle { + const style: VNodeStyle = { + opacity: this.opacity.toString(), + }; + if (!labelTypeRegistry) return style; + const valid = this.tree.verify(this.behavior.split("\n")).length == 0; + + if (!valid) { + style["--port-border"] = "#ff0000"; + style["--port-color"] = "#ff6961"; + } + + return style; + } } @injectable() export class DfdOutputPortView extends ShapeView { - render(node: Readonly, context: RenderingContext): VNode | undefined { + render(node: Readonly, context: RenderingContext): VNode | undefined { if (!this.isVisible(node, context)) { return undefined; } @@ -129,7 +156,7 @@ export class DfdOutputPortView extends ShapeView { const { width, height } = node.bounds; return ( - + O diff --git a/src/index.ts b/src/index.ts index b770db3..0b285e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,7 @@ import { settingsModule } from "./features/settingsMenu/di.config"; import { LoadDiagramAction } from "./features/serialize/load"; import { commandPaletteModule } from "./features/commandPalette/di.config"; import { LoadingIndicator } from "./common/loadingIndicator"; +import { LabelTypeRegistry } from "./features/labels/labelTypeRegistry"; const container = new Container(); @@ -77,6 +78,7 @@ export function setModelFileName(name: string): void { export function getModelFileName(): string { return modelFileName; } +export const labelTypeRegistry = container.get(LabelTypeRegistry); export function setModelSource(file: File): void { modelSource