// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.Rename; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking { internal sealed partial class RenameTrackingTaggerProvider { internal enum TriggerIdentifierKind { NotRenamable, RenamableDeclaration, RenamableReference, } /// /// Determines whether the original token was a renameable identifier on a background thread /// private class TrackingSession : ForegroundThreadAffinitizedObject { private static readonly Task s_notRenamableTask = Task.FromResult(TriggerIdentifierKind.NotRenamable); private readonly Task _isRenamableIdentifierTask; private readonly CancellationTokenSource _cancellationTokenSource; private readonly CancellationToken _cancellationToken; private readonly IAsynchronousOperationListener _asyncListener; private Task _newIdentifierBindsTask = SpecializedTasks.False; private readonly string _originalName; public string OriginalName => _originalName; private readonly ITrackingSpan _trackingSpan; public ITrackingSpan TrackingSpan => _trackingSpan; private bool _forceRenameOverloads; public bool ForceRenameOverloads => _forceRenameOverloads; public TrackingSession(StateMachine stateMachine, SnapshotSpan snapshotSpan, IAsynchronousOperationListener asyncListener) : base(stateMachine.ThreadingContext) { AssertIsForeground(); _asyncListener = asyncListener; _trackingSpan = snapshotSpan.Snapshot.CreateTrackingSpan(snapshotSpan.Span, SpanTrackingMode.EdgeInclusive); _cancellationTokenSource = new CancellationTokenSource(); _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 = Task.Factory.SafeStartNewFromAsync( () => DetermineIfRenamableIdentifierAsync(snapshotSpan, initialCheck: true), _cancellationToken, TaskScheduler.Default); var asyncToken = _asyncListener.BeginAsyncOperation(GetType().Name + ".UpdateTrackingSessionAfterIsRenamableIdentifierTask"); _isRenamableIdentifierTask.SafeContinueWithFromAsync( async t => { await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationToken); _cancellationToken.ThrowIfCancellationRequested(); stateMachine.UpdateTrackingSessionIfRenamable(); }, _cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).CompletesAsyncOperation(asyncToken); 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; } } private void QueueUpdateToStateMachine(StateMachine stateMachine, Task task) { var asyncToken = _asyncListener.BeginAsyncOperation($"{GetType().Name}.{nameof(QueueUpdateToStateMachine)}"); task.SafeContinueWithFromAsync(async t => { await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationToken); _cancellationToken.ThrowIfCancellationRequested(); if (_isRenamableIdentifierTask.Result != TriggerIdentifierKind.NotRenamable) { stateMachine.OnTrackingSessionUpdated(this); } }, _cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default).CompletesAsyncOperation(asyncToken); } internal void CheckNewIdentifier(StateMachine stateMachine, ITextSnapshot snapshot) { AssertIsForeground(); _newIdentifierBindsTask = _isRenamableIdentifierTask.SafeContinueWithFromAsync( async t => t.Result != TriggerIdentifierKind.NotRenamable && TriggerIdentifierKind.RenamableReference == await DetermineIfRenamableIdentifierAsync( TrackingSpan.GetSpan(snapshot), initialCheck: false).ConfigureAwait(false), _cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default); QueueUpdateToStateMachine(stateMachine, _newIdentifierBindsTask); } internal bool IsDefinitelyRenamableIdentifier() { // This needs to be able to run on a background thread for the CodeFix return IsRenamableIdentifier(_isRenamableIdentifierTask, waitForResult: false, cancellationToken: CancellationToken.None); } public void Cancel() { AssertIsForeground(); _cancellationTokenSource.Cancel(); } private async Task DetermineIfRenamableIdentifierAsync(SnapshotSpan snapshotSpan, bool initialCheck) { AssertIsBackground(); var document = snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); if (document != null) { var syntaxFactsService = document.GetLanguageService(); 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(); if (syntaxFactsService.IsIdentifier(token) && languageHeuristicsService.IsIdentifierValidForRenameTracking(token.Text)) { var semanticModel = await document.GetSemanticModelForNodeAsync(token.Parent, _cancellationToken).ConfigureAwait(false); var semanticFacts = document.GetLanguageService(); 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 await DetermineIfRenamableSymbolsAsync(renameSymbolInfo.Symbols, document, token).ConfigureAwait(false); } 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 await DetermineIfRenamableSymbolAsync(renameSymbolInfo.Symbols.Single(), document, token).ConfigureAwait(false); } } } return TriggerIdentifierKind.NotRenamable; } private async Task DetermineIfRenamableSymbolsAsync(IEnumerable symbols, Document document, SyntaxToken token) { foreach (var symbol in symbols) { // Get the source symbol if possible var sourceSymbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, document.Project.Solution, _cancellationToken).ConfigureAwait(false) ?? symbol; if (!sourceSymbol.IsFromSource()) { return TriggerIdentifierKind.NotRenamable; } } return TriggerIdentifierKind.RenamableReference; } private async Task DetermineIfRenamableSymbolAsync(ISymbol symbol, Document document, SyntaxToken token) { // Get the source symbol if possible var sourceSymbol = await SymbolFinder.FindSourceDefinitionAsync(symbol, document.Project.Solution, _cancellationToken).ConfigureAwait(false) ?? 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(loc => loc == token.GetLocation()) ? TriggerIdentifierKind.RenamableDeclaration : TriggerIdentifierKind.RenamableReference; } internal bool CanInvokeRename( ISyntaxFactsService syntaxFactsService, IRenameTrackingLanguageHeuristicsService languageHeuristicsService, bool isSmartTagCheck, bool waitForResult, CancellationToken cancellationToken) { if (IsRenamableIdentifier(_isRenamableIdentifierTask, waitForResult, cancellationToken)) { var isRenamingDeclaration = _isRenamableIdentifierTask.Result == 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() { return _newIdentifierBindsTask.Status == TaskStatus.RanToCompletion && _newIdentifierBindsTask.Result; } } } }