File: RenameTracking\RenameTrackingTaggerProvider.TrackingSession.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;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking;
 
internal sealed partial class RenameTrackingTaggerProvider
{
    internal enum TriggerIdentifierKind
    {
        NotRenamable,
        RenamableDeclaration,
        RenamableReference,
    }
 
    /// <summary>
    /// Determines whether the original token was a renameable identifier on a background thread
    /// </summary>
    private class TrackingSession
    {
        private static readonly Task<TriggerIdentifierKind> s_notRenamableTask = Task.FromResult(TriggerIdentifierKind.NotRenamable);
        private readonly Task<TriggerIdentifierKind> _isRenamableIdentifierTask;
        private readonly CancellationTokenSource _cancellationTokenSource = new();
        private readonly CancellationToken _cancellationToken;
        private readonly IThreadingContext _threadingContext;
        private readonly IAsynchronousOperationListener _asyncListener;
 
        private Task<bool> _newIdentifierBindsTask = SpecializedTasks.False;
 
        public string OriginalName { get; }
 
        public ITrackingSpan TrackingSpan { get; }
 
        public bool ForceRenameOverloads { get; private set; }
 
        public TrackingSession(
            StateMachine stateMachine,
            SnapshotSpan snapshotSpan,
            IAsynchronousOperationListener asyncListener)
        {
            _threadingContext = stateMachine.ThreadingContext;
            _asyncListener = asyncListener;
            TrackingSpan = snapshotSpan.Snapshot.CreateTrackingSpan(snapshotSpan.Span, SpanTrackingMode.EdgeInclusive);
            _cancellationToken = _cancellationTokenSource.Token;
 
            if (snapshotSpan.Length > 0)
            {
                // If the snapshotSpan is nonempty, then the session began with a change that
                // was touching a word. Asynchronously determine whether that word was a
                // renameable identifier. If it is, alert the state machine so it can trigger
                // tagging.
 
                OriginalName = snapshotSpan.GetText();
                _isRenamableIdentifierTask = DetermineIfRenamableIdentifierAsync(snapshotSpan, initialCheck: true);
                _isRenamableIdentifierTask.ReportNonFatalErrorAsync();
 
                SwitchToMainThreadAfterAndUpdateSessionTrackerAsync(_isRenamableIdentifierTask).CompletesAsyncOperation(
                     _asyncListener.BeginAsyncOperation(GetType().Name + ".UpdateTrackingSessionAfterIsRenamableIdentifierTask"));
 
                QueueUpdateToStateMachine(stateMachine, _isRenamableIdentifierTask);
            }
            else
            {
                // If the snapshotSpan is empty, that means text was added in a location that is
                // not touching an existing word, which happens a fair amount when writing new
                // code. In this case we already know that the user is not renaming an
                // identifier.
 
                _isRenamableIdentifierTask = s_notRenamableTask;
            }
 
            return;
 
            async Task SwitchToMainThreadAfterAndUpdateSessionTrackerAsync(Task isRenamableIdentifierTask)
            {
                // Use CA(true) so we can stay on the UI thread if already there.
                await isRenamableIdentifierTask.ConfigureAwait(true);
 
                // Avoid throwing an exception in this common case
                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationToken).NoThrowAwaitable();
                if (_cancellationToken.IsCancellationRequested)
                    return;
 
                stateMachine.UpdateTrackingSessionIfRenamable();
            }
        }
 
