From 3485b905c1731b67147303df0c8418ad9ec07e30 Mon Sep 17 00:00:00 2001 From: reduckted Date: Sun, 20 Sep 2026 13:41:12 +1000 Subject: [PATCH 1/4] Used a Visual Studio SearchControl for the search box. --- .../UI/AttachedProperties/ClearOnEscape.cs | 73 -- .../AttachedProperties/FocusWhenRequested.cs | 88 -- .../UI/AttachedProperties/FocusWhenVisible.cs | 70 -- .../UI/AttachedProperties/Watermark.cs | 86 -- source/ProjectFilter/UI/FilterDialog.xaml | 21 +- source/ProjectFilter/UI/FilterDialog.xaml.cs | 47 +- .../ProjectFilter/UI/FilterDialogViewModel.cs | 223 ++-- source/ProjectFilter/UI/Resources.xaml | 71 -- .../ProjectFilter/UI/Utilities/Debouncer.cs | 56 - .../ProjectFilter/UI/Utilities/FocusSource.cs | 16 - .../ProjectFilter/UI/Utilities/IDebouncer.cs | 17 - .../AttachedProperties/ClearOnEscapeTests.cs | 94 -- .../UI/AttachedProperties/WatermarkTests.cs | 104 -- .../UI/FilterDialogViewModelTests.cs | 979 +++++++++--------- 14 files changed, 663 insertions(+), 1282 deletions(-) delete mode 100644 source/ProjectFilter/UI/AttachedProperties/ClearOnEscape.cs delete mode 100644 source/ProjectFilter/UI/AttachedProperties/FocusWhenRequested.cs delete mode 100644 source/ProjectFilter/UI/AttachedProperties/FocusWhenVisible.cs delete mode 100644 source/ProjectFilter/UI/AttachedProperties/Watermark.cs delete mode 100644 source/ProjectFilter/UI/Utilities/Debouncer.cs delete mode 100644 source/ProjectFilter/UI/Utilities/FocusSource.cs delete mode 100644 source/ProjectFilter/UI/Utilities/IDebouncer.cs delete mode 100644 tests/ProjectFilter.UnitTests/UI/AttachedProperties/ClearOnEscapeTests.cs delete mode 100644 tests/ProjectFilter.UnitTests/UI/AttachedProperties/WatermarkTests.cs diff --git a/source/ProjectFilter/UI/AttachedProperties/ClearOnEscape.cs b/source/ProjectFilter/UI/AttachedProperties/ClearOnEscape.cs deleted file mode 100644 index cfc344c..0000000 --- a/source/ProjectFilter/UI/AttachedProperties/ClearOnEscape.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; - - -namespace ProjectFilter.UI.AttachedProperties; - - -public static class ClearOnEscape { - - public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached( - "Enabled", - typeof(bool), - typeof(ClearOnEscape), - new PropertyMetadata(false, OnEnabledChanged) - ); - - - - public static bool GetEnabled(DependencyObject obj) { - if (obj is null) { - throw new ArgumentNullException(nameof(obj)); - } - - return (bool)obj.GetValue(EnabledProperty); - } - - - public static void SetEnabled(DependencyObject obj, bool value) { - if (obj is null) { - throw new ArgumentNullException(nameof(obj)); - } - - obj.SetValue(EnabledProperty, value); - } - - - private static void OnEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - TextBox? box; - - - box = d as TextBox; - - if (box is not null) { - if ((bool)e.OldValue) { - box.KeyDown -= OnTextBoxKeyDown; - } - - if ((bool)e.NewValue) { - box.KeyDown += OnTextBoxKeyDown; - } - } - } - - - private static void OnTextBoxKeyDown(object sender, KeyEventArgs e) { - if (e.OriginalSource == sender) { - if (e.Key == Key.Escape) { - TextBox box; - - - box = (TextBox)sender; - - if (!string.IsNullOrEmpty(box.Text)) { - box.Text = ""; - e.Handled = true; - } - } - } - } - -} diff --git a/source/ProjectFilter/UI/AttachedProperties/FocusWhenRequested.cs b/source/ProjectFilter/UI/AttachedProperties/FocusWhenRequested.cs deleted file mode 100644 index e219228..0000000 --- a/source/ProjectFilter/UI/AttachedProperties/FocusWhenRequested.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Microsoft.VisualStudio; -using ProjectFilter.UI.Utilities; -using System; -using System.Diagnostics; -using System.Windows; - - -namespace ProjectFilter.UI.AttachedProperties; - - -public static class FocusWhenRequested { - - public static readonly DependencyProperty SourceProperty = DependencyProperty.RegisterAttached( - "Source", - typeof(FocusSource), - typeof(FocusWhenRequested), - new PropertyMetadata(null, OnSourceChanged) - ); - - - private static readonly DependencyProperty EventHandlerProperty = DependencyProperty.RegisterAttached( - "EventHandler", - typeof(EventHandler), - typeof(FocusWhenRequested), - new PropertyMetadata(null) - ); - - - public static FocusSource GetSource(DependencyObject obj) { - if (obj is null) { - throw new ArgumentNullException(nameof(obj)); - } - - return (FocusSource)obj.GetValue(SourceProperty); - } - - - public static void SetSource(DependencyObject obj, FocusSource value) { - if (obj is null) { - throw new ArgumentNullException(nameof(obj)); - } - - obj.SetValue(SourceProperty, value); - } - - - private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - UIElement? target; - - - target = d as UIElement; - - if (target is not null) { - EventHandler? handler; - - - if (e.OldValue is FocusSource oldSource) { - // Remove the event handler from the old source. - // The event handler is saved against the target object. - handler = target.GetValue(EventHandlerProperty) as EventHandler; - target.SetValue(EventHandlerProperty, null); - - if (handler is not null) { - oldSource.FocusRequested -= handler; - } - } - - if (e.NewValue is FocusSource newSource) { - handler = (_, _) => OnFocusRequested(target); - newSource.FocusRequested += handler; - - // Save the event handler against the target so - // that we can remove it of the source changes. - target.SetValue(EventHandlerProperty, handler); - } - } - } - - - private static void OnFocusRequested(UIElement target) { - try { - target.Focus(); - } catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) { - Debug.WriteLine(ex); - } - } - -} diff --git a/source/ProjectFilter/UI/AttachedProperties/FocusWhenVisible.cs b/source/ProjectFilter/UI/AttachedProperties/FocusWhenVisible.cs deleted file mode 100644 index d019e10..0000000 --- a/source/ProjectFilter/UI/AttachedProperties/FocusWhenVisible.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Microsoft.VisualStudio; -using System; -using System.Windows; - - -namespace ProjectFilter.UI.AttachedProperties; - - -public static class FocusWhenVisible { - - public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached( - "Enabled", - typeof(bool), - typeof(FocusWhenVisible), - new PropertyMetadata(false, OnEnabledChanged) - ); - - - public static bool GetEnabled(DependencyObject obj) { - if (obj is null) { - throw new ArgumentNullException(nameof(obj)); - } - - return (bool)obj.GetValue(EnabledProperty); - } - - - public static void SetEnabled(DependencyObject obj, bool value) { - if (obj is null) { - throw new ArgumentNullException(nameof(obj)); - } - - obj.SetValue(EnabledProperty, value); - } - - - private static void OnEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - UIElement? target; - - - target = d as UIElement; - - if (target is not null) { - if ((bool)e.OldValue) { - target.IsVisibleChanged -= OnIsVisibleChanged; - } - - if ((bool)e.NewValue) { - target.IsVisibleChanged += OnIsVisibleChanged; - } - } - } - - - private static void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { - UIElement? target; - - - target = sender as UIElement; - - if (target is not null && target.IsVisible) { - try { - target.Focus(); - } catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) { - System.Diagnostics.Debug.WriteLine(ex); - } - } - } - -} diff --git a/source/ProjectFilter/UI/AttachedProperties/Watermark.cs b/source/ProjectFilter/UI/AttachedProperties/Watermark.cs deleted file mode 100644 index 8dc4e90..0000000 --- a/source/ProjectFilter/UI/AttachedProperties/Watermark.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Controls; - - -namespace ProjectFilter.UI.AttachedProperties; - - -public static class Watermark { - - public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached( - "Text", - typeof(string), - typeof(Watermark), - new PropertyMetadata(null, OnTextChanged) - ); - - - private static readonly DependencyPropertyKey HidePropertyKey = DependencyProperty.RegisterAttachedReadOnly( - "Hide", - typeof(bool), - typeof(Watermark), - new PropertyMetadata(false) - ); - - - public static readonly DependencyProperty HideProperty = HidePropertyKey.DependencyProperty; - - - public static string GetText(DependencyObject obj) { - if (obj is null) { - throw new ArgumentNullException(nameof(obj)); - } - - return (string)obj.GetValue(TextProperty); - } - - - public static void SetText(DependencyObject obj, string value) { - if (obj is null) { - throw new ArgumentNullException(nameof(obj)); - } - - obj.SetValue(TextProperty, value); - } - - - public static bool GetHide(DependencyObject obj) { - if (obj is null) { - throw new ArgumentNullException(nameof(obj)); - } - - return (bool)obj.GetValue(HideProperty); - } - - - private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - TextBox? box; - - - box = d as TextBox; - - if (box is not null) { - if (e.OldValue is not null) { - box.TextChanged -= OnTextBoxTextChanged; - box.ClearValue(HidePropertyKey); - } - - if (e.NewValue is not null) { - box.TextChanged += OnTextBoxTextChanged; - UpdateHideProperty(box); - } - } - } - - - private static void OnTextBoxTextChanged(object sender, TextChangedEventArgs e) { - UpdateHideProperty((TextBox)sender); - } - - - private static void UpdateHideProperty(TextBox box) { - box.SetValue(HidePropertyKey, !string.IsNullOrEmpty(box.Text)); - } - -} diff --git a/source/ProjectFilter/UI/FilterDialog.xaml b/source/ProjectFilter/UI/FilterDialog.xaml index 23ef096..c99848c 100644 --- a/source/ProjectFilter/UI/FilterDialog.xaml +++ b/source/ProjectFilter/UI/FilterDialog.xaml @@ -166,26 +166,7 @@ - - - - - - + - /// This is the same as the resource key with the same name that is used in the internal "Find UI" control. - /// - public static ThemeResourceKey ErrorBorderBrushKey => _errorBorderBrushKey ??= new ThemeResourceKey( - new Guid("4370282e-987e-4ac4-ad14-5ffed2ad1e14"), - "ErrorBorder", - ThemeResourceKeyType.BackgroundBrush - ); + private static readonly object? ToolBarStyleKey = typeof(SearchControl) + .GetProperty("ToolBarStyleKey", BindingFlags.Public | BindingFlags.Static)? + .GetValue(null); public FilterDialog() { @@ -26,8 +22,31 @@ public FilterDialog() { } - private void OnLoaded(object sender, System.Windows.RoutedEventArgs e) { - ((FilterDialogViewModel)DataContext).OnLoadedAsync().FileAndForget(nameof(FilterDialog), faultDescription: nameof(OnLoaded)); + private void OnLoaded(object sender, RoutedEventArgs e) { + OnLoadedAsync((FilterDialogViewModel)DataContext).FileAndForget( + nameof(FilterDialog), + faultDescription: nameof(OnLoaded) + ); + } + + + private async Task OnLoadedAsync(FilterDialogViewModel vm) { + IVsWindowSearchHostFactory factory; + IVsWindowSearchHost host; + + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + factory = await VS.GetRequiredServiceAsync(); + host = factory.CreateWindowSearchHost(SearchContainer); + + await vm.OnLoadedAsync(host); + + if (host.GetType().GetProperty("SearchControl")?.GetValue(host) is SearchControl control) { + if (ToolBarStyleKey is not null) { + control.SetResourceReference(StyleProperty, ToolBarStyleKey); + } + } } } diff --git a/source/ProjectFilter/UI/FilterDialogViewModel.cs b/source/ProjectFilter/UI/FilterDialogViewModel.cs index 3e87b08..9c98f52 100644 --- a/source/ProjectFilter/UI/FilterDialogViewModel.cs +++ b/source/ProjectFilter/UI/FilterDialogViewModel.cs @@ -1,9 +1,12 @@ +using Microsoft.Internal.VisualStudio.PlatformUI; +using Microsoft.VisualStudio; using Microsoft.VisualStudio.PlatformUI; -using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; using ProjectFilter.Services; -using ProjectFilter.UI.Utilities; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using System.Windows; @@ -12,7 +15,7 @@ namespace ProjectFilter.UI; -public sealed class FilterDialogViewModel : ObservableObject, IDisposable { +public sealed class FilterDialogViewModel : ObservableObject, IVsWindowSearch { private static readonly Predicate CanAlwaysExecute = (x) => true; @@ -20,9 +23,8 @@ public sealed class FilterDialogViewModel : ObservableObject, IDisposable { private readonly Func>> _hierarchyFactory; private readonly TextFilterFactory _textFilterFactory; private readonly Dictionary? _nodeSettings; - private readonly IDebouncer _debouncer; + private IVsWindowSearchHost? _searchHost; private HierarchyTreeViewItemCollection _items; - private string _searchText; private bool _loadProjectDependencies; private FilterOptions? _result; private Visibility _loadingVisibility; @@ -30,103 +32,96 @@ public sealed class FilterDialogViewModel : ObservableObject, IDisposable { private bool _useRegularExpressions; private bool _expandLoadedProjects; private bool _hideSolutionFoldersWithoutLoadedProjects; - private bool _invalidFilter; public FilterDialogViewModel( Func>> hierarchyFactory, - Func debouncerFactory, TextFilterFactory textFilterFactory, - Dictionary? nodeSettings, - JoinableTaskFactory joinableTaskFactory + Dictionary? nodeSettings ) { - if (debouncerFactory is null) { - throw new ArgumentNullException(nameof(debouncerFactory)); - } - _hierarchyFactory = hierarchyFactory ?? throw new ArgumentNullException(nameof(hierarchyFactory)); _textFilterFactory = textFilterFactory ?? throw new ArgumentNullException(nameof(textFilterFactory)); _nodeSettings = nodeSettings; - _items = new HierarchyTreeViewItemCollection([]); - _searchText = ""; _loadingVisibility = Visibility.Visible; _loadedVisibility = Visibility.Collapsed; - FocusSearchBoxSource = new FocusSource(); - ToggleLoadProjectDependenciesCommand = new DelegateCommand( (_) => LoadProjectDependencies = !LoadProjectDependencies, CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); CollapseAllCommand = new DelegateCommand( CollapseAll, CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); ExpandAllCommand = new DelegateCommand( ExpandAll, CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); CheckAllCommand = new DelegateCommand( (_) => SetAllChecked(true), CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); UncheckAllCommand = new DelegateCommand( (_) => SetAllChecked(false), CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); ToggleRegularExpressionModeCommand = new DelegateCommand( (_) => UseRegularExpressions = !UseRegularExpressions, CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); ToggleExpandLoadedProjectsCommand = new DelegateCommand( (_) => ExpandLoadedProjects = !ExpandLoadedProjects, CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); ToggleHideSolutionFoldersWithoutLoadedProjectsCommand = new DelegateCommand( (_) => HideSolutionFoldersWithoutLoadedProjects = !HideSolutionFoldersWithoutLoadedProjects, CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); FocusSearchBoxCommand = new DelegateCommand( - (_) => FocusSearchBoxSource.RequestFocus(), + (_) => { + ThreadHelper.ThrowIfNotOnUIThread(); + _searchHost?.Activate(); + }, CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); AcceptCommand = new DelegateCommand( (_) => AcceptSelection(), CanAlwaysExecute, - joinableTaskFactory + ThreadHelper.JoinableTaskFactory ); - // Use a `DispatcherTimer` constructor without a callback - // so that the timer doesn't start immediately. We only - // want to start the timer when the search text changes. - _debouncer = debouncerFactory.Invoke(TimeSpan.FromMilliseconds(500)); - _debouncer.Stable += OnSearchStable; + SearchFiltersEnum = new WindowSearchFilterEnumerator([]); + SearchOptionsEnum = new WindowSearchOptionEnumerator([]); } - public async Task OnLoadedAsync() { + public async Task OnLoadedAsync(IVsWindowSearchHost searchHost) { IEnumerable hierarchy; + _searchHost = searchHost; + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + hierarchy = await _hierarchyFactory.Invoke(); Items = new HierarchyTreeViewItemCollection( @@ -135,6 +130,9 @@ public async Task OnLoadedAsync() { LoadedVisibility = Visibility.Visible; LoadingVisibility = Visibility.Collapsed; + + searchHost.SetupSearch(this); + searchHost.Activate(); } @@ -222,33 +220,12 @@ public HierarchyTreeViewItemCollection Items { public DelegateCommand AcceptCommand { get; } - public FocusSource FocusSearchBoxSource { get; } - - public FilterOptions? Result { get { return _result; } private set { SetProperty(ref _result, value); } } - public string SearchText { - get { return _searchText; } - set { - SetProperty(ref _searchText, value); - - _debouncer.Cancel(); - - // If the text is cleared, bypass the debounce and "search" - // immediately so that the original items are restored. - if (string.IsNullOrWhiteSpace(_searchText)) { - Search(); - } else { - _debouncer.Start(); - } - } - } - - public bool LoadProjectDependencies { get { return _loadProjectDependencies; } set { SetProperty(ref _loadProjectDependencies, value); } @@ -258,8 +235,9 @@ public bool LoadProjectDependencies { public bool UseRegularExpressions { get { return _useRegularExpressions; } set { + ThreadHelper.ThrowIfNotOnUIThread(); SetProperty(ref _useRegularExpressions, value); - Search(); + _searchHost?.SearchAsync(_searchHost.SearchQuery); } } @@ -276,12 +254,6 @@ public bool HideSolutionFoldersWithoutLoadedProjects { } - public bool InvalidFilter { - get { return _invalidFilter; } - private set { SetProperty(ref _invalidFilter, value); } - } - - private void CollapseAll(HierarchyTreeViewItem? root) { if (root is not null) { // If the given item doesn't have any children to collapse, @@ -350,41 +322,128 @@ from node in Items.GetFullHierarchy() } - private void OnSearchStable(object sender, EventArgs e) { - Search(); + private void ApplyFilter(string text) { + if (!string.IsNullOrEmpty(text)) { + Items.Filter(_textFilterFactory(text, UseRegularExpressions)); + } else { + Items.ClearFilter(); + } } - private void Search() { - string text; + public void ClearSearch() { + Items.ClearFilter(); + } - text = _searchText.Trim(); + public IVsSearchTask CreateSearch(uint cookie, IVsSearchQuery query, IVsSearchCallback callback) { + return new SearchTask(cookie, query, callback, ApplyFilter, ClearSearch); + } - if (!string.IsNullOrEmpty(text)) { - ITextFilter filter; + public void ProvideSearchSettings(IVsUIDataSource pSearchSettings) { + ThreadHelper.ThrowIfNotOnUIThread(); - try { - filter = _textFilterFactory(text, UseRegularExpressions); - } catch (ArgumentException) { - InvalidFilter = true; - return; - } + SetString(SearchSettingsDataSource.PropertyNames.SearchWatermark, "Filter projects... (Ctrl+F)"); + SetString(SearchSettingsDataSource.PropertyNames.SearchWatermarkWhenFocused, "Filter projects... (Ctrl+F)"); + SetString(SearchSettingsDataSource.PropertyNames.SearchTooltip, "Type words to filter the projects"); + SetString(SearchSettingsDataSource.PropertyNames.SearchStartTooltip, "Filter"); + SetString(SearchSettingsDataSource.PropertyNames.SearchStopTooltip, "Cancel"); + SetString(SearchSettingsDataSource.PropertyNames.SearchClearTooltip, "Clear"); - Items.Filter(filter); + SetUint(SearchSettingsDataSource.PropertyNames.ControlMaxWidth, 10000u); - } else { - Items.ClearFilter(); + SetUint(SearchSettingsDataSource.PropertyNames.SearchStartType, (uint)VSSEARCHSTARTTYPE.SST_DELAYED); + SetUint(SearchSettingsDataSource.PropertyNames.SearchStartDelay, 500u); + SetUint(SearchSettingsDataSource.PropertyNames.SearchProgressType, (uint)VSSEARCHPROGRESSTYPE.SPT_INDETERMINATE); + + SetBool(SearchSettingsDataSource.PropertyNames.SearchUseMRU, false); + + void SetString(string prop, string value) { + pSearchSettings.SetValue(prop, BuiltInPropertyValue.Create(value)); + } + + void SetUint(string prop, uint value) { + pSearchSettings.SetValue(prop, BuiltInPropertyValue.Create(value)); } - InvalidFilter = false; + void SetBool(string prop, bool value) { + pSearchSettings.SetValue(prop, BuiltInPropertyValue.Create(value)); + } + } + + + public bool OnNavigationKeyDown(uint dwNavigationKey, uint dwModifiers) { + return false; } - public void Dispose() { - _debouncer.Dispose(); - _debouncer.Stable -= OnSearchStable; + public bool SearchEnabled => true; + + + public Guid Category => Guid.Empty; + + + public IVsEnumWindowSearchFilters SearchFiltersEnum { get; } + + + public IVsEnumWindowSearchOptions SearchOptionsEnum { get; } + + + private sealed class SearchTask : VsSearchTask { + + private readonly Action _search; + private readonly Action _clear; + + + public SearchTask( + uint dwCookie, + IVsSearchQuery pSearchQuery, + IVsSearchCallback pSearchCallback, + Action search, + Action clear + ) : base(dwCookie, pSearchQuery, pSearchCallback) { + _search = search; + _clear = clear; + } + + + [SuppressMessage( + "Reliability", + "VSSDK007:ThreadHelper.JoinableTaskFactory.RunAsync", + Justification = "Cannot await or join here." + )] + protected override void OnStartSearch() { + ErrorCode = VSConstants.S_OK; + + // We are always called on a background thread, but + // because filtering changes properties that the view + // binds to, we need to switch to the main thread. + ThreadHelper.JoinableTaskFactory.RunAsync(async () => { + try { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (TaskStatus != VSConstants.VsSearchTaskStatus.Stopped) { + _search(SearchQuery.SearchString); + } else { + _clear(); + } + + // The number of search results are not shown anywhere, + // but if it's zero, then the search control gets an + // "error" border. Set it to one to prevent that. + SearchResults = 1; + + } catch (Exception ex) when (!ErrorHandler.IsCriticalException(ex)) { + ErrorCode = VSConstants.E_FAIL; + } + + // Confusingly, calling the base `OnStartSearch` + // method actually completes the search. + base.OnStartSearch(); + }).FireAndForget(); + } + } } diff --git a/source/ProjectFilter/UI/Resources.xaml b/source/ProjectFilter/UI/Resources.xaml index 0402e90..7a42524 100644 --- a/source/ProjectFilter/UI/Resources.xaml +++ b/source/ProjectFilter/UI/Resources.xaml @@ -232,77 +232,6 @@ - - @@ -238,16 +243,19 @@ + - - + - + @@ -294,7 +302,7 @@ - + @@ -307,7 +315,7 @@