133 lines
5.9 KiB
JavaScript
133 lines
5.9 KiB
JavaScript
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 };
|