✨ Reload projects when analyzer dlls change
This commit is contained in:
@@ -34,6 +34,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<RoslynAnalysis>();
|
||||
services.AddScoped<IdeFileOperationsService>();
|
||||
services.AddScoped<SharpIdeSolutionModificationService>();
|
||||
services.AddScoped<AnalyzerFileWatcher>();
|
||||
services.AddLogging();
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -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)}");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user