Skip to content

Commit 42a47ce

Browse files
committed
fix: update code according to the comments
1 parent 1d257e9 commit 42a47ce

File tree

3 files changed

+252
-31
lines changed

3 files changed

+252
-31
lines changed

src/copilot/context/contextCache.ts

Lines changed: 224 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,47 @@
55
import * as vscode from 'vscode';
66
import * as crypto from 'crypto';
77
import { 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
*/
1212
interface 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
*/
2032
interface 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
*/
211404
export 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+
});

src/copilot/context/copilotHelper.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { commands, Uri } from "vscode";
55
import { logger } from "../utils";
6+
import { validateAndRecommendExtension } from "../../recommendation";
67

78
export interface INodeImportClass {
89
uri: string;
@@ -11,15 +12,18 @@ export interface INodeImportClass {
1112
/**
1213
* Helper class for Copilot integration to analyze Java project dependencies
1314
*/
14-
export namespace CopilotHelper {
15+
export namespace CopilotHelper {
1516
/**
1617
* Resolves all local project types imported by the given file
1718
* @param fileUri The URI of the Java file to analyze
1819
* @returns Array of strings in format "type:fully.qualified.name" where type is class|interface|enum|annotation
1920
*/
2021
export async function resolveLocalImports(fileUri: Uri): Promise<INodeImportClass[]> {
22+
if (!await validateAndRecommendExtension("vscjava.vscode-java-dependency", "Project Manager for Java extension is recommended to provide additional Java project explorer features.", true)) {
23+
return [];
24+
}
2125
try {
22-
return await commands.executeCommand("java.execute.workspaceCommand", "java.project.getImportClassContent", fileUri) || [];
26+
return await commands.executeCommand("java.execute.workspaceCommand", "java.project.getImportClassContent", fileUri.toString()) || [];
2327
} catch (error) {
2428
logger.error("Error resolving copilot request:", error);
2529
return [];

0 commit comments

Comments
 (0)