@dot-agent/language-server 0.4.1 → 0.5.0-alpha.2
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/AGENTS.md +27 -8
- package/features/completions.js +3 -7
- package/features/definition.js +9 -13
- package/features/diagnostics.js +83 -179
- package/features/formatting.js +1 -5
- package/features/hover.js +2 -6
- package/features/links.js +6 -10
- package/features/references.js +2 -6
- package/features/rename.js +2 -6
- package/features/symbols.js +2 -6
- package/merge-graph.js +129 -0
- package/package.json +32 -8
- package/parser.js +23 -29
- package/server.js +45 -28
- package/tests/diagnostics.test.js +209 -0
- package/tests/formatting.test.js +87 -0
- package/tests/hover.test.js +87 -0
- package/tests/parser.test.js +215 -0
- package/tests/references.test.js +52 -0
- package/tests/rename.test.js +47 -0
- package/tests/symbols.test.js +68 -0
- package/.github/workflows/publish.yml +0 -28
- package/features/graph.js +0 -53
package/AGENTS.md
CHANGED
|
@@ -10,17 +10,19 @@ A standalone [Language Server Protocol](https://microsoft.github.io/language-ser
|
|
|
10
10
|
|
|
11
11
|
All structural analysis is performed on **tree-sitter ASTs** (not regex). `parser.js` owns the WASM parser lifecycle, document cache, and the helper functions that feature modules use to traverse parse trees.
|
|
12
12
|
|
|
13
|
+
The package is **ESM** (`"type": "module"` in `package.json`). All files use `import`/`export` — no `require()` or `module.exports`.
|
|
14
|
+
|
|
13
15
|
---
|
|
14
16
|
|
|
15
17
|
## Module responsibilities
|
|
16
18
|
|
|
17
19
|
| File | Responsibility |
|
|
18
20
|
|------|---------------|
|
|
19
|
-
| `server.js` | LSP wiring — creates the connection, awaits `initParsers()` in `onInitialize`, registers all `connection.onXxx()` handlers, delegates to `features/` |
|
|
21
|
+
| `server.js` | LSP wiring — creates the connection, awaits `initParsers()` and `bpInit()` in `onInitialize`, registers all `connection.onXxx()` handlers, delegates to `features/` |
|
|
20
22
|
| `parser.js` | Tree-sitter engine — WASM initialization, per-document AST cache, and shared traversal helpers |
|
|
21
23
|
| `features/hover.js` | Hover documentation for all DSL keywords (static lookup, no tree traversal) |
|
|
22
24
|
| `features/completions.js` | Context-aware completions using `getContextNode` and `nodesOfType` for name lookups |
|
|
23
|
-
| `features/diagnostics.js` |
|
|
25
|
+
| `features/diagnostics.js` | Thin adapter — delegates to `lintDescription`/`lintBehavior` from `@dot-agent/compiler`, maps `LintMessage[]` → LSP `Diagnostic[]` |
|
|
24
26
|
| `features/definition.js` | Go-to-definition via `state_decl` / `type_decl` node lookup |
|
|
25
27
|
| `features/references.js` | Find all references via `transition_stmt`, `intent_trigger`, `type_ref` traversal |
|
|
26
28
|
| `features/rename.js` | Rename symbol — same traversal as references, produces `TextEdit[]` |
|
|
@@ -96,26 +98,43 @@ Add shared helpers here — never duplicate tree traversal logic across feature
|
|
|
96
98
|
Production dependencies:
|
|
97
99
|
- `vscode-languageserver` and `vscode-languageserver-textdocument` — LSP protocol implementation
|
|
98
100
|
- `web-tree-sitter` — WASM-based tree-sitter runtime
|
|
99
|
-
- `@dot-agent/tree-sitter` — Agent and
|
|
101
|
+
- `@dot-agent/tree-sitter` — Agent and Behavior grammar WASM binaries
|
|
102
|
+
- `@dot-agent/compiler` — Linting (`lintDescription`, `lintBehavior`). Do not reimplement lint rules locally; always delegate to the compiler.
|
|
103
|
+
- `@dot-agent/behavior-parser` — FSM WASM; used directly in `server.js` for `agent/behaviorGraph` (returns SCXML string via `get_graph(text)`).
|
|
100
104
|
|
|
101
|
-
Do not add framework dependencies, bundlers, or anything that requires a build step on the language-server side. The server must start with a bare `node server.js --stdio
|
|
105
|
+
Do not add framework dependencies, bundlers, or anything that requires a build step on the language-server side. The server must start with a bare `node server.js --stdio`.
|
|
102
106
|
|
|
103
|
-
The grammar WASM binaries are built by running `npm run build` in the `tree-sitter-agent` package (requires Emscripten). When published to npm, `dist/` is included in the package and no build is needed.
|
|
107
|
+
The grammar WASM binaries are built by running `npm run build` in the `tree-sitter-agent` package (requires Emscripten). When published to npm, `dist/` is included in the package and no build is needed. The compiler must be built (`npm run build` in `packages/compiler/`) before the language-server can use it in the monorepo.
|
|
104
108
|
|
|
105
109
|
---
|
|
106
110
|
|
|
107
111
|
## Async lifecycle rule
|
|
108
112
|
|
|
109
|
-
`web-tree-sitter`
|
|
113
|
+
`web-tree-sitter` and `@dot-agent/behavior-parser` initialize asynchronously. The `onInitialize` handler is the only safe place to call both:
|
|
110
114
|
|
|
111
115
|
```js
|
|
112
116
|
connection.onInitialize(async () => {
|
|
113
|
-
await initParsers(); //
|
|
117
|
+
await initParsers(); // tree-sitter WASMs (agent + behavior grammars)
|
|
118
|
+
await bpInit(); // behavior-parser WASM (FSM parser + get_graph)
|
|
114
119
|
return { capabilities: { ... } };
|
|
115
120
|
});
|
|
116
121
|
```
|
|
117
122
|
|
|
118
|
-
No feature handler will fire before `initialize` completes, so this guarantees
|
|
123
|
+
No feature handler will fire before `initialize` completes, so this guarantees all WASMs are ready before any request arrives. Never call these outside `onInitialize`.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## `agent/behaviorGraph` custom request
|
|
128
|
+
|
|
129
|
+
Returns a **SCXML string** (W3C State Chart XML) generated by the behavior-parser WASM from the document text. The VS Code extension consumes this to render a state diagram.
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
connection.onRequest('agent/behaviorGraph', ({ uri }) => {
|
|
133
|
+
const doc = documents.get(uri)
|
|
134
|
+
if (!doc || doc.languageId !== 'behavior') return null
|
|
135
|
+
return get_graph(doc.getText()) // → SCXML string
|
|
136
|
+
})
|
|
137
|
+
```
|
|
119
138
|
|
|
120
139
|
---
|
|
121
140
|
|
package/features/completions.js
CHANGED
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const { CompletionItemKind } = require('vscode-languageserver');
|
|
20
|
-
const { nodesOfType, positionToOffset, getContextNode } = require('../parser');
|
|
17
|
+
import { CompletionItemKind } from 'vscode-languageserver';
|
|
18
|
+
import { nodesOfType, positionToOffset, getContextNode } from '../parser.js';
|
|
21
19
|
|
|
22
20
|
const BEHAVIOR_TOP_KW = ['state', 'merge', 'on event', 'on intent', 'on offtopic'];
|
|
23
21
|
const BEHAVIOR_BLOCK_KW = ['guide', 'teach', 'goal', 'interact', 'run', 'transition', 'set', 'if', 'else', 'end', 'after', 'parallel', 'apply', 'remove', 'on intent', 'on offtopic', 'on failure', 'on success'];
|
|
@@ -38,7 +36,7 @@ function nearestAncestor(node, types) {
|
|
|
38
36
|
return null;
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
function provideCompletions(langId, tree, text, position) {
|
|
39
|
+
export function provideCompletions(langId, tree, text, position) {
|
|
42
40
|
const lines = text.split('\n');
|
|
43
41
|
const line = lines[position.line] || '';
|
|
44
42
|
const before = line.slice(0, position.character);
|
|
@@ -108,5 +106,3 @@ function provideCompletions(langId, tree, text, position) {
|
|
|
108
106
|
|
|
109
107
|
return [];
|
|
110
108
|
}
|
|
111
|
-
|
|
112
|
-
module.exports = { provideCompletions };
|
package/features/definition.js
CHANGED
|
@@ -14,12 +14,10 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const path = require('path');
|
|
22
|
-
const { nodesOfType, nodeToRange, wordAtPosition, parseText } = require('../parser');
|
|
17
|
+
import { readFileSync } from 'fs';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
19
|
+
import { resolve, dirname } from 'path';
|
|
20
|
+
import { nodesOfType, nodeToRange, wordAtPosition, parseText } from '../parser.js';
|
|
23
21
|
|
|
24
22
|
// Busca recursiva de state_decl em arquivos mergeados.
|
|
25
23
|
// visited (Set<absPath>) evita loops em grafos de merge circulares.
|
|
@@ -28,23 +26,23 @@ function findStateInMerges(tree, docDir, word, visited = new Set()) {
|
|
|
28
26
|
const pathNode = mergeNode.childForFieldName('path');
|
|
29
27
|
if (!pathNode) continue;
|
|
30
28
|
const filename = pathNode.text.replace(/^"|"$/g, '');
|
|
31
|
-
const absPath =
|
|
29
|
+
const absPath = resolve(docDir, filename);
|
|
32
30
|
if (visited.has(absPath)) continue;
|
|
33
31
|
visited.add(absPath);
|
|
34
32
|
let mergedText;
|
|
35
|
-
try { mergedText =
|
|
33
|
+
try { mergedText = readFileSync(absPath, 'utf8'); } catch { continue; }
|
|
36
34
|
const mergedTree = parseText('behavior', mergedText);
|
|
37
35
|
if (!mergedTree) continue;
|
|
38
36
|
const found = nodesOfType(mergedTree, 'state_decl')
|
|
39
37
|
.find(n => n.childForFieldName('name')?.text === word);
|
|
40
38
|
if (found) return { uri: pathToFileURL(absPath).toString(), range: nodeToRange(found) };
|
|
41
|
-
const sub = findStateInMerges(mergedTree,
|
|
39
|
+
const sub = findStateInMerges(mergedTree, dirname(absPath), word, visited);
|
|
42
40
|
if (sub) return sub;
|
|
43
41
|
}
|
|
44
42
|
return null;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
function provideDefinition(langId, tree, text, uri, position) {
|
|
45
|
+
export function provideDefinition(langId, tree, text, uri, position) {
|
|
48
46
|
if (!tree) return null;
|
|
49
47
|
|
|
50
48
|
const { word } = wordAtPosition(text, position.line, position.character);
|
|
@@ -55,7 +53,7 @@ function provideDefinition(langId, tree, text, uri, position) {
|
|
|
55
53
|
.find(n => n.childForFieldName('name')?.text === word);
|
|
56
54
|
if (local) return { uri, range: nodeToRange(local) };
|
|
57
55
|
try {
|
|
58
|
-
const docDir =
|
|
56
|
+
const docDir = dirname(fileURLToPath(uri));
|
|
59
57
|
return findStateInMerges(tree, docDir, word) ?? null;
|
|
60
58
|
} catch { return null; }
|
|
61
59
|
}
|
|
@@ -70,5 +68,3 @@ function provideDefinition(langId, tree, text, uri, position) {
|
|
|
70
68
|
|
|
71
69
|
return null;
|
|
72
70
|
}
|
|
73
|
-
|
|
74
|
-
module.exports = { provideDefinition };
|
package/features/diagnostics.js
CHANGED
|
@@ -14,199 +14,103 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
for (const [blockType, blockName] of Object.entries(STRICT_BLOCK_TYPES)) {
|
|
43
|
-
for (const blockNode of nodesOfType(tree, blockType)) {
|
|
44
|
-
// Syntax errors inside the block
|
|
45
|
-
for (const errorNode of blockNode.descendantsOfType('ERROR')) {
|
|
46
|
-
diagnostics.push({
|
|
47
|
-
range: nodeToRange(errorNode),
|
|
48
|
-
message: `Syntax error in '${blockName}' block. Expected: Type or Type "Description", or compact: Type1, Type2.`,
|
|
49
|
-
severity: DiagnosticSeverity.Error,
|
|
50
|
-
source: 'description-dsl',
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Undeclared type references (semantic warning)
|
|
55
|
-
if (declaredTypes.size > 0) {
|
|
56
|
-
for (const typeRefNode of blockNode.descendantsOfType('type_ref')) {
|
|
57
|
-
const idNode = typeRefNode.firstNamedChild;
|
|
58
|
-
if (!idNode) continue;
|
|
59
|
-
const typeName = idNode.text;
|
|
60
|
-
if (!declaredTypes.has(typeName)) {
|
|
61
|
-
diagnostics.push({
|
|
62
|
-
range: nodeToRange(idNode),
|
|
63
|
-
message: `Type '${typeName}' is not declared in this file (assuming native or external).`,
|
|
64
|
-
severity: DiagnosticSeverity.Warning,
|
|
65
|
-
source: 'description-dsl',
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return diagnostics;
|
|
17
|
+
import { lintBehavior, lintDescription, consolidate, parseBehaviorFile } from '@dot-agent/compiler';
|
|
18
|
+
import { DiagnosticSeverity } from 'vscode-languageserver';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import { dirname, relative } from 'node:path';
|
|
21
|
+
import { findMergeRoot, findAgentRoot } from '../merge-graph.js';
|
|
22
|
+
|
|
23
|
+
function toDiagnostic(msg) {
|
|
24
|
+
const severity =
|
|
25
|
+
msg.severity === 'error' ? DiagnosticSeverity.Error :
|
|
26
|
+
msg.severity === 'warning' ? DiagnosticSeverity.Warning :
|
|
27
|
+
msg.severity === 'info' ? DiagnosticSeverity.Information :
|
|
28
|
+
DiagnosticSeverity.Hint;
|
|
29
|
+
const text = msg.hint
|
|
30
|
+
? `[${msg.code}] ${msg.message} (${msg.hint})`
|
|
31
|
+
: `[${msg.code}] ${msg.message}`;
|
|
32
|
+
return {
|
|
33
|
+
range: {
|
|
34
|
+
start: { line: msg.line - 1, character: msg.col - 1 },
|
|
35
|
+
end: { line: msg.line - 1, character: msg.col - 1 },
|
|
36
|
+
},
|
|
37
|
+
message: text,
|
|
38
|
+
severity,
|
|
39
|
+
source: 'dot-agent',
|
|
40
|
+
};
|
|
74
41
|
}
|
|
75
42
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
}
|
|
43
|
+
function hasMerge(text) {
|
|
44
|
+
return /^\s*merge\s+/m.test(text);
|
|
96
45
|
}
|
|
97
46
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
47
|
+
export async function diagnose(uri, langId, text) {
|
|
48
|
+
if (langId !== 'description' && langId !== 'behavior') return [];
|
|
49
|
+
let filePath;
|
|
50
|
+
try {
|
|
51
|
+
filePath = fileURLToPath(uri);
|
|
52
|
+
} catch {
|
|
53
|
+
filePath = uri;
|
|
54
|
+
}
|
|
103
55
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
seen.add(key);
|
|
108
|
-
diagnostics.push({
|
|
109
|
-
range: nodeToRange(node),
|
|
110
|
-
message,
|
|
111
|
-
severity: DiagnosticSeverity.Error,
|
|
112
|
-
source: 'behavior-dsl',
|
|
113
|
-
});
|
|
56
|
+
if (langId === 'description') {
|
|
57
|
+
const msgs = await lintDescription(text, filePath);
|
|
58
|
+
return msgs.map(toDiagnostic);
|
|
114
59
|
}
|
|
115
60
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
61
|
+
// Agent root = nearest ancestor directory (bounded by the client's
|
|
62
|
+
// workspace folder) with a *.description file — same convention pack.ts
|
|
63
|
+
// uses to define an agent bundle. Falls back to the file's own directory
|
|
64
|
+
// when no manifest is found, e.g. a lone .behavior file opened outside
|
|
65
|
+
// an agent.
|
|
66
|
+
const agentRoot = (await findAgentRoot(dirname(filePath))) ?? dirname(filePath);
|
|
67
|
+
const entryFile = relative(agentRoot, filePath);
|
|
68
|
+
|
|
69
|
+
// behavior: run consolidated lint when the file uses merge declarations
|
|
70
|
+
if (!hasMerge(text)) {
|
|
71
|
+
// This file has no merge of its own, but it might be a fragment that
|
|
72
|
+
// another file merges in (e.g. a shared sub-flow). Walk merge edges
|
|
73
|
+
// backward within the agent root to find that root and borrow its
|
|
74
|
+
// consolidated state set, so cross-file transitions don't show as
|
|
75
|
+
// dangling (E005) and this fragment isn't wrongly held to whole-tree
|
|
76
|
+
// rules like E016 (init required) that only make sense at the root.
|
|
77
|
+
const root = await findMergeRoot(agentRoot, entryFile);
|
|
78
|
+
if (root) {
|
|
79
|
+
try {
|
|
80
|
+
const { mergedText } = await consolidate(agentRoot, root);
|
|
81
|
+
const externalStates = new Set((parseBehaviorFile(mergedText).ok?.states ?? []).map(s => s.name));
|
|
82
|
+
const msgs = await lintBehavior(text, filePath, filePath, false, externalStates, mergedText);
|
|
83
|
+
return msgs.map(toDiagnostic);
|
|
84
|
+
} catch {
|
|
85
|
+
// root's own merge chain is broken — fall back to local-only lint below
|
|
129
86
|
}
|
|
130
|
-
// tem ERROR aninhado → desce para reportar o(s) mais específico(s)
|
|
131
87
|
}
|
|
132
|
-
|
|
88
|
+
const msgs = await lintBehavior(text, filePath, filePath, true);
|
|
89
|
+
return msgs.map(toDiagnostic);
|
|
133
90
|
}
|
|
134
91
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
collectSyntaxErrors(tree.rootNode, diagnostics);
|
|
143
|
-
|
|
144
|
-
// ── Rule 1: Dangling transitions ──────────────────────────────────────────
|
|
145
|
-
const definedStates = new Set(
|
|
146
|
-
nodesOfType(tree, 'state_decl')
|
|
147
|
-
.map(n => n.childForFieldName('name')?.text)
|
|
148
|
-
.filter(Boolean)
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
// Enriquecer com estados de arquivos mergeados (recursivo, anti-loop)
|
|
92
|
+
// multi-file behavior: attempt consolidation to surface E012/E013/E014/E015/E016.
|
|
93
|
+
// Local diagnostics still run against this file's own `text` — never the
|
|
94
|
+
// merged blob — so positions stay correct; `mergedText` is only consulted
|
|
95
|
+
// for whole-tree facts (E015/E016/W014) and W001/W009 reachability.
|
|
96
|
+
const consolidationDiags = [];
|
|
97
|
+
let mergedText;
|
|
98
|
+
let isConsolidated = false;
|
|
152
99
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
} catch {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
range: nodeToRange(stateNode),
|
|
164
|
-
message: external
|
|
165
|
-
? `State '${target}' is not defined locally (assuming external flow reference).`
|
|
166
|
-
: `State '${target}' is not defined in this file.`,
|
|
167
|
-
severity: external ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error,
|
|
168
|
-
source: 'behavior-dsl',
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
for (const n of nodesOfType(tree, 'transition_stmt')) {
|
|
174
|
-
checkTransitionTarget(n.childForFieldName('state'));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ── Rule 2: Dead-end interact (now redundant but kept for safety) ────────────
|
|
178
|
-
// With the new grammar, oriented_state_body requires repeat1(handlers),
|
|
179
|
-
// so dead-end interact is structurally impossible. But kept for edge cases.
|
|
180
|
-
for (const interactNode of nodesOfType(tree, 'interact_stmt')) {
|
|
181
|
-
let ancestor = interactNode.parent;
|
|
182
|
-
while (ancestor && ancestor.type !== 'oriented_state_body' && ancestor.type !== 'state_decl') {
|
|
183
|
-
ancestor = ancestor.parent;
|
|
184
|
-
}
|
|
185
|
-
if (!ancestor) continue;
|
|
186
|
-
|
|
187
|
-
// With new grammar: oriented_state_body always has handlers after interact (repeat1)
|
|
188
|
-
// So this check should never trigger, but kept for robustness
|
|
189
|
-
const hasHandlers = ancestor.descendantsOfType('intent_handler').length +
|
|
190
|
-
ancestor.descendantsOfType('offtopic_handler').length > 0;
|
|
191
|
-
|
|
192
|
-
if (!hasHandlers) {
|
|
193
|
-
diagnostics.push({
|
|
194
|
-
range: nodeToRange(interactNode),
|
|
195
|
-
message: "This state calls interact but has no handlers (on intent/offtopic). This will trap the agent.",
|
|
196
|
-
severity: DiagnosticSeverity.Warning,
|
|
197
|
-
source: 'behavior-dsl',
|
|
100
|
+
({ mergedText } = await consolidate(agentRoot, entryFile));
|
|
101
|
+
isConsolidated = true;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const m = /^(E\d+):\s*(.+)/.exec(err.message);
|
|
104
|
+
if (m) {
|
|
105
|
+
consolidationDiags.push({
|
|
106
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
107
|
+
message: `[${m[1]}] ${m[2]}`,
|
|
108
|
+
severity: DiagnosticSeverity.Error,
|
|
109
|
+
source: 'dot-agent',
|
|
198
110
|
});
|
|
199
111
|
}
|
|
200
112
|
}
|
|
201
113
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
function diagnose(langId, tree, text, uri) {
|
|
206
|
-
if (!tree) return [];
|
|
207
|
-
if (langId === 'description') return diagnoseDescription(tree, text);
|
|
208
|
-
if (langId === 'behavior') return diagnoseBehavior(tree, uri);
|
|
209
|
-
return [];
|
|
114
|
+
const msgs = await lintBehavior(text, filePath, filePath, isConsolidated, undefined, mergedText);
|
|
115
|
+
return [...consolidationDiags, ...msgs.map(toDiagnostic)];
|
|
210
116
|
}
|
|
211
|
-
|
|
212
|
-
module.exports = { diagnose };
|
package/features/formatting.js
CHANGED
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
'use strict';
|
|
18
|
-
|
|
19
17
|
const BLOCK_HEADERS = /^(on\s+(intent|offtopic|failure|success)|if|else|after|parallel)\b/;
|
|
20
18
|
const TOP_LEVEL_LINE = /^(state|merge)\s|^on\s+event\b/;
|
|
21
19
|
|
|
@@ -76,10 +74,8 @@ function formatDescription(text) {
|
|
|
76
74
|
return edits;
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
function format(langId, text) {
|
|
77
|
+
export function format(langId, text) {
|
|
80
78
|
if (langId === 'behavior') return formatBehavior(text);
|
|
81
79
|
if (langId === 'description') return formatDescription(text);
|
|
82
80
|
return [];
|
|
83
81
|
}
|
|
84
|
-
|
|
85
|
-
module.exports = { format };
|
package/features/hover.js
CHANGED
|
@@ -14,9 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const { MarkupKind } = require('vscode-languageserver');
|
|
17
|
+
import { MarkupKind } from 'vscode-languageserver';
|
|
20
18
|
|
|
21
19
|
const DESCRIPTION_DOCS = {
|
|
22
20
|
'agent': '**`agent name`**\n\nDeclares a new agent. The central node of the manifest.',
|
|
@@ -60,7 +58,7 @@ const BEHAVIOR_DOCS = {
|
|
|
60
58
|
'success': '**`on success`**\n\nOptional success handler block inside a `parallel` statement.',
|
|
61
59
|
};
|
|
62
60
|
|
|
63
|
-
function provideHover(langId, text, position) {
|
|
61
|
+
export function provideHover(langId, text, position) {
|
|
64
62
|
const lines = text.split('\n');
|
|
65
63
|
const line = lines[position.line] || '';
|
|
66
64
|
const ch = position.character;
|
|
@@ -84,5 +82,3 @@ function provideHover(langId, text, position) {
|
|
|
84
82
|
},
|
|
85
83
|
};
|
|
86
84
|
}
|
|
87
|
-
|
|
88
|
-
module.exports = { provideHover };
|
package/features/links.js
CHANGED
|
@@ -14,11 +14,9 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const path = require('path');
|
|
21
|
-
const { nodesOfType, nodeToRange } = require('../parser');
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
18
|
+
import { resolve, dirname } from 'path';
|
|
19
|
+
import { nodesOfType, nodeToRange } from '../parser.js';
|
|
22
20
|
|
|
23
21
|
// Detecta se um nó bare_string representa um caminho de arquivo.
|
|
24
22
|
// Usa o subtipo do tree-sitter (filename) como primeira fonte de verdade;
|
|
@@ -30,12 +28,12 @@ function isFilename(node) {
|
|
|
30
28
|
return /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(node.text.replace(/^"|"$/g, ''));
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
function provideDocumentLinks(langId, tree, docUri) {
|
|
31
|
+
export function provideDocumentLinks(langId, tree, docUri) {
|
|
34
32
|
if (!tree) return [];
|
|
35
33
|
|
|
36
34
|
let docDir;
|
|
37
35
|
try {
|
|
38
|
-
docDir =
|
|
36
|
+
docDir = dirname(fileURLToPath(docUri));
|
|
39
37
|
} catch {
|
|
40
38
|
return [];
|
|
41
39
|
}
|
|
@@ -47,7 +45,7 @@ function provideDocumentLinks(langId, tree, docUri) {
|
|
|
47
45
|
if (!filename) return;
|
|
48
46
|
links.push({
|
|
49
47
|
range: nodeToRange(fileNode),
|
|
50
|
-
target: pathToFileURL(
|
|
48
|
+
target: pathToFileURL(resolve(docDir, filename)).toString(),
|
|
51
49
|
});
|
|
52
50
|
}
|
|
53
51
|
|
|
@@ -91,5 +89,3 @@ function provideDocumentLinks(langId, tree, docUri) {
|
|
|
91
89
|
|
|
92
90
|
return links;
|
|
93
91
|
}
|
|
94
|
-
|
|
95
|
-
module.exports = { provideDocumentLinks };
|
package/features/references.js
CHANGED
|
@@ -14,11 +14,9 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
import { nodesOfType, nodeToRange, wordAtPosition } from '../parser.js';
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
function provideReferences(langId, tree, text, uri, position) {
|
|
19
|
+
export function provideReferences(langId, tree, text, uri, position) {
|
|
22
20
|
if (!tree) return [];
|
|
23
21
|
|
|
24
22
|
const { word } = wordAtPosition(text, position.line, position.character);
|
|
@@ -56,5 +54,3 @@ function provideReferences(langId, tree, text, uri, position) {
|
|
|
56
54
|
|
|
57
55
|
return locations;
|
|
58
56
|
}
|
|
59
|
-
|
|
60
|
-
module.exports = { provideReferences };
|
package/features/rename.js
CHANGED
|
@@ -14,11 +14,9 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
import { nodesOfType, nodeToRange, wordAtPosition } from '../parser.js';
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
function provideRenameEdits(langId, tree, text, uri, position, newName) {
|
|
19
|
+
export function provideRenameEdits(langId, tree, text, uri, position, newName) {
|
|
22
20
|
if (!tree) return null;
|
|
23
21
|
|
|
24
22
|
const { word: oldName } = wordAtPosition(text, position.line, position.character);
|
|
@@ -57,5 +55,3 @@ function provideRenameEdits(langId, tree, text, uri, position, newName) {
|
|
|
57
55
|
if (edits.length === 0) return null;
|
|
58
56
|
return { changes: { [uri]: edits } };
|
|
59
57
|
}
|
|
60
|
-
|
|
61
|
-
module.exports = { provideRenameEdits };
|
package/features/symbols.js
CHANGED
|
@@ -14,14 +14,12 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const { nodesOfType, nodeToRange } = require('../parser');
|
|
17
|
+
import { nodesOfType, nodeToRange } from '../parser.js';
|
|
20
18
|
|
|
21
19
|
// DocumentSymbol kind constants (LSP spec §3.17.5)
|
|
22
20
|
const Kind = { Class: 5, Struct: 23, Event: 24 };
|
|
23
21
|
|
|
24
|
-
function provideDocumentSymbols(langId, tree) {
|
|
22
|
+
export function provideDocumentSymbols(langId, tree) {
|
|
25
23
|
if (!tree) return [];
|
|
26
24
|
const symbols = [];
|
|
27
25
|
|
|
@@ -74,5 +72,3 @@ function provideDocumentSymbols(langId, tree) {
|
|
|
74
72
|
|
|
75
73
|
return symbols;
|
|
76
74
|
}
|
|
77
|
-
|
|
78
|
-
module.exports = { provideDocumentSymbols };
|