Skip to content

Commit

Permalink
Highlight the search term in the options page
Browse files Browse the repository at this point in the history
  • Loading branch information
davidwengier committed Jun 21, 2022
1 parent 14b00ea commit 527202d
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<GroupBox x:Uid="AnalysisGroupBox"
Header="{x:Static local:AdvancedOptionPageStrings.Option_Analysis}">
<StackPanel>
<Label Content="{x:Static local:AdvancedOptionPageStrings.Option_Run_background_code_analysis_for}"/>
<Label x:Name="Run_background_code_analysis_for_label" Content="{x:Static local:AdvancedOptionPageStrings.Option_Run_background_code_analysis_for}"/>
<StackPanel>
<ComboBox x:Name="Run_background_code_analysis_for" IsEditable="false" AutomationProperties.Name="{x:Static local:AdvancedOptionPageStrings.Option_Run_background_code_analysis_for}">
<ComboBoxItem Content="{x:Static local:AdvancedOptionPageStrings.Option_Background_Analysis_Scope_None}" Tag="{x:Static local:AdvancedOptionPageStrings.Option_Background_Analysis_Scope_None_Tag}" />
Expand All @@ -24,7 +24,7 @@
<ComboBoxItem Content="{x:Static local:AdvancedOptionPageStrings.Option_Background_Analysis_Scope_Full_Solution}" Tag="{x:Static local:AdvancedOptionPageStrings.Option_Background_Analysis_Scope_Full_Solution_Tag}" />
</ComboBox>
</StackPanel>
<Label Content="{x:Static local:AdvancedOptionPageStrings.Option_Show_compiler_errors_and_warnings_for}"/>
<Label x:Name="Show_compiler_errors_and_warnings_for_label" Content="{x:Static local:AdvancedOptionPageStrings.Option_Show_compiler_errors_and_warnings_for}"/>
<StackPanel>
<ComboBox x:Name="Show_compiler_errors_and_warnings_for" IsEditable="false" AutomationProperties.Name="{x:Static local:AdvancedOptionPageStrings.Option_Show_compiler_errors_and_warnings_for}">
<ComboBoxItem Content="{x:Static local:AdvancedOptionPageStrings.Option_Compiler_Diagnostics_Scope_None}" Tag="{x:Static local:AdvancedOptionPageStrings.Option_Compiler_Diagnostics_Scope_None_Tag}" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public AdvancedOptionPageControl(OptionStore optionStore, IComponentModel compon
InitializeComponent();

// Analysis
BindToOption(Run_background_code_analysis_for, SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption, LanguageNames.CSharp);
BindToOption(Show_compiler_errors_and_warnings_for, SolutionCrawlerOptionsStorage.CompilerDiagnosticsScopeOption, LanguageNames.CSharp);
BindToOption(Run_background_code_analysis_for, SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption, LanguageNames.CSharp, label: Run_background_code_analysis_for_label);
BindToOption(Show_compiler_errors_and_warnings_for, SolutionCrawlerOptionsStorage.CompilerDiagnosticsScopeOption, LanguageNames.CSharp, label: Show_compiler_errors_and_warnings_for_label);
BindToOption(DisplayDiagnosticsInline, InlineDiagnosticsOptions.EnableInlineDiagnostics, LanguageNames.CSharp);
BindToOption(at_the_end_of_the_line_of_code, InlineDiagnosticsOptions.Location, InlineDiagnosticsLocations.PlacedAtEndOfCode, LanguageNames.CSharp);
BindToOption(on_the_right_edge_of_the_editor_window, InlineDiagnosticsOptions.Location, InlineDiagnosticsLocations.PlacedAtEndOfEditor, LanguageNames.CSharp);
Expand Down Expand Up @@ -206,6 +206,7 @@ private void UpdatePullDiagnosticsOptions()
{
var normalPullDiagnosticsOption = OptionStore.GetOption(InternalDiagnosticsOptions.NormalDiagnosticMode);
Enable_pull_diagnostics_experimental_requires_restart.IsChecked = GetCheckboxValueForDiagnosticMode(normalPullDiagnosticsOption);
AddSearchHandler(Enable_pull_diagnostics_experimental_requires_restart);

static bool? GetCheckboxValueForDiagnosticMode(DiagnosticMode mode)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public IntelliSenseOptionPageControl(OptionStore optionStore) : base(optionStore
BindToOption(Show_completion_list_after_a_character_is_typed, CompletionOptionsStorage.TriggerOnTypingLetters, LanguageNames.CSharp);
Show_completion_list_after_a_character_is_deleted.IsChecked = this.OptionStore.GetOption(CompletionOptionsStorage.TriggerOnDeletion, LanguageNames.CSharp) == true;
Show_completion_list_after_a_character_is_deleted.IsEnabled = Show_completion_list_after_a_character_is_typed.IsChecked == true;
AddSearchHandler(Show_completion_list_after_a_character_is_deleted);

BindToOption(Never_include_snippets, CompletionOptionsStorage.SnippetsBehavior, SnippetsRule.NeverInclude, LanguageNames.CSharp);
BindToOption(Always_include_snippets, CompletionOptionsStorage.SnippetsBehavior, SnippetsRule.AlwaysInclude, LanguageNames.CSharp);
Expand All @@ -39,8 +40,9 @@ public IntelliSenseOptionPageControl(OptionStore optionStore) : base(optionStore
BindToOption(Automatically_show_completion_list_in_argument_lists, CompletionOptionsStorage.TriggerInArgumentLists, LanguageNames.CSharp);

Show_items_from_unimported_namespaces.IsChecked = this.OptionStore.GetOption(CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp);
AddSearchHandler(Show_items_from_unimported_namespaces);
Tab_twice_to_insert_arguments.IsChecked = this.OptionStore.GetOption(CompletionViewOptions.EnableArgumentCompletionSnippets, LanguageNames.CSharp);

AddSearchHandler(Tab_twice_to_insert_arguments);
}

private void Show_completion_list_after_a_character_is_typed_Checked(object sender, RoutedEventArgs e)
Expand Down
7 changes: 7 additions & 0 deletions src/VisualStudio/Core/Impl/Options/AbstractOptionPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,19 @@ public override void SaveSettingsToStorage()
_needsLoadOnNextActivate = true;
}

protected override void SearchStringChanged(string searchString)
{
pageControl.OnSearch(searchString);
}

protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);

if (pageControl != null)
{
// Clear the search because we don't recreate controls
pageControl.OnSearch(string.Empty);
pageControl.Close();
}
}
Expand Down
55 changes: 52 additions & 3 deletions src/VisualStudio/Core/Impl/Options/AbstractOptionPageControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
Expand All @@ -22,6 +21,7 @@ public abstract class AbstractOptionPageControl : UserControl
{
internal readonly OptionStore OptionStore;
private readonly List<BindingExpressionBase> _bindingExpressions = new List<BindingExpressionBase>();
private readonly List<OptionPageSearchHandler> _searchHandlers = new();

protected AbstractOptionPageControl(OptionStore optionStore)
{
Expand Down Expand Up @@ -73,6 +73,8 @@ private protected void BindToOption(CheckBox checkbox, Option2<bool> optionKey)
UpdateSourceTrigger = UpdateSourceTrigger.Default
};

AddSearchHandler(checkbox);

var bindingExpression = checkbox.SetBinding(CheckBox.IsCheckedProperty, binding);
_bindingExpressions.Add(bindingExpression);
}
Expand All @@ -87,6 +89,8 @@ private protected void BindToOption(CheckBox checkbox, Option2<bool?> nullableOp
Converter = new NullableBoolOptionConverter(onNullValue)
};

