Browse Source

Analyzer pane copy/remove entries + navigation

Three pieces complete the analyzer feature.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
bd685020fe
  1. 90
      ILSpy.Tests/Analyzers/AnalyzerNavigationTests.cs
  2. 118
      ILSpy.Tests/Analyzers/AnalyzerPaneCopyResultsTests.cs
  3. 94
      ILSpy.Tests/Analyzers/AnalyzerPaneRemoveTests.cs
  4. 32
      ILSpy/Analyzers/AnalyzedModuleTreeNode.cs
  5. 21
      ILSpy/Analyzers/AnalyzerEntityTreeNode.cs
  6. 81
      ILSpy/Analyzers/AnalyzerTreeView.axaml.cs
  7. 75
      ILSpy/Analyzers/CopyAnalysisResultsContextMenuEntry.cs
  8. 56
      ILSpy/Analyzers/RemoveAnalyzeContextMenuEntry.cs

90
ILSpy.Tests/Analyzers/AnalyzerNavigationTests.cs

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions;
using ILSpy.Analyzers.TreeNodes;
using ILSpy.AppEnv;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Analyzers;
[TestFixture]
public class AnalyzerNavigationTests
{
sealed class StubRoutedEventArgs : IPlatformRoutedEventArgs
{
public bool Handled { get; set; }
}
[AvaloniaTest]
public async Task ActivateItem_On_An_AnalyzedTypeTreeNode_Selects_That_TypeTreeNode_In_The_Assembly_Tree()
{
// Double-click on an analyser result must navigate back to the entity's home in
// the assembly tree — same UX as clicking a hyperlink in the decompiler view.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
var entity = (ICSharpCode.Decompiler.TypeSystem.ITypeDefinition)typeNode.Member!;
var analyzed = new AnalyzedTypeTreeNode(entity, source: null);
analyzed.ActivateItem(new StubRoutedEventArgs());
((object?)vm.AssemblyTreeModel.SelectedItem).Should().BeSameAs(typeNode,
"ActivateItem must move the assembly-tree selection to the entity's tree node");
}
[AvaloniaTest]
public async Task ActivateItem_On_An_AnalyzedMethodTreeNode_Selects_The_MethodTreeNode_Sibling()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.EnsureLazyChildren();
var methodTreeNode = typeNode.Children
.OfType<MethodTreeNode>().First(n => n.MethodDefinition.Name == "Empty");
var method = (ICSharpCode.Decompiler.TypeSystem.IMethod)methodTreeNode.Member!;
var analyzed = new AnalyzedMethodTreeNode(method, source: null);
analyzed.ActivateItem(new StubRoutedEventArgs());
((object?)vm.AssemblyTreeModel.SelectedItem).Should().BeSameAs(methodTreeNode,
"ActivateItem must walk down to the right method tree-node under its declaring type");
}
}

118
ILSpy.Tests/Analyzers/AnalyzerPaneCopyResultsTests.cs

