55import * as vscode from 'vscode' ;
66import * as crypto from 'crypto' ;
77import { INodeImportClass } from './copilotHelper' ;
8-
8+ import { logger } from "../utils" ;
99/**
10- * Cache entry interface for storing import data with timestamp
10+ * Cache entry interface for storing import data with enhanced metadata
1111 */
1212interface CacheEntry {
13+ /** Unique cache entry ID for tracking */
14+ id : string ;
15+ /** Cached import data */
1316 value : INodeImportClass [ ] ;
17+ /** Creation timestamp */
1418 timestamp : number ;
19+ /** Document version when cached */
20+ documentVersion ?: number ;
21+ /** Last access timestamp */
22+ lastAccess : number ;
23+ /** File content hash for change detection */
24+ contentHash ?: string ;
25+ /** Caret offset when cached (for position-sensitive invalidation) */
26+ caretOffset ?: number ;
1527}
1628
1729/**
1830 * Configuration options for the context cache
1931 */
2032interface ContextCacheOptions {
21- /** Cache expiry time in milliseconds. Default: 5 minutes */
33+ /** Cache expiry time in milliseconds. Default: 10 minutes */
2234 expiryTime ?: number ;
2335 /** Enable automatic cleanup interval. Default: true */
2436 enableAutoCleanup ?: boolean ;
2537 /** Enable file watching for cache invalidation. Default: true */
2638 enableFileWatching ?: boolean ;
39+ /** Maximum cache size (number of entries). Default: 100 */
40+ maxCacheSize ?: number ;
41+ /** Enable content-based invalidation. Default: true */
42+ enableContentHashing ?: boolean ;
43+ /** Cleanup interval in milliseconds. Default: 2 minutes */
44+ cleanupInterval ?: number ;
45+ /** Maximum distance from cached caret position before cache becomes stale. Default: 8192 */
46+ maxCaretDistance ?: number ;
47+ /** Enable position-sensitive cache invalidation. Default: false */
48+ enablePositionSensitive ?: boolean ;
2749}
2850
2951/**
@@ -34,14 +56,25 @@ export class ContextCache {
3456 private readonly expiryTime : number ;
3557 private readonly enableAutoCleanup : boolean ;
3658 private readonly enableFileWatching : boolean ;
59+ private readonly maxCacheSize : number ;
60+ private readonly enableContentHashing : boolean ;
61+ private readonly cleanupIntervalMs : number ;
62+ private readonly maxCaretDistance : number ;
63+ private readonly enablePositionSensitive : boolean ;
3764
38- private cleanupInterval ?: NodeJS . Timeout ;
65+ private cleanupTimer ?: NodeJS . Timeout ;
3966 private fileWatcher ?: vscode . FileSystemWatcher ;
67+ private accessCount = 0 ; // For statistics tracking
4068
4169 constructor ( options : ContextCacheOptions = { } ) {
42- this . expiryTime = options . expiryTime ?? 5 * 60 * 1000 ; // 5 minutes default
70+ this . expiryTime = options . expiryTime ?? 10 * 60 * 1000 ; // 10 minutes default
4371 this . enableAutoCleanup = options . enableAutoCleanup ?? true ;
4472 this . enableFileWatching = options . enableFileWatching ?? true ;
73+ this . maxCacheSize = options . maxCacheSize ?? 100 ;
74+ this . enableContentHashing = options . enableContentHashing ?? true ;
75+ this . cleanupIntervalMs = options . cleanupInterval ?? 2 * 60 * 1000 ; // 2 minutes
76+ this . maxCaretDistance = options . maxCaretDistance ?? 8192 ; // Same as CopilotCompletionContextProvider
77+ this . enablePositionSensitive = options . enablePositionSensitive ?? false ;
4578 }
4679
4780 /**
@@ -79,37 +112,104 @@ export class ContextCache {
79112 }
80113
81114 /**
82- * Get cached imports for a document URI
115+ * Get cached imports for a document URI with enhanced validation
83116 * @param uri Document URI
117+ * @param currentCaretOffset Optional current caret offset for position-sensitive validation
118+ * @returns Cached imports or null if not found/expired/stale
119+ */
120+ public async get ( uri : vscode . Uri , currentCaretOffset ?: number ) : Promise < INodeImportClass [ ] | null > {
121+ const key = this . generateCacheKey ( uri ) ;
122+ const cached = this . cache . get ( key ) ;
123+
124+ if ( ! cached ) {
125+ return null ;
126+ }
127+
128+ // Check if cache is expired or stale
129+ if ( await this . isExpiredOrStale ( uri , cached , currentCaretOffset ) ) {
130+ this . cache . delete ( key ) ;
131+ return null ;
132+ }
133+
134+ // Update last access time and increment access count
135+ cached . lastAccess = Date . now ( ) ;
136+ this . accessCount ++ ;
137+
138+ return cached . value ;
139+ }
140+
141+ /**
142+ * Get cached imports synchronously (fallback method for compatibility)
143+ * @param uri Document URI
144+ * @param currentCaretOffset Optional current caret offset for position-sensitive validation
84145 * @returns Cached imports or null if not found/expired
85146 */
86- public get ( uri : vscode . Uri ) : INodeImportClass [ ] | null {
147+ public getSync ( uri : vscode . Uri , currentCaretOffset ?: number ) : INodeImportClass [ ] | null {
87148 const key = this . generateCacheKey ( uri ) ;
88149 const cached = this . cache . get ( key ) ;
89150
90151 if ( ! cached ) {
91152 return null ;
92153 }
93154
94- // Check if cache is expired
155+ // Check time-based expiry
95156 if ( this . isExpired ( cached ) ) {
96157 this . cache . delete ( key ) ;
97158 return null ;
98159 }
99160
161+ // Check position-sensitive expiry if enabled and caret offsets available
162+ if ( this . enablePositionSensitive &&
163+ cached . caretOffset !== undefined &&
164+ currentCaretOffset !== undefined ) {
165+ if ( this . isStaleCacheHit ( currentCaretOffset , cached . caretOffset ) ) {
166+ this . cache . delete ( key ) ;
167+ return null ;
168+ }
169+ }
170+
171+ // Update last access time and increment access count
172+ cached . lastAccess = Date . now ( ) ;
173+ this . accessCount ++ ;
174+
100175 return cached . value ;
101176 }
102177
103178 /**
104179 * Set cached imports for a document URI
105180 * @param uri Document URI
106181 * @param imports Import class array to cache
182+ * @param documentVersion Optional document version
183+ * @param caretOffset Optional caret offset for position-sensitive caching
107184 */
108- public set ( uri : vscode . Uri , imports : INodeImportClass [ ] ) : void {
185+ public async set ( uri : vscode . Uri , imports : INodeImportClass [ ] , documentVersion ?: number , caretOffset ?: number ) : Promise < void > {
109186 const key = this . generateCacheKey ( uri ) ;
187+ const now = Date . now ( ) ;
188+
189+ // Check cache size limit and evict if necessary
190+ if ( this . cache . size >= this . maxCacheSize ) {
191+ this . evictLeastRecentlyUsed ( ) ;
192+ }
193+
194+ // Generate content hash if enabled
195+ let contentHash : string | undefined ;
196+ if ( this . enableContentHashing ) {
197+ try {
198+ const document = await vscode . workspace . openTextDocument ( uri ) ;
199+ contentHash = crypto . createHash ( 'md5' ) . update ( document . getText ( ) ) . digest ( 'hex' ) ;
200+ } catch ( error ) {
201+ logger . error ( 'Failed to generate content hash:' , error ) ;
202+ }
203+ }
204+
110205 this . cache . set ( key , {
206+ id : crypto . randomUUID ( ) ,
111207 value : imports ,
112- timestamp : Date . now ( )
208+ timestamp : now ,
209+ lastAccess : now ,
210+ documentVersion,
211+ contentHash,
212+ caretOffset
113213 } ) ;
114214 }
115215
@@ -122,6 +222,89 @@ export class ContextCache {
122222 return Date . now ( ) - entry . timestamp > this . expiryTime ;
123223 }
124224
225+ /**
226+ * Check if cache is stale based on caret position (similar to CopilotCompletionContextProvider)
227+ * @param currentCaretOffset Current caret offset
228+ * @param cachedCaretOffset Cached caret offset
229+ * @returns True if stale, false otherwise
230+ */
231+ private isStaleCacheHit ( currentCaretOffset : number , cachedCaretOffset : number ) : boolean {
232+ return Math . abs ( currentCaretOffset - cachedCaretOffset ) > this . maxCaretDistance ;
233+ }
234+
235+ /**
236+ * Enhanced expiry check including content changes and position sensitivity
237+ * @param uri Document URI
238+ * @param entry Cache entry to check
239+ * @param currentCaretOffset Optional current caret offset
240+ * @returns True if expired or stale
241+ */
242+ private async isExpiredOrStale ( uri : vscode . Uri , entry : CacheEntry , currentCaretOffset ?: number ) : Promise < boolean > {
243+ // Check time-based expiry
244+ if ( this . isExpired ( entry ) ) {
245+ return true ;
246+ }
247+
248+ // Check position-sensitive expiry if enabled and caret offsets available
249+ if ( this . enablePositionSensitive &&
250+ entry . caretOffset !== undefined &&
251+ currentCaretOffset !== undefined ) {
252+ if ( this . isStaleCacheHit ( currentCaretOffset , entry . caretOffset ) ) {
253+ return true ;
254+ }
255+ }
256+
257+ // Check content-based changes
258+ if ( await this . hasContentChanged ( uri , entry ) ) {
259+ return true ;
260+ }
261+
262+ return false ;
263+ }
264+
265+ /**
266+ * Evict least recently used cache entries when cache is full
267+ */
268+ private evictLeastRecentlyUsed ( ) : void {
269+ if ( this . cache . size === 0 ) return ;
270+
271+ let oldestTime = Date . now ( ) ;
272+ let oldestKey = '' ;
273+
274+ for ( const [ key , entry ] of this . cache . entries ( ) ) {
275+ if ( entry . lastAccess < oldestTime ) {
276+ oldestTime = entry . lastAccess ;
277+ oldestKey = key ;
278+ }
279+ }
280+
281+ if ( oldestKey ) {
282+ this . cache . delete ( oldestKey ) ;
283+ logger . trace ( 'Evicted LRU cache entry:' , oldestKey ) ;
284+ }
285+ }
286+
287+ /**
288+ * Check if content has changed by comparing hash
289+ * @param uri Document URI
290+ * @param entry Cache entry to check
291+ * @returns True if content has changed
292+ */
293+ private async hasContentChanged ( uri : vscode . Uri , entry : CacheEntry ) : Promise < boolean > {
294+ if ( ! this . enableContentHashing || ! entry . contentHash ) {
295+ return false ;
296+ }
297+
298+ try {
299+ const document = await vscode . workspace . openTextDocument ( uri ) ;
300+ const currentHash = crypto . createHash ( 'md5' ) . update ( document . getText ( ) ) . digest ( 'hex' ) ;
301+ return currentHash !== entry . contentHash ;
302+ } catch ( error ) {
303+ logger . error ( 'Failed to check content change:' , error ) ;
304+ return false ;
305+ }
306+ }
307+
125308 /**
126309 * Clear expired cache entries
127310 */
@@ -149,28 +332,38 @@ export class ContextCache {
149332 const key = this . generateCacheKey ( uri ) ;
150333 if ( this . cache . has ( key ) ) {
151334 this . cache . delete ( key ) ;
152- console . log ( 'Cache invalidated for:' , uri . toString ( ) ) ;
335+ logger . trace ( 'Cache invalidated for:' , uri . toString ( ) ) ;
153336 }
154337 }
155338
156339 /**
157340 * Get cache statistics
158341 * @returns Object containing cache size and other statistics
159342 */
160- public getStats ( ) : { size : number ; expiryTime : number } {
343+ public getStats ( ) : {
344+ size : number ;
345+ expiryTime : number ;
346+ accessCount : number ;
347+ maxSize : number ;
348+ hitRate ?: number ;
349+ positionSensitive : boolean ;
350+ } {
161351 return {
162352 size : this . cache . size ,
163- expiryTime : this . expiryTime
353+ expiryTime : this . expiryTime ,
354+ accessCount : this . accessCount ,
355+ maxSize : this . maxCacheSize ,
356+ positionSensitive : this . enablePositionSensitive
164357 } ;
165358 }
166359
167360 /**
168361 * Start periodic cleanup of expired cache entries
169362 */
170363 private startPeriodicCleanup ( ) : void {
171- this . cleanupInterval = setInterval ( ( ) => {
364+ this . cleanupTimer = setInterval ( ( ) => {
172365 this . clearExpired ( ) ;
173- } , this . expiryTime ) ;
366+ } , this . cleanupIntervalMs ) ;
174367 }
175368
176369 /**
@@ -191,9 +384,9 @@ export class ContextCache {
191384 * Dispose of all resources (intervals, watchers, etc.)
192385 */
193386 public dispose ( ) : void {
194- if ( this . cleanupInterval ) {
195- clearInterval ( this . cleanupInterval ) ;
196- this . cleanupInterval = undefined ;
387+ if ( this . cleanupTimer ) {
388+ clearInterval ( this . cleanupTimer ) ;
389+ this . cleanupTimer = undefined ;
197390 }
198391
199392 if ( this . fileWatcher ) {
@@ -209,3 +402,16 @@ export class ContextCache {
209402 * Default context cache instance
210403 */
211404export const contextCache = new ContextCache ( ) ;
405+
406+ /**
407+ * Enhanced context cache instance with position-sensitive features enabled
408+ * for more precise code completion context
409+ */
410+ export const enhancedContextCache = new ContextCache ( {
411+ expiryTime : 10 * 60 * 1000 , // 10 minutes
412+ enablePositionSensitive : true ,
413+ maxCaretDistance : 8192 , // Same as CopilotCompletionContextProvider
414+ enableContentHashing : true ,
415+ maxCacheSize : 100 ,
416+ cleanupInterval : 2 * 60 * 1000 // 2 minutes
417+ } ) ;
0 commit comments