Skip to content

Commit 6ef61f7

Browse files
committed
Add editor_definition and editor_references tools backed by editor's LSP
New editor/getDefinition and editor/getReferences server requests let the LLM navigate to a symbol definition/references using the editor's language server, resolving the symbol position server-side from path + line + symbol. Clients answer success/starting/no-server/error so the LLM gets actionable feedback: retry while the LSP is starting, fall back to grep otherwise. Gated by new client capabilities and toolCall.editorNav.enabled config. #351
1 parent 769ed7f commit 6ef61f7

20 files changed

Lines changed: 849 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Add `editor_definition` and `editor_references` tools backed by the editor's language server, behind new client capabilities and `toolCall.editorNav.enabled` config. #351
6+
57
## 0.154.2
68

79
- Retry LLM requests on network failures (VPN/wifi drops: DNS, refused or timed-out connects) with "Network issues" progress, instead of failing or hanging.

docs/config.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1188,7 +1188,21 @@
11881188
"type": "integer",
11891189
"description": "Maximum length of shell command summary.",
11901190
"markdownDescription": "Maximum length of shell command summary.",
1191-
"default": 25
1191+
"default": 35
1192+
}
1193+
}
1194+
},
1195+
"editorNav": {
1196+
"type": "object",
1197+
"description": "Configuration for the editor LSP navigation tools (editor_definition and editor_references).",
1198+
"markdownDescription": "Configuration for the editor LSP navigation tools (`editor_definition` and `editor_references`).",
1199+
"additionalProperties": false,
1200+
"properties": {
1201+
"enabled": {
1202+
"type": "boolean",
1203+
"description": "Whether to offer the editor_definition and editor_references tools when the client declares support for them.",
1204+
"markdownDescription": "Whether to offer the `editor_definition` and `editor_references` tools when the client declares support for them.",
1205+
"default": true
11921206
}
11931207
}
11941208
},

docs/config/introduction.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ By default ECA consider the following as the base configuration:
136136
"maxLines": 2000
137137
},
138138
"shellCommand": {
139-
"summaryMaxLength": 30
139+
"summaryMaxLength": 35
140+
},
141+
"editorNav": {
142+
"enabled": true
140143
}
141144
},
142145
"mcpTimeoutSeconds" : 60,

docs/features.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ ECA support built-in tools to avoid user extra installation and configuration, t
7777
Provides access to get information from editor workspaces.
7878

7979
- `editor_diagnostics`: Ask client about the diagnostics (like LSP diagnostics).
80+
- `editor_definition`: Ask client for the definition locations of a symbol (like LSP definition). Requires client capability, can be disabled via `toolCall.editorNav.enabled` config.
81+
- `editor_references`: Ask client for the references of a symbol (like LSP references). Requires client capability, can be disabled via `toolCall.editorNav.enabled` config.
8082

8183
!!! info "Custom Tools"
8284

docs/protocol.md

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,18 @@ interface ClientCapabilities {
211211
* server request.
212212
*/
213213
diagnostics?: boolean;
214+
215+
/**
216+
* Whether client supports provide the definition locations of a symbol
217+
* (Ex: LSP definition) via `editor/getDefinition` server request.
218+
*/
219+
definition?: boolean;
220+
221+
/**
222+
* Whether client supports provide the references of a symbol
223+
* (Ex: LSP references) via `editor/getReferences` server request.
224+
*/
225+
references?: boolean;
214226
}
215227

