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*/`