- 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
136 lines
7.5 KiB
TypeScript
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);
|
|
}),
|
|
];
|
|
}
|