✨ Reload projects when analyzer dlls change
This commit is contained in:
@@ -34,6 +34,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<RoslynAnalysis>();
|
services.AddScoped<RoslynAnalysis>();
|
||||||
services.AddScoped<IdeFileOperationsService>();
|
services.AddScoped<IdeFileOperationsService>();
|
||||||
services.AddScoped<SharpIdeSolutionModificationService>();
|
services.AddScoped<SharpIdeSolutionModificationService>();
|
||||||
|
services.AddScoped<AnalyzerFileWatcher>();
|
||||||
services.AddLogging();
|
services.AddLogging();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ using SharpIDE.Application.Features.Analysis.FixLoaders;
|
|||||||
using SharpIDE.Application.Features.Analysis.ProjectLoader;
|
using SharpIDE.Application.Features.Analysis.ProjectLoader;
|
||||||
using SharpIDE.Application.Features.Analysis.Razor;
|
using SharpIDE.Application.Features.Analysis.Razor;
|
||||||
using SharpIDE.Application.Features.Build;
|
using SharpIDE.Application.Features.Build;
|
||||||
|
using SharpIDE.Application.Features.FileWatching;
|
||||||
using SharpIDE.Application.Features.SolutionDiscovery;
|
using SharpIDE.Application.Features.SolutionDiscovery;
|
||||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||||
using CodeAction = Microsoft.CodeAnalysis.CodeActions.CodeAction;
|
using CodeAction = Microsoft.CodeAnalysis.CodeActions.CodeAction;
|
||||||
@@ -40,10 +41,11 @@ using DiagnosticSeverity = Microsoft.CodeAnalysis.DiagnosticSeverity;
|
|||||||
|
|
||||||
namespace SharpIDE.Application.Features.Analysis;
|
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 ILogger<RoslynAnalysis> _logger = logger;
|
||||||
private readonly BuildService _buildService = buildService;
|
private readonly BuildService _buildService = buildService;
|
||||||
|
private readonly AnalyzerFileWatcher _analyzerFileWatcher = analyzerFileWatcher;
|
||||||
|
|
||||||
public static AdhocWorkspace? _workspace;
|
public static AdhocWorkspace? _workspace;
|
||||||
private static CustomMsBuildProjectLoader? _msBuildProjectLoader;
|
private static CustomMsBuildProjectLoader? _msBuildProjectLoader;
|
||||||
@@ -119,6 +121,13 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
|
|||||||
//_msBuildProjectLoader!.LoadMetadataForReferencedProjects = true;
|
//_msBuildProjectLoader!.LoadMetadataForReferencedProjects = true;
|
||||||
var (solutionInfo, projectFileInfos) = await _msBuildProjectLoader!.LoadSolutionInfoAsync(_sharpIdeSolutionModel.FilePath, cancellationToken: cancellationToken);
|
var (solutionInfo, projectFileInfos) = await _msBuildProjectLoader!.LoadSolutionInfoAsync(_sharpIdeSolutionModel.FilePath, cancellationToken: cancellationToken);
|
||||||
_projectFileInfoMap = projectFileInfos;
|
_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();
|
_workspace.ClearSolution();
|
||||||
var solution = _workspace.AddSolution(solutionInfo);
|
var solution = _workspace.AddSolution(solutionInfo);
|
||||||
}
|
}
|
||||||
@@ -270,6 +279,29 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
|
|||||||
}).ToImmutableArray();
|
}).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)
|
public async Task UpdateSolutionDiagnostics(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(UpdateSolutionDiagnostics)}");
|
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;
|
||||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ public class GlobalEvents
|
|||||||
public EventWrapper<Task> SolutionAltered { get; } = new(() => Task.CompletedTask);
|
public EventWrapper<Task> SolutionAltered { get; } = new(() => Task.CompletedTask);
|
||||||
|
|
||||||
public FileSystemWatcherInternal FileSystemWatcherInternal { get; } = new();
|
public FileSystemWatcherInternal FileSystemWatcherInternal { get; } = new();
|
||||||
|
public EventWrapper<ImmutableArray<string>, Task> AnalyzerDllsChanged { get; } = new(_ => Task.CompletedTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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.CodeAnalysis.Threading;
|
||||||
using Microsoft.VisualStudio.SolutionPersistence.Model;
|
using Microsoft.VisualStudio.SolutionPersistence.Model;
|
||||||
using SharpIDE.Application.Features.Analysis;
|
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
|
// All file changes should go via this service
|
||||||
public async Task SharpIdeFileChanged(SharpIdeFile file, string newContents, FileChangeType changeType, SharpIdeFileLinePosition? linePosition = null)
|
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 Microsoft.Extensions.Logging;
|
||||||
using SharpIDE.Application.Features.Events;
|
using SharpIDE.Application.Features.Events;
|
||||||
using SharpIDE.Application.Features.SolutionDiscovery;
|
using SharpIDE.Application.Features.SolutionDiscovery;
|
||||||
@@ -24,6 +25,7 @@ public class IdeFileExternalChangeHandler
|
|||||||
GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryCreated.Subscribe(OnFolderCreated);
|
GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryCreated.Subscribe(OnFolderCreated);
|
||||||
GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryDeleted.Subscribe(OnFolderDeleted);
|
GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryDeleted.Subscribe(OnFolderDeleted);
|
||||||
GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryRenamed.Subscribe(OnFolderRenamed);
|
GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryRenamed.Subscribe(OnFolderRenamed);
|
||||||
|
GlobalEvents.Instance.AnalyzerDllsChanged.Subscribe(OnAnalyzerDllsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnFileRenamed(string oldFilePath, string newFilePath)
|
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);
|
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 Microsoft.Extensions.Logging;
|
||||||
using SharpIDE.Application.Features.Analysis;
|
using SharpIDE.Application.Features.Analysis;
|
||||||
using SharpIDE.Application.Features.Build;
|
using SharpIDE.Application.Features.Build;
|
||||||
|
using SharpIDE.Application.Features.FileWatching;
|
||||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||||
|
|
||||||
[assembly: CaptureConsole]
|
[assembly: CaptureConsole]
|
||||||
@@ -29,8 +30,9 @@ public class RoslynAnalysisTests
|
|||||||
var services = serviceCollection.BuildServiceProvider();
|
var services = serviceCollection.BuildServiceProvider();
|
||||||
var logger = services.GetRequiredService<ILogger<RoslynAnalysis>>();
|
var logger = services.GetRequiredService<ILogger<RoslynAnalysis>>();
|
||||||
var buildService = services.GetRequiredService<BuildService>();
|
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 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");
|
var sharpIdeApplicationProject = solutionModel.AllProjects.Single(p => p.Name == "SharpIDE.Application");
|
||||||
|
|||||||
Reference in New Issue
Block a user