// 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.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup { internal sealed partial class EventHookupSessionManager { /// /// 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. /// internal class EventHookupSession : ForegroundThreadAffinitizedObject { public readonly Task GetEventNameTask; private readonly CancellationTokenSource _cancellationTokenSource; private readonly ITrackingPoint _trackingPoint; 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; public ITrackingPoint TrackingPoint { get { AssertIsForeground(); return _trackingPoint; } } public ITrackingSpan TrackingSpan { get { AssertIsForeground(); return _trackingSpan; } } public ITextView TextView { get { AssertIsForeground(); return _textView; } } public ITextBuffer SubjectBuffer { get { AssertIsForeground(); return _subjectBuffer; } } public void Cancel() { AssertIsForeground(); _cancellationTokenSource.Cancel(); } public EventHookupSession( EventHookupSessionManager eventHookupSessionManager, EventHookupCommandHandler commandHandler, ITextView textView, ITextBuffer subjectBuffer, IAsynchronousOperationListener asyncListener, Mutex testSessionHookupMutex) : base(eventHookupSessionManager.ThreadingContext) { AssertIsForeground(); _cancellationTokenSource = new CancellationTokenSource(); _textView = textView; _subjectBuffer = subjectBuffer; this.TESTSessionHookupMutex = testSessionHookupMutex; var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges(); if (document != null && document.Project.Solution.Workspace.CanApplyChange(ApplyChangesKind.ChangeDocument)) { var position = textView.GetCaretPoint(subjectBuffer).Value.Position; _trackingPoint = textView.TextSnapshot.CreateTrackingPoint(position, PointTrackingMode.Negative); _trackingSpan = textView.TextSnapshot.CreateTrackingSpan(new Span(position, 1), SpanTrackingMode.EdgeInclusive); var asyncToken = asyncListener.BeginAsyncOperation(GetType().Name + ".Start"); this.GetEventNameTask = Task.Factory.SafeStartNewFromAsync( () => DetermineIfEventHookupAndGetHandlerNameAsync(document, position, _cancellationTokenSource.Token), _cancellationTokenSource.Token, TaskScheduler.Default); var continuedTask = this.GetEventNameTask.SafeContinueWithFromAsync( async t => { await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationTokenSource.Token); _cancellationTokenSource.Token.ThrowIfCancellationRequested(); if (t.Result != null) { commandHandler.EventHookupSessionManager.EventHookupFoundInSession(this); } }, _cancellationTokenSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); continuedTask.CompletesAsyncOperation(asyncToken); } else { _trackingPoint = textView.TextSnapshot.CreateTrackingPoint(0, PointTrackingMode.Negative); _trackingSpan = textView.TextSnapshot.CreateTrackingSpan(new Span(), SpanTrackingMode.EdgeInclusive); this.GetEventNameTask = SpecializedTasks.Default(); eventHookupSessionManager.CancelAndDismissExistingSessions(); } } private async Task DetermineIfEventHookupAndGetHandlerNameAsync(Document document, int position, CancellationToken cancellationToken) { AssertIsBackground(); // 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.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); var eventSymbol = GetEventSymbol(semanticModel, plusEqualsToken.Value, cancellationToken); if (eventSymbol == null) { return null; } return GetEventHandlerName(eventSymbol, plusEqualsToken.Value, semanticModel, document.GetLanguageService()); } } private async Task GetPlusEqualsTokenInsideAddAssignExpressionAsync(Document document, int position, CancellationToken cancellationToken) { AssertIsBackground(); var syntaxTree = await document.GetSyntaxTreeAsync(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) { AssertIsBackground(); if (!(plusEqualsToken.Parent is AssignmentExpressionSyntax parentToken)) { return null; } var symbol = semanticModel.GetSymbolInfo(parentToken.Left, cancellationToken).Symbol; if (symbol == null) { return null; } return symbol as IEventSymbol; } private string GetEventHandlerName(IEventSymbol eventSymbol, SyntaxToken plusEqualsToken, SemanticModel semanticModel, ISyntaxFactsService syntaxFactsService) { AssertIsBackground(); var basename = string.Format("{0}_{1}", GetNameObjectPart(eventSymbol, plusEqualsToken, semanticModel, syntaxFactsService), eventSymbol.Name); basename = basename.ToPascalCase(trimLeadingTypePrefix: false); var reservedNames = semanticModel.LookupSymbols(plusEqualsToken.SpanStart).Select(m => m.Name); return NameGenerator.EnsureUniqueness(basename, reservedNames); } /// /// 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. /// private string GetNameObjectPart(IEventSymbol eventSymbol, SyntaxToken plusEqualsToken, SemanticModel semanticModel, ISyntaxFactsService syntaxFactsService) { AssertIsBackground(); var parentToken = plusEqualsToken.Parent as AssignmentExpressionSyntax; if (parentToken.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; if (lhs is MemberAccessExpressionSyntax lhsMemberAccessExpression) { // Okay, cool. The name we're after is in the RHS of this dot. return lhsMemberAccessExpression.Name.ToString(); } if (lhs is NameSyntax lhsNameSyntax) { // Even easier -- the LHS of the dot is the name itself return lhsNameSyntax.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. return syntaxFactsService.GetContainingTypeDeclaration( semanticModel.SyntaxTree.GetRoot(), plusEqualsToken.SpanStart) as BaseTypeDeclarationSyntax; return typeDeclaration != null ? typeDeclaration.Identifier.Text : eventSymbol.ContainingType.Name; } } } }