File: InlineRename\InlineRenameService.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.
 
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.InlineRename;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;
 
[Export(typeof(IInlineRenameService))]
[Export(typeof(InlineRenameService))]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class InlineRenameService(
    IThreadingContext threadingContext,
    IUIThreadOperationExecutor uiThreadOperationExecutor,
    ITextBufferAssociatedViewService textBufferAssociatedViewService,
    ITextBufferFactoryService textBufferFactoryService,
    ITextBufferCloneService textBufferCloneService,
    IFeatureServiceFactory featureServiceFactory,
    IGlobalOptionService globalOptions,
    [ImportMany] IEnumerable<IRefactorNotifyService> refactorNotifyServices,
    IAsynchronousOperationListenerProvider listenerProvider) : IInlineRenameService
{
    private readonly IThreadingContext _threadingContext = threadingContext;
    private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor = uiThreadOperationExecutor;
    private readonly ITextBufferAssociatedViewService _textBufferAssociatedViewService = textBufferAssociatedViewService;
    private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.Rename);
    private readonly IEnumerable<IRefactorNotifyService> _refactorNotifyServices = refactorNotifyServices;
    private readonly ITextBufferFactoryService _textBufferFactoryService = textBufferFactoryService;
    private readonly ITextBufferCloneService _textBufferCloneService = textBufferCloneService;
    private readonly IFeatureServiceFactory _featureServiceFactory = featureServiceFactory;
 
    internal readonly IGlobalOptionService GlobalOptions = globalOptions;
 
    private InlineRenameSession? _activeRenameSession;
 
    public InlineRenameSessionInfo StartInlineSession(
        Document document,
        TextSpan textSpan,
        CancellationToken cancellationToken)
    {
        return _threadingContext.JoinableTaskFactory.Run(() => StartInlineSessionAsync(document, textSpan, cancellationToken));
    }
 
    public async Task<InlineRenameSessionInfo> StartInlineSessionAsync(
        Document document,
        TextSpan textSpan,
        CancellationToken cancellationToken)
    {
        if (_activeRenameSession != null)
        {
            throw new InvalidOperationException(EditorFeaturesResources.An_active_inline_rename_session_is_still_active_Complete_it_before_starting_a_new_one);
        }
 
        var editorRenameService = document.GetRequiredLanguageService<IEditorInlineRenameService>();
        var renameInfo = await editorRenameService.GetRenameInfoAsync(document, textSpan.Start, cancellationToken).ConfigureAwait(false);
 
        var readOnlyOrCannotNavigateToSpanSessionInfo = await IsReadOnlyOrCannotNavigateToSpanAsync(
            _threadingContext, renameInfo, document, cancellationToken).ConfigureAwait(false);
        if (readOnlyOrCannotNavigateToSpanSessionInfo != null)
        {
            return readOnlyOrCannotNavigateToSpanSessionInfo;
        }
 
        if (!renameInfo.CanRename)
        {
            return new InlineRenameSessionInfo(renameInfo.LocalizedErrorMessage);
        }
 
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var snapshot = text.FindCorrespondingEditorTextSnapshot();
        Contract.ThrowIfNull(snapshot, "The document used for starting the inline rename session should still be open and associated with a snapshot.");
 
        var fileRenameInfo = renameInfo.GetFileRenameInfo();
        var canRenameFile = fileRenameInfo == InlineRenameFileRenameInfo.Allowed;
 
        var options = new SymbolRenameOptions(
            RenameOverloads: renameInfo.MustRenameOverloads || GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameOverloads),
            RenameInStrings: GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameInStrings),
            RenameInComments: GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameInComments),
            RenameFile: canRenameFile && GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.RenameFile));
 
        var previewChanges = GlobalOptions.GetOption(InlineRenameSessionOptionsStorage.PreviewChanges);
 
        // The session currently has UI thread affinity.
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
        ActiveSession = new InlineRenameSession(
            _threadingContext,
            this,
            document.Project.Solution.Workspace,
            renameInfo.TriggerSpan.ToSnapshotSpan(snapshot),
            renameInfo,
            options,
            previewChanges,
            _uiThreadOperationExecutor,
            _textBufferAssociatedViewService,
            _textBufferFactoryService,
            _textBufferCloneService,
            _featureServiceFactory,
            _refactorNotifyServices,
            _asyncListener);
 
        return new InlineRenameSessionInfo(ActiveSession);
 
        static async Task<InlineRenameSessionInfo?> IsReadOnlyOrCannotNavigateToSpanAsync(
            IThreadingContext threadingContext, IInlineRenameInfo renameInfo, Document document, CancellationToken cancellationToken)
        {
            if (renameInfo is IInlineRenameInfo inlineRenameInfo && inlineRenameInfo.DefinitionLocations != default)
            {
                var workspace = document.Project.Solution.Workspace;
                var navigationService = workspace.Services.GetRequiredService<IDocumentNavigationService>();
                using var _ = PooledObjects.ArrayBuilder<(ITextBuffer, SnapshotSpan)>.GetInstance(out var buffersAndSpans);
                foreach (var documentSpan in inlineRenameInfo.DefinitionLocations)
                {
                    var sourceText = await documentSpan.Document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                    var textSnapshot = sourceText.FindCorrespondingEditorTextSnapshot();
 
                    if (textSnapshot != null)
                    {
                        var buffer = textSnapshot.TextBuffer;
                        var originalSpan = documentSpan.SourceSpan.ToSnapshotSpan(textSnapshot).TranslateTo(buffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive);
                        buffersAndSpans.Add((buffer, originalSpan));
                    }
 
                    var canNavigate = await navigationService.CanNavigateToSpanAsync(
                        workspace, document.Id, documentSpan.SourceSpan, cancellationToken).ConfigureAwait(false);
                    if (!canNavigate)
                    {
                        return new InlineRenameSessionInfo(EditorFeaturesResources.You_cannot_rename_this_element_because_it_is_in_a_location_that_cannot_be_navigated_to);
                    }
                }
 
                await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                foreach (var (buffer, originalSpan) in buffersAndSpans)
                {
                    if (buffer.IsReadOnly(originalSpan))
                    {
                        return new InlineRenameSessionInfo(EditorFeaturesResources.You_cannot_rename_this_element_because_it_is_contained_in_a_read_only_file);
                    }
                }
            }
 
            return null;
        }
    }
 
    IInlineRenameSession? IInlineRenameService.ActiveSession => _activeRenameSession;
 
    internal InlineRenameSession? ActiveSession
    {
        get
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            return _activeRenameSession;
        }
 
        set
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            // This is also checked in InlineRenameSession (which should be the only thing that ever sets this).
            // However, this just adds an extra level of safety to make sure nothing bad is about to happen.
            Contract.ThrowIfTrue(_activeRenameSession != null && value != null, "Cannot assign an active rename session when one is already in progress.");
 
            var previousSession = _activeRenameSession;
            _activeRenameSession = value;
            ActiveSessionChanged?.Invoke(this, new ActiveSessionChangedEventArgs(previousSession!));
        }
    }
 
    /// <summary>
    /// Raised when the ActiveSession property has changed.
    /// </summary>
    internal event EventHandler<ActiveSessionChangedEventArgs>? ActiveSessionChanged;
 
    internal class ActiveSessionChangedEventArgs(InlineRenameSession previousSession) : EventArgs
    {
        public InlineRenameSession PreviousSession { get; } = previousSession;
    }
}