|
// 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.Immutable;
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.ErrorReporting;
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.CodeAnalysis.Text;
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
{
public readonly Task<string> GetEventNameTask;
private readonly IThreadingContext _threadingContext;
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly ITrackingPoint _trackingPoint;
private readonly ITrackingSpan _trackingSpan;
private readonly ITextView _textView;
private readonly ITextBuffer _subjectBuffer;
private readonly IGlobalOptionService _globalOptions;
public event Action Dismissed = () => { };
// For testing purposes only! Should always be null except in tests.
internal Mutex TESTSessionHookupMutex = null;
public ITrackingPoint TrackingPoint
{
get
{
_threadingContext.ThrowIfNotOnUIThread();
return _trackingPoint;
}
}
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 Cancel()
{
_threadingContext.ThrowIfNotOnUIThread();
_cancellationTokenSource.Cancel();
}
public EventHookupSession(
EventHookupSessionManager eventHookupSessionManager,
EventHookupCommandHandler commandHandler,
ITextView textView,
ITextBuffer subjectBuffer,
IAsynchronousOperationListener asyncListener,
IGlobalOptionService globalOptions,
Mutex testSessionHookupMutex)
{
_threadingContext = eventHookupSessionManager.ThreadingContext;
var cancellationToken = _cancellationTokenSource.Token;
_textView = textView;
_subjectBuffer = subjectBuffer;
_globalOptions = globalOptions;
this.TESTSessionHookupMutex = testSessionHookupMutex;
var document = textView.TextSnapshot.GetOpenDocumentInCurrentContextWithChanges();
var workspace = textView.TextSnapshot.TextBuffer.GetWorkspace();
if (document != null && workspace != null && workspace.CanApplyChange(ApplyChangesKind.ChangeDocument))
{
var position = textView.GetCaretPoint(subjectBuffer).Value.Position;
_trackingPoint = textView.TextSnapshot.CreateTrackingPoint(position, PointTrackingMode.Negative);
// If the caret is at the end of the document we just create an empty span
var length = textView.TextSnapshot.Length > position + 1 ? 1 : 0;
_trackingSpan = textView.TextSnapshot.CreateTrackingSpan(new Span(position, length), SpanTrackingMode.EdgeInclusive);
var asyncToken = asyncListener.BeginAsyncOperation(GetType().Name + ".Start");
this.GetEventNameTask = Task.Factory.SafeStartNewFromAsync(
() => DetermineIfEventHookupAndGetHandlerNameAsync(document, position, cancellationToken),
cancellationToken,
TaskScheduler.Default);
var continuedTask = this.GetEventNameTask.SafeContinueWithFromAsync(
async t =>
{
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
if (t.Result != null)
{
commandHandler.EventHookupSessionManager.EventHookupFoundInSession(this);
}
},
cancellationToken,
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.Null<string>();
eventHookupSessionManager.CancelAndDismissExistingSessions();
}
}
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.GetSemanticModelAsync(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.Parent.IsInStaticContext()),
Accessibility.Private,
_globalOptions.CreateProvider(),
cancellationToken).ConfigureAwait(false);
return GetEventHandlerName(
eventSymbol, plusEqualsToken.Value, semanticModel,
document.GetLanguageService<ISyntaxFactsService>(), namingRule);
}
}
private async Task<SyntaxToken?> GetPlusEqualsTokenInsideAddAssignExpressionAsync(Document document, int position, CancellationToken cancellationToken)
{
_threadingContext.ThrowIfNotOnBackgroundThread();
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)
{
_threadingContext.ThrowIfNotOnBackgroundThread();
if (plusEqualsToken.Parent is not 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, NamingRule namingRule)
{
_threadingContext.ThrowIfNotOnBackgroundThread();
var objectPart = GetNameObjectPart(eventSymbol, plusEqualsToken, semanticModel, syntaxFactsService);
var basename = namingRule.NamingStyle.CreateName(ImmutableArray.Create(
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();
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.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;
}
}
}
}
|