@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 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` | Lintingdangling transitions, dead-end interact (AST), undeclared types (AST) |
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 Flow grammar WASM binaries
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` once the WASM binaries are in `@dot-agent/tree-sitter/dist/`.
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` initializes asynchronously. The `onInitialize` handler is the only safe place to call `await initParsers()`:
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(); // blocks until both WASMs are loaded
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 parsers are ready before any `onHover`, `onCompletion`, etc. request arrives. Never call `initParsers()` outside `onInitialize`.
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
 
@@ -14,10 +14,8 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- 'use strict';
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 };
@@ -14,12 +14,10 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- 'use strict';
18
-
19
- const fs = require('fs');
20
- const { fileURLToPath, pathToFileURL } = require('url');
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 = path.resolve(docDir, filename);
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 = fs.readFileSync(absPath, 'utf8'); } catch { continue; }
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, path.dirname(absPath), word, visited);
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 = path.dirname(fileURLToPath(uri));
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 };
@@ -14,199 +14,103 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- 'use strict';
18
-
19
- const fs = require('fs');
20
- const { fileURLToPath } = require('url');
21
- const path = require('path');
22
- const { DiagnosticSeverity } = require('vscode-languageserver');
23
- const { nodesOfType, nodeToRange, parseText } = require('../parser');
24
-
25
- const STRICT_BLOCK_TYPES = {
26
- input_block: 'input',
27
- output_block: 'output',
28
- requires_block: 'requires',
29
- capabilities_block: 'capabilities',
30
- };
31
-
32
- function diagnoseDescription(tree, text) {
33
- const diagnostics = [];
34
-
35
- // ── Strict block validation ───────────────────────────────────────────────
36
- const declaredTypes = new Set(
37
- nodesOfType(tree, 'type_decl')
38
- .map(n => n.childForFieldName('name')?.text)
39
- .filter(Boolean)
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
- // 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
- }
43
+ function hasMerge(text) {
44
+ return /^\s*merge\s+/m.test(text);
96
45
  }
97
46
 
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();
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
- 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
- });
56
+ if (langId === 'description') {
57
+ const msgs = await lintDescription(text, filePath);
58
+ return msgs.map(toDiagnostic);
114
59
  }
115
60
 
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;
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
- for (const child of node.children) walk(child);
88
+ const msgs = await lintBehavior(text, filePath, filePath, true);
89
+ return msgs.map(toDiagnostic);
133
90
  }
134
91
 
135
- walk(root);
136
- }
137
-
138
- function diagnoseBehavior(tree, docUri) {
139
- const diagnostics = [];
140
-
141
- // ── Rule 0: Syntax errors (ERROR / MISSING nodes) ─────────────────────────
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
- 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
-
157
- function checkTransitionTarget(stateNode) {
158
- if (!stateNode) return;
159
- const target = stateNode.text;
160
- if (!definedStates.has(target)) {
161
- const external = target.includes('.');
162
- diagnostics.push({
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
- return diagnostics;
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 };
@@ -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
- 'use strict';
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
- 'use strict';
18
-
19
- const { fileURLToPath, pathToFileURL } = require('url');
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 = path.dirname(fileURLToPath(docUri));
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(path.resolve(docDir, filename)).toString(),
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 };
@@ -14,11 +14,9 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- 'use strict';
17
+ import { nodesOfType, nodeToRange, wordAtPosition } from '../parser.js';
18
18
 
19
- const { nodesOfType, nodeToRange, wordAtPosition } = require('../parser');
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 };
@@ -14,11 +14,9 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- 'use strict';
17
+ import { nodesOfType, nodeToRange, wordAtPosition } from '../parser.js';
18
18
 
19
- const { nodesOfType, nodeToRange, wordAtPosition } = require('../parser');
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 };
@@ -14,14 +14,12 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- 'use strict';
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 };