@ -0,0 +1,118 @@ @@ -0,0 +1,118 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// 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.Linq;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpyX.TreeView;
using ILSpy;
using ILSpy.Analyzers;
using ILSpy.AppEnv;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Analyzers;
[TestFixture]
public class AnalyzerPaneCopyResultsTests
{
[AvaloniaTest]
public async Task CopyResults_Entry_Is_Visible_For_AnalyzerSearchTreeNode_And_Hidden_Otherwise()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var registry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>();
var entry = registry.Entries
.Single(e => e.Metadata.Header == "Copy results")
.Value;
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
var target = (ICSharpCode.Decompiler.TypeSystem.ITypeDefinition)typeNode.Member!;
var analyzer = AnalyzerTreeNode.Analyzers
.Where(a => a.Metadata?.Header == "Used By")
.Select(a => a.CreateExport().Value)
.First(a => a.Show(target));
var search = new AnalyzerSearchTreeNode(target, analyzer, "Used By");
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { search } })
.Should().BeTrue("Copy results applies to analyzer search-node rows");
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { typeNode } })
.Should().BeFalse("the entry must NOT surface on the assembly tree");
}
[AvaloniaTest]
public async Task CopyResults_Execute_Writes_Each_Child_On_Its_Own_Line()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var registry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>();
var entry = registry.Entries
.Single(e => e.Metadata.Header == "Copy results")
.Value;
// Build a search node and stuff some deterministic children in directly, bypassing
// the analyzer altogether — the copy entry should only care about Text values.
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
var target = (ICSharpCode.Decompiler.TypeSystem.ITypeDefinition)typeNode.Member!;
var analyzer = AnalyzerTreeNode.Analyzers
.Where(a => a.Metadata?.Header == "Used By")
.Select(a => a.CreateExport().Value)
.First(a => a.Show(target));
var search = new AnalyzerSearchTreeNode(target, analyzer, "Used By");
search.Children.Add(new StubResultNode("alpha"));
search.Children.Add(new StubResultNode("beta"));
entry.Execute(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { search } });
// We can't inspect the clipboard contract in a headless test without a window with
// a real lifetime — the test asserts the entry handled execution without throwing
// and the children Text values are still intact (i.e. Execute didn't mutate them).
search.Children[0].Text.Should().Be("alpha");
search.Children[1].Text.Should().Be("beta");
}
sealed class StubResultNode : AnalyzerTreeNode
{
readonly string text;
public StubResultNode(string text) { this.text = text; }
public override object Text => text;
public override bool HandleAssemblyListChanged(
System.Collections.Generic.ICollection<ICSharpCode.ILSpyX.LoadedAssembly> removedAssemblies,
System.Collections.Generic.ICollection<ICSharpCode.ILSpyX.LoadedAssembly> addedAssemblies) => true;
}
}

94
ILSpy.Tests/Analyzers/AnalyzerPaneRemoveTests.cs

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// 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.Linq;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpyX.TreeView;
using ILSpy;
using ILSpy.Analyzers;
using ILSpy.AppEnv;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Analyzers;
[TestFixture]
public class AnalyzerPaneRemoveTests
{
[AvaloniaTest]
public async Task Remove_Entry_Is_Visible_Only_For_Top_Level_Analysed_Entities()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var registry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>();
var entry = registry.Entries
.Single(e => e.Metadata.Header == "Remove"
&& e.Value is RemoveAnalyzeContextMenuEntry)
.Value;
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
var analyzerVm = AppComposition.Current.GetExport<AnalyzerTreeViewModel>();
var added = analyzerVm.Analyze((ICSharpCode.Decompiler.TypeSystem.IEntity)typeNode.Member!);
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { added } })
.Should().BeTrue("Remove applies to top-level analysed entities");
entry.IsVisible(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { typeNode } })
.Should().BeFalse("Remove must not surface on the assembly tree");
}
[AvaloniaTest]
public async Task Remove_Execute_Drops_The_Selected_Root_Children()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var registry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>();
var entry = registry.Entries
.Single(e => e.Metadata.Header == "Remove"
&& e.Value is RemoveAnalyzeContextMenuEntry)
.Value;
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
var analyzerVm = AppComposition.Current.GetExport<AnalyzerTreeViewModel>();
var added = analyzerVm.Analyze((ICSharpCode.Decompiler.TypeSystem.IEntity)typeNode.Member!);
var beforeCount = analyzerVm.Root.Children.Count;
entry.Execute(new TextViewContext { SelectedTreeNodes = new SharpTreeNode[] { added } });
analyzerVm.Root.Children.Should().NotContain(added,
"Execute on Remove must drop the row from the pane root");
analyzerVm.Root.Children.Count.Should().Be(beforeCount - 1);
}
}

32
ILSpy/Analyzers/AnalyzedModuleTreeNode.cs

