Skip to content

Commit bfcbddd

Browse files
authored
feat: context provider api support for java (#1489)
* feat: context provider api support for java * feat: register registerCopilotContextProviders * feat: register registerCopilotContextProviders * feat: update * fix: update code according to the comments * fix: update the docu hash logic * fix: update code * fix: update the code logic according to comments * fix: update code according to comments * feat: remove cache design * feat: add telemetry data report * feat: remove get java version * fix: update
1 parent cf629fb commit bfcbddd

File tree

9 files changed

+568
-2
lines changed

9 files changed

+568
-2
lines changed

package-lock.json

Lines changed: 110 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@
380380
"VisualStudioExptTeam.vscodeintellicode"
381381
],
382382
"dependencies": {
383+
"@github/copilot-language-server": "^1.316.0",
383384
"@iconify-icons/codicon": "1.2.8",
384385
"@iconify/react": "^1.1.4",
385386
"@reduxjs/toolkit": "^1.8.6",

src/commands/handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,4 @@ export async function toggleAwtDevelopmentHandler(context: vscode.ExtensionConte
126126

127127
fetchInitProps(context);
128128
vscode.window.showInformationMessage(`Java AWT development is ${enable ? "enabled" : "disabled"}.`);
129-
}
129+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
import { commands, Uri, CancellationToken } from "vscode";
5+
import { logger } from "../utils";
6+
import { validateExtensionInstalled } from "../../recommendation";
7+
8+
export interface INodeImportClass {
9+
uri: string;
10+
className: string; // Changed from 'class' to 'className' to match Java code
11+
}
12+
/**
13+
* Helper class for Copilot integration to analyze Java project dependencies
14+
*/
15+
export namespace CopilotHelper {
16+
/**
17+
* Resolves all local project types imported by the given file
18+
* @param fileUri The URI of the Java file to analyze
19+
* @param cancellationToken Optional cancellation token to abort the operation
20+
* @returns Array of strings in format "type:fully.qualified.name" where type is class|interface|enum|annotation
21+
*/
22+
export async function resolveLocalImports(fileUri: Uri, cancellationToken?: CancellationToken): Promise<INodeImportClass[]> {
23+
if (cancellationToken?.isCancellationRequested) {
24+
return [];
25+
}
26+
// Ensure the Java Dependency extension is installed and meets the minimum version requirement.
27+
if (!await validateExtensionInstalled("vscjava.vscode-java-dependency", "0.26.0")) {
28+
return [];
29+
}
30+
31+
if (cancellationToken?.isCancellationRequested) {
32+
return [];
33+
}
34+
35+
try {
36+
// Create a promise that can be cancelled
37+
const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getImportClassContent", fileUri.toString()) as Promise<INodeImportClass[]>;
38+
39+
if (cancellationToken) {
40+
const result = await Promise.race([
41+
commandPromise,
42+
new Promise<INodeImportClass[]>((_, reject) => {
43+
cancellationToken.onCancellationRequested(() => {
44+
reject(new Error('Operation cancelled'));
45+
});
46+
})
47+
]);
48+
return result || [];
49+
} else {
50+
const result = await commandPromise;
51+
return result || [];
52+
}
53+
} catch (error: any) {
54+
if (error.message === 'Operation cancelled') {
55+
logger.info('Resolve local imports cancelled');
56+
return [];
57+
}
58+
logger.error("Error resolving copilot request:", error);
59+
return [];
60+
}
61+
}
62+
}

src/copilot/contextProvider.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import {
6+
ResolveRequest,
7+
SupportedContextItem,
8+
type ContextProvider,
9+
} from '@github/copilot-language-server';
10+
import * as vscode from 'vscode';
11+
import { CopilotHelper } from './context/copilotHelper';
12+
import { sendInfo } from "vscode-extension-telemetry-wrapper";
13+
import {
14+
logger,
15+
JavaContextProviderUtils,
16+
CancellationError,
17+
InternalCancellationError,
18+
CopilotCancellationError,
19+
ContextResolverFunction,
20+
CopilotApi
21+
} from './utils';
22+
import { getExtensionName } from '../utils/extension';
23+
24+
export async function registerCopilotContextProviders(
25+
context: vscode.ExtensionContext
26+
) {
27+
try {
28+
const apis = await JavaContextProviderUtils.getCopilotApis();
29+
if (!apis.clientApi || !apis.chatApi) {
30+
logger.info('Failed to find compatible version of GitHub Copilot extension installed. Skip registration of Copilot context provider.');
31+
return;
32+
}
33+
34+
// Register the Java completion context provider
35+
const provider: ContextProvider<SupportedContextItem> = {
36+
id: getExtensionName(), // use extension id as provider id for now
37+
selector: [{ language: "java" }],
38+
resolver: { resolve: createJavaContextResolver() }
39+
};
40+
41+
const installCount = await JavaContextProviderUtils.installContextProviderOnApis(apis, provider, context, installContextProvider);
42+
43+
if (installCount === 0) {
44+
logger.info('Incompatible GitHub Copilot extension installed. Skip registration of Java context providers.');
45+
return;
46+
}
47+
48+
logger.info('Registration of Java context provider for GitHub Copilot extension succeeded.');
49+
sendInfo("", {
50+
"action": "registerCopilotContextProvider",
51+
"extension": getExtensionName(),
52+
"status": "succeeded",
53+
"installCount": installCount
54+
});
55+
}
56+
catch (error) {
57+
logger.error('Error occurred while registering Java context provider for GitHub Copilot extension:', error);
58+
}
59+
}
60+
61+
/**
62+
* Create the Java context resolver function
63+
*/
64+
function createJavaContextResolver(): ContextResolverFunction {
65+
return async (request: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise<SupportedContextItem[]> => {
66+
const resolveStartTime = performance.now();
67+
let logMessage = `Java Context Provider: resolve(${request.documentContext.uri}:${request.documentContext.offset}):`;
68+
69+
try {
70+
// Check for immediate cancellation
71+
JavaContextProviderUtils.checkCancellation(copilotCancel);
72+
73+
return await resolveJavaContext(request, copilotCancel);
74+
} catch (error: any) {
75+
try {
76+
JavaContextProviderUtils.handleError(error, 'Java context provider resolve', resolveStartTime, logMessage);
77+
} catch (handledError) {
78+
// Return empty array if error handling throws
79+
return [];
80+
}
81+
// This should never be reached due to handleError throwing, but TypeScript requires it
82+
return [];
83+
} finally {
84+
const duration = Math.round(performance.now() - resolveStartTime);
85+
if (!logMessage.includes('cancellation')) {
86+
logMessage += `(completed in ${duration}ms)`;
87+
logger.info(logMessage);
88+
}
89+
}
90+
};
91+
}
92+
93+
/**
94+
* Send telemetry data for Java context resolution
95+
*/
96+
function sendContextTelemetry(request: ResolveRequest, start: number, itemCount: number, status: string, error?: string) {
97+
const duration = Math.round(performance.now() - start);
98+
const telemetryData: any = {
99+
"action": "resolveJavaContext",
100+
"completionId": request.completionId,
101+
"duration": duration,
102+
"itemCount": itemCount,
103+
"status": status
104+
};
105+
106+
if (error) {
107+
telemetryData.error = error;
108+
}
109+
110+
sendInfo("", telemetryData);
111+
}
112+
113+
async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise<SupportedContextItem[]> {
114+
const items: SupportedContextItem[] = [];
115+
const start = performance.now();
116+
const documentUri = request.documentContext.uri;
117+
const caretOffset = request.documentContext.offset;
118+
119+
try {
120+
// Check for cancellation before starting
121+
JavaContextProviderUtils.checkCancellation(copilotCancel);
122+
123+
// Get current document and position information
124+
const activeEditor = vscode.window.activeTextEditor;
125+
if (!activeEditor || activeEditor.document.languageId !== 'java') {
126+
return items;
127+
}
128+
129+
const document = activeEditor.document;
130+
131+
// Resolve imports directly without caching
132+
const importClass = await CopilotHelper.resolveLocalImports(document.uri, copilotCancel);
133+
logger.trace('Resolved imports count:', importClass?.length || 0);
134+
135+
// Check for cancellation after resolution
136+
JavaContextProviderUtils.checkCancellation(copilotCancel);
137+
138+
// Check for cancellation before processing results
139+
JavaContextProviderUtils.checkCancellation(copilotCancel);
140+
141+
if (importClass) {
142+
// Process imports in batches to reduce cancellation check overhead
143+
const contextItems = JavaContextProviderUtils.createContextItemsFromImports(importClass);
144+
145+
// Check cancellation once after creating all items
146+
JavaContextProviderUtils.checkCancellation(copilotCancel);
147+
148+
items.push(...contextItems);
149+
}
150+
} catch (error: any) {
151+
if (error instanceof CopilotCancellationError) {
152+
sendContextTelemetry(request, start, items.length, "cancelled_by_copilot");
153+
throw error;
154+
}
155+
if (error instanceof vscode.CancellationError || error.message === CancellationError.Canceled) {
156+
sendContextTelemetry(request, start, items.length, "cancelled_internally");
157+
throw new InternalCancellationError();
158+
}
159+
160+
// Send telemetry for general errors (but continue with partial results)
161+
sendContextTelemetry(request, start, items.length, "error_partial_results", error.message || "unknown_error");
162+
163+
logger.error(`Error resolving Java context for ${documentUri}:${caretOffset}:`, error);
164+
165+
// Return partial results and log completion for error case
166+
JavaContextProviderUtils.logCompletion('Java context resolution', documentUri, caretOffset, start, items.length);
167+
return items;
168+
}
169+
170+
// Send telemetry data once at the end for success case
171+
sendContextTelemetry(request, start, items.length, "succeeded");
172+
173+
JavaContextProviderUtils.logCompletion('Java context resolution', documentUri, caretOffset, start, items.length);
174+
return items;
175+
}
176+
177+
export async function installContextProvider(
178+
copilotAPI: CopilotApi,
179+
contextProvider: ContextProvider<SupportedContextItem>
180+
): Promise<vscode.Disposable | undefined> {
181+
const hasGetContextProviderAPI = typeof copilotAPI.getContextProviderAPI === 'function';
182+
if (hasGetContextProviderAPI) {
183+
const contextAPI = await copilotAPI.getContextProviderAPI('v1');
184+
if (contextAPI) {
185+
return contextAPI.registerContextProvider(contextProvider);
186+
}
187+
}
188+
return undefined;
189+
}

0 commit comments

Comments
 (0)