Files
archify-vscode-ext/src/htmlViewer.ts
T
root-at-skicandClaude Opus 5.5 5e53348252 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>
2026-09-30 12:44:15 +03:00

117 lines
4.1 KiB
TypeScript

import path from 'node:path';
import * as vscode from 'vscode';
import { isArchifyHtml } from './detect';
import { viewerTheme } from './runtime';
import { handleWebviewMessage, WebviewMessage } from './webviewMessages';
import { makeNonce, messagePage, prepareDiagramHtml } from './webviewHtml';
export const HTML_VIEW_TYPE = 'archify.htmlViewer';
class HtmlDocument implements vscode.CustomDocument {
constructor(readonly uri: vscode.Uri) {}
dispose(): void {}
}
/** Read-only custom editor that shows rendered Archify HTML files. */
export class HtmlViewerProvider implements vscode.CustomReadonlyEditorProvider<HtmlDocument>, vscode.Disposable {
private readonly viewers = new Map<vscode.WebviewPanel, () => Promise<void>>();
private active: { uri: vscode.Uri; panel: vscode.WebviewPanel } | undefined;
private readonly disposables: vscode.Disposable[];
constructor() {
this.disposables = [
vscode.window.registerCustomEditorProvider(HTML_VIEW_TYPE, this, {
webviewOptions: { retainContextWhenHidden: true, enableFindWidget: true },
supportsMultipleEditorsPerDocument: true,
}),
vscode.window.onDidChangeActiveColorTheme(() => this.reloadAll()),
vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration('archify.theme')) this.reloadAll();
}),
];
}
/** File shown in the focused Archify HTML viewer, if any. */
get activeUri(): vscode.Uri | undefined {
return this.active?.uri;
}
dispose(): void {
for (const disposable of this.disposables.splice(0)) disposable.dispose();
}
openCustomDocument(uri: vscode.Uri): HtmlDocument {
return new HtmlDocument(uri);
}
async resolveCustomEditor(document: HtmlDocument, panel: vscode.WebviewPanel): Promise<void> {
const uri = document.uri;
let hash: string | undefined;
panel.webview.options = { enableScripts: true, localResourceRoots: [] };
const reload = async () => {
let text: string;
try {
text = new TextDecoder('utf-8').decode(await vscode.workspace.fs.readFile(uri));
} catch (error) {
panel.webview.html = messagePage(
{ cspSource: panel.webview.cspSource, nonce: makeNonce() },
'Cannot open file',
uri.fsPath,
error instanceof Error ? error.message : String(error),
);
return;
}
panel.webview.html = prepareDiagramHtml(text, {
cspSource: panel.webview.cspSource,
nonce: makeNonce(),
theme: viewerTheme(),
hash,
notice: isArchifyHtml(text)
? undefined
: { kind: 'info', text: 'This file was not generated by Archify. It is shown in a sandbox without network access.' },
});
};
this.viewers.set(panel, reload);
const disposables: vscode.Disposable[] = [
panel.webview.onDidReceiveMessage((message: WebviewMessage) => {
if (message.type === 'hash') {
hash = message.hash;
return;
}
void handleWebviewMessage(message, uri, panel);
}),
panel.onDidChangeViewState(() => {
if (panel.active) this.active = { uri, panel };
else if (this.active?.panel === panel) this.active = undefined;
}),
];
if (uri.scheme === 'file') {
const watcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(vscode.Uri.file(path.dirname(uri.fsPath)), path.basename(uri.fsPath)),
);
let timer: NodeJS.Timeout | undefined;
const onChange = () => {
clearTimeout(timer);
timer = setTimeout(() => void reload(), 300);
};
disposables.push(watcher, watcher.onDidChange(onChange), watcher.onDidCreate(onChange), {
dispose: () => clearTimeout(timer),
});
}
panel.onDidDispose(() => {
this.viewers.delete(panel);
if (this.active?.panel === panel) this.active = undefined;
for (const disposable of disposables) disposable.dispose();
});
if (panel.active) this.active = { uri, panel };
await reload();
}
reloadAll(): void {
for (const reload of this.viewers.values()) void reload();
}
}