Check msbuild globs before adding file to workspace
This commit is contained in:
@@ -111,7 +111,7 @@ public partial class CustomMsBuildProjectLoader
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ImmutableArray<ProjectInfo>> LoadAsync(CancellationToken cancellationToken)
|
public async Task<(ImmutableArray<ProjectInfo>, Dictionary<ProjectId, ProjectFileInfo>)> LoadAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var results = ImmutableArray.CreateBuilder<ProjectInfo>();
|
var results = ImmutableArray.CreateBuilder<ProjectInfo>();
|
||||||
var processedPaths = new HashSet<string>(PathUtilities.Comparer);
|
var processedPaths = new HashSet<string>(PathUtilities.Comparer);
|
||||||
@@ -150,7 +150,7 @@ public partial class CustomMsBuildProjectLoader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.ToImmutableAndClear();
|
return (results.ToImmutableAndClear(), _projectIdToFileInfoMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ImmutableArray<ProjectFileInfo>> LoadProjectFileInfosAsync(string projectPath, DiagnosticReportingOptions reportingOptions, CancellationToken cancellationToken)
|
private async Task<ImmutableArray<ProjectFileInfo>> LoadProjectFileInfosAsync(string projectPath, DiagnosticReportingOptions reportingOptions, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace SharpIDE.Application.Features.Analysis.ProjectLoader;
|
|||||||
// https://github.com/dotnet/roslyn/blob/main/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
|
// https://github.com/dotnet/roslyn/blob/main/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
|
||||||
public partial class CustomMsBuildProjectLoader(Workspace workspace, ImmutableDictionary<string, string>? properties = null) : MSBuildProjectLoader(workspace, properties)
|
public partial class CustomMsBuildProjectLoader(Workspace workspace, ImmutableDictionary<string, string>? properties = null) : MSBuildProjectLoader(workspace, properties)
|
||||||
{
|
{
|
||||||
public async Task<ImmutableArray<ProjectInfo>> LoadProjectInfosAsync(
|
public async Task<(ImmutableArray<ProjectInfo>, Dictionary<ProjectId, ProjectFileInfo>)> LoadProjectInfosAsync(
|
||||||
List<string> projectFilePaths,
|
List<string> projectFilePaths,
|
||||||
ProjectMap? projectMap = null,
|
ProjectMap? projectMap = null,
|
||||||
IProgress<ProjectLoadProgress>? progress = null,
|
IProgress<ProjectLoadProgress>? progress = null,
|
||||||
@@ -64,7 +64,7 @@ public partial class CustomMsBuildProjectLoader(Workspace workspace, ImmutableDi
|
|||||||
/// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the solution is loaded.</param>
|
/// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the solution is loaded.</param>
|
||||||
/// <param name="msbuildLogger">An optional <see cref="ILogger"/> that will log MSBuild results.</param>
|
/// <param name="msbuildLogger">An optional <see cref="ILogger"/> that will log MSBuild results.</param>
|
||||||
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
|
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
|
||||||
public new async Task<SolutionInfo> LoadSolutionInfoAsync(
|
public new async Task<(SolutionInfo, Dictionary<ProjectId, ProjectFileInfo>)> LoadSolutionInfoAsync(
|
||||||
string solutionFilePath,
|
string solutionFilePath,
|
||||||
IProgress<ProjectLoadProgress>? progress = null,
|
IProgress<ProjectLoadProgress>? progress = null,
|
||||||
ILogger? msbuildLogger = null,
|
ILogger? msbuildLogger = null,
|
||||||
@@ -109,13 +109,15 @@ public partial class CustomMsBuildProjectLoader(Workspace workspace, ImmutableDi
|
|||||||
discoveredProjectOptions: reportingOptions,
|
discoveredProjectOptions: reportingOptions,
|
||||||
preferMetadataForReferencesOfDiscoveredProjects: false);
|
preferMetadataForReferencesOfDiscoveredProjects: false);
|
||||||
|
|
||||||
var projectInfos = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
|
var (projectInfos, projectFileInfos) = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// construct workspace from loaded project infos
|
// construct workspace from loaded project infos
|
||||||
return SolutionInfo.Create(
|
var solutionInfo = SolutionInfo.Create(
|
||||||
SolutionId.CreateNewId(debugName: absoluteSolutionPath),
|
SolutionId.CreateNewId(debugName: absoluteSolutionPath),
|
||||||
version: default,
|
version: default,
|
||||||
absoluteSolutionPath,
|
absoluteSolutionPath,
|
||||||
projectInfos);
|
projectInfos);
|
||||||
|
|
||||||
|
return (solutionInfo, projectFileInfos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ using Microsoft.CodeAnalysis.Remote.Razor.SemanticTokens;
|
|||||||
using Microsoft.CodeAnalysis.Rename;
|
using Microsoft.CodeAnalysis.Rename;
|
||||||
using Microsoft.CodeAnalysis.Shared.Extensions;
|
using Microsoft.CodeAnalysis.Shared.Extensions;
|
||||||
using Microsoft.CodeAnalysis.Text;
|
using Microsoft.CodeAnalysis.Text;
|
||||||
|
using Microsoft.Extensions.FileSystemGlobbing;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NuGet.Frameworks;
|
using NuGet.Frameworks;
|
||||||
using Roslyn.LanguageServer.Protocol;
|
using Roslyn.LanguageServer.Protocol;
|
||||||
@@ -53,6 +54,9 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
|
|||||||
private static HashSet<CodeFixProvider> _codeFixProviders = [];
|
private static HashSet<CodeFixProvider> _codeFixProviders = [];
|
||||||
private static HashSet<CodeRefactoringProvider> _codeRefactoringProviders = [];
|
private static HashSet<CodeRefactoringProvider> _codeRefactoringProviders = [];
|
||||||
|
|
||||||
|
// Primarily used for getting the globs for a project
|
||||||
|
private Dictionary<ProjectId, ProjectFileInfo> _projectFileInfoMap = new();
|
||||||
|
|
||||||
private TaskCompletionSource _solutionLoadedTcs = null!;
|
private TaskCompletionSource _solutionLoadedTcs = null!;
|
||||||
private SharpIdeSolutionModel? _sharpIdeSolutionModel;
|
private SharpIdeSolutionModel? _sharpIdeSolutionModel;
|
||||||
public void StartSolutionAnalysis(SharpIdeSolutionModel solutionModel)
|
public void StartSolutionAnalysis(SharpIdeSolutionModel solutionModel)
|
||||||
@@ -110,7 +114,8 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
|
|||||||
|
|
||||||
// MsBuildProjectLoader doesn't do a restore which is absolutely required for resolving PackageReferences, if they have changed. I am guessing it just reads from project.assets.json
|
// MsBuildProjectLoader doesn't do a restore which is absolutely required for resolving PackageReferences, if they have changed. I am guessing it just reads from project.assets.json
|
||||||
await _buildService.MsBuildAsync(_sharpIdeSolutionModel.FilePath, BuildType.Restore, cancellationToken);
|
await _buildService.MsBuildAsync(_sharpIdeSolutionModel.FilePath, BuildType.Restore, cancellationToken);
|
||||||
var solutionInfo = await _msBuildProjectLoader!.LoadSolutionInfoAsync(_sharpIdeSolutionModel.FilePath, cancellationToken: cancellationToken);
|
var (solutionInfo, projectFileInfos) = await _msBuildProjectLoader!.LoadSolutionInfoAsync(_sharpIdeSolutionModel.FilePath, cancellationToken: cancellationToken);
|
||||||
|
_projectFileInfoMap = projectFileInfos;
|
||||||
_workspace.ClearSolution();
|
_workspace.ClearSolution();
|
||||||
var solution = _workspace.AddSolution(solutionInfo);
|
var solution = _workspace.AddSolution(solutionInfo);
|
||||||
}
|
}
|
||||||
@@ -185,7 +190,8 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
|
|||||||
var __ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.MSBuildProjectLoader.LoadSolutionInfoAsync");
|
var __ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.MSBuildProjectLoader.LoadSolutionInfoAsync");
|
||||||
// This call is the expensive part - MSBuild is slow. There doesn't seem to be any incrementalism for solutions.
|
// This call is the expensive part - MSBuild is slow. There doesn't seem to be any incrementalism for solutions.
|
||||||
// The best we could do to speed it up is do .LoadProjectInfoAsync for the single project, and somehow munge that into the existing solution
|
// The best we could do to speed it up is do .LoadProjectInfoAsync for the single project, and somehow munge that into the existing solution
|
||||||
var newSolutionInfo = await _msBuildProjectLoader.LoadSolutionInfoAsync(_sharpIdeSolutionModel!.FilePath, cancellationToken: cancellationToken);
|
var (newSolutionInfo, projectFileInfos) = await _msBuildProjectLoader.LoadSolutionInfoAsync(_sharpIdeSolutionModel!.FilePath, cancellationToken: cancellationToken);
|
||||||
|
_projectFileInfoMap = projectFileInfos;
|
||||||
__?.Dispose();
|
__?.Dispose();
|
||||||
|
|
||||||
var ___ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.Workspace.OnSolutionReloaded");
|
var ___ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.Workspace.OnSolutionReloaded");
|
||||||
@@ -219,7 +225,11 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
|
|||||||
// This will get all projects necessary to build this group of projects, regardless of whether those projects are actually affected by the original project change
|
// This will get all projects necessary to build this group of projects, regardless of whether those projects are actually affected by the original project change
|
||||||
// We can potentially optimise this, but given this is the expensive part, lets just proceed with reloading them all in the solution
|
// We can potentially optimise this, but given this is the expensive part, lets just proceed with reloading them all in the solution
|
||||||
// We potentially lose performance because Workspace/Solution caches are dropped, but lets not prematurely optimise
|
// We potentially lose performance because Workspace/Solution caches are dropped, but lets not prematurely optimise
|
||||||
var loadedProjectInfos = await _msBuildProjectLoader.LoadProjectInfosAsync(projectPathsToReload, null, cancellationToken: cancellationToken);
|
var (loadedProjectInfos, projectFileInfos) = await _msBuildProjectLoader.LoadProjectInfosAsync(projectPathsToReload, null, cancellationToken: cancellationToken);
|
||||||
|
foreach (var (projectId, projectFileInfo) in projectFileInfos)
|
||||||
|
{
|
||||||
|
_projectFileInfoMap[projectId] = projectFileInfo;
|
||||||
|
}
|
||||||
__?.Dispose();
|
__?.Dispose();
|
||||||
|
|
||||||
var ___ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.Workspace.UpdateSolution");
|
var ___ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.Workspace.UpdateSolution");
|
||||||
@@ -960,12 +970,46 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
|
|||||||
Guard.Against.Null(fileModel, nameof(fileModel));
|
Guard.Against.Null(fileModel, nameof(fileModel));
|
||||||
Guard.Against.Null(content, nameof(content));
|
Guard.Against.Null(content, nameof(content));
|
||||||
|
|
||||||
var project = GetProjectForSharpIdeFile(fileModel);
|
var sharpIdeProject = GetSharpIdeProjectForSharpIdeFile(fileModel);
|
||||||
|
var probableProject = GetProjectForSharpIdeProjectModel(sharpIdeProject);
|
||||||
|
// This file probably belongs to this project, but we need to check its path against the globs for the project to make sure
|
||||||
|
var projectFileInfo = _projectFileInfoMap.GetValueOrDefault(probableProject.Id);
|
||||||
|
Guard.Against.Null(projectFileInfo);
|
||||||
|
var matchers = projectFileInfo.FileGlobs.Select(glob =>
|
||||||
|
{
|
||||||
|
var matcher = new Matcher();
|
||||||
|
matcher.AddIncludePatterns(glob.Includes);
|
||||||
|
matcher.AddExcludePatterns(glob.Excludes);
|
||||||
|
matcher.AddExcludePatterns(glob.Removes);
|
||||||
|
return matcher;
|
||||||
|
});
|
||||||
|
|
||||||
|
var belongsToProject = false;
|
||||||
|
// Check if the file path matches any of the globs in the project file.
|
||||||
|
foreach (var matcher in matchers)
|
||||||
|
{
|
||||||
|
// CPS re-creates the msbuild globs from the includes/excludes/removes and the project XML directory and
|
||||||
|
// ignores the MSBuildGlob.FixedDirectoryPart. We'll do the same here and match using the project directory as the relative path.
|
||||||
|
// See https://devdiv.visualstudio.com/DevDiv/_git/CPS?path=/src/Microsoft.VisualStudio.ProjectSystem/Build/MsBuildGlobFactory.cs
|
||||||
|
var relativeDirectory = sharpIdeProject.DirectoryPath;
|
||||||
|
|
||||||
|
var matches = matcher.Match(relativeDirectory, fileModel.Path);
|
||||||
|
if (matches.HasMatches)
|
||||||
|
{
|
||||||
|
belongsToProject = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (belongsToProject is false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var existingDocument = fileModel switch
|
var existingDocument = fileModel switch
|
||||||
{
|
{
|
||||||
{ IsRazorFile: true } => project.AdditionalDocuments.SingleOrDefault(s => s.FilePath == fileModel.Path),
|
{ IsRazorFile: true } => probableProject.AdditionalDocuments.SingleOrDefault(s => s.FilePath == fileModel.Path),
|
||||||
{ IsCsharpFile: true } => project.Documents.SingleOrDefault(s => s.FilePath == fileModel.Path),
|
{ IsCsharpFile: true } => probableProject.Documents.SingleOrDefault(s => s.FilePath == fileModel.Path),
|
||||||
_ => throw new InvalidOperationException("AddDocument failed: File is not a workspace file")
|
_ => throw new InvalidOperationException("AddDocument failed: File is not a workspace file")
|
||||||
};
|
};
|
||||||
if (existingDocument is not null)
|
if (existingDocument is not null)
|
||||||
@@ -977,8 +1021,8 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
|
|||||||
|
|
||||||
var newSolution = fileModel switch
|
var newSolution = fileModel switch
|
||||||
{
|
{
|
||||||
{ IsRazorFile: true } => _workspace.CurrentSolution.AddAdditionalDocument(DocumentId.CreateNewId(project.Id), fileModel.Name, sourceText, filePath: fileModel.Path),
|
{ IsRazorFile: true } => _workspace.CurrentSolution.AddAdditionalDocument(DocumentId.CreateNewId(probableProject.Id), fileModel.Name, sourceText, filePath: fileModel.Path),
|
||||||
{ IsCsharpFile: true } => _workspace.CurrentSolution.AddDocument(DocumentId.CreateNewId(project.Id), fileModel.Name, sourceText, filePath: fileModel.Path),
|
{ IsCsharpFile: true } => _workspace.CurrentSolution.AddDocument(DocumentId.CreateNewId(probableProject.Id), fileModel.Name, sourceText, filePath: fileModel.Path),
|
||||||
_ => throw new InvalidOperationException("AddDocument failed: File is not in workspace")
|
_ => throw new InvalidOperationException("AddDocument failed: File is not in workspace")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1037,9 +1081,15 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
|
|||||||
return outputPath;
|
return outputPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Project GetProjectForSharpIdeFile(SharpIdeFile sharpIdeFile)
|
private static SharpIdeProjectModel GetSharpIdeProjectForSharpIdeFile(SharpIdeFile sharpIdeFile)
|
||||||
{
|
{
|
||||||
var sharpIdeProjectModel = ((IChildSharpIdeNode)sharpIdeFile).GetNearestProjectNode()!;
|
var sharpIdeProjectModel = ((IChildSharpIdeNode)sharpIdeFile).GetNearestProjectNode()!;
|
||||||
|
return sharpIdeProjectModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Project GetProjectForSharpIdeFile(SharpIdeFile sharpIdeFile)
|
||||||
|
{
|
||||||
|
var sharpIdeProjectModel = GetSharpIdeProjectForSharpIdeFile(sharpIdeFile);
|
||||||
var project = GetProjectForSharpIdeProjectModel(sharpIdeProjectModel);
|
var project = GetProjectForSharpIdeProjectModel(sharpIdeProjectModel);
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user