From ad6a83275eff7165ff9640f2966a49093ff05f85 Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Fri, 1 Dec 2023 16:20:37 -0800 Subject: [PATCH] Pass through folders for additional files Our workspace model always allowed this at the document level, we just never had a request for it until recently for some XAML support. This adds support for legacy project systems in VS and the base C# extension project system. Changes will need to be made in other repos for the CPS-based project systems to consume the APIs being added here. Partially fixes https://github.com/dotnet/roslyn/issues/65589 --- .../WorkspaceProjectFactoryServiceTests.cs | 15 ++++++++++----- .../HostWorkspace/LoadedProject.cs | 4 +--- .../HostWorkspace/WorkspaceProject.cs | 9 +++++++++ .../BrokeredService/WorkspaceProject.cs | 9 +++++++++ .../CPS/IWorkspaceProjectContext.cs | 2 ++ .../Legacy/AbstractLegacyProject.cs | 19 ++++++++++++------- .../AbstractLegacyProject_IAnalyzerHost.cs | 2 +- .../CPSProject_IWorkspaceProjectContext.cs | 3 +++ .../ProjectSystem/ProjectSystemProject.cs | 4 ++-- .../Core/ProjectSystem/IWorkspaceProject.cs | 2 ++ 10 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs index e575d0879e230..2b5aac043fcc2 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs @@ -4,7 +4,6 @@ using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; -using Microsoft.CodeAnalysis.LanguageServer.Logging; using Microsoft.CodeAnalysis.Remote.ProjectSystem; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Shell.ServiceBroker; @@ -37,14 +36,20 @@ public async Task CreateProjectAndBatch() var sourceFilePath = MakeAbsolutePath("SourceFile.cs"); var additionalFilePath = MakeAbsolutePath("AdditionalFile.txt"); - await workspaceProject.AddSourceFilesAsync(new[] { new SourceFileInfo(sourceFilePath, Array.Empty()) }, CancellationToken.None); - await workspaceProject.AddAdditionalFilesAsync(new[] { additionalFilePath }, CancellationToken.None); + await workspaceProject.AddSourceFilesAsync([new SourceFileInfo(sourceFilePath, ["Folder"])], CancellationToken.None); + await workspaceProject.AddAdditionalFilesAsync([new SourceFileInfo(additionalFilePath, FolderNames: ["Folder"])], CancellationToken.None); await batch.ApplyAsync(CancellationToken.None); // Verify it actually did something; we won't exclusively test each method since those are tested at lower layers var project = workspaceFactory.Workspace.CurrentSolution.Projects.Single(); - Assert.Equal(sourceFilePath, project.Documents.Single().FilePath); - Assert.Equal(additionalFilePath, project.AdditionalDocuments.Single().FilePath); + + var document = Assert.Single(project.Documents); + Assert.Equal(sourceFilePath, document.FilePath); + Assert.Equal("Folder", Assert.Single(document.Folders)); + + var additionalDocument = Assert.Single(project.AdditionalDocuments); + Assert.Equal(additionalFilePath, additionalDocument.FilePath); + Assert.Equal("Folder", Assert.Single(additionalDocument.Folders)); } private static string MakeAbsolutePath(string relativePath) diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs index 9bcbca26a22d4..5ccba70025f1f 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs @@ -3,10 +3,8 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; -using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.ProjectSystem; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; @@ -163,7 +161,7 @@ public void Dispose() newProjectInfo.AdditionalDocuments, _mostRecentFileInfo?.AdditionalDocuments, DocumentFileInfoComparer.Instance, - document => _projectSystemProject.AddAdditionalFile(document.FilePath), + document => _projectSystemProject.AddAdditionalFile(document.FilePath, folders: document.Folders), document => _projectSystemProject.RemoveAdditionalFile(document.FilePath), "Project {0} now has {1} additional file(s)."); diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs index 5367e67e18719..ba1da2fa0461d 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs @@ -24,6 +24,7 @@ public WorkspaceProject(ProjectSystemProject project, SolutionServices solutionS _targetFrameworkManager = targetFrameworkManager; } + [Obsolete($"Call the {nameof(AddAdditionalFilesAsync)} overload that takes {nameof(SourceFileInfo)}.")] public async Task AddAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken _) { await using var batchScope = _project.CreateBatchScope(); @@ -32,6 +33,14 @@ public async Task AddAdditionalFilesAsync(IReadOnlyList additionalFilePa _project.AddAdditionalFile(additionalFilePath); } + public async Task AddAdditionalFilesAsync(IReadOnlyList additionalFiles, CancellationToken cancellationToken) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var additionalFile in additionalFiles) + _project.AddAdditionalFile(additionalFile.FilePath, folders: additionalFile.FolderNames.ToImmutableArray()); + } + public async Task AddAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken _) { await using var batchScope = _project.CreateBatchScope(); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs index f789f0eacd8b6..2a028acc93c15 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs @@ -29,6 +29,7 @@ public void Dispose() _project.Dispose(); } + [Obsolete($"Call the {nameof(AddAdditionalFilesAsync)} overload that takes {nameof(SourceFileInfo)}.")] public async Task AddAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken) { await using var batch = _project.CreateBatchScope().ConfigureAwait(false); @@ -37,6 +38,14 @@ public async Task AddAdditionalFilesAsync(IReadOnlyList additionalFilePa _project.AddAdditionalFile(additionalFilePath); } + public async Task AddAdditionalFilesAsync(IReadOnlyList additionalFiles, CancellationToken cancellationToken) + { + await using var batchScope = _project.CreateBatchScope().ConfigureAwait(false); + + foreach (var additionalFile in additionalFiles) + _project.AddAdditionalFile(additionalFile.FilePath, folderNames: additionalFile.FolderNames.ToImmutableArray()); + } + public async Task RemoveAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken) { await using var batch = _project.CreateBatchScope().ConfigureAwait(false); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs b/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs index 401225c41b2b9..a9daef45804a7 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs @@ -54,7 +54,9 @@ internal interface IWorkspaceProjectContext : IDisposable // Files. void AddSourceFile(string filePath, bool isInCurrentContext = true, IEnumerable? folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular); void RemoveSourceFile(string filePath); + [Obsolete($"Call the {nameof(AddAdditionalFile)} method that takes folder names.")] void AddAdditionalFile(string filePath, bool isInCurrentContext = true); + void AddAdditionalFile(string filePath, IEnumerable folderNames, bool isInCurrentContext = true); void RemoveAdditionalFile(string filePath); void AddDynamicFile(string filePath, IEnumerable? folderNames = null); void RemoveDynamicFile(string filePath); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject.cs b/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject.cs index 3bfa9acab151c..b5369fcf98154 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject.cs @@ -183,13 +183,7 @@ protected void AddFile( return; } - ImmutableArray folders = default; - - var itemid = Hierarchy.TryGetItemId(filename); - if (itemid != VSConstants.VSITEMID_NIL) - { - folders = GetFolderNamesForDocument(itemid); - } + var folders = GetFolderNamesForDocument(filename); ProjectSystemProject.AddSourceFile(filename, sourceCodeKind, folders); } @@ -309,6 +303,17 @@ private static Guid GetProjectIDGuid(IVsHierarchy hierarchy) /// IVsHierarchy providers, but this code (which is fairly old) still makes the assumptions anyways. private readonly Dictionary> _folderNameMap = new(); + private ImmutableArray GetFolderNamesForDocument(string filename) + { + var itemid = Hierarchy.TryGetItemId(filename); + if (itemid != VSConstants.VSITEMID_NIL) + { + return GetFolderNamesForDocument(itemid); + } + + return default; + } + private ImmutableArray GetFolderNamesForDocument(uint documentItemID) { AssertIsForeground(); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject_IAnalyzerHost.cs b/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject_IAnalyzerHost.cs index e67eed2bf0896..c04b42c506840 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject_IAnalyzerHost.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject_IAnalyzerHost.cs @@ -36,7 +36,7 @@ void IAnalyzerHost.SetRuleSetFile(string ruleSetFileFullPath) } void IAnalyzerHost.AddAdditionalFile(string additionalFilePath) - => ProjectSystemProject.AddAdditionalFile(additionalFilePath); + => ProjectSystemProject.AddAdditionalFile(additionalFilePath, folders: GetFolderNamesForDocument(additionalFilePath)); void IAnalyzerHost.RemoveAdditionalFile(string additionalFilePath) => ProjectSystemProject.RemoveAdditionalFile(additionalFilePath); diff --git a/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs b/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs index 97aef6ae8ff3b..55ae78713d580 100644 --- a/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs +++ b/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs @@ -235,6 +235,9 @@ public void RemoveSourceFile(string filePath) public void AddAdditionalFile(string filePath, bool isInCurrentContext = true) => _projectSystemProject.AddAdditionalFile(filePath); + public void AddAdditionalFile(string filePath, IEnumerable folderNames, bool isInCurrentContext = true) + => _projectSystemProject.AddAdditionalFile(filePath, folders: folderNames.ToImmutableArray()); + public void Dispose() { _projectCodeModel?.OnProjectClosed(); diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs index 51cf73f5f7936..008d6065f2571 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs @@ -719,8 +719,8 @@ public void RemoveSourceTextContainer(SourceTextContainer textContainer) #region Additional File Addition/Removal // TODO: should AdditionalFiles have source code kinds? - public void AddAdditionalFile(string fullPath, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular) - => _additionalFiles.AddFile(fullPath, sourceCodeKind, folders: default); + public void AddAdditionalFile(string fullPath, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, ImmutableArray folders = default) + => _additionalFiles.AddFile(fullPath, sourceCodeKind, folders); public bool ContainsAdditionalFile(string fullPath) => _additionalFiles.ContainsFile(fullPath); diff --git a/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProject.cs b/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProject.cs index 4e6ab5f9fbefc..87f60be46f2a5 100644 --- a/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProject.cs +++ b/src/Workspaces/Remote/Core/ProjectSystem/IWorkspaceProject.cs @@ -24,7 +24,9 @@ internal interface IWorkspaceProject : IDisposable Task AddMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken cancellationToken); Task RemoveMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken cancellationToken); + [Obsolete($"Call the {nameof(AddAdditionalFilesAsync)} overload that takes {nameof(SourceFileInfo)}.")] Task AddAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken); + Task AddAdditionalFilesAsync(IReadOnlyList additionalFiles, CancellationToken cancellationToken); Task RemoveAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken cancellationToken); Task AddAnalyzerReferencesAsync(IReadOnlyList analyzerPaths, CancellationToken cancellationToken);