/* Copyright (C) 2014-2019 de4dot@gmail.com This file is part of dnSpy dnSpy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. dnSpy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with dnSpy. If not, see . */ using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Diagnostics; using System.Linq; using dnlib.DotNet; using dnSpy.AsmEditor.Commands; using dnSpy.AsmEditor.Properties; using dnSpy.AsmEditor.UndoRedo; using dnSpy.Contracts.Controls; using dnSpy.Contracts.Documents.TreeView; using dnSpy.Contracts.Extension; using dnSpy.Contracts.Images; using dnSpy.Contracts.Menus; namespace dnSpy.AsmEditor.Namespace { [ExportAutoLoaded] sealed class CommandLoader : IAutoLoaded { [ImportingConstructor] CommandLoader(IWpfCommandService wpfCommandService, DeleteNamespaceCommand.EditMenuCommand removeCmd) => wpfCommandService.AddRemoveCommand(removeCmd); } [DebuggerDisplay("{Description}")] sealed class DeleteNamespaceCommand : IUndoCommand { [ExportMenuItem(Icon = DsImagesAttribute.Cancel, InputGestureText = "res:DeleteCommandKey", Group = MenuConstants.GROUP_CTX_DOCUMENTS_ASMED_DELETE, Order = 70)] sealed class DocumentsCommand : DocumentsContextMenuHandler { readonly Lazy undoCommandService; [ImportingConstructor] DocumentsCommand(Lazy undoCommandService) => this.undoCommandService = undoCommandService; public override bool IsVisible(AsmEditorContext context) => DeleteNamespaceCommand.CanExecute(context.Nodes); public override void Execute(AsmEditorContext context) => DeleteNamespaceCommand.Execute(undoCommandService, context.Nodes); public override string? GetHeader(AsmEditorContext context) => GetCommandName(context.Nodes.Length); } [Export, ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Icon = DsImagesAttribute.Cancel, InputGestureText = "res:DeleteCommandKey", Group = MenuConstants.GROUP_APP_MENU_EDIT_ASMED_DELETE, Order = 70)] internal sealed class EditMenuCommand : EditMenuHandler { readonly Lazy undoCommandService; [ImportingConstructor] EditMenuCommand(Lazy undoCommandService, IDocumentTreeView documentTreeView) : base(documentTreeView) => this.undoCommandService = undoCommandService; public override bool IsVisible(AsmEditorContext context) => DeleteNamespaceCommand.CanExecute(context.Nodes); public override void Execute(AsmEditorContext context) => DeleteNamespaceCommand.Execute(undoCommandService, context.Nodes); public override string? GetHeader(AsmEditorContext context) => GetCommandName(context.Nodes.Length); } static string GetCommandName(int count) => count == 1 ? dnSpy_AsmEditor_Resources.DeleteNamespaceCommand : string.Format(dnSpy_AsmEditor_Resources.DeleteNamespacesCommand, count); static bool CanExecute(DocumentTreeNodeData[] nodes) => nodes is not null && nodes.Length > 0 && nodes.All(a => a is NamespaceNode); static void Execute(Lazy undoCommandService, DocumentTreeNodeData[] nodes) { if (!CanExecute(nodes)) return; var nsNodes = nodes.Cast().ToArray(); undoCommandService.Value.Add(new DeleteNamespaceCommand(nsNodes)); } struct DeleteModelNodes { ModuleInfo[]? infos; sealed class ModuleInfo { public readonly ModuleDef Module; public readonly TypeDef[] Types; public readonly int[] Indexes; public ModuleInfo(ModuleDef module, int count) { Module = module; Types = new TypeDef[count]; Indexes = new int[count]; } } public void Delete(NamespaceNode[] nodes, DocumentTreeNodeData[] parents) { Debug2.Assert(parents is not null && nodes.Length == parents.Length); Debug2.Assert(infos is null); if (infos is not null) throw new InvalidOperationException(); infos = new ModuleInfo[nodes.Length]; for (int i = 0; i < infos.Length; i++) { var node = nodes[i]; var module = parents[i].GetModule(); Debug2.Assert(module is not null); if (module is null) throw new InvalidOperationException(); var info = new ModuleInfo(module, node.TreeNode.Children.Count); infos[i] = info; for (int j = 0; j < node.TreeNode.Children.Count; j++) { var typeNode = (TypeNode)node.TreeNode.Children[j].Data; int index = module.Types.IndexOf(typeNode.TypeDef); Debug.Assert(index >= 0); if (index < 0) throw new InvalidOperationException(); module.Types.RemoveAt(index); info.Types[j] = typeNode.TypeDef; info.Indexes[j] = index; } } } public void Restore(NamespaceNode[] nodes, DocumentTreeNodeData[] parents) { Debug2.Assert(infos is not null); if (infos is null) throw new InvalidOperationException(); Debug.Assert(infos.Length == nodes.Length); if (infos.Length != nodes.Length) throw new InvalidOperationException(); for (int i = infos.Length - 1; i >= 0; i--) { var info = infos[i]; for (int j = info.Types.Length - 1; j >= 0; j--) info.Module.Types.Insert(info.Indexes[j], info.Types[j]); } infos = null; } } DocumentTreeNodeData[] parents; DeletableNodes nodes; DeleteModelNodes modelNodes; DeleteNamespaceCommand(NamespaceNode[] nodes) { parents = nodes.Select(a => (DocumentTreeNodeData)a.TreeNode.Parent!.Data).ToArray(); this.nodes = new DeletableNodes(nodes); modelNodes = new DeleteModelNodes(); } public string Description => GetCommandName(nodes.Count); public void Execute() { nodes.Delete(); modelNodes.Delete(nodes.Nodes, parents); } public void Undo() { modelNodes.Restore(nodes.Nodes, parents); nodes.Restore(); } public IEnumerable ModifiedObjects => nodes.Nodes; } readonly struct TypeRefInfo { public readonly TypeRef TypeRef; public readonly UTF8String OrigNamespace; public TypeRefInfo(TypeRef tr) { TypeRef = tr; OrigNamespace = tr.Namespace; } } [DebuggerDisplay("{Description}")] sealed class MoveNamespaceTypesToEmptypNamespaceCommand : IUndoCommand { [ExportMenuItem(Header = "res:MoveTypesToEmptyNamespaceCommand", Icon = DsImagesAttribute.Namespace, Group = MenuConstants.GROUP_CTX_DOCUMENTS_ASMED_MISC, Order = 0)] sealed class DocumentsCommand : DocumentsContextMenuHandler { readonly Lazy undoCommandService; [ImportingConstructor] DocumentsCommand(Lazy undoCommandService) => this.undoCommandService = undoCommandService; public override bool IsVisible(AsmEditorContext context) => MoveNamespaceTypesToEmptypNamespaceCommand.CanExecute(context.Nodes); public override void Execute(AsmEditorContext context) => MoveNamespaceTypesToEmptypNamespaceCommand.Execute(undoCommandService, context.Nodes); } [ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:MoveTypesToEmptyNamespaceCommand", Icon = DsImagesAttribute.Namespace, Group = MenuConstants.GROUP_APP_MENU_EDIT_ASMED_MISC, Order = 0)] sealed class EditMenuCommand : EditMenuHandler { readonly Lazy undoCommandService; [ImportingConstructor] EditMenuCommand(Lazy undoCommandService, IDocumentTreeView documentTreeView) : base(documentTreeView) => this.undoCommandService = undoCommandService; public override bool IsVisible(AsmEditorContext context) => MoveNamespaceTypesToEmptypNamespaceCommand.CanExecute(context.Nodes); public override void Execute(AsmEditorContext context) => MoveNamespaceTypesToEmptypNamespaceCommand.Execute(undoCommandService, context.Nodes); } static bool CanExecute(DocumentTreeNodeData[] nodes) => nodes is not null && nodes.Length > 0 && nodes.All(a => a is NamespaceNode) && nodes.Any(a => ((NamespaceNode)a).Name != string.Empty) && IsInSameModule(nodes) && nodes[0].TreeNode.Parent is not null && nodes[0].TreeNode.Parent!.DataChildren.Any(a => a is NamespaceNode && ((NamespaceNode)a).Name == string.Empty); static bool IsInSameModule(DocumentTreeNodeData[] nodes) { if (nodes is null || nodes.Length == 0) return false; var module = nodes[0].GetModule(); if (module is null) return false; for (int i = 0; i < nodes.Length; i++) { if (module != nodes[i].GetModule()) return false; } return true; } static void Execute(Lazy undoCommandService, DocumentTreeNodeData[] nodes) { if (!CanExecute(nodes)) return; undoCommandService.Value.Add(new MoveNamespaceTypesToEmptypNamespaceCommand(nodes)); } MoveNamespaceTypesToEmptypNamespaceCommand(DocumentTreeNodeData[] nodes) { var nsNodes = nodes.Cast().Where(a => a.Name != string.Empty).ToArray(); Debug.Assert(nsNodes.Length > 0); this.nodes = new DeletableNodes(nsNodes); nsTarget = GetTarget(); typeRefInfos = RenameNamespaceCommand.GetTypeRefInfos(nodes[0].GetModule()!, nsNodes); } public string Description => dnSpy_AsmEditor_Resources.MoveTypesToEmptyNamespaceCommand; readonly NamespaceNode? nsTarget; DeletableNodes nodes; ModelInfo[]? infos; readonly TypeRefInfo[] typeRefInfos; sealed class ModelInfo { public UTF8String[]? Namespaces; public DeletableNodes TypeNodes; } NamespaceNode? GetTarget() => nodes.Nodes.Length == 0 ? null : (NamespaceNode)nodes.Nodes[0].TreeNode.Parent!.DataChildren.First(a => a is NamespaceNode && ((NamespaceNode)a).Name == string.Empty); public void Execute() { Debug2.Assert(infos is null); if (infos is not null) throw new InvalidOperationException(); nodes.Delete(); infos = new ModelInfo[nodes.Count]; for (int i = 0; i < infos.Length; i++) { var nsNode = nodes.Nodes[i]; var info = new ModelInfo(); infos[i] = info; info.Namespaces = new UTF8String[nsNode.TreeNode.Children.Count]; info.TypeNodes = new DeletableNodes(nsNode.TreeNode.DataChildren.Cast()); info.TypeNodes.Delete(); for (int j = 0; j < info.Namespaces.Length; j++) { var typeNode = info.TypeNodes.Nodes[j]; info.Namespaces[j] = typeNode.TypeDef.Namespace; typeNode.TypeDef.Namespace = UTF8String.Empty; nsTarget!.TreeNode.Children.Add(typeNode.TreeNode); } } foreach (var info in typeRefInfos) info.TypeRef.Namespace = UTF8String.Empty; } public void Undo() { Debug2.Assert(infos is not null); if (infos is null) throw new InvalidOperationException(); for (int i = infos.Length - 1; i >= 0; i--) { var info = infos[i]; for (int j = info.Namespaces!.Length - 1; j >= 0; j--) { var typeNode = (TypeNode)nsTarget!.TreeNode.Children[nsTarget.TreeNode.Children.Count - 1].Data; nsTarget.TreeNode.Children.RemoveAt(nsTarget.TreeNode.Children.Count - 1); bool b = info.TypeNodes.Nodes[j] == typeNode; Debug.Assert(b); if (!b) throw new InvalidOperationException(); typeNode.TypeDef.Namespace = info.Namespaces[j]; } info.TypeNodes.Restore(); } nodes.Restore(); foreach (var info in typeRefInfos) info.TypeRef.Namespace = info.OrigNamespace; infos = null; } public IEnumerable ModifiedObjects { get { if (nsTarget is not null) yield return nsTarget; foreach (var n in nodes.Nodes) yield return n; } } } [DebuggerDisplay("{Description}")] sealed class RenameNamespaceCommand : IUndoCommand { [ExportMenuItem(Header = "res:RenameNamespaceCommand", Icon = DsImagesAttribute.Namespace, Group = MenuConstants.GROUP_CTX_DOCUMENTS_ASMED_MISC, Order = 10)] sealed class DocumentsCommand : DocumentsContextMenuHandler { readonly Lazy undoCommandService; readonly IAppService appService; [ImportingConstructor] DocumentsCommand(Lazy undoCommandService, IAppService appService) { this.undoCommandService = undoCommandService; this.appService = appService; } public override bool IsVisible(AsmEditorContext context) => RenameNamespaceCommand.CanExecute(context.Nodes); public override void Execute(AsmEditorContext context) => RenameNamespaceCommand.Execute(undoCommandService, appService, context.Nodes); } [ExportMenuItem(OwnerGuid = MenuConstants.APP_MENU_EDIT_GUID, Header = "res:RenameNamespaceCommand", Icon = DsImagesAttribute.Namespace, Group = MenuConstants.GROUP_APP_MENU_EDIT_ASMED_MISC, Order = 10)] sealed class EditMenuCommand : EditMenuHandler { readonly Lazy undoCommandService; readonly IAppService appService; [ImportingConstructor] EditMenuCommand(Lazy undoCommandService, IAppService appService) : base(appService.DocumentTreeView) { this.undoCommandService = undoCommandService; this.appService = appService; } public override bool IsVisible(AsmEditorContext context) => RenameNamespaceCommand.CanExecute(context.Nodes); public override void Execute(AsmEditorContext context) => RenameNamespaceCommand.Execute(undoCommandService, appService, context.Nodes); } static bool CanExecute(DocumentTreeNodeData[] nodes) => nodes is not null && nodes.Length == 1 && nodes[0] is NamespaceNode; static void Execute(Lazy undoCommandService, IAppService appService, DocumentTreeNodeData[] nodes) { if (!CanExecute(nodes)) return; var nsNode = (NamespaceNode)nodes[0]; var data = new NamespaceVM(nsNode.Name); var win = new NamespaceDlg(); win.DataContext = data; win.Owner = appService.MainWindow; if (win.ShowDialog() != true) return; if (nsNode.Name == data.Name) return; undoCommandService.Value.Add(new RenameNamespaceCommand(data.Name!, nsNode)); } readonly string newName; readonly string origName; readonly NamespaceNode nsNode; readonly NamespaceNode? existingNsNode; readonly DocumentTreeNodeData origParentNode; readonly int origParentChildIndex; readonly UTF8String[] typeNamespaces; readonly TypeNode[] origChildren; readonly TypeRefInfo[] typeRefInfos; internal static TypeRefInfo[] GetTypeRefInfos(ModuleDef module, IEnumerable nsNodes) { var types = new HashSet(RefFinder.TypeEqualityComparerInstance); foreach (var nsNode in nsNodes) { foreach (TypeNode typeNode in nsNode.TreeNode.DataChildren) types.Add(typeNode.TypeDef); } var typeRefs = RefFinder.FindTypeRefsToThisModule(module); return typeRefs.Where(a => types.Contains(a)).Select(a => new TypeRefInfo(a)).ToArray(); } RenameNamespaceCommand(string newName, NamespaceNode nsNode) { this.newName = newName; origName = nsNode.Name; this.nsNode = nsNode; existingNsNode = (NamespaceNode?)nsNode.TreeNode.Parent!.DataChildren.FirstOrDefault(a => a is NamespaceNode && newName == ((NamespaceNode)a).Name); var module = nsNode.GetModule(); Debug2.Assert(module is not null); if (module is null) throw new InvalidOperationException(); origParentNode = (DocumentTreeNodeData)nsNode.TreeNode.Parent.Data; origParentChildIndex = origParentNode.TreeNode.Children.IndexOf(nsNode.TreeNode); Debug.Assert(origParentChildIndex >= 0); if (origParentChildIndex < 0) throw new InvalidOperationException(); // Make sure the exact same namespace names are restored if we undo. The names are UTF8 // strings, but not necessarily canonicalized if it's an obfuscated assembly. nsNode.TreeNode.EnsureChildrenLoaded(); origChildren = nsNode.TreeNode.DataChildren.Cast().ToArray(); typeNamespaces = new UTF8String[nsNode.TreeNode.Children.Count]; for (int i = 0; i < typeNamespaces.Length; i++) typeNamespaces[i] = origChildren[i].TypeDef.Namespace; typeRefInfos = GetTypeRefInfos(module, new[] { nsNode }); } public string Description => dnSpy_AsmEditor_Resources.RenameNamespaceCommand; public void Execute() { UTF8String newNamespace = newName; if (existingNsNode is not null) { Debug.Assert(origChildren.Length == nsNode.TreeNode.Children.Count); nsNode.TreeNode.Children.Clear(); foreach (var typeNode in origChildren) { typeNode.TypeDef.Namespace = newNamespace; existingNsNode.TreeNode.AddChild(typeNode.TreeNode); } } else { bool b = origParentChildIndex < origParentNode.TreeNode.Children.Count && origParentNode.TreeNode.Children[origParentChildIndex] == nsNode.TreeNode; Debug.Assert(b); if (!b) throw new InvalidOperationException(); var isNodeSelected = nsNode.TreeNode.TreeView.SelectedItem == nsNode; origParentNode.TreeNode.Children.RemoveAt(origParentChildIndex); nsNode.Name = newName; foreach (var typeNode in origChildren) typeNode.TypeDef.Namespace = newNamespace; origParentNode.TreeNode.AddChild(nsNode.TreeNode); if (isNodeSelected) origParentNode.TreeNode.TreeView.SelectItems(new[] { nsNode }); nsNode.TreeNode.RefreshUI(); } foreach (var info in typeRefInfos) info.TypeRef.Namespace = newNamespace; } public void Undo() { if (existingNsNode is not null) { Debug.Assert(nsNode.TreeNode.Children.Count == 0); foreach (var typeNode in origChildren) { bool b = existingNsNode.TreeNode.Children.Remove(typeNode.TreeNode); Debug.Assert(b); if (!b) throw new InvalidOperationException(); } for (int i = 0; i < origChildren.Length; i++) { var typeNode = origChildren[i]; typeNode.TypeDef.Namespace = typeNamespaces[i]; nsNode.TreeNode.Children.Add(typeNode.TreeNode); } } else { var isNodeSelected = nsNode.TreeNode.TreeView.SelectedItem == nsNode; bool b = origParentNode.TreeNode.Children.Remove(nsNode.TreeNode); Debug.Assert(b); if (!b) throw new InvalidOperationException(); for (int i = 0; i < origChildren.Length; i++) origChildren[i].TypeDef.Namespace = typeNamespaces[i]; nsNode.Name = origName; origParentNode.TreeNode.Children.Insert(origParentChildIndex, nsNode.TreeNode); if (isNodeSelected) origParentNode.TreeNode.TreeView.SelectItems(new[] { nsNode }); nsNode.TreeNode.RefreshUI(); } foreach (var info in typeRefInfos) info.TypeRef.Namespace = info.OrigNamespace; } public IEnumerable ModifiedObjects { get { yield return nsNode; } } } }