Skip to content

Commit 96e06a3

Browse files
committed
fix LLM tools registration in multiroot workspaces
1 parent 32320be commit 96e06a3

File tree

8 files changed

+375
-92
lines changed

8 files changed

+375
-92
lines changed

package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,10 @@
689689
"symbol": {
690690
"type": "string",
691691
"description": "The symbol to find definition for. Supports: modules ('MyModule', 'GenServer'), functions ('MyModule.function', 'String.split/2'), types ('@type my_type'), macros ('defmacro'), Erlang modules (':gen_server'), Erlang functions (':lists.map/2'). Use qualified names (Module.function) for best results."
692+
},
693+
"file": {
694+
"type": "string",
695+
"description": "Optional absolute file path to specify which workspace to search. If not provided, uses the active editor's workspace or the first available workspace."
692696
}
693697
}
694698
}
@@ -727,6 +731,10 @@
727731
"module": {
728732
"type": "string",
729733
"description": "The module name to analyze dependencies for. Supports Elixir modules ('MyApp.MyModule', 'GenServer', 'Enum') and Erlang modules (':gen_server', ':ets'). Returns categorized dependency breakdown showing what the module depends on and what depends on it."
734+
},
735+
"file": {
736+
"type": "string",
737+
"description": "Optional absolute file path to specify which workspace to analyze. If not provided, uses the active editor's workspace or the first available workspace."
730738
}
731739
}
732740
}
@@ -749,6 +757,10 @@
749757
"items": {
750758
"type": "string"
751759
}
760+
},
761+
"file": {
762+
"type": "string",
763+
"description": "Optional absolute file path to specify which workspace to query. If not provided, uses the active editor's workspace or the first available workspace."
752764
}
753765
}
754766
}
@@ -768,6 +780,10 @@
768780
"symbol": {
769781
"type": "string",
770782
"description": "The symbol to find implementations for. Supports: behaviours ('GenServer', 'Application', 'Supervisor'), protocols ('Enumerable', 'Inspect', 'String.Chars'), callbacks ('GenServer.handle_call', 'Application.start'), functions with @impl annotations. Returns file locations and implementation signatures."
783+
},
784+
"file": {
785+
"type": "string",
786+
"description": "Optional absolute file path to specify which workspace to search. If not provided, uses the active editor's workspace or the first available workspace."
771787
}
772788
}
773789
}
@@ -787,6 +803,10 @@
787803
"module": {
788804
"type": "string",
789805
"description": "The module name to analyze type information for. Supports Elixir modules ('GenServer', 'MyApp.MyModule', 'Enum') and Erlang modules (':gen_server', ':ets'). Returns detailed type specifications, function signatures, callback definitions, and custom types with their documentation."
806+
},
807+
"file": {
808+
"type": "string",
809+
"description": "Optional absolute file path to specify which workspace to analyze. If not provided, uses the active editor's workspace or the first available workspace."
790810
}
791811
}
792812
}

src/definition-tool.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import {
44
ExecuteCommandRequest,
55
type LanguageClient,
66
} from "vscode-languageclient/node";
7+
import type { LanguageClientManager } from "./languageClientManager";
78

89
interface IParameters {
910
symbol: string;
11+
file?: string;
1012
}
1113

