| File: CodeRefactorings\SyncNamespace\AbstractSyncNamespaceCodeRefactoringProvider.MoveFileCodeAction.cs | Web Access |
| Project: src\roslyn\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features) |
// 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. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CodeRefactorings.SyncNamespace; internal abstract partial class AbstractSyncNamespaceCodeRefactoringProvider<TNamespaceDeclarationSyntax, TCompilationUnitSyntax, TMemberDeclarationSyntax> : CodeRefactoringProvider where TNamespaceDeclarationSyntax : SyntaxNode where TCompilationUnitSyntax : SyntaxNode where TMemberDeclarationSyntax : SyntaxNode { private sealed class MoveFileCodeAction(State state, ImmutableArray<string> newFolders) : CodeAction { private readonly State _state = state; private readonly ImmutableArray<string> _newfolders = newFolders; public override string Title => _newfolders.Length > 0 ? string.Format(FeaturesResources.Move_file_to_0, string.Join(PathUtilities.DirectorySeparatorStr, _newfolders)) : FeaturesResources.Move_file_to_project_root_folder; protected override async Task<ImmutableArray<CodeActionOperation>> ComputeOperationsAsync( IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken) { var document = _state.Document; var solution = _state.Document.Project.Solution; var newDocumentId = DocumentId.CreateNewId(document.Project.Id, document.Name); solution = solution.RemoveDocument(document.Id); var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); solution = solution.AddDocument(newDocumentId, document.Name, text, folders: _newfolders); return [new ApplyChangesOperation(solution), new OpenDocumentOperation(newDocumentId, activateIfAlreadyOpen: true)]; } public static ImmutableArray<MoveFileCodeAction> Create(State state) { Debug.Assert(state.RelativeDeclaredNamespace != null); // Since all documents have identical folder structure, we can do the computation on any of them. var document = state.Document; // In case the relative namespace is "", the file should be moved to project root, // set `parts` to empty to indicate that. var parts = state.RelativeDeclaredNamespace.Length == 0 ? [] : state.RelativeDeclaredNamespace.Split(['.']).ToImmutableArray(); // Invalid char can only appear in namespace name when there's error, // which we have checked before creating any code actions. Debug.Assert(parts.IsEmpty || parts.Any(static s => s.IndexOfAny(Path.GetInvalidPathChars()) < 0)); var projectRootFolder = FolderInfo.CreateFolderHierarchyForProject(document.Project); var candidateFolders = FindCandidateFolders(projectRootFolder, parts, []); return candidateFolders.SelectAsArray(folders => new MoveFileCodeAction(state, folders)); } /// <summary> /// We try to provide additional "move file" options if we can find existing folders that matches target namespace. /// For example, if the target namespace is 'DefaultNamesapce.A.B.C', and there's a folder 'ProjectRoot\A.B\' already /// exists, then will provide two actions, "move file to ProjectRoot\A.B\C\" and "move file to ProjectRoot\A\B\C\". /// </summary> private static ImmutableArray<ImmutableArray<string>> FindCandidateFolders( FolderInfo currentFolderInfo, ImmutableArray<string> parts, ImmutableArray<string> currentFolder) { if (parts.IsEmpty) { return [currentFolder]; } // Try to figure out all possible folder names that can match the target namespace. // For example, if the target is "A.B.C", then the matching folder names include // "A", "A.B" and "A.B.C". The item "index" in the result tuple is the number // of items in namespace parts used to construct iten "foldername". var candidates = Enumerable.Range(1, parts.Length) .Select(i => (foldername: string.Join(".", parts.Take(i)), index: i)) .ToImmutableDictionary(t => t.foldername, t => t.index, PathUtilities.Comparer); var subFolders = currentFolderInfo.ChildFolders; var builder = ArrayBuilder<ImmutableArray<string>>.GetInstance(); foreach (var (folderName, index) in candidates) { if (subFolders.TryGetValue(folderName, out var matchingFolderInfo)) { var newParts = index >= parts.Length ? [] : ImmutableArray.Create(parts, index, parts.Length - index); var newCurrentFolder = currentFolder.Add(matchingFolderInfo.Name); builder.AddRange(FindCandidateFolders(matchingFolderInfo, newParts, newCurrentFolder)); } } // Make sure we always have the default path as an available option to the user // (which might have been found by the search above, therefore the check here) // For example, if the target namespace is "A.B.C.D", and there's folder <ROOT>\A.B\, // the search above would only return "<ROOT>\A.B\C\D". We'd want to provide // "<ROOT>\A\B\C\D" as the default path. var defaultPathBasedOnCurrentFolder = currentFolder.AddRange(parts); if (builder.All(folders => !folders.SequenceEqual(defaultPathBasedOnCurrentFolder, PathUtilities.Comparer))) { builder.Add(defaultPathBasedOnCurrentFolder); } return builder.ToImmutableAndFree(); } private sealed class FolderInfo { private readonly Dictionary<string, FolderInfo> _childFolders; public string Name { get; } public IReadOnlyDictionary<string, FolderInfo> ChildFolders => _childFolders; private FolderInfo(string name) { Name = name; _childFolders = new Dictionary<string, FolderInfo>(StringComparer.Ordinal); } private void AddFolder(IEnumerable<string> folder) { if (!folder.Any()) { return; } var firstFolder = folder.First(); if (!_childFolders.TryGetValue(firstFolder, out var firstFolderInfo)) { firstFolderInfo = new FolderInfo(firstFolder); _childFolders[firstFolder] = firstFolderInfo; } firstFolderInfo.AddFolder(folder.Skip(1)); } // TODO: // Since we are getting folder data from documents, only non-empty folders // in the project are discovered. It's possible to get complete folder structure // from VS but it requires UI thread to do so. We might want to revisit this later. public static FolderInfo CreateFolderHierarchyForProject(Project project) { var handledFolders = new HashSet<string>(StringComparer.Ordinal); var rootFolderInfo = new FolderInfo("<ROOT>"); foreach (var document in project.Documents) { var folders = document.Folders; if (handledFolders.Add(string.Join(PathUtilities.DirectorySeparatorStr, folders))) { rootFolderInfo.AddFolder(folders); } } return rootFolderInfo; } } } }