Skip to content
Merged
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
10 changes: 10 additions & 0 deletions extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ RL1 {
}
```

#### Highlighting
Strings in `.stpa` files support inline markers that highlight specific parts of the text. These markers allow formatting such as italic, bold, underline, and strikethrough.
By default, markers are hidden during normal reading and only appear when the cursor is placed within the highlighted section. One can switch to editor mode using the toggle in the top-right corner of the interface. In this mode, all markers are always visible, regardless of cursor position.
If a marker character needs to be displayed literally (without it being interpreted as formatting), one can escape it with a backslash (`\`).
Supported markers are:
- `*<text>*` for italic
- `**<text>**` for bold
- `_<text>_` for underline
- `~<text>~` for strikethrough

### FTA

The file in which the analysis is done must have `.fta` as its file ending. Each component type in the fault tree has its own section. Components are stated with an ID and a descriptions. Afterwards, Conditions for Inhibit gates can be stated. The actual fault tree is than stated with a top event and the gates leading to this event. The gates can be annotated with a description as well. The gate types Or, And, Inhibit, and n/k are supported.
Expand Down
22 changes: 22 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@
"icon": "$(table)",
"category": "Context Table"
},
{
"command": "pasta.editor.editorMode",
"title": "Switch to read mode",
"icon": "$(edit)",
"category": "navigation"
},
{
"command": "pasta.editor.watchMode",
"title": "Switch to editor mode",
"icon": "$(book)",
"category": "navigation"
},
{
"command": "pasta.diagram.fit",
"title": "Fit to Screen",
Expand Down Expand Up @@ -443,6 +455,16 @@
"command": "pasta.contextTable.open",
"when": "editorLangId == 'stpa'",
"group": "navigation"
},
{
"command": "pasta.editor.editorMode",
"when": "pasta.highlightingActive && editorLangId == 'stpa' && pasta.markersVisible",
"group": "navigation"
},
{
"command": "pasta.editor.watchMode",
"when": "pasta.highlightingActive && editorLangId == 'stpa' && !pasta.markersVisible",
"group": "navigation"
}
],
"explorer/context": [
Expand Down
2 changes: 0 additions & 2 deletions extension/src-context-table/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ import {
} from "./utils";
import { ContextTableControlAction, ContextTableRule, ContextTableSystemVariables, ContextTableVariable, ContextTableVariableValues, Row, Type, ContextCell } from './utils-classes';

declare const vscode: { postMessage(message: any): void };


export class ContextTable extends Table {
/** Ids for the html elements */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from "./stpa-model.js";
import { StpaSynthesisOptions } from "./stpa-synthesis-options.js";
import { getCommonAncestor, sortPorts } from "./utils.js";
import { getRawStringInnerFromCst, stripInlineMarkers } from "../../utils.js";

/**
* Creates the control structure diagram for the given {@code controlStructure}.
Expand Down Expand Up @@ -64,7 +65,7 @@ export function createControlStructure(
// children (nodes and edges) of the control structure
const CSChildren = [
...csNodes,
...generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache, missingReferences, addMissing, missingFeedback),
...generateVerticalCSEdges(controlStructure.nodes, idToSNode, options, idCache, missingReferences, addMissing, missingFeedback),
//...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, idCache)
];
// sort the ports in order to group edges based on the nodes they are connected to
Expand Down Expand Up @@ -186,6 +187,8 @@ export function createProcessModelNode(variables: Variable[], idCache: IdCache<A
/**
* Creates the edges for the control structure.
* @param nodes The nodes of the control structure.
* @param idToSNode The map of IDs to SNodes.
* @param options The synthesis options of the STPA model.
* @param idCache The ID cache of the STPA model.
* @param missingReferences The Map of elements with missing references with their warning messages.
* @param addMissing Whether missing feedback should be added to the control structure.
Expand All @@ -195,6 +198,7 @@ export function createProcessModelNode(variables: Variable[], idCache: IdCache<A
export function generateVerticalCSEdges(
nodes: Node[],
idToSNode: Map<string, SNode>,
options: StpaSynthesisOptions,
idCache: IdCache<AstNode>,
missingReferences: Map<string, string[]>,
addMissing: boolean,
Expand All @@ -210,23 +214,24 @@ export function generateVerticalCSEdges(
node.actions,
EdgeType.CONTROL_ACTION,
idToSNode,
options,
idCache,
missingReferences,
addMissing,
missingFeedback
)
);
// create edges representing feedback
edges.push(...translateCommandsToEdges(node, node.feedbacks, EdgeType.FEEDBACK, idToSNode, idCache, missingReferences, false));
edges.push(...translateCommandsToEdges(node, node.feedbacks, EdgeType.FEEDBACK, idToSNode, options, idCache, missingReferences, false));
// create edges representing the other inputs
edges.push(...translateIOToEdgeAndNode(node.inputs, node, EdgeType.INPUT, idToSNode, idCache));
edges.push(...translateIOToEdgeAndNode(node.inputs, node, EdgeType.INPUT, idToSNode, options, idCache));
// create edges representing the other outputs
edges.push(...translateIOToEdgeAndNode(node.outputs, node, EdgeType.OUTPUT, idToSNode, idCache));
edges.push(...translateIOToEdgeAndNode(node.outputs, node, EdgeType.OUTPUT, idToSNode, options, idCache));

// add edges of the children of the node if the node is expanded
if (expansionState.get(node.name) === true) {
// create edges for children and add the ones that must be added at the top level
edges.push(...generateVerticalCSEdges(node.children, idToSNode, idCache, missingReferences, addMissing, missingFeedback));
edges.push(...generateVerticalCSEdges(node.children, idToSNode, options, idCache, missingReferences, addMissing, missingFeedback));
}
}
return edges;
Expand All @@ -235,6 +240,7 @@ export function generateVerticalCSEdges(
/**
* Translates the commands (control action or feedback) of a node to (intermediate) edges and adds them to the correct nodes.
* @param node The node of the commands.
* @param options The synthesis options of the STPA model.
* @param commands The control actions or feedback of a node.
* @param edgeType The type of the edge (control action or feedback).
* @param idToSNode The map of IDs to SNodes.
Expand All @@ -249,6 +255,7 @@ export function translateCommandsToEdges(
commands: VerticalEdge[],
edgeType: EdgeType,
idToSNode: Map<string, SNode>,
options: StpaSynthesisOptions,
idCache: IdCache<AstNode>,
missingReferences: Map<string, string[]>,
addMissing: boolean,
Expand All @@ -267,7 +274,17 @@ export function translateCommandsToEdges(

if (target) {
// multiple commands to same target is represented by one edge -> combine labels to one
const label = edge.comms.map(com => com.label);
const label: string[] = [];
for (let i = 0; i < edge.comms.length; i++) {
const com = edge.comms[i];

let raw = getRawStringInnerFromCst(com) ?? com.label ?? ""; // get the raw string from the CST so that backslashes remain
// Strip markers when showInlineMarkers is false
if (!options.getShowInlineMarkers()) {
raw = stripInlineMarkers(raw);
}
label.push(raw);
}
const isReferenceMissing: [boolean, string[]][] = controlActions.map(ca => [missingReferences.has(ca), missingReferences.get(ca) ?? []]);

createEdgeForCommand(source, target, edgeId, edgeType, label, idToSNode, idCache, edges, controlActions, isReferenceMissing);
Expand Down Expand Up @@ -363,6 +380,8 @@ export function createEdgeForCommand(
* @param io The inputs or outputs of a node.
* @param node The node of the inputs or outputs.
* @param edgetype The type of the edge (input or output).
* @param idToSNode The map of IDs to SNodes.
* @param options The synthesis options of the STPA model.
* @param idCache The ID cache of the STPA model.
* @returns a list of edges representing the inputs or outputs that should be added at the top level.
*/
Expand All @@ -371,6 +390,7 @@ export function translateIOToEdgeAndNode(
node: Node,
edgetype: EdgeType,
idToSNode: Map<string, SNode>,
options: StpaSynthesisOptions,
idCache: IdCache<AstNode>
): (CSNode | CSEdge)[] {
if (io.length !== 0) {
Expand All @@ -379,8 +399,14 @@ export function translateIOToEdgeAndNode(
// create the label of the edge
const label: string[] = [];
for (let i = 0; i < io.length; i++) {
const command = io[i];
label.push(command.label);
const com = io[i];

let raw = getRawStringInnerFromCst(com) ?? com.label ?? ""; // get the raw string from the CST so that backslashes remain
// Strip markers when showInlineMarkers is false
if (!options.getShowInlineMarkers()) {
raw = stripInlineMarkers(raw);
}
label.push(raw);
}

let graphComponents: (CSNode | CSEdge)[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
getTargets,
setLevelsForSTPANodes,
} from "./utils.js";
import { getRawStringInnerFromCst, stripInlineMarkers } from "../../utils.js";

/**
* Creates the relationship graph for the STPA model.
Expand Down Expand Up @@ -370,7 +371,23 @@ export function generateSTPANode(
highlightedIDs?: string[],
): STPANode {
const nodeId = idCache.uniqueId(node.name.replace(/[.]/g, "_"), node);
const showHighlights = options.getShowDescriptionsHighlights();
const showHighlights = options.getShowDescriptionsHighlights();

// Get the label text, ensuring inline markers are stripped
const getRawLabel = (): string => {
const raw = getRawStringInnerFromCst(node); // get the raw string from the CST so that backslashes remain
if (!raw) {
return "";
}

return options.getShowInlineMarkers() ? raw : stripInlineMarkers(raw);
};

const label = isContext(node)
? createUCAContextDescription(node)
: getRawLabel();


// determines the hierarchy level for subcomponents. For other components the value is 0.
let lvl = 0;
let container = node.$container;
Expand All @@ -385,7 +402,7 @@ export function generateSTPANode(
node.name,
options,
idCache,
isContext(node) ? createUCAContextDescription(node) : node.description
label
);
// if the hierarchy option is true, the subcomponents are added as children to the parent
if (options.getHierarchy() && isHazard(node) && node.subComponents.length !== 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const showSafetyConstraintsID = "showSafetyConstraints";
const filterCategoryID = "filterCategory";
const labelCategoryID = "labelCategory";
const highlightsID = "highlights";
const showInlineMarkersID = "showInlineMarkers";

const showControlStructureID = "showControlStructure";
const showProcessModelsID = "showProcessModels";
Expand Down Expand Up @@ -531,6 +532,22 @@ const showDescriptionsHighlightsOption: ValuedSynthesisOption = {
currentValue: false,
};

/**
* Boolean option to toggle the visualization of inline highlights in the diagram.
*/
const showInlineMarkersOption: ValuedSynthesisOption = {
synthesisOption: {
id: showInlineMarkersID,
name: "Inline Markers",
type: TransformationOptionType.CHECK,
initialValue: false,
currentValue: false,
values: [true, false],
category: filterCategory,
},
currentValue: false,
};


/**
* Boolean option to toggle the visualization of missing feedback in the control structure.
Expand Down Expand Up @@ -598,6 +615,7 @@ export class StpaSynthesisOptions extends SynthesisOptions {
showDescriptionsScenariosOption,
showDescriptionsSafetyConstraintsOption,
showDescriptionsHighlightsOption,
showInlineMarkersOption,
groupingOfUCAs,
useHyperedgesOption,
hierarchicalGraphOption,
Expand Down Expand Up @@ -790,6 +808,14 @@ export class StpaSynthesisOptions extends SynthesisOptions {
return this.getOption(showMissingReferencesID)?.currentValue;
}

getShowInlineMarkers(): boolean {
return this.getOption(showInlineMarkersID)?.currentValue;
}

setShowInlineMarkers(value: boolean): void {
this.setOption(showInlineMarkersID, value);
}

setUseHyperEdges(value: boolean): void {
this.setOption(useHyperedgesID, value);
}
Expand Down
1 change: 1 addition & 0 deletions extension/src-language-server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "../",
"outDir": "../pack/src-language-server",
"target": "ES2017",
"lib": [
Expand Down
84 changes: 84 additions & 0 deletions extension/src-language-server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,90 @@ import { SLabel } from "sprotty-protocol";
import { URI } from "vscode-uri";
import { StpaValidator } from "./stpa/services/stpa-validator.js";
import { labelManagementValue } from "./synthesis-options.js";
import { MarkerDefinition, INLINE_MARKER_DEFINITIONS } from "../src/utils-classes.js";
Comment thread
Tokessa marked this conversation as resolved.

function escapeForRegexLiteral(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
* Removes all markdown-style markers that would be hidden by the InlineMarkdownDecorator.
* @param input The input string with markdown markers
* @param markerConfigs Optional marker configurations. If not provided, uses the default configs.
* @returns The string with hidden markers and escape backslashes removed
*/
export function stripInlineMarkers(
input: string,
markerConfigs: readonly MarkerDefinition[] = INLINE_MARKER_DEFINITIONS
): string {
let result = input;

// Remove marker pairs (Only matches markers that aren't escaped)
for (const config of markerConfigs) {
const escapedMarker = escapeForRegexLiteral(config.marker);
const regex = new RegExp(`(?<!\\\\)(${escapedMarker}+)(.+?)(?<!\\\\)\\1(?!${escapedMarker})`, "g");

result = result.replace(regex, (match, markers, content) => {
return content;
});
}

// Remove all escape backslashes before markers and backslashes
const markers = new Set<string>(markerConfigs.map(c => c.marker));
markers.add("\\");

const alternation = Array.from(markers).map(escapeForRegexLiteral).join("|");
if (alternation) {
const escapeRegex = new RegExp(`\\\\(${alternation})`, "g");

result = result.replace(escapeRegex, "$1");
}
return result;
}

/**
* Return the raw inner content of the first STRING token found for the given AST node.
* @param node The AST node to extract the string from.
* @returns the inner text (without surrounding quotes) or undefined when not available.
*/
export function getRawStringInnerFromCst(node: any): string | undefined {
if (!node || !node.$cstNode) {
return undefined;
}

const cst = node.$cstNode;

// depth-first search for a leaf token that contains the quoted text
try {
const stack: any[] = [cst];
while (stack.length > 0) {
const n = stack.pop();
if (!n) {
continue;
}

// If composite node with children, push them
if (Array.isArray((n as any).children) && (n as any).children.length > 0) {
for (let i = (n as any).children.length - 1; i >= 0; --i) {
stack.push((n as any).children[i]);
}
continue;
}

const img = (n as any).text ?? undefined;
if (typeof img === "string" && img.length >= 2) {
const first = img[0];
const last = img[img.length - 1];
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
// return without surrounding quotes
return img.substring(1, img.length - 1);
}
}
}
} catch (e) {
console.error("Error while extracting raw string inner content:", e);
}
}

/**
* Determines the model for {@code uri}.
Expand Down
Loading
Loading