AddSearchHandler(checkbox);

var bindingExpression = checkbox.SetBinding(CheckBox.IsCheckedProperty, binding);
_bindingExpressions.Add(bindingExpression);
}
Expand All @@ -100,6 +104,8 @@ private protected void BindToOption(CheckBox checkbox, PerLanguageOption2<bool>
UpdateSourceTrigger = UpdateSourceTrigger.Default
};

AddSearchHandler(checkbox);

var bindingExpression = checkbox.SetBinding(CheckBox.IsCheckedProperty, binding);
_bindingExpressions.Add(bindingExpression);
}
Expand All @@ -114,6 +120,8 @@ private protected void BindToOption(CheckBox checkbox, PerLanguageOption2<bool?>
Converter = new NullableBoolOptionConverter(onNullValue)
};

AddSearchHandler(checkbox);

var bindingExpression = checkbox.SetBinding(CheckBox.IsCheckedProperty, binding);
_bindingExpressions.Add(bindingExpression);
}
Expand Down Expand Up @@ -144,7 +152,7 @@ private protected void BindToOption(TextBox textBox, PerLanguageOption2<int> opt
_bindingExpressions.Add(bindingExpression);
}

private protected void BindToOption<T>(ComboBox comboBox, Option2<T> optionKey)
private protected void BindToOption<T>(ComboBox comboBox, Option2<T> optionKey, ContentControl label = null)
{
var binding = new Binding()
{
Expand All @@ -154,11 +162,16 @@ private protected void BindToOption<T>(ComboBox comboBox, Option2<T> optionKey)
ConverterParameter = comboBox
};

AddSearchHandler(comboBox);

if (label is not null)
AddSearchHandler(label);

var bindingExpression = comboBox.SetBinding(ComboBox.SelectedIndexProperty, binding);
_bindingExpressions.Add(bindingExpression);
}

