Skip to content

Commit 2e90376

Browse files
authored
feat: Enhance the context returned by the context provider (#1518)
* feat: support new command from deps * chore: update * chore: update * feat: support deps command to enhance the context provider * fix: update
1 parent bf22873 commit 2e90376

File tree

3 files changed

+145
-12
lines changed

3 files changed

+145
-12
lines changed

src/copilot/context/copilotHelper.ts

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export interface INodeImportClass {
99
uri: string;
1010
className: string; // Changed from 'class' to 'className' to match Java code
1111
}
12+
13+
export interface IProjectDependency {
14+
[key: string]: string;
15+
}
1216
/**
1317
* Helper class for Copilot integration to analyze Java project dependencies
1418
*/
@@ -23,8 +27,9 @@ export namespace CopilotHelper {
2327
if (cancellationToken?.isCancellationRequested) {
2428
return [];
2529
}
30+
2631
// Ensure the Java Dependency extension is installed and meets the minimum version requirement.
27-
if (!await validateExtensionInstalled("vscjava.vscode-java-dependency", "0.26.0")) {
32+
if (!await validateExtensionInstalled("vscjava.vscode-java-dependency", "0.26.2")) {
2833
return [];
2934
}
3035

@@ -43,11 +48,23 @@ export namespace CopilotHelper {
4348
cancellationToken.onCancellationRequested(() => {
4449
reject(new Error('Operation cancelled'));
4550
});
51+
}),
52+
new Promise<INodeImportClass[]>((_, reject) => {
53+
setTimeout(() => {
54+
reject(new Error('Operation timed out'));
55+
}, 80); // 80ms timeout
4656
})
4757
]);
4858
return result || [];
4959
} else {
50-
const result = await commandPromise;
60+
const result = await Promise.race([
61+
commandPromise,
62+
new Promise<INodeImportClass[]>((_, reject) => {
63+
setTimeout(() => {
64+
reject(new Error('Operation timed out'));
65+
}, 80); // 80ms timeout
66+
})
67+
]);
5168
return result || [];
5269
}
5370
} catch (error: any) {
@@ -59,4 +76,64 @@ export namespace CopilotHelper {
5976
return [];
6077
}
6178
}
79+
80+
/**
81+
* Resolves project dependencies for the given project URI
82+
* @param projectUri The URI of the Java project to analyze
83+
* @param cancellationToken Optional cancellation token to abort the operation
84+
* @returns Object containing project dependencies as key-value pairs
85+
*/
86+
export async function resolveProjectDependencies(projectUri: Uri, cancellationToken?: CancellationToken): Promise<IProjectDependency> {
87+
if (cancellationToken?.isCancellationRequested) {
88+
return {};
89+
}
90+
91+
// Ensure the Java Dependency extension is installed and meets the minimum version requirement.
92+
if (!await validateExtensionInstalled("vscjava.vscode-java-dependency", "0.26.2")) {
93+
return {};
94+
}
95+
96+
if (cancellationToken?.isCancellationRequested) {
97+
return {};
98+
}
99+
100+
try {
101+
// Create a promise that can be cancelled
102+
const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getDependencies", projectUri.toString()) as Promise<IProjectDependency>;
103+
//set timeout
104+
if (cancellationToken) {
105+
const result = await Promise.race([
106+
commandPromise,
107+
new Promise<IProjectDependency>((_, reject) => {
108+
cancellationToken.onCancellationRequested(() => {
109+
reject(new Error('Operation cancelled'));
110+
});
111+
}),
112+
new Promise<IProjectDependency>((_, reject) => {
113+
setTimeout(() => {
114+
reject(new Error('Operation timed out'));
115+
}, 40); // 40ms timeout
116+
})
117+
]);
118+
return result || {};
119+
} else {
120+
const result = await Promise.race([
121+
commandPromise,
122+
new Promise<IProjectDependency>((_, reject) => {
123+
setTimeout(() => {
124+
reject(new Error('Operation timed out'));
125+
}, 40); // 40ms timeout
126+
})
127+
]);
128+
return result || {};
129+
}
130+
} catch (error: any) {
131+
if (error.message === 'Operation cancelled') {
132+
logger.info('Resolve project dependencies cancelled');
133+
return {};
134+
}
135+
logger.error("Error resolving project dependencies:", error);
136+
return {};
137+
}
138+
}
62139
}