@ -18,9 +18,15 @@ @@ -18,9 +18,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions;
using ILSpy.AppEnv;
using ILSpy.AssemblyTree;
using ILSpy.TreeNodes;
namespace ILSpy.Analyzers.TreeNodes
{
@ -50,6 +56,32 @@ namespace ILSpy.Analyzers.TreeNodes @@ -50,6 +56,32 @@ namespace ILSpy.Analyzers.TreeNodes
public IModule Module => analyzedModule;
public override void ActivateItem(IPlatformRoutedEventArgs e)
{
e.Handled = true;
if (analyzedModule.MetadataFile == null)
return;
try
{
var assemblyTreeModel = AppComposition.Current.GetExport<AssemblyTreeModel>();
var loaded = assemblyTreeModel.AssemblyList?
.GetAssemblies()
.FirstOrDefault(a => a.GetMetadataFileOrNull() == analyzedModule.MetadataFile);
if (loaded == null)
return;
var listRoot = assemblyTreeModel.Root as AssemblyListTreeNode;
if (listRoot == null)
return;
var node = listRoot.FindAssemblyNode(loaded);
if (node != null)
assemblyTreeModel.SelectedItem = node;
}
catch
{
// Design-time / minimal-test path.
}
}
protected override void LoadChildren()
{
foreach (var factory in Analyzers)

21
ILSpy/Analyzers/AnalyzerEntityTreeNode.cs

@ -21,6 +21,10 @@ using System.Collections.Generic; @@ -21,6 +21,10 @@ using System.Collections.Generic;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.TreeView;
using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions;
using ILSpy.AppEnv;
using ILSpy.AssemblyTree;
namespace ILSpy.Analyzers
{
@ -33,6 +37,23 @@ namespace ILSpy.Analyzers @@ -33,6 +37,23 @@ namespace ILSpy.Analyzers
/// </summary>
public abstract class AnalyzerEntityTreeNode : AnalyzerTreeNode
{
public override void ActivateItem(IPlatformRoutedEventArgs e)
{
e.Handled = true;
if (Member == null || Member.MetadataToken.IsNil)
return;
try
{
var assemblyTreeModel = AppComposition.Current.GetExport<AssemblyTreeModel>();
if (assemblyTreeModel.FindTreeNode(Member) is { } resolved)
assemblyTreeModel.SelectedItem = resolved;
}
catch
{
// Composition isn't available in design-time previews — gesture is a no-op there.
}
}
/// <summary>
/// The entity this row analyses (or represents as an analyser result). Subclasses
/// return <see langword="null"/> only for non-entity rows such as

81
ILSpy/Analyzers/AnalyzerTreeView.axaml.cs

@ -21,10 +21,16 @@ using System.Collections.Generic; @@ -21,10 +21,16 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.DataGridHierarchical;
using Avalonia.Input;
using Avalonia.VisualTree;
using ICSharpCode.ILSpyX.TreeView;
using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions;
using ILSpy.AppEnv;
namespace ILSpy.Analyzers
{
@ -32,10 +38,85 @@ namespace ILSpy.Analyzers @@ -32,10 +38,85 @@ namespace ILSpy.Analyzers
{
bool syncingSelection;
AnalyzerTreeViewModel? boundModel;
IReadOnlyList<IContextMenuEntryExport> contextMenuEntries = Array.Empty<IContextMenuEntryExport>();
public AnalyzerTreeView()
{
InitializeComponent();
TreeGrid.DoubleTapped += OnTreeGridDoubleTapped;
var registry = TryGetContextMenuRegistry();
AttachContextMenu(registry?.Entries ?? Array.Empty<IContextMenuEntryExport>());
}
static ContextMenuEntryRegistry? TryGetContextMenuRegistry()
{
try
{
return AppComposition.Current.GetExport<ContextMenuEntryRegistry>();
}
catch
{
return null;
}
}
internal void AttachContextMenu(IReadOnlyList<IContextMenuEntryExport> entries)
{
contextMenuEntries = entries;
var menu = new ContextMenu();
menu.Opening += OnContextMenuOpening;
TreeGrid.ContextMenu = menu;
}
void OnContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
{
if (sender is not ContextMenu menu)
return;
var built = BuildContextMenuForCurrentState(contextMenuEntries);
if (built == null)
{
e.Cancel = true;
return;
}
menu.Items.Clear();
foreach (var item in built.Items.OfType<Control>().ToArray())
{
built.Items.Remove(item);
menu.Items.Add(item);
}
}
internal ContextMenu? BuildContextMenuForCurrentState(IReadOnlyList<IContextMenuEntryExport> entries)
=> ContextMenuProvider.Build(entries, CreateContextMenuContext());
TextViewContext CreateContextMenuContext()
{
var nodes = boundModel?.SelectedItems.ToArray() ?? Array.Empty<SharpTreeNode>();
return new TextViewContext {
TreeGrid = TreeGrid,
SelectedTreeNodes = nodes,
};
}
void OnTreeGridDoubleTapped(object? sender, TappedEventArgs e)
{
// Walk up the visual tree to find the row's HierarchicalNode wrapper, unwrap it
// to the SharpTreeNode, then route to ActivateItem so the entity row navigates
// back to its node in the assembly tree.
var visual = e.Source as Visual;
while (visual != null && visual.DataContext is not HierarchicalNode)
visual = visual.GetVisualParent();
if (visual?.DataContext is not HierarchicalNode hn)
return;
if (hn.Item is not SharpTreeNode node)
return;
node.ActivateItem(new StubRoutedEventArgs());
e.Handled = true;
}
sealed class StubRoutedEventArgs : IPlatformRoutedEventArgs
{
public bool Handled { get; set; }
}
protected override void OnDataContextChanged(EventArgs e)

75
ILSpy/Analyzers/CopyAnalysisResultsContextMenuEntry.cs

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// 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.Composition;
using System.Linq;
using System.Text;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input.Platform;
namespace ILSpy.Analyzers
{
/// <summary>
/// Right-click on an analyser-result row → "Copy results" — puts each child node's
/// Text on the clipboard, one per line. Visible only when the selection is an
/// <see cref="AnalyzerSearchTreeNode"/> (the headers under an analysed entity).
/// </summary>
[ExportContextMenuEntry(Header = "Copy results", Order = 9100)]
[Shared]
public sealed class CopyAnalysisResultsContextMenuEntry : IContextMenuEntry
{
public bool IsVisible(TextViewContext context)
{
if (context.SelectedTreeNodes is not { Length: > 0 } nodes)
return false;
return nodes.All(n => n is AnalyzerSearchTreeNode);
}
public bool IsEnabled(TextViewContext context) => IsVisible(context);
public void Execute(TextViewContext context)
{
if (context.SelectedTreeNodes is not { Length: > 0 } nodes)
return;
var builder = new StringBuilder();
foreach (var node in nodes.OfType<AnalyzerSearchTreeNode>())
{
foreach (var child in node.Children)
{
var text = child.Text?.ToString();
if (!string.IsNullOrEmpty(text))
builder.AppendLine(text);
}
}
if (builder.Length == 0)
return;
TryWriteToClipboard(builder.ToString());
}
static void TryWriteToClipboard(string payload)
{
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime lifetime
|| lifetime.MainWindow?.Clipboard is not { } clipboard)
return;
// SetTextAsync is the extension method on IClipboard — needs Avalonia.Input.Platform.
_ = clipboard.SetTextAsync(payload);
}
}
}

56
ILSpy/Analyzers/RemoveAnalyzeContextMenuEntry.cs

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// 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.Composition;
using System.Linq;
using ICSharpCode.ILSpyX.TreeView;
namespace ILSpy.Analyzers
{
/// <summary>
/// Right-click on a top-level analysed entity → "Remove" — drops the row from the
/// pane root. Only visible on rows whose parent is the analyzer root (the per-entity
/// rows); search-tree-node headers and result rows can't be removed individually.
/// </summary>
[ExportContextMenuEntry(Header = "Remove", Order = 9200)]
[Shared]
public sealed class RemoveAnalyzeContextMenuEntry : IContextMenuEntry
{
public bool IsVisible(TextViewContext context)
{
if (context.SelectedTreeNodes is not { Length: > 0 } nodes)
return false;
return nodes.All(IsRemovableRoot);
}
public bool IsEnabled(TextViewContext context) => IsVisible(context);
public void Execute(TextViewContext context)
{
if (context.SelectedTreeNodes is not { Length: > 0 } nodes)
return;
foreach (var node in nodes.Where(IsRemovableRoot).ToArray())
node.Parent?.Children.Remove(node);
}
static bool IsRemovableRoot(SharpTreeNode node)
=> node is AnalyzerTreeNode and AnalyzerEntityTreeNode
&& node.Parent is { IsRoot: true };
}
}
Loading…
Cancel
Save