|
// 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.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.CodeAnalysis.Editor.CSharp.EventHookup;
[Export]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed partial class EventHookupSessionManager(
IThreadingContext threadingContext,
IToolTipService toolTipService)
{
public readonly IThreadingContext ThreadingContext = threadingContext;
private readonly IToolTipService _toolTipService = toolTipService;
private IToolTipPresenter _toolTipPresenter;
private EventHookupSession _currentSessionDoNotAccessDirectly;
internal EventHookupSession CurrentSession
{
get
{
ThreadingContext.ThrowIfNotOnUIThread();
return _currentSessionDoNotAccessDirectly;
}
set
{
ThreadingContext.ThrowIfNotOnUIThread();
_currentSessionDoNotAccessDirectly?.CancelBackgroundTasks();
_currentSessionDoNotAccessDirectly = value;
}
}
// For test purposes only!
internal ClassifiedTextElement[] TEST_MostRecentToolTipContent { get; set; }
public async Task EventHookupFoundInSessionAsync(
EventHookupSession analyzedSession, string eventName, CancellationToken cancellationToken)
{
await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);
if (cancellationToken.IsCancellationRequested)
return;
var caretPoint = analyzedSession.TextView.GetCaretPoint(analyzedSession.SubjectBuffer);
// only generate tooltip if it is not already shown (_toolTipPresenter == null)
// Ensure the analyzed session matches the current session and that the caret is still
// in the session's tracking span.
if (_toolTipPresenter == null &&
CurrentSession == analyzedSession &&
caretPoint.HasValue &&
IsCaretWithinSpanOrAtEnd(analyzedSession.TrackingSpan, analyzedSession.SubjectBuffer.CurrentSnapshot, caretPoint.Value))
{
// Create a tooltip presenter that stays alive, even when the user types, without tracking the mouse.
_toolTipPresenter = _toolTipService.CreatePresenter(analyzedSession.TextView,
new ToolTipParameters(trackMouse: false, ignoreBufferChange: true));
// tooltips text is: Program_MyEvents; (Press TAB to insert)
// GetEventNameTask() gets back the event name, only needs to add a semicolon after it.
var textRuns = new[]
{
new ClassifiedTextRun(ClassificationTypeNames.MethodName, eventName, ClassifiedTextRunStyle.UseClassificationFont),
new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ";", ClassifiedTextRunStyle.UseClassificationFont),
new ClassifiedTextRun(ClassificationTypeNames.Text, CSharpEditorResources.Press_TAB_to_insert),
};
var content = new[] { new ClassifiedTextElement(textRuns) };
_toolTipPresenter.StartOrUpdate(analyzedSession.TrackingSpan, content);
// For test purposes only!
TEST_MostRecentToolTipContent = content;
// Watch all text buffer changes & caret moves while this event hookup session is active
analyzedSession.TextView.TextSnapshot.TextBuffer.Changed += TextBuffer_Changed;
CurrentSession.Dismissed += () => { analyzedSession.TextView.TextSnapshot.TextBuffer.Changed -= TextBuffer_Changed; };
analyzedSession.TextView.Caret.PositionChanged += Caret_PositionChanged;
CurrentSession.Dismissed += () => { analyzedSession.TextView.Caret.PositionChanged -= Caret_PositionChanged; };
}
}
private static bool IsCaretWithinSpanOrAtEnd(ITrackingSpan trackingSpan, ITextSnapshot textSnapshot, SnapshotPoint caretPoint)
{
var snapshotSpan = trackingSpan.GetSpan(textSnapshot);
// If the caret is within the span, then we want to show the tooltip
if (snapshotSpan.Contains(caretPoint))
{
return true;
}
// Otherwise if the span is empty, and at the end of the file, and the caret
// is also at the end of the file, then show the tooltip.
if (snapshotSpan.IsEmpty &&
snapshotSpan.Start.Position == caretPoint.Position &&
caretPoint.Position == textSnapshot.Length)
{
return true;
}
return false;
}
internal void BeginSession(
EventHookupCommandHandler eventHookupCommandHandler,
ITextView textView,
ITextBuffer subjectBuffer,
int position,
Document document,
IAsynchronousOperationListener asyncListener,
Mutex testSessionHookupMutex)
{
CurrentSession = new EventHookupSession(
this, eventHookupCommandHandler, textView, subjectBuffer, position, document, asyncListener, testSessionHookupMutex);
}
public void DismissExistingSessions()
{
ThreadingContext.ThrowIfNotOnUIThread();
_toolTipPresenter?.Dismiss();
_toolTipPresenter = null;
CurrentSession = null;
// For test purposes only!
TEST_MostRecentToolTipContent = null;
}
/// <summary>
/// If any text is deleted or any non-space text is entered, cancel the session.
/// </summary>
private void TextBuffer_Changed(object sender, TextContentChangedEventArgs e)
{
ThreadingContext.ThrowIfNotOnUIThread();
foreach (var change in e.Changes)
{
if (change.OldText.Length > 0 || change.NewText.Any(c => c != ' '))
{
DismissExistingSessions();
return;
}
}
}
/// <summary>
/// If the caret moves outside the session's tracking span, cancel the session.
/// </summary>
private void Caret_PositionChanged(object sender, EventArgs e)
{
ThreadingContext.ThrowIfNotOnUIThread();
if (CurrentSession == null)
{
DismissExistingSessions();
return;
}
var caretPoint = CurrentSession.TextView.GetCaretPoint(CurrentSession.SubjectBuffer);
if (!caretPoint.HasValue)
{
DismissExistingSessions();
}
var snapshotSpan = CurrentSession.TrackingSpan.GetSpan(CurrentSession.SubjectBuffer.CurrentSnapshot);
if (snapshotSpan.Snapshot != caretPoint.Value.Snapshot || !snapshotSpan.Contains(caretPoint.Value))
{
DismissExistingSessions();
}
}
}
|