diff --git a/client/src/webview/diagram.ts b/client/src/webview/diagram.ts
index 4ecfdae..fd1a8e6 100644
--- a/client/src/webview/diagram.ts
+++ b/client/src/webview/diagram.ts
@@ -43,7 +43,7 @@ export function createMermaidDiagram(sm: LJStateMachine | undefined, orientation
sm.transitions.forEach(transition => {
const key = `${transition.from}|${transition.to}`;
if (!transitionMap.has(key)) transitionMap.set(key, []);
- transitionMap.get(key).push(transition.label);
+ transitionMap.get(key)?.push(transition.label);
});
// add transitions
diff --git a/client/src/webview/script.ts b/client/src/webview/script.ts
index a426f99..c71fb28 100644
--- a/client/src/webview/script.ts
+++ b/client/src/webview/script.ts
@@ -6,7 +6,7 @@ import type { LJDiagnostic, RefinementMismatchError } from "../types/diagnostics
import type { Range } from "../types/context";
import type { LJStateMachine } from "../types/fsm";
import type { NavTab } from "./views/sections";
-import { renderDiagnosticsView } from "./views/diagnostics/diagnostics";
+import { copyDiagnosticToClipboard, getDisplayDiagnostics, renderDiagnosticsView } from "./views/diagnostics/diagnostics";
import type { LJContext } from "../types/context";
import { ContextSectionState, renderContextView } from "./views/context/context";
@@ -212,6 +212,15 @@ export function getScript(vscode: any, document: any, window: any) {
}
return;
}
+
+ // copy diagnostic
+ const diagnosticCopyBtn = target.closest?.('.copy-diagnostic-btn');
+ if (diagnosticCopyBtn) {
+ e.preventDefault();
+ e.stopPropagation();
+ copyDiagnosticToClipboard(diagnosticCopyBtn, getDisplayDiagnostics(diagnostics, showAllDiagnostics, currentFile));
+ return;
+ }
});
// message event listener from extension
diff --git a/client/src/webview/styles.ts b/client/src/webview/styles.ts
index 1b4cf5a..c6d08a6 100644
--- a/client/src/webview/styles.ts
+++ b/client/src/webview/styles.ts
@@ -95,9 +95,34 @@ export function getStyles(): string {
}
.diagnostic-item {
background-color: var(--vscode-textCodeBlock-background);
- padding: 0.5rem 1rem;
+ padding: 0.5rem 3rem 0.5rem 1rem;
margin-bottom: 1rem;
border-radius: 4px;
+ position: relative;
+ }
+ .copy-diagnostic-btn {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.75rem;
+ height: 1.75rem;
+ padding: 0;
+ color: var(--vscode-foreground);
+ background: transparent;
+ border: none;
+ opacity: 0.65;
+ font-size: 1.25rem;
+ }
+ .copy-diagnostic-btn:hover {
+ background: var(--vscode-editor-background);
+ opacity: 1;
+ }
+ .copy-diagnostic-btn:disabled {
+ opacity: 0.8;
+ cursor: default;
}
.error-item {
border-left: 4px solid var(--vscode-editorError-foreground);
diff --git a/client/src/webview/views/diagnostics/diagnostics.ts b/client/src/webview/views/diagnostics/diagnostics.ts
index 8ae9105..39c54cb 100644
--- a/client/src/webview/views/diagnostics/diagnostics.ts
+++ b/client/src/webview/views/diagnostics/diagnostics.ts
@@ -3,6 +3,8 @@ import { renderErrors } from "./errors";
import { renderMainHeader } from "../sections";
import { renderWarnings } from "./warnings";
+const COPY_BUTTON_RESET_MS = 2000;
+
export function renderDiagnosticsView(
diagnostics: LJDiagnostic[],
showAll: boolean,
@@ -41,4 +43,93 @@ export function renderDiagnosticsView(
`;
-}
\ No newline at end of file
+}
+
+export function getDisplayDiagnostics(diagnostics: LJDiagnostic[], showAll: boolean, currentFile: string | undefined): LJDiagnostic[] {
+ if (showAll) return diagnostics;
+ return diagnostics.filter(diagnostic => diagnostic.file?.toLowerCase() === currentFile?.toLowerCase() || !diagnostic.file);
+}
+
+export function renderCopyDiagnosticButton(indexType: 'error' | 'warning', index: number): string {
+ return /*html*/`⎘ `;
+}
+
+export async function copyDiagnosticToClipboard(button: any, displayDiagnostics: LJDiagnostic[]) {
+ const errorIndex = parseInt(button.getAttribute('data-error-index') || '-1', 10);
+ const warningIndex = parseInt(button.getAttribute('data-warning-index') || '-1', 10);
+ const diagnostic = errorIndex >= 0
+ ? displayDiagnostics.filter(d => d.category === 'error')[errorIndex]
+ : displayDiagnostics.filter(d => d.category === 'warning')[warningIndex];
+ if (!diagnostic) return;
+
+ const diagnosticText = formatDiagnosticForClipboard(diagnostic);
+ const originalTitle = button.getAttribute('title');
+ const originalContent = button.innerHTML;
+
+ try {
+ button.disabled = true;
+ await navigator.clipboard.writeText(diagnosticText);
+ button.textContent = '✓';
+ button.setAttribute('title', 'Copied!');
+ } catch (e) {
+ button.textContent = '✗';
+ button.setAttribute('title', 'Copy failed');
+ } finally {
+ setTimeout(() => {
+ button.innerHTML = originalContent;
+ button.setAttribute('title', originalTitle);
+ button.disabled = false;
+ }, COPY_BUTTON_RESET_MS);
+ }
+}
+
+export function formatDiagnosticForClipboard(diagnostic: LJDiagnostic): string {
+ const skippedFields = new Set(['category', 'type', 'translationTable', 'position', 'file']);
+ const lines: string[] = [];
+
+ Object.entries(diagnostic).forEach(([key, value]) => {
+ if (skippedFields.has(key)) return;
+
+ const formattedValue = formatClipboardValue(value);
+ if (!formattedValue) return;
+
+ lines.push(`${formatClipboardLabel(key)}: ${formattedValue}`);
+ });
+
+ const location = formatDiagnosticLocation(diagnostic);
+ if (location) lines.push(`Location: ${location}`);
+
+ return lines.join('\n');
+}
+
+function formatClipboardLabel(key: string): string {
+ return key
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
+ .replace(/^./, char => char.toUpperCase());
+}
+
+function formatClipboardValue(value: unknown): string {
+ if (value === null || value === undefined) return '';
+
+ if (typeof value === 'string') return value.trim();
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
+
+ if (Array.isArray(value)) {
+ const values = value.map(formatClipboardValue).filter(Boolean);
+ if (values.length === 0) return '';
+ return values.some(v => v.includes('\n')) ? `\n${values.join('\n')}` : values.join(', ');
+ }
+
+ if (typeof value === 'object' && 'value' in value) {
+ return formatClipboardValue((value as { value: unknown }).value);
+ }
+
+ return JSON.stringify(value);
+}
+
+function formatDiagnosticLocation(diagnostic: LJDiagnostic): string {
+ if (!diagnostic.file || !diagnostic.position) return '';
+
+ const filename = diagnostic.file.split('/').pop()?.trim() || diagnostic.file;
+ return `${filename}:${diagnostic.position.lineStart + 1}`;
+}
diff --git a/client/src/webview/views/diagnostics/errors.ts b/client/src/webview/views/diagnostics/errors.ts
index 037d542..ddb5256 100644
--- a/client/src/webview/views/diagnostics/errors.ts
+++ b/client/src/webview/views/diagnostics/errors.ts
@@ -11,16 +11,17 @@ import type {
SyntaxError,
TranslationTable,
} from "../../../types/diagnostics";
+import { renderCopyDiagnosticButton } from "./diagnostics";
export function renderErrors(errors: LJError[], expandedErrors: Set): string {
return /*html*/`
- ${errors.map(error => {
- const errorIndex = errors.indexOf(error);
- const isExpanded = expandedErrors.has(errorIndex);
+ ${errors.map((error, index) => {
+ const isExpanded = expandedErrors.has(index);
return /*html*/`
- ${renderError(error, errorIndex, isExpanded)}
+ ${renderCopyDiagnosticButton('error', index)}
+ ${renderError(error, index, isExpanded)}
`;
}).join("")}
@@ -77,4 +78,4 @@ function renderExtra(error: LJError, errorIndex: number, isExpanded: boolean): s
extra += renderTranslationTable((error as any).translationTable as TranslationTable);
}
return extra ? isExpanded ? /*html*/`${button}` : button : "";
-}
\ No newline at end of file
+}
diff --git a/client/src/webview/views/diagnostics/warnings.ts b/client/src/webview/views/diagnostics/warnings.ts
index dde140e..9a91b47 100644
--- a/client/src/webview/views/diagnostics/warnings.ts
+++ b/client/src/webview/views/diagnostics/warnings.ts
@@ -1,11 +1,13 @@
import type { ExternalClassNotFoundWarning, ExternalMethodNotFoundWarning, LJWarning } from "../../../types/diagnostics";
import { renderDiagnosticHeader, renderLocation, renderSection } from "../sections";
+import { renderCopyDiagnosticButton } from "./diagnostics";
export function renderWarnings(warnings: LJWarning[]): string {
return /*html*/`
- ${warnings.map(warning => /*html*/`
+ ${warnings.map((warning, index) => /*html*/`
+ ${renderCopyDiagnosticButton('warning', index)}
${renderWarning(warning)}
`).join("")}