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:
2026-09-30 12:44:15 +03:00
co-authored by Claude Opus 5.5
commit 5e53348252
119 changed files with 66708 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
import { parse, ParseError } from 'jsonc-parser';
import * as vscode from 'vscode';
import { readDiagramSource } from './detect';
import { rangeForPointer } from './jsonPointer';
import { ArchifyDiagnostic, isEvidenceError, validate } from './renderer';
import { evidenceMode, log, runtimeFor } from './runtime';
const JSON_LANGUAGES = new Set(['json', 'jsonc']);
function severity(diagnostic: ArchifyDiagnostic, downgradeEvidence: boolean): vscode.DiagnosticSeverity {
if (downgradeEvidence && isEvidenceError(`${diagnostic.code ?? ''} ${diagnostic.message}`)) {
return vscode.DiagnosticSeverity.Warning;
}
switch (diagnostic.severity) {
case 'warning':
return vscode.DiagnosticSeverity.Warning;
case 'info':
return vscode.DiagnosticSeverity.Information;
default:
return vscode.DiagnosticSeverity.Error;
}
}
/** Runs `archify validate` on diagram sources and reports into Problems. */
export class DiagnosticsController implements vscode.Disposable {
private readonly collection = vscode.languages.createDiagnosticCollection('archify');
private readonly timers = new Map<string, NodeJS.Timeout>();
private readonly sequence = new Map<string, number>();
private readonly disposables: vscode.Disposable[];
constructor(private readonly context: vscode.ExtensionContext) {
this.disposables = [
this.collection,
vscode.workspace.onDidOpenTextDocument((doc) => this.schedule(doc, 0)),
vscode.workspace.onDidChangeTextDocument((event) => {
if (event.contentChanges.length) this.schedule(event.document, 700);
}),
vscode.workspace.onDidSaveTextDocument((doc) => this.schedule(doc, 0)),
vscode.workspace.onDidCloseTextDocument((doc) => this.clear(doc.uri)),
vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration('archify')) this.revalidateAll();
}),
];
this.revalidateAll();
}
private enabled(uri: vscode.Uri): boolean {
return vscode.workspace.getConfiguration('archify', uri).get<boolean>('validation.enabled', true);
}
revalidateAll(): void {
for (const doc of vscode.workspace.textDocuments) this.schedule(doc, 0);
}
private clear(uri: vscode.Uri): void {
const key = uri.toString();
clearTimeout(this.timers.get(key));
this.timers.delete(key);
this.sequence.set(key, (this.sequence.get(key) ?? 0) + 1);
this.collection.delete(uri);
}
private schedule(doc: vscode.TextDocument, delay: number): void {
if (!JSON_LANGUAGES.has(doc.languageId)) return;
const key = doc.uri.toString();
clearTimeout(this.timers.get(key));
this.timers.set(key, setTimeout(() => void this.validate(doc), delay));
}
private async validate(doc: vscode.TextDocument): Promise<void> {
const key = doc.uri.toString();
this.timers.delete(key);
const seq = (this.sequence.get(key) ?? 0) + 1;
this.sequence.set(key, seq);
const text = doc.getText();
const source = readDiagramSource(text, doc.fileName);
if (!source || !this.enabled(doc.uri) || doc.isClosed) {
this.collection.delete(doc.uri);
return;
}
// Leave syntax errors to the JSON language service.
const syntax: ParseError[] = [];
parse(text, syntax, { allowTrailingComma: false });
if (syntax.length) {
this.collection.delete(doc.uri);
return;
}
try {
const runtime = await runtimeFor(this.context, doc.uri);
const result = await validate(runtime, source.type, text);
if (seq !== this.sequence.get(key) || doc.isClosed) return;
if (result.crash) {
const diagnostic = new vscode.Diagnostic(
new vscode.Range(0, 0, 0, 0),
`Archify validation did not complete: ${result.crash.split('\n')[0]}`,
vscode.DiagnosticSeverity.Error,
);
diagnostic.source = 'archify';
this.collection.set(doc.uri, [diagnostic]);
return;
}
const downgradeEvidence = evidenceMode(doc.uri) === 'fallback';
this.collection.set(
doc.uri,
result.diagnostics.map((item) => {
const offsets = rangeForPointer(text, item.subject?.path);
const range = new vscode.Range(doc.positionAt(offsets.start), doc.positionAt(offsets.end));
const fix = item.supportedFixes?.[0];
const diagnostic = new vscode.Diagnostic(
range,
fix ? `${item.message}\nFix: ${fix}` : item.message,
severity(item, downgradeEvidence),
);
diagnostic.source = 'archify';
if (item.code) diagnostic.code = item.code;
return diagnostic;
}),
);
} catch (error) {
log().error(`Validation failed for ${doc.uri.fsPath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
dispose(): void {
for (const timer of this.timers.values()) clearTimeout(timer);
for (const disposable of this.disposables.splice(0)) disposable.dispose();
}
}