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