@dot-agent/language-server 0.2.0
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 +156 -0
- package/LICENSE +185 -0
- package/README.md +186 -0
- package/features/completions.js +112 -0
- package/features/definition.js +42 -0
- package/features/diagnostics.js +164 -0
- package/features/formatting.js +80 -0
- package/features/hover.js +86 -0
- package/features/links.js +71 -0
- package/features/references.js +65 -0
- package/features/rename.js +66 -0
- package/features/symbols.js +78 -0
- package/package.json +32 -0
- package/parser.js +126 -0
- package/server.js +169 -0
|
@@ -0,0 +1,42 @@
|
|
|
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, nodeToRange, wordAtPosition } = require('../parser');
|
|
20
|
+
|
|
21
|
+
function provideDefinition(langId, tree, text, uri, position) {
|
|
22
|
+
if (!tree) return null;
|
|
23
|
+
|
|
24
|
+
const { word } = wordAtPosition(text, position.line, position.character);
|
|
25
|
+
if (!word) return null;
|
|
26
|
+
|
|
27
|
+
let targetNode = null;
|
|
28
|
+
|
|
29
|
+
if (langId === 'behavior') {
|
|
30
|
+
targetNode = nodesOfType(tree, 'state_decl')
|
|
31
|
+
.find(n => n.childForFieldName('name')?.text === word);
|
|
32
|
+
} else if (langId === 'agent') {
|
|
33
|
+
if (!/^[A-Z]/.test(word) && !word.includes('.')) return null;
|
|
34
|
+
targetNode = nodesOfType(tree, 'type_decl')
|
|
35
|
+
.find(n => n.childForFieldName('name')?.text === word);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!targetNode) return null;
|
|
39
|
+
return { uri, range: nodeToRange(targetNode) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { provideDefinition };
|
|
@@ -0,0 +1,164 @@
|
|
|
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 { DiagnosticSeverity } = require('vscode-languageserver');
|
|
20
|
+
const { nodesOfType, nodeToRange } = require('../parser');
|
|
21
|
+
|
|
22
|
+
const DEPRECATED_AGENT_KW = new Set([
|
|
23
|
+
'do', 'server', 'endpoint', 'author', 'version', 'requirements', 'step',
|
|
24
|
+
'softwareVersion', 'applicationCategory', 'character', 'publishingPrinciples',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const STRICT_BLOCK_TYPES = {
|
|
28
|
+
input_block: 'input',
|
|
29
|
+
output_block: 'output',
|
|
30
|
+
requires_block: 'requires',
|
|
31
|
+
capabilities_block: 'capabilities',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function diagnoseAgent(tree, text) {
|
|
35
|
+
const diagnostics = [];
|
|
36
|
+
|
|
37
|
+
// ── Deprecated keywords: line scan (no tree-sitter benefit here) ──────────
|
|
38
|
+
const lines = text.split('\n');
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const raw = lines[i];
|
|
41
|
+
const stripped = raw.split('//')[0].trim();
|
|
42
|
+
if (!stripped) continue;
|
|
43
|
+
const wordM = stripped.match(/^([a-zA-Z0-9_.-]+)\b/);
|
|
44
|
+
if (!wordM) continue;
|
|
45
|
+
const word = wordM[1];
|
|
46
|
+
if (DEPRECATED_AGENT_KW.has(word)) {
|
|
47
|
+
const col = raw.indexOf(word);
|
|
48
|
+
diagnostics.push({
|
|
49
|
+
range: { start: { line: i, character: col }, end: { line: i, character: col + word.length } },
|
|
50
|
+
message: `The keyword '${word}' is deprecated or invalid in the current .agent specification.`,
|
|
51
|
+
severity: DiagnosticSeverity.Error,
|
|
52
|
+
source: 'agent-dsl',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Strict block validation ───────────────────────────────────────────────
|
|
58
|
+
const declaredTypes = new Set(
|
|
59
|
+
nodesOfType(tree, 'type_decl')
|
|
60
|
+
.map(n => n.childForFieldName('name')?.text)
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
for (const [blockType, blockName] of Object.entries(STRICT_BLOCK_TYPES)) {
|
|
65
|
+
for (const blockNode of nodesOfType(tree, blockType)) {
|
|
66
|
+
// Syntax errors inside the block
|
|
67
|
+
for (const errorNode of blockNode.descendantsOfType('ERROR')) {
|
|
68
|
+
diagnostics.push({
|
|
69
|
+
range: nodeToRange(errorNode),
|
|
70
|
+
message: `Syntax error in '${blockName}' block. Expected: Type or Type "Description", or compact: Type1, Type2.`,
|
|
71
|
+
severity: DiagnosticSeverity.Error,
|
|
72
|
+
source: 'agent-dsl',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Undeclared type references (semantic warning)
|
|
77
|
+
if (declaredTypes.size > 0) {
|
|
78
|
+
for (const typeRefNode of blockNode.descendantsOfType('type_ref')) {
|
|
79
|
+
const idNode = typeRefNode.firstNamedChild;
|
|
80
|
+
if (!idNode) continue;
|
|
81
|
+
const typeName = idNode.text;
|
|
82
|
+
if (!declaredTypes.has(typeName)) {
|
|
83
|
+
diagnostics.push({
|
|
84
|
+
range: nodeToRange(idNode),
|
|
85
|
+
message: `Type '${typeName}' is not declared in this file (assuming native or external).`,
|
|
86
|
+
severity: DiagnosticSeverity.Warning,
|
|
87
|
+
source: 'agent-dsl',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return diagnostics;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function diagnoseFlow(tree) {
|
|
99
|
+
const diagnostics = [];
|
|
100
|
+
|
|
101
|
+
// ── Rule 1: Dangling transitions ──────────────────────────────────────────
|
|
102
|
+
const definedStates = new Set(
|
|
103
|
+
nodesOfType(tree, 'state_decl')
|
|
104
|
+
.map(n => n.childForFieldName('name')?.text)
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
function checkTransitionTarget(stateNode) {
|
|
109
|
+
if (!stateNode) return;
|
|
110
|
+
const target = stateNode.text;
|
|
111
|
+
if (!definedStates.has(target)) {
|
|
112
|
+
const external = target.includes('.');
|
|
113
|
+
diagnostics.push({
|
|
114
|
+
range: nodeToRange(stateNode),
|
|
115
|
+
message: external
|
|
116
|
+
? `State '${target}' is not defined locally (assuming external flow reference).`
|
|
117
|
+
: `State '${target}' is not defined in this file.`,
|
|
118
|
+
severity: external ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error,
|
|
119
|
+
source: 'behavior-dsl',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const n of nodesOfType(tree, 'transition_stmt')) {
|
|
125
|
+
checkTransitionTarget(n.childForFieldName('state'));
|
|
126
|
+
}
|
|
127
|
+
for (const n of nodesOfType(tree, 'intent_trigger')) {
|
|
128
|
+
// inline form: on intent "..." next <state>
|
|
129
|
+
checkTransitionTarget(n.childForFieldName('state'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Rule 2: Dead-end interact ─────────────────────────────────────────────
|
|
133
|
+
for (const interactNode of nodesOfType(tree, 'interact_stmt')) {
|
|
134
|
+
let ancestor = interactNode.parent;
|
|
135
|
+
while (ancestor && ancestor.type !== 'state_decl') {
|
|
136
|
+
ancestor = ancestor.parent;
|
|
137
|
+
}
|
|
138
|
+
if (!ancestor) continue;
|
|
139
|
+
|
|
140
|
+
const hasNext = ancestor.descendantsOfType('transition_stmt').length > 0;
|
|
141
|
+
const hasIntent = ancestor.descendantsOfType('intent_trigger').length > 0;
|
|
142
|
+
const hasOfftopic = ancestor.descendantsOfType('offtopic_stmt').length > 0;
|
|
143
|
+
|
|
144
|
+
if (!hasNext && !hasIntent && !hasOfftopic) {
|
|
145
|
+
diagnostics.push({
|
|
146
|
+
range: nodeToRange(interactNode),
|
|
147
|
+
message: "This state calls interact but has no 'transition' or 'on intent/offtopic'. This will trap the agent.",
|
|
148
|
+
severity: DiagnosticSeverity.Warning,
|
|
149
|
+
source: 'behavior-dsl',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return diagnostics;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function diagnose(langId, tree, text) {
|
|
158
|
+
if (!tree) return [];
|
|
159
|
+
if (langId === 'agent') return diagnoseAgent(tree, text);
|
|
160
|
+
if (langId === 'behavior') return diagnoseFlow(tree);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { diagnose };
|
|
@@ -0,0 +1,80 @@
|
|
|
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 BLOCK_HEADERS = /^(on\s+(intent|offtopic|fallback|complete|failed)|if|else|after|parallel)\b/;
|
|
20
|
+
const TOP_LEVEL_LINE = /^(state|merge)\s|^on\s+event\b/;
|
|
21
|
+
|
|
22
|
+
function formatBehavior(text) {
|
|
23
|
+
const edits = [];
|
|
24
|
+
const lines = text.split('\n');
|
|
25
|
+
let mode = 'PREAMBLE'; // PREAMBLE | STATE_BODY | NESTED_BODY
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const raw = lines[i];
|
|
29
|
+
const trimmed = raw.trim();
|
|
30
|
+
if (!trimmed) continue;
|
|
31
|
+
|
|
32
|
+
let expectedIndent;
|
|
33
|
+
if (TOP_LEVEL_LINE.test(trimmed)) {
|
|
34
|
+
mode = /^state\s/.test(trimmed) ? 'STATE_BODY' : 'PREAMBLE';
|
|
35
|
+
expectedIndent = 0;
|
|
36
|
+
} else if (mode === 'PREAMBLE') {
|
|
37
|
+
expectedIndent = 0;
|
|
38
|
+
} else if (mode === 'STATE_BODY') {
|
|
39
|
+
if (BLOCK_HEADERS.test(trimmed)) mode = 'NESTED_BODY';
|
|
40
|
+
expectedIndent = 2;
|
|
41
|
+
} else {
|
|
42
|
+
expectedIndent = BLOCK_HEADERS.test(trimmed) ? 2 : 4;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const actualIndent = raw.length - raw.trimStart().length;
|
|
46
|
+
if (actualIndent !== expectedIndent) {
|
|
47
|
+
edits.push({
|
|
48
|
+
range: { start: { line: i, character: 0 }, end: { line: i, character: actualIndent } },
|
|
49
|
+
newText: ' '.repeat(expectedIndent),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return edits;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatAgent(text) {
|
|
57
|
+
const edits = [];
|
|
58
|
+
const lines = text.split('\n');
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
const raw = lines[i];
|
|
61
|
+
if (!raw.trim()) continue;
|
|
62
|
+
const expectedIndent = /^\s/.test(raw) ? 2 : 0;
|
|
63
|
+
const actualIndent = raw.length - raw.trimStart().length;
|
|
64
|
+
if (actualIndent !== expectedIndent) {
|
|
65
|
+
edits.push({
|
|
66
|
+
range: { start: { line: i, character: 0 }, end: { line: i, character: actualIndent } },
|
|
67
|
+
newText: ' '.repeat(expectedIndent),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return edits;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function format(langId, text) {
|
|
75
|
+
if (langId === 'behavior') return formatBehavior(text);
|
|
76
|
+
if (langId === 'agent') return formatAgent(text);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { format };
|
|
@@ -0,0 +1,86 @@
|
|
|
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 { MarkupKind } = require('vscode-languageserver');
|
|
20
|
+
|
|
21
|
+
const AGENT_DOCS = {
|
|
22
|
+
'agent': '**`agent name`**\n\nDeclares a new agent. The central node of the manifest.',
|
|
23
|
+
'domain': '**`domain url`**\n\nDeclares the canonical domain for this agent, establishing cryptographic identity and ownership.',
|
|
24
|
+
'license': '**`license type`**\n\nDeclares the license under which this agent is distributed (e.g., MIT, Copyright).',
|
|
25
|
+
'terms': '**`terms url`**\n\nLink to the terms of service.',
|
|
26
|
+
'privacy': '**`privacy url`**\n\nLink to the privacy policy.',
|
|
27
|
+
'description': '**`description`**\n\nA brief description of the agent, used by the Runtime for semantic indexing.',
|
|
28
|
+
'behavior': '**`behavior file.behavior`**\n\nThe `.behavior` file that manages the state and transitions of this agent.',
|
|
29
|
+
'requires': '**`requires Type`**\n\nTypes (native or custom) that the Runtime must ensure exist in context before triggering the `.behavior`.',
|
|
30
|
+
'input': '**`input Type`**\n\nThe input data types expected for this agent to operate.',
|
|
31
|
+
'capabilities': '**`capabilities Action`**\n\nThe Actions or capabilities this agent can execute. Also acts as a Sandboxing Contract.',
|
|
32
|
+
'output': '**`output Type`**\n\nThe data type this agent returns.',
|
|
33
|
+
'type': '**`type name`**\n\nDeclares a custom type to anchor custom typing to Wikidata or Schema.org.',
|
|
34
|
+
'concept': '**`concept url`**\n\nThe Wikidata or Schema.org concept URL this type maps to.',
|
|
35
|
+
'schema': '**`schema file.json`**\n\nA JSON schema file for this type.',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const BEHAVIOR_DOCS = {
|
|
39
|
+
'merge': '**`merge "file.behavior"`**\n\nIncludes another `.behavior` file. Must appear before any `state` or `on event` declarations (preamble-only, eager loading).',
|
|
40
|
+
'state': '**`state name`**\n\nDeclares a named state. States contain the logic that runs while the agent is in that state.',
|
|
41
|
+
'on': '**`on event|intent|offtopic|fallback|complete|failed`**\n\nBinds a handler to a trigger. Top-level: `on event`. Inside a state: `on intent`, `on offtopic`, `on fallback`. After parallel: `on complete`, `on failed`.',
|
|
42
|
+
'run': '**`run script|subagent|tool "target"`**\n\nExecutes a script, subagent, or tool. Accepts optional modifiers: `silent`, `in background`, `each collection`.',
|
|
43
|
+
'guide': '**`guide "text"`**\n\nInjects a system-level instruction into the conversation context, shaping the agent\'s persona or approach without being visible as a reply.',
|
|
44
|
+
'teach': '**`teach "text"`**\n\nAdds a fact or constraint to the agent\'s working knowledge for the duration of this state.',
|
|
45
|
+
'goal': '**`goal "text"`**\n\nSets the agent\'s objective for this state, used by the runtime for planning and alignment checks.',
|
|
46
|
+
'interact': '**`interact [requiring "text"]`**\n\nPauses execution and waits for user input. Optionally enforces a requirement before continuing.',
|
|
47
|
+
'set': '**`set domain.var = value`**\n\nAssigns a value to a memory variable. Domains: `context`, `session`, `worksession`, `user`.',
|
|
48
|
+
'context': '**`context`** memory domain — scoped to the current agent run.',
|
|
49
|
+
'session': '**`session`** memory domain — persists for the user\'s current session.',
|
|
50
|
+
'worksession': '**`worksession`** memory domain — persists across a task-oriented work session (isolated per task).',
|
|
51
|
+
'user': '**`user`** memory domain — persists across sessions for a given user.',
|
|
52
|
+
'transition': '**`transition to state`**\n\nTransitions immediately to the named state.',
|
|
53
|
+
'if': '**`if condition`**\n\nConditional execution. Condition can use `==`, `!=`, `>`, `<`, `>=`, `<=`, `and`, `or`.',
|
|
54
|
+
'else': '**`else`**\n\nAlternative branch of an `if` statement.',
|
|
55
|
+
'after': '**`after N prompts`**\n\n[Experimental] Executes a block after N user prompts have occurred in this state.',
|
|
56
|
+
'parallel': '**`parallel`**\n\n[Experimental] Runs a block of `run` statements concurrently. Follow with `on complete` and `on failed` handlers.',
|
|
57
|
+
'apply': '**`apply css|html|video "text"`**\n\nApplies a UI manipulation to a CSS selector, HTML element, or video element.',
|
|
58
|
+
'remove': '**`remove css|html|video "text"`**\n\nRemoves a UI element by selector or reference.',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function provideHover(langId, text, position) {
|
|
62
|
+
const lines = text.split('\n');
|
|
63
|
+
const line = lines[position.line] || '';
|
|
64
|
+
const ch = position.character;
|
|
65
|
+
|
|
66
|
+
// Find word at cursor
|
|
67
|
+
let start = ch, end = ch;
|
|
68
|
+
while (start > 0 && /[a-zA-Z0-9_]/.test(line[start - 1])) start--;
|
|
69
|
+
while (end < line.length && /[a-zA-Z0-9_]/.test(line[end])) end++;
|
|
70
|
+
const word = line.slice(start, end);
|
|
71
|
+
if (!word) return null;
|
|
72
|
+
|
|
73
|
+
const docs = langId === 'agent' ? AGENT_DOCS : BEHAVIOR_DOCS;
|
|
74
|
+
const doc = docs[word];
|
|
75
|
+
if (!doc) return null;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
contents: { kind: MarkupKind.Markdown, value: doc },
|
|
79
|
+
range: {
|
|
80
|
+
start: { line: position.line, character: start },
|
|
81
|
+
end: { line: position.line, character: end },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { provideHover };
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { fileURLToPath, pathToFileURL } = require('url');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { nodesOfType, nodeToRange } = require('../parser');
|
|
22
|
+
|
|
23
|
+
function provideDocumentLinks(langId, tree, docUri) {
|
|
24
|
+
if (!tree) return [];
|
|
25
|
+
|
|
26
|
+
let docDir;
|
|
27
|
+
try {
|
|
28
|
+
docDir = path.dirname(fileURLToPath(docUri));
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const links = [];
|
|
34
|
+
|
|
35
|
+
function addLink(fileNode, rawText) {
|
|
36
|
+
const filename = rawText.replace(/^"|"$/g, ''); // strip optional quotes
|
|
37
|
+
if (!filename) return;
|
|
38
|
+
links.push({
|
|
39
|
+
range: nodeToRange(fileNode),
|
|
40
|
+
target: pathToFileURL(path.resolve(docDir, filename)).toString(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (langId === 'agent') {
|
|
45
|
+
for (const node of nodesOfType(tree, 'behavior_block')) {
|
|
46
|
+
const fileNode = node.childForFieldName('file');
|
|
47
|
+
if (fileNode) addLink(fileNode, fileNode.text);
|
|
48
|
+
}
|
|
49
|
+
for (const node of nodesOfType(tree, 'schema_prop')) {
|
|
50
|
+
const fileNode = node.childForFieldName('file');
|
|
51
|
+
if (fileNode) addLink(fileNode, fileNode.text);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (langId === 'behavior') {
|
|
56
|
+
for (const node of nodesOfType(tree, 'merge_decl')) {
|
|
57
|
+
const pathNode = node.childForFieldName('path');
|
|
58
|
+
if (pathNode) addLink(pathNode, pathNode.text);
|
|
59
|
+
}
|
|
60
|
+
for (const node of nodesOfType(tree, 'run_stmt')) {
|
|
61
|
+
const runTypeNode = node.childForFieldName('run_type');
|
|
62
|
+
if (runTypeNode?.text !== 'script') continue;
|
|
63
|
+
const targetNode = node.childForFieldName('target');
|
|
64
|
+
if (targetNode) addLink(targetNode, targetNode.text);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return links;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { provideDocumentLinks };
|
|
@@ -0,0 +1,65 @@
|
|
|
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, nodeToRange, wordAtPosition } = require('../parser');
|
|
20
|
+
|
|
21
|
+
function provideReferences(langId, tree, text, uri, position) {
|
|
22
|
+
if (!tree) return [];
|
|
23
|
+
|
|
24
|
+
const { word } = wordAtPosition(text, position.line, position.character);
|
|
25
|
+
if (!word) return [];
|
|
26
|
+
|
|
27
|
+
const locations = [];
|
|
28
|
+
|
|
29
|
+
function add(node) {
|
|
30
|
+
locations.push({ uri, range: nodeToRange(node) });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (langId === 'behavior') {
|
|
34
|
+
// Declaration
|
|
35
|
+
for (const n of nodesOfType(tree, 'state_decl')) {
|
|
36
|
+
const nameNode = n.childForFieldName('name');
|
|
37
|
+
if (nameNode?.text === word) add(nameNode);
|
|
38
|
+
}
|
|
39
|
+
// Direct transitions: next <state>
|
|
40
|
+
for (const n of nodesOfType(tree, 'transition_stmt')) {
|
|
41
|
+
const stateNode = n.childForFieldName('state');
|
|
42
|
+
if (stateNode?.text === word) add(stateNode);
|
|
43
|
+
}
|
|
44
|
+
// Inline intent handlers: on intent "..." next <state>
|
|
45
|
+
for (const n of nodesOfType(tree, 'intent_trigger')) {
|
|
46
|
+
const stateNode = n.childForFieldName('state');
|
|
47
|
+
if (stateNode?.text === word) add(stateNode);
|
|
48
|
+
}
|
|
49
|
+
} else if (langId === 'agent') {
|
|
50
|
+
// Declaration
|
|
51
|
+
for (const n of nodesOfType(tree, 'type_decl')) {
|
|
52
|
+
const nameNode = n.childForFieldName('name');
|
|
53
|
+
if (nameNode?.text === word) add(nameNode);
|
|
54
|
+
}
|
|
55
|
+
// All type_ref usages (input/output/requires/capabilities blocks and property types)
|
|
56
|
+
for (const n of nodesOfType(tree, 'type_ref')) {
|
|
57
|
+
const idNode = n.firstNamedChild;
|
|
58
|
+
if (idNode?.text === word) add(idNode);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return locations;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { provideReferences };
|
|
@@ -0,0 +1,66 @@
|
|
|
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, nodeToRange, wordAtPosition } = require('../parser');
|
|
20
|
+
|
|
21
|
+
function provideRenameEdits(langId, tree, text, uri, position, newName) {
|
|
22
|
+
if (!tree) return null;
|
|
23
|
+
|
|
24
|
+
const { word: oldName } = wordAtPosition(text, position.line, position.character);
|
|
25
|
+
if (!oldName) return null;
|
|
26
|
+
|
|
27
|
+
const edits = [];
|
|
28
|
+
|
|
29
|
+
function addEdit(node) {
|
|
30
|
+
edits.push({ range: nodeToRange(node), newText: newName });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (langId === 'behavior') {
|
|
34
|
+
// Rename state declarations
|
|
35
|
+
for (const n of nodesOfType(tree, 'state_decl')) {
|
|
36
|
+
const nameNode = n.childForFieldName('name');
|
|
37
|
+
if (nameNode?.text === oldName) addEdit(nameNode);
|
|
38
|
+
}
|
|
39
|
+
// Rename transition targets
|
|
40
|
+
for (const n of nodesOfType(tree, 'transition_stmt')) {
|
|
41
|
+
const stateNode = n.childForFieldName('state');
|
|
42
|
+
if (stateNode?.text === oldName) addEdit(stateNode);
|
|
43
|
+
}
|
|
44
|
+
// Rename inline intent handler targets
|
|
45
|
+
for (const n of nodesOfType(tree, 'intent_trigger')) {
|
|
46
|
+
const stateNode = n.childForFieldName('state');
|
|
47
|
+
if (stateNode?.text === oldName) addEdit(stateNode);
|
|
48
|
+
}
|
|
49
|
+
} else if (langId === 'agent') {
|
|
50
|
+
// Rename type declarations
|
|
51
|
+
for (const n of nodesOfType(tree, 'type_decl')) {
|
|
52
|
+
const nameNode = n.childForFieldName('name');
|
|
53
|
+
if (nameNode?.text === oldName) addEdit(nameNode);
|
|
54
|
+
}
|
|
55
|
+
// Rename all type_ref usages
|
|
56
|
+
for (const n of nodesOfType(tree, 'type_ref')) {
|
|
57
|
+
const idNode = n.firstNamedChild;
|
|
58
|
+
if (idNode?.text === oldName) addEdit(idNode);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (edits.length === 0) return null;
|
|
63
|
+
return { changes: { [uri]: edits } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { provideRenameEdits };
|
|
@@ -0,0 +1,78 @@
|
|
|
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, nodeToRange } = require('../parser');
|
|
20
|
+
|
|
21
|
+
// DocumentSymbol kind constants (LSP spec §3.17.5)
|
|
22
|
+
const Kind = { Class: 5, Struct: 23, Event: 24 };
|
|
23
|
+
|
|
24
|
+
function provideDocumentSymbols(langId, tree) {
|
|
25
|
+
if (!tree) return [];
|
|
26
|
+
const symbols = [];
|
|
27
|
+
|
|
28
|
+
if (langId === 'behavior') {
|
|
29
|
+
for (const node of nodesOfType(tree, 'state_decl')) {
|
|
30
|
+
const nameNode = node.childForFieldName('name');
|
|
31
|
+
if (!nameNode) continue;
|
|
32
|
+
symbols.push({
|
|
33
|
+
name: nameNode.text,
|
|
34
|
+
kind: Kind.Class,
|
|
35
|
+
range: nodeToRange(node),
|
|
36
|
+
selectionRange: nodeToRange(nameNode),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
for (const node of nodesOfType(tree, 'trigger_decl')) {
|
|
40
|
+
const eventNode = node.childForFieldName('event');
|
|
41
|
+
if (!eventNode) continue;
|
|
42
|
+
const name = eventNode.text.replace(/^"|"$/g, '');
|
|
43
|
+
symbols.push({
|
|
44
|
+
name: `on event: ${name}`,
|
|
45
|
+
kind: Kind.Event,
|
|
46
|
+
range: nodeToRange(node),
|
|
47
|
+
selectionRange: nodeToRange(eventNode),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (langId === 'agent') {
|
|
53
|
+
for (const node of nodesOfType(tree, 'agent_decl')) {
|
|
54
|
+
const nameNode = node.childForFieldName('name');
|
|
55
|
+
if (!nameNode) continue;
|
|
56
|
+
symbols.push({
|
|
57
|
+
name: nameNode.text,
|
|
58
|
+
kind: Kind.Class,
|
|
59
|
+
range: nodeToRange(node),
|
|
60
|
+
selectionRange: nodeToRange(nameNode),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
for (const node of nodesOfType(tree, 'type_decl')) {
|
|
64
|
+
const nameNode = node.childForFieldName('name');
|
|
65
|
+
if (!nameNode) continue;
|
|
66
|
+
symbols.push({
|
|
67
|
+
name: nameNode.text,
|
|
68
|
+
kind: Kind.Struct,
|
|
69
|
+
range: nodeToRange(node),
|
|
70
|
+
selectionRange: nodeToRange(nameNode),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return symbols;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { provideDocumentSymbols };
|