0.3.0: .archify files are YAML and open straight into the diagram

- .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
This commit is contained in:
2026-09-30 15:21:55 +03:00
parent 7c2624c465
commit d2f52d43ac
18 changed files with 744 additions and 250 deletions
+43 -10
View File
@@ -7,6 +7,8 @@ import { handleWebviewMessage, WebviewMessage } from './webviewMessages';
import { makeNonce, messagePage, prepareDiagramHtml } from './webviewHtml';
export const PREVIEW_VIEW_TYPE = 'archify.preview';
/** Default editor for .archify files: the rendered diagram. */
export const DIAGRAM_EDITOR_VIEW_TYPE = 'archify.diagram';
interface PreviewState {
uri: string;
@@ -28,11 +30,15 @@ class DiagramPreview implements vscode.Disposable {
readonly panel: vscode.WebviewPanel,
readonly uri: vscode.Uri,
state?: PreviewState,
/** True when this is the custom editor of the file, not a side preview. */
readonly isEditor = false,
) {
this.hash = state?.hash;
panel.webview.options = { enableScripts: true, localResourceRoots: [] };
panel.iconPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'preview.svg');
panel.title = `Preview ${path.basename(uri.path)}`;
if (!isEditor) {
panel.iconPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'preview.svg');
panel.title = `Preview ${path.basename(uri.path)}`;
}
this.disposables.push(
panel.onDidDispose(() => this.dispose()),
@@ -102,7 +108,7 @@ class DiagramPreview implements vscode.Disposable {
let result;
try {
const runtime = await runtimeFor(this.context, this.uri);
result = await renderDiagram(runtime, { ...source, text }, evidenceMode(this.uri));
result = await renderDiagram(runtime, source, evidenceMode(this.uri));
} catch (error) {
result = { ok: false, error: error instanceof Error ? error.message : String(error) };
}
@@ -152,19 +158,30 @@ function firstLines(text: string | undefined, count = 6): string {
return (text ?? '').split('\n').slice(0, count).join('\n');
}
export class PreviewManager implements vscode.Disposable, vscode.WebviewPanelSerializer<PreviewState> {
export class PreviewManager
implements vscode.Disposable, vscode.WebviewPanelSerializer<PreviewState>, vscode.CustomTextEditorProvider
{
/** Side previews, one per source file. */
private readonly previews = new Map<string, DiagramPreview>();
/** Custom-editor tabs of .archify files (several per file are allowed). */
private readonly editors = new Set<DiagramPreview>();
private active: DiagramPreview | undefined;
private readonly disposables: vscode.Disposable[] = [];
constructor(private readonly context: vscode.ExtensionContext) {
this.disposables.push(
vscode.window.registerWebviewPanelSerializer(PREVIEW_VIEW_TYPE, this),
vscode.window.registerCustomEditorProvider(DIAGRAM_EDITOR_VIEW_TYPE, this, {
webviewOptions: { retainContextWhenHidden: true, enableFindWidget: true },
supportsMultipleEditorsPerDocument: true,
}),
vscode.workspace.onDidChangeTextDocument((event) => {
const preview = this.previews.get(event.document.uri.toString());
if (preview && event.contentChanges.length && this.liveUpdate(preview.uri)) preview.scheduleUpdate();
if (!event.contentChanges.length || !this.liveUpdate(event.document.uri)) return;
for (const preview of this.forUri(event.document.uri)) preview.scheduleUpdate();
}),
vscode.workspace.onDidSaveTextDocument((doc) => {
for (const preview of this.forUri(doc.uri)) void preview.update();
}),
vscode.workspace.onDidSaveTextDocument((doc) => void this.previews.get(doc.uri.toString())?.update()),
vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration('archify')) this.refreshAll();
}),
@@ -180,6 +197,17 @@ export class PreviewManager implements vscode.Disposable, vscode.WebviewPanelSer
return this.active;
}
private forUri(uri: vscode.Uri): DiagramPreview[] {
const key = uri.toString();
return [...this.previews.values(), ...this.editors].filter((preview) => preview.uri.toString() === key);
}
/** Opening a .archify file lands here: the diagram is the editor. */
async resolveCustomTextEditor(document: vscode.TextDocument, panel: vscode.WebviewPanel): Promise<void> {
panel.webview.options = { enableScripts: true, localResourceRoots: [] };
this.track(new DiagramPreview(this.context, panel, document.uri, undefined, true));
}
show(uri: vscode.Uri, viewColumn: vscode.ViewColumn, preserveFocus = false): void {
const existing = this.previews.get(uri.toString());
if (existing) {
@@ -205,8 +233,11 @@ export class PreviewManager implements vscode.Disposable, vscode.WebviewPanelSer
private track(preview: DiagramPreview): void {
const key = preview.uri.toString();
this.previews.get(key)?.panel.dispose();
this.previews.set(key, preview);
if (preview.isEditor) this.editors.add(preview);
else {
this.previews.get(key)?.panel.dispose();
this.previews.set(key, preview);
}
const onViewState = () => {
if (preview.panel.active) this.active = preview;
else if (this.active === preview) this.active = undefined;
@@ -215,6 +246,7 @@ export class PreviewManager implements vscode.Disposable, vscode.WebviewPanelSer
preview.panel.onDidChangeViewState(onViewState);
onViewState();
preview.onDidDispose(() => {
this.editors.delete(preview);
if (this.previews.get(key) === preview) this.previews.delete(key);
if (this.active === preview) {
this.active = undefined;
@@ -224,12 +256,13 @@ export class PreviewManager implements vscode.Disposable, vscode.WebviewPanelSer
}
refreshAll(force = false): void {
for (const preview of this.previews.values()) void preview.update(force);
for (const preview of [...this.previews.values(), ...this.editors]) void preview.update(force);
}
dispose(): void {
for (const preview of this.previews.values()) preview.panel.dispose();
this.previews.clear();
this.editors.clear();
for (const disposable of this.disposables.splice(0)) disposable.dispose();
}
}