@dot-agent/language-server 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/features/definition.js +40 -8
- package/features/diagnostics.js +78 -5
- package/features/graph.js +53 -0
- package/features/links.js +33 -5
- package/package.json +2 -2
- package/parser.js +7 -0
- package/server.js +30 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A standalone [Language Server Protocol (LSP)](https://microsoft.github.io/langua
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
| Capability | `.
|
|
7
|
+
| Capability | `.description` | `.behavior` |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| **Hover** | Keyword documentation | Keyword documentation |
|
|
10
10
|
| **Completion** | Manifest keywords, custom types | Keywords, state names, memory domains (`context.`, `session.`, …) |
|
|
@@ -36,7 +36,7 @@ A standalone [Language Server Protocol (LSP)](https://microsoft.github.io/langua
|
|
|
36
36
|
|
|
37
37
|
The server speaks LSP over `stdio`. Each editor starts it as a subprocess and communicates via JSON-RPC messages.
|
|
38
38
|
|
|
39
|
-
All structural analysis uses the **tree-sitter** parse trees from [`@dot-agent/tree-sitter
|
|
39
|
+
All structural analysis uses the **tree-sitter** parse trees from [`@dot-agent/tree-sitter`](https://github.com/dot-agent-spec/tree-sitter). `parser.js` initializes the WASM-based parsers during `initialize` and maintains a per-document AST cache with incremental reparse.
|
|
40
40
|
|
|
41
41
|
## Prerequisites
|
|
42
42
|
|
|
@@ -84,7 +84,7 @@ if not configs.agent_dsl then
|
|
|
84
84
|
configs.agent_dsl = {
|
|
85
85
|
default_config = {
|
|
86
86
|
cmd = { 'node', '/path/to/language-server/server.js', '--stdio' },
|
|
87
|
-
filetypes = { '
|
|
87
|
+
filetypes = { 'description', 'behavior' },
|
|
88
88
|
root_dir = lspconfig.util.root_pattern('.git'),
|
|
89
89
|
},
|
|
90
90
|
}
|
|
@@ -97,7 +97,7 @@ lspconfig.agent_dsl.setup {}
|
|
|
97
97
|
|
|
98
98
|
```toml
|
|
99
99
|
[[language]]
|
|
100
|
-
name = "
|
|
100
|
+
name = "description"
|
|
101
101
|
language-servers = ["agent-dsl-lsp"]
|
|
102
102
|
|
|
103
103
|
[[language]]
|
package/features/definition.js
CHANGED
|
@@ -16,7 +16,33 @@
|
|
|
16
16
|
|
|
17
17
|
'use strict';
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const { fileURLToPath, pathToFileURL } = require('url');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { nodesOfType, nodeToRange, wordAtPosition, parseText } = require('../parser');
|
|
23
|
+
|
|
24
|
+
// Busca recursiva de state_decl em arquivos mergeados.
|
|
25
|
+
// visited (Set<absPath>) evita loops em grafos de merge circulares.
|
|
26
|
+
function findStateInMerges(tree, docDir, word, visited = new Set()) {
|
|
27
|
+
for (const mergeNode of nodesOfType(tree, 'merge_decl')) {
|
|
28
|
+
const pathNode = mergeNode.childForFieldName('path');
|
|
29
|
+
if (!pathNode) continue;
|
|
30
|
+
const filename = pathNode.text.replace(/^"|"$/g, '');
|
|
31
|
+
const absPath = path.resolve(docDir, filename);
|
|
32
|
+
if (visited.has(absPath)) continue;
|
|
33
|
+
visited.add(absPath);
|
|
34
|
+
let mergedText;
|
|
35
|
+
try { mergedText = fs.readFileSync(absPath, 'utf8'); } catch { continue; }
|
|
36
|
+
const mergedTree = parseText('behavior', mergedText);
|
|
37
|
+
if (!mergedTree) continue;
|
|
38
|
+
const found = nodesOfType(mergedTree, 'state_decl')
|
|
39
|
+
.find(n => n.childForFieldName('name')?.text === word);
|
|
40
|
+
if (found) return { uri: pathToFileURL(absPath).toString(), range: nodeToRange(found) };
|
|
41
|
+
const sub = findStateInMerges(mergedTree, path.dirname(absPath), word, visited);
|
|
42
|
+
if (sub) return sub;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
20
46
|
|
|
21
47
|
function provideDefinition(langId, tree, text, uri, position) {
|
|
22
48
|
if (!tree) return null;
|
|
@@ -24,19 +50,25 @@ function provideDefinition(langId, tree, text, uri, position) {
|
|
|
24
50
|
const { word } = wordAtPosition(text, position.line, position.character);
|
|
25
51
|
if (!word) return null;
|
|
26
52
|
|
|
27
|
-
let targetNode = null;
|
|
28
|
-
|
|
29
53
|
if (langId === 'behavior') {
|
|
30
|
-
|
|
54
|
+
const local = nodesOfType(tree, 'state_decl')
|
|
31
55
|
.find(n => n.childForFieldName('name')?.text === word);
|
|
32
|
-
|
|
56
|
+
if (local) return { uri, range: nodeToRange(local) };
|
|
57
|
+
try {
|
|
58
|
+
const docDir = path.dirname(fileURLToPath(uri));
|
|
59
|
+
return findStateInMerges(tree, docDir, word) ?? null;
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (langId === 'description') {
|
|
33
64
|
if (!/^[A-Z]/.test(word) && !word.includes('.')) return null;
|
|
34
|
-
targetNode = nodesOfType(tree, 'type_decl')
|
|
65
|
+
const targetNode = nodesOfType(tree, 'type_decl')
|
|
35
66
|
.find(n => n.childForFieldName('name')?.text === word);
|
|
67
|
+
if (!targetNode) return null;
|
|
68
|
+
return { uri, range: nodeToRange(targetNode) };
|
|
36
69
|
}
|
|
37
70
|
|
|
38
|
-
|
|
39
|
-
return { uri, range: nodeToRange(targetNode) };
|
|
71
|
+
return null;
|
|
40
72
|
}
|
|
41
73
|
|
|
42
74
|
module.exports = { provideDefinition };
|
package/features/diagnostics.js
CHANGED
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
'use strict';
|
|
18
18
|
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const { fileURLToPath } = require('url');
|
|
21
|
+
const path = require('path');
|
|
19
22
|
const { DiagnosticSeverity } = require('vscode-languageserver');
|
|
20
|
-
const { nodesOfType, nodeToRange } = require('../parser');
|
|
23
|
+
const { nodesOfType, nodeToRange, parseText } = require('../parser');
|
|
21
24
|
|
|
22
25
|
const STRICT_BLOCK_TYPES = {
|
|
23
26
|
input_block: 'input',
|
|
@@ -70,17 +73,87 @@ function diagnoseDescription(tree, text) {
|
|
|
70
73
|
return diagnostics;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
// Coleta recursivamente todos os state names declarados nos arquivos mergeados.
|
|
77
|
+
// visited (Set<absPath>) evita loops em grafos de merge circulares.
|
|
78
|
+
function collectMergedStates(tree, docDir, definedStates, visited = new Set()) {
|
|
79
|
+
for (const mergeNode of nodesOfType(tree, 'merge_decl')) {
|
|
80
|
+
const pathNode = mergeNode.childForFieldName('path');
|
|
81
|
+
if (!pathNode) continue;
|
|
82
|
+
const filename = pathNode.text.replace(/^"|"$/g, '');
|
|
83
|
+
const absPath = path.resolve(docDir, filename);
|
|
84
|
+
if (visited.has(absPath)) continue;
|
|
85
|
+
visited.add(absPath);
|
|
86
|
+
let mergedText;
|
|
87
|
+
try { mergedText = fs.readFileSync(absPath, 'utf8'); } catch { continue; }
|
|
88
|
+
const mergedTree = parseText('behavior', mergedText);
|
|
89
|
+
if (!mergedTree) continue;
|
|
90
|
+
nodesOfType(mergedTree, 'state_decl').forEach(n => {
|
|
91
|
+
const name = n.childForFieldName('name')?.text;
|
|
92
|
+
if (name) definedStates.add(name);
|
|
93
|
+
});
|
|
94
|
+
collectMergedStates(mergedTree, path.dirname(absPath), definedStates, visited);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Coleta erros de sintaxe (nós ERROR e MISSING) para que a causa real apareça
|
|
99
|
+
// sublinhada no editor, e não apenas o efeito colateral (ex.: "state not defined").
|
|
100
|
+
// Reporta apenas o ERROR mais externo de cada ramo, e tokens MISSING (ex.: falta 'to').
|
|
101
|
+
function collectSyntaxErrors(root, diagnostics) {
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
|
|
104
|
+
function push(node, message) {
|
|
105
|
+
const key = `${node.startIndex}:${node.endIndex}:${message}`;
|
|
106
|
+
if (seen.has(key)) return;
|
|
107
|
+
seen.add(key);
|
|
108
|
+
diagnostics.push({
|
|
109
|
+
range: nodeToRange(node),
|
|
110
|
+
message,
|
|
111
|
+
severity: DiagnosticSeverity.Error,
|
|
112
|
+
source: 'behavior-dsl',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function walk(node) {
|
|
117
|
+
if (node.isMissing) {
|
|
118
|
+
push(node, `Syntax error: missing '${node.type}'.`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (node.type === 'ERROR') {
|
|
122
|
+
// Reporta o ERROR mais profundo (folha) para apontar o token preciso,
|
|
123
|
+
// em vez do span externo que pode embrulhar o bloco inteiro.
|
|
124
|
+
const hasNestedError = node.descendantsOfType('ERROR').some(e => e.id !== node.id);
|
|
125
|
+
if (!hasNestedError) {
|
|
126
|
+
const snippet = node.text.replace(/\s+/g, ' ').trim().slice(0, 40);
|
|
127
|
+
push(node, snippet ? `Syntax error near '${snippet}'.` : 'Syntax error.');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// tem ERROR aninhado → desce para reportar o(s) mais específico(s)
|
|
131
|
+
}
|
|
132
|
+
for (const child of node.children) walk(child);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
walk(root);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function diagnoseBehavior(tree, docUri) {
|
|
74
139
|
const diagnostics = [];
|
|
75
140
|
|
|
141
|
+
// ── Rule 0: Syntax errors (ERROR / MISSING nodes) ─────────────────────────
|
|
142
|
+
collectSyntaxErrors(tree.rootNode, diagnostics);
|
|
143
|
+
|
|
76
144
|
// ── Rule 1: Dangling transitions ──────────────────────────────────────────
|
|
77
|
-
// Collect all defined state names (from both oriented_state_body and setup_state_body)
|
|
78
145
|
const definedStates = new Set(
|
|
79
146
|
nodesOfType(tree, 'state_decl')
|
|
80
147
|
.map(n => n.childForFieldName('name')?.text)
|
|
81
148
|
.filter(Boolean)
|
|
82
149
|
);
|
|
83
150
|
|
|
151
|
+
// Enriquecer com estados de arquivos mergeados (recursivo, anti-loop)
|
|
152
|
+
try {
|
|
153
|
+
const docDir = path.dirname(fileURLToPath(docUri));
|
|
154
|
+
collectMergedStates(tree, docDir, definedStates);
|
|
155
|
+
} catch { /* URI inválida ou sem acesso — diagnostica apenas com estados locais */ }
|
|
156
|
+
|
|
84
157
|
function checkTransitionTarget(stateNode) {
|
|
85
158
|
if (!stateNode) return;
|
|
86
159
|
const target = stateNode.text;
|
|
@@ -129,10 +202,10 @@ function diagnoseBehavior(tree) {
|
|
|
129
202
|
return diagnostics;
|
|
130
203
|
}
|
|
131
204
|
|
|
132
|
-
function diagnose(langId, tree, text) {
|
|
205
|
+
function diagnose(langId, tree, text, uri) {
|
|
133
206
|
if (!tree) return [];
|
|
134
207
|
if (langId === 'description') return diagnoseDescription(tree, text);
|
|
135
|
-
if (langId === 'behavior') return diagnoseBehavior(tree);
|
|
208
|
+
if (langId === 'behavior') return diagnoseBehavior(tree, uri);
|
|
136
209
|
return [];
|
|
137
210
|
}
|
|
138
211
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Danilo Borges (https://github.com/daniloborges)
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { nodesOfType } = require('../parser');
|
|
20
|
+
|
|
21
|
+
function extractBehaviorGraph(tree) {
|
|
22
|
+
const states = nodesOfType(tree, 'state_decl')
|
|
23
|
+
.map(n => n.childForFieldName('name')?.text)
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
|
|
26
|
+
const transitions = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
for (const stateNode of nodesOfType(tree, 'state_decl')) {
|
|
29
|
+
const from = stateNode.childForFieldName('name')?.text;
|
|
30
|
+
if (!from) continue;
|
|
31
|
+
for (const t of stateNode.descendantsOfType('transition_stmt')) {
|
|
32
|
+
const to = t.childForFieldName('state')?.text;
|
|
33
|
+
if (to) {
|
|
34
|
+
const key = `${from}→${to}`;
|
|
35
|
+
if (!seen.has(key)) { seen.add(key); transitions.push({ from, to }); }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const entryPoints = [];
|
|
41
|
+
for (const triggerNode of nodesOfType(tree, 'trigger_decl')) {
|
|
42
|
+
// event field is a quoted_string WITHOUT surrounding quotes (quotes are literals in the grammar rule)
|
|
43
|
+
const eventText = triggerNode.childForFieldName('event')?.text;
|
|
44
|
+
for (const t of triggerNode.descendantsOfType('transition_stmt')) {
|
|
45
|
+
const to = t.childForFieldName('state')?.text;
|
|
46
|
+
if (eventText && to) entryPoints.push({ event: eventText, to });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { states, transitions, entryPoints };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { extractBehaviorGraph };
|
package/features/links.js
CHANGED
|
@@ -20,6 +20,16 @@ const { fileURLToPath, pathToFileURL } = require('url');
|
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const { nodesOfType, nodeToRange } = require('../parser');
|
|
22
22
|
|
|
23
|
+
// Detecta se um nó bare_string representa um caminho de arquivo.
|
|
24
|
+
// Usa o subtipo do tree-sitter (filename) como primeira fonte de verdade;
|
|
25
|
+
// cai no regex do grammar.js como fallback para ambientes que não expõem subnós.
|
|
26
|
+
// O padrão aceita: extensão simples (a.md), dupla (a.b.persona), longa (.behavior)
|
|
27
|
+
// e exclui texto livre (que tem espaços/pontuação fora do charset).
|
|
28
|
+
function isFilename(node) {
|
|
29
|
+
if (node.firstNamedChild?.type === 'filename') return true;
|
|
30
|
+
return /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(node.text.replace(/^"|"$/g, ''));
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
function provideDocumentLinks(langId, tree, docUri) {
|
|
24
34
|
if (!tree) return [];
|
|
25
35
|
|
|
@@ -32,8 +42,8 @@ function provideDocumentLinks(langId, tree, docUri) {
|
|
|
32
42
|
|
|
33
43
|
const links = [];
|
|
34
44
|
|
|
35
|
-
function
|
|
36
|
-
const filename = rawText.replace(/^"|"$/g, '');
|
|
45
|
+
function addFileLink(fileNode, rawText) {
|
|
46
|
+
const filename = rawText.replace(/^"|"$/g, '');
|
|
37
47
|
if (!filename) return;
|
|
38
48
|
links.push({
|
|
39
49
|
range: nodeToRange(fileNode),
|
|
@@ -41,23 +51,41 @@ function provideDocumentLinks(langId, tree, docUri) {
|
|
|
41
51
|
});
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
function addUrlLink(uriNode) {
|
|
55
|
+
const url = uriNode.text.trim();
|
|
56
|
+
if (!url) return;
|
|
57
|
+
links.push({ range: nodeToRange(uriNode), target: url });
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
if (langId === 'description') {
|
|
45
61
|
for (const node of nodesOfType(tree, 'behavior_block')) {
|
|
46
62
|
const fileNode = node.childForFieldName('file');
|
|
47
|
-
if (fileNode)
|
|
63
|
+
if (fileNode) addFileLink(fileNode, fileNode.text);
|
|
64
|
+
}
|
|
65
|
+
for (const node of nodesOfType(tree, 'persona_block')) {
|
|
66
|
+
const fileNode = node.childForFieldName('file');
|
|
67
|
+
if (fileNode && isFilename(fileNode)) addFileLink(fileNode, fileNode.text);
|
|
68
|
+
}
|
|
69
|
+
for (const node of nodesOfType(tree, 'category_prop')) {
|
|
70
|
+
const uriNode = node.childForFieldName('uri');
|
|
71
|
+
if (uriNode) addUrlLink(uriNode);
|
|
72
|
+
}
|
|
73
|
+
for (const node of nodesOfType(tree, 'concept_prop')) {
|
|
74
|
+
const uriNode = node.childForFieldName('uri');
|
|
75
|
+
if (uriNode) addUrlLink(uriNode);
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
78
|
|
|
51
79
|
if (langId === 'behavior') {
|
|
52
80
|
for (const node of nodesOfType(tree, 'merge_decl')) {
|
|
53
81
|
const pathNode = node.childForFieldName('path');
|
|
54
|
-
if (pathNode)
|
|
82
|
+
if (pathNode) addFileLink(pathNode, pathNode.text);
|
|
55
83
|
}
|
|
56
84
|
for (const node of nodesOfType(tree, 'run_stmt')) {
|
|
57
85
|
const runTypeNode = node.childForFieldName('type');
|
|
58
86
|
if (runTypeNode?.text !== 'script') continue;
|
|
59
87
|
const targetNode = node.childForFieldName('target');
|
|
60
|
-
if (targetNode)
|
|
88
|
+
if (targetNode) addFileLink(targetNode, targetNode.text);
|
|
61
89
|
}
|
|
62
90
|
}
|
|
63
91
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dot-agent/language-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "LSP server for .agent DSL files",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Danilo Borges <contato@daniloborg.es>",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"start": "node server.js --stdio"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@dot-agent/tree-sitter": "^0.3.
|
|
30
|
+
"@dot-agent/tree-sitter": "^0.3.6",
|
|
31
31
|
"vscode-languageserver": "^10.0.0",
|
|
32
32
|
"vscode-languageserver-textdocument": "^1.0.12",
|
|
33
33
|
"web-tree-sitter": "^0.26.9"
|
package/parser.js
CHANGED
|
@@ -53,6 +53,12 @@ function evict(uri) {
|
|
|
53
53
|
cache.delete(uri);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function parseText(langId, text) {
|
|
57
|
+
const parser = langId === 'behavior' ? behaviorParser : descriptionParser;
|
|
58
|
+
if (!parser) return null;
|
|
59
|
+
return parser.parse(text);
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
// ── AST helpers ──────────────────────────────────────────────────────────────
|
|
57
63
|
|
|
58
64
|
function nodesOfType(tree, type) {
|
|
@@ -116,6 +122,7 @@ function getContextNode(tree, offset) {
|
|
|
116
122
|
module.exports = {
|
|
117
123
|
initParsers,
|
|
118
124
|
parse,
|
|
125
|
+
parseText,
|
|
119
126
|
evict,
|
|
120
127
|
nodesOfType,
|
|
121
128
|
nodeAtOffset,
|
package/server.js
CHANGED
|
@@ -25,7 +25,7 @@ const {
|
|
|
25
25
|
} = require('vscode-languageserver/node');
|
|
26
26
|
const { TextDocument } = require('vscode-languageserver-textdocument');
|
|
27
27
|
|
|
28
|
-
const { initParsers, parse, evict } = require('./parser');
|
|
28
|
+
const { initParsers, parse, evict, nodesOfType } = require('./parser');
|
|
29
29
|
|
|
30
30
|
const { provideHover } = require('./features/hover');
|
|
31
31
|
const { provideCompletions } = require('./features/completions');
|
|
@@ -36,6 +36,7 @@ const { provideReferences } = require('./features/references');
|
|
|
36
36
|
const { provideRenameEdits } = require('./features/rename');
|
|
37
37
|
const { format } = require('./features/formatting');
|
|
38
38
|
const { provideDocumentLinks } = require('./features/links');
|
|
39
|
+
const { extractBehaviorGraph } = require('./features/graph');
|
|
39
40
|
|
|
40
41
|
const connection = createConnection(ProposedFeatures.all);
|
|
41
42
|
const documents = new TextDocuments(TextDocument);
|
|
@@ -73,7 +74,7 @@ function validate(doc) {
|
|
|
73
74
|
const langId = doc.languageId;
|
|
74
75
|
if (langId !== 'description' && langId !== 'behavior') return;
|
|
75
76
|
const tree = getTree(doc);
|
|
76
|
-
const diagnostics = diagnose(langId, tree, doc.getText());
|
|
77
|
+
const diagnostics = diagnose(langId, tree, doc.getText(), doc.uri);
|
|
77
78
|
connection.sendDiagnostics({ uri: doc.uri, diagnostics });
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -163,6 +164,33 @@ connection.onDocumentFormatting(({ textDocument }) => {
|
|
|
163
164
|
return format(doc.languageId, doc.getText());
|
|
164
165
|
});
|
|
165
166
|
|
|
167
|
+
// ── Behavior Graph (custom request) ──────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
connection.onRequest('agent/behaviorGraph', ({ uri }) => {
|
|
170
|
+
const doc = documents.get(uri);
|
|
171
|
+
if (!doc || doc.languageId !== 'behavior') return null;
|
|
172
|
+
const tree = getTree(doc);
|
|
173
|
+
return tree ? extractBehaviorGraph(tree) : null;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── Current State at Position (custom request) ────────────────────────────────
|
|
177
|
+
|
|
178
|
+
connection.onRequest('agent/currentState', ({ uri, position }) => {
|
|
179
|
+
const doc = documents.get(uri);
|
|
180
|
+
if (!doc || doc.languageId !== 'behavior') return null;
|
|
181
|
+
const tree = getTree(doc);
|
|
182
|
+
if (!tree) return null;
|
|
183
|
+
const line = position.line;
|
|
184
|
+
let result = null;
|
|
185
|
+
for (const node of nodesOfType(tree, 'state_decl')) {
|
|
186
|
+
if (node.startPosition.row > line) break;
|
|
187
|
+
if (node.startPosition.row <= line && node.endPosition.row >= line) {
|
|
188
|
+
result = node.childForFieldName('name')?.text ?? null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
});
|
|
193
|
+
|
|
166
194
|
// ── Start ────────────────────────────────────────────────────────────────────
|
|
167
195
|
|
|
168
196
|
documents.listen(connection);
|