Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
node_modules/
*.vsix
dist/
/core.[0-9]*
+12
View File
@@ -0,0 +1,12 @@
**/*
!package.json
!README.md
!language-configuration.json
!syntaxes/**
!extension.cjs
!graph.cjs
!inspection.cjs
!navigation.cjs
!reading.cjs
!hover.cjs
!dist/**
+50
View File
@@ -0,0 +1,50 @@
# Dever for VS Code
为 `.dever` 提供高亮、悬浮说明、源码跳转和调用流程。
## 安装
从 [发布页](https://git.dever.cc/dever/core/releases/tag/v0.0.1) 下载 `dever-language-0.0.1.vsix`,在 VS Code 执行 **Extensions: Install from VSIX**。
插件默认调用 PATH 中的 `dever`,需要带 `inspect` 的编译器。可用 `dever.command` 设置命令及固定参数,值为数组;远程执行时,编译器须能读取相同的源码路径。未信任的工作区不会执行编译器。
## 使用
| 操作 | 功能 |
| --- | --- |
| 悬浮函数 | 显示签名、主题说明和调用摘要 |
| F12 | 跳转定义 |
| Shift+F12 | 查看引用 |
| 显示调用层次结构 | 查看调用方和被调用方 |
| Dever: Preview Documentation | 打开主题说明 |
| Dever: Show Calls | 查看每处调用及源码位置 |
| Dever: Open Domain Links | 阅读领域调用图 |
源码与说明保存后刷新索引。未保存修改或源码检查错误会暂停相关导航并提示原因。查询不运行应用,也不调用 AI。
调用图内置 Mermaid,可离线阅读和点击节点跳转。修改代码后生成流程:
```sh
dever inspect . --links
dever inspect . --links --check
```
图保留分句、参数求值顺序、短路、集合处理、任务、事务和 Port/Adapter 选择。它展示可能执行的路径;部署配置决定实际 Adapter。
当前支持函数、API/CMD、REST、认证、Job、回调、SQL 和 Port/Adapter 导航,尚不提供通用类型和字段引用导航。源码与说明的组织见 [说明写法](https://git.dever.cc/dever/core/src/branch/main/DOCUMENTATION.md)。
## 开发插件
在本目录执行:
```sh
npm ci
npm run build
DEVER_TEST_BIN=/path/to/dever npm run test:navigation
npm test -- --markdown-grammar /path/to/markdown.tmLanguage.json
npm run package
```
流程图浏览器测试使用 `npm run test:flows`,需设置 `DEVER_TEST_BIN` 和 `DEVER_BROWSER_TOOLS`,后者指向已有 playwright-core、Chromium 的 node_modules;系统库路径按测试环境设置。
构建会把 Markdown 解析器和 Mermaid 放入 VSIX,用户无需另装 npm 或渲染库。开发阶段版本统一为 `0.0.1`。
+12
View File
@@ -0,0 +1,12 @@
const esbuild = require('esbuild');
const fs = require('node:fs/promises');
async function build() {
await fs.mkdir('dist', { recursive: true });
await Promise.all([
fs.copyFile(require.resolve('@mermaid-js/tiny'), 'dist/mermaid.js'),
fs.copyFile('flow-browser.js', 'dist/flow-browser.js'),
esbuild.build({ entryPoints: ['flow-preview.cjs'], bundle: true, outfile: 'dist/flow-preview.cjs', minify: true, legalComments: 'eof', platform: 'node', external: ['vscode'] })
]);
}
build().catch(error => { console.error(error); process.exitCode = 1; });
+7
View File
@@ -0,0 +1,7 @@
const { registerNavigation } = require('./navigation.cjs');
function activate(context) {
registerNavigation(context);
}
module.exports = { activate };
+39
View File
@@ -0,0 +1,39 @@
(() => {
const mermaid = globalThis.mermaid;
const vscode = acquireVsCodeApi();
mermaid.initialize({ startOnLoad: false, securityLevel: 'loose', theme: document.body.classList.contains('vscode-dark') ? 'dark' : 'default', flowchart: { htmlLabels: false } });
// 渲染失败保留原始 Mermaid 并显示原因,不把空白图当成成功。
async function render() {
let sequence = 0;
for (const code of document.querySelectorAll('pre > code.language-mermaid')) {
try {
const { svg } = await mermaid.render(`dever-flow-${sequence++}`, code.textContent);
const container = document.createElement('div');
container.className = 'dever-flow';
container.innerHTML = svg;
code.parentElement.replaceWith(container);
} catch (error) {
const message = document.createElement('p');
message.className = 'dever-flow-error';
message.textContent = `流程图渲染失败:${error.message}`;
code.parentElement.before(message);
}
// Mermaid 的连续 Promise 会占满微任务队列;每张图后让页面处理加载、滚动和点击。
await new Promise(resolve => setTimeout(resolve, 0));
}
document.getElementById(document.body.dataset.anchor)?.scrollIntoView();
document.body.dataset.rendered = 'true';
}
document.addEventListener('click', event => {
const link = event.target.closest('a');
if (!link) return;
const href = link.getAttribute('href') || link.getAttribute('xlink:href');
if (!href) return;
event.preventDefault();
if (href.startsWith('#')) document.getElementById(decodeURIComponent(href.slice(1)))?.scrollIntoView();
else vscode.postMessage({ type: 'open', href });
});
render();
})();
+84
View File
@@ -0,0 +1,84 @@
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 };
+28
View File
@@ -0,0 +1,28 @@
// 所有关系来自编译器;这里只按源码位置选择符号和分组显示。
function contains(range, position) {
const compare = (left, right) => left.line - right.line || left.character - right.character;
return compare(range.start, position) <= 0 && compare(position, range.end) < 0;
}
// 自动分派用于调用层级;不能抢走显式绑定或 Port 声明的“转到定义”。
const generated = new Set(['verify', 'rest-field', 'rest-model', 'adapter']);
function at(index, file, position) {
const calls = index.calls.filter(call => !generated.has(call.kind) && call.site.file === file && contains(call.site, position));
const targets = new Set(calls.map(call => call.callee));
if (targets.size) return index.symbols.filter(symbol => targets.has(symbol.id));
return index.symbols.filter(symbol => symbol.declarations.some(location =>
location.file === file && contains(location, position)));
}
function relations(index, id, incoming) {
const grouped = new Map();
for (const call of index.calls) {
if ((incoming ? call.callee : call.caller) !== id) continue;
const target = incoming ? call.caller : call.callee;
if (!grouped.has(target)) grouped.set(target, []);
grouped.get(target).push(call);
}
return [...grouped].map(([id, calls]) => ({ symbol: index.symbols.find(symbol => symbol.id === id), calls }));
}
module.exports = { at, relations, contains };
+60
View File
@@ -0,0 +1,60 @@
const vscode = require('vscode');
const { relations } = require('./graph.cjs');
const { uri } = require('./inspection.cjs');
function command(name, args) {
return `command:${name}?${encodeURIComponent(JSON.stringify(args))}`;
}
async function description(inspection, index, symbol, root) {
const value = index.documentation.find(item => item.symbol === symbol.name);
if (!value) return undefined;
const documentIndex = { files: [index.documentation_files[value.file]] };
await inspection.verify(documentIndex, 0, root);
return { ...value, uri: uri(documentIndex, 0, root) };
}
function registerHover(context, selector, inspection, lookup) {
context.subscriptions.push(vscode.languages.registerHoverProvider(selector, {
async provideHover(document, position) {
const state = await lookup(document, position);
if (!state?.symbols.length) return undefined;
const contents = [];
for (const symbol of state.symbols) {
const page = new vscode.MarkdownString();
page.isTrusted = { enabledCommands: ['dever.showCalls', 'dever.openDomainLinks', 'dever.previewDocumentation'] };
page.appendMarkdown(`**${symbol.name}**\n\n`);
if (state.index.documentation_errors.length) {
page.appendMarkdown('**说明校验未通过。** 失效或重复的标题详见“Dever”输出;签名和调用关系仍来自源码。\n\n');
}
for (const signature of symbol.signatures) page.appendCodeblock(signature, 'dever');
const detail = await description(inspection, state.index, symbol, state.root);
if (detail) {
page.baseUri = detail.uri;
page.appendMarkdown(`${detail.markdown}\n\n`);
} else {
page.appendMarkdown('尚未提供领域说明;签名和关系来自已保存源码。\n\n');
}
for (const incoming of [true, false]) {
const related = relations(state.index, symbol.id, incoming);
const label = incoming ? '调用方' : '调用目标';
const names = related.slice(0, 4).map(item => `\`${item.symbol.name}\``).join('、');
page.appendMarkdown(`${label}:${names || '当前静态索引中无记录'}${related.length > 4 ? `,共 ${related.length} 个` : ''}。\n\n`);
}
const links = [];
for (const site of symbol.declarations.slice(0, 1)) {
const target = uri(state.index, site.file, state.root).with({ fragment: `L${site.start.line + 1},${site.start.character + 1}` });
links.push(`[源码](${target.toString()})`);
}
if (detail) links.push(`[说明](${command('dever.previewDocumentation', [state.root, symbol.name])})`);
links.push(`[调用详情](${command('dever.showCalls', [state.root, symbol.name])})`);
links.push(`[领域流程](${command('dever.openDomainLinks', [state.root, symbol.name])})`);
page.appendMarkdown(links.join(' · '));
contents.push(page);
}
return new vscode.Hover(contents);
}
}));
}
module.exports = { registerHover, description, command };
+132
View File
@@ -0,0 +1,132 @@
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 };
+18
View File
@@ -0,0 +1,18 @@
{
"comments": {
"lineComment": "#"
},
"brackets": [["{", "}"], ["[", "]"], ["(", ")"]],
"autoClosingPairs": [
{ "open": "{", "close": "}", "notIn": ["string", "comment"] },
{ "open": "[", "close": "]", "notIn": ["string", "comment"] },
{ "open": "(", "close": ")", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] }
],
"surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]],
"wordPattern": "[A-Za-z_][A-Za-z0-9_]*",
"indentationRules": {
"increaseIndentPattern": "^\\s*(?!#)(?:[^\"#]|\"(?:\\\\.|[^\"\\\\])*\")*[({\\[]\\s*(?:#.*)?$",
"decreaseIndentPattern": "^\\s*[})\\]]"
}
}
+103
View File
@@ -0,0 +1,103 @@
const vscode = require('vscode');
const { createInspection, range, uri } = require('./inspection.cjs');
const { at, relations } = require('./graph.cjs');
const { registerReading } = require('./reading.cjs');
const { registerHover } = require('./hover.cjs');
function registerNavigation(context) {
const inspection = createInspection(context);
const selector = [
{ language: 'dever', scheme: 'file' },
{ language: 'dever', scheme: 'dever-source' }
];
async function item(index, symbol, root) {
const first = symbol.declarations[0];
if (!first) return undefined;
await inspection.verify(index, first.file, root);
const end = symbol.declarations.at(-1).end;
const span = range({ start: first.start, end });
const selected = new vscode.Range(span.start, span.start);
const value = new vscode.CallHierarchyItem(vscode.SymbolKind.Function, symbol.name, symbol.kind,
uri(index, first.file, root), span, selected);
return value;
}
async function lookup(document, position) {
const state = await inspection.current(document);
return state ? { ...state, symbols: at(state.index, state.file, position) } : undefined;
}
context.subscriptions.push(vscode.languages.registerDefinitionProvider(selector, {
async provideDefinition(document, position) {
const state = await lookup(document, position);
if (!state) return [];
return Promise.all(state.symbols.flatMap(symbol => symbol.declarations)
.map(site => inspection.location(state.index, site, state.root)));
}
}));
context.subscriptions.push(vscode.languages.registerReferenceProvider(selector, {
async provideReferences(document, position, options) {
const state = await lookup(document, position);
if (!state) return [];
const ids = new Set(state.symbols.map(symbol => symbol.id));
const sites = state.index.calls.filter(call => ids.has(call.callee)).map(call => call.site);
if (options.includeDeclaration) sites.push(...state.symbols.flatMap(symbol => symbol.declarations));
const unique = [...new Map(sites.map(site => [JSON.stringify(site), site])).values()];
return Promise.all(unique.map(site => inspection.location(state.index, site, state.root)));
}
}));
async function hierarchy(value, incoming) {
const document = await vscode.workspace.openTextDocument(value.uri);
const state = await inspection.current(document);
if (!state) throw new Error('请保存源码后重新打开调用层级。');
const { root, index, file } = state;
const start = value.selectionRange.start;
const symbol = index.symbols.find(symbol => symbol.name === value.name && symbol.declarations.some(site =>
site.file === file && site.start.line === start.line && site.start.character === start.character));
if (!symbol) throw new Error('函数位置已变化,请重新打开调用层级。');
await inspection.verify(index, symbol.declarations[0].file, root);
const result = [];
for (const relation of relations(index, symbol.id, incoming)) {
const target = await item(index, relation.symbol, root);
if (!target) continue;
for (const call of relation.calls) await inspection.verify(index, call.site.file, root);
const ranges = relation.calls.map(call => range(call.site));
result.push(incoming ? new vscode.CallHierarchyIncomingCall(target, ranges)
: new vscode.CallHierarchyOutgoingCall(target, ranges));
}
return result;
}
context.subscriptions.push(vscode.languages.registerCallHierarchyProvider(selector, {
async prepareCallHierarchy(document, position) {
const state = await lookup(document, position);
if (!state) return [];
return (await Promise.all(state.symbols.map(symbol => item(state.index, symbol, state.root)))).filter(Boolean);
},
provideCallHierarchyIncomingCalls(value) { return hierarchy(value, true); },
provideCallHierarchyOutgoingCalls(value) { return hierarchy(value, false); }
}));
context.subscriptions.push(vscode.languages.registerCodeLensProvider(selector, {
onDidChangeCodeLenses: inspection.changed.event,
async provideCodeLenses(document) {
const state = await inspection.current(document);
if (!state) return [];
return state.index.symbols.filter(symbol => symbol.kind !== 'generated'
&& symbol.declarations[0]?.file === state.file).map(symbol => {
const incoming = relations(state.index, symbol.id, true);
const count = incoming.reduce((total, relation) => total + relation.calls.length, 0);
const start = range(symbol.declarations[0]).start;
return new vscode.CodeLens(new vscode.Range(start, start), {
title: `${incoming.length} 个调用方 · ${count} 处调用 · 查看调用关系`,
command: 'dever.showCalls', arguments: [state.root, symbol.name]
});
});
}
}));
registerReading(context, inspection, lookup);
registerHover(context, selector, inspection, lookup);
}
module.exports = { registerNavigation };
+2458
View File
File diff suppressed because it is too large. Load diff
+110
View File
@@ -0,0 +1,110 @@
{
"name": "dever-language",
"displayName": "Dever Language",
"description": "Dever source navigation, domain documentation, hover and call flows.",
"version": "0.0.1",
"main": "./extension.cjs",
"activationEvents": [
"onLanguage:dever"
],
"publisher": "shemic",
"private": true,
"license": "UNLICENSED",
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Programming Languages"
],
"keywords": [
"dever",
"markdown",
"syntax highlighting"
],
"repository": {
"type": "git",
"url": "ssh://git@git.dever.cc:2222/dever/core.git",
"directory": "editor/vscode"
},
"contributes": {
"commands": [
{
"command": "dever.previewDocumentation",
"title": "Dever: Preview Documentation"
},
{
"command": "dever.showCalls",
"title": "Dever: Show Calls"
},
{
"command": "dever.openDomainLinks",
"title": "Dever: Open Domain Links"
}
],
"configuration": {
"title": "Dever",
"properties": {
"dever.command": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"default": [
"dever"
],
"description": "Compiler executable followed by fixed arguments; inspect is appended without a shell."
}
}
},
"languages": [
{
"id": "dever",
"aliases": [
"Dever",
"dever"
],
"extensions": [
".dever"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "dever",
"scopeName": "source.dever",
"path": "./syntaxes/dever.tmLanguage.json"
},
{
"scopeName": "markdown.dever.codeblock",
"path": "./syntaxes/dever-markdown.tmLanguage.json",
"injectTo": [
"text.html.markdown"
],
"embeddedLanguages": {
"meta.embedded.block.dever": "dever"
},
"tokenTypes": {
"meta.embedded.block.dever": "other"
}
}
]
},
"scripts": {
"build": "node build.cjs",
"vscode:prepublish": "npm run build",
"test": "node ../../test/editor-vscode/highlight.test.cjs",
"test:navigation": "node --test ../../test/editor-vscode/navigation.test.cjs",
"test:flows": "node --test ../../test/editor-vscode/flows.test.cjs",
"package": "vsce package --no-dependencies --skip-license"
},
"devDependencies": {
"@mermaid-js/tiny": "12.2.0",
"@vscode/vsce": "4.0.0",
"esbuild": "0.28.2",
"markdown-it": "15.0.3",
"vscode-oniguruma": "2.0.1",
"vscode-textmate": "9.2.0"
}
}
+119
View File
@@ -0,0 +1,119 @@
const vscode = require('vscode');
const path = require('node:path');
const { relations } = require('./graph.cjs');
const { uri, projectRoot } = require('./inspection.cjs');
const { openFlowPreview } = require('./dist/flow-preview.cjs');
const { description, command } = require('./hover.cjs');
function registerReading(context, inspection, lookup) {
const changed = new vscode.EventEmitter();
const previews = new Map();
context.subscriptions.push(changed, inspection.changed.event(() => {
for (const uri of previews.values()) changed.fire(uri);
}));
context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(document => {
previews.delete(document.uri.toString());
}));
const provider = {
onDidChange: changed.event,
async provideTextDocumentContent(documentUri) {
previews.set(documentUri.toString(), documentUri);
const { root, name } = JSON.parse(documentUri.query);
const index = await inspection.load(root);
const symbols = index.symbols.filter(symbol => symbol.name === name);
if (!symbols.length) throw new Error(`函数已不存在:${name}`);
const lines = [`# ${name}`, '', '静态源码关系;分支可能只执行一部分,任务派发不表示同步执行。', '', index.coverage, ''];
if (index.documentation_errors.length) lines.push('**说明校验未通过。** 详见“Dever”输出。', '');
function link(site) {
const file = index.files[site.file];
const target = uri(index, site.file, root).with({ fragment: `L${site.start.line + 1},${site.start.character + 1}` });
return `[${file.path}:${site.start.line + 1}](${target.toString()})`;
}
for (const symbol of symbols) {
for (const signature of symbol.signatures) lines.push('```dever', signature, '```', '');
const detail = await description(inspection, index, symbol, root);
if (detail) lines.push(detail.markdown, '', `[主题说明](${detail.uri.toString()})`, '');
for (const site of symbol.declarations) {
await inspection.verify(index, site.file, root);
lines.push('## 定义', '', link(site), '');
}
for (const incoming of [true, false]) {
lines.push(incoming ? '## 谁调用它' : '## 它调用谁', '');
const related = relations(index, symbol.id, incoming);
if (!related.length) lines.push('当前已检查源码中没有此类关系。', '');
else lines.push('| 函数 | 领域 | 关系 | 调用位置 | 定义 / 上下游 |', '| --- | --- | --- | --- | --- |');
for (const relation of related) {
for (const call of relation.calls) {
await inspection.verify(index, call.site.file, root);
const definitions = relation.symbol.declarations.map(link).join('、') || '内建操作';
const chain = command('dever.showCalls', [root, relation.symbol.name]);
lines.push(`| ${relation.symbol.name} | ${relation.symbol.name.split('.').slice(0, 2).join('.')} | ${call.kind} | ${link(call.site)} | ${definitions} · [上下游](${chain}) |`);
}
}
lines.push('');
}
}
return lines.join('\n');
}
};
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('dever-calls', provider));
context.subscriptions.push(vscode.commands.registerCommand('dever.showCalls', async (root, name) => {
try {
if (!root || !name) {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const state = await lookup(editor.document, editor.selection.active);
if (!state) throw new Error('请先保存源码,再查看调用关系。');
if (!state.symbols.length) throw new Error('请把光标放在函数或调用位置。');
root = state.root;
name = state.symbols[0].name;
}
const target = vscode.Uri.from({ scheme: 'dever-calls', path: '/calls.md', query: JSON.stringify({ root, name }) });
await openFlowPreview(context, target, root, {
name,
changed: inspection.changed,
read: name => provider.provideTextDocumentContent(target.with({ query: JSON.stringify({ root, name }) }))
});
} catch (error) { vscode.window.showErrorMessage(error.message); }
}));
context.subscriptions.push(vscode.commands.registerCommand('dever.previewDocumentation', async (root, name) => {
try {
const document = vscode.window.activeTextEditor?.document;
if (!root || !name) {
if (!document) return;
const state = await lookup(document, vscode.window.activeTextEditor.selection.active);
root = state?.root || await projectRoot(document);
name = state?.symbols[0]?.name;
}
const index = await inspection.load(root);
const symbol = index.symbols.find(value => value.name === name);
const detail = symbol && await description(inspection, index, symbol, root);
if (!detail) throw new Error('该符号尚未提供 doc/ 说明。');
await vscode.commands.executeCommand('markdown.showPreviewToSide', detail.uri.with({ fragment: `L${detail.line + 1}` }));
} catch (error) { vscode.window.showErrorMessage(error.message); }
}));
context.subscriptions.push(vscode.commands.registerCommand('dever.openDomainLinks', async (root, name) => {
try {
const document = vscode.window.activeTextEditor?.document;
if (!root && !document) return;
root ||= await projectRoot(document);
const index = await inspection.load(root);
const symbol = index.symbols.find(value => value.name === name);
const source = symbol?.declarations[0];
const filename = source ? index.files[source.file].path : document.uri.fsPath;
const relative = path.relative(path.join(root, 'module'), filename).split(path.sep);
if (relative.length < 3 || relative[0] === '..') {
throw new Error('请在 module/<组件>/<领域>/ 内的源码中执行此命令。');
}
const target = vscode.Uri.file(path.join(root, 'module', ...relative.slice(0, 2), 'link.md'));
await inspection.checkLinks(root);
const panel = await openFlowPreview(context, target, root);
const changed = inspection.changed.event(() => panel.dispose());
panel.onDidDispose(() => changed.dispose());
} catch (error) {
vscode.window.showErrorMessage(`${error.message} 可在项目根运行 dever inspect . --links 生成索引。`);
}
}));
}
module.exports = { registerReading };
@@ -0,0 +1,31 @@
{
"name": "Dever Markdown blocks",
"scopeName": "markdown.dever.codeblock",
"injectionSelector": "L:text.html.markdown -markup.fenced_code.block -markup.raw.block -markup.quote -markup.list -meta.embedded.block -comment",
"patterns": [
{
"name": "markup.fenced_code.block.markdown",
"contentName": "meta.embedded.block.dever source.dever",
"begin": "^ {0,3}(`{3,})[ \\t]*(dever|typescript dever)[ \\t]*$",
"end": "^ {0,3}\\1`* *$",
"beginCaptures": {
"1": { "name": "punctuation.definition.markdown.dever" },
"2": { "name": "fenced_code.block.language.markdown" }
},
"endCaptures": { "0": { "name": "punctuation.definition.markdown.dever" } },
"patterns": [{ "include": "source.dever" }]
},
{
"name": "markup.fenced_code.block.markdown",
"contentName": "meta.embedded.block.dever source.dever",
"begin": "^ {0,3}(~{3,})[ \\t]*(dever|typescript dever)[ \\t]*$",
"end": "^ {0,3}\\1~* *$",
"beginCaptures": {
"1": { "name": "punctuation.definition.markdown.dever" },
"2": { "name": "fenced_code.block.language.markdown" }
},
"endCaptures": { "0": { "name": "punctuation.definition.markdown.dever" } },
"patterns": [{ "include": "source.dever" }]
}
]
}
@@ -0,0 +1,95 @@
{
"name": "Dever",
"scopeName": "source.dever",
"patterns": [
{ "include": "#comments" },
{ "include": "#strings" },
{ "include": "#unsupported" },
{ "include": "#declarations" },
{ "include": "#constants" },
{ "include": "#numbers" },
{ "include": "#names" },
{ "include": "#keywords" },
{ "include": "#operators" }
],
"repository": {
"comments": {
"name": "comment.line.number-sign.dever",
"begin": "#",
"end": "$"
},
"strings": {
"name": "string.quoted.double.dever",
"begin": "\"",
"end": "\"|$",
"beginCaptures": { "0": { "name": "punctuation.definition.string.begin.dever" } },
"endCaptures": { "0": { "name": "punctuation.definition.string.end.dever" } },
"patterns": [
{ "name": "constant.character.escape.dever", "match": "\\\\(?:[\"\\\\nrt]|u\\{[0-9A-Fa-f]{1,6}\\})" },
{ "name": "invalid.illegal.escape.dever", "match": "\\\\." }
]
},
"declarations": {
"patterns": [
{
"match": "\\b(type)\\s+([A-Z][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.type.dever" },
"2": { "name": "entity.name.type.dever" }
}
},
{
"match": "^\\s*(?:(public)\\s+)?(get|post|put|delete|cmd|rest)\\b",
"captures": {
"1": { "name": "storage.modifier.dever" },
"2": { "name": "keyword.control.api.dever" }
}
},
{
"match": "^\\s*(external)\\s+(command|exec|pip|npm|go)\\b",
"captures": {
"1": { "name": "storage.modifier.external.dever" },
"2": { "name": "keyword.other.external.dever" }
}
},
{ "name": "keyword.other.declaration.dever", "match": "^\\s*(database|relation|seed|migrate|sql|index|unique|schedule)\\b" }
]
},
"constants": {
"name": "constant.language.dever",
"match": "\\b(true|false|null|other)\\b"
},
"unsupported": {
"name": "invalid.illegal.keyword.dever",
"match": "\\b(package|exposes|if|else|switch|match|when|by|for|while|break|continue|iterate|recur|repeat|return|function|throw|catch|try|import|use|class|interface|unsafe|macro)\\b"
},
"numbers": {
"name": "constant.numeric.dever",
"match": "(?<![A-Za-z0-9_])[0-9]+(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?(?![A-Za-z0-9_])"
},
"names": {
"patterns": [
{ "name": "variable.other.member.dever", "match": "\\b[a-z_][A-Za-z0-9_]*(?=\\s*:)" },
{ "name": "entity.name.function.dever", "match": "\\b[a-z_][A-Za-z0-9_]*(?=\\s*\\()" },
{ "name": "support.type.dever", "match": "\\b(Int|Float|Decimal|Bool|Text|Bytes|Secret|List|Map|Stream|AsyncStream|Channel|Task|Group|Json|Date|Time|DateTime|Duration|Id|Uuid)\\b" },
{ "name": "entity.name.type.dever", "match": "\\b[A-Z][A-Za-z0-9_]*\\b" },
{ "name": "entity.name.namespace.dever", "match": "\\b(dever|app|domain|model|port|adapter)(?=\\.)" }
]
},
"keywords": {
"patterns": [
{ "name": "keyword.operator.logical.dever", "match": "\\b(and|or|not)\\b" },
{ "name": "storage.modifier.dever", "match": "\\b(public|global|pure|transaction|job|recover|fails|private|owner)\\b" },
{ "name": "keyword.other.model.dever", "match": "\\b(default|unique|index|raw|lower|upper|before|after|drop)\\b" },
{ "name": "keyword.other.adapter.dever", "match": "\\b(setting|lib|allow)\\b" }
]
},
"operators": {
"patterns": [
{ "name": "keyword.operator.dever", "match": "//|==|!=|<=|>=|[=+*/%<>?-]" },
{ "name": "punctuation.section.dever", "match": "[{}()\\[\\]]" },
{ "name": "punctuation.separator.dever", "match": "[,:.]" }
]
}
}
}