        private void QueueUpdateToStateMachine(StateMachine stateMachine, Task task)
        {
            QueueUpdateToStateMachineAsync().CompletesAsyncOperation(
                _asyncListener.BeginAsyncOperation($"{GetType().Name}.{nameof(QueueUpdateToStateMachine)}"));
 
            async Task QueueUpdateToStateMachineAsync()
            {
                // Use CA(true) so we can stay on the UI thread if already there.
                await task.ConfigureAwait(true);
 
                // Avoid throwing an exception in this common case
                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationToken).NoThrowAwaitable();
                if (_cancellationToken.IsCancellationRequested)
                    return;
 
                if (await _isRenamableIdentifierTask.ConfigureAwait(true) != TriggerIdentifierKind.NotRenamable)
                    stateMachine.OnTrackingSessionUpdated(this);
            }
        }
 
        internal void CheckNewIdentifier(StateMachine stateMachine, ITextSnapshot snapshot)
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            _newIdentifierBindsTask = DetermineIfNewIdentifierBindsAsync(_isRenamableIdentifierTask);
            _newIdentifierBindsTask.ReportNonFatalErrorAsync();
 
            QueueUpdateToStateMachine(stateMachine, _newIdentifierBindsTask);
 
            async Task<bool> DetermineIfNewIdentifierBindsAsync(Task<TriggerIdentifierKind> isRenamableIdentifierTask)
            {
                // Ensure we do this work on a BG thread.
                await TaskScheduler.Default;
                var isRenamableIdentifier = await isRenamableIdentifierTask.ConfigureAwait(false);
                return isRenamableIdentifier != TriggerIdentifierKind.NotRenamable &&
                    TriggerIdentifierKind.RenamableReference == await DetermineIfRenamableIdentifierAsync(
                        TrackingSpan.GetSpan(snapshot),
                        initialCheck: false).ConfigureAwait(false);
            }
        }
 
        internal bool IsDefinitelyRenamableIdentifierFastCheck()
        {
            // This needs to be able to run on a background thread for the CodeFix
            return IsRenamableIdentifierFastCheck(_isRenamableIdentifierTask, out _);
        }
 
        public void Cancel()
        {
            _threadingContext.ThrowIfNotOnUIThread();
            _cancellationTokenSource.Cancel();
        }
 
        private async Task<TriggerIdentifierKind> DetermineIfRenamableIdentifierAsync(SnapshotSpan snapshotSpan, bool initialCheck)
        {
            // Ensure we do this work on the background.
            await TaskScheduler.Default;
 
            var document = snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document != null)
            {
                var syntaxFactsService = document.GetLanguageService<ISyntaxFactsService>();
                var syntaxTree = await document.GetSyntaxTreeAsync(_cancellationToken).ConfigureAwait(false);
                var token = await syntaxTree.GetTouchingWordAsync(snapshotSpan.Start.Position, syntaxFactsService, _cancellationToken).ConfigureAwait(false);
 
                // The OriginalName is determined with a simple textual check, so for a
                // statement such as "Dim [x = 1" the textual check will return a name of "[x".
                // The token found for "[x" is an identifier token, but only due to error 
                // recovery (the "[x" is actually in the trailing trivia). If the OriginalName
                // found through the textual check has a different length than the span of the 
                // touching word, then we cannot perform a rename.
                if (initialCheck && token.Span.Length != this.OriginalName.Length)
                {
                    return TriggerIdentifierKind.NotRenamable;
                }
 
                var languageHeuristicsService = document.GetLanguageService<IRenameTrackingLanguageHeuristicsService>();
                if (syntaxFactsService.IsIdentifier(token) && languageHeuristicsService.IsIdentifierValidForRenameTracking(token.Text))
                {
                    var semanticModel = await document.ReuseExistingSpeculativeModelAsync(token.Parent, _cancellationToken).ConfigureAwait(false);
                    var semanticFacts = document.GetLanguageService<ISemanticFactsService>();
 
                    var renameSymbolInfo = RenameUtilities.GetTokenRenameInfo(semanticFacts, semanticModel, token, _cancellationToken);
                    if (!renameSymbolInfo.HasSymbols)
                    {
                        return TriggerIdentifierKind.NotRenamable;
                    }
 
                    if (renameSymbolInfo.IsMemberGroup)
                    {
                        // This is a reference from a nameof expression. Allow the rename but set the RenameOverloads option
                        ForceRenameOverloads = true;
 
                        return DetermineIfRenamableSymbols(renameSymbolInfo.Symbols, document);
                    }
                    else
                    {
                        // We do not yet support renaming (inline rename or rename tracking) on
                        // named tuple elements.
                        if (renameSymbolInfo.Symbols.Single().ContainingType?.IsTupleType() == true)
                        {
                            return TriggerIdentifierKind.NotRenamable;
                        }
 
                        return DetermineIfRenamableSymbol(renameSymbolInfo.Symbols.Single(), document, token);
                    }
                }
            }
 
            return TriggerIdentifierKind.NotRenamable;
        }
 
        private TriggerIdentifierKind DetermineIfRenamableSymbols(IEnumerable<ISymbol> symbols, Document document)
        {
            foreach (var symbol in symbols)
            {
                // Get the source symbol if possible
                var sourceSymbol = SymbolFinder.FindSourceDefinition(symbol, document.Project.Solution, _cancellationToken) ?? symbol;
 
                if (!sourceSymbol.IsFromSource())
                {
                    return TriggerIdentifierKind.NotRenamable;
                }
            }
 
            return TriggerIdentifierKind.RenamableReference;
        }
 
        private TriggerIdentifierKind DetermineIfRenamableSymbol(ISymbol symbol, Document document, SyntaxToken token)
        {
            // Get the source symbol if possible
            var sourceSymbol = SymbolFinder.FindSourceDefinition(symbol, document.Project.Solution, _cancellationToken) ?? symbol;
 
            if (sourceSymbol.Kind == SymbolKind.Field &&
                ((IFieldSymbol)sourceSymbol).ContainingType.IsTupleType &&
                sourceSymbol.IsImplicitlyDeclared)
            {
                // should not rename Item1, Item2...
                // when user did not declare them in source.
                return TriggerIdentifierKind.NotRenamable;
            }
 
            if (!sourceSymbol.IsFromSource())
            {
                return TriggerIdentifierKind.NotRenamable;
            }
 
            return sourceSymbol.Locations.Any(static (loc, token) => loc == token.GetLocation(), token)
                    ? TriggerIdentifierKind.RenamableDeclaration
                    : TriggerIdentifierKind.RenamableReference;
        }
 
        internal bool CanInvokeRename(
            ISyntaxFactsService syntaxFactsService,
            IRenameTrackingLanguageHeuristicsService languageHeuristicsService,
            bool isSmartTagCheck)
        {
            if (IsRenamableIdentifierFastCheck(_isRenamableIdentifierTask, out var triggerIdentifierKind))
            {
                var isRenamingDeclaration = triggerIdentifierKind == TriggerIdentifierKind.RenamableDeclaration;
                var newName = TrackingSpan.GetText(TrackingSpan.TextBuffer.CurrentSnapshot);
                var comparison = isRenamingDeclaration || syntaxFactsService.IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
 
                if (!string.Equals(OriginalName, newName, comparison) &&
                    syntaxFactsService.IsValidIdentifier(newName) &&
                    languageHeuristicsService.IsIdentifierValidForRenameTracking(newName))
                {
                    // At this point, we want to allow renaming if the user invoked Ctrl+. explicitly, but we
                    // want to avoid showing a smart tag if we're renaming a reference that binds to an existing
                    // symbol.
                    if (!isSmartTagCheck || isRenamingDeclaration || !NewIdentifierDefinitelyBindsToReference())
                    {
                        return true;
                    }
                }
            }
 
            return false;
        }
 
        private bool NewIdentifierDefinitelyBindsToReference()
            => _newIdentifierBindsTask.Status == TaskStatus.RanToCompletion && _newIdentifierBindsTask.Result;
    }
}