Skip to content

Revert "Stop using the rename dashboard" #77770

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

Merged
merged 1 commit into from
Mar 24, 2025
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 @@ -38,6 +38,14 @@ internal partial class RenameCommandHandler(
IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider)
: AbstractRenameCommandHandler(threadingContext, renameService, globalOptionService, asynchronousOperationListenerProvider.GetListener(FeatureAttribute.Rename))
{
protected override bool AdornmentShouldReceiveKeyboardNavigation(ITextView textView)
=> GetAdornment(textView) switch
{
RenameDashboard dashboard => dashboard.ShouldReceiveKeyboardNavigation,
RenameFlyout => true, // Always receive keyboard navigation for the inline adornment
_ => false
};

protected override void SetFocusToTextView(ITextView textView)
{
(textView as IWpfTextView)?.VisualElement.Focus();
Expand All @@ -51,6 +59,22 @@ protected override void SetFocusToAdornment(ITextView textView)
}
}

protected override void SetAdornmentFocusToNextElement(ITextView textView)
{
if (GetAdornment(textView) is RenameDashboard dashboard)
{
dashboard.FocusNextElement();
}
}

protected override void SetAdornmentFocusToPreviousElement(ITextView textView)
{
if (GetAdornment(textView) is RenameDashboard dashboard)
{
dashboard.FocusNextElement();
}
}

