|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;
using VSLangProj;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.RuleSets;
[Export(typeof(RuleSetEventHandler))]
internal sealed class RuleSetEventHandler : IVsTrackProjectDocumentsEvents2, IVsTrackProjectDocumentsEvents3, IVsTrackProjectDocumentsEvents4
{
private readonly IThreadingContext _threadingContext;
private readonly IServiceProvider _serviceProvider;
private bool _eventsHookedUp = false;
private uint _cookie = 0;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RuleSetEventHandler(
IThreadingContext threadingContext,
[Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider)
{
_threadingContext = threadingContext;
_serviceProvider = serviceProvider;
}
public async Task RegisterAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken)
{
if (!_eventsHookedUp)
{
var trackProjectDocuments = await serviceProvider.GetServiceAsync<SVsTrackProjectDocuments, IVsTrackProjectDocuments2>(_threadingContext.JoinableTaskFactory).ConfigureAwait(false);
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
if (!_eventsHookedUp)
{
if (ErrorHandler.Succeeded(trackProjectDocuments.AdviseTrackProjectDocumentsEvents(this, out _cookie)))
_eventsHookedUp = true;
}
}
}
public void Unregister()
{
if (_eventsHookedUp)
{
var trackProjectDocuments = (IVsTrackProjectDocuments2)_serviceProvider.GetService(typeof(SVsTrackProjectDocuments));
// Null check, because sometimes during shutdown the IVsTrackProjectDocuments2 is cleaned up before we get told to unregister
if (trackProjectDocuments is null || ErrorHandler.Succeeded(trackProjectDocuments.UnadviseTrackProjectDocumentsEvents(_cookie)))
{
_eventsHookedUp = false;
_cookie = 0;
}
}
}
int IVsTrackProjectDocumentsEvents2.OnAfterAddDirectoriesEx(int cProjects, int cDirectories, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSADDDIRECTORYFLAGS[] rgFlags)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnAfterAddFilesEx(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSADDFILEFLAGS[] rgFlags)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnAfterRemoveDirectories(int cProjects, int cDirectories, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSREMOVEDIRECTORYFLAGS[] rgFlags)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnAfterRemoveFiles(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSREMOVEFILEFLAGS[] rgFlags)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnAfterRenameDirectories(int cProjects, int cDirs, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgszMkOldNames, string[] rgszMkNewNames, VSRENAMEDIRECTORYFLAGS[] rgFlags)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnAfterRenameFiles(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgszMkOldNames, string[] rgszMkNewNames, VSRENAMEFILEFLAGS[] rgFlags)
{
var ruleSetRenames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < rgszMkOldNames.Length; i++)
{
var oldFileFullPath = rgszMkOldNames[i];
if (Path.GetExtension(oldFileFullPath).Equals(".ruleset", StringComparison.OrdinalIgnoreCase))
{
var newFileFullPath = rgszMkNewNames[i];
ruleSetRenames[oldFileFullPath] = newFileFullPath;
}
}
foreach (var path in ruleSetRenames.Values)
{
UpdateCodeAnalysisRuleSetPropertiesInAllProjects(path);
}
return VSConstants.S_OK;
}
int IVsTrackProjectDocumentsEvents2.OnAfterSccStatusChanged(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, uint[] rgdwSccStatus)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnQueryAddDirectories(IVsProject pProject, int cDirectories, string[] rgpszMkDocuments, VSQUERYADDDIRECTORYFLAGS[] rgFlags, VSQUERYADDDIRECTORYRESULTS[] pSummaryResult, VSQUERYADDDIRECTORYRESULTS[] rgResults)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnQueryAddFiles(IVsProject pProject, int cFiles, string[] rgpszMkDocuments, VSQUERYADDFILEFLAGS[] rgFlags, VSQUERYADDFILERESULTS[] pSummaryResult, VSQUERYADDFILERESULTS[] rgResults)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnQueryRemoveDirectories(IVsProject pProject, int cDirectories, string[] rgpszMkDocuments, VSQUERYREMOVEDIRECTORYFLAGS[] rgFlags, VSQUERYREMOVEDIRECTORYRESULTS[] pSummaryResult, VSQUERYREMOVEDIRECTORYRESULTS[] rgResults)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnQueryRemoveFiles(IVsProject pProject, int cFiles, string[] rgpszMkDocuments, VSQUERYREMOVEFILEFLAGS[] rgFlags, VSQUERYREMOVEFILERESULTS[] pSummaryResult, VSQUERYREMOVEFILERESULTS[] rgResults)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnQueryRenameDirectories(IVsProject pProject, int cDirs, string[] rgszMkOldNames, string[] rgszMkNewNames, VSQUERYRENAMEDIRECTORYFLAGS[] rgFlags, VSQUERYRENAMEDIRECTORYRESULTS[] pSummaryResult, VSQUERYRENAMEDIRECTORYRESULTS[] rgResults)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents2.OnQueryRenameFiles(IVsProject pProject, int cFiles, string[] rgszMkOldNames, string[] rgszMkNewNames, VSQUERYRENAMEFILEFLAGS[] rgFlags, VSQUERYRENAMEFILERESULTS[] pSummaryResult, VSQUERYRENAMEFILERESULTS[] rgResults)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents3.OnBeginQueryBatch()
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents3.OnEndQueryBatch(out int pfActionOK)
{
pfActionOK = 1;
return VSConstants.S_OK;
}
int IVsTrackProjectDocumentsEvents3.OnCancelQueryBatch()
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents3.OnQueryAddFilesEx(IVsProject pProject, int cFiles, string[] rgpszNewMkDocuments, string[] rgpszSrcMkDocuments, VSQUERYADDFILEFLAGS[] rgFlags, VSQUERYADDFILERESULTS[] pSummaryResult, VSQUERYADDFILERESULTS[] rgResults)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents3.HandsOffFiles(uint grfRequiredAccess, int cFiles, string[] rgpszMkDocuments)
=> VSConstants.S_OK;
int IVsTrackProjectDocumentsEvents3.HandsOnFiles(int cFiles, string[] rgpszMkDocuments)
=> VSConstants.S_OK;
void IVsTrackProjectDocumentsEvents4.OnQueryRemoveFilesEx(IVsProject pProject, int cFiles, string[] rgpszMkDocuments, uint[] rgFlags, VSQUERYREMOVEFILERESULTS[] pSummaryResult, VSQUERYREMOVEFILERESULTS[] rgResults)
{
}
void IVsTrackProjectDocumentsEvents4.OnQueryRemoveDirectoriesEx(IVsProject pProject, int cDirectories, string[] rgpszMkDocuments, uint[] rgFlags, VSQUERYREMOVEDIRECTORYRESULTS[] pSummaryResult, VSQUERYREMOVEDIRECTORYRESULTS[] rgResults)
{
}
void IVsTrackProjectDocumentsEvents4.OnAfterRemoveFilesEx(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, uint[] rgFlags)
{
// First, handle the files that have been removed from projects (rather than deleted).
// Here we only want to update the projects from which the file was removed.
for (var i = 0; i < rgpProjects.Length; i++)
{
var indexOfFirstDocumentInProject = IndexOfFirstDocumentInProject(i, rgFirstIndices);
var indexOfFirstDocumentInNextProject = IndexOfFirstDocumentInProject(i + 1, rgFirstIndices);
for (var j = indexOfFirstDocumentInProject; j < indexOfFirstDocumentInNextProject; j++)
{
var fileFullPath = rgpszMkDocuments[j];
var removed = (rgFlags[j] & (uint)__VSREMOVEFILEFLAGS2.VSREMOVEFILEFLAGS_IsRemovedFromProjectOnly) != 0;
if (removed &&
Path.GetExtension(fileFullPath).Equals(".ruleset", StringComparison.OrdinalIgnoreCase))
{
if (rgpProjects[i] is IVsHierarchy hierarchy &&
hierarchy.TryGetProject(out var project))
{
UpdateCodeAnalysisRuleSetPropertiesInProject(project, string.Empty);
}
}
}
}
// Second, handle the files that have been deleted. In this case we need to update
// every project that was using this file in some way.
var ruleSetDeletions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < rgpszMkDocuments.Length; i++)
{
var fileFullPath = rgpszMkDocuments[i];
var deleted = (rgFlags[i] & (uint)__VSREMOVEFILEFLAGS2.VSREMOVEFILEFLAGS_IsRemovedFromProjectOnly) == 0;
if (deleted &&
Path.GetExtension(fileFullPath).Equals(".ruleset", StringComparison.OrdinalIgnoreCase))
{
ruleSetDeletions.Add(fileFullPath);
}
}
#pragma warning disable IDE0059 // Unnecessary assignment of a value - https://github.com/dotnet/roslyn/issues/46168
foreach (var fileFullPath in ruleSetDeletions)
#pragma warning restore IDE0059 // Unnecessary assignment of a value
{
UpdateCodeAnalysisRuleSetPropertiesInAllProjects(string.Empty);
}
}
void IVsTrackProjectDocumentsEvents4.OnAfterRemoveDirectoriesEx(int cProjects, int cDirectories, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, uint[] rgFlags)
{
}
private void UpdateCodeAnalysisRuleSetPropertiesInAllProjects(string newFileFullPath)
{
var dte = (EnvDTE.DTE)_serviceProvider.GetService(typeof(SDTE));
foreach (EnvDTE.Project project in dte.Solution.Projects)
{
UpdateCodeAnalysisRuleSetPropertiesInProject(project, newFileFullPath);
}
}
private static void UpdateCodeAnalysisRuleSetPropertiesInProject(EnvDTE.Project project, string newRuleSetFilePath)
{
if (project.Kind is PrjKind.prjKindCSharpProject or
PrjKind.prjKindVBProject)
{
var projectFullName = project.FullName;
if (!string.IsNullOrWhiteSpace(projectFullName))
{
var projectDirectoryFullPath = Path.GetDirectoryName(project.FullName);
foreach (EnvDTE.Configuration config in project.ConfigurationManager)
{
UpdateCodeAnalysisRuleSetPropertyInConfiguration(config, newRuleSetFilePath, projectDirectoryFullPath);
}
}
}
}
private static void UpdateCodeAnalysisRuleSetPropertyInConfiguration(EnvDTE.Configuration config, string newRuleSetFilePath, string projectDirectoryFullPath)
{
var properties = config.Properties;
try
{
var codeAnalysisRuleSetFileProperty = properties?.Item("CodeAnalysisRuleSet");
if (codeAnalysisRuleSetFileProperty != null)
{
var codeAnalysisRuleSetFileName = codeAnalysisRuleSetFileProperty.Value as string;
if (!string.IsNullOrWhiteSpace(codeAnalysisRuleSetFileName))
{
var codeAnalysisRuleSetFullPath = FileUtilities.ResolveRelativePath(codeAnalysisRuleSetFileName, projectDirectoryFullPath);
codeAnalysisRuleSetFullPath = FileUtilities.NormalizeAbsolutePath(codeAnalysisRuleSetFullPath);
var oldRuleSetFilePath = FileUtilities.NormalizeAbsolutePath(codeAnalysisRuleSetFullPath);
if (codeAnalysisRuleSetFullPath.Equals(oldRuleSetFilePath, StringComparison.OrdinalIgnoreCase))
{
var newRuleSetRelativePath = PathUtilities.GetRelativePath(projectDirectoryFullPath, newRuleSetFilePath);
codeAnalysisRuleSetFileProperty.Value = newRuleSetRelativePath;
}
}
}
}
catch (ArgumentException)
{
// Unfortunately the properties collection sometimes throws an ArgumentException
// instead of returning null if the current configuration doesn't support CodeAnalysisRuleSet.
// Ignore it and move on.
}
}
private static int IndexOfFirstDocumentInProject(int projectIndex, int[] firstDocumentIndices)
{
if (projectIndex >= firstDocumentIndices.Length)
{
return firstDocumentIndices.Length;
}
else
{
return firstDocumentIndices[projectIndex];
}
}
}
|