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('')) { 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 = ` ${markdown.render(text)} `; } 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 };