diff --git a/CSharpCodeAnalyst/App.xaml.cs b/CSharpCodeAnalyst/App.xaml.cs index fdb9d4e..f7d8994 100644 --- a/CSharpCodeAnalyst/App.xaml.cs +++ b/CSharpCodeAnalyst/App.xaml.cs @@ -1,6 +1,5 @@ using System.IO; using System.Windows; -using System.Windows.Controls; using CodeParser.Parser; using CSharpCodeAnalyst.Analyzers; using CSharpCodeAnalyst.Areas.AdvancedSearchArea; @@ -36,10 +35,9 @@ protected override async void OnStartup(StartupEventArgs e) // Run in UI mode StartUi(); - + // Faster debugging await LoadProjectFileFromCommandLineAsync(e); - } private async Task LoadProjectFileFromCommandLineAsync(StartupEventArgs e) @@ -52,7 +50,7 @@ private async Task LoadProjectFileFromCommandLineAsync(StartupEventArgs e) { return; } - + // Allow loading a project file (json) via command line for faster debugging if (MainWindow?.DataContext is MainViewModel dc) { @@ -70,8 +68,8 @@ private void StartUi() // ToolTipService.BetweenShowDelayProperty.OverrideMetadata( // typeof(DependencyObject), // new FrameworkPropertyMetadata(delayMs)); - - + + try { Initializer.InitializeMsBuildLocator(); @@ -113,7 +111,7 @@ private void StartUi() var viewModel = new MainViewModel(messaging, applicationSettings, userSettings, analyzerManager, refactoringService); var graphViewModel = new GraphViewModel(explorationGraphViewer, explorer, messaging, applicationSettings, refactoringService); var treeViewModel = new TreeViewModel(messaging, refactoringService); - var searchViewModel = new AdvancedSearchViewModel(messaging); + var searchViewModel = new AdvancedSearchViewModel(messaging, refactoringService); var infoPanelViewModel = new InfoPanelViewModel(); viewModel.InfoPanelViewModel = infoPanelViewModel; @@ -130,13 +128,13 @@ private void StartUi() messaging.Subscribe(viewModel.HandleShowPartitionsRequest); messaging.Subscribe(viewModel.HandleShowCycleGroupRequest); - + // Refactorings are forwarded to all other view models messaging.Subscribe(viewModel.HandleCodeGraphRefactored); // messaging.Subscribe(viewModel.HandleCodeGraphRefactored); // messaging.Subscribe(viewModel.HandleCodeGraphRefactored); // messaging.Subscribe(viewModel.HandleCodeGraphRefactored); - + mainWindow.DataContext = viewModel; MainWindow = mainWindow; diff --git a/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchControl.xaml b/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchControl.xaml index 751bc54..4359280 100644 --- a/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchControl.xaml +++ b/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchControl.xaml @@ -20,154 +20,167 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchControl.xaml.cs b/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchControl.xaml.cs index 872a918..dfb2a36 100644 --- a/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchControl.xaml.cs +++ b/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchControl.xaml.cs @@ -2,6 +2,7 @@ using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; +using CSharpCodeAnalyst.Resources; namespace CSharpCodeAnalyst.Areas.AdvancedSearchArea; @@ -11,7 +12,7 @@ public AdvancedSearchControl() { InitializeComponent(); } - + private void DropdownButton_Click(object sender, RoutedEventArgs e) { if (sender is Button { ContextMenu: not null } button) @@ -26,11 +27,21 @@ private void SearchDataGrid_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.A && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { - if (sender is DataGrid {DataContext: AdvancedSearchViewModel viewModel}) + if (sender is DataGrid { DataContext: AdvancedSearchViewModel viewModel }) { e.Handled = true; viewModel.SelectAllCommand.Execute(null); } } } + + private void SearchDataGrid_OnContextMenuOpening(object sender, ContextMenuEventArgs e) + { + // Update the refactoring movement parent + if (SearchDataGrid.DataContext is AdvancedSearchViewModel vm) + { + var parent = vm.GetRefactoringNewMoveParent(); + MenuRefactoringMove.Header = string.IsNullOrEmpty(parent) ? Strings.Refactor_MoveSelectedCodeElements : string.Format(Strings.Refactor_MoveSelectedCodeElementTo, parent); + } + } } \ No newline at end of file diff --git a/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchViewModel.cs b/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchViewModel.cs index afca3de..33c5208 100644 --- a/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchViewModel.cs +++ b/CSharpCodeAnalyst/Areas/AdvancedSearchArea/AdvancedSearchViewModel.cs @@ -1,4 +1,3 @@ -using System.Collections; using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows; @@ -7,6 +6,7 @@ using Contracts.Graph; using CSharpCodeAnalyst.Common; using CSharpCodeAnalyst.Messages; +using CSharpCodeAnalyst.Refactoring; using CSharpCodeAnalyst.Wpf; namespace CSharpCodeAnalyst.Areas.AdvancedSearchArea; @@ -14,15 +14,17 @@ namespace CSharpCodeAnalyst.Areas.AdvancedSearchArea; public sealed class AdvancedSearchViewModel : INotifyPropertyChanged { private readonly MessageBus _messaging; + private readonly RefactoringService _refactoringService; private readonly DispatcherTimer _searchTimer; private ObservableCollection _allItems; private CodeGraph? _codeGraph; private ObservableCollection _filteredItems; private string _searchText; - public AdvancedSearchViewModel(MessageBus messaging) + public AdvancedSearchViewModel(MessageBus messaging, RefactoringService refactoringService) { _messaging = messaging; + _refactoringService = refactoringService; _searchText = string.Empty; _allItems = []; _filteredItems = []; @@ -45,8 +47,12 @@ public AdvancedSearchViewModel(MessageBus messaging) CopyToClipboardCommand = new WpfCommand(OnCopyToClipboard); SelectAllCommand = new WpfCommand(SelectAll); DeselectAllCommand = new WpfCommand(DeselectAll); + + SetMovementTargetCommand = new WpfCommand(RefactoringSetMovementTarget, RefactoringCanSetMovementTarget); + MoveSelectedCommand = new WpfCommand(RefactoringMoveCodeElement, RefactoringCanMoveCodeElement); } + public ObservableCollection AllItems { get => _allItems; @@ -87,9 +93,43 @@ public string SearchText public ICommand CopyToClipboardCommand { get; } public ICommand SelectAllCommand { get; } public ICommand DeselectAllCommand { get; } + public ICommand SetMovementTargetCommand { get; } + public ICommand MoveSelectedCommand { get; } public event PropertyChangedEventHandler? PropertyChanged; + private bool RefactoringCanSetMovementTarget(SearchItemViewModel vm) + { + return _refactoringService.CanSetMovementTarget(vm.CodeElement?.Id); + } + + private bool RefactoringCanMoveCodeElement() + { + var ids = GetSelectedCodeElements().Select(e => e.Id).ToHashSet(); + return _refactoringService.CanMoveCodeElements(ids); + } + + private void RefactoringMoveCodeElement() + { + var elementIds = GetSelectedCodeElements().Select(e => e.Id).ToHashSet(); + if (elementIds.Any()) + { + _refactoringService.MoveCodeElements(elementIds); + } + } + + public string GetRefactoringNewMoveParent() + { + var target = _refactoringService.GetMovementTarget(); + return target?.Name != null ? target.Name : string.Empty; + } + + + private void RefactoringSetMovementTarget(SearchItemViewModel vm) + { + _refactoringService.SetMovementTarget(vm.CodeElement?.Id); + } + private void OnCopyToClipboard(SearchItemViewModel item) { var text = item.CodeElement?.FullName; @@ -214,8 +254,8 @@ private void AddSelectedToGraphInternal(bool addCollapsed) } /// - /// Gets selected code elements from all items, including the - /// currently non-visible. + /// Gets selected code elements from all items, including the + /// currently non-visible. /// private List GetSelectedCodeElements() { diff --git a/CSharpCodeAnalyst/Areas/GraphArea/GraphViewModel.cs b/CSharpCodeAnalyst/Areas/GraphArea/GraphViewModel.cs index b02ff7b..d897603 100644 --- a/CSharpCodeAnalyst/Areas/GraphArea/GraphViewModel.cs +++ b/CSharpCodeAnalyst/Areas/GraphArea/GraphViewModel.cs @@ -31,7 +31,6 @@ internal sealed class GraphViewModel : INotifyPropertyChanged private readonly ApplicationSettings _settings; private readonly LinkedList _undoStack; private readonly IGraphViewer _viewer; - private readonly GraphDropHandler _dropHandler; private HighlightOption _selectedHighlightOption; private RenderOption _selectedRenderOption; @@ -47,7 +46,7 @@ internal GraphViewModel(IGraphViewer viewer, ICodeGraphExplorer explorer, IPubli _publisher = publisher; _settings = settings; _refactoringService = refactoringService; - _dropHandler = new GraphDropHandler(publisher); + DropHandler = new GraphDropHandler(publisher); // Initialize RenderOptions RenderOptions = @@ -277,7 +276,7 @@ public HighlightOption SelectedHighlightOption public ICommand AddParentsCommand { get; } - public GraphDropHandler DropHandler => _dropHandler; + public GraphDropHandler DropHandler { get; } public event PropertyChangedEventHandler? PropertyChanged; @@ -474,7 +473,7 @@ private void OnAddParents() { elementIds = _viewer.GetGraph().GetRoots().Select(r => r.Id).ToList(); } - + if (elementIds.Any()) { AddParents(elementIds); @@ -591,7 +590,7 @@ private void AddToGraph(IEnumerable originalCodeElements, IEnumerab // Don't trigger undo return; } - + PushUndo(); // Apply "Automatically add containing type" setting @@ -828,6 +827,7 @@ public void HandleCodeGraphRefactored(CodeGraphRefactored message) } else if (message is CodeElementsMoved moved) { + // TODO May be sufficient to just check the source ids and not their children. // Add the same node ids with the same relationships. This fixes parent/child hierarchy. // We may have moved more nodes than in the graph. Or the graph is not affected at all by this movement. @@ -836,13 +836,19 @@ public void HandleCodeGraphRefactored(CodeGraphRefactored message) // Is the canvas graph affected at all? var originalGraph = moved.Graph; - var movedIds = originalGraph.Nodes[moved.SourceId].GetChildrenIncludingSelf().ToHashSet(); + var movedIds = new HashSet(); + foreach (var element in moved.SourceIds.Select(movedId => originalGraph.Nodes[movedId])) + { + movedIds.UnionWith(element.GetChildrenIncludingSelf()); + } + if (!movedIds.Intersect(ids).Any()) { + // None of the moved ids is in the graph return; } - // I don't know where the element was moved to. I add its parent. + // Add the new parent to ensure the moved elements are visible with correct hierarchy // Since I cant move an assembly parent is never null ids.Add(moved.NewParentId); diff --git a/CSharpCodeAnalyst/Areas/TreeArea/TreeViewModel.cs b/CSharpCodeAnalyst/Areas/TreeArea/TreeViewModel.cs index 1d16f69..14c9dde 100644 --- a/CSharpCodeAnalyst/Areas/TreeArea/TreeViewModel.cs +++ b/CSharpCodeAnalyst/Areas/TreeArea/TreeViewModel.cs @@ -97,12 +97,19 @@ private void OnSelectedItemChanged(TreeItemViewModel item) private bool RefactoringCanMoveCodeElement(TreeItemViewModel tvm) { - return _refactoringService.CanMoveCodeElement(tvm?.CodeElement?.Id); + var id = tvm?.CodeElement?.Id; + return id != null && _refactoringService.CanMoveCodeElements([id]); } private void RefactoringMoveCodeElement(TreeItemViewModel? tvm) { - _refactoringService.MoveCodeElement(tvm?.CodeElement?.Id); + var id = tvm?.CodeElement?.Id; + if (id == null) + { + return; + } + + _refactoringService.MoveCodeElements([id]); } private bool RefactoringCanSetMovementTarget(TreeItemViewModel tvm) @@ -119,7 +126,7 @@ private void RefactoringSetMovementTarget(TreeItemViewModel tvm) private static void OnCopyToClipboard(TreeItemViewModel vm) { - var text = vm?.CodeElement?.FullName; + var text = vm.CodeElement?.FullName; if (string.IsNullOrEmpty(text)) { return; @@ -192,7 +199,14 @@ public void HandleCodeGraphRefactored(CodeGraphRefactored message) { // This may be slow but easy. LoadCodeGraph(moved.Graph); - _messaging.Publish(new LocateInTreeRequest(moved.SourceId)); + if (moved.SourceIds.Count == 1) + { + _messaging.Publish(new LocateInTreeRequest(moved.SourceIds.Single())); + } + else + { + _messaging.Publish(new LocateInTreeRequest(moved.NewParentId)); + } } else if (message is RelationshipsDeleted relationshipsDeleted) { diff --git a/CSharpCodeAnalyst/Messages/CodeGraphRefactored.cs b/CSharpCodeAnalyst/Messages/CodeGraphRefactored.cs index 4da2328..4432459 100644 --- a/CSharpCodeAnalyst/Messages/CodeGraphRefactored.cs +++ b/CSharpCodeAnalyst/Messages/CodeGraphRefactored.cs @@ -27,12 +27,10 @@ internal class CodeElementsDeleted(CodeGraph codeGraph, string deletedElementId, internal class RelationshipsDeleted(CodeGraph codeGraph, List deleted) : CodeGraphRefactored(codeGraph) { public List Deleted { get; } = deleted; - } -internal class CodeElementsMoved(CodeGraph codeGraph, string sourceId, string oldParentId, string newParentId) : CodeGraphRefactored(codeGraph) +internal class CodeElementsMoved(CodeGraph codeGraph, HashSet sourceIds, string newParentId) : CodeGraphRefactored(codeGraph) { - public string SourceId { get; } = sourceId; - public string OldParentId { get; } = oldParentId; + public HashSet SourceIds { get; } = sourceIds; public string NewParentId { get; } = newParentId; } \ No newline at end of file diff --git a/CSharpCodeAnalyst/Refactoring/RefactoringService.cs b/CSharpCodeAnalyst/Refactoring/RefactoringService.cs index 13189f4..cf6ef80 100644 --- a/CSharpCodeAnalyst/Refactoring/RefactoringService.cs +++ b/CSharpCodeAnalyst/Refactoring/RefactoringService.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using CodeParser.Extensions; using Contracts.Graph; using CSharpCodeAnalyst.Messages; @@ -117,11 +118,11 @@ public void CreateCodeElement(string? parentId) }; var result = _graph.IntegrateCodeElementFromOriginal(element); - + // Important: Return the cloned element that is actually integrated into the graph. if (result.IsAdded) { - _messaging.Publish(new CodeElementCreated(_graph, result.CodeElement)); + _messaging.Publish(new CodeElementCreated(_graph, result.CodeElement)); } } @@ -131,7 +132,7 @@ public void CreateCodeElement(string? parentId) { return null; } - + return id == null ? null : _graph.Nodes[id]; } @@ -152,7 +153,7 @@ public bool CanCreateCodeElement(string? parentId) { return false; } - + var parent = FindCodeElement(parentId); var validChildren = GetValidChildTypes(parent); return validChildren.Count > 0; @@ -185,32 +186,60 @@ public void DeleteCodeElementAndAllChildren(string? elementId) } } - public bool CanMoveCodeElement(string? sourceId) + + + public bool CanMoveCodeElements(HashSet sourceIds) { if (_graph is null) { return false; } - if (sourceId is null || _target is null) + if (!sourceIds.Any() || _target is null) { return false; } - var source = _graph.Nodes[sourceId]; - - if (source.ElementType is CodeElementType.Assembly) + var involved = new List(); + foreach (var id in sourceIds) { - return false; + if (!_graph.Nodes.TryGetValue(id, out var source)) + { + return false; + } + + if (source.ElementType is CodeElementType.Assembly) + { + return false; + } + + if (_target.Id == source.Id) + { + return false; + } + + // Check if target is a child of source (would create circular hierarchy) + if (_target.IsChildOf(source)) + { + return false; + } + + var validChildTypesForParent = GetValidChildTypes(_target); + if (!validChildTypesForParent.Contains(source.ElementType)) + { + return false; + } + + // Don't move overlapping elements. + if (involved.Any(i => i.IsChildOf(source) || i.IsParentOf(source))) + { + return false; + } + + involved.Add(source); } - if (_target.Id == source.Id) - { - return false; - } - - var validChildTypesForParent = GetValidChildTypes(_target); - return validChildTypesForParent.Contains(source.ElementType); + return true; } public bool CanSetMovementTarget(string? elementId) @@ -253,14 +282,14 @@ public bool SetMovementTarget(string? targetId) } - public void MoveCodeElement(string? sourceId) + public void MoveCodeElements(HashSet sourceIds) { - if (_graph is null || sourceId is null) + if (_graph is null || !sourceIds.Any() || _target is null) { return; } - if (!CanMoveCodeElement(sourceId)) + if (!CanMoveCodeElements(sourceIds)) { return; } @@ -270,19 +299,16 @@ public void MoveCodeElement(string? sourceId) return; } - var source = _graph.Nodes[sourceId]; - source.MoveTo(_target!); - - - var oldParent = source.Parent; - var newParent = GetMovementTarget(); - if (newParent == null || oldParent == null) + foreach (var sourceId in sourceIds) { - // We can't move assemblies, so old parent is never null - return; + var source = _graph.Nodes[sourceId]; + source.MoveTo(_target!); + + Debug.Assert(source.ElementType != CodeElementType.Assembly && source.Parent != null); } - _messaging.Publish(new CodeElementsMoved(_graph, source.Id, oldParent.Id, newParent.Id)); + + _messaging.Publish(new CodeElementsMoved(_graph, sourceIds, _target.Id)); } public CodeElement? GetMovementTarget() diff --git a/CSharpCodeAnalyst/Resources/Strings.Designer.cs b/CSharpCodeAnalyst/Resources/Strings.Designer.cs index 50fe30c..ff54a1c 100644 --- a/CSharpCodeAnalyst/Resources/Strings.Designer.cs +++ b/CSharpCodeAnalyst/Resources/Strings.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -1648,6 +1647,24 @@ public static string Refactor_MoveCodeElementTo { } } + /// + /// Looks up a localized string similar to _Move selected. + /// + public static string Refactor_MoveSelectedCodeElements { + get { + return ResourceManager.GetString("Refactor_MoveSelectedCodeElements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move selected to '{0}'. + /// + public static string Refactor_MoveSelectedCodeElementTo { + get { + return ResourceManager.GetString("Refactor_MoveSelectedCodeElementTo", resourceCulture); + } + } + /// /// Looks up a localized string similar to Deleting model elements cannot be undone. Make sure your project is saved. Do you want to proceed?. /// diff --git a/CSharpCodeAnalyst/Resources/Strings.resx b/CSharpCodeAnalyst/Resources/Strings.resx index 5bbb1ab..f6690d6 100644 --- a/CSharpCodeAnalyst/Resources/Strings.resx +++ b/CSharpCodeAnalyst/Resources/Strings.resx @@ -841,6 +841,14 @@ Search with resharper style = Use at least one uppercase character in a search t _Move + + _Move selected + + + Move selected to '{0}' + + + _Set as movement parent @@ -955,7 +963,7 @@ ISOLATE: MyApp.Domain.** Import plain text ... - + Select all visible