/* 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; using System.ComponentModel.Composition; using System.Diagnostics; using System.Linq; using System.Windows.Input; using dnlib.DotNet; using dnSpy.Analyzer.TreeNodes; using dnSpy.Contracts.Controls; using dnSpy.Contracts.Decompiler; using dnSpy.Contracts.Documents; using dnSpy.Contracts.Documents.Tabs; using dnSpy.Contracts.Documents.Tabs.DocViewer; using dnSpy.Contracts.Images; using dnSpy.Contracts.Menus; using dnSpy.Contracts.MVVM; using dnSpy.Contracts.Text; using dnSpy.Contracts.TreeView; using dnSpy.Contracts.TreeView.Text; using Microsoft.VisualStudio.Text; namespace dnSpy.Analyzer { interface IAnalyzerService { /// /// Gets the instance /// ITreeView TreeView { get; } /// /// Called when it's been closed /// void OnClose(); /// /// Adds if it hasn't been added and gives it focus. /// /// Node void Add(AnalyzerTreeNodeData node); /// /// Called by when its method /// has been called. /// /// Activated node (it's the caller) void OnActivated(AnalyzerTreeNodeData node); /// /// Follows the reference /// /// Node /// true to show it in a new tab /// true to show the reference in a method body void FollowNode(TreeNodeData node, bool newTab, bool? useCodeRef); /// /// Returns true if can execute /// /// Node /// true to show the reference in a method body /// bool CanFollowNode(TreeNodeData node, bool useCodeRef); } [Export(typeof(IAnalyzerService))] sealed class AnalyzerService : IAnalyzerService, ITreeViewListener { static readonly Guid ANALYZER_TREEVIEW_GUID = new Guid("8981898A-1384-4B67-9577-3CB096195146"); public ITreeView TreeView { get; } sealed class GuidObjectsProvider : IGuidObjectsProvider { readonly ITreeView treeView; public GuidObjectsProvider(ITreeView treeView) => this.treeView = treeView; public IEnumerable GetGuidObjects(GuidObjectsProviderArgs args) { yield return new GuidObject(MenuConstants.GUIDOBJ_TREEVIEW_NODES_ARRAY_GUID, treeView.TopLevelSelection); } } readonly AnalyzerTreeNodeDataContext context; readonly IDocumentTabService documentTabService; [ImportingConstructor] AnalyzerService(IWpfCommandService wpfCommandService, IDocumentTabService documentTabService, ITreeViewService treeViewService, IMenuService menuService, IAnalyzerSettings analyzerSettings, IDotNetImageService dotNetImageService, IDecompilerService decompilerService, ITreeViewNodeTextElementProvider treeViewNodeTextElementProvider) { this.documentTabService = documentTabService; context = new AnalyzerTreeNodeDataContext { DotNetImageService = dotNetImageService, Decompiler = decompilerService.Decompiler, TreeViewNodeTextElementProvider = treeViewNodeTextElementProvider, DocumentService = documentTabService.DocumentTreeView.DocumentService, ShowToken = analyzerSettings.ShowToken, SingleClickExpandsChildren = analyzerSettings.SingleClickExpandsChildren, SyntaxHighlight = analyzerSettings.SyntaxHighlight, AnalyzerService = this, }; var options = new TreeViewOptions { CanDragAndDrop = false, TreeViewListener = this, }; TreeView = treeViewService.Create(ANALYZER_TREEVIEW_GUID, options); context.TreeView = TreeView; documentTabService.DocumentTreeView.DocumentService.CollectionChanged += DocumentService_CollectionChanged; documentTabService.DocumentModified += DocumentTabService_FileModified; decompilerService.DecompilerChanged += DecompilerService_DecompilerChanged; analyzerSettings.PropertyChanged += AnalyzerSettings_PropertyChanged; menuService.InitializeContextMenu(TreeView.UIObject, new Guid(MenuConstants.GUIDOBJ_ANALYZER_TREEVIEW_GUID), new GuidObjectsProvider(TreeView)); wpfCommandService.Add(ControlConstants.GUID_ANALYZER_TREEVIEW, TreeView.UIObject); var cmds = wpfCommandService.GetCommands(ControlConstants.GUID_ANALYZER_TREEVIEW); var command = new RelayCommand(a => ActivateNode()); cmds.Add(command, ModifierKeys.Control, Key.Enter); cmds.Add(command, ModifierKeys.Shift, Key.Enter); } void DocumentTabService_FileModified(object? sender, DocumentModifiedEventArgs e) { AnalyzerTreeNodeData.HandleModelUpdated(TreeView.Root, e.Documents); RefreshNodes(); } void ActivateNode() { var nodes = TreeView.TopLevelSelection; var node = nodes.Length == 0 ? null : nodes[0] as TreeNodeData; if (node is not null) node.Activate(); } void DocumentService_CollectionChanged(object? sender, NotifyDocumentCollectionChangedEventArgs e) { switch (e.Type) { case NotifyDocumentCollectionType.Clear: ClearAll(); break; case NotifyDocumentCollectionType.Add: AnalyzerTreeNodeData.HandleAssemblyListChanged(TreeView.Root, Array.Empty(), e.Documents); break; case NotifyDocumentCollectionType.Remove: AnalyzerTreeNodeData.HandleAssemblyListChanged(TreeView.Root, e.Documents, Array.Empty()); break; default: break; } } void DecompilerService_DecompilerChanged(object? sender, EventArgs e) { context.Decompiler = ((IDecompilerService)sender!).Decompiler; RefreshNodes(); } void AnalyzerSettings_PropertyChanged(object? sender, PropertyChangedEventArgs e) { var analyzerSettings = (IAnalyzerSettings)sender!; switch (e.PropertyName) { case nameof(analyzerSettings.ShowToken): context.ShowToken = analyzerSettings.ShowToken; RefreshNodes(); break; case nameof(analyzerSettings.SyntaxHighlight): context.SyntaxHighlight = analyzerSettings.SyntaxHighlight; RefreshNodes(); break; case nameof(analyzerSettings.SingleClickExpandsChildren): context.SingleClickExpandsChildren = analyzerSettings.SingleClickExpandsChildren; break; } } void RefreshNodes() => TreeView.RefreshAllNodes(); void ITreeViewListener.OnEvent(ITreeView treeView, TreeViewListenerEventArgs e) { if (e.Event == TreeViewListenerEvent.NodeCreated) { Debug2.Assert(context is not null); var node = (ITreeNode)e.Argument; if (node.Data is AnalyzerTreeNodeData d) d.Context = context; return; } } void Cancel() => AnalyzerTreeNodeData.CancelSelfAndChildren(TreeView.Root.Data); public void OnClose() => ClearAll(); void ClearAll() { Cancel(); TreeView.Root.Children.Clear(); } public void Add(AnalyzerTreeNodeData node) { if (node is EntityNode an) { var found = TreeView.Root.DataChildren.OfType().FirstOrDefault(n => n.Member == an.Member); if (found is not null) { found.TreeNode.IsExpanded = true; TreeView.SelectItems(new TreeNodeData[] { found }); TreeView.Focus(); return; } } TreeView.Root.Children.Add(TreeView.Create(node)); node.TreeNode.IsExpanded = true; TreeView.SelectItems(new TreeNodeData[] { node }); TreeView.Focus(); } public void OnActivated(AnalyzerTreeNodeData node) { if (node is null) throw new ArgumentNullException(nameof(node)); bool newTab = Keyboard.Modifiers == ModifierKeys.Control || Keyboard.Modifiers == ModifierKeys.Shift; FollowNode(node, newTab, null); } public void FollowNode(TreeNodeData node, bool newTab, bool? useCodeRef) { var tokNode = node as IMDTokenNode; var @ref = tokNode?.Reference; var entityNode = node as EntityNode; var srcRef = entityNode?.SourceRef; bool code = useCodeRef ?? srcRef is not null; if (code) { if (srcRef is null) return; if (srcRef.Value.ILOffset is not null) { documentTabService.FollowReference(srcRef.Value.Method, newTab, true, a => { if (!a.HasMovedCaret && a.Success && srcRef is not null) a.HasMovedCaret = GoTo(a.Tab, srcRef.Value.Method, srcRef.Value.ILOffset, srcRef.Value.Reference); }); } else documentTabService.FollowReference(srcRef.Value.Method, newTab); } else { if (@ref is null) return; documentTabService.FollowReference(@ref, newTab); } } public bool CanFollowNode(TreeNodeData node, bool useCodeRef) { var tokNode = node as IMDTokenNode; var @ref = tokNode?.Reference; var entityNode = node as EntityNode; var srcRef = entityNode?.SourceRef; if (useCodeRef) return srcRef is not null; return @ref is not null; } bool GoTo(IDocumentTab tab, MethodDef method, uint? ilOffset, object? @ref) { if (method is null || ilOffset is null) return false; var documentViewer = tab.TryGetDocumentViewer(); if (documentViewer is null) return false; var methodDebugService = documentViewer.GetMethodDebugService(); var methodStatement = methodDebugService.FindByCodeOffset(method, ilOffset.Value); if (methodStatement is null) return false; var textSpan = methodStatement.Value.Statement.TextSpan; var loc = FindLocation(documentViewer.Content.ReferenceCollection.FindFrom(textSpan.Start), documentViewer.TextView.TextSnapshot, methodStatement.Value.Statement.TextSpan.End, @ref); if (loc is null) loc = textSpan.Start; documentViewer.MoveCaretToPosition(loc.Value); return true; } static int GetLineNumber(ITextSnapshot snapshot, int position) { Debug.Assert((uint)position <= (uint)snapshot.Length); if ((uint)position > (uint)snapshot.Length) return int.MaxValue; return snapshot.GetLineFromPosition(position).LineNumber; } int? FindLocation(IEnumerable> refs, ITextSnapshot snapshot, int endPos, object? @ref) { int lb = GetLineNumber(snapshot, endPos); foreach (var info in refs) { var la = GetLineNumber(snapshot, info.Span.Start); int c = Compare(la, info.Span.Start, lb, endPos); if (c > 0) break; if (RefEquals(@ref, info.Data.Reference)) return info.Span.Start; } return null; } static int Compare(int la, int ca, int lb, int cb) { if (la > lb) return 1; if (la == lb) return ca - cb; return -1; } static bool RefEquals(object? a, object? b) { if (Equals(a, b)) return true; if (a is null || b is null) return false; { if (b is PropertyDef) { var tmp = a; a = b; b = tmp; } if (b is EventDef) { var tmp = a; a = b; b = tmp; } } const SigComparerOptions flags = SigComparerOptions.CompareDeclaringTypes | SigComparerOptions.PrivateScopeIsComparable; if (a is IType type) return new SigComparer().Equals(type, b as IType); if (a is IMethod method && method.IsMethod) return new SigComparer(flags).Equals(method, b as IMethod); if (a is IField field && field.IsField) return new SigComparer(flags).Equals(field, b as IField); if (a is PropertyDef prop) { if (new SigComparer(flags).Equals(prop, b as PropertyDef)) return true; var bm = b as IMethod; return bm is not null && (new SigComparer(flags).Equals(prop.GetMethod, bm) || new SigComparer(flags).Equals(prop.SetMethod, bm)); } if (a is EventDef evt) { if (new SigComparer(flags).Equals(evt, b as EventDef)) return true; var bm = b as IMethod; return bm is not null && (new SigComparer(flags).Equals(evt.AddMethod, bm) || new SigComparer(flags).Equals(evt.InvokeMethod, bm) || new SigComparer(flags).Equals(evt.RemoveMethod, bm)); } Debug.Fail("Shouldn't be here"); return false; } } }