EventHookupSessionManager_EventHookupSession.cs 11.8 KB
Newer Older
S
Sam Harwell 已提交
1
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92

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
    {
        /// <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 : ForegroundThreadAffinitizedObject
        {
            public readonly Task<string> 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)
93
                : base(eventHookupSessionManager.ThreadingContext)
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
            {
                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);

115 116
                    var continuedTask = this.GetEventNameTask.SafeContinueWithFromAsync(
                        async t =>
117
                        {
118
                            await ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, _cancellationTokenSource.Token);
119 120
                            _cancellationTokenSource.Token.ThrowIfCancellationRequested();

121 122 123 124 125 126
                            if (t.Result != null)
                            {
                                commandHandler.EventHookupSessionManager.EventHookupFoundInSession(this);
                            }
                        },
                        _cancellationTokenSource.Token,
127 128
                        TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously,
                        TaskScheduler.Default);
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159

                    continuedTask.CompletesAsyncOperation(asyncToken);
                }
                else
                {
                    _trackingPoint = textView.TextSnapshot.CreateTrackingPoint(0, PointTrackingMode.Negative);
                    _trackingSpan = textView.TextSnapshot.CreateTrackingSpan(new Span(), SpanTrackingMode.EdgeInclusive);
                    this.GetEventNameTask = SpecializedTasks.Default<string>();
                    eventHookupSessionManager.CancelAndDismissExistingSessions();
                }
            }

            private async Task<string> 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;
                    }

D
Dustin Campbell 已提交
160
                    var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
161 162 163 164 165 166 167 168 169 170 171 172 173 174

                    var eventSymbol = GetEventSymbol(semanticModel, plusEqualsToken.Value, cancellationToken);
                    if (eventSymbol == null)
                    {
                        return null;
                    }

                    return GetEventHandlerName(eventSymbol, plusEqualsToken.Value, semanticModel, document.GetLanguageService<ISyntaxFactsService>());
                }
            }

            private async Task<SyntaxToken?> GetPlusEqualsTokenInsideAddAssignExpressionAsync(Document document, int position, CancellationToken cancellationToken)
            {
                AssertIsBackground();
D
Dustin Campbell 已提交
175
                var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
                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();
                var parentToken = plusEqualsToken.Parent as AssignmentExpressionSyntax;
                if (parentToken == null)
                {
                    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);
            }

            /// <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)
            {
                AssertIsBackground();
                var parentToken = plusEqualsToken.Parent as AssignmentExpressionSyntax;

C
CyrusNajmabadi 已提交
233
                if (parentToken.Left is MemberAccessExpressionSyntax memberAccessExpression)
234 235 236 237 238 239
                {
                    // 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;

C
CyrusNajmabadi 已提交
240
                    if (lhs is MemberAccessExpressionSyntax lhsMemberAccessExpression)
241 242 243 244 245
                    {
                        // Okay, cool.  The name we're after is in the RHS of this dot.
                        return lhsMemberAccessExpression.Name.ToString();
                    }

C
CyrusNajmabadi 已提交
246
                    if (lhs is NameSyntax lhsNameSyntax)
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
                    {
                        // 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.

                var typeDeclaration = syntaxFactsService.GetContainingTypeDeclaration(
                    semanticModel.SyntaxTree.GetRoot(),
                    plusEqualsToken.SpanStart) as BaseTypeDeclarationSyntax;

                return typeDeclaration != null
                    ? typeDeclaration.Identifier.Text
                    : eventSymbol.ContainingType.Name;
            }
        }
    }
}