Browse Source

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
pull/3847/head
Siegfried Pammer 6 days ago committed by Siegfried Pammer
parent
commit
32cc7510ef
  1. 49
      ILSpy.Tests/Views/DebugStepsTests.cs
  2. 14
      ILSpy/ViewModels/DebugStepsPaneModel.cs
  3. 60
      ILSpy/Views/DebugStepFilterConverter.cs
  4. 60
      ILSpy/Views/DebugSteps.axaml

49
ILSpy.Tests/Views/DebugStepsTests.cs

@ -19,11 +19,13 @@ @@ -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 @@ -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()
{

14
ILSpy/ViewModels/DebugStepsPaneModel.cs

@ -100,6 +100,20 @@ namespace ICSharpCode.ILSpy.ViewModels @@ -100,6 +100,20 @@ namespace ICSharpCode.ILSpy.ViewModels
[ObservableProperty]
bool isAvailable;
/// <summary>
/// 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.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsFiltering))]
string? filterText;
/// <summary>
/// True while <see cref="FilterText"/> is non-empty. Drives auto-expansion of the tree so that
/// matches nested under transform groups are revealed rather than hidden in collapsed groups.
/// </summary>
public bool IsFiltering => !string.IsNullOrWhiteSpace(FilterText);
public IRelayCommand ShowStateBeforeCommand { get; }
public IRelayCommand ShowStateAfterCommand { get; }
public IRelayCommand DebugStepCommand { get; }

60
ILSpy/Views/DebugStepFilterConverter.cs

@ -0,0 +1,60 @@ @@ -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
{
/// <summary>
/// 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 ].
/// </summary>
public sealed class DebugStepFilterConverter : IMultiValueConverter
{
public object Convert(IList<object?> 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

60
ILSpy/Views/DebugSteps.axaml

@ -3,40 +3,64 @@ @@ -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">
<UserControl.Resources>
<views:DebugStepFilterConverter x:Key="StepFilter" />
</UserControl.Resources>
<Panel>
<TextBlock IsVisible="{Binding !IsAvailable}"
HorizontalAlignment="Center" VerticalAlignment="Center"
Margin="8" TextWrapping="Wrap" Opacity="0.7"
Text="Debug steps are not available for the current language." />
<DockPanel IsVisible="{Binding IsAvailable}">
<!-- Options are contributed by the active step-provider language and selected by type:
ILAst renders its writing-options checkboxes; C# (null Options) renders nothing. -->
<ContentControl DockPanel.Dock="Top" Margin="2" Content="{Binding Options}">
<ContentControl.DataTemplates>
<DataTemplate DataType="il:ILAstWritingOptions">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="3" Content="Field sugar"
IsChecked="{Binding UseFieldSugar, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Logic operation sugar"
IsChecked="{Binding UseLogicOperationSugar, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Show IL ranges"
IsChecked="{Binding ShowILRanges, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Show child index in block"
IsChecked="{Binding ShowChildIndexInBlock, Mode=TwoWay}" />
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<!-- Top row: language-contributed options on the left, filter box in the top-right corner.
Options are selected by type: ILAst renders its writing-options checkboxes; C# (null
Options) renders nothing, leaving just the filter box. -->
<DockPanel DockPanel.Dock="Top" Margin="2">
<TextBox DockPanel.Dock="Right" Width="160" Margin="4,0,0,0"
VerticalAlignment="Center" PlaceholderText="Filter steps"
Text="{Binding FilterText}" />
<ContentControl Content="{Binding Options}">
<ContentControl.DataTemplates>
<DataTemplate DataType="il:ILAstWritingOptions">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="3" Content="Field sugar"
IsChecked="{Binding UseFieldSugar, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Logic operation sugar"
IsChecked="{Binding UseLogicOperationSugar, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Show IL ranges"
IsChecked="{Binding ShowILRanges, Mode=TwoWay}" />
<CheckBox Margin="3" Content="Show child index in block"
IsChecked="{Binding ShowChildIndexInBlock, Mode=TwoWay}" />
</StackPanel>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</DockPanel>
<TreeView Name="StepsTree"
ItemsSource="{Binding Steps}"
SelectedItem="{Binding SelectedStep, Mode=TwoWay}"
DoubleTapped="OnTreeDoubleTapped"
KeyDown="OnTreeKeyDown"
x:CompileBindings="False">
<TreeView.Styles>
<!-- Hide rows that neither match the filter nor lead to a match, and expand the tree
while filtering so surviving matches under transform groups stay visible. -->
<Style Selector="TreeViewItem">
<Setter Property="IsVisible">
<MultiBinding Converter="{StaticResource StepFilter}">
<Binding />
<Binding Path="DataContext.FilterText" RelativeSource="{RelativeSource AncestorType=TreeView}" />
</MultiBinding>
</Setter>
<Setter Property="IsExpanded"
Value="{Binding DataContext.IsFiltering, RelativeSource={RelativeSource AncestorType=TreeView}}" />
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Description}" />

Loading…
Cancel
Save