import path from 'node:path'; import * as vscode from 'vscode'; import { repoRootFor } from '../archify/runtime'; import { log } from '../util/log'; import type { LiveAction } from '../sync/liveBridge'; export type WebviewMessage = | { type: 'hash'; hash: string } | { type: 'save'; filename: string; base64: string } | { type: 'openExternal'; href: string } | { type: 'openRelative'; href: string } | { type: 'openSource'; path: string; line?: number; endLine?: number; href?: string } | { type: 'openDiagram'; path: string } | { type: 'openRegistry'; file: string; line?: number } | { type: 'openDecision'; file: string } | { type: 'showGraph'; query?: string } | { type: 'showModel' } | { type: 'liveReady' } | { type: 'liveAction'; action: LiveAction } | { type: 'syncMenu' } | { type: 'syncReady' } | { type: 'error'; message: string }; /** Column for opening files next to a webview without covering it. */ function besideColumn(panel: vscode.WebviewPanel): vscode.ViewColumn { return panel.viewColumn === vscode.ViewColumn.One ? vscode.ViewColumn.Two : vscode.ViewColumn.One; } async function exists(uri: vscode.Uri): Promise { try { await vscode.workspace.fs.stat(uri); return true; } catch { return false; } } /** * Opens a diagram's source evidence in the editor. The path is relative to * the repository root; the diagram's checkout and the workspace folders are * tried in turn before falling back to the web link. */ async function openSource( message: Extract, context: vscode.Uri, panel: vscode.WebviewPanel, ): Promise { const relative = message.path.replace(/^\/+/, ''); const roots = new Set(); const repoRoot = await repoRootFor(context); if (repoRoot) roots.add(repoRoot); for (const folder of vscode.workspace.workspaceFolders ?? []) roots.add(folder.uri.fsPath); for (const root of roots) { const candidate = vscode.Uri.file(path.join(root, relative)); if (relative && (await exists(candidate))) { const start = Math.max((message.line ?? 1) - 1, 0); const end = Math.max((message.endLine ?? message.line ?? 1) - 1, start); const selection = new vscode.Range(start, 0, end, Number.MAX_SAFE_INTEGER); await vscode.window.showTextDocument(candidate, { viewColumn: besideColumn(panel), selection }); return; } } if (message.href && /^https?:/i.test(message.href)) { await vscode.env.openExternal(vscode.Uri.parse(message.href)); return; } vscode.window.showWarningMessage(`Archify: source file "${message.path}" was not found in the workspace.`); } /** * Whether the file's model is shared: an .archify-sync file next to its * main.archify. Read from disk, so the sync bundle stays unloaded. */ async function isShared(file: vscode.Uri): Promise { const folder = vscode.workspace.getWorkspaceFolder(file); let dir = vscode.Uri.joinPath(file, '..'); for (let depth = 0; depth < 16; depth++) { if (await exists(vscode.Uri.joinPath(dir, '.archify-sync'))) return true; if (await exists(vscode.Uri.joinPath(dir, 'main.archify'))) return false; const parent = vscode.Uri.joinPath(dir, '..'); if (!folder || dir.path === folder.uri.path || parent.path === dir.path) return false; dir = parent; } return false; } async function postSyncState(file: vscode.Uri, panel: vscode.WebviewPanel): Promise { const shared = file.scheme === 'file' && (await isShared(file)); await Promise.resolve(panel.webview.postMessage({ type: 'syncState', shared })).catch(() => undefined); } export async function handleWebviewMessage( message: WebviewMessage, context: vscode.Uri, panel: vscode.WebviewPanel, ): Promise { try { switch (message.type) { case 'openExternal': await vscode.env.openExternal(vscode.Uri.parse(message.href)); break; case 'openRelative': { const [file] = message.href.split('#'); if (!file) break; const target = vscode.Uri.joinPath(context, '..', decodeURIComponent(file)); await vscode.commands.executeCommand('vscode.open', target, besideColumn(panel)); break; } case 'openSource': await openSource(message, context, panel); break; case 'save': { const name = path.basename(message.filename || 'archify-export'); const defaultUri = vscode.Uri.joinPath(context, '..', name); const target = await vscode.window.showSaveDialog({ defaultUri, saveLabel: 'Export' }); if (!target) break; await vscode.workspace.fs.writeFile(target, Buffer.from(message.base64, 'base64')); const choice = await vscode.window.showInformationMessage(`Exported ${path.basename(target.fsPath)}`, 'Reveal'); if (choice === 'Reveal') await vscode.commands.executeCommand('revealFileInOS', target); break; } case 'error': vscode.window.showErrorMessage(`Archify: ${message.message}`); break; case 'syncReady': await postSyncState(context, panel); break; case 'syncMenu': await vscode.commands.executeCommand('archify.sync.menu'); // Share or Stop may have changed it. await postSyncState(context, panel); break; case 'hash': case 'openDiagram': case 'showGraph': case 'showModel': case 'liveReady': case 'liveAction': // Handled by the view, which knows the model root. break; } } catch (error) { log().error(error instanceof Error ? error : String(error)); vscode.window.showErrorMessage(`Archify: ${error instanceof Error ? error.message : String(error)}`); } }