Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
*.vsix
|
||||
dist/
|
||||
/core.[0-9]*
|
||||
@@ -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/**
|
||||
@@ -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`。
|
||||
@@ -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; });
|
||||
@@ -0,0 +1,7 @@
|
||||
const { registerNavigation } = require('./navigation.cjs');
|
||||
|
||||
function activate(context) {
|
||||
registerNavigation(context);
|
||||
}
|
||||
|
||||
module.exports = { activate };
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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*[})\\]]"
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
Generated
+2458
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "[,:.]" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user