diff --git a/ILSpy.Tests/Views/DebugStepsFilterStateTests.cs b/ILSpy.Tests/Views/DebugStepsFilterStateTests.cs new file mode 100644 index 000000000..7b380630f --- /dev/null +++ b/ILSpy.Tests/Views/DebugStepsFilterStateTests.cs @@ -0,0 +1,175 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Avalonia.Controls; +using Avalonia.Headless.NUnit; +using Avalonia.Threading; +using Avalonia.VisualTree; + +using AwesomeAssertions; + +using ICSharpCode.Decompiler.DebugSteps; + +using ICSharpCode.ILSpy.ViewModels; +using ICSharpCode.ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Views; + +/// +/// The Debug Steps filter box must be a transient view on the step tree: it may expand and +/// hide rows while active, but expansion states the user established before filtering and +/// the selection made while filtering must survive clearing it. Rows are located through the +/// visual tree (header text -> nearest TreeViewItem) and user gestures are simulated with +/// SetCurrentValue, which is what every real expander gesture (toggle-arrow, double-tap, +/// arrow keys) ends in — a plain local SetValue would mask style-driven bindings and test a +/// gesture that cannot occur. +/// +[TestFixture] +public class DebugStepsFilterStateTests +{ + const string MatchGroupDescription = "CombineQueryExpressions"; + const string OtherGroupDescription = "TransformExpressionTrees"; + const string MatchingLeafDescription = "3: Introduce query continuation"; + const string SiblingLeafDescription = "4: Flatten switch section block"; + const string OtherLeafDescription = "7: Copy annotations"; + + static IList BuildStepTree() + { + var matchGroup = new Stepper.Node(MatchGroupDescription); + matchGroup.Children.Add(new Stepper.Node(MatchingLeafDescription)); + matchGroup.Children.Add(new Stepper.Node(SiblingLeafDescription)); + var otherGroup = new Stepper.Node(OtherGroupDescription); + otherGroup.Children.Add(new Stepper.Node(OtherLeafDescription)); + return new[] { matchGroup, otherGroup }; + } + + static (Window Window, DebugStepsPaneModel Vm, TreeView Tree) ShowPane() + { + var vm = new DebugStepsPaneModel(); + SetSteps(vm, BuildStepTree()); + vm.IsAvailable = true; + var window = new Window { Width = 400, Height = 300, Content = new DebugSteps { DataContext = vm } }; + window.Show(); + Dispatcher.UIThread.RunJobs(); + var tree = window.GetVisualDescendants().OfType().First(); + return (window, vm, tree); + } + + static void SetSteps(DebugStepsPaneModel vm, IList steps) + { + vm.SetStepsSource(steps); + } + + // Rows are looked up by DataContext rather than by header text: a row hidden by the filter + // is never measured, so its header TextBlock may not have been templated yet, but its + // container still exists in the visual tree. + static TreeViewItem RowFor(TreeView tree, string description) + { + var row = tree.GetVisualDescendants().OfType() + .FirstOrDefault(item => DescriptionOf(item.DataContext) == description); + row.Should().NotBeNull($"a row with the description '{description}' must be materialised"); + return row!; + } + + static string? DescriptionOf(object? dataContext) => dataContext switch { + StepNodeViewModel node => node.Description, + _ => null, + }; + + [AvaloniaTest] + public Task Manual_Expansion_Survives_A_Filter_Round_Trip() + { + var (window, vm, tree) = ShowPane(); + var matchGroup = RowFor(tree, MatchGroupDescription); + var otherGroup = RowFor(tree, OtherGroupDescription); + + matchGroup.SetCurrentValue(TreeViewItem.IsExpandedProperty, true); + Dispatcher.UIThread.RunJobs(); + + vm.FilterText = "continuation"; + Dispatcher.UIThread.RunJobs(); + vm.FilterText = ""; + Dispatcher.UIThread.RunJobs(); + + matchGroup.IsExpanded.Should().BeTrue( + "a group the user expanded before filtering must still be expanded after the filter is cleared"); + otherGroup.IsExpanded.Should().BeFalse( + "a group the user never expanded must not stay expanded after the filter is cleared"); + + window.Close(); + return Task.CompletedTask; + } + + [AvaloniaTest] + public Task Filtering_Expands_Groups_Leading_To_Matches_And_Hides_The_Rest() + { + var (window, vm, tree) = ShowPane(); + + vm.FilterText = "continuation"; + Dispatcher.UIThread.RunJobs(); + + var matchGroup = RowFor(tree, MatchGroupDescription); + matchGroup.IsExpanded.Should().BeTrue( + "a group containing a match must expand so the match is revealed"); + matchGroup.IsVisible.Should().BeTrue("the path to a match must stay visible"); + RowFor(tree, MatchingLeafDescription).IsVisible.Should().BeTrue("the match itself must be visible"); + RowFor(tree, SiblingLeafDescription).IsVisible.Should().BeFalse( + "a sibling that does not match must be hidden"); + RowFor(tree, OtherGroupDescription).IsVisible.Should().BeFalse( + "a group without any match must be hidden"); + + window.Close(); + return Task.CompletedTask; + } + + [AvaloniaTest] + public Task Selection_Made_While_Filtering_Stays_Visible_After_Clearing() + { + var (window, vm, tree) = ShowPane(); + + vm.FilterText = "continuation"; + Dispatcher.UIThread.RunJobs(); + + var leaf = RowFor(tree, MatchingLeafDescription); + tree.SelectedItem = leaf.DataContext; + Dispatcher.UIThread.RunJobs(); + + vm.FilterText = ""; + Dispatcher.UIThread.RunJobs(); + + tree.SelectedItem.Should().BeSameAs(leaf.DataContext, + "clearing the filter must not change the selection"); + RowFor(tree, MatchGroupDescription).IsExpanded.Should().BeTrue( + "the selected step's group must stay expanded so the selection remains visible"); + leaf.IsEffectivelyVisible.Should().BeTrue( + "the step selected while filtering must still be on screen after the filter is cleared"); + + window.Close(); + return Task.CompletedTask; + } +} + +#endif diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index ef49b192b..251201477 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -19,7 +19,6 @@ #if DEBUG using System; -using System.Globalization; using System.Linq; using System.Threading.Tasks; @@ -173,7 +172,7 @@ public class DebugStepsTests "nested C# debug steps must describe individual AST mutation points"); var collectedSteps = debugStepsVm.Steps; - var replayStep = transformGroupWithChanges.Children.First(); + var replayStep = transformGroupWithChanges.Children.First().Step; var tab = vm.DockWorkspace.ActiveDecompilerTab!; await tab.RestartDecompileWithStepLimit(replayStep.BeginStep, isDebug: false, replayStep.BeginStep); @@ -240,14 +239,14 @@ public class DebugStepsTests // The first leaf step that acts on a concrete instruction; a step whose Position is null // (e.g. an empty transform group) has nothing to highlight and is not what a user replays. - static Stepper.Node? FirstLeafStep(System.Collections.Generic.IEnumerable steps) + static Stepper.Node? FirstLeafStep(System.Collections.Generic.IEnumerable steps) { foreach (var step in steps) { if (step.Children.Count == 0) { - if (step.Position != null) - return step; + if (step.Step.Position != null) + return step.Step; continue; } var leaf = FirstLeafStep(step.Children); @@ -347,40 +346,44 @@ public class DebugStepsTests [AvaloniaTest] public Task DebugStepFilter_Keeps_Matches_And_The_Path_To_Them() { - var converter = new DebugStepFilterConverter(); + var vm = new DebugStepsPaneModel(); var matchingLeaf = new Stepper.Node("3: Introduce query continuation"); var otherLeaf = new Stepper.Node("4: Flatten switch section block"); var group = new 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(); + vm.SetStepsSource(new[] { group }); + + var groupVm = vm.Steps![0]; + + // Matching is case-insensitive; a group survives because a descendant matches (keeping + // the path to the match open), while a sibling that neither matches nor leads to a + // match is hidden. + vm.FilterText = "CONTINUATION"; + groupVm.IsVisible.Should().BeTrue(); + groupVm.IsExpanded.Should().BeTrue(); + groupVm.Children[0].IsVisible.Should().BeTrue(); + groupVm.Children[1].IsVisible.Should().BeFalse(); + + // A whitespace-only filter counts as empty and shows every row again. + vm.FilterText = " "; + groupVm.IsVisible.Should().BeTrue(); + groupVm.Children[0].IsVisible.Should().BeTrue(); + groupVm.Children[1].IsVisible.Should().BeTrue(); return Task.CompletedTask; - - bool Filter(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. + // Guards the filter wiring in the XAML -- the per-row style Setter bindings -- 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 Stepper.Node("CombineQueryExpressions"); group.Children.Add(new Stepper.Node("3: Introduce query continuation")); group.Children.Add(new Stepper.Node("4: Flatten switch section block")); - vm.Steps = new[] { group }; + vm.SetStepsSource(new[] { group }); vm.IsAvailable = true; var window = new Window { Width = 400, Height = 300, Content = new DebugSteps { DataContext = vm } }; @@ -483,7 +486,7 @@ public class DebugStepsTests debugStepsVm.IsAvailable.Should().BeTrue("C# provides debug steps"); // Simulate a populated tree from the C# run, then flip to the disassembler language. - debugStepsVm.Steps = new[] { new Stepper.Node("stale") }; + debugStepsVm.SetStepsSource(new[] { new Stepper.Node("stale") }); languageService.CurrentLanguage = languageService.Languages.OfType().First(l => l.Name == "IL"); global::Avalonia.Threading.Dispatcher.UIThread.RunJobs(); diff --git a/ILSpy/ViewModels/DebugStepsPaneModel.cs b/ILSpy/ViewModels/DebugStepsPaneModel.cs index 013841852..2f7603f22 100644 --- a/ILSpy/ViewModels/DebugStepsPaneModel.cs +++ b/ILSpy/ViewModels/DebugStepsPaneModel.cs @@ -81,16 +81,31 @@ namespace ICSharpCode.ILSpy.ViewModels object? options; /// - /// Currently displayed list of recorded transform steps. Re-assigned (not mutated) - /// whenever the active step provider's reports a new run, so - /// late-binding views pick up the latest list via the observable change. + /// The recorded transform steps currently backing . Tracked so that + /// re-assignments of the same run (each step replay pumps StepperUpdated with the same + /// list instance) don't rebuild the wrapper tree and thereby wipe its expansion and + /// selection state. + /// + IList? stepsSource; + + /// + /// True while the wrapper tree holds a pre-filter expansion snapshot, i.e. from the first + /// non-empty until the filter is cleared or the tree is rebuilt. + /// + bool filterSnapshotTaken; + + /// + /// Currently displayed tree of recorded transform steps, wrapped with per-row UI state. + /// Re-assigned (not mutated) whenever the active step provider's + /// reports a new run, so late-binding views pick up the latest tree via the observable + /// change. /// [ObservableProperty] - IList? steps; + IReadOnlyList? steps; /// Two-way bound to the TreeView's selected item. [ObservableProperty] - Stepper.Node? selectedStep; + StepNodeViewModel? selectedStep; /// /// True while the current language is an . When false, @@ -109,8 +124,9 @@ namespace ICSharpCode.ILSpy.ViewModels 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. + /// True while is non-empty. A filter session is transient: the + /// expansion states are snapshotted when it starts and restored when it ends, so filtering + /// never destroys the tree state the user built up manually. /// public bool IsFiltering => !string.IsNullOrWhiteSpace(FilterText); @@ -123,8 +139,8 @@ namespace ICSharpCode.ILSpy.ViewModels { Id = PaneContentId; Title = "Debug Steps"; - ShowStateBeforeCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: false, SelectedStep?.BeginStep)); - ShowStateAfterCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.EndStep ?? int.MaxValue, isDebug: false, SelectedStep?.BeginStep)); + ShowStateBeforeCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.Step.BeginStep ?? int.MaxValue, isDebug: false, SelectedStep?.Step.BeginStep)); + ShowStateAfterCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.Step.EndStep ?? int.MaxValue, isDebug: false, SelectedStep?.Step.BeginStep)); DebugStepCommand = new RelayCommand(() => { // "Debug this step" relies on Stepper.Step calling Debugger.Break() when // step == StepLimit — which is a silent no-op without a debugger attached. @@ -137,7 +153,7 @@ namespace ICSharpCode.ILSpy.ViewModels if (!System.Diagnostics.Debugger.Launch()) AppEnv.AppLog.Mark("DebugStep: Debugger.Launch returned false; the upcoming Stepper.Step break is a no-op without a debugger attached."); } - RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: true, SelectedStep?.BeginStep); + RequestRedecompile(SelectedStep?.Step.BeginStep ?? int.MaxValue, isDebug: true, SelectedStep?.Step.BeginStep); }); } @@ -183,14 +199,14 @@ namespace ICSharpCode.ILSpy.ViewModels { // Same language instance — just refresh the steps in case a decompile happened // while we were detached. - Steps = activeLanguage!.Stepper.Steps; + SetStepsSource(activeLanguage!.Stepper.Steps); IsAvailable = true; return; } DetachFromLanguage(); activeLanguage = language; language.StepperUpdated += OnStepperUpdated; - Steps = language.Stepper.Steps; + SetStepsSource(language.Stepper.Steps); Options = language.StepOptions; IsAvailable = true; } @@ -198,7 +214,7 @@ namespace ICSharpCode.ILSpy.ViewModels void DetachFromLanguage() { IsAvailable = false; - Steps = null; + SetStepsSource(null); if (activeLanguage != null) { activeLanguage.StepperUpdated -= OnStepperUpdated; @@ -212,12 +228,104 @@ namespace ICSharpCode.ILSpy.ViewModels Dispatcher.UIThread.Post(() => { if (activeLanguage != null) { - Steps = activeLanguage.Stepper.Steps; + SetStepsSource(activeLanguage.Stepper.Steps); lastSelectedStep = int.MaxValue; } }); } + /// + /// Replaces the displayed step tree with a wrapper tree over . + /// A reference-equal source is a no-op: step replays re-report the same run, and + /// rebuilding the wrappers then would discard the expansion and selection state the + /// user is navigating with. + /// + public void SetStepsSource(IList? source) + { + if (ReferenceEquals(stepsSource, source)) + return; + stepsSource = source; + filterSnapshotTaken = false; + Steps = source == null ? null : StepNodeViewModel.Wrap(source); + if (Steps != null && IsFiltering) + ApplyFilter(); + } + + partial void OnFilterTextChanged(string? value) + { + ApplyFilter(); + } + + /// + /// Reflects the current in the wrapper tree's per-row state. + /// Entering a filter session snapshots each row's expansion first; every filter change + /// hides non-matching rows and expands the groups on the path to each match; leaving the + /// session restores the snapshot and then re-expands the selected step's ancestors so the + /// selection never vanishes into a collapsed group. + /// + void ApplyFilter() + { + if (Steps == null) + { + filterSnapshotTaken = false; + return; + } + if (IsFiltering) + { + if (!filterSnapshotTaken) + { + filterSnapshotTaken = true; + foreach (var step in Steps) + SnapshotExpansion(step); + } + string filter = FilterText!.Trim(); + foreach (var step in Steps) + ApplyFilterToNode(step, filter); + } + else if (filterSnapshotTaken) + { + filterSnapshotTaken = false; + foreach (var step in Steps) + RestoreExpansion(step); + for (var ancestor = SelectedStep?.Parent; ancestor != null; ancestor = ancestor.Parent) + ancestor.IsExpanded = true; + } + } + + static void SnapshotExpansion(StepNodeViewModel node) + { + node.ExpansionBeforeFilter = node.IsExpanded; + foreach (var child in node.Children) + SnapshotExpansion(child); + } + + static void RestoreExpansion(StepNodeViewModel node) + { + node.IsVisible = true; + if (node.ExpansionBeforeFilter is bool expanded) + node.IsExpanded = expanded; + node.ExpansionBeforeFilter = null; + foreach (var child in node.Children) + RestoreExpansion(child); + } + + /// + /// Applies the filter to one subtree: a row stays visible when its description — or a + /// descendant's — contains the filter (ordinal, case-insensitive), and a group with a + /// surviving descendant is expanded so the path to every match is open. + /// + static bool ApplyFilterToNode(StepNodeViewModel node, string filter) + { + bool descendantMatches = false; + foreach (var child in node.Children) + descendantMatches |= ApplyFilterToNode(child, filter); + bool selfMatches = node.Description.Contains(filter, System.StringComparison.OrdinalIgnoreCase); + node.IsVisible = selfMatches || descendantMatches; + if (descendantMatches) + node.IsExpanded = true; + return selfMatches || descendantMatches; + } + void OnSelectionChanged(object? sender, AssemblyTreeSelectionChangedEventArgs e) { // User picked a new tree node — the previous run's stepper is stale until the next @@ -237,7 +345,7 @@ namespace ICSharpCode.ILSpy.ViewModels void ClearSteps() { - Steps = null; + SetStepsSource(null); lastSelectedStep = int.MaxValue; } } diff --git a/ILSpy/ViewModels/StepNodeViewModel.cs b/ILSpy/ViewModels/StepNodeViewModel.cs new file mode 100644 index 000000000..ed09b1138 --- /dev/null +++ b/ILSpy/ViewModels/StepNodeViewModel.cs @@ -0,0 +1,83 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// 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.Collections.Generic; + +using CommunityToolkit.Mvvm.ComponentModel; + +using ICSharpCode.Decompiler.DebugSteps; + +namespace ICSharpCode.ILSpy.ViewModels +{ + /// + /// Per-row UI state for the Debug Steps tree. is a decompiler-side + /// record with no notion of expansion or visibility, and TreeViewItem containers cannot carry + /// that state reliably either: every expander gesture writes IsExpanded via SetCurrentValue, + /// which any style-driven binding overwrites the next time it produces a value. Keeping the + /// state on a view-model wrapper makes it authoritative — the view binds each row's + /// IsVisible/IsExpanded here, and the pane can snapshot and restore expansion around filter + /// sessions instead of losing it to the containers. + /// + public sealed partial class StepNodeViewModel : ObservableObject + { + public Stepper.Node Step { get; } + public StepNodeViewModel? Parent { get; } + public IReadOnlyList Children { get; } + public string Description => Step.Description; + + /// Two-way bound to the row's TreeViewItem.IsExpanded. + [ObservableProperty] + bool isExpanded; + + /// Bound to the row's TreeViewItem.IsVisible; false while the filter hides the row. + [ObservableProperty] + bool isVisible = true; + + /// + /// Expansion state captured when a filter session starts, restored when it ends. + /// Null outside filter sessions. + /// + internal bool? ExpansionBeforeFilter { get; set; } + + StepNodeViewModel(Stepper.Node step, StepNodeViewModel? parent) + { + Step = step; + Parent = parent; + var children = new List(step.Children.Count); + foreach (var child in step.Children) + { + children.Add(new StepNodeViewModel(child, this)); + } + Children = children; + } + + public static IReadOnlyList Wrap(IList steps) + { + var wrapped = new List(steps.Count); + foreach (var step in steps) + { + wrapped.Add(new StepNodeViewModel(step, null)); + } + return wrapped; + } + } +} + +#endif diff --git a/ILSpy/Views/DebugStepFilterConverter.cs b/ILSpy/Views/DebugStepFilterConverter.cs deleted file mode 100644 index 5e78a9ccf..000000000 --- a/ILSpy/Views/DebugStepFilterConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -// 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.DebugSteps; - -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 591997054..f258334d5 100644 --- a/ILSpy/Views/DebugSteps.axaml +++ b/ILSpy/Views/DebugSteps.axaml @@ -3,14 +3,10 @@ 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"> - - - - +