Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@
"VisualStudioExptTeam.vscodeintellicode"
],
"dependencies": {
"@github/copilot-language-server": "^1.316.0",
"@iconify-icons/codicon": "1.2.8",
"@iconify/react": "^1.1.4",
"@reduxjs/toolkit": "^1.8.6",
Expand Down
2 changes: 1 addition & 1 deletion src/commands/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,4 @@ export async function toggleAwtDevelopmentHandler(context: vscode.ExtensionConte

fetchInitProps(context);
vscode.window.showInformationMessage(`Java AWT development is ${enable ? "enabled" : "disabled"}.`);
}
}
62 changes: 62 additions & 0 deletions src/copilot/context/copilotHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

import { commands, Uri, CancellationToken } from "vscode";
import { logger } from "../utils";
import { validateExtensionInstalled } from "../../recommendation";

export interface INodeImportClass {
uri: string;
className: string; // Changed from 'class' to 'className' to match Java code
}
/**
* Helper class for Copilot integration to analyze Java project dependencies
*/
export namespace CopilotHelper {
/**
* Resolves all local project types imported by the given file
* @param fileUri The URI of the Java file to analyze
* @param cancellationToken Optional cancellation token to abort the operation
* @returns Array of strings in format "type:fully.qualified.name" where type is class|interface|enum|annotation
*/
export async function resolveLocalImports(fileUri: Uri, cancellationToken?: CancellationToken): Promise<INodeImportClass[]> {
if (cancellationToken?.isCancellationRequested) {
return [];
}
// Ensure the Java Dependency extension is installed and meets the minimum version requirement.
if (!await validateExtensionInstalled("vscjava.vscode-java-dependency", "0.26.0")) {
return [];
}

if (cancellationToken?.isCancellationRequested) {
return [];
}

try {
// Create a promise that can be cancelled
const commandPromise = commands.executeCommand("java.execute.workspaceCommand", "java.project.getImportClassContent", fileUri.toString()) as Promise<INodeImportClass[]>;

if (cancellationToken) {
const result = await Promise.race([
commandPromise,
new Promise<INodeImportClass[]>((_, reject) => {
cancellationToken.onCancellationRequested(() => {
reject(new Error('Operation cancelled'));
});
})
]);
return result || [];
} else {
const result = await commandPromise;
return result || [];
}
} catch (error: any) {
if (error.message === 'Operation cancelled') {
logger.info('Resolve local imports cancelled');
return [];
}
logger.error("Error resolving copilot request:", error);
return [];
}
}
}
189 changes: 189 additions & 0 deletions src/copilot/contextProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
ResolveRequest,
SupportedContextItem,
type ContextProvider,
} from '@github/copilot-language-server';
import * as vscode from 'vscode';
import { CopilotHelper } from './context/copilotHelper';
import { sendInfo } from "vscode-extension-telemetry-wrapper";
import {
logger,
JavaContextProviderUtils,
CancellationError,
InternalCancellationError,
CopilotCancellationError,
ContextResolverFunction,
CopilotApi
} from './utils';
import { getExtensionName } from '../utils/extension';

export async function registerCopilotContextProviders(
context: vscode.ExtensionContext
) {
try {
const apis = await JavaContextProviderUtils.getCopilotApis();
if (!apis.clientApi || !apis.chatApi) {
logger.info('Failed to find compatible version of GitHub Copilot extension installed. Skip registration of Copilot context provider.');
return;
}

// Register the Java completion context provider
const provider: ContextProvider<SupportedContextItem> = {
id: getExtensionName(), // use extension id as provider id for now
selector: [{ language: "java" }],
resolver: { resolve: createJavaContextResolver() }
};

const installCount = await JavaContextProviderUtils.installContextProviderOnApis(apis, provider, context, installContextProvider);

if (installCount === 0) {
logger.info('Incompatible GitHub Copilot extension installed. Skip registration of Java context providers.');
return;
}

logger.info('Registration of Java context provider for GitHub Copilot extension succeeded.');
sendInfo("", {
"action": "registerCopilotContextProvider",
"extension": getExtensionName(),
"status": "succeeded",
"installCount": installCount
});
}
catch (error) {
logger.error('Error occurred while registering Java context provider for GitHub Copilot extension:', error);
}
}