1214
interface IDefinitionResult {
@@ -15,7 +17,7 @@ interface IDefinitionResult {
1517
}
1618

1719
export class DefinitionTool implements vscode.LanguageModelTool<IParameters> {
18-
constructor(private client: LanguageClient) {}
20+
constructor(private clientManager: LanguageClientManager) {}
1921

2022
async prepareInvocation(
2123
options: vscode.LanguageModelToolInvocationPrepareOptions<IParameters>,
@@ -26,16 +28,58 @@ export class DefinitionTool implements vscode.LanguageModelTool<IParameters> {
2628
};
2729
}
2830

31+
private getClient(file?: string): LanguageClient | null {
32+
if (file) {
33+
try {
34+
const uri = vscode.Uri.file(file);
35+
return this.clientManager.getClientByUri(uri);
36+
} catch (error) {
37+
console.warn(`ElixirLS: Failed to get client for file ${file}:`, error);
38+
}
39+
}
40+
41+
// Fall back to active editor
42+
const activeEditor = vscode.window.activeTextEditor;
43+
if (activeEditor) {
44+
try {
45+
return this.clientManager.getClientByUri(activeEditor.document.uri);
46+
} catch (error) {
47+
console.warn(
48+
"ElixirLS: Failed to get client for active editor:",
49+
error,
50+
);
51+
}
52+
}
53+
54+
// Fall back to default client
55+
if (this.clientManager.defaultClient) {
56+
return this.clientManager.defaultClient;
57+
}
58+
59+
// Fall back to first available client
60+
const clients = this.clientManager.allClients();
61+
return clients.length > 0 ? clients[0] : null;
62+
}
63+
2964
async invoke(
3065
options: vscode.LanguageModelToolInvocationOptions<IParameters>,
3166
token: vscode.CancellationToken,
3267
): Promise<vscode.LanguageModelToolResult> {
33-
const { symbol } = options.input;
68+
const { symbol, file } = options.input;
69+
70+
const client = this.getClient(file);
71+
if (!client) {
72+
return new vscode.LanguageModelToolResult([
73+
new vscode.LanguageModelTextPart(
74+
"ElixirLS language server is not available. Please open an Elixir file or workspace.",
75+
),
76+
]);
77+
}
3478

3579
try {
3680
// Find the llmDefinition command from server capabilities
3781
const command =
38-
this.client.initializeResult?.capabilities.executeCommandProvider?.commands.find(
82+
client.initializeResult?.capabilities.executeCommandProvider?.commands.find(
3983
(c) => c.startsWith("llmDefinition:"),
4084
);
4185

@@ -52,7 +96,7 @@ export class DefinitionTool implements vscode.LanguageModelTool<IParameters> {
5296
arguments: [symbol],
5397
};
5498

55-
const result = await this.client.sendRequest<IDefinitionResult>(
99+
const result = await client.sendRequest<IDefinitionResult>(
56100
ExecuteCommandRequest.method,
57101
params,
58102
token,

src/docs-aggregator-tool.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import {
44
ExecuteCommandRequest,
55
type LanguageClient,
66
} from "vscode-languageclient/node";
7+
import type { LanguageClientManager } from "./languageClientManager";
78

89
interface IParameters {
910
modules: string[];
11+
file?: string;
1012
}
1113

1214
export class DocsAggregatorTool
1315
implements vscode.LanguageModelTool<IParameters>
1416
{
15-
constructor(private client: LanguageClient) {}
17+
constructor(private clientManager: LanguageClientManager) {}
1618

1719
async prepareInvocation(
1820
options: vscode.LanguageModelToolInvocationPrepareOptions<IParameters>,
@@ -23,16 +25,58 @@ export class DocsAggregatorTool
2325
};
2426
}
2527

28+
private getClient(file?: string): LanguageClient | null {
29+
if (file) {
30+
try {
31+
const uri = vscode.Uri.file(file);
32+
return this.clientManager.getClientByUri(uri);
33+
} catch (error) {
34+
console.warn(`ElixirLS: Failed to get client for file ${file}:`, error);
35+
}
36+
}
37+
38+
// Fall back to active editor
39+
const activeEditor = vscode.window.activeTextEditor;
40+
if (activeEditor) {
41+
try {
42+
return this.clientManager.getClientByUri(activeEditor.document.uri);
43+
} catch (error) {
44+
console.warn(
45+
"ElixirLS: Failed to get client for active editor:",
46+
error,
47+
);
48+
}
49+
}
50+
51+
// Fall back to default client
52+
if (this.clientManager.defaultClient) {
53+
return this.clientManager.defaultClient;
54+
}
55+
56+
// Fall back to first available client
57+
const clients = this.clientManager.allClients();
58+
return clients.length > 0 ? clients[0] : null;
59+
}
60+
2661
async invoke(
2762
options: vscode.LanguageModelToolInvocationOptions<IParameters>,
2863
token: vscode.CancellationToken,
2964
): Promise<vscode.LanguageModelToolResult> {
30-
const { modules } = options.input;
65+
const { modules, file } = options.input;
66+
67+
const client = this.getClient(file);
68+
if (!client) {
69+
return new vscode.LanguageModelToolResult([
70+
new vscode.LanguageModelTextPart(
71+
"ElixirLS language server is not available. Please open an Elixir file or workspace.",
72+
),
73+
]);
74+
}
3175

3276
try {
3377
// Find the llmDocsAggregator command from server capabilities
3478
const command =
35-
this.client.initializeResult?.capabilities.executeCommandProvider?.commands.find(
79+
client.initializeResult?.capabilities.executeCommandProvider?.commands.find(
3680
(c) => c.startsWith("llmDocsAggregator:"),
3781
);
3882

@@ -49,7 +93,7 @@ export class DocsAggregatorTool
4993
arguments: [modules],
5094
};
5195

52-
const result = await this.client.sendRequest<{
96+
const result = await client.sendRequest<{
5397
results?: Array<{
5498
// Module documentation fields
5599
module?: string;

src/environment-tool.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ExecuteCommandRequest,
55
type LanguageClient,
66
} from "vscode-languageclient/node";
7+
import type { LanguageClientManager } from "./languageClientManager";
78

89
interface IParameters {
910
location: string;
@@ -49,7 +50,7 @@ interface IEnvironmentResult {
4950
}
5051

5152
export class EnvironmentTool implements vscode.LanguageModelTool<IParameters> {
52-
constructor(private client: LanguageClient) {}
53+
constructor(private clientManager: LanguageClientManager) {}
5354

5455
async prepareInvocation(
5556
options: vscode.LanguageModelToolInvocationPrepareOptions<IParameters>,
@@ -60,16 +61,84 @@ export class EnvironmentTool implements vscode.LanguageModelTool<IParameters> {
6061
};
6162
}
6263

64+
private parseLocationFile(location: string): string | null {
65+
// Location formats: "file.ex:line:column" or "file:///absolute/path/file.ex:line:column"
66+
try {
67+
if (location.startsWith("file://")) {
68+
// Extract path from URI
69+
const uri = vscode.Uri.parse(
70+
`${location.split(":")[0]}:${location.split(":")[1]}`,
71+
);
72+
return uri.fsPath;
73+
}
74+
// Simple file path - extract everything before the first colon
75+
const colonIndex = location.indexOf(":");
76+
if (colonIndex > 0) {
77+
return location.substring(0, colonIndex);
78+
}
79+
return location;
80+
} catch (error) {
81+
console.warn(`ElixirLS: Failed to parse location ${location}:`, error);
82+
return null;
83+
}
84+
}
85+
86+
private getClient(location: string): LanguageClient | null {
87+
const filePath = this.parseLocationFile(location);
88+
if (filePath) {
89+
try {
90+
const uri = vscode.Uri.file(filePath);
91+
return this.clientManager.getClientByUri(uri);
92+
} catch (error) {
93+
console.warn(
94+
`ElixirLS: Failed to get client for location ${location}:`,
95+
error,
96+
);
97+
}
98+
}
99+
100+
// Fall back to active editor
101+
const activeEditor = vscode.window.activeTextEditor;
102+
if (activeEditor) {
103+
try {
104+
return this.clientManager.getClientByUri(activeEditor.document.uri);
105+
} catch (error) {
106+
console.warn(
107+
"ElixirLS: Failed to get client for active editor:",
108+
error,
109+
);
110+
}
111+
}
112+
113+
// Fall back to default client
114+
if (this.clientManager.defaultClient) {
115+
return this.clientManager.defaultClient;
116+
}
117+
118+
// Fall back to first available client
119+
const clients = this.clientManager.allClients();
120+
return clients.length > 0 ? clients[0] : null;
121+
}
122+
63123
async invoke(
64124
options: vscode.LanguageModelToolInvocationOptions<IParameters>,
65125
token: vscode.CancellationToken,
66126
): Promise<vscode.LanguageModelToolResult> {
67127
const { location } = options.input;
68128

129+
const client = this.getClient(location);
130+
if (!client) {
131+
return new vscode.LanguageModelToolResult([
132+
new vscode.LanguageModelTextPart(
133+
"ElixirLS language server is not available. Please open an Elixir file or workspace.",
134+
),
135+
]);
136+
}
137+
69138
try {
70139
// Find the llmEnvironment command from server capabilities
71140
const command =
72-
this.client.initializeResult?.capabilities.executeCommandProvider?.commands.find(
141+
client.initializeResult?.capabilities.executeCommandProvider?.commands.find(
73142
(c) => c.startsWith("llmEnvironment:"),
74143
);
75144

@@ -86,7 +155,7 @@ export class EnvironmentTool implements vscode.LanguageModelTool<IParameters> {
86155
arguments: [location],
87156
};
88157

89-
const result = await this.client.sendRequest<IEnvironmentResult>(
158+
const result = await client.sendRequest<IEnvironmentResult>(
90159
ExecuteCommandRequest.method,
91160
params,
92161
token,

0 commit comments

Comments
 (0)