Reload projects when analyzer dlls change

This commit is contained in:
Matt Parker
2025-11-28 22:26:39 +10:00
parent f4b1e9c1c0
commit e75d1319ef
7 changed files with 113 additions and 5 deletions

View File

@@ -34,6 +34,7 @@ public static class DependencyInjection
services.AddScoped<RoslynAnalysis>();
services.AddScoped<IdeFileOperationsService>();
services.AddScoped<SharpIdeSolutionModificationService>();
services.AddScoped<AnalyzerFileWatcher>();
services.AddLogging();
return services;
}

View File

@@ -30,6 +30,7 @@ using SharpIDE.Application.Features.Analysis.FixLoaders;
using SharpIDE.Application.Features.Analysis.ProjectLoader;
using SharpIDE.Application.Features.Analysis.Razor;
using SharpIDE.Application.Features.Build;
using SharpIDE.Application.Features.FileWatching;
using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
using CodeAction = Microsoft.CodeAnalysis.CodeActions.CodeAction;
@@ -40,10 +41,11 @@ using DiagnosticSeverity = Microsoft.CodeAnalysis.DiagnosticSeverity;
namespace SharpIDE.Application.Features.Analysis;
public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildService)
public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildService, AnalyzerFileWatcher analyzerFileWatcher)
{
private readonly ILogger<RoslynAnalysis> _logger = logger;
private readonly BuildService _buildService = buildService;
private readonly AnalyzerFileWatcher _analyzerFileWatcher = analyzerFileWatcher;
public static AdhocWorkspace? _workspace;
private static CustomMsBuildProjectLoader? _msBuildProjectLoader;
@@ -119,6 +121,13 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
//_msBuildProjectLoader!.LoadMetadataForReferencedProjects = true;
var (solutionInfo, projectFileInfos) = await _msBuildProjectLoader!.LoadSolutionInfoAsync(_sharpIdeSolutionModel.FilePath, cancellationToken: cancellationToken);
_projectFileInfoMap = projectFileInfos;
var analyzerReferencePaths = solutionInfo.Projects
.SelectMany(p => p.AnalyzerReferences.OfType<IsolatedAnalyzerFileReference>().Select(a => a.FullPath))
.OfType<string>()
.Distinct()
.ToImmutableArray();
await _analyzerFileWatcher.StartWatchingFiles(analyzerReferencePaths);
_workspace.ClearSolution();
var solution = _workspace.AddSolution(solutionInfo);
}
@@ -270,6 +279,29 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
}).ToImmutableArray();
}
public async Task<bool> ReloadProjectsWithAnyOfAnalyzerFileReferences(ImmutableArray<string> analyzerFilePaths, CancellationToken cancellationToken = default)
{
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(ReloadProjectsWithAnyOfAnalyzerFileReferences)}");
await _solutionLoadedTcs.Task;
var projectsToReload = _workspace!.CurrentSolution.Projects
.Where(p => p.AnalyzerReferences
.OfType<IsolatedAnalyzerFileReference>()
.Where(s => s.FullPath is not null)
.Any(a => analyzerFilePaths.Contains(a.FullPath!)))
.ToList();
if (projectsToReload.Count is 0) return false;
_logger.LogInformation("RoslynAnalysis: Reloading {ProjectCount} projects that reference an analyzer that changed", projectsToReload.Count);
foreach (var project in projectsToReload)
{
var sharpIdeProjectModel = _sharpIdeSolutionModel!.AllProjects.Single(p => p.FilePath == project.FilePath);
await ReloadProject(sharpIdeProjectModel, cancellationToken);
}
return true;
}
public async Task UpdateSolutionDiagnostics(CancellationToken cancellationToken = default)
{
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(UpdateSolutionDiagnostics)}");

View File

@@ -1,4 +1,5 @@
using SharpIDE.Application.Features.Debugging;
using System.Collections.Immutable;
using SharpIDE.Application.Features.Debugging;
using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
@@ -18,6 +19,7 @@ public class GlobalEvents
public EventWrapper<Task> SolutionAltered { get; } = new(() => Task.CompletedTask);
public FileSystemWatcherInternal FileSystemWatcherInternal { get; } = new();
public EventWrapper<ImmutableArray<string>, Task> AnalyzerDllsChanged { get; } = new(_ => Task.CompletedTask);
}
/// <summary>

View File