/**
* Create the Java context resolver function
*/
function createJavaContextResolver(): ContextResolverFunction {
return async (request: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise<SupportedContextItem[]> => {
const resolveStartTime = performance.now();
let logMessage = `Java Context Provider: resolve(${request.documentContext.uri}:${request.documentContext.offset}):`;

try {
// Check for immediate cancellation
JavaContextProviderUtils.checkCancellation(copilotCancel);

return await resolveJavaContext(request, copilotCancel);
} catch (error: any) {
try {
JavaContextProviderUtils.handleError(error, 'Java context provider resolve', resolveStartTime, logMessage);
} catch (handledError) {
// Return empty array if error handling throws
return [];
}
// This should never be reached due to handleError throwing, but TypeScript requires it
return [];
} finally {
const duration = Math.round(performance.now() - resolveStartTime);
if (!logMessage.includes('cancellation')) {
logMessage += `(completed in ${duration}ms)`;
logger.info(logMessage);
}
}
};
}

/**
* Send telemetry data for Java context resolution
*/
function sendContextTelemetry(request: ResolveRequest, start: number, itemCount: number, status: string, error?: string) {
const duration = Math.round(performance.now() - start);
const telemetryData: any = {
"action": "resolveJavaContext",
"completionId": request.completionId,
"duration": duration,
"itemCount": itemCount,
"status": status
};

if (error) {
telemetryData.error = error;
}

sendInfo("", telemetryData);
}

async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode.CancellationToken): Promise<SupportedContextItem[]> {
const items: SupportedContextItem[] = [];
const start = performance.now();
const documentUri = request.documentContext.uri;
const caretOffset = request.documentContext.offset;

try {
// Check for cancellation before starting
JavaContextProviderUtils.checkCancellation(copilotCancel);

// Get current document and position information
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor || activeEditor.document.languageId !== 'java') {
return items;
}

const document = activeEditor.document;

// Resolve imports directly without caching
const importClass = await CopilotHelper.resolveLocalImports(document.uri, copilotCancel);
logger.trace('Resolved imports count:', importClass?.length || 0);

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

// Check for cancellation before processing results
JavaContextProviderUtils.checkCancellation(copilotCancel);

if (importClass) {
// Process imports in batches to reduce cancellation check overhead
const contextItems = JavaContextProviderUtils.createContextItemsFromImports(importClass);

// Check cancellation once after creating all items
JavaContextProviderUtils.checkCancellation(copilotCancel);

items.push(...contextItems);
}
} catch (error: any) {
if (error instanceof CopilotCancellationError) {
sendContextTelemetry(request, start, items.length, "cancelled_by_copilot");
throw error;
}
if (error instanceof vscode.CancellationError || error.message === CancellationError.Canceled) {
sendContextTelemetry(request, start, items.length, "cancelled_internally");
throw new InternalCancellationError();
}

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

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

// Return partial results and log completion for error case
JavaContextProviderUtils.logCompletion('Java context resolution', documentUri, caretOffset, start, items.length);
return items;
}

// Send telemetry data once at the end for success case
sendContextTelemetry(request, start, items.length, "succeeded");

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

export async function installContextProvider(
copilotAPI: CopilotApi,
contextProvider: ContextProvider<SupportedContextItem>
): Promise<vscode.Disposable | undefined> {
const hasGetContextProviderAPI = typeof copilotAPI.getContextProviderAPI === 'function';
if (hasGetContextProviderAPI) {
const contextAPI = await copilotAPI.getContextProviderAPI('v1');
if (contextAPI) {
return contextAPI.registerContextProvider(contextProvider);
}
}
return undefined;
}
Loading