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

[Inline Rename] Update the UI related to Copilot #71332

Merged
merged 14 commits into from
Dec 30, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,10 @@
<ColumnDefinition Width="22" />
</Grid.ColumnDefinitions>

<TextBox
<rename:RenameUserInputPresenter
Grid.Column="0"
x:Name="IdentifierTextBox"
Text="{Binding IdentifierText, UpdateSourceTrigger=PropertyChanged}"
GotFocus="IdentifierTextBox_GotFocus"
HorizontalAlignment="Stretch"
PreviewKeyDown="IdentifierTextBox_KeyDown"/>
x:Name="RenameUserInputPresenter"
HorizontalAlignment="Stretch"/>

<!-- Expand/Collapse button and glyph -->
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
Expand Down Expand Up @@ -52,23 +53,29 @@ public RenameFlyout(
_threadingContext = threadingContext;
_wpfThemeService = themeService;

RenameUserInput = _viewModel.SmartRenameViewModel is null ? new RenameUserInputTextBox(_viewModel) : new SmartRenameUserInputComboBox(_viewModel);

// On load focus the first tab target
Loaded += (s, e) =>
{
// Wait until load to position adornment for space negotiation
PositionAdornment();

IdentifierTextBox.Focus();
IdentifierTextBox.Select(_viewModel.StartingSelection.Start, _viewModel.StartingSelection.Length);
IdentifierTextBox.SelectionChanged += IdentifierTextBox_SelectionChanged;
RenameUserInput.Focus();
RenameUserInput.SelectText(_viewModel.StartingSelection.Start, _viewModel.StartingSelection.Length);
RenameUserInput.TextSelectionChanged += RenameUserInput_TextSelectionChanged;
RenameUserInput.GotFocus += RenameUserInput_GotFocus;
};

InitializeComponent();

RenameUserInputPresenter.Content = RenameUserInput;
RenameUserInput.PreviewKeyDown += RenameUserInput_PreviewKeyDown;

// If smart rename is available, insert the control after the identifier text box.
if (viewModel.SmartRenameViewModel is not null)
veler marked this conversation as resolved.
Show resolved Hide resolved
{
var smartRenameControl = new SmartRenameControl(viewModel.SmartRenameViewModel);
var smartRenameControl = new SmartRenameStatusControl(viewModel.SmartRenameViewModel);
var index = MainPanel.Children.IndexOf(IdentifierAndExpandButtonGrid);
MainPanel.Children.Insert(index + 1, smartRenameControl);
}
Expand All @@ -86,6 +93,8 @@ public RenameFlyout(
_ = DismissToolTipsAsync().CompletesAsyncOperation(token);
}

internal IRenameUserInput RenameUserInput { get; }

private void FormatMappingChanged(object sender, FormatItemsEventArgs e)
{
RefreshColors();
Expand Down Expand Up @@ -195,9 +204,9 @@ private void Adornment_KeyDown(object sender, KeyEventArgs e)
case Key.Tab:
// We don't want tab to lose focus for the adornment, so manually
// loop focus back to the first item that is focusable.
FrameworkElement lastItem = _viewModel.IsExpanded
var lastItem = _viewModel.IsExpanded
? FileRenameCheckbox
: IdentifierTextBox;
: (FrameworkElement)RenameUserInput;

if (lastItem.IsFocused)
{
Expand All @@ -206,12 +215,19 @@ private void Adornment_KeyDown(object sender, KeyEventArgs e)
}

break;
}
}

private void IdentifierTextBox_GotFocus(object sender, RoutedEventArgs e)
veler marked this conversation as resolved.
Show resolved Hide resolved
{
IdentifierTextBox.SelectAll();
case Key.Space:
if (Keyboard.Modifiers == ModifierKeys.Control)
{
e.Handled = true;
// If smart rename is available, trigger it.
if (_viewModel.SmartRenameViewModel is not null && _viewModel.SmartRenameViewModel.GetSuggestionsCommand.CanExecute(null))
{
_viewModel.SmartRenameViewModel.GetSuggestionsCommand.Execute(null);
}
}
break;
}
}

private void Adornment_ConsumeMouseEvent(object sender, MouseButtonEventArgs e)
Expand All @@ -221,56 +237,59 @@ private void Adornment_ConsumeMouseEvent(object sender, MouseButtonEventArgs e)

private void Adornment_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (e.OldFocus == this)
if (e.NewFocus != RenameUserInput)
{
return;
RenameUserInput.Focus();
e.Handled = true;
}

IdentifierTextBox.Focus();
e.Handled = true;
}

private void ToggleExpand(object sender, RoutedEventArgs e)
{
_viewModel.IsExpanded = !_viewModel.IsExpanded;
}

