File: InlineRename\CommandHandlers\AbstractRenameCommandHandler_RenameHandler.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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.
 
#nullable disable
 
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.InlineRename;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;
 
internal abstract partial class AbstractRenameCommandHandler : ICommandHandler<RenameCommandArgs>
{
    public CommandState GetCommandState(RenameCommandArgs args)
    {
        var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer);
        if (!caretPoint.HasValue)
        {
            return CommandState.Unspecified;
        }
 
        if (!CanRename(args))
        {
            return CommandState.Unspecified;
        }
 
        return CommandState.Available;
    }
 
    public bool ExecuteCommand(RenameCommandArgs args, CommandExecutionContext context)
    {
        if (!CanRename(args))
        {
            return false;
        }
 
        var token = listener.BeginAsyncOperation(nameof(ExecuteCommand));
        _ = ExecuteCommandAsync(args, context.OperationContext).CompletesAsyncOperation(token);
        return true;
    }
 
    private async Task ExecuteCommandAsync(RenameCommandArgs args, IUIThreadOperationContext editorOperationContext)
    {
        threadingContext.ThrowIfNotOnUIThread();
 
        if (!args.SubjectBuffer.TryGetWorkspace(out var workspace))
        {
            return;
        }
 
        var caretPoint = args.TextView.GetCaretPoint(args.SubjectBuffer);
        if (!caretPoint.HasValue)
        {
            await ShowErrorDialogAsync(workspace, FeaturesResources.You_must_rename_an_identifier).ConfigureAwait(false);
            return;
        }
 
        // If there is already an active session, commit it first
        if (renameService.ActiveSession != null)
        {
            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;
            }
            else
            {
                // Otherwise, commit or cancel the existing session and start a new one.
                // Set placeCaretAtTheEndOfIdentifier to false because a new rename session will be created based on caret's location.
                CommitIfSynchronousOrCancelIfAsynchronous(args, editorOperationContext, placeCaretAtTheEndOfIdentifier: false);
            }
        }
 
        var backgroundWorkIndicatorFactory = workspace.Services.GetRequiredService<IBackgroundWorkIndicatorFactory>();
        using var context = backgroundWorkIndicatorFactory.Create(
            args.TextView,
            args.TextView.GetTextElementSpan(caretPoint.Value),
            EditorFeaturesResources.Finding_token_to_rename);
 
        var cancellationToken = context.UserCancellationToken;
 
        var document = await args
            .SubjectBuffer
            .CurrentSnapshot
            .GetFullyLoadedOpenDocumentInCurrentContextWithChangesAsync(context)
            .ConfigureAwait(false);
 
        if (document == null)
        {
            await ShowErrorDialogAsync(workspace, FeaturesResources.You_must_rename_an_identifier).ConfigureAwait(false);
            return;
        }
 
        var selectedSpans = args.TextView.Selection.GetSnapshotSpansOnBuffer(args.SubjectBuffer);
 
        // Now make sure the entire selection is contained within that token.
        // There can be zero selectedSpans in projection scenarios.
        if (selectedSpans.Count != 1)
        {
            await ShowErrorDialogAsync(workspace, FeaturesResources.You_must_rename_an_identifier).ConfigureAwait(false);
            return;
        }
 
        var sessionInfo = await renameService.StartInlineSessionAsync(document, selectedSpans.Single().Span.ToTextSpan(), cancellationToken).ConfigureAwait(false);
        if (!sessionInfo.CanRename)
        {
            await ShowErrorDialogAsync(workspace, sessionInfo.LocalizedErrorMessage).ConfigureAwait(false);
            return;
        }
    }
 
    private static bool CanRename(RenameCommandArgs args)
    {
        return args.SubjectBuffer.TryGetWorkspace(out var workspace) &&
            workspace.CanApplyChange(ApplyChangesKind.ChangeDocument) &&
            args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges() is Document document &&
            document.GetLanguageService<IEditorInlineRenameService>() is IEditorInlineRenameService inlineRenameService &&
            inlineRenameService.IsEnabled &&
            args.SubjectBuffer.SupportsRename() && !args.SubjectBuffer.IsInLspEditorContext();
    }
 
    private async Task ShowErrorDialogAsync(Workspace workspace, string message)
    {
        await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();
        var notificationService = workspace.Services.GetService<INotificationService>();
        notificationService.SendNotification(message, title: EditorFeaturesResources.Rename, severity: NotificationSeverity.Error);
    }
}