- one collapsible header per .archify-registry file with its element count and an Open button that opens the registry view; files start collapsed, the Registry panel open - view types moved to views/viewTypes.ts so the diagram page can open a registry without a circular import - 0.7.1
151 lines
6.0 KiB
TypeScript
151 lines
6.0 KiB
TypeScript
import path from 'node:path';
|
|
import * as vscode from 'vscode';
|
|
import { isMainFile, isRegistryFile, parseRegistry } from '../model/format';
|
|
import { RegistryPanel, registryPanelFromIndex, registryPanelStandalone } from '../model/panel';
|
|
import { ModelService } from '../model/service';
|
|
import { readText } from '../util/files';
|
|
import { log } from '../util/log';
|
|
import { handleWebviewMessage, WebviewMessage } from '../webview/messages';
|
|
import { makeNonce, registryPage } from '../webview/page';
|
|
import { DIAGRAM_EDITOR_VIEW_TYPE, REGISTRY_VIEW_TYPE } from './viewTypes';
|
|
|
|
export { REGISTRY_VIEW_TYPE };
|
|
|
|
class RegistryDocument implements vscode.CustomDocument {
|
|
constructor(readonly uri: vscode.Uri) {}
|
|
dispose(): void {}
|
|
}
|
|
|
|
class RegistryView implements vscode.Disposable {
|
|
private seq = 0;
|
|
private timer: NodeJS.Timeout | undefined;
|
|
private last: string | undefined;
|
|
root: vscode.Uri | undefined;
|
|
private readonly disposables: vscode.Disposable[] = [];
|
|
|
|
constructor(
|
|
private readonly models: ModelService,
|
|
readonly panel: vscode.WebviewPanel,
|
|
readonly uri: vscode.Uri,
|
|
) {
|
|
this.disposables.push(
|
|
panel.webview.onDidReceiveMessage((message: WebviewMessage) => {
|
|
if (message.type === 'openDiagram') return void this.openInModel(message.path, DIAGRAM_EDITOR_VIEW_TYPE);
|
|
if (message.type === 'openRegistry') return void this.openRegistryLine(message.file, message.line);
|
|
void handleWebviewMessage(message, this.uri, this.panel);
|
|
}),
|
|
);
|
|
void this.update();
|
|
}
|
|
|
|
private async openInModel(relativePath: string, viewType: string): Promise<void> {
|
|
if (!this.root) return;
|
|
const target = vscode.Uri.joinPath(this.root, ...relativePath.split('/'));
|
|
try {
|
|
await vscode.workspace.fs.stat(target);
|
|
} catch {
|
|
vscode.window.showWarningMessage(`Archify: ${relativePath} does not exist in this model.`);
|
|
return;
|
|
}
|
|
await vscode.commands.executeCommand('vscode.openWith', target, viewType, vscode.ViewColumn.Active);
|
|
}
|
|
|
|
private async openRegistryLine(file: string, line?: number): Promise<void> {
|
|
const target = this.root ? vscode.Uri.joinPath(this.root, ...file.split('/')) : this.uri;
|
|
const column = this.panel.viewColumn === vscode.ViewColumn.One ? vscode.ViewColumn.Two : vscode.ViewColumn.One;
|
|
const at = Math.max(line ?? 0, 0);
|
|
await vscode.window.showTextDocument(target, { viewColumn: column, selection: new vscode.Range(at, 0, at, 0) });
|
|
}
|
|
|
|
scheduleUpdate(): void {
|
|
clearTimeout(this.timer);
|
|
this.timer = setTimeout(() => void this.update(), 300);
|
|
}
|
|
|
|
async update(): Promise<void> {
|
|
clearTimeout(this.timer);
|
|
const seq = ++this.seq;
|
|
let registry: RegistryPanel;
|
|
try {
|
|
this.root = await this.models.rootOf(this.uri);
|
|
if (this.root) {
|
|
const rel = path.relative(this.root.fsPath, this.uri.fsPath).split(path.sep).join('/');
|
|
registry = registryPanelFromIndex(await this.models.index(this.root), rel);
|
|
} else {
|
|
const name = path.basename(this.uri.path);
|
|
registry = registryPanelStandalone(name, parseRegistry(await readText(this.uri), name));
|
|
}
|
|
} catch (error) {
|
|
log().warn(`Registry view failed for ${this.uri.fsPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
return;
|
|
}
|
|
if (seq !== this.seq) return;
|
|
const fingerprint = JSON.stringify(registry);
|
|
if (fingerprint === this.last) return;
|
|
this.last = fingerprint;
|
|
this.panel.webview.html = registryPage({ cspSource: this.panel.webview.cspSource, nonce: makeNonce() }, registry);
|
|
}
|
|
|
|
dispose(): void {
|
|
clearTimeout(this.timer);
|
|
for (const d of this.disposables.splice(0)) d.dispose();
|
|
}
|
|
}
|
|
|
|
export class RegistryViewProvider implements vscode.CustomReadonlyEditorProvider<RegistryDocument>, vscode.Disposable {
|
|
private readonly views = new Set<RegistryView>();
|
|
private active: RegistryView | undefined;
|
|
private readonly disposables: vscode.Disposable[];
|
|
|
|
constructor(private readonly models: ModelService) {
|
|
this.disposables = [
|
|
vscode.window.registerCustomEditorProvider(REGISTRY_VIEW_TYPE, this, {
|
|
webviewOptions: { retainContextWhenHidden: true, enableFindWidget: true },
|
|
supportsMultipleEditorsPerDocument: true,
|
|
}),
|
|
// Any change in the model (a diagram starts or stops using an element, a registry edit) refreshes the page.
|
|
models.onDidChangeModel(({ root }) => {
|
|
for (const view of this.views) if (view.root?.toString() === root.toString()) view.scheduleUpdate();
|
|
}),
|
|
vscode.workspace.onDidChangeTextDocument((event) => {
|
|
if (!isRegistryFile(event.document.fileName) && !isMainFile(event.document.fileName)) return;
|
|
// Outside a model the model service stays quiet; refresh the page of the edited file itself.
|
|
for (const view of this.views) if (!view.root && view.uri.toString() === event.document.uri.toString()) view.scheduleUpdate();
|
|
}),
|
|
];
|
|
}
|
|
|
|
/** File shown in the focused registry view, if any. */
|
|
get activeUri(): vscode.Uri | undefined {
|
|
return this.active?.uri;
|
|
}
|
|
|
|
openCustomDocument(uri: vscode.Uri): RegistryDocument {
|
|
return new RegistryDocument(uri);
|
|
}
|
|
|
|
async resolveCustomEditor(document: RegistryDocument, panel: vscode.WebviewPanel): Promise<void> {
|
|
panel.webview.options = { enableScripts: true };
|
|
const view = new RegistryView(this.models, panel, document.uri);
|
|
this.views.add(view);
|
|
const onViewState = () => {
|
|
if (panel.active) this.active = view;
|
|
else if (this.active === view) this.active = undefined;
|
|
if (panel.active) void vscode.commands.executeCommand('setContext', 'archify.inModel', Boolean(view.root));
|
|
};
|
|
panel.onDidChangeViewState(onViewState);
|
|
onViewState();
|
|
panel.onDidDispose(() => {
|
|
this.views.delete(view);
|
|
if (this.active === view) this.active = undefined;
|
|
view.dispose();
|
|
});
|
|
}
|
|
|
|
dispose(): void {
|
|
for (const view of this.views) view.dispose();
|
|
this.views.clear();
|
|
for (const d of this.disposables.splice(0)) d.dispose();
|
|
}
|
|
}
|