216228
/**
@@ -2390,6 +2402,143 @@ interface EditorDiagnostic {
23902402
}
23912403
```
23922404

2405+
### Editor definition (↪️)
2406+
2407+
A server request to retrieve from the editor the definition location(s) of the symbol
2408+
at a position in a file, typically backed by the editor's language server (LSP).
2409+
Only sent when client declared the `codeAssistant.editor.definition` capability.
2410+
2411+
All positions in request and response are 1-based and `character` offsets count
2412+
UTF-16 code units (the LSP default encoding); clients should convert to/from
2413+
their language server positions (LSP ones are 0-based).
2414+
2415+
Clients should answer with a `status` the server can act on instead of hanging:
2416+
2417+
- If no language server is attached to the file yet, clients SHOULD attempt to
2418+
start/attach the appropriate one (e.g. opening the file in background) and answer
2419+
`starting` while it initializes; the server retries the request periodically within
2420+
its `lspTimeoutSeconds` budget before giving up.
2421+
- `no-server` should be answered only when the client cannot provide a language
2422+
server for that file at all.
2423+
- `error` (with `message`) should be answered on failures, so the server can give
2424+
actionable feedback to the LLM.
2425+
2426+
_Request:_
2427+
2428+
* method: `editor/getDefinition`
2429+
* params: `EditorGetDefinitionParams` defined as follows:
2430+
2431+
```typescript
2432+
interface EditorGetDefinitionParams {
2433+
/**
2434+
* The uri of the file containing the symbol.
2435+
*/
2436+
uri: string;
2437+
2438+
/**
2439+
* The position (1-based) of the symbol in the file.
2440+
*/
2441+
position: {
2442+
line: number;
2443+
character: number;
2444+
};
2445+
}
2446+
```
2447+
2448+
_Response:_
2449+
2450+
```typescript
2451+
interface EditorGetDefinitionResponse {
2452+
/**
2453+
* The outcome of the request:
2454+
* - 'success': locations contain the results (may be empty when symbol has no definition).
2455+
* - 'starting': a language server is starting/initializing for this file, server may retry.
2456+
* - 'no-server': no language server available for this file.
2457+
* - 'error': failed to compute the result, message should explain why.
2458+
*/
2459+
status: 'success' | 'starting' | 'no-server' | 'error';
2460+
2461+
/**
2462+
* The definition locations when status is 'success'.
2463+
*/
2464+
locations?: EditorLocation[];
2465+
2466+
/**
2467+
* Optional detail for 'no-server' and 'error' statuses.
2468+
*/
2469+
message?: string;
2470+
}
2471+
2472+
interface EditorLocation {
2473+
/**
2474+
* The location file uri.
2475+
*/
2476+
uri: string;
2477+
2478+
/**
2479+
* The location range (1-based).
2480+
*/
2481+
range: Range;
2482+
}
2483+
```
2484+
2485+
### Editor references (↪️)
2486+
2487+
A server request to retrieve from the editor the references of the symbol
2488+
at a position in a file, typically backed by the editor's language server (LSP).
2489+
Only sent when client declared the `codeAssistant.editor.references` capability.
2490+
2491+
Follows the same position conventions and `status` semantics as `editor/getDefinition`.
2492+
2493+
_Request:_
2494+
2495+
* method: `editor/getReferences`
2496+
* params: `EditorGetReferencesParams` defined as follows:
2497+
2498+
```typescript
2499+
interface EditorGetReferencesParams {
2500+
/**
2501+
* The uri of the file containing the symbol.
2502+
*/
2503+
uri: string;
2504+
2505+
/**
2506+
* The position (1-based) of the symbol in the file.
2507+
*/
2508+
position: {
2509+
line: number;
2510+
character: number;
2511+
};
2512+
2513+
/**
2514+
* Whether to include the symbol declaration in the results.
2515+
* Defaults to true when absent.
2516+
*/
2517+
includeDeclaration?: boolean;
2518+
}
2519+
```
2520+
2521+
_Response:_
2522+
2523+
```typescript
2524+
interface EditorGetReferencesResponse {
2525+
/**
2526+
* Same semantics as EditorGetDefinitionResponse status.
2527+
*/
2528+
status: 'success' | 'starting' | 'no-server' | 'error';
2529+
2530+
/**
2531+
* The reference locations when status is 'success'.
2532+
*/
2533+
locations?: EditorLocation[];
2534+
2535+
/**
2536+
* Optional detail for 'no-server' and 'error' statuses.
2537+
*/
2538+
message?: string;
2539+
}
2540+
```
2541+
23932542
### Chat ask question (↪️)
23942543

23952544
A server request to ask the user a question and receive an answer during a chat session.
@@ -2777,8 +2926,8 @@ interface EcaServerUpdatedParams {
27772926
* The built-in tools supported by eca.
27782927
*
27792928
* Built-in tools include: read_file, write_file, edit_file, move_file,
2780-
* directory_tree, shell_command, editor_diagnostics, compact_chat,
2781-
* skill, spawn_agent, and task.
2929+
* directory_tree, shell_command, editor_diagnostics, editor_definition,
2930+
* editor_references, compact_chat, skill, spawn_agent, and task.
27822931
*
27832932
* Note: `spawn_agent` and `task` are excluded from subagent tool sets.
27842933
* `spawn_agent` is excluded to prevent nesting, and `task` because

integration-test/entrypoint.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
integration.chat.hooks-test
2121
integration.chat.commands-test
2222
integration.chat.mcp-remote-test
23+
integration.chat.editor-lsp-test
2324
integration.chat.invalid-image-test
2425
integration.chat.background-jobs-test
2526
integration.chat.subagent-test
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
(ns integration.chat.editor-lsp-test
2+
(:require
3+
[clojure.string :as string]
4+
[clojure.test :refer [deftest is testing]]
5+
[integration.eca :as eca]
6+
[integration.fixture :as fixture]
7+
[integration.helper :as h]
8+
[llm-mock.mocks :as llm.mocks]
9+
[matcher-combinators.core :as mc]
10+
[matcher-combinators.matchers :as m]
11+
[matcher-combinators.test :refer [match?]]))
12+
13+
(eca/clean-after-test)
14+
15+
(defn ^:private await-content-matching
16+
"Consumes chat/contentReceived notifications until one matches, returning it."
17+
[chat-id role content]
18+
(loop [tries 0]
19+
(if (< tries 50)
20+
(let [actual (eca/client-awaits-server-notification :chat/contentReceived)]
21+
(if (mc/indicates-match? (mc/match {:chatId chat-id :role role :content content} actual))
22+
actual
23+
(recur (inc tries))))
24+
(throw (ex-info "Timeout waiting for matching content" {:content content})))))
25+
26+
(deftest editor-definition-and-references
27+
(eca/start-process!)
28+
29+
(eca/request! (fixture/initialize-request
30+
{:initializationOptions fixture/default-init-options
31+
:capabilities {:codeAssistant {:chat {}
32+
:editor {:definition true
33+
:references true}}}}))
34+
(eca/notify! (fixture/initialized-notification))
35+
(let [file1-path (h/project-path->canon-path "resources/file1.md")
36+
file1-uri (h/file->uri file1-path)]
37+
(testing "definition found via the editor's language server"
38+
(eca/mock-response :editor/getDefinition
39+
{:status "success"
40+
:locations [{:uri file1-uri
41+
:range {:start {:line 1 :character 1}
42+
:end {:line 1 :character 10}}}]})
43+
(llm.mocks/set-case! :editor-lsp-0)
44+
(let [resp (eca/request! (fixture/chat-prompt-request
45+
{:model "openai/gpt-5-mini"
46+
:message "Where is Something defined?"}))
47+
chat-id (:chatId resp)]
48+
(is (match? {:chatId (m/pred string?) :status "prompting"} resp))
49+
(is (match?
50+
{:type "toolCalled"
51+
:origin "native"
52+
:name "editor_definition"
53+
:summary "LSP definition: Something"
54+
:error false
55+
:outputs [{:type "text"
56+
:text (str file1-path ":1:1: Something here")}]}
57+
(:content (await-content-matching chat-id "assistant" {:type "toolCalled"
58+
:name "editor_definition"}))))
59+
(testing "the server sent the resolved 1-based position to the client"
60+
(is (match?
61+
{:uri (m/pred #(string/ends-with? % "file1.md"))
62+
:position {:line 1 :character 1}}
63+
(eca/client-awaits-server-request :editor/getDefinition))))
64+
(testing "capability-gated tools were offered to the LLM"
65+
(is (match?
66+
{:tools (m/embeds [{:name "eca__editor_definition"}
67+
{:name "eca__editor_references"}])}
68+
(llm.mocks/get-req-body :editor-lsp-0))))))
69+
(testing "references failing in the editor gives actionable error to the LLM"
70+
(eca/mock-response :editor/getReferences
71+
{:status "error"
72+
:message "lsp busy"})
73+
(llm.mocks/set-case! :editor-lsp-1)
74+
(let [resp (eca/request! (fixture/chat-prompt-request
75+
{:model "openai/gpt-5-mini"
76+
:message "Who uses Something?"}))
77+
chat-id (:chatId resp)]
78+
(is (match?
79+
{:type "toolCalled"
80+
:origin "native"
81+
:name "editor_references"
82+
:error true
83+
:outputs [{:type "text"
84+
:text "Editor failed to find references: lsp busy. Use eca__grep as fallback."}]}
85+
(:content (await-content-matching chat-id "assistant" {:type "toolCalled"
86+
:name "editor_references"}))))
87+
(is (match?
88+
{:uri (m/pred #(string/ends-with? % "file1.md"))
89+
:position {:line 1 :character 1}
90+
:includeDeclaration false}
91+
(eca/client-awaits-server-request :editor/getReferences)))))))

integration-test/llm_mock/openai.clj

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,23 @@
226226
:status "completed"}})
227227
(hk/close ch)))
228228

229+
(defn ^:private editor-lsp-0 [ch body]
230+
(if (some #(= "function_call_output" (:type %)) (:input body))
231+
(send-text-response! ch "Found it")
232+
(send-function-call-response! ch "item-1" "tool-1" "eca__editor_definition"
233+
{:path (h/project-path->canon-path "resources/file1.md")
234+
:line 1
235+
:symbol "Something"})))
236+
237+
(defn ^:private editor-lsp-1 [ch body]
238+
(if (some #(= "function_call_output" (:type %)) (:input body))
239+
(send-text-response! ch "Could not check references")
240+
(send-function-call-response! ch "item-1" "tool-1" "eca__editor_references"
241+
{:path (h/project-path->canon-path "resources/file1.md")
242+
:line 1
243+
:symbol "Something"
244+
:include_declaration false})))
245+
229246
(defonce ^:private subagent-follow-up-attempt* (atom 0))
230247

231248
(defn ^:private subagent-spawn-0
@@ -302,5 +319,7 @@
302319
:reasoning-0 (reasoning-0 ch)
303320
:reasoning-1 (reasoning-1 ch)
304321
:tool-calling-0 (tool-calling-0 ch body)
322+
:editor-lsp-0 (editor-lsp-0 ch body)
323+
:editor-lsp-1 (editor-lsp-1 ch body)
305324
:subagent-spawn-0 (subagent-spawn-0 ch body)
306325
:subagent-retry-0 (subagent-spawn-0 ch body true)))))})))

resources/META-INF/native-image/eca/eca/native-image.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ Args=-J-Dborkdude.dynaload.aot=true \
3030
-H:IncludeResources=prompts/rewrite.md \
3131
-H:IncludeResources=prompts/tools/directory_tree.md \
3232
-H:IncludeResources=prompts/tools/edit_file.md \
33+
-H:IncludeResources=prompts/tools/editor_definition.md \
3334
-H:IncludeResources=prompts/tools/editor_diagnostics.md \
35+
-H:IncludeResources=prompts/tools/editor_references.md \
3436
-H:IncludeResources=prompts/tools/grep.md \
3537
-H:IncludeResources=prompts/tools/move_file.md \
3638
-H:IncludeResources=prompts/tools/preview_file_change.md \

resources/prompts/code_agent.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ You have tools at your disposal to solve the coding task. Follow these rules reg
2626
2. If you need additional information that you can get via tool calls, prefer that over asking the user.
2727
3. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information: do NOT guess or make up an answer.
2828
4. You have the capability to call multiple tools in a single response, batch your tool calls together for optimal performance.
29+
{% if toolEnabled_eca__editor_definition %}
30+
When you need to know where a symbol is defined, prefer `eca__editor_definition` over text search: it uses the editor's language server, which is precise and uses fewer tokens. Fall back to `eca__grep` if it fails.
31+
{% endif %}
32+
{% if toolEnabled_eca__editor_references %}
33+
When you need to find usages of a symbol, prefer `eca__editor_references` over text search: it uses the editor's language server, avoiding textual false positives. Fall back to `eca__grep` if it fails.
34+
{% endif %}
2935
{% if toolEnabled_eca__task %}
3036
## Task Tracking
3137

0 commit comments

Comments
 (0)