From 7b2b173823fe121fcc53fd71d6e646bb3d891566 Mon Sep 17 00:00:00 2001 From: Max Doerner Date: Sat, 21 Mar 2020 01:26:48 +0100 Subject: [PATCH] Add MoveContainingFolderRefactoring It moves the containing folder into another folder. Also fixes ToVbaStringLiteral. (It was missing the surrounding quotes.) --- .../CodeExplorerCustomFolderViewModel.cs | 6 +- .../Folders/DeclarationFolderExtensions.cs | 49 +++++ .../Navigation/Folders/FolderExtensions.cs | 78 ------- Rubberduck.Core/Rubberduck.Core.csproj | 3 + ...ctorMoveContainingFolderCommandMenuItem.cs | 22 ++ .../ParentMenus/RefactoringsParentMenu.cs | 1 + ...PaneRefactorMoveContainingFolderCommand.cs | 42 ++++ ...ntainingFolderRefactoringFailedNotifier.cs | 38 ++++ .../MoveToFolderRefactoringFailedNotifier.cs | 10 +- .../MoveMultipleFoldersPresenter.cs | 15 ++ .../MoveFolder/MoveMultipleFoldersView.xaml | 77 +++++++ .../MoveMultipleFoldersView.xaml.cs | 19 ++ .../MoveMultipleFoldersViewModel.cs | 73 +++++++ .../Extensions/FolderExtensions.cs | 60 ++++++ .../Output/StringExtensions.cs | 2 +- .../Root/RubberduckIoCInstaller.cs | 3 +- .../AffectedModuleIsStaleException.cs | 14 ++ .../MoveToFolder/NoTargetFolderException.cs | 5 + .../IMoveMultipleFoldersPresenter.cs | 5 + .../MoveContainingFolderRefactoring.cs | 87 ++++++++ .../MoveFolder/MoveFolderModel.cs | 19 ++ .../MoveFolder/MoveFolderRefactoringAction.cs | 34 +++ .../MoveFolder/MoveMultipleFoldersModel.cs | 17 ++ .../MoveMultipleFoldersRefactoringAction.cs | 26 +++ .../MoveToFolder/MoveToFolderRefactoring.cs | 23 +- .../Menus/RubberduckMenus.Designer.cs | 9 + .../Menus/RubberduckMenus.de.resx | 3 + .../Menus/RubberduckMenus.resx | 3 + Rubberduck.Resources/RubberduckUI.Designer.cs | 65 +++++- Rubberduck.Resources/RubberduckUI.de.resx | 21 ++ Rubberduck.Resources/RubberduckUI.resx | 26 ++- .../CodeExplorerCustomFolderViewModelTests.cs | 11 +- .../CodeExplorer/CodeExplorerTestSetup.cs | 3 +- ...eMultipleToFolderRefactoringActionTests.cs | 113 ++++++++++ .../MoveToFolderRefactoringActionTests.cs | 204 ++++++++++++++++++ ...eMultipleToFolderRefactoringActionTests.cs | 2 +- .../MoveToFolderRefactoringActionTests.cs | 2 +- 37 files changed, 1095 insertions(+), 95 deletions(-) create mode 100644 Rubberduck.Core/Navigation/Folders/DeclarationFolderExtensions.cs delete mode 100644 Rubberduck.Core/Navigation/Folders/FolderExtensions.cs create mode 100644 Rubberduck.Core/UI/Command/MenuItems/CodePaneRefactorMoveContainingFolderCommandMenuItem.cs create mode 100644 Rubberduck.Core/UI/Command/Refactorings/CodePaneRefactorMoveContainingFolderCommand.cs create mode 100644 Rubberduck.Core/UI/Command/Refactorings/Notifiers/MoveContainingFolderRefactoringFailedNotifier.cs create mode 100644 Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersPresenter.cs create mode 100644 Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersView.xaml create mode 100644 Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersView.xaml.cs create mode 100644 Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersViewModel.cs create mode 100644 Rubberduck.JunkDrawer/Extensions/FolderExtensions.cs create mode 100644 Rubberduck.Refactorings/Exceptions/AffectedModuleIsStaleException.cs create mode 100644 Rubberduck.Refactorings/Exceptions/MoveToFolder/NoTargetFolderException.cs create mode 100644 Rubberduck.Refactorings/MoveFolder/IMoveMultipleFoldersPresenter.cs create mode 100644 Rubberduck.Refactorings/MoveFolder/MoveContainingFolderRefactoring.cs create mode 100644 Rubberduck.Refactorings/MoveFolder/MoveFolderModel.cs create mode 100644 Rubberduck.Refactorings/MoveFolder/MoveFolderRefactoringAction.cs create mode 100644 Rubberduck.Refactorings/MoveFolder/MoveMultipleFoldersModel.cs create mode 100644 Rubberduck.Refactorings/MoveFolder/MoveMultipleFoldersRefactoringAction.cs create mode 100644 RubberduckTests/Refactoring/MoveFolders/MoveMultipleToFolderRefactoringActionTests.cs create mode 100644 RubberduckTests/Refactoring/MoveFolders/MoveToFolderRefactoringActionTests.cs rename RubberduckTests/Refactoring/{ => MoveToFolder}/MoveMultipleToFolderRefactoringActionTests.cs (98%) rename RubberduckTests/Refactoring/{ => MoveToFolder}/MoveToFolderRefactoringActionTests.cs (98%) diff --git a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerCustomFolderViewModel.cs b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerCustomFolderViewModel.cs index dd840d0749..32daa5da88 100644 --- a/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerCustomFolderViewModel.cs +++ b/Rubberduck.Core/Navigation/CodeExplorer/CodeExplorerCustomFolderViewModel.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Rubberduck.Common; +using Rubberduck.JunkDrawer.Extensions; using Rubberduck.Navigation.Folders; using Rubberduck.Parsing.Symbols; using Rubberduck.VBEditor; @@ -30,7 +32,7 @@ public sealed class CodeExplorerCustomFolderViewModel : CodeExplorerItemViewMode { _vbe = vbe; FolderDepth = parent is CodeExplorerCustomFolderViewModel folder ? folder.FolderDepth + 1 : 1; - FullPath = fullPath?.Trim('"') ?? string.Empty; + FullPath = fullPath?.FromVbaStringLiteral() ?? string.Empty; Name = name.Replace("\"", string.Empty); AddNewChildren(ref declarations); @@ -44,7 +46,7 @@ public sealed class CodeExplorerCustomFolderViewModel : CodeExplorerItemViewMode public string FullPath { get; } - public string FolderAttribute => $"'@Folder(\"{FullPath.Replace("\"", string.Empty)}\")"; + public string FolderAttribute => $"'@Folder({FullPath.ToVbaStringLiteral()})"; /// /// One-based depth in the folder hierarchy. diff --git a/Rubberduck.Core/Navigation/Folders/DeclarationFolderExtensions.cs b/Rubberduck.Core/Navigation/Folders/DeclarationFolderExtensions.cs new file mode 100644 index 0000000000..5a4e17697a --- /dev/null +++ b/Rubberduck.Core/Navigation/Folders/DeclarationFolderExtensions.cs @@ -0,0 +1,49 @@ +using System; +using Rubberduck.Parsing.Symbols; +using Rubberduck.JunkDrawer.Extensions; + +namespace Rubberduck.Navigation.Folders +{ + public static class DeclarationFolderExtensions + { + public static string RootFolder(this Declaration declaration) + { + return declaration?.CustomFolder?.RootFolder() + ?? declaration?.ProjectName + ?? string.Empty; + } + + public static bool IsInFolder(this Declaration declaration, string folder) + { + if (declaration?.CustomFolder is null || folder is null) + { + return false; + } + + return declaration.CustomFolder.Equals(folder, StringComparison.Ordinal); + } + + public static bool IsInSubFolder(this Declaration declaration, string folder) + { + var declarationFolder = declaration?.CustomFolder; + if (declarationFolder is null || folder is null) + { + return false; + } + + return declarationFolder.IsSubFolderOf(folder); + } + + public static bool IsInFolderOrSubFolder(this Declaration declaration, string folder) + { + var declarationFolder = declaration?.CustomFolder; + if (declarationFolder is null || folder is null) + { + return false; + } + + return declaration.IsInFolder(folder) + || declarationFolder.IsSubFolderOf(folder); + } + } +} diff --git a/Rubberduck.Core/Navigation/Folders/FolderExtensions.cs b/Rubberduck.Core/Navigation/Folders/FolderExtensions.cs deleted file mode 100644 index e4e2db6391..0000000000 --- a/Rubberduck.Core/Navigation/Folders/FolderExtensions.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Linq; -using Rubberduck.Parsing.Symbols; - -namespace Rubberduck.Navigation.Folders -{ - public static class FolderExtensions - { - public const char FolderDelimiter = '.'; - - public static string RootFolder(this Declaration declaration) - { - return (declaration?.CustomFolder ?? string.Empty).Split(FolderDelimiter).FirstOrDefault() - ?? declaration?.ProjectName - ?? string.Empty; - } - - public static string SubFolderRoot(this string folder, string subfolder) - { - if (folder is null || subfolder is null || !folder.StartsWith(subfolder)) - { - return string.Empty; - } - - var subPath = folder.Substring(subfolder.Length + 1); - return subPath.Split(FolderDelimiter).FirstOrDefault() ?? string.Empty; - } - - public static bool IsInFolder(this Declaration declaration, string folder) - { - if (declaration?.CustomFolder is null || folder is null) - { - return false; - } - - return declaration.CustomFolder.Equals(folder, StringComparison.Ordinal); - } - - public static bool IsInSubFolder(this Declaration declaration, string folder) - { - if (declaration?.CustomFolder is null || folder is null) - { - return false; - } - - var folderPath = folder.Split(FolderDelimiter); - var declarationPath = declaration.CustomFolder.Split(FolderDelimiter); - - if (folderPath.Length >= declarationPath.Length) - { - return false; - } - - return declarationPath.Take(folderPath.Length).SequenceEqual(folderPath, StringComparer.Ordinal); - } - - public static bool IsInFolderOrSubFolder(this Declaration declaration, string folder) - { - if (declaration?.CustomFolder is null || folder is null) - { - return false; - } - - var folderPath = folder.Split(FolderDelimiter); - var declarationPath = declaration.CustomFolder.Split(FolderDelimiter); - - for (var depth = 0; depth < folderPath.Length && depth < declarationPath.Length; depth++) - { - if (!folderPath[depth].Equals(declarationPath[depth], StringComparison.Ordinal)) - { - return false; - } - } - - return true; - } - } -} diff --git a/Rubberduck.Core/Rubberduck.Core.csproj b/Rubberduck.Core/Rubberduck.Core.csproj index 09c8570226..4078ea58d7 100644 --- a/Rubberduck.Core/Rubberduck.Core.csproj +++ b/Rubberduck.Core/Rubberduck.Core.csproj @@ -117,6 +117,9 @@ True True + + MoveMultipleFoldersView.xaml + MoveMultipleToFolderView.xaml diff --git a/Rubberduck.Core/UI/Command/MenuItems/CodePaneRefactorMoveContainingFolderCommandMenuItem.cs b/Rubberduck.Core/UI/Command/MenuItems/CodePaneRefactorMoveContainingFolderCommandMenuItem.cs new file mode 100644 index 0000000000..3315ba4749 --- /dev/null +++ b/Rubberduck.Core/UI/Command/MenuItems/CodePaneRefactorMoveContainingFolderCommandMenuItem.cs @@ -0,0 +1,22 @@ +using Rubberduck.Parsing.VBA; +using Rubberduck.UI.Command.MenuItems.ParentMenus; +using Rubberduck.UI.Command.Refactorings; + +namespace Rubberduck.UI.Command.MenuItems +{ + public class CodePaneRefactorMoveContainingFolderCommandMenuItem : CommandMenuItemBase + { + public CodePaneRefactorMoveContainingFolderCommandMenuItem(CodePaneRefactorMoveContainingFolderCommand command) + : base(command) + {} + + public override string Key => "RefactorMenu_MoveContainingFolder"; + public override int DisplayOrder => (int)RefactoringsMenuItemDisplayOrder.MoveContainingFolder; + public override bool BeginGroup => false; + + public override bool EvaluateCanExecute(RubberduckParserState state) + { + return state != null && Command.CanExecute(null); + } + } +} diff --git a/Rubberduck.Core/UI/Command/MenuItems/ParentMenus/RefactoringsParentMenu.cs b/Rubberduck.Core/UI/Command/MenuItems/ParentMenus/RefactoringsParentMenu.cs index fb82caa586..30b2d6951c 100644 --- a/Rubberduck.Core/UI/Command/MenuItems/ParentMenus/RefactoringsParentMenu.cs +++ b/Rubberduck.Core/UI/Command/MenuItems/ParentMenus/RefactoringsParentMenu.cs @@ -25,6 +25,7 @@ public enum RefactoringsMenuItemDisplayOrder IntroduceParameter, IntroduceField, MoveToFolder, + MoveContainingFolder, AddRemoveReferences } } diff --git a/Rubberduck.Core/UI/Command/Refactorings/CodePaneRefactorMoveContainingFolderCommand.cs b/Rubberduck.Core/UI/Command/Refactorings/CodePaneRefactorMoveContainingFolderCommand.cs new file mode 100644 index 0000000000..e9bdef52eb --- /dev/null +++ b/Rubberduck.Core/UI/Command/Refactorings/CodePaneRefactorMoveContainingFolderCommand.cs @@ -0,0 +1,42 @@ +using Rubberduck.Parsing.Symbols; +using Rubberduck.Parsing.VBA; +using Rubberduck.Refactorings.MoveFolder; +using Rubberduck.UI.Command.Refactorings.Notifiers; +using Rubberduck.VBEditor.Utility; + +namespace Rubberduck.UI.Command.Refactorings +{ + public class CodePaneRefactorMoveContainingFolderCommand : RefactorCodePaneCommandBase + { + private readonly RubberduckParserState _state; + private readonly ISelectedDeclarationProvider _selectedDeclarationProvider; + + public CodePaneRefactorMoveContainingFolderCommand( + MoveContainingFolderRefactoring refactoring, + MoveContainingFolderRefactoringFailedNotifier failureNotifier, + ISelectionProvider selectionProvider, + RubberduckParserState state, + ISelectedDeclarationProvider selectedDeclarationProvider) + : base(refactoring, failureNotifier, selectionProvider, state) + { + _selectedDeclarationProvider = selectedDeclarationProvider; + _state = state; + + AddToCanExecuteEvaluation(SpecializedEvaluateCanExecute); + } + + private bool SpecializedEvaluateCanExecute(object parameter) + { + var target = GetTarget(); + + return target != null + && target is ModuleDeclaration + && !_state.IsNewOrModified(target.QualifiedModuleName); + } + + private Declaration GetTarget() + { + return _selectedDeclarationProvider.SelectedModule(); + } + } +} \ No newline at end of file diff --git a/Rubberduck.Core/UI/Command/Refactorings/Notifiers/MoveContainingFolderRefactoringFailedNotifier.cs b/Rubberduck.Core/UI/Command/Refactorings/Notifiers/MoveContainingFolderRefactoringFailedNotifier.cs new file mode 100644 index 0000000000..32877956e9 --- /dev/null +++ b/Rubberduck.Core/UI/Command/Refactorings/Notifiers/MoveContainingFolderRefactoringFailedNotifier.cs @@ -0,0 +1,38 @@ +using Rubberduck.Interaction; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Refactorings.Exceptions; +using Rubberduck.Refactorings.Exceptions.MoveToFolder; + +namespace Rubberduck.UI.Command.Refactorings.Notifiers +{ + public class MoveContainingFolderRefactoringFailedNotifier : RefactoringFailureNotifierBase + { + public MoveContainingFolderRefactoringFailedNotifier(IMessageBox messageBox) + : base(messageBox) + {} + + protected override string Caption => Resources.RubberduckUI.MoveToFolderDialog_Caption; + + protected override string Message(RefactoringException exception) + { + switch (exception) + { + case InvalidDeclarationTypeException invalidDeclarationType: + Logger.Warn(invalidDeclarationType); + return string.Format( + Resources.RubberduckUI.RefactoringFailure_InvalidDeclarationType, + invalidDeclarationType.TargetDeclaration.QualifiedName, + invalidDeclarationType.TargetDeclaration.DeclarationType, + DeclarationType.Module); + case NoTargetFolderException noTargetFolder: + return Resources.RubberduckUI.RefactoringFailure_NoTargetFolder; + case AffectedModuleIsStaleException affectedModuleIsStale: + return string.Format( + Resources.RubberduckUI.RefactoringFailure_AffectedModuleIsStale, + affectedModuleIsStale.StaleModule.ToString()); + default: + return base.Message(exception); + } + } + } +} \ No newline at end of file diff --git a/Rubberduck.Core/UI/Command/Refactorings/Notifiers/MoveToFolderRefactoringFailedNotifier.cs b/Rubberduck.Core/UI/Command/Refactorings/Notifiers/MoveToFolderRefactoringFailedNotifier.cs index 2dfd0ff851..b38e9fbf47 100644 --- a/Rubberduck.Core/UI/Command/Refactorings/Notifiers/MoveToFolderRefactoringFailedNotifier.cs +++ b/Rubberduck.Core/UI/Command/Refactorings/Notifiers/MoveToFolderRefactoringFailedNotifier.cs @@ -1,6 +1,7 @@ using Rubberduck.Interaction; using Rubberduck.Parsing.Symbols; using Rubberduck.Refactorings.Exceptions; +using Rubberduck.Refactorings.Exceptions.MoveToFolder; namespace Rubberduck.UI.Command.Refactorings.Notifiers { @@ -18,10 +19,17 @@ protected override string Message(RefactoringException exception) { case InvalidDeclarationTypeException invalidDeclarationType: Logger.Warn(invalidDeclarationType); - return string.Format(Resources.RubberduckUI.RefactoringFailure_InvalidDeclarationType, + return string.Format( + Resources.RubberduckUI.RefactoringFailure_InvalidDeclarationType, invalidDeclarationType.TargetDeclaration.QualifiedName, invalidDeclarationType.TargetDeclaration.DeclarationType, DeclarationType.Module); + case NoTargetFolderException noTargetFolder: + return Resources.RubberduckUI.RefactoringFailure_NoTargetFolder; + case AffectedModuleIsStaleException affectedModuleIsStale: + return string.Format( + Resources.RubberduckUI.RefactoringFailure_AffectedModuleIsStale, + affectedModuleIsStale.StaleModule.ToString()); default: return base.Message(exception); } diff --git a/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersPresenter.cs b/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersPresenter.cs new file mode 100644 index 0000000000..726f4b3af6 --- /dev/null +++ b/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersPresenter.cs @@ -0,0 +1,15 @@ +using Rubberduck.Refactorings.MoveFolder; +using Rubberduck.Resources; + +namespace Rubberduck.UI.Refactorings.MoveFolder +{ + internal class MoveMultipleFoldersPresenter : RefactoringPresenterBase, IMoveMultipleFoldersPresenter + { + private static readonly DialogData DialogData = DialogData.Create(RubberduckUI.MoveToFolderDialog_Caption, 164, 684); + + public MoveMultipleFoldersPresenter(MoveMultipleFoldersModel model, IRefactoringDialogFactory dialogFactory) : + base(DialogData, model, dialogFactory) + {} + } +} + diff --git a/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersView.xaml b/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersView.xaml new file mode 100644 index 0000000000..ea8414b728 --- /dev/null +++ b/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersView.xaml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersView.xaml.cs b/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersView.xaml.cs new file mode 100644 index 0000000000..3ce321f159 --- /dev/null +++ b/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersView.xaml.cs @@ -0,0 +1,19 @@ +using Rubberduck.Refactorings; +using Rubberduck.Refactorings.MoveFolder; + +namespace Rubberduck.UI.Refactorings.MoveFolder +{ + public partial class MoveMultipleFoldersView : IRefactoringView + { + public MoveMultipleFoldersView() + { + InitializeComponent(); + + Loaded += (o, e) => + { + MoveToFolderTextBox.Focus(); + MoveToFolderTextBox.SelectAll(); + }; + } + } +} diff --git a/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersViewModel.cs b/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersViewModel.cs new file mode 100644 index 0000000000..5dd3bd4f13 --- /dev/null +++ b/Rubberduck.Core/UI/Refactorings/MoveFolder/MoveMultipleFoldersViewModel.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Refactorings.MoveFolder; +using Rubberduck.Resources; +using Rubberduck.JunkDrawer.Extensions; + +namespace Rubberduck.UI.Refactorings.MoveFolder +{ + public class MoveMultipleFoldersViewModel : RefactoringViewModelBase + { + public MoveMultipleFoldersViewModel(MoveMultipleFoldersModel model) + : base(model) + {} + + private IDictionary> ModulesBySourceFolder => Model.ModulesBySourceFolder; + + public string Instructions + { + get + { + if (ModulesBySourceFolder == null || !ModulesBySourceFolder.Any()) + { + return string.Empty; + } + + var sourceFolders = ModulesBySourceFolder.Keys; + if (sourceFolders.Count == 1) + { + var sourceFolder = sourceFolders.First(); + var sourceParent = sourceFolder.ParentFolder(); + + if (sourceParent.Length == 0) + { + return string.Format(RubberduckUI.MoveRootFolderDialog_InstructionsLabelText, sourceFolder); + } + + return string.Format(RubberduckUI.MoveFolderDialog_InstructionsLabelText, sourceFolder, sourceParent); + } + + return string.Format(RubberduckUI.MoveFoldersDialog_InstructionsLabelText); + } + } + + public string NewFolder + { + get => Model.TargetFolder; + set + { + Model.TargetFolder = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsValidFolder)); + } + } + + public bool IsValidFolder => ModulesBySourceFolder != null + && ModulesBySourceFolder.Any() + && NewFolder != null + && !NewFolder.Any(char.IsControl); + + protected override void DialogOk() + { + if (ModulesBySourceFolder == null || !ModulesBySourceFolder.Any()) + { + base.DialogCancel(); + } + else + { + base.DialogOk(); + } + } + } +} diff --git a/Rubberduck.JunkDrawer/Extensions/FolderExtensions.cs b/Rubberduck.JunkDrawer/Extensions/FolderExtensions.cs new file mode 100644 index 0000000000..2262230986 --- /dev/null +++ b/Rubberduck.JunkDrawer/Extensions/FolderExtensions.cs @@ -0,0 +1,60 @@ +using System.Linq; + +namespace Rubberduck.JunkDrawer.Extensions +{ + public static class FolderExtensions + { + public const char FolderDelimiter = '.'; + + public static string RootFolder(this string folder) + { + return (folder ?? string.Empty).Split(FolderExtensions.FolderDelimiter).FirstOrDefault(); + } + + public static string SubFolderPathRelativeTo(this string subFolder, string folder) + { + if (subFolder is null || folder is null) + { + return string.Empty; + } + + if (folder.Length == 0) + { + return subFolder; + } + + if (!subFolder.StartsWith(folder)) + { + return string.Empty; + } + + return subFolder.Substring(folder.Length + 1); + } + + public static string SubFolderRoot(this string subFolder, string folder) + { + var subPath = subFolder?.SubFolderPathRelativeTo(folder) ?? string.Empty; + return subPath.Split(FolderDelimiter).FirstOrDefault() ?? string.Empty; + } + + public static string ParentFolder(this string folder) + { + if (folder is null || !folder.Contains(FolderDelimiter)) + { + return string.Empty; + } + + var lastDelimiterIndex = folder.LastIndexOf(FolderDelimiter); + return folder.Substring(0, lastDelimiterIndex); + } + + public static bool IsSubFolderOf(this string subFolder, string folder) + { + return subFolder != null + && folder != null + && folder.Length < subFolder.Length + && subFolder.StartsWith(folder) + && subFolder[folder.Length] == FolderDelimiter; + } + } +} \ No newline at end of file diff --git a/Rubberduck.JunkDrawer/Output/StringExtensions.cs b/Rubberduck.JunkDrawer/Output/StringExtensions.cs index d0985143ac..57de3d63da 100644 --- a/Rubberduck.JunkDrawer/Output/StringExtensions.cs +++ b/Rubberduck.JunkDrawer/Output/StringExtensions.cs @@ -73,7 +73,7 @@ public static string FromVbaStringLiteral(this string input) public static string ToVbaStringLiteral(this string input) { - return input.Replace("\"", "\"\""); + return $"\"{input.Replace("\"", "\"\"")}\""; } public static bool TryMatchHungarianNotationCriteria(this string identifier, out string nonHungarianName) diff --git a/Rubberduck.Main/Root/RubberduckIoCInstaller.cs b/Rubberduck.Main/Root/RubberduckIoCInstaller.cs index 648f29e748..88b889204f 100644 --- a/Rubberduck.Main/Root/RubberduckIoCInstaller.cs +++ b/Rubberduck.Main/Root/RubberduckIoCInstaller.cs @@ -670,7 +670,8 @@ private Type[] RefactoringsMenuItems() typeof(RefactorMoveCloserToUsageCommandMenuItem), typeof(RefactorExtractInterfaceCommandMenuItem), typeof(RefactorImplementInterfaceCommandMenuItem), - typeof(CodePaneRefactorMoveToFolderCommandMenuItem) + typeof(CodePaneRefactorMoveToFolderCommandMenuItem), + typeof(CodePaneRefactorMoveContainingFolderCommandMenuItem) }; } diff --git a/Rubberduck.Refactorings/Exceptions/AffectedModuleIsStaleException.cs b/Rubberduck.Refactorings/Exceptions/AffectedModuleIsStaleException.cs new file mode 100644 index 0000000000..03bc9b44a6 --- /dev/null +++ b/Rubberduck.Refactorings/Exceptions/AffectedModuleIsStaleException.cs @@ -0,0 +1,14 @@ +using Rubberduck.VBEditor; + +namespace Rubberduck.Refactorings.Exceptions +{ + public class AffectedModuleIsStaleException : RefactoringException + { + public AffectedModuleIsStaleException(QualifiedModuleName staleModule) + { + StaleModule = staleModule; + } + + public QualifiedModuleName StaleModule { get; } + } +} \ No newline at end of file diff --git a/Rubberduck.Refactorings/Exceptions/MoveToFolder/NoTargetFolderException.cs b/Rubberduck.Refactorings/Exceptions/MoveToFolder/NoTargetFolderException.cs new file mode 100644 index 0000000000..2fa5013e10 --- /dev/null +++ b/Rubberduck.Refactorings/Exceptions/MoveToFolder/NoTargetFolderException.cs @@ -0,0 +1,5 @@ +namespace Rubberduck.Refactorings.Exceptions.MoveToFolder +{ + public class NoTargetFolderException : RefactoringException + {} +} \ No newline at end of file diff --git a/Rubberduck.Refactorings/MoveFolder/IMoveMultipleFoldersPresenter.cs b/Rubberduck.Refactorings/MoveFolder/IMoveMultipleFoldersPresenter.cs new file mode 100644 index 0000000000..e30357e962 --- /dev/null +++ b/Rubberduck.Refactorings/MoveFolder/IMoveMultipleFoldersPresenter.cs @@ -0,0 +1,5 @@ +namespace Rubberduck.Refactorings.MoveFolder +{ + public interface IMoveMultipleFoldersPresenter : IRefactoringPresenter + {} +} \ No newline at end of file diff --git a/Rubberduck.Refactorings/MoveFolder/MoveContainingFolderRefactoring.cs b/Rubberduck.Refactorings/MoveFolder/MoveContainingFolderRefactoring.cs new file mode 100644 index 0000000000..e2e08e6ba9 --- /dev/null +++ b/Rubberduck.Refactorings/MoveFolder/MoveContainingFolderRefactoring.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using Rubberduck.JunkDrawer.Extensions; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Parsing.UIContext; +using Rubberduck.Parsing.VBA; +using Rubberduck.Refactorings.Exceptions; +using Rubberduck.Refactorings.Exceptions.MoveToFolder; +using Rubberduck.VBEditor; +using Rubberduck.VBEditor.Utility; + +namespace Rubberduck.Refactorings.MoveFolder +{ + public class MoveContainingFolderRefactoring : InteractiveRefactoringBase + { + private readonly IRefactoringAction _moveFoldersAction; + private readonly ISelectedDeclarationProvider _selectedDeclarationProvider; + private readonly IDeclarationFinderProvider _declarationFinderProvider; + private readonly RubberduckParserState _state; + + public MoveContainingFolderRefactoring( + MoveMultipleFoldersRefactoringAction moveFoldersAction, + ISelectedDeclarationProvider selectedDeclarationProvider, + ISelectionProvider selectionProvider, + IRefactoringPresenterFactory factory, + IUiDispatcher uiDispatcher, + IDeclarationFinderProvider declarationFinderProvider, + RubberduckParserState state) + : base(selectionProvider, factory, uiDispatcher) + { + _moveFoldersAction = moveFoldersAction; + _selectedDeclarationProvider = selectedDeclarationProvider; + _declarationFinderProvider = declarationFinderProvider; + _state = state; + } + + protected override Declaration FindTargetDeclaration(QualifiedSelection targetSelection) + { + return _selectedDeclarationProvider.SelectedModule(targetSelection); + } + + protected override MoveMultipleFoldersModel InitializeModel(Declaration target) + { + if (!(target is ModuleDeclaration targetModule)) + { + throw new InvalidDeclarationTypeException(target); + } + + var finder = _declarationFinderProvider.DeclarationFinder; + + var sourceFolder = targetModule.CustomFolder; + var containedModules = finder.UserDeclarations(DeclarationType.Module) + .OfType() + .Where(module => module.ProjectId.Equals(target.ProjectId) + && (module.CustomFolder.Equals(sourceFolder) + || module.CustomFolder.IsSubFolderOf(sourceFolder))) + .ToList(); + + var modulesBySourceFolder = new Dictionary>{ {sourceFolder, containedModules} }; + var parentFolder = sourceFolder.ParentFolder(); + + return new MoveMultipleFoldersModel(modulesBySourceFolder, parentFolder); + } + + protected override void RefactorImpl(MoveMultipleFoldersModel model) + { + ValidateModel(model); + _moveFoldersAction.Refactor(model); + } + + private void ValidateModel(MoveMultipleFoldersModel model) + { + if (string.IsNullOrEmpty(model.TargetFolder)) + { + throw new NoTargetFolderException(); + } + + var firstStaleAffectedModules = model.ModulesBySourceFolder.Values + .SelectMany(modules => modules) + .FirstOrDefault(module => _state.IsNewOrModified(module.QualifiedModuleName)); + if (firstStaleAffectedModules != null) + { + throw new AffectedModuleIsStaleException(firstStaleAffectedModules.QualifiedModuleName); + } + } + } +} \ No newline at end of file diff --git a/Rubberduck.Refactorings/MoveFolder/MoveFolderModel.cs b/Rubberduck.Refactorings/MoveFolder/MoveFolderModel.cs new file mode 100644 index 0000000000..a02559a24f --- /dev/null +++ b/Rubberduck.Refactorings/MoveFolder/MoveFolderModel.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Rubberduck.Parsing.Symbols; + +namespace Rubberduck.Refactorings.MoveFolder +{ + public class MoveFolderModel : IRefactoringModel + { + public string SourceFolder { get; } + public ICollection ContainedModules { get; } + public string TargetFolder { get; set; } + + public MoveFolderModel(string sourceFolder, ICollection containedModules, string targetFolder) + { + SourceFolder = sourceFolder; + ContainedModules = containedModules; + TargetFolder = targetFolder; + } + } +} \ No newline at end of file diff --git a/Rubberduck.Refactorings/MoveFolder/MoveFolderRefactoringAction.cs b/Rubberduck.Refactorings/MoveFolder/MoveFolderRefactoringAction.cs new file mode 100644 index 0000000000..e967110f11 --- /dev/null +++ b/Rubberduck.Refactorings/MoveFolder/MoveFolderRefactoringAction.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Rubberduck.JunkDrawer.Extensions; +using Rubberduck.Parsing.Rewriter; +using Rubberduck.Refactorings.MoveToFolder; + +namespace Rubberduck.Refactorings.MoveFolder +{ + public class MoveFolderRefactoringAction : CodeOnlyRefactoringActionBase + { + private readonly ICodeOnlyRefactoringAction _moveToFolder; + + public MoveFolderRefactoringAction( + IRewritingManager rewritingManager, + MoveToFolderRefactoringAction moveToFolder) + : base(rewritingManager) + { + _moveToFolder = moveToFolder; + } + + public override void Refactor(MoveFolderModel model, IRewriteSession rewriteSession) + { + var sourceFolderParent = model.SourceFolder.ParentFolder(); + + foreach (var module in model.ContainedModules.Distinct()) + { + var currentFolder = module.CustomFolder; + var subPath = currentFolder.SubFolderPathRelativeTo(sourceFolderParent); + var newFolder = $"{model.TargetFolder}{FolderExtensions.FolderDelimiter}{subPath}"; + var moduleModel = new MoveToFolderModel(module, newFolder); + _moveToFolder.Refactor(moduleModel, rewriteSession); + } + } + } +} \ No newline at end of file diff --git a/Rubberduck.Refactorings/MoveFolder/MoveMultipleFoldersModel.cs b/Rubberduck.Refactorings/MoveFolder/MoveMultipleFoldersModel.cs new file mode 100644 index 0000000000..d899bbcc06 --- /dev/null +++ b/Rubberduck.Refactorings/MoveFolder/MoveMultipleFoldersModel.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Rubberduck.Parsing.Symbols; + +namespace Rubberduck.Refactorings.MoveFolder +{ + public class MoveMultipleFoldersModel : IRefactoringModel + { + public IDictionary> ModulesBySourceFolder { get; } + public string TargetFolder { get; set; } + + public MoveMultipleFoldersModel(IDictionary> modulesBySourceFolder, string targetFolder) + { + ModulesBySourceFolder = modulesBySourceFolder; + TargetFolder = targetFolder; + } + } +} \ No newline at end of file diff --git a/Rubberduck.Refactorings/MoveFolder/MoveMultipleFoldersRefactoringAction.cs b/Rubberduck.Refactorings/MoveFolder/MoveMultipleFoldersRefactoringAction.cs new file mode 100644 index 0000000000..dcb76cf592 --- /dev/null +++ b/Rubberduck.Refactorings/MoveFolder/MoveMultipleFoldersRefactoringAction.cs @@ -0,0 +1,26 @@ +using Rubberduck.Parsing.Rewriter; + +namespace Rubberduck.Refactorings.MoveFolder +{ + public class MoveMultipleFoldersRefactoringAction : CodeOnlyRefactoringActionBase + { + private readonly ICodeOnlyRefactoringAction _moveFolder; + + public MoveMultipleFoldersRefactoringAction( + IRewritingManager rewritingManager, + MoveFolderRefactoringAction moveFolder) + : base(rewritingManager) + { + _moveFolder = moveFolder; + } + + public override void Refactor(MoveMultipleFoldersModel model, IRewriteSession rewriteSession) + { + foreach (var sourceFolder in model.ModulesBySourceFolder.Keys) + { + var targetModel = new MoveFolderModel(sourceFolder, model.ModulesBySourceFolder[sourceFolder], model.TargetFolder); + _moveFolder.Refactor(targetModel, rewriteSession); + } + } + } +} \ No newline at end of file diff --git a/Rubberduck.Refactorings/MoveToFolder/MoveToFolderRefactoring.cs b/Rubberduck.Refactorings/MoveToFolder/MoveToFolderRefactoring.cs index 6da87694a4..fe0c8f9261 100644 --- a/Rubberduck.Refactorings/MoveToFolder/MoveToFolderRefactoring.cs +++ b/Rubberduck.Refactorings/MoveToFolder/MoveToFolderRefactoring.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using System.Linq; using Rubberduck.Parsing.Symbols; using Rubberduck.Parsing.UIContext; using Rubberduck.Parsing.VBA; using Rubberduck.Refactorings.Exceptions; +using Rubberduck.Refactorings.Exceptions.MoveToFolder; using Rubberduck.VBEditor; using Rubberduck.VBEditor.Utility; @@ -12,17 +14,20 @@ public class MoveToFolderRefactoring : InteractiveRefactoringBase _moveToFolderAction; private readonly ISelectedDeclarationProvider _selectedDeclarationProvider; + private readonly RubberduckParserState _state; public MoveToFolderRefactoring( MoveMultipleToFolderRefactoringAction moveToFolderAction, ISelectedDeclarationProvider selectedDeclarationProvider, ISelectionProvider selectionProvider, IRefactoringPresenterFactory factory, - IUiDispatcher uiDispatcher) + IUiDispatcher uiDispatcher, + RubberduckParserState state) : base(selectionProvider, factory, uiDispatcher) { _moveToFolderAction = moveToFolderAction; _selectedDeclarationProvider = selectedDeclarationProvider; + _state = state; } protected override Declaration FindTargetDeclaration(QualifiedSelection targetSelection) @@ -44,7 +49,23 @@ protected override MoveMultipleToFolderModel InitializeModel(Declaration target) protected override void RefactorImpl(MoveMultipleToFolderModel model) { + ValidateModel(model); _moveToFolderAction.Refactor(model); } + + private void ValidateModel(MoveMultipleToFolderModel model) + { + if (string.IsNullOrEmpty(model.TargetFolder)) + { + throw new NoTargetFolderException(); + } + + var firstStaleAffectedModules = model.Targets + .FirstOrDefault(module => _state.IsNewOrModified(module.QualifiedModuleName)); + if (firstStaleAffectedModules != null) + { + throw new AffectedModuleIsStaleException(firstStaleAffectedModules.QualifiedModuleName); + } + } } } \ No newline at end of file diff --git a/Rubberduck.Resources/Menus/RubberduckMenus.Designer.cs b/Rubberduck.Resources/Menus/RubberduckMenus.Designer.cs index c279bf9be8..434c2c00f0 100644 --- a/Rubberduck.Resources/Menus/RubberduckMenus.Designer.cs +++ b/Rubberduck.Resources/Menus/RubberduckMenus.Designer.cs @@ -222,6 +222,15 @@ public class RubberduckMenus { } } + /// + /// Looks up a localized string similar to Move Containing Folder. + /// + public static string RefactorMenu_MoveContainingFolder { + get { + return ResourceManager.GetString("RefactorMenu_MoveContainingFolder", resourceCulture); + } + } + /// /// Looks up a localized string similar to Move To Folder. /// diff --git a/Rubberduck.Resources/Menus/RubberduckMenus.de.resx b/Rubberduck.Resources/Menus/RubberduckMenus.de.resx index b8cf4ea56a..d250dcdcc9 100644 --- a/Rubberduck.Resources/Menus/RubberduckMenus.de.resx +++ b/Rubberduck.Resources/Menus/RubberduckMenus.de.resx @@ -246,4 +246,7 @@ In Ordner verschieben + + Enthaltenden Ordner verschieben + \ No newline at end of file diff --git a/Rubberduck.Resources/Menus/RubberduckMenus.resx b/Rubberduck.Resources/Menus/RubberduckMenus.resx index dc99926a4a..aa6ac0a59a 100644 --- a/Rubberduck.Resources/Menus/RubberduckMenus.resx +++ b/Rubberduck.Resources/Menus/RubberduckMenus.resx @@ -247,4 +247,7 @@ Move To Folder + + Move Containing Folder + \ No newline at end of file diff --git a/Rubberduck.Resources/RubberduckUI.Designer.cs b/Rubberduck.Resources/RubberduckUI.Designer.cs index fbb89b0353..3962e46e8e 100644 --- a/Rubberduck.Resources/RubberduckUI.Designer.cs +++ b/Rubberduck.Resources/RubberduckUI.Designer.cs @@ -2792,6 +2792,42 @@ public class RubberduckUI { } } + /// + /// Looks up a localized string similar to Please specify a new parent folder for the subfolder '{0}' of '{1}'.. + /// + public static string MoveFolderDialog_InstructionsLabelText { + get { + return ResourceManager.GetString("MoveFolderDialog_InstructionsLabelText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rubberduck - Move Folders. + /// + public static string MoveFoldersDialog_Caption { + get { + return ResourceManager.GetString("MoveFoldersDialog_Caption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please specify a new parent folder for the folders.. + /// + public static string MoveFoldersDialog_InstructionsLabelText { + get { + return ResourceManager.GetString("MoveFoldersDialog_InstructionsLabelText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move Folders. + /// + public static string MoveFoldersDialog_TitleText { + get { + return ResourceManager.GetString("MoveFoldersDialog_TitleText", resourceCulture); + } + } + /// /// Looks up a localized string similar to Please specify new folder for the components.. /// @@ -2801,6 +2837,15 @@ public class RubberduckUI { } } + /// + /// Looks up a localized string similar to Please specify a new parent folder for the older '{0}'.. + /// + public static string MoveRootFolderDialog_InstructionsLabelText { + get { + return ResourceManager.GetString("MoveRootFolderDialog_InstructionsLabelText", resourceCulture); + } + } + /// /// Looks up a localized string similar to Rubberduck - Move to Folder. /// @@ -3254,6 +3299,15 @@ public class RubberduckUI { } } + /// + /// Looks up a localized string similar to The component '{0}' would have been affected by the refactoring, but its state in Rubberduck's memory is stale. Please refresh Rubberduck and try again.. + /// + public static string RefactoringFailure_AffectedModuleIsStale { + get { + return ResourceManager.GetString("RefactoringFailure_AffectedModuleIsStale", resourceCulture); + } + } + /// /// Looks up a localized string similar to Refactoring failed.. /// @@ -3300,7 +3354,16 @@ public class RubberduckUI { } /// - /// Looks up a localized string similar to Unable to suspend the Parser to perform the refactoring operation. + /// Looks up a localized string similar to No target has been specified.. + /// + public static string RefactoringFailure_NoTargetFolder { + get { + return ResourceManager.GetString("RefactoringFailure_NoTargetFolder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to suspend the Parser to perform the refactoring operation.. /// public static string RefactoringFailure_SuspendParserFailure { get { diff --git a/Rubberduck.Resources/RubberduckUI.de.resx b/Rubberduck.Resources/RubberduckUI.de.resx index 198deb821d..642c4c443a 100644 --- a/Rubberduck.Resources/RubberduckUI.de.resx +++ b/Rubberduck.Resources/RubberduckUI.de.resx @@ -1586,4 +1586,25 @@ Import abgebrochen. Bitte geben Sie einen neuen Ordner für die Komponenten an. + + Bitte geben Sie einen Ordner an, in den der Unterordner '{0}' des Ordners '{1}' verschoben werden soll. + + + Bitte geben Sie einen Ordner an, in den die Ordner verschoben werden sollen. + + + Bitte geben Sie einen Ordner an, in den der Ordner '{0}' verschoben werden soll. + + + Rubberduck - Ordner verschieben + + + Ordner verschieben + + + Es wurde kein Zielordner festgelegt. + + + Die Komponente '{0}' würde durch das Refactoring verändert, aber ihr Inhalt ist in Rubberduck veraltet. Bitte lassen Sie Rubberduck seinen Stand aktualisieren und versuchen Sie es erneut. + \ No newline at end of file diff --git a/Rubberduck.Resources/RubberduckUI.resx b/Rubberduck.Resources/RubberduckUI.resx index acea695289..cf8e218e00 100644 --- a/Rubberduck.Resources/RubberduckUI.resx +++ b/Rubberduck.Resources/RubberduckUI.resx @@ -1655,7 +1655,7 @@ NOTE: Restart is required for the setting to take effect. {0}=Project; {1}=Folder; {2}=Component; {3}=DeclarationType; {4}=Scope; {5}=Name - Unable to suspend the Parser to perform the refactoring operation + Unable to suspend the Parser to perform the refactoring operation. Tell me if a newer pre-release build is available @@ -1795,4 +1795,28 @@ Import aborted. Please specify new folder for the components. + + Please specify a new parent folder for the older '{0}'. + {0} folder + + + Please specify a new parent folder for the folders. + + + Please specify a new parent folder for the subfolder '{0}' of '{1}'. + {0} folder; {1} parent folder + + + Rubberduck - Move Folders + + + Move Folders + + + No target has been specified. + + + The component '{0}' would have been affected by the refactoring, but its state in Rubberduck's memory is stale. Please refresh Rubberduck and try again. + {0} stale module + \ No newline at end of file diff --git a/RubberduckTests/CodeExplorer/CodeExplorerCustomFolderViewModelTests.cs b/RubberduckTests/CodeExplorer/CodeExplorerCustomFolderViewModelTests.cs index aa6d5caf98..bd53532e8b 100644 --- a/RubberduckTests/CodeExplorer/CodeExplorerCustomFolderViewModelTests.cs +++ b/RubberduckTests/CodeExplorer/CodeExplorerCustomFolderViewModelTests.cs @@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using NUnit.Framework; +using Rubberduck.JunkDrawer.Extensions; using Rubberduck.Navigation.CodeExplorer; -using Rubberduck.Navigation.Folders; namespace RubberduckTests.CodeExplorer { @@ -74,8 +74,7 @@ public void Constructor_PanelTitleIsFullPath(object[] parameters) var declarations = CodeExplorerTestSetup.TestProjectWithFolderStructure(structure, out _, out var state); using (state) { - var folder = - new CodeExplorerCustomFolderViewModel(null, path.First(), path.First(), null, ref declarations); + var folder = new CodeExplorerCustomFolderViewModel(null, path.First(), path.First(), null, ref declarations); foreach (var _ in path) { @@ -87,9 +86,9 @@ public void Constructor_PanelTitleIsFullPath(object[] parameters) [Test] [Category("Code Explorer")] - [TestCase(new object[] { CodeExplorerTestSetup.TestModuleName, "Foo" }, TestName = "Constructor_PanelTitleIsFullPath_RootFolder")] - [TestCase(new object[] { CodeExplorerTestSetup.TestModuleName, "Foo.Bar" }, TestName = "Constructor_PanelTitleIsFullPath_SubFolder")] - [TestCase(new object[] { CodeExplorerTestSetup.TestModuleName, "Foo.Bar.Baz" }, TestName = "Constructor_PanelTitleIsFullPath_SubSubFolder")] + [TestCase(new object[] { CodeExplorerTestSetup.TestModuleName, "Foo" }, TestName = "Constructor_FolderAttributeIsCorrect_RootFolder")] + [TestCase(new object[] { CodeExplorerTestSetup.TestModuleName, "Foo.Bar" }, TestName = "Constructor_FolderAttributeIsCorrect_SubFolder")] + [TestCase(new object[] { CodeExplorerTestSetup.TestModuleName, "Foo.Bar.Baz" }, TestName = "Constructor_FolderAttributeIsCorrect_SubSubFolder")] public void Constructor_FolderAttributeIsCorrect(object[] parameters) { var structure = ToFolderStructure(parameters.Cast()); diff --git a/RubberduckTests/CodeExplorer/CodeExplorerTestSetup.cs b/RubberduckTests/CodeExplorer/CodeExplorerTestSetup.cs index 8da2ec9e84..52f1c4e887 100644 --- a/RubberduckTests/CodeExplorer/CodeExplorerTestSetup.cs +++ b/RubberduckTests/CodeExplorer/CodeExplorerTestSetup.cs @@ -5,6 +5,7 @@ using RubberduckTests.Mocks; using System.Collections.Generic; using System.Linq; +using Rubberduck.Common; using Rubberduck.Parsing.VBA; using Rubberduck.VBEditor.ComManagement; using RubberduckTests.AddRemoveReferences; @@ -128,7 +129,7 @@ public static List TestProjectWithFolderStructure(IEnumerable<(stri { var code = string.IsNullOrEmpty(folder) ? CodeByModuleName[name] - : string.Join(Environment.NewLine, $"'@Folder(\"{folder}\")", CodeByModuleName[name]); + : string.Join(Environment.NewLine, $"'@Folder({folder.ToVbaStringLiteral()})", CodeByModuleName[name]); var type = ComponentTypeByModuleName[name]; if (type == ComponentType.UserForm) diff --git a/RubberduckTests/Refactoring/MoveFolders/MoveMultipleToFolderRefactoringActionTests.cs b/RubberduckTests/Refactoring/MoveFolders/MoveMultipleToFolderRefactoringActionTests.cs new file mode 100644 index 0000000000..d233e16c14 --- /dev/null +++ b/RubberduckTests/Refactoring/MoveFolders/MoveMultipleToFolderRefactoringActionTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Rubberduck.JunkDrawer.Extensions; +using Rubberduck.Parsing.Rewriter; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Parsing.VBA; +using Rubberduck.Refactorings; +using Rubberduck.Refactorings.MoveFolder; +using Rubberduck.Refactorings.MoveToFolder; +using Rubberduck.VBEditor.SafeComWrappers; + +namespace RubberduckTests.Refactoring.MoveFolders +{ + [TestFixture] + public class MoveMultipleFoldersRefactoringActionTests : RefactoringActionTestBase + { + [Test] + [Category("Refactorings")] + public void MoveMultipleFoldersRefactoringAction_WorksForMultipleFolders() + { + const string code1 = @" +'@Folder(""MyOldFolder.MyOldSubfolder.SubSub"") +Public Sub Foo() +End Sub +"; + const string code2 = @" +'@Folder(""MyOldFolder.MyOldSubfolder"") +Public Sub Foo() +End Sub +"; + const string code3 = @" +'@Folder(""MyOtherFolder.MyOtherOldSubfolder"") +Public Sub Foo() +End Sub +"; + const string code4 = @" +'@Folder(""MyOtherFolder.MyOtherSubfolder"") +Public Sub Foo() +End Sub +"; + const string code5 = @" +Public Sub Foo() +End Sub +"; + const string expectedCode1 = @" +'@Folder ""MyNewFolder.MyOldSubfolder.SubSub"" +Public Sub Foo() +End Sub +"; + const string expectedCode2 = @" +'@Folder ""MyNewFolder.MyOldSubfolder"" +Public Sub Foo() +End Sub +"; + const string expectedCode3 = @" +'@Folder ""MyNewFolder.MyOtherOldSubfolder"" +Public Sub Foo() +End Sub +"; + const string expectedCode4 = code4; + const string expectedCode5 = code5; + Func modelBuilder = (state) => + { + var modules = state.DeclarationFinder + .UserDeclarations(DeclarationType.Module) + .OfType() + .ToList(); + + var firstFolderModules = modules + .Where(module => module.CustomFolder.Equals("MyOldFolder.MyOldSubfolder") + || module.CustomFolder.IsSubFolderOf("MyOldFolder.MyOldSubfolder")) + .ToList(); + + var secondFolderModules = modules + .Where(module => module.CustomFolder.Equals("MyOtherFolder.MyOtherOldSubfolder") + || module.CustomFolder.IsSubFolderOf("MyOtherFolder.MyOtherOldSubfolder")) + .ToList(); + + var modulesByFolders = new Dictionary> + { + {"MyOldFolder.MyOldSubfolder", firstFolderModules}, + {"MyOtherFolder.MyOtherOldSubfolder", secondFolderModules} + }; + + return new MoveMultipleFoldersModel(modulesByFolders, "MyNewFolder"); + }; + + var refactoredCode = RefactoredCode( + modelBuilder, + ("SubSubFolderModule", code1, ComponentType.StandardModule), + ("SubFolderModule", code2, ComponentType.ClassModule), + ("OtherSubFolderModule", code3, ComponentType.ClassModule), + ("UnaffectedSubFolderModule", code4, ComponentType.StandardModule), + ("NoFolderModule", code5, ComponentType.StandardModule)); + + Assert.AreEqual(expectedCode1, refactoredCode["SubSubFolderModule"]); + Assert.AreEqual(expectedCode2, refactoredCode["SubFolderModule"]); + Assert.AreEqual(expectedCode3, refactoredCode["OtherSubFolderModule"]); + Assert.AreEqual(expectedCode4, refactoredCode["UnaffectedSubFolderModule"]); + Assert.AreEqual(expectedCode5, refactoredCode["NoFolderModule"]); + } + + protected override IRefactoringAction TestBaseRefactoring(RubberduckParserState state, IRewritingManager rewritingManager) + { + var annotationUpdater = new AnnotationUpdater(); + var moveToFolderAction = new MoveToFolderRefactoringAction(rewritingManager, annotationUpdater); + var moveFolderAction = new MoveFolderRefactoringAction(rewritingManager, moveToFolderAction); + return new MoveMultipleFoldersRefactoringAction(rewritingManager, moveFolderAction); + } + } +} \ No newline at end of file diff --git a/RubberduckTests/Refactoring/MoveFolders/MoveToFolderRefactoringActionTests.cs b/RubberduckTests/Refactoring/MoveFolders/MoveToFolderRefactoringActionTests.cs new file mode 100644 index 0000000000..ff4d62b3fa --- /dev/null +++ b/RubberduckTests/Refactoring/MoveFolders/MoveToFolderRefactoringActionTests.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Rubberduck.Parsing.Rewriter; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Parsing.VBA; +using Rubberduck.Refactorings; +using Rubberduck.Refactorings.MoveFolder; +using Rubberduck.Refactorings.MoveToFolder; +using Rubberduck.VBEditor.SafeComWrappers; +using RubberduckTests.Mocks; + +namespace RubberduckTests.Refactoring.MoveFolders +{ + [TestFixture] + public class MoveFolderRefactoringActionTests : RefactoringActionTestBase + { + [Test] + [Category("Refactorings")] + public void MoveFolderRefactoringAction_NoAnnotation() + { + const string code = @" +Public Sub Foo() +End Sub +"; + const string expectedCode = @"'@Folder ""MyNewFolder.MySubFolder.TestProject"" + +Public Sub Foo() +End Sub +"; + Func modelBuilder = (state) => + { + var module = state.DeclarationFinder + .UserDeclarations(DeclarationType.ProceduralModule) + .Single() as ModuleDeclaration; + return new MoveFolderModel("TestProject", new List{module}, "MyNewFolder.MySubFolder"); + }; + + var vbe = new MockVbeBuilder() + .ProjectBuilder("TestProject", ProjectProtection.Unprotected) + .AddComponent("TestModule", ComponentType.StandardModule, code) + .AddProjectToVbeBuilder() + .Build() + .Object; + + var refactoredCode = RefactoredCode(vbe, modelBuilder); + + Assert.AreEqual(expectedCode, refactoredCode["TestModule"]); + } + + [Test] + [Category("Refactorings")] + public void MoveFolderRefactoringAction_TopLevelFolder() + { + const string code = @" +'@Folder(""MyOldFolder"") +Public Sub Foo() +End Sub +"; + const string expectedCode = @" +'@Folder ""MyNewFolder.MySubFolder.MyOldFolder"" +Public Sub Foo() +End Sub +"; + Func modelBuilder = (state) => + { + var module = state.DeclarationFinder + .UserDeclarations(DeclarationType.ProceduralModule) + .Single() as ModuleDeclaration; + return new MoveFolderModel("MyOldFolder", new List { module }, "MyNewFolder.MySubFolder"); + }; + + var refactoredCode = RefactoredCode(code, modelBuilder); + + Assert.AreEqual(expectedCode, refactoredCode); + } + + [Test] + [Category("Refactorings")] + public void MoveFolderRefactoringAction_SubFolder() + { + const string code = @" +'@Folder(""MyOldFolder.MyOldSubFolder.SubSub"") +Public Sub Foo() +End Sub +"; + const string expectedCode = @" +'@Folder ""MyNewFolder.SubSub"" +Public Sub Foo() +End Sub +"; + Func modelBuilder = (state) => + { + var module = state.DeclarationFinder + .UserDeclarations(DeclarationType.ProceduralModule) + .Single() as ModuleDeclaration; + return new MoveFolderModel("MyOldFolder.MyOldSubFolder.SubSub", new List { module }, "MyNewFolder"); + }; + + var refactoredCode = RefactoredCode(code, modelBuilder); + + Assert.AreEqual(expectedCode, refactoredCode); + } + + [Test] + [Category("Refactorings")] + public void MoveFolderRefactoringAction_PreservesSubFolderStructure() + { + const string code = @" +'@Folder(""MyOldFolder.MyOldSubFolder.SubSub.Sub"") +Public Sub Foo() +End Sub +"; + const string expectedCode = @" +'@Folder ""MyNewFolder.MySubFolder.MyOldSubFolder.SubSub.Sub"" +Public Sub Foo() +End Sub +"; + Func modelBuilder = (state) => + { + var module = state.DeclarationFinder + .UserDeclarations(DeclarationType.ProceduralModule) + .Single() as ModuleDeclaration; + return new MoveFolderModel("MyOldFolder.MyOldSubFolder", new List { module }, "MyNewFolder.MySubFolder"); + }; + + var refactoredCode = RefactoredCode(code, modelBuilder); + + Assert.AreEqual(expectedCode, refactoredCode); + } + + [Test] + [Category("Refactorings")] + public void MoveFolderRefactoringAction_WorksForMultipleInFolder() + { + const string code1 = @" +'@Folder(""MyOldFolder.MyOldSubFolder"") +Public Sub Foo() +End Sub +"; + const string code2 = @" +'@Folder(""MyOldFolder.MyOldSubFolder"") +Public Sub Foo() +End Sub +"; + const string code3 = @" +'@Folder(""MyOldFolder.MyOldSubFolder.SubSub"") +Public Sub Foo() +End Sub +"; + const string code4 = @" +'@Folder(""MyOldFolder.MyOtherSubFolder"") +Public Sub Foo() +End Sub +"; + const string expectedCode1 = @" +'@Folder ""MyNewFolder.MySubFolder.MyOldSubFolder"" +Public Sub Foo() +End Sub +"; + const string expectedCode2 = @" +'@Folder ""MyNewFolder.MySubFolder.MyOldSubFolder"" +Public Sub Foo() +End Sub +"; + const string expectedCode3 = @" +'@Folder ""MyNewFolder.MySubFolder.MyOldSubFolder.SubSub"" +Public Sub Foo() +End Sub +"; + const string expectedCode4 = code4; + + Func modelBuilder = (state) => + { + var modules = state.DeclarationFinder + .UserDeclarations(DeclarationType.Module) + .OfType() + .Where(module => module.IdentifierName != "OtherFolderModule") + .ToList(); + return new MoveFolderModel("MyOldFolder.MyOldSubFolder", modules, "MyNewFolder.MySubFolder"); + }; + + var refactoredCode = RefactoredCode( + modelBuilder, + ("TestModule", code1, ComponentType.StandardModule), + ("SameFolderModule", code2, ComponentType.StandardModule), + ("SubFolderModule", code3, ComponentType.StandardModule), + ("OtherFolderModule", code4, ComponentType.StandardModule)); + + Assert.AreEqual(expectedCode1, refactoredCode["TestModule"]); + Assert.AreEqual(expectedCode2, refactoredCode["SameFolderModule"]); + Assert.AreEqual(expectedCode3, refactoredCode["SubFolderModule"]); + Assert.AreEqual(expectedCode4, refactoredCode["OtherFolderModule"]); + } + + protected override IRefactoringAction TestBaseRefactoring(RubberduckParserState state, IRewritingManager rewritingManager) + { + var annotationUpdater = new AnnotationUpdater(); + var moveToFolderAction = new MoveToFolderRefactoringAction(rewritingManager, annotationUpdater); + return new MoveFolderRefactoringAction(rewritingManager, moveToFolderAction); + } + } +} \ No newline at end of file diff --git a/RubberduckTests/Refactoring/MoveMultipleToFolderRefactoringActionTests.cs b/RubberduckTests/Refactoring/MoveToFolder/MoveMultipleToFolderRefactoringActionTests.cs similarity index 98% rename from RubberduckTests/Refactoring/MoveMultipleToFolderRefactoringActionTests.cs rename to RubberduckTests/Refactoring/MoveToFolder/MoveMultipleToFolderRefactoringActionTests.cs index 9854537e4d..52441db72b 100644 --- a/RubberduckTests/Refactoring/MoveMultipleToFolderRefactoringActionTests.cs +++ b/RubberduckTests/Refactoring/MoveToFolder/MoveMultipleToFolderRefactoringActionTests.cs @@ -9,7 +9,7 @@ using Rubberduck.Refactorings.MoveToFolder; using Rubberduck.VBEditor.SafeComWrappers; -namespace RubberduckTests.Refactoring +namespace RubberduckTests.Refactoring.MoveToFolder { [TestFixture] public class MoveMultipleToFolderRefactoringActionTests : RefactoringActionTestBase diff --git a/RubberduckTests/Refactoring/MoveToFolderRefactoringActionTests.cs b/RubberduckTests/Refactoring/MoveToFolder/MoveToFolderRefactoringActionTests.cs similarity index 98% rename from RubberduckTests/Refactoring/MoveToFolderRefactoringActionTests.cs rename to RubberduckTests/Refactoring/MoveToFolder/MoveToFolderRefactoringActionTests.cs index f3dfc998d4..46b973ffb0 100644 --- a/RubberduckTests/Refactoring/MoveToFolderRefactoringActionTests.cs +++ b/RubberduckTests/Refactoring/MoveToFolder/MoveToFolderRefactoringActionTests.cs @@ -7,7 +7,7 @@ using Rubberduck.Refactorings; using Rubberduck.Refactorings.MoveToFolder; -namespace RubberduckTests.Refactoring +namespace RubberduckTests.Refactoring.MoveToFolder { [TestFixture] public class MoveToFolderRefactoringActionTests : RefactoringActionTestBase