mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
ILSpy showed the current location only implicitly in the tree selection and kept search in a separate docked pane. The omnibar, modelled on the Files community app and jiripolasek's EditorBar, puts an address-bar atop each decompiler document: a breadcrumb of the node (Assembly > Namespace > Type > Member) whose segments navigate and whose chevrons list child nodes, turning into a search box on typing that reuses the existing search engine. It coexists with the docked search pane and ships off by default behind the Options / Display 'Tab options' EnableOmnibar toggle, which applies live. Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3794/head
16 changed files with 1026 additions and 1 deletions
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
// 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 ICSharpCode.ILSpy.Controls.Omnibar; |
||||
using ICSharpCode.ILSpy.TreeNodes; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Controls; |
||||
|
||||
/// <summary>
|
||||
/// The omnibar breadcrumb is the decompiler's "you are here" trail: it must mirror the
|
||||
/// assembly-tree path of the decompiled node (Assembly > Namespace > Type > Member), excluding
|
||||
/// the invisible tree root, and clicking a segment must move the tree selection there.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class OmnibarBreadcrumbTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task BuildSegments_Yields_Assembly_Namespace_Type_Member_In_Order() |
||||
{ |
||||
var (_, vm) = await TestHarness.BootAsync(3); |
||||
|
||||
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>( |
||||
"System.Linq", "System.Linq", "System.Linq.Enumerable"); |
||||
typeNode.EnsureLazyChildren(); |
||||
var methodNode = typeNode.Children.OfType<MethodTreeNode>() |
||||
.First(m => m.MethodDefinition.Name == "Empty"); |
||||
|
||||
var segments = OmnibarViewModel.BuildSegments(methodNode); |
||||
|
||||
Assert.That(segments, Has.Count.EqualTo(4), |
||||
"a member node breadcrumb is Assembly > Namespace > Type > Member (root excluded)"); |
||||
Assert.That(segments[0].Node, Is.InstanceOf<AssemblyTreeNode>(), |
||||
"the first crumb is the assembly, not the invisible AssemblyListTreeNode root"); |
||||
Assert.That(segments[1].Node, Is.InstanceOf<NamespaceTreeNode>()); |
||||
Assert.That(segments[1].Text, Is.EqualTo("System.Linq")); |
||||
Assert.That(segments[2].Node, Is.SameAs(typeNode)); |
||||
Assert.That(segments[3].Node, Is.SameAs(methodNode)); |
||||
Assert.That(segments[3].Text, Does.Contain("Empty")); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task Leaf_Crumb_Has_No_Children_While_Container_Crumbs_Do() |
||||
{ |
||||
var (_, vm) = await TestHarness.BootAsync(3); |
||||
|
||||
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>( |
||||
"System.Linq", "System.Linq", "System.Linq.Enumerable"); |
||||
typeNode.EnsureLazyChildren(); |
||||
var methodNode = typeNode.Children.OfType<MethodTreeNode>() |
||||
.First(m => m.MethodDefinition.Name == "Empty"); |
||||
|
||||
var segments = OmnibarViewModel.BuildSegments(methodNode); |
||||
|
||||
// Container crumbs (assembly / namespace / type) host children, so they keep the chevron.
|
||||
Assert.That(segments[0].HasChildren, Is.True, "the assembly crumb has children"); |
||||
Assert.That(segments[2].HasChildren, Is.True, "the type crumb has members"); |
||||
// The method is a leaf: no chevron, so it can't open an empty dropdown.
|
||||
Assert.That(segments[3].HasChildren, Is.False, "a method crumb is a leaf and drops the chevron"); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task NavigateSegment_Moves_The_Tree_Selection() |
||||
{ |
||||
var (_, vm) = await TestHarness.BootAsync(3); |
||||
|
||||
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>( |
||||
"System.Linq", "System.Linq", "System.Linq.Enumerable"); |
||||
typeNode.EnsureLazyChildren(); |
||||
var methodNode = typeNode.Children.OfType<MethodTreeNode>() |
||||
.First(m => m.MethodDefinition.Name == "Empty"); |
||||
|
||||
var omnibar = new OmnibarViewModel(); |
||||
omnibar.SetNode(methodNode); |
||||
|
||||
// Click the "Type" crumb (index 2): selection jumps from the method to its type.
|
||||
omnibar.NavigateSegment(omnibar.Segments[2]); |
||||
|
||||
Assert.That(vm.AssemblyTreeModel.SelectedItem, Is.SameAs(typeNode)); |
||||
} |
||||
|
||||
[AvaloniaTest] |
||||
public async Task EnterSearch_And_ExitSearch_Toggle_Mode_And_Clear_Text() |
||||
{ |
||||
await TestHarness.BootAsync(1); |
||||
|
||||
var omnibar = new OmnibarViewModel(); |
||||
Assert.That(omnibar.Mode, Is.EqualTo(OmnibarMode.Breadcrumb)); |
||||
|
||||
omnibar.EnterSearch(); |
||||
Assert.That(omnibar.Mode, Is.EqualTo(OmnibarMode.Search)); |
||||
|
||||
omnibar.SearchText = "Enumerable"; |
||||
omnibar.ExitSearch(); |
||||
|
||||
Assert.That(omnibar.Mode, Is.EqualTo(OmnibarMode.Breadcrumb)); |
||||
Assert.That(omnibar.SearchText, Is.Empty); |
||||
} |
||||
} |
||||
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
// 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 Avalonia.Threading; |
||||
using Avalonia.VisualTree; |
||||
|
||||
using ICSharpCode.ILSpy.AppEnv; |
||||
using ICSharpCode.ILSpy.Controls.Omnibar; |
||||
using ICSharpCode.ILSpy.TextView; |
||||
using ICSharpCode.ILSpy.TreeNodes; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Controls; |
||||
|
||||
/// <summary>
|
||||
/// The omnibar ships off by default behind the Options / Display "Tab options"
|
||||
/// <c>EnableOmnibar</c> toggle, and the toggle takes effect live (no re-decompile).
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class OmnibarSettingTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task Omnibar_Is_Off_By_Default_And_Revealed_Live_When_Enabled() |
||||
{ |
||||
var (window, vm) = await TestHarness.BootAsync(3); |
||||
var display = AppComposition.Current.GetExport<SettingsService>().DisplaySettings; |
||||
Assert.That(display.EnableOmnibar, Is.False, "the omnibar is off by default"); |
||||
|
||||
// Realize a decompiler document by selecting a node.
|
||||
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>( |
||||
"System.Linq", "System.Linq", "System.Linq.Enumerable"); |
||||
vm.AssemblyTreeModel.SelectedItem = typeNode; |
||||
Omnibar? omnibar = null; |
||||
for (int i = 0; i < 200; i++) |
||||
{ |
||||
Dispatcher.UIThread.RunJobs(); |
||||
omnibar = window.GetVisualDescendants().OfType<DecompilerTextView>() |
||||
.Where(v => v.IsEffectivelyVisible) |
||||
.SelectMany(v => v.GetVisualDescendants().OfType<Omnibar>()) |
||||
.FirstOrDefault(); |
||||
if (omnibar != null) |
||||
break; |
||||
await Task.Delay(20); |
||||
} |
||||
Assert.That(omnibar, Is.Not.Null, "selecting a node realizes a decompiler text view hosting the omnibar"); |
||||
|
||||
Assert.That(omnibar!.IsVisible, Is.False, |
||||
"the omnibar is hidden while the EnableOmnibar setting is off"); |
||||
|
||||
display.EnableOmnibar = true; |
||||
Dispatcher.UIThread.RunJobs(); |
||||
|
||||
Assert.That(omnibar.IsVisible, Is.True, |
||||
"enabling the setting reveals the omnibar without a re-decompile"); |
||||
} |
||||
} |
||||
@ -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 ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
namespace ICSharpCode.ILSpy.Controls.Omnibar |
||||
{ |
||||
/// <summary>
|
||||
/// One crumb in the omnibar breadcrumb: a tree node rendered as icon + label. The
|
||||
/// <see cref="Node"/> is the live <see cref="SharpTreeNode"/> the crumb stands for, so clicking
|
||||
/// the crumb (or one of its <see cref="Children"/> from the chevron dropdown) navigates the
|
||||
/// assembly tree straight there. Used both for the trail itself and for the sibling entries the
|
||||
/// chevron lists.
|
||||
/// </summary>
|
||||
public sealed class BreadcrumbSegment |
||||
{ |
||||
public BreadcrumbSegment(SharpTreeNode node) |
||||
{ |
||||
Node = node; |
||||
Text = node.Text?.ToString() ?? string.Empty; |
||||
Icon = node.Icon; |
||||
} |
||||
|
||||
/// <summary>The tree node this crumb represents; navigating selects it.</summary>
|
||||
public SharpTreeNode Node { get; } |
||||
|
||||
/// <summary>Display label, taken from <see cref="SharpTreeNode.Text"/>.</summary>
|
||||
public string Text { get; } |
||||
|
||||
/// <summary>Display icon (an <c>IImage</c>), taken from <see cref="SharpTreeNode.Icon"/>.</summary>
|
||||
public object? Icon { get; } |
||||
|
||||
/// <summary>
|
||||
/// Whether the chevron dropdown has anything to show. Reuses the same predicate the tree uses
|
||||
/// for its expander (<see cref="SharpTreeNode.ShowExpander"/>: lazy-loadable, or has visible
|
||||
/// children), so leaf crumbs such as methods and fields drop the chevron instead of opening an
|
||||
/// empty list.
|
||||
/// </summary>
|
||||
public bool HasChildren => Node.ShowExpander; |
||||
} |
||||
} |
||||
@ -0,0 +1,197 @@
@@ -0,0 +1,197 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui" |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
xmlns:local="using:ICSharpCode.ILSpy" |
||||
xmlns:omnibar="using:ICSharpCode.ILSpy.Controls.Omnibar" |
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="28" |
||||
x:Class="ICSharpCode.ILSpy.Controls.Omnibar.Omnibar" |
||||
x:DataType="omnibar:OmnibarViewModel" |
||||
Name="self"> |
||||
|
||||
<UserControl.Styles> |
||||
<!-- Flat, transparent-until-hover treatment mirroring the toolbar buttons, so the bar |
||||
reads as chrome rather than a row of raised buttons. --> |
||||
<Style Selector="Button.crumb"> |
||||
<Setter Property="Background" Value="Transparent" /> |
||||
<Setter Property="BorderThickness" Value="0" /> |
||||
<Setter Property="Padding" Value="4,2" /> |
||||
<Setter Property="CornerRadius" Value="3" /> |
||||
<Setter Property="Foreground" Value="{DynamicResource ILSpy.WindowForeground}" /> |
||||
</Style> |
||||
<Style Selector="Button.crumb:pointerover /template/ ContentPresenter"> |
||||
<Setter Property="Background" Value="{DynamicResource ILSpy.OmnibarHover}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="Button.chevron"> |
||||
<Setter Property="Background" Value="Transparent" /> |
||||
<Setter Property="BorderThickness" Value="0" /> |
||||
<Setter Property="Padding" Value="3,2" /> |
||||
<Setter Property="CornerRadius" Value="3" /> |
||||
<Setter Property="VerticalAlignment" Value="Center" /> |
||||
</Style> |
||||
<Style Selector="Button.chevron:pointerover /template/ ContentPresenter"> |
||||
<Setter Property="Background" Value="{DynamicResource ILSpy.OmnibarHover}" /> |
||||
</Style> |
||||
|
||||
<Style Selector="Button.searchToggle"> |
||||
<Setter Property="Background" Value="Transparent" /> |
||||
<Setter Property="BorderThickness" Value="0" /> |
||||
<Setter Property="Padding" Value="4,2" /> |
||||
<Setter Property="CornerRadius" Value="3" /> |
||||
</Style> |
||||
<Style Selector="Button.searchToggle:pointerover /template/ ContentPresenter"> |
||||
<Setter Property="Background" Value="{DynamicResource ILSpy.OmnibarHover}" /> |
||||
</Style> |
||||
</UserControl.Styles> |
||||
|
||||
<Panel> |
||||
<Border Height="28" |
||||
Background="{DynamicResource ILSpy.OmnibarBackground}" |
||||
BorderBrush="{DynamicResource ILSpy.OmnibarBorder}" |
||||
BorderThickness="0,0,0,1" |
||||
Padding="4,0"> |
||||
<Panel> |
||||
|
||||
<!-- Breadcrumb face: the trail plus a search affordance on the right. --> |
||||
<Grid ColumnDefinitions="*,Auto" IsVisible="{Binding IsBreadcrumbMode}"> |
||||
<ScrollViewer Grid.Column="0" |
||||
HorizontalScrollBarVisibility="Auto" |
||||
VerticalScrollBarVisibility="Disabled" |
||||
VerticalAlignment="Center"> |
||||
<ItemsControl ItemsSource="{Binding Segments}"> |
||||
<ItemsControl.ItemsPanel> |
||||
<ItemsPanelTemplate> |
||||
<StackPanel Orientation="Horizontal" /> |
||||
</ItemsPanelTemplate> |
||||
</ItemsControl.ItemsPanel> |
||||
<ItemsControl.ItemTemplate> |
||||
<DataTemplate x:DataType="omnibar:BreadcrumbSegment"> |
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> |
||||
<Button Classes="crumb" Click="OnSegmentClick" Tag="{Binding}" |
||||
ToolTip.Tip="{Binding Text}"> |
||||
<StackPanel Orientation="Horizontal" Spacing="4"> |
||||
<Image Source="{Binding Icon}" Width="16" Height="16" |
||||
VerticalAlignment="Center" /> |
||||
<TextBlock Text="{Binding Text}" VerticalAlignment="Center" /> |
||||
</StackPanel> |
||||
</Button> |
||||
<Button Classes="chevron" Click="OnChevronClick" Tag="{Binding}" |
||||
IsVisible="{Binding HasChildren}" |
||||
ToolTip.Tip="Show contents"> |
||||
<Path Width="8" Height="5" |
||||
Stretch="None" |
||||
Stroke="{DynamicResource ILSpy.SecondaryForeground}" |
||||
StrokeThickness="1.3" |
||||
StrokeLineCap="Round" |
||||
StrokeJoin="Round" |
||||
Data="M 1 1 L 4 4 L 7 1" /> |
||||
</Button> |
||||
</StackPanel> |
||||
</DataTemplate> |
||||
</ItemsControl.ItemTemplate> |
||||
</ItemsControl> |
||||
</ScrollViewer> |
||||
<Button Grid.Column="1" Classes="searchToggle" Click="OnEnterSearchClick" |
||||
VerticalAlignment="Center" |
||||
ToolTip.Tip="Search (Ctrl+L)"> |
||||
<Image Source="{x:Static local:Images.Search}" Width="16" Height="16" /> |
||||
</Button> |
||||
</Grid> |
||||
|
||||
<!-- Search face: a single-line query box with a leading magnifier and a clear button. --> |
||||
<Grid ColumnDefinitions="Auto,*" IsVisible="{Binding IsSearchMode}"> |
||||
<Image Grid.Column="0" Source="{x:Static local:Images.Search}" |
||||
Width="16" Height="16" Margin="2,0,4,0" VerticalAlignment="Center" /> |
||||
<TextBox Grid.Column="1" Name="SearchInput" |
||||
BorderThickness="0" |
||||
Background="Transparent" |
||||
VerticalAlignment="Center" |
||||
VerticalContentAlignment="Center" |
||||
PlaceholderText="Search types and members - t:Type, m:Member, c:Constant; -exclude, +require, /regex/" |
||||
Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> |
||||
<TextBox.InnerRightContent> |
||||
<Button IsVisible="{Binding SearchText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" |
||||
Background="Transparent" BorderThickness="0" |
||||
Padding="4,0" Margin="0" |
||||
ToolTip.Tip="Clear (Esc)" |
||||
Click="OnClearSearchClick"> |
||||
<TextBlock Text="x" FontSize="11" VerticalAlignment="Center" /> |
||||
</Button> |
||||
</TextBox.InnerRightContent> |
||||
</TextBox> |
||||
</Grid> |
||||
</Panel> |
||||
</Border> |
||||
|
||||
<!-- Inline suggestion list, anchored under the bar. Declared inside the control tree so the |
||||
overlay popup host resolves its control theme through this logical parent (see |
||||
DecompilerTextView's RichHoverPopup note). --> |
||||
<Popup Name="SuggestionsPopup" |
||||
PlacementTarget="{Binding #self}" |
||||
Placement="BottomEdgeAlignedLeft" |
||||
IsLightDismissEnabled="True"> |
||||
<Border Background="{DynamicResource ILSpy.InputBackground}" |
||||
BorderBrush="{DynamicResource ILSpy.OmnibarBorder}" |
||||
BorderThickness="1" |
||||
CornerRadius="3" |
||||
MinWidth="420" |
||||
MaxHeight="360"> |
||||
<ListBox Name="SuggestionsList" |
||||
ItemsSource="{Binding Suggestions}" |
||||
x:CompileBindings="False" |
||||
BorderThickness="0"> |
||||
<ListBox.ItemTemplate> |
||||
<DataTemplate> |
||||
<Grid ColumnDefinitions="Auto,6,*,12,Auto" Margin="2,1"> |
||||
<Image Grid.Column="0" Source="{Binding Image}" Width="16" Height="16" |
||||
VerticalAlignment="Center" /> |
||||
<TextBlock Grid.Column="2" Text="{Binding Name}" |
||||
VerticalAlignment="Center" |
||||
TextTrimming="CharacterEllipsis" /> |
||||
<TextBlock Grid.Column="4" Text="{Binding Location}" |
||||
Foreground="{DynamicResource ILSpy.SecondaryForeground}" |
||||
VerticalAlignment="Center" |
||||
TextTrimming="CharacterEllipsis" /> |
||||
</Grid> |
||||
</DataTemplate> |
||||
</ListBox.ItemTemplate> |
||||
</ListBox> |
||||
</Border> |
||||
</Popup> |
||||
|
||||
<!-- Chevron dropdown: a filterable list of a crumb's child nodes for lateral navigation. |
||||
One shared popup repositioned to whichever chevron was clicked. --> |
||||
<Popup Name="ChevronPopup" |
||||
Placement="BottomEdgeAlignedLeft" |
||||
IsLightDismissEnabled="True"> |
||||
<Border Background="{DynamicResource ILSpy.InputBackground}" |
||||
BorderBrush="{DynamicResource ILSpy.OmnibarBorder}" |
||||
BorderThickness="1" |
||||
CornerRadius="3" |
||||
MinWidth="220" |
||||
MaxHeight="400"> |
||||
<DockPanel> |
||||
<TextBox Name="ChevronFilter" DockPanel.Dock="Top" |
||||
Margin="4" |
||||
PlaceholderText="Filter..." |
||||
TextChanged="OnChevronFilterChanged" /> |
||||
<ListBox Name="ChevronList" |
||||
BorderThickness="0"> |
||||
<ListBox.ItemTemplate> |
||||
<DataTemplate x:DataType="omnibar:BreadcrumbSegment"> |
||||
<Grid ColumnDefinitions="Auto,6,*" Margin="2,1"> |
||||
<Image Grid.Column="0" Source="{Binding Icon}" Width="16" Height="16" |
||||
VerticalAlignment="Center" /> |
||||
<TextBlock Grid.Column="2" Text="{Binding Text}" |
||||
VerticalAlignment="Center" |
||||
TextTrimming="CharacterEllipsis" /> |
||||
</Grid> |
||||
</DataTemplate> |
||||
</ListBox.ItemTemplate> |
||||
</ListBox> |
||||
</DockPanel> |
||||
</Border> |
||||
</Popup> |
||||
</Panel> |
||||
</UserControl> |
||||
@ -0,0 +1,214 @@
@@ -0,0 +1,214 @@
|
||||
// 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; |
||||
using System.Collections.Specialized; |
||||
using System.ComponentModel; |
||||
using System.Linq; |
||||
|
||||
using Avalonia.Controls; |
||||
using Avalonia.Input; |
||||
using Avalonia.Threading; |
||||
|
||||
using ICSharpCode.ILSpyX.Search; |
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
namespace ICSharpCode.ILSpy.Controls.Omnibar |
||||
{ |
||||
/// <summary>
|
||||
/// The decompiler's address-bar: a breadcrumb of the current node that turns into a search box
|
||||
/// when the user types. Hosted at the top of the decompiled-content view; one instance per
|
||||
/// document tab, each owning its own <see cref="OmnibarViewModel"/>.
|
||||
/// </summary>
|
||||
public partial class Omnibar : UserControl |
||||
{ |
||||
readonly OmnibarViewModel viewModel = new(); |
||||
|
||||
public Omnibar() |
||||
{ |
||||
// Per-instance VM (not inherited from the hosting document's DataContext) so each tab's
|
||||
// bar tracks its own node and search.
|
||||
DataContext = viewModel; |
||||
InitializeComponent(); |
||||
|
||||
viewModel.PropertyChanged += OnViewModelPropertyChanged; |
||||
viewModel.Suggestions.CollectionChanged += OnSuggestionsChanged; |
||||
|
||||
SearchInput.KeyDown += OnSearchInputKeyDown; |
||||
SuggestionsList.KeyDown += OnSuggestionsKeyDown; |
||||
SuggestionsList.Tapped += OnSuggestionTapped; |
||||
ChevronList.Tapped += OnChevronItemTapped; |
||||
ChevronList.KeyDown += OnChevronKeyDown; |
||||
} |
||||
|
||||
/// <summary>Points the breadcrumb at the document's current node.</summary>
|
||||
public void SetNode(SharpTreeNode? node) => viewModel.SetNode(node); |
||||
|
||||
/// <summary>Drops the bar into search mode and focuses the query box (Ctrl+L from the host).</summary>
|
||||
public void FocusSearch() |
||||
{ |
||||
viewModel.EnterSearch(); |
||||
Dispatcher.UIThread.Post(() => { |
||||
SearchInput.Focus(); |
||||
SearchInput.SelectAll(); |
||||
}); |
||||
} |
||||
|
||||
void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) |
||||
{ |
||||
if (e.PropertyName == nameof(OmnibarViewModel.Mode)) |
||||
{ |
||||
if (viewModel.Mode == OmnibarMode.Search) |
||||
{ |
||||
Dispatcher.UIThread.Post(() => SearchInput.Focus()); |
||||
} |
||||
else |
||||
{ |
||||
SuggestionsPopup.IsOpen = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
void OnSuggestionsChanged(object? sender, NotifyCollectionChangedEventArgs e) |
||||
{ |
||||
SuggestionsPopup.IsOpen = viewModel.Mode == OmnibarMode.Search && viewModel.Suggestions.Count > 0; |
||||
} |
||||
|
||||
void OnSegmentClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) |
||||
{ |
||||
if (sender is Control { Tag: BreadcrumbSegment segment }) |
||||
viewModel.NavigateSegment(segment); |
||||
} |
||||
|
||||
void OnEnterSearchClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) |
||||
=> FocusSearch(); |
||||
|
||||
void OnClearSearchClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) |
||||
{ |
||||
viewModel.SearchText = string.Empty; |
||||
SearchInput.Focus(); |
||||
} |
||||
|
||||
void OnSearchInputKeyDown(object? sender, KeyEventArgs e) |
||||
{ |
||||
switch (e.Key) |
||||
{ |
||||
case Key.Escape: |
||||
viewModel.ExitSearch(); |
||||
e.Handled = true; |
||||
break; |
||||
case Key.Down when viewModel.Suggestions.Count > 0: |
||||
SuggestionsPopup.IsOpen = true; |
||||
SuggestionsList.SelectedIndex = 0; |
||||
SuggestionsList.Focus(); |
||||
e.Handled = true; |
||||
break; |
||||
case Key.Enter: |
||||
ActivateSuggestion( |
||||
SuggestionsList.SelectedItem as SearchResult ?? viewModel.Suggestions.FirstOrDefault(), |
||||
inNewTabPage: e.KeyModifiers.HasFlag(KeyModifiers.Control)); |
||||
e.Handled = true; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
void OnSuggestionsKeyDown(object? sender, KeyEventArgs e) |
||||
{ |
||||
switch (e.Key) |
||||
{ |
||||
case Key.Enter: |
||||
ActivateSuggestion(SuggestionsList.SelectedItem as SearchResult, |
||||
inNewTabPage: e.KeyModifiers.HasFlag(KeyModifiers.Control)); |
||||
e.Handled = true; |
||||
break; |
||||
case Key.Escape: |
||||
viewModel.ExitSearch(); |
||||
e.Handled = true; |
||||
break; |
||||
case Key.Up when SuggestionsList.SelectedIndex <= 0: |
||||
SearchInput.Focus(); |
||||
e.Handled = true; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
void OnSuggestionTapped(object? sender, TappedEventArgs e) |
||||
=> ActivateSuggestion(SuggestionsList.SelectedItem as SearchResult, inNewTabPage: false); |
||||
|
||||
void ActivateSuggestion(SearchResult? result, bool inNewTabPage) |
||||
{ |
||||
if (result == null) |
||||
return; |
||||
SuggestionsPopup.IsOpen = false; |
||||
viewModel.Activate(result, inNewTabPage); |
||||
} |
||||
|
||||
// The crumb whose children the chevron popup is currently showing; used to refilter as the
|
||||
// user types in the popup's filter box.
|
||||
System.Collections.Generic.IReadOnlyList<BreadcrumbSegment> chevronChildren |
||||
= Array.Empty<BreadcrumbSegment>(); |
||||
|
||||
void OnChevronClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) |
||||
{ |
||||
if (sender is not Control { Tag: BreadcrumbSegment segment } button) |
||||
return; |
||||
chevronChildren = viewModel.GetChildSegments(segment); |
||||
ChevronFilter.Text = string.Empty; |
||||
ChevronList.ItemsSource = chevronChildren; |
||||
ChevronPopup.PlacementTarget = button; |
||||
ChevronPopup.IsOpen = chevronChildren.Count > 0; |
||||
if (ChevronPopup.IsOpen) |
||||
Dispatcher.UIThread.Post(() => ChevronFilter.Focus()); |
||||
} |
||||
|
||||
void OnChevronFilterChanged(object? sender, TextChangedEventArgs e) |
||||
{ |
||||
var filter = ChevronFilter.Text ?? string.Empty; |
||||
ChevronList.ItemsSource = filter.Length == 0 |
||||
? chevronChildren |
||||
: chevronChildren |
||||
.Where(c => c.Text.Contains(filter, StringComparison.OrdinalIgnoreCase)) |
||||
.ToArray(); |
||||
} |
||||
|
||||
void OnChevronItemTapped(object? sender, TappedEventArgs e) |
||||
=> NavigateToChevronSelection(); |
||||
|
||||
void OnChevronKeyDown(object? sender, KeyEventArgs e) |
||||
{ |
||||
if (e.Key == Key.Enter) |
||||
{ |
||||
NavigateToChevronSelection(); |
||||
e.Handled = true; |
||||
} |
||||
else if (e.Key == Key.Escape) |
||||
{ |
||||
ChevronPopup.IsOpen = false; |
||||
e.Handled = true; |
||||
} |
||||
} |
||||
|
||||
void NavigateToChevronSelection() |
||||
{ |
||||
if (ChevronList.SelectedItem is not BreadcrumbSegment segment) |
||||
return; |
||||
ChevronPopup.IsOpen = false; |
||||
viewModel.NavigateSegment(segment); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
// 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.
|
||||
|
||||
namespace ICSharpCode.ILSpy.Controls.Omnibar |
||||
{ |
||||
/// <summary>
|
||||
/// The two faces of the omnibar. <see cref="Breadcrumb"/> shows the path of the decompiled
|
||||
/// node and is the resting state; <see cref="Search"/> turns the bar into a query input with
|
||||
/// an inline suggestions dropdown. Typing switches into <see cref="Search"/>; Escape (or
|
||||
/// navigating to a result) returns to <see cref="Breadcrumb"/>.
|
||||
/// </summary>
|
||||
public enum OmnibarMode |
||||
{ |
||||
Breadcrumb, |
||||
Search, |
||||
} |
||||
} |
||||
@ -0,0 +1,255 @@
@@ -0,0 +1,255 @@
|
||||
// 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.Collections.ObjectModel; |
||||
using System.Collections.Specialized; |
||||
using System.ComponentModel; |
||||
using System.Linq; |
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel; |
||||
using CommunityToolkit.Mvvm.Input; |
||||
|
||||
using ICSharpCode.ILSpyX; |
||||
using ICSharpCode.ILSpyX.Search; |
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
using ICSharpCode.ILSpy.AppEnv; |
||||
using ICSharpCode.ILSpy.AssemblyTree; |
||||
using ICSharpCode.ILSpy.Languages; |
||||
using ICSharpCode.ILSpy.Options; |
||||
using ICSharpCode.ILSpy.Search; |
||||
using ICSharpCode.ILSpy.ViewModels; |
||||
|
||||
namespace ICSharpCode.ILSpy.Controls.Omnibar |
||||
{ |
||||
/// <summary>
|
||||
/// Drives one omnibar instance: the breadcrumb trail for the hosting document's current node
|
||||
/// and the inline search that the bar turns into when the user types. Created per-control (not
|
||||
/// MEF-shared) so each document tab carries its own trail; shared services (assembly tree,
|
||||
/// language, settings, search engine) are resolved lazily from the composition host the same
|
||||
/// way <see cref="SearchPaneModel"/> does.
|
||||
/// </summary>
|
||||
public sealed partial class OmnibarViewModel : ViewModelBase |
||||
{ |
||||
// The inline dropdown shows the best few hits; the docked Search pane (Ctrl+Shift+F) stays
|
||||
// the place for the full, sortable result list.
|
||||
const int MaxSuggestions = 20; |
||||
|
||||
/// <summary>The breadcrumb trail, root-first (Assembly > Namespace > Type > Member).</summary>
|
||||
public ObservableCollection<BreadcrumbSegment> Segments { get; } = new(); |
||||
|
||||
/// <summary>The best <see cref="MaxSuggestions"/> hits of the live search, fitness-sorted.</summary>
|
||||
public ObservableCollection<SearchResult> Suggestions { get; } = new(); |
||||
|
||||
// Full streaming sink for the current RunningSearch (kept fitness-sorted by the engine).
|
||||
// Suggestions mirrors only its head; this stays private so the bound collection never shows
|
||||
// the engine's 1000-result cap row.
|
||||
readonly ObservableCollection<SearchResult> searchSink = new(); |
||||
|
||||
[ObservableProperty] |
||||
public partial OmnibarMode Mode { get; set; } = OmnibarMode.Breadcrumb; |
||||
|
||||
[ObservableProperty] |
||||
public partial string SearchText { get; set; } = string.Empty; |
||||
|
||||
[ObservableProperty] |
||||
public partial bool IsSearching { get; set; } |
||||
|
||||
/// <summary>True while the bar shows the breadcrumb trail. Drives view visibility.</summary>
|
||||
public bool IsBreadcrumbMode => Mode == OmnibarMode.Breadcrumb; |
||||
|
||||
/// <summary>True while the bar shows the search input. Drives view visibility.</summary>
|
||||
public bool IsSearchMode => Mode == OmnibarMode.Search; |
||||
|
||||
public OmnibarViewModel() |
||||
{ |
||||
PropertyChanged += OnPropertyChangedDispatch; |
||||
searchSink.CollectionChanged += OnSearchSinkChanged; |
||||
} |
||||
|
||||
partial void OnModeChanged(OmnibarMode value) |
||||
{ |
||||
OnPropertyChanged(nameof(IsBreadcrumbMode)); |
||||
OnPropertyChanged(nameof(IsSearchMode)); |
||||
} |
||||
|
||||
void OnPropertyChangedDispatch(object? sender, PropertyChangedEventArgs e) |
||||
{ |
||||
if (e.PropertyName != nameof(SearchText)) |
||||
return; |
||||
// Typing anything turns the bar into a search box; clearing it (or Escape) is what
|
||||
// returns to the breadcrumb, handled by ExitSearch.
|
||||
if (!string.IsNullOrEmpty(SearchText)) |
||||
Mode = OmnibarMode.Search; |
||||
RestartSearch(); |
||||
} |
||||
|
||||
// The node currently shown in the host document, tracked so a repeated SetNode (e.g. an
|
||||
// intermediate decompile property change) doesn't rebuild the trail or kick the user out
|
||||
// of a search they're in the middle of typing.
|
||||
SharpTreeNode? currentNode; |
||||
|
||||
/// <summary>
|
||||
/// Points the breadcrumb at <paramref name="node"/> (the document's current node). A real
|
||||
/// change rebuilds the trail and leaves search mode, since the displayed location moved.
|
||||
/// </summary>
|
||||
public void SetNode(SharpTreeNode? node) |
||||
{ |
||||
if (ReferenceEquals(currentNode, node)) |
||||
return; |
||||
currentNode = node; |
||||
Segments.Clear(); |
||||
foreach (var segment in BuildSegments(node)) |
||||
Segments.Add(segment); |
||||
if (Mode == OmnibarMode.Search) |
||||
ExitSearch(); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The breadcrumb for <paramref name="node"/>: its ancestor chain root-first, excluding the
|
||||
/// invisible <c>AssemblyListTreeNode</c> root (whose <see cref="SharpTreeNode.Parent"/> is
|
||||
/// null). Mirrors <see cref="AssemblyTreeModel.GetPathForNode"/>'s root exclusion.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<BreadcrumbSegment> BuildSegments(SharpTreeNode? node) |
||||
{ |
||||
var segments = new List<BreadcrumbSegment>(); |
||||
for (var current = node; current is { Parent: not null }; current = current.Parent) |
||||
segments.Add(new BreadcrumbSegment(current)); |
||||
segments.Reverse(); |
||||
return segments; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// The child nodes of <paramref name="segment"/> wrapped as crumbs, for the chevron
|
||||
/// dropdown's sibling list. Materialises the node's lazy children on demand.
|
||||
/// </summary>
|
||||
public IReadOnlyList<BreadcrumbSegment> GetChildSegments(BreadcrumbSegment? segment) |
||||
{ |
||||
if (segment?.Node == null) |
||||
return System.Array.Empty<BreadcrumbSegment>(); |
||||
segment.Node.EnsureLazyChildren(); |
||||
return segment.Node.Children.Select(child => new BreadcrumbSegment(child)).ToArray(); |
||||
} |
||||
|
||||
/// <summary>Moves the assembly-tree selection to <paramref name="segment"/>'s node.</summary>
|
||||
[RelayCommand] |
||||
public void NavigateSegment(BreadcrumbSegment? segment) |
||||
{ |
||||
if (segment?.Node == null) |
||||
return; |
||||
AppComposition.TryGetExport<AssemblyTreeModel>()?.SelectNode(segment.Node); |
||||
} |
||||
|
||||
/// <summary>Switches the bar into search mode (e.g. Ctrl+L / clicking the bar).</summary>
|
||||
[RelayCommand] |
||||
public void EnterSearch() => Mode = OmnibarMode.Search; |
||||
|
||||
/// <summary>Returns to the breadcrumb and clears the query.</summary>
|
||||
[RelayCommand] |
||||
public void ExitSearch() |
||||
{ |
||||
Mode = OmnibarMode.Breadcrumb; |
||||
SearchText = string.Empty; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Navigates to a search suggestion: resolves its reference to a tree node and moves the
|
||||
/// selection there (or opens a new document tab when <paramref name="inNewTabPage"/> is
|
||||
/// set). Mirrors <see cref="SearchPaneModel.Activate(SearchResult, bool)"/>.
|
||||
/// </summary>
|
||||
public void Activate(SearchResult? result, bool inNewTabPage = false) |
||||
{ |
||||
if (result?.Reference == null) |
||||
return; |
||||
var atm = AppComposition.TryGetExport<AssemblyTreeModel>(); |
||||
if (atm == null) |
||||
return; |
||||
var node = atm.FindTreeNode(result.Reference); |
||||
if (node == null) |
||||
return; |
||||
if (inNewTabPage) |
||||
AppComposition.TryGetExport<Docking.DockWorkspace>()?.OpenNodeInNewTab(node); |
||||
else |
||||
atm.SelectedItem = node; |
||||
} |
||||
|
||||
RunningSearch? currentSearch; |
||||
|
||||
void RestartSearch() |
||||
{ |
||||
currentSearch?.Cancel(); |
||||
currentSearch = null; |
||||
searchSink.Clear(); |
||||
Suggestions.Clear(); |
||||
IsSearching = false; |
||||
|
||||
var term = SearchText ?? string.Empty; |
||||
if (string.IsNullOrWhiteSpace(term)) |
||||
return; |
||||
|
||||
var assemblyList = AppComposition.TryGetExport<AssemblyTreeModel>()?.AssemblyList; |
||||
if (assemblyList == null) |
||||
return; |
||||
var language = AppComposition.TryGetExport<LanguageService>()?.CurrentLanguage; |
||||
if (language == null) |
||||
return; |
||||
var apiVisibility = AppComposition.TryGetExport<SettingsService>()?.SessionSettings?.LanguageSettings?.ShowApiLevel |
||||
?? ApiVisibility.PublicOnly; |
||||
|
||||
var run = new RunningSearch( |
||||
assemblyList.GetAssemblies(), |
||||
term, |
||||
SearchMode.TypeAndMember, |
||||
language, |
||||
apiVisibility, |
||||
new AvaloniaSearchResultFactory(language), |
||||
searchSink, |
||||
SearchResult.ComparerByFitness); |
||||
run.Completed += OnRunCompleted; |
||||
currentSearch = run; |
||||
IsSearching = true; |
||||
run.Start(); |
||||
} |
||||
|
||||
void OnRunCompleted(RunningSearch sender) |
||||
{ |
||||
if (!ReferenceEquals(sender, currentSearch)) |
||||
return; |
||||
IsSearching = false; |
||||
} |
||||
|
||||
// Mirror only the head of the fitness-sorted sink into the bound Suggestions, updating at
|
||||
// the edges so the dropdown changes in place rather than rebuilding on every streamed hit.
|
||||
void OnSearchSinkChanged(object? sender, NotifyCollectionChangedEventArgs e) |
||||
{ |
||||
switch (e.Action) |
||||
{ |
||||
case NotifyCollectionChangedAction.Add |
||||
when e.NewItems?.Count == 1 && e.NewStartingIndex < MaxSuggestions: |
||||
Suggestions.Insert(e.NewStartingIndex, (SearchResult)e.NewItems[0]!); |
||||
while (Suggestions.Count > MaxSuggestions) |
||||
Suggestions.RemoveAt(Suggestions.Count - 1); |
||||
break; |
||||
case NotifyCollectionChangedAction.Reset: |
||||
Suggestions.Clear(); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue