- .archify content is YAML (JSON still accepted); converted to JSON for the renderer - Archify Diagram custom editor is the default for *.archify; Show Source opens the YAML beside it - YAML syntax errors and validation problems mapped to YAML lines - yamlValidation for schema completion with redhat.vscode-yaml - scripts/to-yaml.mjs converts existing .archify files
189 lines
8.3 KiB
TypeScript
189 lines
8.3 KiB
TypeScript
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 {}
|