Files
archify-vscode-ext/src/webview/messages.ts
T
root-at-skic 90c7be06de 0.10.4: a real Sync button on every Archify page
A labelled Sync button sits top right on every Archify page, also in Source, and opens the Sync menu. On a shared model it turns green and reads Shared. The title-bar icon and the pre-share status bar entry are gone, with their settings.
2026-11-07 01:25:34 +02:00

148 lines
5.6 KiB
TypeScript

import path from 'node:path';
import * as vscode from 'vscode';
import { repoRootFor } from '../archify/runtime';
import { log } from '../util/log';
import type { LiveAction } from '../sync/liveBridge';
export type WebviewMessage =
| { type: 'hash'; hash: string }
| { type: 'save'; filename: string; base64: string }
| { type: 'openExternal'; href: string }
| { type: 'openRelative'; href: string }
| { type: 'openSource'; path: string; line?: number; endLine?: number; href?: string }
| { type: 'openDiagram'; path: string }
| { type: 'openRegistry'; file: string; line?: number }
| { type: 'openDecision'; file: string }
| { type: 'showGraph'; query?: string }
| { type: 'showModel' }
| { type: 'liveReady' }
| { type: 'liveAction'; action: LiveAction }
| { type: 'syncMenu' }
| { type: 'syncReady' }
| { type: 'error'; message: string };
/** Column for opening files next to a webview without covering it. */
function besideColumn(panel: vscode.WebviewPanel): vscode.ViewColumn {
return panel.viewColumn === vscode.ViewColumn.One ? vscode.ViewColumn.Two : vscode.ViewColumn.One;
}
async function exists(uri: vscode.Uri): Promise<boolean> {
try {
await vscode.workspace.fs.stat(uri);
return true;
} catch {
return false;
}
}
/**
* Opens a diagram's source evidence in the editor. The path is relative to
* the repository root; the diagram's checkout and the workspace folders are
* tried in turn before falling back to the web link.
*/
async function openSource(
message: Extract<WebviewMessage, { type: 'openSource' }>,
context: vscode.Uri,
panel: vscode.WebviewPanel,
): Promise<void> {
const relative = message.path.replace(/^\/+/, '');
const roots = new Set<string>();
const repoRoot = await repoRootFor(context);
if (repoRoot) roots.add(repoRoot);
for (const folder of vscode.workspace.workspaceFolders ?? []) roots.add(folder.uri.fsPath);
for (const root of roots) {
const candidate = vscode.Uri.file(path.join(root, relative));
if (relative && (await exists(candidate))) {
const start = Math.max((message.line ?? 1) - 1, 0);
const end = Math.max((message.endLine ?? message.line ?? 1) - 1, start);
const selection = new vscode.Range(start, 0, end, Number.MAX_SAFE_INTEGER);
await vscode.window.showTextDocument(candidate, { viewColumn: besideColumn(panel), selection });
return;
}
}
if (message.href && /^https?:/i.test(message.href)) {
await vscode.env.openExternal(vscode.Uri.parse(message.href));
return;
}
vscode.window.showWarningMessage(`Archify: source file "${message.path}" was not found in the workspace.`);
}
/**
* Whether the file's model is shared: an .archify-sync file next to its
* main.archify. Read from disk, so the sync bundle stays unloaded.
*/
async function isShared(file: vscode.Uri): Promise<boolean> {
const folder = vscode.workspace.getWorkspaceFolder(file);
let dir = vscode.Uri.joinPath(file, '..');
for (let depth = 0; depth < 16; depth++) {
if (await exists(vscode.Uri.joinPath(dir, '.archify-sync'))) return true;
if (await exists(vscode.Uri.joinPath(dir, 'main.archify'))) return false;
const parent = vscode.Uri.joinPath(dir, '..');
if (!folder || dir.path === folder.uri.path || parent.path === dir.path) return false;
dir = parent;
}
return false;
}
async function postSyncState(file: vscode.Uri, panel: vscode.WebviewPanel): Promise<void> {
const shared = file.scheme === 'file' && (await isShared(file));
await Promise.resolve(panel.webview.postMessage({ type: 'syncState', shared })).catch(() => undefined);
}
export async function handleWebviewMessage(
message: WebviewMessage,
context: vscode.Uri,
panel: vscode.WebviewPanel,
): Promise<void> {
try {
switch (message.type) {
case 'openExternal':
await vscode.env.openExternal(vscode.Uri.parse(message.href));
break;
case 'openRelative': {
const [file] = message.href.split('#');
if (!file) break;
const target = vscode.Uri.joinPath(context, '..', decodeURIComponent(file));
await vscode.commands.executeCommand('vscode.open', target, besideColumn(panel));
break;
}
case 'openSource':
await openSource(message, context, panel);
break;
case 'save': {
const name = path.basename(message.filename || 'archify-export');
const defaultUri = vscode.Uri.joinPath(context, '..', name);
const target = await vscode.window.showSaveDialog({ defaultUri, saveLabel: 'Export' });
if (!target) break;
await vscode.workspace.fs.writeFile(target, Buffer.from(message.base64, 'base64'));
const choice = await vscode.window.showInformationMessage(`Exported ${path.basename(target.fsPath)}`, 'Reveal');
if (choice === 'Reveal') await vscode.commands.executeCommand('revealFileInOS', target);
break;
}
case 'error':
vscode.window.showErrorMessage(`Archify: ${message.message}`);
break;
case 'syncReady':
await postSyncState(context, panel);
break;
case 'syncMenu':
await vscode.commands.executeCommand('archify.sync.menu');
// Share or Stop may have changed it.
await postSyncState(context, panel);
break;
case 'hash':
case 'openDiagram':
case 'showGraph':
case 'showModel':
case 'liveReady':
case 'liveAction':
// Handled by the view, which knows the model root.
break;
}
} catch (error) {
log().error(error instanceof Error ? error : String(error));
vscode.window.showErrorMessage(`Archify: ${error instanceof Error ? error.message : String(error)}`);
}
}