Files
core/editor/vscode/flow-preview.cjs
T
2026-11-03 03:32:07 +00:00

85 lines
5.3 KiB
JavaScript

const vscode = require('vscode');
const fs = require('node:fs/promises');
const path = require('node:path');
const { randomBytes } = require('node:crypto');
const { pathToFileURL, fileURLToPath } = require('node:url');
const markdown = require('markdown-it')({ html: true });
const validateLink = markdown.validateLink;
markdown.validateLink = href => href.startsWith('file:') || validateLink(href);
// 流程图与调用表共用阅读面板;链接打开真实源码或当前索引。
async function openFlowPreview(context, target, root, calls) {
const panel = vscode.window.createWebviewPanel('dever.links', 'Dever 调用流程', vscode.ViewColumn.Beside, {
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'dist')]
});
let current = target.fsPath;
async function render(filename, anchor = '') {
const text = calls ? await calls.read(filename) : await fs.readFile(filename, 'utf8');
if (!calls && !text.startsWith('<!-- Generated by dever inspect --links. Do not edit. -->')) {
throw new Error('此文件不是生成的领域索引,请运行 dever inspect . --links。');
}
current = filename;
panel.title = calls ? `Dever · ${filename}` : `Dever · ${path.basename(path.dirname(filename))}`;
const script = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'dist', 'flow-browser.js'));
const mermaid = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'dist', 'mermaid.js'));
const nonce = randomBytes(16).toString('hex');
panel.webview.html = `<!DOCTYPE html><html><head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${panel.webview.cspSource} 'unsafe-inline'; img-src ${panel.webview.cspSource} data:; font-src ${panel.webview.cspSource} data:;">
<style>body{max-width:1100px;margin:24px auto;padding:0 24px;font:var(--vscode-font-size) var(--vscode-font-family);color:var(--vscode-editor-foreground);background:var(--vscode-editor-background)}a{color:var(--vscode-textLink-foreground)}table{border-collapse:collapse}td,th{padding:6px 12px;border:1px solid var(--vscode-panel-border)}pre{overflow:auto}details{margin:16px 0}.dever-flow{overflow:auto;margin:20px 0}.dever-flow svg{max-width:none!important}.dever-flow-error{color:var(--vscode-errorForeground)}</style>
</head><body data-anchor="${markdown.utils.escapeHtml(anchor)}">${markdown.render(text)}
<script nonce="${nonce}" src="${mermaid}"></script><script nonce="${nonce}" src="${script}"></script></body></html>`;
}
const messages = panel.webview.onDidReceiveMessage(async message => {
if (message.type !== 'open' || typeof message.href !== 'string') return;
try {
if (calls && message.href.startsWith('command:dever.showCalls?')) {
const [requestedRoot, name] = JSON.parse(decodeURIComponent(message.href.split('?')[1]));
if (requestedRoot !== root) throw new Error('链接超出当前项目。');
await render(name);
return;
}
if (calls && message.href.startsWith('dever-source:')) {
const target = vscode.Uri.parse(message.href);
if (JSON.parse(target.query).root !== root) throw new Error('链接超出当前项目。');
const document = await vscode.workspace.openTextDocument(target);
const location = /^#L(\d+)(?:,(\d+))?$/.exec(new URL(message.href).hash);
const line = location ? Number(location[1]) - 1 : 0;
const column = location?.[2] ? Number(location[2]) - 1 : 0;
await vscode.window.showTextDocument(document, { viewColumn: vscode.ViewColumn.One, selection: new vscode.Range(line, column, line, column) });
return;
}
const destination = new URL(message.href, pathToFileURL(calls ? root + path.sep : current));
if (destination.protocol !== 'file:') throw new Error('流程阅读页只打开项目源码和领域索引。');
const filename = fileURLToPath(destination);
const relative = path.relative(root, filename);
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
throw new Error('链接超出当前项目。');
}
if (!calls && path.basename(filename) === 'link.md') {
await render(filename, decodeURIComponent(destination.hash.slice(1)));
} else {
const location = /^#L(\d+)(?:,(\d+))?$/.exec(destination.hash);
const line = location ? Number(location[1]) - 1 : 0;
const column = location?.[2] ? Number(location[2]) - 1 : 0;
const document = await vscode.workspace.openTextDocument(vscode.Uri.file(filename));
await vscode.window.showTextDocument(document, { viewColumn: vscode.ViewColumn.One, selection: new vscode.Range(line, column, line, column) });
}
} catch (error) { vscode.window.showErrorMessage(error.message); }
});
panel.onDidDispose(() => messages.dispose());
if (calls) {
const changed = calls.changed.event(() => render(current).catch(error => {
panel.dispose();
vscode.window.showErrorMessage(error.message);
}));
panel.onDidDispose(() => changed.dispose());
}
context.subscriptions.push(panel);
try { await render(calls ? calls.name : current); } catch (error) { panel.dispose(); throw error; }
return panel;
}
module.exports = { openFlowPreview };