Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Highlight the search term in the options page #61301

Merged
merged 4 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,13 @@ 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);

Show_new_snippet_experience.IsChecked = this.OptionStore.GetOption(CompletionOptionsStorage.ShowNewSnippetExperience, LanguageNames.CSharp);
AddSearchHandler(Show_new_snippet_experience);
}

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

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

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

pageControl?.Close();
if (pageControl != null)
{
// Clear the search because we don't recreate controls
pageControl.OnSearch(string.Empty);
pageControl.Close();
}
}
}
}
55 changes: 53 additions & 2 deletions src/VisualStudio/Core/Impl/Options/AbstractOptionPageControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 +74,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 +90,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 +105,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 +121,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 +153,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 +163,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 +182,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 +202,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 +223,36 @@ 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)
{
Debug.Assert(control.Content is string, $"I don't know how to add keyword search support for the '{control.GetType().Name}' control with content type '{control.Content?.GetType().Name ?? "null"}'");
if (control.Content is string content)
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
{
_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