Archify Diagram Viewer 0.1.0
VS Code extension that previews Archify diagrams from their JSON sources (live, as you type) and opens rendered Archify HTML in a viewer tab. Bundles the Archify 3.0.1 renderer and runs it on VS Code's Node runtime. Adds validation diagnostics, JSON schema help, source-link navigation, export saving, render-to-file and open-in-browser commands. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import * as vscode from 'vscode';
|
||||
import { isArchifyHtml, 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) && 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)} is not an Archify diagram source.`);
|
||||
return;
|
||||
}
|
||||
const meta = (source.diagram.meta ?? {}) as { output?: unknown };
|
||||
const authored = typeof meta.output === 'string' && !/[\\/]/.test(meta.output) ? meta.output : undefined;
|
||||
const fileName = authored ?? `${path.basename(uri.path).replace(/\.json$/i, '')}.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)} is not an Archify diagram source.`);
|
||||
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, `${path.basename(uri.path).replace(/\.json$/i, '')}.html`);
|
||||
await fs.writeFile(file, result.html, 'utf8');
|
||||
await vscode.env.openExternal(vscode.Uri.file(file));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function deactivate(): void {}
|
||||
Reference in New Issue
Block a user