Files
archify-vscode-ext/src/commands/modelCommands.ts
T
root-at-skic c8645b228b Model graph: the whole model as one graph with queries; 0.9.0
- Model | Graph switch on main.archify (Diagram | Graph on diagram pages, narrowed to that diagram)
- union graph of every element and connection, registry and inline, built from the model index
- query box: text, type:, diagram:, decision:, uses:, near:, from: to:, unused, shared, -shared
- side panel per element and connection with diagrams, registry line and decisions one click away
- legend chips, Fit, Layout, Labels, Export PNG; Cytoscape.js bundled as dist/graph.js
- command Archify | Open Model Graph; integration check; unit tests for graph and queries
2026-10-21 15:47:00 +03:00

136 lines
7.5 KiB
TypeScript

import * as vscode from 'vscode';
import { MODEL_EXAMPLES, mainFromExample } from '../language/examples';
import { diagramName, invalidNameReason } from '../language/templates';
import { DECISION_EXT, slugify } from '../model/decisions';
import { MAIN_FILE, REGISTRY_EXT } from '../model/format';
import { REGISTRY_VIEW_TYPE } from '../views/registryView';
import { DECISION_VIEW_TYPE } from '../views/decisionView';
import { readText } from '../util/files';
import { DIAGRAM_EDITOR_VIEW_TYPE } from '../views/diagramView';
import { CommandContext, exists, targetUri } from './context';
/** Folder the new model folder goes into: the clicked folder (or a file's folder), else a workspace folder. */
async function parentFolder(arg?: unknown): Promise<vscode.Uri | undefined> {
if (arg instanceof vscode.Uri) {
const stat = await vscode.workspace.fs.stat(arg);
return stat.type & vscode.FileType.Directory ? arg : vscode.Uri.joinPath(arg, '..');
}
const folders = vscode.workspace.workspaceFolders ?? [];
if (folders.length <= 1) return folders[0]?.uri;
return (await vscode.window.showWorkspaceFolderPick({ placeHolder: 'Folder for the new model' }))?.uri;
}
export function modelCommands(ctx: CommandContext): vscode.Disposable[] {
return [
// Opens the model page (main.archify) of the model the current file is in.
vscode.commands.registerCommand('archify.openModel', async (arg?: unknown) => {
const uri = targetUri(ctx, arg);
const root = uri ? await ctx.models.rootOf(uri) : undefined;
if (!root) {
vscode.window.showInformationMessage(`Archify: this file is not inside a model (no ${MAIN_FILE} in a folder above it).`);
return;
}
await vscode.commands.executeCommand('vscode.openWith', vscode.Uri.joinPath(root, MAIN_FILE), DIAGRAM_EDITOR_VIEW_TYPE, vscode.ViewColumn.Active);
}),
// Opens the model's Graph page: every element and connection of the model in one picture, with a query box.
vscode.commands.registerCommand('archify.openModelGraph', async (arg?: unknown) => {
const uri = targetUri(ctx, arg);
const root = uri ? await ctx.models.rootOf(uri) : undefined;
if (!root) {
vscode.window.showInformationMessage(`Archify: this file is not inside a model (no ${MAIN_FILE} in a folder above it).`);
return;
}
await ctx.views.showGraph(root);
}),
// Creates a registry file in the clicked folder and opens it.
vscode.commands.registerCommand('archify.newRegistry', async (arg?: unknown) => {
const folder = await parentFolder(arg);
if (!folder) return;
const root = await ctx.models.rootOf(vscode.Uri.joinPath(folder, 'x'));
const name = await vscode.window.showInputBox({
title: 'Archify | New Registry',
prompt: `File name in ${vscode.workspace.asRelativePath(folder, true)}${root ? '' : ' (not inside a model: add a main.archify above it so diagrams can refer to its elements)'}`,
value: 'elements',
validateInput: async (value) =>
invalidNameReason(value) ?? ((await exists(vscode.Uri.joinPath(folder, `${diagramName(value).replace(/\.archify-registry$/i, '')}${REGISTRY_EXT}`))) ? `${diagramName(value)}${REGISTRY_EXT} already exists.` : undefined),
});
if (name === undefined) return;
const target = vscode.Uri.joinPath(folder, `${diagramName(name).replace(/\.archify-registry$/i, '')}${REGISTRY_EXT}`);
const template = await readText(vscode.Uri.joinPath(ctx.extension.extensionUri, 'templates', 'registry.archify-registry'));
await vscode.workspace.fs.writeFile(target, Buffer.from(template, 'utf8'));
await vscode.commands.executeCommand('vscode.openWith', target, REGISTRY_VIEW_TYPE);
}),
// Creates a decision file in the clicked folder of a model and opens it.
vscode.commands.registerCommand('archify.newDecision', async (arg?: unknown) => {
const folder = await parentFolder(arg);
if (!folder) return;
const root = await ctx.models.rootOf(vscode.Uri.joinPath(folder, 'x'));
if (!root) {
vscode.window.showWarningMessage(`Archify: ${vscode.workspace.asRelativePath(folder, true)} is not inside a model (no ${MAIN_FILE} in it or a folder above it). Decisions belong to a model.`);
return;
}
const index = await ctx.models.index(root);
const title = await vscode.window.showInputBox({
title: 'Archify | New Decision',
prompt: `Title of the decision (file goes into ${vscode.workspace.asRelativePath(folder, true)})`,
placeHolder: 'OpenSearch for product search',
validateInput: async (value) => {
if (!value.trim()) return 'Enter a title.';
const id = slugify(value);
if (index.decisions.decisions.has(id)) return `A decision with id "${id}" already exists in this model.`;
if (await exists(vscode.Uri.joinPath(folder, `${id}${DECISION_EXT}`))) return `${id}${DECISION_EXT} already exists.`;
return undefined;
},
});
if (title === undefined) return;
const id = slugify(title);
const target = vscode.Uri.joinPath(folder, `${id}${DECISION_EXT}`);
const template = await readText(vscode.Uri.joinPath(ctx.extension.extensionUri, 'templates', 'decision.archify-decision'));
const today = new Date().toISOString().slice(0, 10);
const text = template.replace('__ID__', id).replace('__TITLE__', JSON.stringify(title.trim())).replace('__DATE__', today);
await vscode.workspace.fs.writeFile(target, Buffer.from(text, 'utf8'));
await vscode.commands.executeCommand('vscode.openWith', target, DECISION_VIEW_TYPE);
}),
// Creates a new model folder from one of the shipped examples and opens its model page.
vscode.commands.registerCommand('archify.newModel', async (arg?: unknown) => {
const picked = await vscode.window.showQuickPick(
MODEL_EXAMPLES.map((example) => ({ label: example.label, description: example.description, example })),
{ title: 'Archify | New Model', placeHolder: 'Example to start from' },
);
if (!picked) return;
const { example } = picked;
let parent = await parentFolder(arg);
if (!parent) {
const chosen = await vscode.window.showOpenDialog({ title: 'Archify | New Model', canSelectFiles: false, canSelectFolders: true, openLabel: 'Create model here' });
parent = chosen?.[0];
if (!parent) return;
}
const name = await vscode.window.showInputBox({
title: 'Archify | New Model',
prompt: `Folder name in ${vscode.workspace.asRelativePath(parent, true)}`,
value: example.folder,
validateInput: async (value) =>
invalidNameReason(value) ?? ((await exists(vscode.Uri.joinPath(parent, diagramName(value)))) ? `${diagramName(value)} already exists.` : undefined),
});
if (name === undefined) return;
const folderName = diagramName(name);
const target = vscode.Uri.joinPath(parent, folderName);
if (await exists(target)) {
vscode.window.showWarningMessage(`Archify: ${vscode.workspace.asRelativePath(target, true)} already exists.`);
return;
}
const source = vscode.Uri.joinPath(ctx.extension.extensionUri, 'templates', 'models', example.folder);
await vscode.workspace.fs.copy(source, target, { overwrite: false });
const main = vscode.Uri.joinPath(target, MAIN_FILE);
await vscode.workspace.fs.writeFile(main, Buffer.from(mainFromExample(await readText(main), folderName, example), 'utf8'));
await vscode.commands.executeCommand('vscode.openWith', main, DIAGRAM_EDITOR_VIEW_TYPE);
}),
];
}