@@ -9,10 +9,11 @@ import {
99 type ContextProvider ,
1010} from '@github/copilot-language-server' ;
1111import * as vscode from 'vscode' ;
12- import { CopilotHelper } from '../copilotHelper' ;
12+ import { CopilotHelper , INodeImportClass } from '../copilotHelper' ;
1313import { TreatmentVariables } from '../ext/treatmentVariables' ;
1414import { getExpService } from '../ext/ExperimentationService' ;
1515import { sendInfo } from "vscode-extension-telemetry-wrapper" ;
16+ import * as crypto from 'crypto' ;
1617
1718export enum NodeKind {
1819 Workspace = 1 ,
@@ -27,6 +28,71 @@ export enum NodeKind {
2728 File = 10 ,
2829}
2930
31+ // Global cache for storing resolveLocalImports results
32+ interface CacheEntry {
33+ value : INodeImportClass [ ] ;
34+ timestamp : number ;
35+ }
36+
37+ const globalImportsCache = new Map < string , CacheEntry > ( ) ;
38+ const CACHE_EXPIRY_TIME = 5 * 60 * 1000 ; // 5 minutes
39+
40+ /**
41+ * Generate a hash for the document URI to use as cache key
42+ * @param uri Document URI
43+ * @returns Hashed URI string
44+ */
45+ function generateCacheKey ( uri : vscode . Uri ) : string {
46+ return crypto . createHash ( 'md5' ) . update ( uri . toString ( ) ) . digest ( 'hex' ) ;
47+ }
48+
49+ /**
50+ * Get cached imports for a document URI
51+ * @param uri Document URI
52+ * @returns Cached imports or null if not found/expired
53+ */
54+ function getCachedImports ( uri : vscode . Uri ) : INodeImportClass [ ] | null {
55+ const key = generateCacheKey ( uri ) ;
56+ const cached = globalImportsCache . get ( key ) ;
57+
58+ if ( ! cached ) {
59+ return null ;
60+ }
61+
62+ // Check if cache is expired
63+ if ( Date . now ( ) - cached . timestamp > CACHE_EXPIRY_TIME ) {
64+ globalImportsCache . delete ( key ) ;
65+ return null ;
66+ }
67+
68+ return cached . value ;
69+ }
70+
71+ /**
72+ * Set cached imports for a document URI
73+ * @param uri Document URI
74+ * @param imports Import class array to cache
75+ */
76+ function setCachedImports ( uri : vscode . Uri , imports : INodeImportClass [ ] ) : void {
77+ const key = generateCacheKey ( uri ) ;
78+ globalImportsCache . set ( key , {
79+ value : imports ,
80+ timestamp : Date . now ( )
81+ } ) ;
82+ }
83+
84+ /**
85+ * Clear expired cache entries
86+ */
87+ function clearExpiredCache ( ) : void {
88+ const now = Date . now ( ) ;
89+ for ( const [ key , entry ] of globalImportsCache . entries ( ) ) {
90+ if ( now - entry . timestamp > CACHE_EXPIRY_TIME ) {
91+ globalImportsCache . delete ( key ) ;
92+ }
93+ }
94+ }
95+
3096export async function registerCopilotContextProviders (
3197 context : vscode . ExtensionContext
3298) {
@@ -40,37 +106,90 @@ export async function registerCopilotContextProviders(
40106 sendInfo ( "" , {
41107 "contextProviderEnabled" : "true" ,
42108 } ) ;
109+
110+ // Start periodic cache cleanup
111+ const cacheCleanupInterval = setInterval ( ( ) => {
112+ clearExpiredCache ( ) ;
113+ } , CACHE_EXPIRY_TIME ) ; // Clean up every 5 minutes
114+
115+ // Monitor file changes to invalidate cache
116+ const fileWatcher = vscode . workspace . createFileSystemWatcher ( '**/*.java' ) ;
117+
118+ const invalidateCache = ( uri : vscode . Uri ) => {
119+ const key = generateCacheKey ( uri ) ;
120+ if ( globalImportsCache . has ( key ) ) {
121+ globalImportsCache . delete ( key ) ;
122+ console . log ( '======== Cache invalidated for:' , uri . toString ( ) ) ;
123+ }
124+ } ;
125+
126+ fileWatcher . onDidChange ( invalidateCache ) ;
127+ fileWatcher . onDidDelete ( invalidateCache ) ;
128+
129+ // Dispose the interval and file watcher when extension is deactivated
130+ context . subscriptions . push (
131+ new vscode . Disposable ( ( ) => {
132+ clearInterval ( cacheCleanupInterval ) ;
133+ globalImportsCache . clear ( ) ; // Clear all cache on disposal
134+ } ) ,
135+ fileWatcher
136+ ) ;
137+
43138 try {
44139 const copilotClientApi = await getCopilotClientApi ( ) ;
45140 const copilotChatApi = await getCopilotChatApi ( ) ;
46- if ( ! copilotClientApi && ! copilotChatApi ) {
47- console . log ( 'Failed to find compatible version of GitHub Copilot extension installed. Skip registration of Copilot context provider.' ) ;
141+ if ( ! copilotClientApi || ! copilotChatApi ) {
142+ console . error ( 'Failed to find compatible version of GitHub Copilot extension installed. Skip registration of Copilot context provider.' ) ;
48143 return ;
49144 }
50145 // Register the Java completion context provider
51- const javaCompletionProvider = new JavaCopilotCompletionContextProvider ( ) ;
52- let completionProviderInstallCount = 0 ;
146+ const provider : ContextProvider < SupportedContextItem > = {
147+ id : 'vscjava.vscode-java-pack' , // use extension id as provider id for now
148+ selector : [ { language : "java" } ] ,
149+ resolver : {
150+ resolve : async ( request , token ) => {
151+ // Check if we have a cached result for the current active editor
152+ const activeEditor = vscode . window . activeTextEditor ;
153+ if ( activeEditor && activeEditor . document . languageId === 'java' ) {
154+ const cachedImports = getCachedImports ( activeEditor . document . uri ) ;
155+ if ( cachedImports ) {
156+ console . log ( '======== Using cached imports, cache size:' , cachedImports . length ) ;
157+ // Return cached result as context items
158+ return cachedImports . map ( cls => ( {
159+ uri : cls . uri ,
160+ value : cls . className ,
161+ importance : 70 ,
162+ origin : 'request' as const
163+ } ) ) ;
164+ }
165+ }
166+
167+ return await resolveJavaContext ( request , token ) ;
168+ }
169+ }
170+ } ;
53171
172+ let installCount = 0 ;
54173 if ( copilotClientApi ) {
55- const disposable = await installContextProvider ( copilotClientApi , javaCompletionProvider ) ;
174+ const disposable = await installContextProvider ( copilotClientApi , provider ) ;
56175 if ( disposable ) {
57176 context . subscriptions . push ( disposable ) ;
58- completionProviderInstallCount ++ ;
177+ installCount ++ ;
59178 }
60179 }
61180 if ( copilotChatApi ) {
62- const disposable = await installContextProvider ( copilotChatApi , javaCompletionProvider ) ;
181+ const disposable = await installContextProvider ( copilotChatApi , provider ) ;
63182 if ( disposable ) {
64183 context . subscriptions . push ( disposable ) ;
65- completionProviderInstallCount ++ ;
184+ installCount ++ ;
66185 }
67186 }
68187
69- if ( completionProviderInstallCount > 0 ) {
70- console . log ( 'Registration of Java completion context provider for GitHub Copilot extension succeeded.' ) ;
71- } else {
72- console . log ( 'Failed to register Java completion context provider for GitHub Copilot extension.' ) ;
188+ if ( installCount === 0 ) {
189+ console . log ( 'Incompatible GitHub Copilot extension installed. Skip registration of Java context providers.' ) ;
190+ return ;
73191 }
192+ console . log ( 'Registration of Java context provider for GitHub Copilot extension succeeded.' ) ;
74193 }
75194 catch ( error ) {
76195 console . log ( 'Error occurred while registering Java context provider for GitHub Copilot extension:' , error ) ;
@@ -89,11 +208,6 @@ async function resolveJavaContext(_request: ResolveRequest, _token: vscode.Cance
89208
90209 const document = activeEditor . document ;
91210
92- // const position = activeEditor.selection.active;
93- // const currentRange = activeEditor.selection.isEmpty
94- // ? new vscode.Range(position, position)
95- // : activeEditor.selection;
96-
97211 // 1. Project basic information (High importance)
98212 const projectContext = await collectProjectContext ( document ) ;
99213 const packageName = await getPackageName ( document ) ;
@@ -122,7 +236,17 @@ async function resolveJavaContext(_request: ResolveRequest, _token: vscode.Cance
122236 origin : 'request'
123237 } ) ;
124238
125- const importClass = await CopilotHelper . resolveLocalImports ( document . uri ) ;
239+ // Try to get cached imports first
240+ let importClass = getCachedImports ( document . uri ) ;
241+ if ( ! importClass ) {
242+ // If not cached, resolve and cache the result
243+ importClass = await CopilotHelper . resolveLocalImports ( document . uri ) ;
244+ setCachedImports ( document . uri , importClass ) ;
245+ console . log ( '======== Cached new imports, cache size:' , importClass . length ) ;
246+ } else {
247+ console . log ( '======== Using cached imports in resolveJavaContext, cache size:' , importClass . length ) ;
248+ }
249+
126250 for ( const cls of importClass ) {
127251 items . push ( {
128252 uri : cls . uri ,
@@ -145,16 +269,16 @@ async function resolveJavaContext(_request: ResolveRequest, _token: vscode.Cance
145269 origin : 'request'
146270 } ) ;
147271 }
148- console . log ( 'Total context resolution time:' , performance . now ( ) - start ) ;
149- console . log ( '===== Size of context items:' , items . length ) ;
272+ console . log ( 'Total context resolution time:' , performance . now ( ) - start , 'ms' , ' ,size:' , items . length ) ;
273+ console . log ( 'Context items:' , items ) ;
150274 return items ;
151275}
152276
153277async function collectProjectContext ( document : vscode . TextDocument ) : Promise < { javaVersion : string } > {
154278 try {
155- return await vscode . commands . executeCommand ( "java.project.getSettings" , document . uri , [ "java.home " ] ) ;
279+ return await vscode . commands . executeCommand ( "java.project.getSettings" , document . uri , [ "java.compliance" , "java.source" , "java.target "] ) ;
156280 } catch ( error ) {
157- console . log ( 'Failed to get Java version:' , error ) ;
281+ console . error ( 'Failed to get Java version:' , error ) ;
158282 return { javaVersion : 'unknown' } ;
159283 }
160284}
@@ -218,63 +342,3 @@ async function installContextProvider(
218342 }
219343 return undefined ;
220344}
221-
222- /**
223- * Java-specific Copilot completion context provider
224- * Similar to CopilotCompletionContextProvider but tailored for Java language
225- */
226- export class JavaCopilotCompletionContextProvider implements ContextProvider < SupportedContextItem > {
227- public readonly id = 'java-completion' ;
228- public readonly selector = [ { language : 'java' } ] ;
229- public readonly resolver = this . resolve . bind ( this ) ;
230-
231- // Cache for completion contexts with timeout
232- private cache = new Map < string , { context : SupportedContextItem [ ] ; timestamp : number } > ( ) ;
233- private readonly cacheTimeout = 30000 ; // 30 seconds
234-
235- public async resolve ( request : ResolveRequest , cancellationToken : vscode . CancellationToken ) : Promise < SupportedContextItem [ ] > {
236- // Access document through request properties
237- const docUri = request . documentContext ?. uri ?. toString ( ) ;
238- const docOffset = request . documentContext ?. offset ;
239-
240- // Only process Java files
241- if ( ! docUri || ! docUri . endsWith ( '.java' ) ) {
242- return [ ] ;
243- }
244-
245- const cacheKey = `${ docUri } :${ docOffset } ` ;
246- const cached = this . cache . get ( cacheKey ) ;
247-
248- // Return cached result if still valid
249- if ( cached && Date . now ( ) - cached . timestamp < this . cacheTimeout ) {
250- return cached . context ;
251- }
252-
253- try {
254- const context = await resolveJavaContext ( request , cancellationToken ) ;
255-
256- // Cache the result
257- this . cache . set ( cacheKey , {
258- context,
259- timestamp : Date . now ( )
260- } ) ;
261-
262- // Clean up old cache entries
263- this . cleanCache ( ) ;
264-
265- return context ;
266- } catch ( error ) {
267- console . error ( 'Error generating Java completion context:' , error ) ;
268- return [ ] ;
269- }
270- }
271-
272- private cleanCache ( ) : void {
273- const now = Date . now ( ) ;
274- for ( const [ key , value ] of this . cache . entries ( ) ) {
275- if ( now - value . timestamp > this . cacheTimeout ) {
276- this . cache . delete ( key ) ;
277- }
278- }
279- }
280- }
0 commit comments