@@ -0,0 +1,55 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using FileWatcherEx;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Threading;
using SharpIDE.Application.Features.Events;
namespace SharpIDE.Application.Features.FileWatching;
public class AnalyzerFileWatcher
{
private readonly AsyncBatchingWorkQueue<string> _fileChangedQueue;
public AnalyzerFileWatcher()
{
_fileChangedQueue = new AsyncBatchingWorkQueue<string>(
TimeSpan.FromMilliseconds(500),
async (filePaths, ct) => await GlobalEvents.Instance.AnalyzerDllsChanged.InvokeParallelAsync(filePaths.ToImmutableArray()),
new AsynchronousOperationListenerProvider.NullOperationListener(),
CancellationToken.None);
}
private readonly ConcurrentDictionary<string, FileSystemWatcherEx> _fileWatchers = new();
// I wanted to avoid this, but unfortunately we have to watch individual files in different directories
public async Task StartWatchingFiles(ImmutableArray<string> filePaths)
{
// This can definitely be optimized to not recreate watchers unnecessarily
var existingFileWatchers = _fileWatchers.Values.ToList();
foreach (var watcher in existingFileWatchers)
{
watcher.OnChanged -= OnFileChanged;
watcher.Dispose();
}
_fileWatchers.Clear();
foreach (var filePath in filePaths)
{
var fileWatcher = new FileSystemWatcherEx(Path.GetDirectoryName(filePath)!)
{
Filter = Path.GetFileName(filePath),
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size
};
fileWatcher.OnChanged += OnFileChanged;
fileWatcher.Start();
_fileWatchers.TryAdd(filePath, fileWatcher);
}
}
private void OnFileChanged(object? sender, FileChangedEvent e)
{
_fileChangedQueue.AddWork(e.FullPath);
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis.Shared.TestHooks;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Threading;
using Microsoft.VisualStudio.SolutionPersistence.Model;
using SharpIDE.Application.Features.Analysis;
@@ -71,6 +72,14 @@ public class FileChangedService
}
}
public async Task AnalyzerDllFilesChanged(ImmutableArray<string> changedDllPaths)
{
var success = await _roslynAnalysis.ReloadProjectsWithAnyOfAnalyzerFileReferences(changedDllPaths);
if (success is false) return;
GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget();
_updateSolutionDiagnosticsQueue.AddWork();
}
// All file changes should go via this service
public async Task SharpIdeFileChanged(SharpIdeFile file, string newContents, FileChangeType changeType, SharpIdeFileLinePosition? linePosition = null)
{

View File

@@ -1,4 +1,5 @@
using Ardalis.GuardClauses;
using System.Collections.Immutable;
using Ardalis.GuardClauses;
using Microsoft.Extensions.Logging;
using SharpIDE.Application.Features.Events;
using SharpIDE.Application.Features.SolutionDiscovery;
@@ -24,6 +25,7 @@ public class IdeFileExternalChangeHandler
GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryCreated.Subscribe(OnFolderCreated);
GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryDeleted.Subscribe(OnFolderDeleted);
GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryRenamed.Subscribe(OnFolderRenamed);
GlobalEvents.Instance.AnalyzerDllsChanged.Subscribe(OnAnalyzerDllsChanged);
}
private async Task OnFileRenamed(string oldFilePath, string newFilePath)
@@ -129,4 +131,9 @@ public class IdeFileExternalChangeHandler
await _fileChangedService.SharpIdeFileChanged(file, await File.ReadAllTextAsync(file.Path), FileChangeType.ExternalChange);
}
}
private async Task OnAnalyzerDllsChanged(ImmutableArray<string> analyzerDllPaths)
{
await _fileChangedService.AnalyzerDllFilesChanged(analyzerDllPaths);
}
}

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharpIDE.Application.Features.Analysis;
using SharpIDE.Application.Features.Build;
using SharpIDE.Application.Features.FileWatching;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
[assembly: CaptureConsole]
@@ -29,8 +30,9 @@ public class RoslynAnalysisTests
var services = serviceCollection.BuildServiceProvider();
var logger = services.GetRequiredService<ILogger<RoslynAnalysis>>();
var buildService = services.GetRequiredService<BuildService>();
var analyzerFileWatcher = services.GetRequiredService<AnalyzerFileWatcher>();
var roslynAnalysis = new RoslynAnalysis(logger, buildService);
var roslynAnalysis = new RoslynAnalysis(logger, buildService, analyzerFileWatcher);
var solutionModel = await VsPersistenceMapper.GetSolutionModel(@"C:\Users\Matthew\Documents\Git\SharpIDE\SharpIDE.sln", TestContext.Current.CancellationToken);
var sharpIdeApplicationProject = solutionModel.AllProjects.Single(p => p.Name == "SharpIDE.Application");