File: EventHookup\EventHookupSessionManager_EventHookupSession.cs
Web Access
Project: src\src\EditorFeatures\CSharp\Microsoft.CodeAnalysis.CSharp.EditorFeatures.csproj (Microsoft.CodeAnalysis.CSharp.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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles.SymbolSpecification;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup;
 
internal sealed partial class EventHookupSessionManager
{
    /// <summary>
    /// A session begins when an '=' is typed after a '+' and requires determining whether the
    /// += is being used to add an event handler to an event. If it is, then we also determine 
    /// a candidate name for the event handler.
    /// </summary>
    internal class EventHookupSession
    {
        private readonly IThreadingContext _threadingContext;
        private readonly ITrackingSpan _trackingSpan;
        private readonly ITextView _textView;
        private readonly ITextBuffer _subjectBuffer;
 
        public event Action Dismissed = () => { };
 
        // For testing purposes only! Should always be null except in tests.
        internal Mutex? TESTSessionHookupMutex = null;
 
        private Task<string?>? _eventNameTask;
        private CancellationTokenSource? _cancellationTokenSource;
 
        public ITrackingSpan TrackingSpan
        {
            get
            {
                _threadingContext.ThrowIfNotOnUIThread();
                return _trackingSpan;
            }
        }
 
        public ITextView TextView
        {
            get
            {
                _threadingContext.ThrowIfNotOnUIThread();
                return _textView;
            }
        }
 
        public ITextBuffer SubjectBuffer
        {
            get
            {
                _threadingContext.ThrowIfNotOnUIThread();
                return _subjectBuffer;
            }
        }
 
        public void CancelBackgroundTasks()
        {
            _threadingContext.ThrowIfNotOnUIThread();
            _cancellationTokenSource?.Cancel();
        }
 
        public EventHookupSession(
            EventHookupSessionManager eventHookupSessionManager,
            EventHookupCommandHandler commandHandler,
            ITextView textView,
            ITextBuffer subjectBuffer,
            int position,
            Document document,
            IAsynchronousOperationListener asyncListener,
            Mutex testSessionHookupMutex)
        {
            _threadingContext = eventHookupSessionManager.ThreadingContext;
            _threadingContext.ThrowIfNotOnUIThread();
 
            _cancellationTokenSource = new();
            var cancellationToken = _cancellationTokenSource.Token;
            _textView = textView;
            _subjectBuffer = subjectBuffer;
            this.TESTSessionHookupMutex = testSessionHookupMutex;
 
            // If the caret is at the end of the document we just create an empty span
            var length = subjectBuffer.CurrentSnapshot.Length > position + 1 ? 1 : 0;
            _trackingSpan = subjectBuffer.CurrentSnapshot.CreateTrackingSpan(new Span(position, length), SpanTrackingMode.EdgeInclusive);
 
            var asyncToken = asyncListener.BeginAsyncOperation(GetType().Name + ".Start");
 
            _eventNameTask = Task.Factory.SafeStartNewFromAsync(
                () => DetermineIfEventHookupAndGetHandlerNameAsync(document, position, cancellationToken),
                cancellationToken,
                TaskScheduler.Default);
 
            var continuedTask = _eventNameTask.SafeContinueWithFromAsync(
                async t =>
                {
                    await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
 
                    // Once we compute the name, update the tooltip (if we haven't already been dismissed)
                    if (this._eventNameTask != null && t.Result != null)
                    {
                        commandHandler.EventHookupSessionManager.EventHookupFoundInSession(this, t.Result);
                    }
                },
                cancellationToken,
                TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default);
 
            continuedTask.CompletesAsyncOperation(asyncToken);
        }
 
        public (Task<string?> eventNameTask, CancellationTokenSource cancellationTokenSource) DetachEventNameTask()
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            Contract.ThrowIfNull(_eventNameTask);
            Contract.ThrowIfNull(_cancellationTokenSource);
 
            var eventNameTask = _eventNameTask;
            var cancellationTokenSource = _cancellationTokenSource;
 
            _eventNameTask = null;
            _cancellationTokenSource = null;
 
            return (eventNameTask, cancellationTokenSource);
        }
 
        private async Task<string?> DetermineIfEventHookupAndGetHandlerNameAsync(Document document, int position, CancellationToken cancellationToken)
        {
            _threadingContext.ThrowIfNotOnBackgroundThread();
 
            // For test purposes only!
            if (TESTSessionHookupMutex != null)
            {
                TESTSessionHookupMutex.WaitOne();
                TESTSessionHookupMutex.ReleaseMutex();
            }
 
            using (Logger.LogBlock(FunctionId.EventHookup_Determine_If_Event_Hookup, cancellationToken))
            {
                var plusEqualsToken = await GetPlusEqualsTokenInsideAddAssignExpressionAsync(document, position, cancellationToken).ConfigureAwait(false);
                if (plusEqualsToken == null)
                {
                    return null;
                }
 
                var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
                var eventSymbol = GetEventSymbol(semanticModel, plusEqualsToken.Value, cancellationToken);
                if (eventSymbol == null)
                {
                    return null;
                }
 
                var namingRule = await document.GetApplicableNamingRuleAsync(
                    new SymbolKindOrTypeKind(MethodKind.Ordinary),
                    new DeclarationModifiers(isStatic: plusEqualsToken.Value.GetRequiredParent().IsInStaticContext()),
                    Accessibility.Private,
                    cancellationToken).ConfigureAwait(false);
 
                return GetEventHandlerName(
                    eventSymbol, plusEqualsToken.Value, semanticModel,
                    document.GetRequiredLanguageService<ISyntaxFactsService>(), namingRule);
            }
        }
 
        private async Task<SyntaxToken?> GetPlusEqualsTokenInsideAddAssignExpressionAsync(Document document, int position, CancellationToken cancellationToken)
        {
            _threadingContext.ThrowIfNotOnBackgroundThread();
            var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
            var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
 
            if (token.Kind() != SyntaxKind.PlusEqualsToken)
            {
                return null;
            }
 
            if (!token.Parent.IsKind(SyntaxKind.AddAssignmentExpression))
            {
                return null;
            }
 
            return token;
        }
 
        private IEventSymbol? GetEventSymbol(SemanticModel semanticModel, SyntaxToken plusEqualsToken, CancellationToken cancellationToken)
        {
            _threadingContext.ThrowIfNotOnBackgroundThread();
            if (plusEqualsToken.Parent is not AssignmentExpressionSyntax parentToken)
            {
                return null;
            }
 
            return semanticModel.GetSymbolInfo(parentToken.Left, cancellationToken).Symbol as IEventSymbol;
        }
 
        private string? GetEventHandlerName(
            IEventSymbol eventSymbol, SyntaxToken plusEqualsToken, SemanticModel semanticModel,
            ISyntaxFactsService syntaxFactsService, NamingRule namingRule)
        {
            _threadingContext.ThrowIfNotOnBackgroundThread();
            var objectPart = GetNameObjectPart(eventSymbol, plusEqualsToken, semanticModel, syntaxFactsService);
            if (objectPart is null)
                return null;
 
            var basename = namingRule.NamingStyle.CreateName([string.Format("{0}_{1}", objectPart, eventSymbol.Name)]);
 
            var reservedNames = semanticModel.LookupSymbols(plusEqualsToken.SpanStart).Select(m => m.Name);
 
            return NameGenerator.EnsureUniqueness(basename, reservedNames);
        }
 
        /// <summary>
        /// Take another look at the LHS of the += node -- we need to figure out a default name
        /// for the event handler, and that's usually based on the object (which is usually a
        /// field of 'this', but not always) to which the event belongs. So, if the event is 
        /// something like 'button1.Click' or 'this.listBox1.Select', we want the names 
        /// 'button1' and 'listBox1' respectively. If the field belongs to 'this', then we use
        /// the name of this class, as we do if we can't make any sense out of the parse tree.
        /// </summary>
        private string? GetNameObjectPart(IEventSymbol eventSymbol, SyntaxToken plusEqualsToken, SemanticModel semanticModel, ISyntaxFactsService syntaxFactsService)
        {
            _threadingContext.ThrowIfNotOnBackgroundThread();
            if (plusEqualsToken.Parent is not AssignmentExpressionSyntax assignmentExpression)
                return null;
 
            if (assignmentExpression.Left is MemberAccessExpressionSyntax memberAccessExpression)
            {
                // This is expected -- it means the last thing is(probably) the event name. We 
                // already have that in eventSymbol. What we need is the LHS of that dot.
 
                var lhs = memberAccessExpression.Expression.GetRightmostName();
 
                if (lhs is GenericNameSyntax lhsGenericNameSyntax)
                {
                    // For generic we must exclude type variables
                    return lhsGenericNameSyntax.Identifier.Text;
                }
 
                if (lhs != null)
                {
                    return lhs.ToString();
                }
            }
 
            // If we didn't find an object name above, then the object name is the name of this class.
            // Note: For generic, it's ok(it's even a good idea) to exclude type variables,
            // because the name is only used as a prefix for the method name.
 
            var typeDeclaration = syntaxFactsService.GetContainingTypeDeclaration(
                semanticModel.SyntaxTree.GetRoot(),
                plusEqualsToken.SpanStart) as BaseTypeDeclarationSyntax;
 
            return typeDeclaration != null
                ? typeDeclaration.Identifier.Text
                : eventSymbol.ContainingType.Name;
        }
    }
}