import fs from 'node:fs/promises'; import path from 'node:path'; import * as vscode from 'vscode'; import { diagramBaseName, isArchifyFile, isArchifyHtml, isDiagramFileName, readDiagramSource } from './detect'; import { DiagnosticsController } from './diagnostics'; import { HTML_VIEW_TYPE, HtmlViewerProvider } from './htmlViewer'; import { DIAGRAM_EDITOR_VIEW_TYPE, PreviewManager } from './preview'; import { isEvidenceError, renderDiagram, renderToFile } from './renderer'; import { clearGitRootCache, evidenceMode, log, readText, runtimeFor } from './runtime'; const JSON_LANGUAGES = new Set(['json', 'jsonc']); function isHtmlUri(uri: vscode.Uri): boolean { return /\.html?$/i.test(uri.path); } export function activate(context: vscode.ExtensionContext): void { const previews = new PreviewManager(context); const htmlViewer = new HtmlViewerProvider(); context.subscriptions.push(previews, htmlViewer, new DiagnosticsController(context), log()); /** The file a command applies to: explicit argument, focused preview or viewer, then the text editor. */ function targetUri(arg?: unknown): vscode.Uri | undefined { if (arg instanceof vscode.Uri) return arg; return previews.activePreview?.uri ?? htmlViewer.activeUri ?? vscode.window.activeTextEditor?.document.uri; } function sourceUri(arg?: unknown): vscode.Uri | undefined { const uri = targetUri(arg); if (!uri) vscode.window.showWarningMessage('Archify: open a diagram JSON source first.'); return uri; } // Context keys for editor title buttons and keybindings. let contextTimer: NodeJS.Timeout | undefined; const updateContext = () => { const doc = vscode.window.activeTextEditor?.document; const diagram = Boolean( doc && (isArchifyFile(doc.fileName) || (JSON_LANGUAGES.has(doc.languageId) && (isDiagramFileName(doc.fileName) || readDiagramSource(doc.getText(), doc.fileName)))), ); const html = Boolean(doc && doc.languageId === 'html' && isArchifyHtml(doc.getText())); void vscode.commands.executeCommand('setContext', 'archify.isDiagramSource', diagram); void vscode.commands.executeCommand('setContext', 'archify.isArchifyHtml', html); }; context.subscriptions.push( vscode.window.onDidChangeActiveTextEditor(updateContext), vscode.workspace.onDidChangeTextDocument((event) => { if (event.document !== vscode.window.activeTextEditor?.document) return; clearTimeout(contextTimer); contextTimer = setTimeout(updateContext, 300); }), vscode.workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration('archify.repoRoot')) clearGitRootCache(); }), { dispose: () => clearTimeout(contextTimer) }, ); updateContext(); const openHtmlViewer = (uri: vscode.Uri, viewColumn?: vscode.ViewColumn) => vscode.commands.executeCommand('vscode.openWith', uri, HTML_VIEW_TYPE, viewColumn); context.subscriptions.push( vscode.commands.registerCommand('archify.showPreview', (arg?: unknown) => { const uri = sourceUri(arg); if (!uri) return; if (isHtmlUri(uri)) return openHtmlViewer(uri); if (isArchifyFile(uri.path)) return vscode.commands.executeCommand('vscode.openWith', uri, DIAGRAM_EDITOR_VIEW_TYPE); previews.show(uri, vscode.window.activeTextEditor?.viewColumn ?? vscode.ViewColumn.Active); }), vscode.commands.registerCommand('archify.showPreviewToSide', (arg?: unknown) => { const uri = sourceUri(arg); if (!uri) return; if (isHtmlUri(uri)) return openHtmlViewer(uri, vscode.ViewColumn.Beside); if (isArchifyFile(uri.path)) { return vscode.commands.executeCommand('vscode.openWith', uri, DIAGRAM_EDITOR_VIEW_TYPE, { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true, }); } previews.show(uri, vscode.ViewColumn.Beside, true); }), vscode.commands.registerCommand('archify.openHtmlViewer', (arg?: unknown) => { const uri = sourceUri(arg); if (uri) return openHtmlViewer(uri); }), vscode.commands.registerCommand('archify.refreshPreview', () => previews.activePreview?.update(true)), vscode.commands.registerCommand('archify.showSource', async () => { const preview = previews.activePreview; if (preview?.isEditor) { // The diagram is the editor; open the YAML text beside it. await vscode.commands.executeCommand('vscode.openWith', preview.uri, 'default', vscode.ViewColumn.Beside); return; } if (preview) { const column = preview.panel.viewColumn === vscode.ViewColumn.One ? vscode.ViewColumn.Two : vscode.ViewColumn.One; await vscode.window.showTextDocument(preview.uri, { viewColumn: column }); return; } const html = htmlViewer.activeUri; if (html) await vscode.commands.executeCommand('vscode.openWith', html, 'default'); }), vscode.commands.registerCommand('archify.renderToHtml', async (arg?: unknown) => { const uri = sourceUri(arg); if (!uri) return; const text = await readText(uri); const source = readDiagramSource(text, uri.path); if (!source) { vscode.window.showWarningMessage(`Archify: ${path.basename(uri.path)} has no "diagram_type" (architecture, workflow, sequence, dataflow or lifecycle).`); return; } const meta = (source.diagram.meta ?? {}) as { output?: unknown }; const authored = typeof meta.output === 'string' && !/[\\/]/.test(meta.output) ? meta.output : undefined; const fileName = authored ?? `${diagramBaseName(uri.path)}.html`; const defaultUri = uri.scheme === 'file' ? vscode.Uri.joinPath(uri, '..', fileName) : undefined; const target = await vscode.window.showSaveDialog({ defaultUri, filters: { HTML: ['html'] }, saveLabel: 'Render', }); if (!target) return; const runtime = await runtimeFor(context, uri); let result = await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: 'Archify: rendering…' }, () => renderToFile(runtime, source, target.fsPath), ); if (!result.ok && isEvidenceError(result.error)) { const choice = await vscode.window.showWarningMessage( 'Archify could not verify the diagram\'s source evidence against a local checkout.', { modal: true, detail: result.error }, 'Render Without Source Links', ); if (choice) result = await renderToFile(runtime, source, target.fsPath, true); else return; } if (!result.ok) { log().error(result.error ?? 'render failed'); vscode.window.showErrorMessage('Archify: render failed.', { modal: true, detail: result.error }); return; } const choice = await vscode.window.showInformationMessage( `Rendered ${path.basename(target.fsPath)}`, 'Open in Viewer', 'Open in Browser', ); if (choice === 'Open in Viewer') await openHtmlViewer(target); if (choice === 'Open in Browser') await vscode.env.openExternal(target); }), vscode.commands.registerCommand('archify.openInBrowser', async (arg?: unknown) => { const uri = sourceUri(arg); if (!uri) return; if (isHtmlUri(uri)) { await vscode.env.openExternal(uri); return; } const text = await readText(uri); const source = readDiagramSource(text, uri.path); if (!source) { vscode.window.showWarningMessage(`Archify: ${path.basename(uri.path)} has no "diagram_type" (architecture, workflow, sequence, dataflow or lifecycle).`); return; } const runtime = await runtimeFor(context, uri); const result = await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: 'Archify: rendering…' }, () => renderDiagram(runtime, source, evidenceMode(uri)), ); if (!result.ok || !result.html) { vscode.window.showErrorMessage('Archify: render failed.', { modal: true, detail: result.error }); return; } const dir = path.join(context.globalStorageUri.fsPath, 'browser'); await fs.mkdir(dir, { recursive: true }); const file = path.join(dir, `${diagramBaseName(uri.path)}.html`); await fs.writeFile(file, result.html, 'utf8'); await vscode.env.openExternal(vscode.Uri.file(file)); }), ); } export function deactivate(): void {}