diff options
Diffstat (limited to 'main/src/addins/MonoDevelop.Refactoring/MonoDevelop.CodeIssues/Pad/CodeIssuePad.cs')
-rw-r--r-- | main/src/addins/MonoDevelop.Refactoring/MonoDevelop.CodeIssues/Pad/CodeIssuePad.cs | 534 |
1 files changed, 534 insertions, 0 deletions
diff --git a/main/src/addins/MonoDevelop.Refactoring/MonoDevelop.CodeIssues/Pad/CodeIssuePad.cs b/main/src/addins/MonoDevelop.Refactoring/MonoDevelop.CodeIssues/Pad/CodeIssuePad.cs new file mode 100644 index 0000000000..c875894512 --- /dev/null +++ b/main/src/addins/MonoDevelop.Refactoring/MonoDevelop.CodeIssues/Pad/CodeIssuePad.cs @@ -0,0 +1,534 @@ +// +// CodeIssuePad.cs +// +// Author: +// Mike Krüger <mkrueger@xamarin.com> +// +// Copyright (c) 2013 Xamarin Inc. (http://xamarin.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using MonoDevelop.Ide.Gui; +using Xwt; +using MonoDevelop.Ide; +using MonoDevelop.Projects; +using System.Collections.Generic; +using ICSharpCode.NRefactory.TypeSystem; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using MonoDevelop.Refactoring;
+using Xwt.Drawing;
+using IconSize = Gtk.IconSize; +using MonoDevelop.Core;
+
+namespace MonoDevelop.CodeIssues +{ + public class CodeIssuePadControl : VBox + { + const int UpdatePeriod = 500; + const int BatchChoiceCount = 5; + + readonly TreeView view = new TreeView (); + readonly DataField<string> textField = new DataField<string> (); + readonly DataField<IIssueTreeNode> nodeField = new DataField<IIssueTreeNode> (); + readonly Button runButton = new Button ("Run"); + readonly Button cancelButton = new Button ("Cancel"); + + readonly IssueGroup rootGroup; + + readonly TreeStore store; + + readonly ISet<IIssueTreeNode> syncedNodes = new HashSet<IIssueTreeNode> (); + readonly Dictionary<IIssueTreeNode, TreePosition> nodePositions = new Dictionary<IIssueTreeNode, TreePosition> (); + + bool runPeriodicUpdate; + readonly object queueLock = new object (); + readonly Queue<IIssueTreeNode> updateQueue = new Queue<IIssueTreeNode> (); + + IJobContext currentJobContext; + + public IJobContext CurrentJobContext { + get { + return currentJobContext; + } + set { + currentJobContext = value; + bool working = currentJobContext != null; + runButton.Sensitive = !working; + cancelButton.Sensitive = working; + } + } + + static readonly Type[] groupingProviders = { + typeof(CategoryGroupingProvider), + typeof(ProviderGroupingProvider), + typeof(SeverityGroupingProvider), + typeof(ProjectGroupingProvider), + typeof(FileGroupingProvider) + }; + + public CodeIssuePadControl () + { + var buttonRow = new HBox();
+ runButton.Image = ImageService.GetIcon (Ide.Gui.Stock.Execute, IconSize.Menu); + runButton.Clicked += StartAnalyzation; + buttonRow.PackStart (runButton);
+
+ cancelButton.Image = ImageService.GetIcon (Ide.Gui.Stock.Stop, IconSize.Menu); + cancelButton.Clicked += StopAnalyzation; + cancelButton.Sensitive = false; + buttonRow.PackStart (cancelButton); + var groupingProvider = new CategoryGroupingProvider { + Next = new ProviderGroupingProvider() + }; + rootGroup = new IssueGroup (groupingProvider, "root group"); + var groupingProviderControl = new GroupingProviderChainControl (rootGroup, groupingProviders); + buttonRow.PackStart (groupingProviderControl); + + PackStart (buttonRow); + + store = new TreeStore (textField, nodeField); + view.DataSource = store; + view.HeadersVisible = false; + view.Columns.Add ("Name", textField); + view.SelectionMode = SelectionMode.Multiple; + + view.RowActivated += OnRowActivated; + view.RowExpanding += OnRowExpanding; + view.ButtonPressed += HandleButtonPressed; + view.ButtonReleased += HandleButtonReleased; + PackStart (view, true); + + IIssueTreeNode node = rootGroup; + node.ChildrenInvalidated += (sender, group) => { + Application.Invoke (delegate { + ClearSiblingNodes (store.GetFirstNode ()); + store.Clear (); + foreach(var child in ((IIssueTreeNode)rootGroup).Children) { + var navigator = store.AddNode (); + SetNode (navigator, child); + SyncNode (navigator); + } + }); + }; + node.ChildAdded += HandleRootChildAdded; + + IdeApp.Workspace.LastWorkspaceItemClosed += HandleLastWorkspaceItemClosed; + }
+
+ void HandleLastWorkspaceItemClosed (object sender, EventArgs e) + { + ClearState (); + } + + void ClearState () + { + store.Clear (); + rootGroup.ClearStatistics (); + rootGroup.EnableProcessing (); + + syncedNodes.Clear (); + nodePositions.Clear (); + lock (queueLock) { + updateQueue.Clear (); + } + } + + void StartPeriodicUpdate () + { + Debug.Assert (!runPeriodicUpdate); + runPeriodicUpdate = true; + Application.TimeoutInvoke (UpdatePeriod, RunPeriodicUpdate); + } + + void ProcessUpdateQueue () + { + IList<IIssueTreeNode> nodes; + lock (queueLock) { + nodes = new List<IIssueTreeNode> (updateQueue); + updateQueue.Clear (); + } + foreach (var node in nodes) { + TreePosition position; + if (!nodePositions.TryGetValue (node, out position)) { + // This might be an event for a group that has been invalidated and removed + continue; + } + var navigator = store.GetNavigatorAt (position); + if (!node.Visible) { + // Check above means node is always in nodePositions + nodePositions.Remove (node); + if (syncedNodes.Contains (node)) { + syncedNodes.Remove (node); + } + ClearChildNodes (navigator); + navigator.Remove (); + continue; + } + UpdateText (navigator, node); + if (!syncedNodes.Contains (node) && node.HasVisibleChildren) { + if (navigator.MoveToChild ()) { + navigator.MoveToParent (); + } + else { + AddDummyChild (navigator); + } + } + } + } + + bool RunPeriodicUpdate () + { + ProcessUpdateQueue (); + return runPeriodicUpdate; + } + + void EndPeriodicUpdate () + { + Debug.Assert (runPeriodicUpdate); + runPeriodicUpdate = false; + } + + void HandleRootChildAdded (object sender, IssueTreeNodeEventArgs e) + { + Application.Invoke (delegate { + Debug.Assert (e.Parent == rootGroup); + var navigator = store.AddNode (); + SetNode (navigator, e.Child); + SyncNode (navigator); + }); + } + + void StartAnalyzation (object sender, EventArgs e) + { + var solution = IdeApp.ProjectOperations.CurrentSelectedSolution; + if (solution == null) + return; + + ClearState (); + + var job = new SolutionAnalysisJob (solution); + job.CodeIssueAdded += HandleCodeIssueAdded; + job.Completed += delegate { + CurrentJobContext = null; + }; + CurrentJobContext = RefactoringService.QueueCodeIssueAnalysis (job, "Analyzing solution"); + StartPeriodicUpdate (); + } + + void HandleCodeIssueAdded (object sender, CodeIssueEventArgs e) + { + foreach (var issue in e.CodeIssues) { + var summary = IssueSummary.FromCodeIssue (e.File, e.Provider, issue); + rootGroup.AddIssue (summary); + } + } + + void StopAnalyzation (object sender, EventArgs e) + { + if (CurrentJobContext != null) { + CurrentJobContext.CancelJob (); + CurrentJobContext = null; + } + EndPeriodicUpdate (); + } + + void SetNode (TreeNavigator navigator, IIssueTreeNode node) + { + if (navigator == null) + throw new ArgumentNullException ("navigator"); + if (node == null) + throw new ArgumentNullException ("node"); + + navigator.SetValue (nodeField, node); + Debug.Assert (!nodePositions.ContainsKey (node)); + var position = navigator.CurrentPosition; + nodePositions.Add (node, position); + + node.ChildAdded += (sender, e) => { + Debug.Assert (e.Parent == node); + Application.Invoke (delegate { + var newNavigator = store.GetNavigatorAt (position); + newNavigator.AddChild (); + SetNode (newNavigator, e.Child); + SyncNode (newNavigator); + }); + }; + node.ChildrenInvalidated += (sender, e) => { + Application.Invoke (delegate { + SyncNode (store.GetNavigatorAt (position)); + }); + }; + node.TextChanged += (sender, e) => { + lock (queueLock) { + if (!updateQueue.Contains (e.Node)) { + updateQueue.Enqueue (e.Node); + } + } + }; + node.VisibleChanged += (sender, e) => { + lock (queueLock) { + if (!updateQueue.Contains (e.Node)) { + updateQueue.Enqueue (e.Node); + } + } + }; + } + + void ClearSiblingNodes (TreeNavigator navigator) + { + if (navigator.CurrentPosition == null) + return; + + do { + var node = navigator.GetValue (nodeField); + if (node != null) { + if (syncedNodes.Contains (node)) { + syncedNodes.Remove (node); + } + if (nodePositions.ContainsKey (node)) { + nodePositions.Remove (node); + } + } + ClearChildNodes (navigator); + } while (navigator.MoveNext ()); + } + + void ClearChildNodes (TreeNavigator navigator) + { + if (navigator.MoveToChild ()) { + ClearSiblingNodes (navigator); + navigator.MoveToParent (); + } + } + + void SyncNode (TreeNavigator navigator, bool forceExpansion = false) + { + var node = navigator.GetValue (nodeField); + UpdateText (navigator, node); + bool isExpanded = forceExpansion || view.IsRowExpanded (navigator.CurrentPosition); + ClearChildNodes (navigator); + syncedNodes.Remove (node); + navigator.RemoveChildren (); + if (!node.HasVisibleChildren) + return; + if (isExpanded) { + foreach (var childNode in node.Children.Where (child => child.Visible)) { + navigator.AddChild (); + SetNode (navigator, childNode); + SyncNode (navigator); + navigator.MoveToParent (); + } + } else { + AddDummyChild (navigator); + } + + if (isExpanded) { + syncedNodes.Add (node); + view.ExpandRow (navigator.CurrentPosition, false); + } + + } + + void UpdateText (TreeNavigator navigator, IIssueTreeNode node) + { + navigator.SetValue (textField, node.Text); + } + + void AddDummyChild (TreeNavigator navigator) + { + navigator.AddChild (); + navigator.SetValue (textField, "Loading..."); + navigator.MoveToParent (); + } + + EventHandler<IssueGroupEventArgs> GetChildrenInvalidatedHandler (TreePosition position) + { + return (sender, eventArgs) => { + Application.Invoke(delegate { + var expanded = view.IsRowExpanded (position); + var newNavigator = store.GetNavigatorAt (position); + newNavigator.RemoveChildren (); + SyncNode (newNavigator, expanded); + if (expanded) { + view.ExpandRow (position, false); + } + }); + }; + } + + void OnRowActivated (object sender, TreeViewRowEventArgs e) + { + var position = e.Position; + var node = store.GetNavigatorAt (position).GetValue (nodeField); + + var issueSummary = node as IssueSummary; + if (issueSummary != null) { + var region = issueSummary.Region; + IdeApp.Workbench.OpenDocument (region.FileName, region.BeginLine, region.BeginColumn); + } else { + if (!view.IsRowExpanded (position)) { + view.ExpandRow (position, false); + } else { + view.CollapseRow (position); + } + } + } + + void OnRowExpanding (object sender, TreeViewRowEventArgs e) + { + var navigator = store.GetNavigatorAt (e.Position); + var node = navigator.GetValue (nodeField); + if (!syncedNodes.Contains (node)) { + SyncNode (navigator, true); + } + } + + #region Button event handlers + // Event handling of right click on the TreeView is split in two parts + // This is because no single handler can support intuitive behavior regarding + // what happens to the selection when the right mouse button is pressed: + // if only a single row is selected: change the selection and then show menu + // if multiple rows are selected: show the menu directly + + void HandleButtonReleased (object sender, ButtonEventArgs e) + { + if (e.Button != PointerButton.Right || handledByPress) + return; + + var rows = view.SelectedRows; + if (rows.Length <= 1) { + // Single row or no row + ShowBatchFixContextMenu (e.X, e.Y, view.SelectedRows); + } + } + + bool handledByPress; + + void HandleButtonPressed (object sender, ButtonEventArgs e) + { + if (e.Button != PointerButton.Right) + return; + + var rows = view.SelectedRows; + if (rows.Length > 1) { + // this is a multiple selection + // waiting in this case means the selection disappears + ShowBatchFixContextMenu (e.X, e.Y, rows); + + // Don't let the selection be reset + e.Handled = true; + handledByPress = true; + } else { + handledByPress = false; + } + } + + #endregion + + void UpdateParents (TreeNavigator navigator) + { + do { + var node = navigator.GetValue (nodeField); + UpdateText (navigator, node); + } while (navigator.MoveToParent ()); + } + + void ShowBatchFixContextMenu (double x, double y, IEnumerable<TreePosition> rows) + { + var possibleFixes = rows + .Select (row => store.GetNavigatorAt (row).GetValue (nodeField)) + .Where (node1 => node1 != null) + .SelectMany (node2 => node2.AllChildren.Union (new [] { node2 })) + .Where (node3 => node3.Visible) + .OfType<IssueSummary> () + .Where (issue => issue.Actions.Any (a => a.Batchable)) + .Distinct() + .GroupBy(issue => issue.InspectorIdString) + .OrderBy (group => -group.Count ()); + + var groups = possibleFixes.Take (BatchChoiceCount).ToList (); + if (!groups.Any ()) + return; + + if (groups.Count == 1) { + CreateIssueMenu (groups.First ()).Popup (view, x, y); + } else { + var menu = new Menu (); + foreach (var g in groups) { + var menuItem = new MenuItem (g.First ().ProviderTitle); + menuItem.SubMenu = CreateIssueMenu (g); + menu.Items.Add (menuItem); + } + menu.Popup (view, x, y); + } + } + + Menu CreateIssueMenu (IEnumerable<IssueSummary> issues) + { + var allIssues = issues as IList<IssueSummary> ?? issues.ToList (); + var issueMenu = new Menu (); + + var actionGroups = allIssues + .SelectMany (issue => issue.Actions) + .GroupBy (action => action.SiblingKey); + foreach (var _actionGroup in actionGroups) { + var actionGroup = _actionGroup; + + var actionMenuItem = new MenuItem (actionGroup.First ().Title); + actionMenuItem.Clicked += delegate { + ThreadPool.QueueUserWorkItem (delegate { + try { + using (var monitor = IdeApp.Workbench.ProgressMonitors.GetStatusProgressMonitor ("Applying fixes", null, false)) { + var fixer = new BatchFixer (new ExactIssueMatcher (), monitor); + var appliedActions = fixer.TryFixIssues (actionGroup); + foreach (var action in appliedActions) { + ((IIssueTreeNode)action.IssueSummary).Visible = false; + } + } + Application.Invoke (delegate { + ProcessUpdateQueue (); + }); + } catch (Exception e) { + LoggingService.LogInternalError (e); + } + }); + }; + issueMenu.Items.Add (actionMenuItem); + } + return issueMenu; + } + + } + + public class CodeIssuePad : AbstractPadContent + { + CodeIssuePadControl issueControl; + + public override Gtk.Widget Control { + get { + if (issueControl == null) + issueControl = new CodeIssuePadControl (); + return (Gtk.Widget)Toolkit.CurrentEngine.GetNativeWidget (issueControl); + } + } + } +} + |