private void RenameUserInput_GotFocus(object sender, RoutedEventArgs e)
{
this.RenameUserInput.SelectAllText();
veler marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Respond to selection/cursor changes in the textbox the user is editing by
/// applying the same selection to the textview that initiated the command
/// </summary>
private void IdentifierTextBox_SelectionChanged(object sender, RoutedEventArgs e)
private void RenameUserInput_TextSelectionChanged(object sender, RoutedEventArgs e)
veler marked this conversation as resolved.
Show resolved Hide resolved
{
// When user is editing the text or make selection change in the text box, sync the selection with text view
if (!this.IdentifierTextBox.IsFocused)
if (!this.RenameUserInput.IsFocused)
{
return;
}

var start = IdentifierTextBox.SelectionStart;
var length = IdentifierTextBox.SelectionLength;
var start = RenameUserInput.TextSelectionStart;
var length = RenameUserInput.TextSelectionLength;

var buffer = _viewModel.InitialTrackingSpan.TextBuffer;
var startPoint = _viewModel.InitialTrackingSpan.GetStartPoint(buffer.CurrentSnapshot);
_textView.SetSelection(new SnapshotSpan(startPoint + start, length));
}

private void IdentifierTextBox_KeyDown(object sender, KeyEventArgs e)
private void RenameUserInput_PreviewKeyDown(object sender, KeyEventArgs e)
{
// When smart rename is available, allow the user choose the suggestions using the up/down keys.
_threadingContext.ThrowIfNotOnUIThread();
var smartRenameViewModel = _viewModel.SmartRenameViewModel;
if (smartRenameViewModel is not null)
{
var currentIdentifier = IdentifierTextBox.Text;
var currentIdentifier = RenameUserInput.Text;
if (e.Key is Key.Down or Key.Up)
{
var newIdentifier = smartRenameViewModel.ScrollSuggestions(currentIdentifier, down: e.Key == Key.Down);
if (newIdentifier is not null)
{
_viewModel.IdentifierText = newIdentifier;
// Place the cursor at the end of the input text box.
IdentifierTextBox.Select(newIdentifier.Length, 0);
RenameUserInput.SelectText(newIdentifier.Length, 0);
e.Handled = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ internal class RenameFlyoutViewModel : INotifyPropertyChanged, IDisposable
private readonly InlineRenameSession _session;
private readonly bool _registerOleComponent;
private readonly IGlobalOptionService _globalOptionService;
private readonly IThreadingContext _threadingContext;
private OleComponent? _oleComponent;
private bool _disposedValue;
private bool _isReplacementTextValid = true;
Expand All @@ -51,7 +50,6 @@ public RenameFlyoutViewModel(
_session = session;
_registerOleComponent = registerOleComponent;
_globalOptionService = globalOptionService;
_threadingContext = threadingContext;
_session.ReplacementTextChanged += OnReplacementTextChanged;
_session.ReplacementsComputed += OnReplacementsComputed;
_session.ReferenceLocationsChanged += OnReferenceLocationsChanged;
Expand All @@ -60,23 +58,12 @@ public RenameFlyoutViewModel(
var smartRenameSession = smartRenameSessionFactory?.Value.CreateSmartRenameSession(_session.TriggerSpan);
if (smartRenameSession is not null)
{
SmartRenameViewModel = new SmartRenameViewModel(threadingContext, listenerProvider, smartRenameSession.Value);
SmartRenameViewModel.OnSelectedSuggestedNameChanged += OnSuggestedNameSelected;
SmartRenameViewModel = new SmartRenameViewModel(threadingContext, listenerProvider, smartRenameSession.Value, this);
}

RegisterOleComponent();
}

private void OnSuggestedNameSelected(object sender, string? selectedName)
{
// When user clicks one of the suggestions, update the IdentifierTextBox content to it.
_threadingContext.ThrowIfNotOnUIThread();
if (selectedName is not null)
{
IdentifierText = selectedName;
}
}

public SmartRenameViewModel? SmartRenameViewModel { get; }

public string IdentifierText
Expand Down Expand Up @@ -327,7 +314,6 @@ protected virtual void Dispose(bool disposing)

if (SmartRenameViewModel is not null)
{
SmartRenameViewModel.OnSelectedSuggestedNameChanged -= OnSuggestedNameSelected;
SmartRenameViewModel.Dispose();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<TextBox x:Class="Microsoft.CodeAnalysis.Editor.Implementation.InlineRename.RenameUserInputTextBox"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vsfx="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
mc:Ignorable="d"
Style="{DynamicResource {x:Static vsfx:VsResourceKeys.TextBoxStyleKey}}"
Text="{Binding IdentifierText, UpdateSourceTrigger=PropertyChanged}"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;

/// <summary>
/// Interaction logic for RenameUserInputTextBox.xaml
/// </summary>
internal sealed partial class RenameUserInputTextBox : TextBox, IRenameUserInput
{
internal RenameUserInputTextBox(RenameFlyoutViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}

public int TextSelectionStart
{
get => SelectionStart;
set => SelectionStart = value;
}

public int TextSelectionLength
{
get => SelectionLength;
set => SelectionLength = value;
}

public event RoutedEventHandler? TextSelectionChanged
{
add
{
AddHandler(SelectionChangedEvent, value, handledEventsToo: false);
}
remove
{
RemoveHandler(SelectionChangedEvent, value);
}
}

event KeyEventHandler? IRenameUserInput.PreviewKeyDown
{
add
{
AddHandler(PreviewKeyDownEvent, value, handledEventsToo: false);
}
remove
{
RemoveHandler(PreviewKeyDownEvent, value);
}
}

public void SelectText(int start, int length)
{
Select(start, length);
}

public void SelectAllText()
{
SelectAll();
}

void IRenameUserInput.Focus()
{
this.Focus();
}
}
31 changes: 31 additions & 0 deletions src/EditorFeatures/Core.Wpf/InlineRename/UI/IRenameUserInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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.Windows;
using System.Windows.Input;

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;

internal interface IRenameUserInput
{
string Text { get; set; }

bool IsFocused { get; }

int TextSelectionStart { get; set; }

int TextSelectionLength { get; set; }

event RoutedEventHandler? TextSelectionChanged;

event RoutedEventHandler? GotFocus;

event KeyEventHandler? PreviewKeyDown;

void Focus();

void SelectText(int start, int length);

void SelectAllText();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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.Windows.Controls;

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;

internal sealed class RenameUserInputPresenter : ContentPresenter
{
internal IRenameUserInput? RenameUserInput => Content as IRenameUserInput;
}
Loading
Loading