src/copilot/contextProvider.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as vscode from 'vscode';
1111
import { CopilotHelper } from './context/copilotHelper';
1212
import { sendInfo } from "vscode-extension-telemetry-wrapper";
1313
import {
14-
logger,
14+
logger,
1515
JavaContextProviderUtils,
1616
CancellationError,
1717
InternalCancellationError,
@@ -93,13 +93,15 @@ function createJavaContextResolver(): ContextResolverFunction {
9393
/**
9494
* Send telemetry data for Java context resolution
9595
*/
96-
function sendContextTelemetry(request: ResolveRequest, start: number, itemCount: number, status: string, error?: string) {
96+
function sendContextTelemetry(request: ResolveRequest, start: number, items: SupportedContextItem[], status: string, error?: string) {
9797
const duration = Math.round(performance.now() - start);
98+
const tokenCount = JavaContextProviderUtils.calculateTokenCount(items);
9899
const telemetryData: any = {
99100
"action": "resolveJavaContext",
100101
"completionId": request.completionId,
101102
"duration": duration,
102-
"itemCount": itemCount,
103+
"itemCount": items.length,
104+
"tokenCount": tokenCount,
103105
"status": status
104106
};
105107

@@ -128,16 +130,37 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode
128130

129131
const document = activeEditor.document;
130132

133+
// Resolve project dependencies first
134+
const projectDependencies = await CopilotHelper.resolveProjectDependencies(document.uri, copilotCancel);
135+
logger.info('Resolved project dependencies count:', Object.keys(projectDependencies).length);
136+
137+
// Check for cancellation after dependency resolution
138+
JavaContextProviderUtils.checkCancellation(copilotCancel);
139+
140+
// Convert project dependencies to Trait items
141+
if (projectDependencies && Object.keys(projectDependencies).length > 0) {
142+
for (const [key, value] of Object.entries(projectDependencies)) {
143+
items.push({
144+
name: key,
145+
value: value,
146+
importance: 50
147+
});
148+
}
149+
}
150+
151+
// Check for cancellation before resolving imports
152+
JavaContextProviderUtils.checkCancellation(copilotCancel);
153+
131154
// Resolve imports directly without caching
132155
const importClass = await CopilotHelper.resolveLocalImports(document.uri, copilotCancel);
133-
logger.trace('Resolved imports count:', importClass?.length || 0);
156+
logger.info('Resolved imports count:', importClass?.length || 0);
134157

135158
// Check for cancellation after resolution
136159
JavaContextProviderUtils.checkCancellation(copilotCancel);
137160

138161
// Check for cancellation before processing results
139162
JavaContextProviderUtils.checkCancellation(copilotCancel);
140-
163+
141164
if (importClass) {
142165
// Process imports in batches to reduce cancellation check overhead
143166
const contextItems = JavaContextProviderUtils.createContextItemsFromImports(importClass);
@@ -149,26 +172,26 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode
149172
}
150173
} catch (error: any) {
151174
if (error instanceof CopilotCancellationError) {
152-
sendContextTelemetry(request, start, items.length, "cancelled_by_copilot");
175+
sendContextTelemetry(request, start, items, "cancelled_by_copilot");
153176
throw error;
154177
}
155178
if (error instanceof vscode.CancellationError || error.message === CancellationError.Canceled) {
156-
sendContextTelemetry(request, start, items.length, "cancelled_internally");
179+
sendContextTelemetry(request, start, items, "cancelled_internally");
157180
throw new InternalCancellationError();
158181
}
159182

160183
// Send telemetry for general errors (but continue with partial results)
161-
sendContextTelemetry(request, start, items.length, "error_partial_results", error.message || "unknown_error");
184+
sendContextTelemetry(request, start, items, "error_partial_results", error.message || "unknown_error");
162185

163186
logger.error(`Error resolving Java context for ${documentUri}:${caretOffset}:`, error);
164187

165188
// Return partial results and log completion for error case
166189
JavaContextProviderUtils.logCompletion('Java context resolution', documentUri, caretOffset, start, items.length);
167190
return items;
168191
}
169-
192+
170193
// Send telemetry data once at the end for success case
171-
sendContextTelemetry(request, start, items.length, "succeeded");
194+
sendContextTelemetry(request, start, items, "succeeded");
172195

173196
JavaContextProviderUtils.logCompletion('Java context resolution', documentUri, caretOffset, start, items.length);
174197
return items;

src/copilot/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,39 @@ export class JavaContextProviderUtils {
301301

302302
return installCount;
303303
}
304+
305+
/**
306+
* Calculate approximate token count for context items
307+
* Using a simple heuristic: ~4 characters per token
308+
* Optimized for performance by using reduce and direct property access
309+
*/
310+
static calculateTokenCount(items: SupportedContextItem[]): number {
311+
// Fast path: if no items, return 0
312+
if (items.length === 0) {
313+
return 0;
314+
}
315+
316+
// Use reduce for better performance
317+
const totalChars = items.reduce((sum, item) => {
318+
let itemChars = 0;
319+
// Direct property access is faster than 'in' operator
320+
const value = (item as any).value;
321+
const name = (item as any).name;
322+
323+
if (value && typeof value === 'string') {
324+
itemChars += value.length;
325+
}
326+
if (name && typeof name === 'string') {
327+
itemChars += name.length;
328+
}
329+
330+
return sum + itemChars;
331+
}, 0);
332+
333+
// Approximate: 1 token ≈ 4 characters
334+
// Use bitwise shift for faster division by 4
335+
return (totalChars >> 2) + (totalChars & 3 ? 1 : 0);
336+
}
304337
}
305338

306339
/**

0 commit comments

Comments
 (0)