private protected void BindToOption<T>(ComboBox comboBox, PerLanguageOption2<T> optionKey, string languageName)
private protected void BindToOption<T>(ComboBox comboBox, PerLanguageOption2<T> optionKey, string languageName, ContentControl label = null)
{
var binding = new Binding()
{
Expand All @@ -168,6 +181,11 @@ private protected void BindToOption<T>(ComboBox comboBox, PerLanguageOption2<T>
ConverterParameter = comboBox
};

AddSearchHandler(comboBox);

if (label is not null)
AddSearchHandler(label);

var bindingExpression = comboBox.SetBinding(ComboBox.SelectedIndexProperty, binding);
_bindingExpressions.Add(bindingExpression);
}
Expand All @@ -183,6 +201,8 @@ private protected void BindToOption<T>(RadioButton radiobutton, PerLanguageOptio
ConverterParameter = optionValue
};

AddSearchHandler(radiobutton);

var bindingExpression = radiobutton.SetBinding(RadioButton.IsCheckedProperty, binding);
_bindingExpressions.Add(bindingExpression);
}
Expand All @@ -202,6 +222,35 @@ internal virtual void OnSave()
internal virtual void Close()
{
}

internal virtual void OnSearch(string searchString)
{
var shouldScrollIntoView = true;
foreach (var handler in _searchHandlers)
{
if (handler.TryHighlightSearchString(searchString) && shouldScrollIntoView)
{
handler.EnsureVisible();
shouldScrollIntoView = false;
}
}
}

private protected void AddSearchHandler(ComboBox comboBox)
{
foreach (ComboBoxItem item in comboBox.Items)
{
AddSearchHandler(item);
}
}

private protected void AddSearchHandler(ContentControl control)
{
if (control.Content is string content)
{
_searchHandlers.Add(new OptionPageSearchHandler(control, content));
}
}
}

public class RadioButtonCheckedConverter : IValueConverter
Expand Down
123 changes: 123 additions & 0 deletions src/VisualStudio/Core/Impl/Options/OptionPageSearchHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics;
using System.Windows;
using System;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.Options
{
internal class OptionPageSearchHandler
{
public static readonly Brush HighlightForeground = SystemColors.HighlightTextBrush;
public static readonly Brush HighlightBackground = SystemColors.HighlightBrush;

private readonly ContentControl _control;
private readonly string _originalContent;
private readonly int _accessKeyIndex;
private readonly string _content;

public OptionPageSearchHandler(ContentControl control, string originalContent)
{
_control = control;
_originalContent = originalContent;

// Currently only support one access key, and no underscores
Debug.Assert(_originalContent.Split('_').Length <= 2);

_accessKeyIndex = _originalContent.IndexOf('_');

// We strip out the access key so it doesn't interupt search, and because we have to handle displaying it ourselves anyway.
// Since we strip it out, we also don't need to worry about the access key being the character after the underscore
_content = _originalContent.Replace("_", "");
}

public bool TryHighlightSearchString(string searchTerm)
{
var index = _content.IndexOf(searchTerm, StringComparison.CurrentCultureIgnoreCase);
if (index == -1 || string.IsNullOrWhiteSpace(searchTerm))
{
if (_accessKeyIndex != -1)
{
// Unregister and let the content control handle access keys
AccessKeyManager.Unregister(_content[_accessKeyIndex].ToString(), _control);
}

_control.Content = _originalContent;
return false;
}

if (_accessKeyIndex != -1)
{
// Because we are overriding the content entirely, we have to handle access keys
AccessKeyManager.Register(_content[_accessKeyIndex].ToString(), _control);
}

_control.Content = CreateHighlightingTextRun(index, searchTerm.Length);
return true;
}

public void EnsureVisible()
{
_control.BringIntoView();
}

private TextBlock CreateHighlightingTextRun(int highlightStart, int length)
{
var textBlock = new TextBlock();
AddTextRun(textBlock, 0, highlightStart, highlight: false);
AddTextRun(textBlock, highlightStart, length, highlight: true);

var highlightEnd = highlightStart + length;
AddTextRun(textBlock, highlightEnd, _content.Length - highlightEnd, highlight: false);

return textBlock;
}

private void AddTextRun(TextBlock textBlock, int start, int length, bool highlight)
{
if (length <= 0)
return;

// If the access key is in this run, then we actually need to add three runs
if (_accessKeyIndex >= start && _accessKeyIndex < start + length)
{
var firstPartLength = _accessKeyIndex - start;
var lastPartLength = length - firstPartLength - 1;

if (firstPartLength > 0)
textBlock.Inlines.Add(CreateRun(start, firstPartLength, highlight, underline: false));

textBlock.Inlines.Add(CreateRun(_accessKeyIndex, 1, highlight, underline: true));

if (lastPartLength > 0)
textBlock.Inlines.Add(CreateRun(_accessKeyIndex + 1, lastPartLength, highlight, underline: false));
}
else
{
textBlock.Inlines.Add(CreateRun(start, length, highlight, underline: false));
}
}

private Run CreateRun(int start, int length, bool highlight, bool underline)
{
var run = new Run(_content.Substring(start, length));

if (highlight)
{
run.Background = HighlightBackground;
run.Foreground = HighlightForeground;
}

if (underline)
run.TextDecorations.Add(TextDecorations.Underline);

return run;
}
}
}
Loading

0 comments on commit 527202d

Please sign in to comment.