Files
archify-vscode-ext/src/extension.ts
T
root-at-skicandClaude Opus 5.5 03d30500e4 Support .archify diagram files (0.2.0)
.archify files are Archify JSON diagram sources. They open as JSON,
preview with the same commands, report validation problems, and get
completion from a schema that dispatches on diagram_type. Adds a
hello.archify example and tests for detection, the dispatching schema
and the VS Code behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 12:56:12 +03:00

175 lines
7.7 KiB
TypeScript

import fs from 'node:fs/promises';
import path from 'node:path';
import * as vscode from 'vscode';
import { diagramBaseName, isArchifyHtml, isDiagramFileName, readDiagramSource } from './detect';
import { DiagnosticsController } from './diagnostics';
import { HTML_VIEW_TYPE, HtmlViewerProvider } from './htmlViewer';
import { 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 && 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);
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);
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) {
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, text }, 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, text }, 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, text }, 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 {}