diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/RenameFlyoutViewModel.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/RenameFlyoutViewModel.cs index 073bbc74a5850..fd3fa12848099 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/RenameFlyoutViewModel.cs +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Adornment/RenameFlyoutViewModel.cs @@ -24,6 +24,7 @@ using Microsoft.VisualStudio.Imaging.Interop; using Microsoft.VisualStudio.PlatformUI.OleComponentSupport; using Microsoft.VisualStudio.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename { @@ -228,7 +229,7 @@ public bool Submit() } SmartRenameViewModel?.Commit(IdentifierText); - Session.Commit(); + Session.InitiateCommit(); return true; } diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/RenameDashboard.xaml.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/RenameDashboard.xaml.cs index eab834a57bc04..65752cc5d5b70 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/RenameDashboard.xaml.cs +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/RenameDashboard.xaml.cs @@ -324,7 +324,7 @@ private void Commit() { try { - _model.Session.Commit(); + _model.Session.InitiateCommit(); _textView.VisualElement.Focus(); } catch (NotSupportedException ex) diff --git a/src/EditorFeatures/Core/IInlineRenameSession.cs b/src/EditorFeatures/Core/IInlineRenameSession.cs index f644a16a423b5..f758c652b4f75 100644 --- a/src/EditorFeatures/Core/IInlineRenameSession.cs +++ b/src/EditorFeatures/Core/IInlineRenameSession.cs @@ -52,5 +52,8 @@ internal interface IInlineRenameSession /// /// Dismisses the rename session, completing the rename operation across all files. /// + /// + /// It will only be async when InlineRenameUIOptionsStorage.CommitRenameAsynchronously is set to true. + /// Task CommitAsync(bool previewChanges, IUIThreadOperationContext editorOperationContext = null); } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler.cs index 1418ff7d6b998..c23d64c5c9f7d 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler.cs @@ -55,6 +55,11 @@ private void HandlePossibleTypingCommand(TArgs args, Action nextHandler, return; } + if (renameService.ActiveSession.IsCommitInProgress) + { + return; + } + var selectedSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(args.SubjectBuffer); if (selectedSpans.Count > 1) @@ -104,4 +109,7 @@ private void Commit(IUIThreadOperationContext operationContext) RoslynDebug.AssertNotNull(renameService.ActiveSession); renameService.ActiveSession.Commit(previewChanges: false, operationContext); } + + private bool IsRenameCommitInProgress() + => renameService.ActiveSession?.IsCommitInProgress is true; } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_MoveSelectedLinesHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_MoveSelectedLinesHandler.cs index e9a140815a143..3e7e92b0f14c1 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_MoveSelectedLinesHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_MoveSelectedLinesHandler.cs @@ -15,6 +15,9 @@ public CommandState GetCommandState(MoveSelectedLinesUpCommandArgs args) public bool ExecuteCommand(MoveSelectedLinesUpCommandArgs args, CommandExecutionContext context) { + if (IsRenameCommitInProgress()) + return true; + CommitIfActive(args, context.OperationContext); return false; } @@ -24,6 +27,9 @@ public CommandState GetCommandState(MoveSelectedLinesDownCommandArgs args) public bool ExecuteCommand(MoveSelectedLinesDownCommandArgs args, CommandExecutionContext context) { + if (IsRenameCommitInProgress()) + return true; + CommitIfActive(args, context.OperationContext); return false; } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RefactoringWithCommandHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RefactoringWithCommandHandler.cs index a3413138c2e74..7d2a383d99b9a 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RefactoringWithCommandHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RefactoringWithCommandHandler.cs @@ -18,6 +18,9 @@ public CommandState GetCommandState(ReorderParametersCommandArgs args) public bool ExecuteCommand(ReorderParametersCommandArgs args, CommandExecutionContext context) { + if (IsRenameCommitInProgress()) + return true; + CommitIfActive(args, context.OperationContext); return false; } @@ -27,6 +30,9 @@ public CommandState GetCommandState(RemoveParametersCommandArgs args) public bool ExecuteCommand(RemoveParametersCommandArgs args, CommandExecutionContext context) { + if (IsRenameCommitInProgress()) + return true; + CommitIfActive(args, context.OperationContext); return false; } @@ -36,6 +42,9 @@ public CommandState GetCommandState(ExtractInterfaceCommandArgs args) public bool ExecuteCommand(ExtractInterfaceCommandArgs args, CommandExecutionContext context) { + if (IsRenameCommitInProgress()) + return true; + CommitIfActive(args, context.OperationContext); return false; } @@ -45,6 +54,9 @@ public CommandState GetCommandState(EncapsulateFieldCommandArgs args) public bool ExecuteCommand(EncapsulateFieldCommandArgs args, CommandExecutionContext context) { + if (IsRenameCommitInProgress()) + return true; + CommitIfActive(args, context.OperationContext); return false; } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RenameHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RenameHandler.cs index 6e02773342067..e2b09c4c412a3 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RenameHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_RenameHandler.cs @@ -67,10 +67,15 @@ private async Task ExecuteCommandAsync(RenameCommandArgs args, IUIThreadOperatio // If there is already an active session, commit it first if (renameService.ActiveSession != null) { - // Is the caret within any of the rename fields in this buffer? - // If so, focus the dashboard + if (renameService.ActiveSession.IsCommitInProgress) + { + return; + } + if (renameService.ActiveSession.TryGetContainingEditableSpan(caretPoint.Value, out _)) { + // Is the caret within any of the rename fields in this buffer? + // If so, focus the dashboard SetFocusToAdornment(args.TextView); return; } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_ReturnHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_ReturnHandler.cs index e7d36e424cd9a..cd51267b3d521 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_ReturnHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_ReturnHandler.cs @@ -27,7 +27,7 @@ public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext co protected virtual void CommitAndSetFocus(InlineRenameSession activeSession, ITextView textView, IUIThreadOperationContext operationContext) { - Commit(operationContext); + activeSession.InitiateCommit(operationContext); SetFocusToTextView(textView); } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_UndoRedoHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_UndoRedoHandler.cs index 5f665da1742a3..04c9d2d2aaa32 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_UndoRedoHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_UndoRedoHandler.cs @@ -18,31 +18,43 @@ public CommandState GetCommandState(RedoCommandArgs args) public bool ExecuteCommand(UndoCommandArgs args, CommandExecutionContext context) { - if (renameService.ActiveSession != null) + if (renameService.ActiveSession == null) { - for (var i = 0; i < args.Count && renameService.ActiveSession != null; i++) - { - renameService.ActiveSession.UndoManager.Undo(args.SubjectBuffer); - } + return false; + } + if (renameService.ActiveSession.IsCommitInProgress) + { + // When rename commit is in progress, handle the command so it won't change the workspace return true; } - return false; + for (var i = 0; i < args.Count && renameService.ActiveSession != null; i++) + { + renameService.ActiveSession.UndoManager.Undo(args.SubjectBuffer); + } + + return true; } public bool ExecuteCommand(RedoCommandArgs args, CommandExecutionContext context) { - if (renameService.ActiveSession != null) + if (renameService.ActiveSession == null) { - for (var i = 0; i < args.Count && renameService.ActiveSession != null; i++) - { - renameService.ActiveSession.UndoManager.Redo(args.SubjectBuffer); - } + return false; + } + if (renameService.ActiveSession.IsCommitInProgress) + { + // When rename commit is in progress, handle the command so it won't change the workspace return true; } - return false; + for (var i = 0; i < args.Count && renameService.ActiveSession != null; i++) + { + renameService.ActiveSession.UndoManager.Redo(args.SubjectBuffer); + } + + return true; } } diff --git a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_WordDeleteHandler.cs b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_WordDeleteHandler.cs index efbf835488bd9..c11b6dddfe73d 100644 --- a/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_WordDeleteHandler.cs +++ b/src/EditorFeatures/Core/InlineRename/CommandHandlers/AbstractRenameCommandHandler_WordDeleteHandler.cs @@ -34,6 +34,12 @@ private bool HandleWordDeleteCommand(ITextBuffer subjectBuffer, ITextView view, return false; } + if (renameService.ActiveSession.IsCommitInProgress) + { + // When rename commit is in progress, swallow the command so it won't change the workspace + return true; + } + var caretPoint = view.GetCaretPoint(subjectBuffer); if (caretPoint.HasValue) { diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs index 7637bb6cef2a5..4ea243f8f2c37 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs @@ -744,12 +744,25 @@ private bool CommitSynchronously(bool previewChanges, IUIThreadOperationContext return _threadingContext.JoinableTaskFactory.Run(() => CommitWorkerAsync(previewChanges, canUseBackgroundWorkIndicator: false, operationContext)); } + /// + /// Start to commit the rename session. + /// Session might be committed sync or async, depends on the value of InlineRenameUIOptionsStorage.CommitRenameAsynchronously. + /// If it is committed async, method will only kick off the task. + /// + /// + public void InitiateCommit(IUIThreadOperationContext editorOperationContext = null) + { + var token = _asyncListener.BeginAsyncOperation(nameof(InitiateCommit)); + _ = CommitAsync(previewChanges: false, editorOperationContext) + .ReportNonFatalErrorAsync().CompletesAsyncOperation(token); + } + /// /// Caller should pass in the IUIThreadOperationContext if it is called from editor so rename commit operation could set up the its own context correctly. /// public async Task CommitAsync(bool previewChanges, IUIThreadOperationContext editorOperationContext = null) { - if (this.RenameService.GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameAsynchronously)) + if (this.RenameService.GlobalOptions.ShouldCommitAsynchronously()) { await CommitWorkerAsync(previewChanges, canUseBackgroundWorkIndicator: true, editorOperationContext).ConfigureAwait(false); } @@ -782,6 +795,12 @@ private async Task CommitWorkerAsync(bool previewChanges, bool canUseBackg return false; } + // Don't dup commit. + if (this.IsCommitInProgress) + { + return false; + } + previewChanges = previewChanges || PreviewChanges; if (editorUIOperationContext is not null) diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameSessionOptionsStorage.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameSessionOptionsStorage.cs index 7655b058f080c..bb7197eaec312 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameSessionOptionsStorage.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameSessionOptionsStorage.cs @@ -13,5 +13,10 @@ internal static class InlineRenameSessionOptionsStorage public static readonly Option2 RenameInComments = new("dotnet_rename_in_comments", defaultValue: false); public static readonly Option2 RenameFile = new("dotnet_rename_file", defaultValue: true); public static readonly Option2 PreviewChanges = new("dotnet_preview_inline_rename_changes", defaultValue: false); - public static readonly Option2 RenameAsynchronously = new("dotnet_rename_asynchronously", defaultValue: true); + + public static readonly Option2 CommitRenameAsynchronously = new("dotnet_commit_rename_asynchronously", defaultValue: null); + public static readonly Option2 CommitRenameAsynchronouslyFeatureFlag = new("dotnet_commit_rename_asynchronously_feature_flag", defaultValue: false); + + public static bool ShouldCommitAsynchronously(this IGlobalOptionService globalOptionService) + => globalOptionService.GetOption(CommitRenameAsynchronously) ?? globalOptionService.GetOption(CommitRenameAsynchronouslyFeatureFlag); } diff --git a/src/EditorFeatures/Test2/Rename/RenameViewModelTests.vb b/src/EditorFeatures/Test2/Rename/RenameViewModelTests.vb index 16b38ba740a4e..e38a28bf024f3 100644 --- a/src/EditorFeatures/Test2/Rename/RenameViewModelTests.vb +++ b/src/EditorFeatures/Test2/Rename/RenameViewModelTests.vb @@ -561,6 +561,8 @@ class D : B Dim configService = workspace.ExportProvider.GetExportedValue(Of TestWorkspaceConfigurationService) configService.Options = New WorkspaceConfigurationOptions(SourceGeneratorExecution:=executionPreference) + Dim listenerProvider = workspace.ExportProvider.GetExport(Of IAsynchronousOperationListenerProvider)().Value + Dim cursorDocument = workspace.Documents.Single(Function(d) d.CursorPosition.HasValue) Dim cursorPosition = cursorDocument.CursorPosition.Value @@ -625,7 +627,6 @@ class D : B End Using Dim TestQuickInfoBroker = New TestQuickInfoBroker() - Dim listenerProvider = workspace.ExportProvider.GetExport(Of IAsynchronousOperationListenerProvider)().Value Dim editorFormatMapService = workspace.ExportProvider.GetExport(Of IEditorFormatMapService)().Value Using flyout = New RenameFlyout( diff --git a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs index e440ed8430a4b..8223907a25851 100644 --- a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs +++ b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs @@ -105,7 +105,10 @@ public AdvancedOptionPageControl(OptionStore optionStore, IComponentModel compon BindToOption(Always_use_default_symbol_servers_for_navigation, MetadataAsSourceOptionsStorage.AlwaysUseDefaultSymbolServers); // Rename - BindToOption(Rename_asynchronously_exerimental, InlineRenameSessionOptionsStorage.RenameAsynchronously); + BindToOption(Rename_asynchronously_exerimental, InlineRenameSessionOptionsStorage.CommitRenameAsynchronously, () => + { + return optionStore.GetOption(InlineRenameSessionOptionsStorage.CommitRenameAsynchronouslyFeatureFlag); + }); BindToOption(Rename_UI_setting, InlineRenameUIOptionsStorage.UseInlineAdornment, label: Rename_UI_setting_label); // Using Directives diff --git a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs index 8ceae9cd6694d..5a2e148c15abc 100644 --- a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs +++ b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs @@ -344,8 +344,11 @@ public bool TryFetch(LocalUserRegistryOptionPersister persister, OptionKey2 opti {"dotnet_rename_use_inline_adornment", new RoamingProfileStorage("TextEditor.RenameUseInlineAdornment")}, {"dotnet_preview_inline_rename_changes", new RoamingProfileStorage("TextEditor.Specific.PreviewRename")}, {"dotnet_collapse_suggestions_in_inline_rename_ui", new RoamingProfileStorage("TextEditor.CollapseRenameSuggestionsUI")}, + {"dotnet_commit_rename_asynchronously", new RoamingProfileStorage("TextEditor.Specific.CommitRenameAsynchronously")}, + {"dotnet_commit_rename_asynchronously_feature_flag", new FeatureFlagStorage("Roslyn.CommitRenameAsynchronously")}, {"dotnet_rename_get_suggestions_automatically", new FeatureFlagStorage(@"Editor.AutoSmartRenameSuggestions")}, - {"dotnet_rename_asynchronously", new RoamingProfileStorage("TextEditor.Specific.RenameAsynchronously")}, + // Option is deprecated, don't use the same RoamingProfileStorage key + // {"dotnet_rename_asynchronously", new RoamingProfileStorage("TextEditor.Specific.RenameAsynchronously")}, {"dotnet_rename_file", new RoamingProfileStorage("TextEditor.Specific.RenameFile")}, {"dotnet_rename_in_comments", new RoamingProfileStorage("TextEditor.Specific.RenameInComments")}, {"dotnet_rename_in_strings", new RoamingProfileStorage("TextEditor.Specific.RenameInStrings")}, diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/CSharp/CSharpRename.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/CSharp/CSharpRename.cs index 537211b1d1250..a21f24aa2f944 100644 --- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/CSharp/CSharpRename.cs +++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/CSharp/CSharpRename.cs @@ -43,6 +43,7 @@ public override async Task InitializeAsync() globalOptions.SetGlobalOption(InlineRenameSessionOptionsStorage.RenameOverloads, false); globalOptions.SetGlobalOption(InlineRenameSessionOptionsStorage.RenameFile, true); globalOptions.SetGlobalOption(InlineRenameSessionOptionsStorage.PreviewChanges, false); + globalOptions.SetGlobalOption(InlineRenameSessionOptionsStorage.CommitRenameAsynchronously, false); } [IdeFact] @@ -805,4 +806,51 @@ void Method() // Make sure the file is renamed. If the file is not found, this call would throw exception await TestServices.SolutionExplorer.GetProjectItemAsync(projectName, "MyTestClass.cs", HangMitigatingCancellationToken); } + + [CombinatorialData] + [IdeTheory] + public async Task VerifyAsyncRename(bool useInlineRename) + { + var globalOptions = await TestServices.Shell.GetComponentModelServiceAsync(HangMitigatingCancellationToken); + globalOptions.SetGlobalOption(InlineRenameSessionOptionsStorage.CommitRenameAsynchronously, true); + + if (!useInlineRename) + globalOptions.SetGlobalOption(InlineRenameUIOptionsStorage.UseInlineAdornment, false); + + var markup = """ + class Program + { + static void Main(string[] args) + { + int x = 100; + Te$$stMethod(x); + } + + static void TestMethod(int y) + { + + } + } + """; + await SetUpEditorAsync(markup, HangMitigatingCancellationToken); + await TestServices.InlineRename.InvokeAsync(HangMitigatingCancellationToken); + await TestServices.Input.SendWithoutActivateAsync(["AsyncRenameMethod", VirtualKeyCode.RETURN], HangMitigatingCancellationToken); + await TestServices.Workspace.WaitForRenameAsync(HangMitigatingCancellationToken); + await TestServices.EditorVerifier.TextEqualsAsync( + """ + class Program + { + static void Main(string[] args) + { + int x = 100; + AsyncRenameMethod$$(x); + } + + static void AsyncRenameMethod(int y) + { + + } + } + """, HangMitigatingCancellationToken); + } } diff --git a/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml.vb b/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml.vb index f3383eddb4831..363c82418445a 100644 --- a/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml.vb +++ b/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml.vb @@ -98,7 +98,10 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Options End Function) ' Rename - BindToOption(Rename_asynchronously_exerimental, InlineRenameSessionOptionsStorage.RenameAsynchronously) + BindToOption(Rename_asynchronously_exerimental, InlineRenameSessionOptionsStorage.CommitRenameAsynchronously, + Function() + Return optionStore.GetOption(InlineRenameSessionOptionsStorage.CommitRenameAsynchronouslyFeatureFlag) + End Function) BindToOption(Rename_UI_setting, InlineRenameUIOptionsStorage.UseInlineAdornment, label:=Rename_UI_setting_label) ' Import directives