diff --git a/ILSpy.Tests/Controls/OmnibarBreadcrumbTests.cs b/ILSpy.Tests/Controls/OmnibarBreadcrumbTests.cs
new file mode 100644
index 000000000..d6351fc58
--- /dev/null
+++ b/ILSpy.Tests/Controls/OmnibarBreadcrumbTests.cs
@@ -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;
+
+///
+/// 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.
+///
+[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(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ typeNode.EnsureLazyChildren();
+ var methodNode = typeNode.Children.OfType()
+ .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(),
+ "the first crumb is the assembly, not the invisible AssemblyListTreeNode root");
+ Assert.That(segments[1].Node, Is.InstanceOf());
+ 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(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ typeNode.EnsureLazyChildren();
+ var methodNode = typeNode.Children.OfType()
+ .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(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ typeNode.EnsureLazyChildren();
+ var methodNode = typeNode.Children.OfType()
+ .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);
+ }
+}
diff --git a/ILSpy.Tests/Controls/OmnibarSettingTests.cs b/ILSpy.Tests/Controls/OmnibarSettingTests.cs
new file mode 100644
index 000000000..61c8d9d67
--- /dev/null
+++ b/ILSpy.Tests/Controls/OmnibarSettingTests.cs
@@ -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;
+
+///
+/// The omnibar ships off by default behind the Options / Display "Tab options"
+/// EnableOmnibar toggle, and the toggle takes effect live (no re-decompile).
+///
+[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().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(
+ "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()
+ .Where(v => v.IsEffectivelyVisible)
+ .SelectMany(v => v.GetVisualDescendants().OfType())
+ .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");
+ }
+}
diff --git a/ILSpy/App.axaml b/ILSpy/App.axaml
index b89594b1f..23c7988d0 100644
--- a/ILSpy/App.axaml
+++ b/ILSpy/App.axaml
@@ -90,6 +90,11 @@
+
+
+
+
@@ -142,6 +147,11 @@
+
+
+
+
diff --git a/ILSpy/Controls/Omnibar/BreadcrumbSegment.cs b/ILSpy/Controls/Omnibar/BreadcrumbSegment.cs
new file mode 100644
index 000000000..2970e84cd
--- /dev/null
+++ b/ILSpy/Controls/Omnibar/BreadcrumbSegment.cs
@@ -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
+{
+ ///
+ /// One crumb in the omnibar breadcrumb: a tree node rendered as icon + label. The
+ /// is the live the crumb stands for, so clicking
+ /// the crumb (or one of its from the chevron dropdown) navigates the
+ /// assembly tree straight there. Used both for the trail itself and for the sibling entries the
+ /// chevron lists.
+ ///
+ public sealed class BreadcrumbSegment
+ {
+ public BreadcrumbSegment(SharpTreeNode node)
+ {
+ Node = node;
+ Text = node.Text?.ToString() ?? string.Empty;
+ Icon = node.Icon;
+ }
+
+ /// The tree node this crumb represents; navigating selects it.
+ public SharpTreeNode Node { get; }
+
+ /// Display label, taken from .
+ public string Text { get; }
+
+ /// Display icon (an IImage), taken from .
+ public object? Icon { get; }
+
+ ///
+ /// Whether the chevron dropdown has anything to show. Reuses the same predicate the tree uses
+ /// for its expander (: lazy-loadable, or has visible
+ /// children), so leaf crumbs such as methods and fields drop the chevron instead of opening an
+ /// empty list.
+ ///
+ public bool HasChildren => Node.ShowExpander;
+ }
+}
diff --git a/ILSpy/Controls/Omnibar/Omnibar.axaml b/ILSpy/Controls/Omnibar/Omnibar.axaml
new file mode 100644
index 000000000..221e38527
--- /dev/null
+++ b/ILSpy/Controls/Omnibar/Omnibar.axaml
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ILSpy/Controls/Omnibar/Omnibar.axaml.cs b/ILSpy/Controls/Omnibar/Omnibar.axaml.cs
new file mode 100644
index 000000000..208d4c131
--- /dev/null
+++ b/ILSpy/Controls/Omnibar/Omnibar.axaml.cs
@@ -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
+{
+ ///
+ /// 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 .
+ ///
+ 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;
+ }
+
+ /// Points the breadcrumb at the document's current node.
+ public void SetNode(SharpTreeNode? node) => viewModel.SetNode(node);
+
+ /// Drops the bar into search mode and focuses the query box (Ctrl+L from the host).
+ 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 chevronChildren
+ = Array.Empty();
+
+ 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);
+ }
+ }
+}
diff --git a/ILSpy/Controls/Omnibar/OmnibarMode.cs b/ILSpy/Controls/Omnibar/OmnibarMode.cs
new file mode 100644
index 000000000..95392f86c
--- /dev/null
+++ b/ILSpy/Controls/Omnibar/OmnibarMode.cs
@@ -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
+{
+ ///
+ /// The two faces of the omnibar. shows the path of the decompiled
+ /// node and is the resting state; turns the bar into a query input with
+ /// an inline suggestions dropdown. Typing switches into ; Escape (or
+ /// navigating to a result) returns to .
+ ///
+ public enum OmnibarMode
+ {
+ Breadcrumb,
+ Search,
+ }
+}
diff --git a/ILSpy/Controls/Omnibar/OmnibarViewModel.cs b/ILSpy/Controls/Omnibar/OmnibarViewModel.cs
new file mode 100644
index 000000000..1f9099978
--- /dev/null
+++ b/ILSpy/Controls/Omnibar/OmnibarViewModel.cs
@@ -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
+{
+ ///
+ /// 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 does.
+ ///
+ 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;
+
+ /// The breadcrumb trail, root-first (Assembly > Namespace > Type > Member).
+ public ObservableCollection Segments { get; } = new();
+
+ /// The best hits of the live search, fitness-sorted.
+ public ObservableCollection 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 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; }
+
+ /// True while the bar shows the breadcrumb trail. Drives view visibility.
+ public bool IsBreadcrumbMode => Mode == OmnibarMode.Breadcrumb;
+
+ /// True while the bar shows the search input. Drives view visibility.
+ 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;
+
+ ///
+ /// Points the breadcrumb at (the document's current node). A real
+ /// change rebuilds the trail and leaves search mode, since the displayed location moved.
+ ///
+ 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();
+ }
+
+ ///
+ /// The breadcrumb for : its ancestor chain root-first, excluding the
+ /// invisible AssemblyListTreeNode root (whose is
+ /// null). Mirrors 's root exclusion.
+ ///
+ public static IReadOnlyList BuildSegments(SharpTreeNode? node)
+ {
+ var segments = new List();
+ for (var current = node; current is { Parent: not null }; current = current.Parent)
+ segments.Add(new BreadcrumbSegment(current));
+ segments.Reverse();
+ return segments;
+ }
+
+ ///
+ /// The child nodes of wrapped as crumbs, for the chevron
+ /// dropdown's sibling list. Materialises the node's lazy children on demand.
+ ///
+ public IReadOnlyList GetChildSegments(BreadcrumbSegment? segment)
+ {
+ if (segment?.Node == null)
+ return System.Array.Empty();
+ segment.Node.EnsureLazyChildren();
+ return segment.Node.Children.Select(child => new BreadcrumbSegment(child)).ToArray();
+ }
+
+ /// Moves the assembly-tree selection to 's node.
+ [RelayCommand]
+ public void NavigateSegment(BreadcrumbSegment? segment)
+ {
+ if (segment?.Node == null)
+ return;
+ AppComposition.TryGetExport()?.SelectNode(segment.Node);
+ }
+
+ /// Switches the bar into search mode (e.g. Ctrl+L / clicking the bar).
+ [RelayCommand]
+ public void EnterSearch() => Mode = OmnibarMode.Search;
+
+ /// Returns to the breadcrumb and clears the query.
+ [RelayCommand]
+ public void ExitSearch()
+ {
+ Mode = OmnibarMode.Breadcrumb;
+ SearchText = string.Empty;
+ }
+
+ ///
+ /// Navigates to a search suggestion: resolves its reference to a tree node and moves the
+ /// selection there (or opens a new document tab when is
+ /// set). Mirrors .
+ ///
+ public void Activate(SearchResult? result, bool inNewTabPage = false)
+ {
+ if (result?.Reference == null)
+ return;
+ var atm = AppComposition.TryGetExport();
+ if (atm == null)
+ return;
+ var node = atm.FindTreeNode(result.Reference);
+ if (node == null)
+ return;
+ if (inNewTabPage)
+ AppComposition.TryGetExport()?.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()?.AssemblyList;
+ if (assemblyList == null)
+ return;
+ var language = AppComposition.TryGetExport()?.CurrentLanguage;
+ if (language == null)
+ return;
+ var apiVisibility = AppComposition.TryGetExport()?.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;
+ }
+ }
+ }
+}
diff --git a/ILSpy/Options/DisplaySettingReactions.cs b/ILSpy/Options/DisplaySettingReactions.cs
index b89ebf3ef..422aaf4d2 100644
--- a/ILSpy/Options/DisplaySettingReactions.cs
+++ b/ILSpy/Options/DisplaySettingReactions.cs
@@ -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,
diff --git a/ILSpy/Options/DisplaySettings.cs b/ILSpy/Options/DisplaySettings.cs
index a0dbd0111..bfd7503d4 100644
--- a/ILSpy/Options/DisplaySettings.cs
+++ b/ILSpy/Options/DisplaySettings.cs
@@ -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
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
section.SetAttributeValue(nameof(ShowRawOffsetsAndBytesBeforeInstruction), ShowRawOffsetsAndBytesBeforeInstruction);
section.SetAttributeValue(nameof(StyleWindowTitleBar), StyleWindowTitleBar);
section.SetAttributeValue(nameof(DecodeCustomAttributeBlobs), DecodeCustomAttributeBlobs);
+ section.SetAttributeValue(nameof(EnableOmnibar), EnableOmnibar);
return section;
}
}
diff --git a/ILSpy/Options/DisplaySettingsPanel.axaml b/ILSpy/Options/DisplaySettingsPanel.axaml
index 0156e8019..9d9190a60 100644
--- a/ILSpy/Options/DisplaySettingsPanel.axaml
+++ b/ILSpy/Options/DisplaySettingsPanel.axaml
@@ -103,6 +103,8 @@
Content="Enable multiple rows in tab strip" />
+
diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs
index 678d613d0..6285f90e9 100644
--- a/ILSpy/Properties/Resources.Designer.cs
+++ b/ILSpy/Properties/Resources.Designer.cs
@@ -2997,7 +2997,16 @@ namespace ICSharpCode.ILSpy.Properties {
return ResourceManager.GetString("ShowMetadataTokensInBase10", resourceCulture);
}
}
-
+
+ ///
+ /// Looks up a localized string similar to Show omnibar (breadcrumb / search bar) above the decompiled code.
+ ///
+ public static string ShowOmnibar {
+ get {
+ return ResourceManager.GetString("ShowOmnibar", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Show only public types and members.
///
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index 256beeab2..857418494 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -1021,6 +1021,9 @@ Do you want to continue?
Show metadata tokens in base 10
+
+ Show omnibar (breadcrumb / search bar) above the decompiled code
+
Show only public types and members
diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs
index 4eff012cd..bfd487e5e 100644
--- a/ILSpy/TextView/DecompilerTabPageModel.cs
+++ b/ILSpy/TextView/DecompilerTabPageModel.cs
@@ -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();
}
}
diff --git a/ILSpy/TextView/DecompilerTextView.axaml b/ILSpy/TextView/DecompilerTextView.axaml
index c0130d9d7..71c2462f0 100644
--- a/ILSpy/TextView/DecompilerTextView.axaml
+++ b/ILSpy/TextView/DecompilerTextView.axaml
@@ -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 @@
+
+
+
+
@@ -87,4 +93,5 @@
materialises and the popup is invisible. Renders nothing while closed. -->
+
diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs
index 1e01f38de..693a0777e 100644
--- a/ILSpy/TextView/DecompilerTextView.axaml.cs
+++ b/ILSpy/TextView/DecompilerTextView.axaml.cs
@@ -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
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
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
// 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
// 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.