Browse Source

Add omnibar breadcrumb and search bar above the decompiled code

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 Code
pull/3794/head
Christoph Wille 3 weeks ago
parent
commit
c84a330ee1
  1. 120
      ILSpy.Tests/Controls/OmnibarBreadcrumbTests.cs
  2. 76
      ILSpy.Tests/Controls/OmnibarSettingTests.cs
  3. 10
      ILSpy/App.axaml
  4. 56
      ILSpy/Controls/Omnibar/BreadcrumbSegment.cs
  5. 197
      ILSpy/Controls/Omnibar/Omnibar.axaml
  6. 214
      ILSpy/Controls/Omnibar/Omnibar.axaml.cs
  7. 32
      ILSpy/Controls/Omnibar/OmnibarMode.cs
  8. 255
      ILSpy/Controls/Omnibar/OmnibarViewModel.cs
  9. 2
      ILSpy/Options/DisplaySettingReactions.cs
  10. 5
      ILSpy/Options/DisplaySettings.cs
  11. 2
      ILSpy/Options/DisplaySettingsPanel.axaml
  12. 11
      ILSpy/Properties/Resources.Designer.cs
  13. 3
      ILSpy/Properties/Resources.resx
  14. 4
      ILSpy/TextView/DecompilerTabPageModel.cs
  15. 7
      ILSpy/TextView/DecompilerTextView.axaml
  16. 33
      ILSpy/TextView/DecompilerTextView.axaml.cs

120
ILSpy.Tests/Controls/OmnibarBreadcrumbTests.cs

@ -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);
}
}

76
ILSpy.Tests/Controls/OmnibarSettingTests.cs

@ -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");
}
}

10
ILSpy/App.axaml

@ -90,6 +90,11 @@ @@ -90,6 +90,11 @@
<SolidColorBrush x:Key="ILSpy.FoldingMarkerForeground" Color="#808080" />
<SolidColorBrush x:Key="ILSpy.FoldingMarkerSelectedBackground" Color="White" />
<SolidColorBrush x:Key="ILSpy.FoldingMarkerSelectedForeground" Color="Black" />
<!-- Omnibar (breadcrumb / search bar atop the decompiled view): a faint surface
set off from the cream editor canvas, with the toolbar accent for crumb hover. -->
<SolidColorBrush x:Key="ILSpy.OmnibarBackground" Color="#F3F3F3" />
<SolidColorBrush x:Key="ILSpy.OmnibarBorder" Color="#FFC8CDD3" />
<SolidColorBrush x:Key="ILSpy.OmnibarHover" Color="#330078D7" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="ILSpy.EditorBackground" Color="#1E1E1E" />
@ -142,6 +147,11 @@ @@ -142,6 +147,11 @@
<SolidColorBrush x:Key="ILSpy.FoldingMarkerForeground" Color="#A0A0A0" />
<SolidColorBrush x:Key="ILSpy.FoldingMarkerSelectedBackground" Color="#2D2D30" />
<SolidColorBrush x:Key="ILSpy.FoldingMarkerSelectedForeground" Color="#DCDCDC" />
<!-- Omnibar: a step lighter than the dark editor canvas so the bar reads as
chrome, sharing the toolbar accent for crumb hover. -->
<SolidColorBrush x:Key="ILSpy.OmnibarBackground" Color="#2D2D30" />
<SolidColorBrush x:Key="ILSpy.OmnibarBorder" Color="#3F3F46" />
<SolidColorBrush x:Key="ILSpy.OmnibarHover" Color="#330078D7" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>

56
ILSpy/Controls/Omnibar/BreadcrumbSegment.cs

@ -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;
}
}

197
ILSpy/Controls/Omnibar/Omnibar.axaml

@ -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>

214
ILSpy/Controls/Omnibar/Omnibar.axaml.cs

@ -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);
}
}
}

32
ILSpy/Controls/Omnibar/OmnibarMode.cs

@ -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,
}
}

255
ILSpy/Controls/Omnibar/OmnibarViewModel.cs

@ -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;
}
}
}
}

2
ILSpy/Options/DisplaySettingReactions.cs

@ -80,6 +80,8 @@ namespace ICSharpCode.ILSpy.Options @@ -80,6 +80,8 @@ namespace ICSharpCode.ILSpy.Options
[nameof(DisplaySettings.EnableWordWrap)] = DisplaySettingReaction.EditorLive,
[nameof(DisplaySettings.HighlightCurrentLine)] = DisplaySettingReaction.EditorLive,
[nameof(DisplaySettings.HighlightMatchingBraces)] = DisplaySettingReaction.EditorLive,
// The text view shows/hides its own omnibar from this; tree and output are unaffected.
[nameof(DisplaySettings.EnableOmnibar)] = DisplaySettingReaction.EditorLive,
// No model-side reaction.
[nameof(DisplaySettings.StyleWindowTitleBar)] = DisplaySettingReaction.None,

5
ILSpy/Options/DisplaySettings.cs

@ -96,6 +96,9 @@ namespace ICSharpCode.ILSpy.Options @@ -96,6 +96,9 @@ namespace ICSharpCode.ILSpy.Options
[ObservableProperty]
bool decodeCustomAttributeBlobs;
[ObservableProperty]
bool enableOmnibar;
public XName SectionName => "DisplaySettings";
public void LoadFromXml(XElement section)
@ -121,6 +124,7 @@ namespace ICSharpCode.ILSpy.Options @@ -121,6 +124,7 @@ namespace ICSharpCode.ILSpy.Options
ShowRawOffsetsAndBytesBeforeInstruction = (bool?)section.Attribute(nameof(ShowRawOffsetsAndBytesBeforeInstruction)) ?? false;
StyleWindowTitleBar = (bool?)section.Attribute(nameof(StyleWindowTitleBar)) ?? false;
DecodeCustomAttributeBlobs = (bool?)section.Attribute(nameof(DecodeCustomAttributeBlobs)) ?? false;
EnableOmnibar = (bool?)section.Attribute(nameof(EnableOmnibar)) ?? false;
}
public XElement SaveToXml()
@ -147,6 +151,7 @@ namespace ICSharpCode.ILSpy.Options @@ -147,6 +151,7 @@ namespace ICSharpCode.ILSpy.Options
section.SetAttributeValue(nameof(ShowRawOffsetsAndBytesBeforeInstruction), ShowRawOffsetsAndBytesBeforeInstruction);
section.SetAttributeValue(nameof(StyleWindowTitleBar), StyleWindowTitleBar);
section.SetAttributeValue(nameof(DecodeCustomAttributeBlobs), DecodeCustomAttributeBlobs);
section.SetAttributeValue(nameof(EnableOmnibar), EnableOmnibar);
return section;
}
}

2
ILSpy/Options/DisplaySettingsPanel.axaml

@ -103,6 +103,8 @@ @@ -103,6 +103,8 @@
Content="Enable multiple rows in tab strip" />
<CheckBox IsChecked="{Binding SessionSettings.MouseWheelTogglesTabStripRows, Mode=TwoWay}"
Content="Mouse wheel scroll toggles single/multiple row tab strip" />
<CheckBox IsChecked="{Binding Settings.EnableOmnibar, Mode=TwoWay}"
Content="{x:Static res:Resources.ShowOmnibar}" />
</StackPanel>
</HeaderedContentControl>

11
ILSpy/Properties/Resources.Designer.cs generated

@ -2997,7 +2997,16 @@ namespace ICSharpCode.ILSpy.Properties { @@ -2997,7 +2997,16 @@ namespace ICSharpCode.ILSpy.Properties {
return ResourceManager.GetString("ShowMetadataTokensInBase10", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show omnibar (breadcrumb / search bar) above the decompiled code.
/// </summary>
public static string ShowOmnibar {
get {
return ResourceManager.GetString("ShowOmnibar", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show only public types and members.
/// </summary>

3
ILSpy/Properties/Resources.resx

@ -1021,6 +1021,9 @@ Do you want to continue?</value> @@ -1021,6 +1021,9 @@ Do you want to continue?</value>
<data name="ShowMetadataTokensInBase10" xml:space="preserve">
<value>Show metadata tokens in base 10</value>
</data>
<data name="ShowOmnibar" xml:space="preserve">
<value>Show omnibar (breadcrumb / search bar) above the decompiled code</value>
</data>
<data name="ShowPublicOnlyTypesMembers" xml:space="preserve">
<value>Show only public types and members</value>
</data>

4
ILSpy/TextView/DecompilerTabPageModel.cs

@ -307,6 +307,10 @@ namespace ICSharpCode.ILSpy.TextView @@ -307,6 +307,10 @@ namespace ICSharpCode.ILSpy.TextView
cachedBaseTitle = ComposeBaseTitle();
foreach (var n in currentNodes)
n.PropertyChanged += OnCurrentNodePropertyChanged;
// Let host chrome (the omnibar breadcrumb) react to the tab re-targeting a node.
// Nothing else relied on the absence of this notification.
OnPropertyChanged(nameof(CurrentNodes));
OnPropertyChanged(nameof(CurrentNode));
StartDecompile();
}
}

7
ILSpy/TextView/DecompilerTextView.axaml

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
xmlns:ae="using:AvaloniaEdit"
xmlns:aeediting="using:AvaloniaEdit.Editing"
xmlns:textView="using:ICSharpCode.ILSpy.TextView"
xmlns:omnibar="using:ICSharpCode.ILSpy.Controls.Omnibar"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400"
Name="self"
x:Class="ICSharpCode.ILSpy.TextView.DecompilerTextView"
@ -20,6 +21,11 @@ @@ -20,6 +21,11 @@
</Style>
</UserControl.Styles>
<DockPanel>
<!-- Breadcrumb / search bar atop the editor (EditorBar-style): shows the decompiled node's
path and turns into a search box on typing. Per document tab, owns its own view model. -->
<omnibar:Omnibar Name="Omnibar" DockPanel.Dock="Top" />
<!-- Wait-adorner: a translucent overlay above the editor with a centred title +
progress bar + cancel button while a decompilation is in flight. -->
<Grid>
@ -87,4 +93,5 @@ @@ -87,4 +93,5 @@
materialises and the popup is invisible. Renders nothing while closed. -->
<Popup Name="RichHoverPopup" />
</Grid>
</DockPanel>
</UserControl>

33
ILSpy/TextView/DecompilerTextView.axaml.cs

@ -129,6 +129,19 @@ namespace ICSharpCode.ILSpy.TextView @@ -129,6 +129,19 @@ namespace ICSharpCode.ILSpy.TextView
Editor.TextArea.Caret.PositionChanged += OnCaretPositionChanged;
SetupZoomAndCopy();
// Ctrl+L focuses the omnibar into search mode (browser address-bar gesture). Tunnel so
// it wins before AvaloniaEdit's own key handling while focus is anywhere in the editor.
AddHandler(KeyDownEvent, OnPreviewKeyDownForOmnibar, RoutingStrategies.Tunnel);
}
void OnPreviewKeyDownForOmnibar(object? sender, KeyEventArgs e)
{
if (e.Key == Key.L && e.KeyModifiers == KeyModifiers.Control && Omnibar.IsVisible)
{
Omnibar.FocusSearch();
e.Handled = true;
}
}
// One generator lives for the lifetime of the view; we only swap its References collection
@ -492,6 +505,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -492,6 +505,7 @@ namespace ICSharpCode.ILSpy.TextView
ApplyDisplaySetting(s, nameof(DisplaySettings.HighlightCurrentLine));
ApplyDisplaySetting(s, nameof(DisplaySettings.IndentationSize));
ApplyDisplaySetting(s, nameof(DisplaySettings.IndentationUseTabs));
ApplyDisplaySetting(s, nameof(DisplaySettings.EnableOmnibar));
}
void ApplyDisplaySetting(DisplaySettings s, string? propertyName)
@ -522,6 +536,11 @@ namespace ICSharpCode.ILSpy.TextView @@ -522,6 +536,11 @@ namespace ICSharpCode.ILSpy.TextView
case nameof(DisplaySettings.IndentationUseTabs):
Editor.Options.ConvertTabsToSpaces = !s.IndentationUseTabs;
break;
case nameof(DisplaySettings.EnableOmnibar):
// Off by default; the Options Display "Tab options" toggle shows/hides the bar
// live without a re-decompile, per text view.
Omnibar.IsVisible = s.EnableOmnibar;
break;
}
}
@ -605,6 +624,13 @@ namespace ICSharpCode.ILSpy.TextView @@ -605,6 +624,13 @@ namespace ICSharpCode.ILSpy.TextView
// the first attach and an ABA reattach.
model.CaptureViewState = GetCurrentViewState;
ApplyDocument(model);
// Point the breadcrumb at this tab's node (the bar owns its own VM, so feed it the
// node rather than letting it inherit the document DataContext).
Omnibar.SetNode(model.CurrentNode);
}
else
{
Omnibar.SetNode(null);
}
}
@ -637,6 +663,13 @@ namespace ICSharpCode.ILSpy.TextView @@ -637,6 +663,13 @@ namespace ICSharpCode.ILSpy.TextView
// SyntaxExtension fires an intermediate rebuild that must not move the scroll.
ApplyDocument(model, restoreViewState: e.PropertyName == nameof(DecompilerTabPageModel.Text));
}
// Keep the breadcrumb in step when the tab re-targets a different node (the model
// raises this when CurrentNodes changes).
else if (sender is DecompilerTabPageModel nodeModel
&& e.PropertyName == nameof(DecompilerTabPageModel.CurrentNode))
{
Omnibar.SetNode(nodeModel.CurrentNode);
}
// HighlightedReference can change independently of a re-decompile (e.g. the analyzer
// pane sets it while the user is already on the right tab). Apply it directly without
// rebuilding the document.

Loading…
Cancel
Save