private static InlineRenameAdornment? GetAdornment(ITextView textView)
{
// If our adornment layer somehow didn't get composed, GetAdornmentLayer will throw.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.InlineRename;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.EditorFeatures.Lightup;
using Microsoft.CodeAnalysis.Internal.Log;
Expand All @@ -19,7 +20,7 @@

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename
{
internal sealed class InlineRenameAdornmentManager : IDisposable
internal class InlineRenameAdornmentManager : IDisposable
{
private readonly IWpfTextView _textView;
private readonly IGlobalOptionService _globalOptionService;
Expand Down Expand Up @@ -117,45 +118,59 @@ private void UpdateAdornments()
return null;
}

if (!_textView.HasAggregateFocus)
var useInlineAdornment = _globalOptionService.GetOption(InlineRenameUIOptionsStorage.UseInlineAdornment);
LogAdornmentChoice(useInlineAdornment);
if (useInlineAdornment)
{
// For the rename flyout, the adornment is dismissed on focus lost. There's
// no need to keep an adornment on every textview for show/hide behaviors
return null;
if (!_textView.HasAggregateFocus)
{
// For the rename flyout, the adornment is dismissed on focus lost. There's
// no need to keep an adornment on every textview for show/hide behaviors
return null;
}

// Get the active selection to make sure the rename text is selected in the same way
var originalSpan = _renameService.ActiveSession.TriggerSpan;
var selectionSpan = _textView.Selection.SelectedSpans.First();

var start = selectionSpan.IsEmpty
? 0
: selectionSpan.Start - originalSpan.Start; // The length from the identifier to the start of selection

var length = selectionSpan.IsEmpty
? originalSpan.Length
: selectionSpan.Length;

var identifierSelection = new TextSpan(start, length);

var adornment = new RenameFlyout(
(RenameFlyoutViewModel)s_createdViewModels.GetValue(
_renameService.ActiveSession,
session => new RenameFlyoutViewModel(session,
identifierSelection,
registerOleComponent: true,
_globalOptionService,
_threadingContext,
_listenerProvider,
_smartRenameSessionFactory)),
_textView,
_themeService,
_asyncQuickInfoBroker,
_editorFormatMapService,
_threadingContext,
_listenerProvider);

return adornment;
}
else
{
var newAdornment = new RenameDashboard(
(RenameDashboardViewModel)s_createdViewModels.GetValue(_renameService.ActiveSession, session => new RenameDashboardViewModel(session, _threadingContext, _textView)),
_editorFormatMapService,
_textView);

// Get the active selection to make sure the rename text is selected in the same way
var originalSpan = _renameService.ActiveSession.TriggerSpan;
var selectionSpan = _textView.Selection.SelectedSpans.First();

var start = selectionSpan.IsEmpty
? 0
: selectionSpan.Start - originalSpan.Start; // The length from the identifier to the start of selection

var length = selectionSpan.IsEmpty
? originalSpan.Length
: selectionSpan.Length;

var identifierSelection = new TextSpan(start, length);

var adornment = new RenameFlyout(
(RenameFlyoutViewModel)s_createdViewModels.GetValue(
_renameService.ActiveSession,
session => new RenameFlyoutViewModel(session,
identifierSelection,
registerOleComponent: true,
_globalOptionService,
_threadingContext,
_listenerProvider,
_smartRenameSessionFactory)),
_textView,
_themeService,
_asyncQuickInfoBroker,
_editorFormatMapService,
_threadingContext,
_listenerProvider);

return adornment;
return newAdornment;
}
}

private static bool ViewIncludesBufferFromWorkspace(IWpfTextView textView, Workspace workspace)
Expand All @@ -169,5 +184,13 @@ private static bool ViewIncludesBufferFromWorkspace(IWpfTextView textView, Works
Workspace.TryGetWorkspace(textContainer, out var workspace);
return workspace;
}

private static void LogAdornmentChoice(bool useInlineAdornment)
{
TelemetryLogging.Log(FunctionId.InlineRenameAdornmentChoice, KeyValueLogMessage.Create(m =>
{
m[nameof(useInlineAdornment)] = useInlineAdornment;
}));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ protected class ActiveSpanState
}

protected readonly InlineRenameService InlineRenameService;
private readonly IGlobalOptionService _globalOptionService;
protected readonly Dictionary<ITextBuffer, TBufferState> UndoManagers = [];
protected readonly Stack<ActiveSpanState> UndoStack = new Stack<ActiveSpanState>();
protected readonly Stack<ActiveSpanState> RedoStack = new Stack<ActiveSpanState>();
Expand All @@ -38,9 +39,10 @@ protected class ActiveSpanState

private InlineRenameSession _trackedSession;

public AbstractInlineRenameUndoManager(InlineRenameService inlineRenameService)
public AbstractInlineRenameUndoManager(InlineRenameService inlineRenameService, IGlobalOptionService globalOptionService)
{
this.InlineRenameService = inlineRenameService;
_globalOptionService = globalOptionService;

InlineRenameService.ActiveSessionChanged += InlineRenameService_ActiveSessionChanged;
}
Expand All @@ -52,6 +54,17 @@ private void InlineRenameService_ActiveSessionChanged(object sender, InlineRenam
_trackedSession.ReplacementTextChanged -= InlineRenameSession_ReplacementTextChanged;
}

if (!_globalOptionService.GetOption(InlineRenameUIOptionsStorage.UseInlineAdornment))
{
// If the user is typing directly into the editor as the only way to change
// the replacement text then we don't need to respond to text changes. The
// listener on the textview that calls UpdateCurrentState will handle
// this correctly. This option cannot change when we are currently in a session, so
// only hook up as needed
_trackedSession = null;
return;
}

_trackedSession = InlineRenameService.ActiveSession;

if (_trackedSession is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ internal abstract partial class AbstractRenameCommandHandler(
{
public string DisplayName => EditorFeaturesResources.Rename;

protected abstract bool AdornmentShouldReceiveKeyboardNavigation(ITextView textView);

protected abstract void SetFocusToTextView(ITextView textView);

protected abstract void SetFocusToAdornment(ITextView textView);

protected abstract void SetAdornmentFocusToPreviousElement(ITextView textView);

protected abstract void SetAdornmentFocusToNextElement(ITextView textView);

private CommandState GetCommandState(Func<CommandState> nextHandler)
{
if (renameService.ActiveSession != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;

internal abstract partial class AbstractRenameCommandHandler :
IChainedCommandHandler<TabKeyCommandArgs>,
IChainedCommandHandler<BackTabKeyCommandArgs>
{
public CommandState GetCommandState(TabKeyCommandArgs args, Func<CommandState> nextHandler)
=> GetCommandState(nextHandler);

public void ExecuteCommand(TabKeyCommandArgs args, Action nextHandler, CommandExecutionContext context)
{
// If the Dashboard is focused, just navigate through its UI.
if (AdornmentShouldReceiveKeyboardNavigation(args.TextView))
{
SetAdornmentFocusToNextElement(args.TextView);
return;
}

HandlePossibleTypingCommand(args, nextHandler, context.OperationContext, (activeSession, _, span) =>
{
var spans = new NormalizedSnapshotSpanCollection(
activeSession.GetBufferManager(args.SubjectBuffer)
.GetEditableSpansForSnapshot(args.SubjectBuffer.CurrentSnapshot));

for (var i = 0; i < spans.Count; i++)
{
if (span == spans[i])
{
var selectNext = i < spans.Count - 1 ? i + 1 : 0;
var newSelection = spans[selectNext];
args.TextView.TryMoveCaretToAndEnsureVisible(newSelection.Start);
args.TextView.SetSelection(newSelection);
break;
}
}
});
}

public CommandState GetCommandState(BackTabKeyCommandArgs args, Func<CommandState> nextHandler)
=> GetCommandState(nextHandler);

public void ExecuteCommand(BackTabKeyCommandArgs args, Action nextHandler, CommandExecutionContext context)
{
// If the Dashboard is focused, just navigate through its UI.
if (AdornmentShouldReceiveKeyboardNavigation(args.TextView))
{
SetAdornmentFocusToPreviousElement(args.TextView);
return;
}
else
{
nextHandler();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.CodeAnalysis.Editor.InlineRename;

internal sealed class InlineRenameUIOptionsStorage
{
public static readonly Option2<bool> UseInlineAdornment = new("dotnet_rename_use_inline_adornment", defaultValue: true);
public static readonly Option2<bool> CollapseUI = new("dotnet_collapse_inline_rename_ui", defaultValue: false);
public static readonly Option2<bool> CollapseSuggestionsPanel = new("dotnet_collapse_suggestions_in_inline_rename_ui", defaultValue: false);
public static readonly Option2<bool> GetSuggestionsAutomatically = new("dotnet_rename_get_suggestions_automatically", defaultValue: false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;
[ExportWorkspaceServiceFactory(typeof(IInlineRenameUndoManager), ServiceLayer.Default), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class UndoManagerServiceFactory(InlineRenameService inlineRenameService) : IWorkspaceServiceFactory
internal class UndoManagerServiceFactory(InlineRenameService inlineRenameService, IGlobalOptionService globalOptionService) : IWorkspaceServiceFactory
{
private readonly InlineRenameService _inlineRenameService = inlineRenameService;
private readonly IGlobalOptionService _globalOptionService = globalOptionService;

public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
=> new InlineRenameUndoManager(inlineRenameService);
=> new InlineRenameUndoManager(_inlineRenameService, _globalOptionService);

internal sealed class InlineRenameUndoManager(InlineRenameService inlineRenameService)
: AbstractInlineRenameUndoManager<InlineRenameUndoManager.BufferUndoState>(inlineRenameService), IInlineRenameUndoManager
internal class InlineRenameUndoManager(InlineRenameService inlineRenameService, IGlobalOptionService globalOptionService) : AbstractInlineRenameUndoManager<InlineRenameUndoManager.BufferUndoState>(inlineRenameService, globalOptionService), IInlineRenameUndoManager
{
internal class BufferUndoState
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Microsoft.VisualStudio.Utilities;

using IntellisenseQuickInfoItem = Microsoft.VisualStudio.Language.Intellisense.QuickInfoItem;
using Microsoft.CodeAnalysis.Editor.InlineRename;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo;

Expand Down Expand Up @@ -51,7 +52,7 @@ public async Task<IntellisenseQuickInfoItem> GetQuickInfoItemAsync(IAsyncQuickIn
// Until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1611398 is resolved we can't disable
// quickinfo in InlineRename. Instead, we return no quickinfo information while the adornment
// is being shown. This can be removed after IFeaturesService supports disabling quickinfo
if (_inlineRenameService.ActiveSession is not null)
if (_editorOptionsService.GlobalOptions.GetOption(InlineRenameUIOptionsStorage.UseInlineAdornment) && _inlineRenameService.ActiveSession is not null)
return null;

var triggerPoint = session.GetTriggerPoint(_subjectBuffer.CurrentSnapshot);
Expand Down
44 changes: 44 additions & 0 deletions src/EditorFeatures/Test2/Rename/RenameCommandHandlerTests.vb
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,50 @@ End Class
End Using
End Sub

<WpfTheory>
<CombinatorialData, Trait(Traits.Feature, Traits.Features.Rename)>
Public Async Function TypingTabDuringRename(host As RenameTestHost) As Task
Using workspace = CreateWorkspaceWithWaiter(
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
class $$Goo
{
Goo f;
}
</Document>
</Project>
</Workspace>, host)

' This test specifically matters for the case where a user is typing in the editor
' and is not intended to test the rename flyout tab behavior
Dim optionsService = workspace.GetService(Of IGlobalOptionService)()
optionsService.SetGlobalOption(InlineRenameUIOptionsStorage.UseInlineAdornment, False)

Dim view = workspace.Documents.Single().GetTextView()
view.Caret.MoveTo(New SnapshotPoint(view.TextBuffer.CurrentSnapshot, workspace.Documents.Single(Function(d) d.CursorPosition.HasValue).CursorPosition.Value))

Dim commandHandler = CreateCommandHandler(workspace)

Dim session = StartSession(workspace)

' TODO: should we make tab wait instead?
Await WaitForRename(workspace)

' Unfocus the dashboard
Dim dashboard = DirectCast(view.GetAdornmentLayer("RoslynRenameDashboard").Elements(0).Adornment, RenameDashboard)
dashboard.ShouldReceiveKeyboardNavigation = False

commandHandler.ExecuteCommand(New TabKeyCommandArgs(view, view.TextBuffer),
Sub() AssertEx.Fail("Tab should not have been passed to the editor."),
Utilities.TestCommandExecutionContext.Create())

Assert.Equal(3, view.Caret.Position.BufferPosition.GetContainingLineNumber())

session.Cancel()
End Using
End Function

<WpfTheory>
<CombinatorialData, Trait(Traits.Feature, Traits.Features.Rename)>
Public Async Function SelectAllDuringRename(host As RenameTestHost) As Task
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@
<StackPanel>
<CheckBox x:Name="Rename_asynchronously_exerimental"
Content="{x:Static local:AdvancedOptionPageStrings.Option_Rename_asynchronously_experimental}" />

<Label x:Name="Rename_UI_setting_label" Content="{x:Static local:AdvancedOptionPageStrings.Where_should_the_rename_UI_be_shown}" />
<ComboBox x:Name="Rename_UI_setting">
<ComboBoxItem x:Name="Rename_UI_inline"
Content="{x:Static local:AdvancedOptionPageStrings.Option_Show_UI_inline}"
Tag="{StaticResource True}" />
<ComboBoxItem x:Name="Rename_UI_dashboard"
Content="{x:Static local:AdvancedOptionPageStrings.Option_Show_UI_as_dashboard_in_top_right}"
Tag="{StaticResource False}" />
</ComboBox>
</StackPanel>
</GroupBox>

Expand Down
Loading
Loading