From 32cc7510ef50c921dd5751fb6b4f55a7d5e10ad8 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 11:38:08 +0200 Subject: [PATCH] Add a filter box to the Debug Steps pane The step tree can run to hundreds of entries per decompile, so finding a specific mutation means scrolling. Add a filter box in the pane's top-right corner: a row survives when its description -- or any descendant's -- contains the text (case-insensitive), keeping the path to every match, and the tree auto-expands while filtering so matches nested under transform groups stay visible. Implemented as an item-visibility converter over the existing TreeView rather than switching to SharpTreeView, which would change the Steps contract and rewrite the pane's tests for no functional gain here. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.Tests/Views/DebugStepsTests.cs | 49 ++++++++++++++++++++ ILSpy/ViewModels/DebugStepsPaneModel.cs | 14 ++++++ ILSpy/Views/DebugStepFilterConverter.cs | 60 +++++++++++++++++++++++++ ILSpy/Views/DebugSteps.axaml | 60 +++++++++++++++++-------- 4 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 ILSpy/Views/DebugStepFilterConverter.cs diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 792742a9e..1690169e3 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -19,11 +19,13 @@ #if DEBUG using System; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Headless.NUnit; +using Avalonia.Threading; using Avalonia.VisualTree; using AwesomeAssertions; @@ -332,6 +334,53 @@ public class DebugStepsTests return Task.CompletedTask; } + [AvaloniaTest] + public Task DebugStepFilter_Keeps_Matches_And_The_Path_To_Them() + { + var converter = new DebugStepFilterConverter(); + var matchingLeaf = new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("3: Introduce query continuation"); + var otherLeaf = new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("4: Flatten switch section block"); + var group = new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("CombineQueryExpressions"); + group.Children.Add(matchingLeaf); + group.Children.Add(otherLeaf); + + // An empty filter shows every row. + Filter(group, "").Should().BeTrue(); + Filter(otherLeaf, " ").Should().BeTrue(); + // A group survives because a descendant matches, keeping the path to the match. + Filter(group, "continuation").Should().BeTrue(); + // The matching leaf survives, case-insensitively. + Filter(matchingLeaf, "CONTINUATION").Should().BeTrue(); + // A sibling that neither matches nor leads to a match is hidden. + Filter(otherLeaf, "continuation").Should().BeFalse(); + return Task.CompletedTask; + + bool Filter(ICSharpCode.Decompiler.IL.Transforms.Stepper.Node node, string filter) + => (bool)converter.Convert(new object?[] { node, filter }, typeof(bool), null, CultureInfo.InvariantCulture); + } + + [AvaloniaTest] + public Task DebugSteps_View_Loads_With_Filter_Applied() + { + // Guards the filter wiring in the XAML -- a MultiBinding inside a TreeViewItem style Setter + // plus the RelativeSource lookups -- against a structural break that x:CompileBindings="False" + // would not catch at build time. Realising the view with a populated tree and a live filter + // must not throw. + var vm = new DebugStepsPaneModel(); + var group = new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("CombineQueryExpressions"); + group.Children.Add(new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("3: Introduce query continuation")); + group.Children.Add(new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("4: Flatten switch section block")); + vm.Steps = new[] { group }; + vm.IsAvailable = true; + + var window = new Window { Width = 400, Height = 300, Content = new DebugSteps { DataContext = vm } }; + window.Show(); + vm.FilterText = "continuation"; + Dispatcher.UIThread.RunJobs(); + window.Close(); + return Task.CompletedTask; + } + [AvaloniaTest] public Task MarkNodeStart_Excludes_Leading_Indentation() { diff --git a/ILSpy/ViewModels/DebugStepsPaneModel.cs b/ILSpy/ViewModels/DebugStepsPaneModel.cs index b0dc7ff70..e26460608 100644 --- a/ILSpy/ViewModels/DebugStepsPaneModel.cs +++ b/ILSpy/ViewModels/DebugStepsPaneModel.cs @@ -100,6 +100,20 @@ namespace ICSharpCode.ILSpy.ViewModels [ObservableProperty] bool isAvailable; + /// + /// Free-text filter for the step tree; empty shows everything. Bound to the filter box in the + /// pane's top-right corner. A row survives when its description -- or a descendant's -- matches. + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsFiltering))] + string? filterText; + + /// + /// True while is non-empty. Drives auto-expansion of the tree so that + /// matches nested under transform groups are revealed rather than hidden in collapsed groups. + /// + public bool IsFiltering => !string.IsNullOrWhiteSpace(FilterText); + public IRelayCommand ShowStateBeforeCommand { get; } public IRelayCommand ShowStateAfterCommand { get; } public IRelayCommand DebugStepCommand { get; } diff --git a/ILSpy/Views/DebugStepFilterConverter.cs b/ILSpy/Views/DebugStepFilterConverter.cs new file mode 100644 index 000000000..f94417afb --- /dev/null +++ b/ILSpy/Views/DebugStepFilterConverter.cs @@ -0,0 +1,60 @@ +// 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. + +#if DEBUG + +using System; +using System.Collections.Generic; +using System.Globalization; + +using Avalonia.Data.Converters; + +using ICSharpCode.Decompiler.IL.Transforms; + +namespace ICSharpCode.ILSpy.Views +{ + /// + /// Decides whether a Debug Steps tree row stays visible under the pane's filter box. A step is + /// shown when the filter is empty, or when its description -- or that of any descendant -- + /// contains the filter text, so the path to every match is preserved. Bound per row against + /// [ the step, the filter text ]. + /// + public sealed class DebugStepFilterConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values.Count < 2 || values[1] is not string filter || string.IsNullOrWhiteSpace(filter)) + return true; + return values[0] is Stepper.Node node && Matches(node, filter.Trim()); + } + + static bool Matches(Stepper.Node node, string filter) + { + if (node.Description.Contains(filter, StringComparison.OrdinalIgnoreCase)) + return true; + foreach (var child in node.Children) + { + if (Matches(child, filter)) + return true; + } + return false; + } + } +} + +#endif diff --git a/ILSpy/Views/DebugSteps.axaml b/ILSpy/Views/DebugSteps.axaml index 44144bf94..594653033 100644 --- a/ILSpy/Views/DebugSteps.axaml +++ b/ILSpy/Views/DebugSteps.axaml @@ -3,40 +3,64 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:ICSharpCode.ILSpy.ViewModels" + xmlns:views="using:ICSharpCode.ILSpy.Views" xmlns:il="using:ICSharpCode.Decompiler.IL" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300" x:Class="ICSharpCode.ILSpy.Views.DebugSteps" x:DataType="vm:DebugStepsPaneModel"> + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +