const vscode = require('vscode'); const path = require('node:path'); const fs = require('node:fs/promises'); const { createHash } = require('node:crypto'); const { execFile } = require('node:child_process'); const { promisify } = require('node:util'); const execute = promisify(execFile); function isSource(document) { return document.languageId === 'dever'; } async function projectRoot(document) { if (document.uri.scheme === 'dever-source') return JSON.parse(document.uri.query).root; const folder = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath; for (let directory = path.dirname(document.uri.fsPath); ; directory = path.dirname(directory)) { try { await fs.stat(path.join(directory, 'config', 'setting.json')); return directory; } catch (error) { if (error.code !== 'ENOENT') throw error; } if (directory === folder || directory === path.dirname(directory)) break; } return folder || document.uri.fsPath; } function uri(index, file, root) { const source = index.files[file]; return source.text === undefined ? vscode.Uri.file(source.path) : vscode.Uri.from({ scheme: 'dever-source', path: `/${source.path}`, query: JSON.stringify({ root, path: source.path }) }); } function range(location) { return new vscode.Range(location.start.line, location.start.character, location.end.line, location.end.character); } function createInspection(context) { const cache = new Map(); const changed = new vscode.EventEmitter(); const output = vscode.window.createOutputChannel('Dever'); context.subscriptions.push(changed, output); const invalidate = () => { cache.clear(); changed.fire(); }; const watcher = vscode.workspace.createFileSystemWatcher('**/{*.dever,README.md,doc/*.md,dever.lock,dever.api,setting.json}'); context.subscriptions.push(watcher, watcher.onDidChange(invalidate), watcher.onDidCreate(invalidate), watcher.onDidDelete(invalidate)); context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => { if (isSource(document) || /[/\\]doc[/\\][^/\\]+\.md$/.test(document.uri.fsPath)) invalidate(); })); context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(event => { if (event.affectsConfiguration('dever.command')) invalidate(); })); async function load(root) { if (!vscode.workspace.isTrusted) throw new Error('请信任工作区后再运行 Dever 源码检查。'); if (!cache.has(root)) { const command = vscode.workspace.getConfiguration('dever', vscode.Uri.file(root)).get('command'); const promise = execute(command[0], [...command.slice(1), 'inspect', root, '--json'], { maxBuffer: 32 * 1024 * 1024, windowsHide: true }).then(({ stdout }) => { const index = JSON.parse(stdout); for (const error of index.documentation_errors) output.appendLine(error); return index; }).catch(error => { if (cache.get(root) === promise) cache.delete(root); const message = error.stderr?.trim() || error.message; output.appendLine(message); throw new Error(message); }); cache.set(root, promise); } const pending = cache.get(root); const index = await pending; if (cache.get(root) !== pending) throw new Error('检查期间源码已变化,请重新查询调用关系。'); return index; } async function verify(index, file, root) { const document = await vscode.workspace.openTextDocument(uri(index, file, root)); if (document.isDirty) throw new Error('调用关系基于已保存源码,请先保存文件。'); const digest = createHash('sha256').update(document.getText()).digest('hex'); if (digest !== index.files[file].sha256) { invalidate(); throw new Error('源码已变化,请刷新调用关系。'); } return document; } async function current(document) { if (document.isDirty) return undefined; const root = await projectRoot(document); if (vscode.workspace.textDocuments.some(value => value.isDirty && isSource(value) && (value.uri.fsPath === root || value.uri.fsPath.startsWith(`${root}${path.sep}`)))) return undefined; const index = await load(root); const sourcePath = document.uri.scheme === 'dever-source' ? JSON.parse(document.uri.query).path : document.uri.fsPath; const file = index.files.findIndex(source => source.path === sourcePath); if (file < 0) return undefined; await verify(index, file, root); return { root, index, file }; } async function location(index, site, root) { await verify(index, site.file, root); return new vscode.Location(uri(index, site.file, root), range(site)); } async function checkLinks(root) { if (!vscode.workspace.isTrusted) throw new Error('请信任工作区后再检查领域流程。'); if (vscode.workspace.textDocuments.some(document => document.isDirty && isSource(document) && document.uri.fsPath.startsWith(`${root}${path.sep}`))) throw new Error('请先保存源码,再查看领域流程。'); const command = vscode.workspace.getConfiguration('dever', vscode.Uri.file(root)).get('command'); try { await execute(command[0], [...command.slice(1), 'inspect', root, '--links', '--check'], { windowsHide: true }); } catch (error) { throw new Error(error.stderr?.trim() || error.stdout?.trim() || error.message); } } context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('dever-source', { async provideTextDocumentContent(uri) { const { root, path } = JSON.parse(uri.query); const index = await load(root); const source = index.files.find(source => source.path === path) || index.documentation_files.find(description => description.path === path); if (source?.text === undefined) throw new Error(`未找到内置源码:${path}`); return source.text; } })); return { load, current, verify, location, checkLinks, invalidate, changed }; } module.exports = { createInspection, range, uri, isSource, projectRoot };