using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; namespace MarketAlly.AIPlugin.Refactoring.Plugins { public interface IFileCache { Task GetSyntaxTreeAsync(string filePath); Task GetFileContentAsync(string filePath); void InvalidateCache(string filePath); void InvalidateAll(); long GetCacheSize(); void SetMaxCacheSize(long maxSizeBytes); } public class FileCache : IFileCache { private readonly ConcurrentDictionary _syntaxTreeCache = new(); private readonly ConcurrentDictionary _contentCache = new(); private readonly ReaderWriterLockSlim _cacheLock = new(); private long _maxCacheSize = 100 * 1024 * 1024; // 100MB default private long _currentCacheSize = 0; private class CacheEntry { public object Content { get; set; } public DateTime LastAccessed { get; set; } public DateTime FileLastModified { get; set; } public long Size { get; set; } public CacheEntry(object content, DateTime fileLastModified, long size) { Content = content; LastAccessed = DateTime.UtcNow; FileLastModified = fileLastModified; Size = size; } } public async Task GetSyntaxTreeAsync(string filePath) { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) throw new FileNotFoundException($"File not found: {filePath}"); var fileInfo = new FileInfo(filePath); var normalizedPath = Path.GetFullPath(filePath); // Check cache first if (_syntaxTreeCache.TryGetValue(normalizedPath, out var cachedEntry)) { // Validate cache entry is still fresh if (cachedEntry.FileLastModified >= fileInfo.LastWriteTime) { cachedEntry.LastAccessed = DateTime.UtcNow; return (SyntaxTree)cachedEntry.Content; } else { // File has been modified, remove stale entry _syntaxTreeCache.TryRemove(normalizedPath, out _); Interlocked.Add(ref _currentCacheSize, -cachedEntry.Size); } } // Load and parse file var content = await File.ReadAllTextAsync(filePath); var syntaxTree = CSharpSyntaxTree.ParseText(content, path: filePath); // Estimate memory usage (rough approximation) var size = content.Length * 2 + 1024; // Text + overhead // Check if we need to evict entries before adding new one await EnsureCacheSpace(size); // Add to cache var newEntry = new CacheEntry(syntaxTree, fileInfo.LastWriteTime, size); _syntaxTreeCache.TryAdd(normalizedPath, newEntry); Interlocked.Add(ref _currentCacheSize, size); return syntaxTree; } public async Task GetFileContentAsync(string filePath) { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) throw new FileNotFoundException($"File not found: {filePath}"); var fileInfo = new FileInfo(filePath); var normalizedPath = Path.GetFullPath(filePath); // Check cache first if (_contentCache.TryGetValue(normalizedPath, out var cachedEntry)) { // Validate cache entry is still fresh if (cachedEntry.FileLastModified >= fileInfo.LastWriteTime) { cachedEntry.LastAccessed = DateTime.UtcNow; return (string)cachedEntry.Content; } else { // File has been modified, remove stale entry _contentCache.TryRemove(normalizedPath, out _); Interlocked.Add(ref _currentCacheSize, -cachedEntry.Size); } } // Load file content var content = await File.ReadAllTextAsync(filePath); var size = content.Length * 2; // Rough string memory usage // Check if we need to evict entries before adding new one await EnsureCacheSpace(size); // Add to cache var newEntry = new CacheEntry(content, fileInfo.LastWriteTime, size); _contentCache.TryAdd(normalizedPath, newEntry); Interlocked.Add(ref _currentCacheSize, size); return content; } public void InvalidateCache(string filePath) { if (string.IsNullOrEmpty(filePath)) return; var normalizedPath = Path.GetFullPath(filePath); if (_syntaxTreeCache.TryRemove(normalizedPath, out var syntaxEntry)) { Interlocked.Add(ref _currentCacheSize, -syntaxEntry.Size); } if (_contentCache.TryRemove(normalizedPath, out var contentEntry)) { Interlocked.Add(ref _currentCacheSize, -contentEntry.Size); } } public void InvalidateAll() { _cacheLock.EnterWriteLock(); try { _syntaxTreeCache.Clear(); _contentCache.Clear(); _currentCacheSize = 0; } finally { _cacheLock.ExitWriteLock(); } } public long GetCacheSize() { return _currentCacheSize; } public void SetMaxCacheSize(long maxSizeBytes) { _maxCacheSize = maxSizeBytes; // Trigger cleanup if current size exceeds new limit _ = Task.Run(() => EnsureCacheSpace(0)); } private async Task EnsureCacheSpace(long requiredSpace) { if (_currentCacheSize + requiredSpace <= _maxCacheSize) return; _cacheLock.EnterWriteLock(); try { // Calculate how much space we need to free var targetSize = _maxCacheSize - requiredSpace; var toRemove = _currentCacheSize - targetSize; if (toRemove <= 0) return; // Collect all entries with their access times var allEntries = new List<(string key, CacheEntry entry, bool isSyntaxTree)>(); foreach (var kvp in _syntaxTreeCache) { allEntries.Add((kvp.Key, kvp.Value, true)); } foreach (var kvp in _contentCache) { allEntries.Add((kvp.Key, kvp.Value, false)); } // Sort by last accessed time (LRU) allEntries.Sort((a, b) => a.entry.LastAccessed.CompareTo(b.entry.LastAccessed)); // Remove oldest entries until we have enough space long freedSpace = 0; foreach (var (key, entry, isSyntaxTree) in allEntries) { if (freedSpace >= toRemove) break; if (isSyntaxTree) { _syntaxTreeCache.TryRemove(key, out _); } else { _contentCache.TryRemove(key, out _); } freedSpace += entry.Size; Interlocked.Add(ref _currentCacheSize, -entry.Size); } } finally { _cacheLock.ExitWriteLock(); } } public void Dispose() { _cacheLock?.Dispose(); } } // Singleton instance for global use across plugins public static class GlobalFileCache { private static readonly Lazy _instance = new Lazy(() => new FileCache()); public static IFileCache Instance => _instance.Value; } }