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
6 changes: 6 additions & 0 deletions client/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ vsc-extension-quickstart.md
# Build artifacts
out/**
node_modules/**
!node_modules/
!node_modules/@vscode/
!node_modules/@vscode/codicons/
!node_modules/@vscode/codicons/dist/
!node_modules/@vscode/codicons/dist/codicon.css
!node_modules/@vscode/codicons/dist/codicon.ttf
*.vsix

# Test files
Expand Down
12 changes: 12 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@vscode/codicons": "^0.0.45",
"ftp": "^0.3.10",
"get-port": "^5.1.1",
"jsonc-parser": "^0.4.2",
Expand Down
22 changes: 22 additions & 0 deletions client/src/webview/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const COPY_BUTTON_RESET_MS = 2000;

export async function copyToClipboard(button: HTMLButtonElement, text: string) {
const originalTitle = button.getAttribute('title');

try {
button.disabled = true;
await navigator.clipboard.writeText(text);
button.setAttribute('title', 'Copied!');
} catch (e) {
button.setAttribute('title', 'Copy failed');
} finally {
setTimeout(() => {
if (originalTitle !== null) {
button.setAttribute('title', originalTitle);
} else {
button.removeAttribute('title');
}
button.disabled = false;
}, COPY_BUTTON_RESET_MS);
}
}
33 changes: 8 additions & 25 deletions client/src/webview/diagram.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { LJStateMachine } from "../types/fsm";
import { copyToClipboard } from "./clipboard";

// constants
const MIN_ZOOM = 0.2;
const MAX_ZOOM = 5;
const ZOOM_BUTTON_FACTOR = 1.5;
const SCROLL_ZOOM_IN_FACTOR = 1.05;
const SCROLL_ZOOM_OUT_FACTOR = 0.95;
const COPY_TIMEOUT_MS = 2000;

// state variables
let zoomLevel = 1;
Expand Down Expand Up @@ -51,18 +51,15 @@ export function createMermaidDiagram(sm: LJStateMachine | undefined, orientation
// add transitions
transitionMap.forEach((labels, key) => {
const [from, to] = key.split('|');
const mergedLabel = labels.join(', ');
const mergedLabel = labels.join('<br/>');
lines.push(` ${from} --> ${to} : ${mergedLabel}`);
});

return lines.join('\n');
}

function getTransitionLabel(label: string, preCond?: string | null, postCond?: string | null, showConditions = false): string {
if (!showConditions) {
return escapeMermaidLabel(label);
}

if (!showConditions) return escapeMermaidLabel(label);
return [
getConditionLabel('pre', preCond),
escapeMermaidLabel(label),
Expand All @@ -75,9 +72,8 @@ function getInitialTransitionLabel(postCond?: string | null, showConditions = fa
}

function getConditionLabel(kind: 'pre' | 'post', cond?: string | null): string {
if (!cond) {
return '';
}
if (!cond) return '';

return `<span class="state-cond state-cond-${kind}">${escapeMermaidLabel(cond)}</span>`;
}

Expand All @@ -101,6 +97,8 @@ export async function renderMermaidDiagram(document: any, window: any) {
await mermaid.run({ nodes: mermaidElements });
applyTransform(document);
registerPanListeners(document);
const diagramContainer = document.querySelector('.diagram-container') as HTMLElement | null;
if (diagramContainer) diagramContainer.style.minHeight = '';
} catch (e) {
console.error('Failed to render Mermaid diagram:', e);
}
Expand Down Expand Up @@ -241,20 +239,5 @@ export function registerPanListeners(document: any) {
}

export async function copyDiagramToClipboard(target: any, diagram: string) {
const title = target.getAttribute('title');
try {
target.disabled = true;
await navigator.clipboard.writeText(diagram);
target.classList.add('copied');
target.setAttribute('title', 'Copied!');
} catch (e) {
target.setAttribute('title', 'Copy failed');
} finally {
// reset button after timeout
setTimeout(() => {
target.setAttribute('title', title);
target.classList.remove('copied');
target.disabled = false;
}, COPY_TIMEOUT_MS);
}
await copyToClipboard(target, diagram);
}
4 changes: 3 additions & 1 deletion client/src/webview/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function getHtml(webview: vscode.Webview, extensionUri: vscode.Uri): stri
const nonce = Date.now().toString();
const cspSource = webview.cspSource;
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, "media", "webview.js"));
const codiconsUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, "node_modules", "@vscode", "codicons", "dist", "codicon.css"));
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
Expand All @@ -21,8 +22,9 @@ export function getHtml(webview: vscode.Webview, extensionUri: vscode.Uri): stri
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src ${cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}' https://cdn.jsdelivr.net; connect-src https://cdn.jsdelivr.net;"
content="default-src 'none'; style-src ${cspSource} 'unsafe-inline'; font-src ${cspSource}; script-src 'nonce-${nonce}' https://cdn.jsdelivr.net; connect-src https://cdn.jsdelivr.net;"
>
<link href="${codiconsUri}" rel="stylesheet">
<style>${getStyles()}</style>
</head>
<body>
Expand Down
23 changes: 23 additions & 0 deletions client/src/webview/icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function renderCodicon(name: string, className = ""): string {
const classes = ["codicon", `codicon-${name}`, className].filter(Boolean).join(" ");
return `<span class="${classes}" aria-hidden="true"></span>`;
}

type CodiconButtonOptions = {
id?: string;
className?: string;
title: string;
ariaLabel?: string;
attributes?: string;
disabled?: boolean;
};

export function renderCodiconButton(iconName: string, options: CodiconButtonOptions): string {
const classes = ["icon-button", options.className].filter(Boolean).join(" ");
const id = options.id ? ` id="${options.id}"` : "";
const ariaLabel = ` aria-label="${options.ariaLabel ?? options.title}"`;
const attributes = options.attributes ? ` ${options.attributes}` : "";
const disabled = options.disabled ? " disabled" : "";

return `<button${id} class="${classes}" title="${options.title}"${ariaLabel}${attributes}${disabled} type="button">${renderCodicon(iconName)}</button>`;
}
29 changes: 19 additions & 10 deletions client/src/webview/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
root.addEventListener('click', (e: MouseEvent) => {
const target = e.target instanceof Element ? e.target : null;
if (!target) return;
const iconButton = target.closest?.('.icon-button');
if (iconButton && !(iconButton as HTMLButtonElement).disabled) {
iconButton.classList.remove('icon-button-pop');
void (iconButton as HTMLElement).offsetWidth;
iconButton.classList.add('icon-button-pop');
}

// context section toggle
const contextToggleButton = target.closest?.('.context-toggle-btn');
Expand All @@ -77,7 +83,8 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)

const icon = contextToggleButton.querySelector('.context-toggle-icon');
if (icon) {
icon.textContent = nextExpanded ? '▾' : '▸';
icon.classList.toggle('codicon-triangle-down', nextExpanded);
icon.classList.toggle('codicon-triangle-right', !nextExpanded);
}

return;
Expand Down Expand Up @@ -143,7 +150,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
}

// toggle diagram orientation
if (target.id === 'diagram-orientation-btn') {
if (target.closest?.('#diagram-orientation-btn')) {
e.stopPropagation();
diagramOrientation = diagramOrientation === "TB" ? "LR" : "TB";
resetZoom(document);
Expand All @@ -152,40 +159,42 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
}

// toggle diagram conditions
if (target.id === 'diagram-conditions-btn') {
const diagramConditionsButton = target.closest?.('#diagram-conditions-btn');
if (diagramConditionsButton) {
e.stopPropagation();
if ((target as HTMLButtonElement).disabled) return;
if ((diagramConditionsButton as HTMLButtonElement).disabled) return;
showDiagramConditions = !showDiagramConditions;
updateView();
return;
}

// zoom in
if (target.id === 'zoom-in-btn') {
if (target.closest?.('#zoom-in-btn')) {
e.stopPropagation();
zoomIn(document);
return;
}

// zoom out
if (target.id === 'zoom-out-btn') {
if (target.closest?.('#zoom-out-btn')) {
e.stopPropagation();
zoomOut(document);
return;
}

// reset zoom
if (target.id === 'zoom-reset-btn') {
if (target.closest?.('#zoom-reset-btn')) {
e.stopPropagation();
resetZoom(document);
return;
}

// copy diagram source
if (target.id === 'copy-diagram-btn') {
const copyDiagramButton = target.closest?.('#copy-diagram-btn');
if (copyDiagramButton) {
e.stopPropagation();
if (!currentDiagram) return
copyDiagramToClipboard(target, currentDiagram);
copyDiagramToClipboard(copyDiagramButton, currentDiagram);
return;
}

Expand Down Expand Up @@ -315,7 +324,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
case 'fsm': {
const diagram = createMermaidDiagram(stateMachine, diagramOrientation, showDiagramConditions);
currentDiagram = diagram;
root.innerHTML = renderStateMachineView(stateMachine, diagram, diagramOrientation, showDiagramConditions);
renderStateMachineView(root, stateMachine, diagram, diagramOrientation, showDiagramConditions);
if (stateMachine) renderMermaidDiagram(document, window);
break;
}
Expand Down
Loading