Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/microsoft/vs-editor-api.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKirill Osenkov <github@osenkov.com>2018-08-16 02:14:18 +0300
committerKirill Osenkov <github@osenkov.com>2018-08-16 02:14:18 +0300
commit193f2ac081338d0034a2b2874ba59c8343db56a7 (patch)
tree20b7b010bfbb71f9d15f183fb7329dcf4881c45f
parent21b22d2687687c4013d8e7873dd515518b06b386 (diff)
Add Async Quick Info.
-rw-r--r--src/Language/Impl/Language/QuickInfo/AsyncQuickInfoBroker.cs282
-rw-r--r--src/Language/Impl/Language/QuickInfo/AsyncQuickInfoPresentationSession.cs157
-rw-r--r--src/Language/Impl/Language/QuickInfo/AsyncQuickInfoSession.Legacy.cs51
-rw-r--r--src/Language/Impl/Language/QuickInfo/AsyncQuickInfoSession.cs511
-rw-r--r--src/Language/Impl/Language/QuickInfo/QuickInfoController.cs165
-rw-r--r--src/Language/Impl/Language/QuickInfo/QuickInfoTextViewCreationListener.cs42
-rw-r--r--src/Language/Impl/Language/QuickInfo/SquiggleQuickInfoSource.cs143
-rw-r--r--src/Language/Impl/Language/QuickInfo/SquiggleQuickInfoSourceProvider.cs26
-rw-r--r--src/Microsoft.VisualStudio.Text.Implementation.csproj3
-rw-r--r--src/Text/Impl/XPlat/MultiCaretImpl/Strings.Designer.cs2
10 files changed, 1381 insertions, 1 deletions
diff --git a/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoBroker.cs b/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoBroker.cs
new file mode 100644
index 0000000..53511a3
--- /dev/null
+++ b/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoBroker.cs
@@ -0,0 +1,282 @@
+namespace Microsoft.VisualStudio.Language.Intellisense.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.Composition;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Internal.VisualStudio.Language.Intellisense;
+ //using Microsoft.VisualStudio.Language.Utilities;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Adornments;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Threading;
+ using Microsoft.VisualStudio.Utilities;
+ using IOrderableContentTypeMetadata = Internal.VisualStudio.Language.Intellisense.LegacyQuickInfoMetadata;
+
+ [Export(typeof(IAsyncQuickInfoBroker))]
+ internal sealed class AsyncQuickInfoBroker : IAsyncQuickInfoBroker,
+
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ // This interface exists only to expose additional functionality required by the shims.
+#pragma warning disable 618
+ ILegacyQuickInfoBrokerSupport
+ {
+ private readonly IEnumerable<Lazy<IAsyncQuickInfoSourceProvider, IOrderableContentTypeMetadata>> unorderedSourceProviders;
+ private readonly IGuardedOperations guardedOperations;
+ private readonly IToolTipService toolTipService;
+ private readonly JoinableTaskContext joinableTaskContext;
+ private IEnumerable<Lazy<IAsyncQuickInfoSourceProvider, IOrderableContentTypeMetadata>> orderedSourceProviders;
+
+ [ImportingConstructor]
+ public AsyncQuickInfoBroker(
+ [ImportMany]IEnumerable<Lazy<IAsyncQuickInfoSourceProvider, IOrderableContentTypeMetadata>> unorderedSourceProviders,
+ [Import(AllowDefault = true)]ILegacyQuickInfoSourcesSupport legacyQuickInfoSourcesSupport,
+ IGuardedOperations guardedOperations,
+ IToolTipService toolTipService,
+ JoinableTaskContext joinableTaskContext)
+ {
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ // Combines new + legacy providers into a single series for relative ordering.
+ var combinedProviders = unorderedSourceProviders ?? throw new ArgumentNullException(nameof(unorderedSourceProviders));
+ if (legacyQuickInfoSourcesSupport != null)
+ {
+ combinedProviders = combinedProviders.Concat(legacyQuickInfoSourcesSupport.LegacySources);
+ }
+
+ this.unorderedSourceProviders = combinedProviders;
+#pragma warning restore 618
+ this.guardedOperations = guardedOperations ?? throw new ArgumentNullException(nameof(guardedOperations));
+ this.joinableTaskContext = joinableTaskContext ?? throw new ArgumentNullException(nameof(joinableTaskContext));
+ this.toolTipService = toolTipService;
+ }
+
+ #region IAsyncQuickInfoBroker
+
+ public IAsyncQuickInfoSession GetSession(ITextView textView)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException(nameof(textView));
+ }
+
+ if (textView.Properties.TryGetProperty(typeof(AsyncQuickInfoPresentationSession), out AsyncQuickInfoPresentationSession property))
+ {
+ return property;
+ }
+
+ return null;
+ }
+
+ public bool IsQuickInfoActive(ITextView textView) => GetSession(textView) != null;
+
+ public Task<IAsyncQuickInfoSession> TriggerQuickInfoAsync(
+ ITextView textView,
+ ITrackingPoint triggerPoint,
+ QuickInfoSessionOptions options,
+ CancellationToken cancellationToken)
+ {
+ return this.TriggerQuickInfoAsync(
+ textView,
+ triggerPoint,
+ options,
+ null,
+ cancellationToken);
+ }
+
+ public async Task<QuickInfoItemsCollection> GetQuickInfoItemsAsync(
+ ITextView textView,
+ ITrackingPoint triggerPoint,
+ CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ triggerPoint = await this.ResolveAndMapUpTriggerPointAsync(textView, triggerPoint, cancellationToken).ConfigureAwait(false);
+ if (triggerPoint != null)
+ {
+ var session = new AsyncQuickInfoSession(
+ this.OrderedSourceProviders,
+ this.joinableTaskContext,
+ textView,
+ triggerPoint,
+ QuickInfoSessionOptions.None);
+
+ var startedSession = await StartQuickInfoSessionAsync(session, cancellationToken).ConfigureAwait(false);
+ if (startedSession != null)
+ {
+ var results = new QuickInfoItemsCollection(startedSession.Content, startedSession.ApplicableToSpan);
+ await startedSession.DismissAsync().ConfigureAwait(false);
+
+ return results;
+ }
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ // This overload exists only to expose additional functionality required
+ // by the shims.
+ #region ILegacyQuickInfoBrokerSupport
+
+ public async Task<IAsyncQuickInfoSession> TriggerQuickInfoAsync(
+ ITextView textView,
+ ITrackingPoint triggerPoint,
+ QuickInfoSessionOptions options,
+ PropertyCollection propertyCollection,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Dismiss any currently open session.
+ var currentSession = this.GetSession(textView);
+ if (currentSession != null)
+ {
+ await currentSession.DismissAsync().ConfigureAwait(true);
+ }
+
+ triggerPoint = await this.ResolveAndMapUpTriggerPointAsync(textView, triggerPoint, cancellationToken).ConfigureAwait(false);
+ if (triggerPoint == null)
+ {
+ return null;
+ }
+
+ var newSession = new AsyncQuickInfoPresentationSession(
+ this.OrderedSourceProviders,
+ this.guardedOperations,
+ this.joinableTaskContext,
+ this.toolTipService,
+ textView,
+ triggerPoint,
+ options,
+ propertyCollection);
+
+ // StartAsync() is responsible for dispatching a StateChange
+ // event if canceled so no need to clean these up on cancellation.
+ newSession.StateChanged += this.OnStateChanged;
+ textView.Properties.AddProperty(typeof(AsyncQuickInfoPresentationSession), newSession);
+
+ return await StartQuickInfoSessionAsync(newSession, cancellationToken).ConfigureAwait(false);
+ }
+
+ #endregion
+
+ #region Private Impl
+
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ // This interface exists only to expose additional functionality required by the shims.
+#pragma warning disable 618
+ private IEnumerable<Lazy<IAsyncQuickInfoSourceProvider, IOrderableContentTypeMetadata>> OrderedSourceProviders
+ => this.orderedSourceProviders ?? (this.orderedSourceProviders = Orderer.Order(this.unorderedSourceProviders));
+#pragma warning restore 618
+
+ /// <summary>
+ /// Gets a trigger point for this session on the view's buffer.
+ /// </summary>
+ /// <remarks>
+ /// Get's the caret's tracking point, if <paramref name="trackingPoint"/> is null,
+ /// and maps the chosen tracking point up to the view's buffer.
+ /// </remarks>
+ private async Task<ITrackingPoint> ResolveAndMapUpTriggerPointAsync(
+ ITextView textView,
+ ITrackingPoint trackingPoint,
+ CancellationToken cancellationToken)
+ {
+ // Caret element requires UI thread.
+ await this.joinableTaskContext.Factory.SwitchToMainThreadAsync();
+
+ // We switched threads and there is some latency, so ensure that we're still not canceled.
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (trackingPoint == null)
+ {
+ // Get the trigger point from the caret if none is provided.
+ SnapshotPoint caretPoint = textView.Caret.Position.BufferPosition;
+ trackingPoint = caretPoint.Snapshot.CreateTrackingPoint(
+ caretPoint.Position,
+ PointTrackingMode.Negative);
+ }
+ else
+ {
+ // Map the provided trigger point to the view's buffer.
+ trackingPoint = PointToViewBuffer(textView, trackingPoint);
+ if (trackingPoint == null)
+ {
+ return null;
+ }
+ }
+
+ return trackingPoint;
+ }
+
+ private static async Task<IAsyncQuickInfoSession> StartQuickInfoSessionAsync(AsyncQuickInfoSession session, CancellationToken cancellationToken)
+ {
+ try
+ {
+ await session.UpdateAsync(allowUpdate: false, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
+ {
+ // Don't throw OperationCanceledException unless the caller canceled us.
+ // This can happen if computation was canceled by a quick info source
+ // dismissing the session during computation, which we want to consider
+ // more of a 'N/A' than an error.
+ return null;
+ }
+
+ return session.State == QuickInfoSessionState.Dismissed ? null : session;
+ }
+
+ // Listens for the session being dismissed so that we can remove it from the view's property bag.
+ private void OnStateChanged(object sender, QuickInfoSessionStateChangedEventArgs e)
+ {
+ //IntellisenseUtilities.ThrowIfNotOnMainThread(this.joinableTaskContext);
+
+ if (e.NewState == QuickInfoSessionState.Dismissed)
+ {
+ if (sender is AsyncQuickInfoPresentationSession session)
+ {
+ session.TextView.Properties.RemoveProperty(typeof(AsyncQuickInfoPresentationSession));
+ session.StateChanged -= this.OnStateChanged;
+ return;
+ }
+
+ Debug.Fail("Unexpected sender type");
+ }
+ }
+
+ private ITrackingPoint PointToViewBuffer(ITextView textView, ITrackingPoint trackingPoint)
+ {
+ // Requires UI thread for BufferGraph.
+ //IntellisenseUtilities.ThrowIfNotOnMainThread(this.joinableTaskContext);
+
+ if ((trackingPoint == null) || (textView.TextBuffer == trackingPoint.TextBuffer))
+ {
+ return trackingPoint;
+ }
+
+ var targetSnapshot = textView.TextSnapshot;
+ var point = trackingPoint.GetPoint(trackingPoint.TextBuffer.CurrentSnapshot);
+ var viewBufferPoint = textView.BufferGraph.MapUpToSnapshot(
+ point,
+ trackingPoint.TrackingMode,
+ PositionAffinity.Predecessor,
+ targetSnapshot);
+
+ if (viewBufferPoint == null)
+ {
+ return null;
+ }
+
+ return targetSnapshot.CreateTrackingPoint(
+ viewBufferPoint.Value.Position,
+ trackingPoint.TrackingMode);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoPresentationSession.cs b/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoPresentationSession.cs
new file mode 100644
index 0000000..77a36f0
--- /dev/null
+++ b/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoPresentationSession.cs
@@ -0,0 +1,157 @@
+namespace Microsoft.VisualStudio.Language.Intellisense.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Internal.VisualStudio.Language.Intellisense;
+ using Microsoft.VisualStudio.Language.Intellisense;
+ //using Microsoft.VisualStudio.Language.Utilities;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Adornments;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Threading;
+ using Microsoft.VisualStudio.Utilities;
+
+ internal sealed class AsyncQuickInfoPresentationSession : AsyncQuickInfoSession,
+ IAsyncQuickInfoSession,
+#pragma warning disable 618
+ ILegacyQuickInfoRecalculateSupport
+#pragma warning restore 618
+ {
+ private readonly IGuardedOperations guardedOperations;
+ private readonly IToolTipService toolTipService;
+
+ #region Reable/Writeable via UI Thread Only
+
+ internal IToolTipPresenter uiThreadOnlyPresenter;
+
+ #endregion
+
+ public AsyncQuickInfoPresentationSession(
+#pragma warning disable CS0618 // Type or member is obsolete
+ IEnumerable<Lazy<IAsyncQuickInfoSourceProvider, LegacyQuickInfoMetadata>> orderedSourceProviders,
+#pragma warning restore CS0618 // Type or member is obsolete
+ IGuardedOperations guardedOperations,
+ JoinableTaskContext joinableTaskContext,
+ IToolTipService toolTipService,
+ ITextView textView,
+ ITrackingPoint triggerPoint,
+ QuickInfoSessionOptions options,
+ PropertyCollection propertyCollection) : base(
+ orderedSourceProviders,
+ joinableTaskContext,
+ textView,
+ triggerPoint,
+ options,
+ propertyCollection)
+ {
+ this.guardedOperations = guardedOperations ?? throw new ArgumentNullException(nameof(guardedOperations));
+ this.toolTipService = toolTipService ?? throw new ArgumentNullException(nameof(toolTipService));
+ }
+
+ public override async Task DismissAsync()
+ {
+ // Ensure that we have the UI thread. To avoid races, the rest of this method must be sync.
+ await this.JoinableTaskContext.Factory.SwitchToMainThreadAsync();
+
+ var currentState = this.State;
+ if (currentState != QuickInfoSessionState.Dismissed)
+ {
+ // Dismiss presenter.
+ var presenter = this.uiThreadOnlyPresenter;
+ if (presenter != null)
+ {
+ presenter.Dismissed -= this.OnDismissed;
+ this.uiThreadOnlyPresenter.Dismiss();
+
+ this.uiThreadOnlyPresenter = null;
+ }
+ }
+
+ await base.DismissAsync().ConfigureAwait(false);
+ }
+
+ internal override async Task UpdateAsync(bool allowUpdate, CancellationToken cancellationToken)
+ {
+ // Ensure we have the UI thread.
+ await this.JoinableTaskContext.Factory.SwitchToMainThreadAsync();
+
+ try
+ {
+ await base.UpdateAsync(allowUpdate, cancellationToken).ConfigureAwait(true);
+ await this.UpdatePresenterAsync().ConfigureAwait(false);
+ }
+ catch (AggregateException ex)
+ {
+ // Catch all exceptions and post them here on the UI thread.
+ Debug.Assert(this.JoinableTaskContext.IsOnMainThread);
+ this.guardedOperations.HandleException(this, ex);
+ }
+ }
+
+ private void OnDismissed(object sender, EventArgs e)
+ {
+ //IntellisenseUtilities.ThrowIfNotOnMainThread(this.JoinableTaskContext);
+
+ this.JoinableTaskContext.Factory.RunAsync(async delegate
+ {
+ await this.DismissAsync().ConfigureAwait(false);
+ });
+ }
+
+ private async Task UpdatePresenterAsync()
+ {
+ await this.JoinableTaskContext.Factory.SwitchToMainThreadAsync();
+
+ // Ensure that the session wasn't dismissed.
+ if (this.State == QuickInfoSessionState.Dismissed)
+ {
+ return;
+ }
+
+ // Configure presenter behavior.
+ var parameters = new ToolTipParameters(
+ this.Options.HasFlag(QuickInfoSessionOptions.TrackMouse),
+ keepOpenFunc: this.ContentRequestsKeepOpen);
+
+ // Create presenter if necessary.
+ if (this.uiThreadOnlyPresenter == null)
+ {
+ this.uiThreadOnlyPresenter = this.toolTipService.CreatePresenter(this.TextView, parameters);
+ this.uiThreadOnlyPresenter.Dismissed += this.OnDismissed;
+ }
+
+ // Update presenter content.
+ this.uiThreadOnlyPresenter.StartOrUpdate(this.ApplicableToSpan, this.Content);
+
+ // Ensure that the presenter didn't dismiss the session.
+ if (this.State != QuickInfoSessionState.Dismissed)
+ {
+ // Update state and alert subscribers on the UI thread.
+ this.TransitionTo(QuickInfoSessionState.Visible);
+ }
+ }
+
+ private bool ContentRequestsKeepOpen()
+ {
+ //IntellisenseUtilities.ThrowIfNotOnMainThread(this.JoinableTaskContext);
+
+ if (this.HasInteractiveContent)
+ {
+ foreach (var content in this.Content)
+ {
+ if ((content is IInteractiveQuickInfoContent interactiveContent)
+ && ((interactiveContent.KeepQuickInfoOpen || interactiveContent.IsMouseOverAggregated)))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoSession.Legacy.cs b/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoSession.Legacy.cs
new file mode 100644
index 0000000..f2a8c71
--- /dev/null
+++ b/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoSession.Legacy.cs
@@ -0,0 +1,51 @@
+namespace Microsoft.VisualStudio.Language.Intellisense.Implementation
+{
+ using System.Collections.Generic;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Internal.VisualStudio.Language.Intellisense;
+ using Microsoft.VisualStudio.Text;
+
+#pragma warning disable 618
+ internal partial class AsyncQuickInfoSession : ILegacyQuickInfoRecalculateSupport
+ {
+ #region ILegacyQuickInfoRefreshSupport
+
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ public void Recalculate()
+ {
+ this.JoinableTaskContext.Factory.Run(async delegate
+ {
+ await this.UpdateAsync(allowUpdate: true, cancellationToken: CancellationToken.None).ConfigureAwait(true);
+ });
+ }
+
+ #endregion
+
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ private async Task<bool> TryComputeContentFromLegacySourceAsync(
+ IAsyncQuickInfoSource source,
+ IList<object> items,
+ IList<ITrackingSpan> applicableToSpans)
+ {
+ if (source is ILegacyQuickInfoSource legacySource)
+ {
+#pragma warning restore 618
+
+ // Legacy sources expect to be on the UI thread.
+ await this.JoinableTaskContext.Factory.SwitchToMainThreadAsync();
+
+ legacySource.AugmentQuickInfoSession(this, items, out var applicableToSpan);
+
+ if (applicableToSpan != null)
+ {
+ applicableToSpans.Add(applicableToSpan);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoSession.cs b/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoSession.cs
new file mode 100644
index 0000000..91b2764
--- /dev/null
+++ b/src/Language/Impl/Language/QuickInfo/AsyncQuickInfoSession.cs
@@ -0,0 +1,511 @@
+namespace Microsoft.VisualStudio.Language.Intellisense.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.Language.Intellisense;
+ using Microsoft.VisualStudio.Language.Utilities;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Utilities;
+ using Microsoft.VisualStudio.Threading;
+ using Microsoft.VisualStudio.Utilities;
+ using IOrderableContentTypeMetadata = Internal.VisualStudio.Language.Intellisense.LegacyQuickInfoMetadata;
+
+ internal partial class AsyncQuickInfoSession : IAsyncQuickInfoSession
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ // ILegacyQuickInfoMetadata should be removed and switched out for IOrderableContentTypeMetadata.
+ private readonly IEnumerable<Lazy<IAsyncQuickInfoSourceProvider, IOrderableContentTypeMetadata>> orderedSourceProviders;
+#pragma warning restore CS0618 // Type or member is obsolete
+ protected readonly JoinableTaskContext JoinableTaskContext;
+ private readonly ITrackingPoint triggerPoint;
+
+ // For the purposes of synchronization, state updates are non-atomic and 'Calculating'
+ // state is considered to be transient. The properties of the object can be updated
+ // individually and are immediately visible to all threads. External extenders are
+ // essentially not impacted by this lack of atomicity because they only have a reference
+ // to the session from the broker after it is finished calculating, and the non-atomic
+ // updating of the properties happens after all IAsyncQuickInfoSources have returned.
+ // This class avoids its own races by marshalling all calls into the class and all state
+ // changes through the UI thread and by only allowing one invocation of 'StartAsync()'.
+
+ #region Cross Thread Readable, Modifiable
+
+ // All state in this region can be read or modified from any thread and must
+ // be accessed with VOLATILE.READ() + VOLATILE.WRITE().
+ private ImmutableList<object> content = ImmutableList<object>.Empty;
+ private ITrackingSpan applicableToSpan;
+
+ #endregion
+
+ #region Cross Thread Readable, Modifiable Only Via UI Thread
+
+ // State in this region can be read from any thread at any time (often via properties)
+ // but writes are synchronized via the UI thread. All readers should use VOLATILE.READ()
+ // all writers should use VOLATILE.WRITE().
+ private bool uiThreadWritableHasInteractiveContent;
+ private int uiThreadWritableState = (int)QuickInfoSessionState.Created;
+
+ #endregion
+
+ #region Reable/Writeable via UI Thread Only
+
+ private CancellationTokenSource uiThreadOnlyLinkedCancellationTokenSource;
+
+ #endregion
+
+ #region IAsyncQuickInfoSession
+
+ // All state changes are dispatched on the UI thread via TransitionState().
+ public event EventHandler<QuickInfoSessionStateChangedEventArgs> StateChanged;
+
+ public bool HasInteractiveContent => Volatile.Read(ref this.uiThreadWritableHasInteractiveContent);
+
+ public ITrackingSpan ApplicableToSpan => Volatile.Read(ref this.applicableToSpan);
+
+ public QuickInfoSessionOptions Options { get; }
+
+ public PropertyCollection Properties { get; }
+
+ public QuickInfoSessionState State => (QuickInfoSessionState)Volatile.Read(ref this.uiThreadWritableState);
+
+ public ITextView TextView { get; }
+
+ public IEnumerable<object> Content => Volatile.Read(ref this.content);
+
+#pragma warning disable 618
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ // ILegacyQuickInfoMetadata should be removed and switched out for IOrderableContentTypeMetadata.
+ public AsyncQuickInfoSession(
+ IEnumerable<Lazy<IAsyncQuickInfoSourceProvider, IOrderableContentTypeMetadata>> orderedSourceProviders,
+ JoinableTaskContext joinableTaskContext,
+ ITextView textView,
+ ITrackingPoint triggerPoint,
+ QuickInfoSessionOptions options,
+ PropertyCollection propertyCollection = null)
+ {
+#pragma warning restore 618
+ this.orderedSourceProviders = orderedSourceProviders ?? throw new ArgumentNullException(nameof(orderedSourceProviders));
+ this.JoinableTaskContext = joinableTaskContext ?? throw new ArgumentNullException(nameof(joinableTaskContext));
+ this.TextView = textView ?? throw new ArgumentNullException(nameof(textView));
+ this.triggerPoint = triggerPoint ?? throw new ArgumentNullException(nameof(triggerPoint));
+ this.Options = options;
+
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ // We can remove this null check once we remove the legacy APIs.
+ this.Properties = propertyCollection ?? new PropertyCollection();
+
+ // Trigger point must be a tracking point on the view's buffer.
+ if (triggerPoint.TextBuffer != textView.TextBuffer)
+ {
+ throw new ArgumentException("The specified ITextSnapshot doesn't belong to the correct TextBuffer");
+ }
+ }
+
+ public virtual async Task DismissAsync()
+ {
+ // Ensure that we have the UI thread. To avoid races, the rest of this method must be sync.
+ await this.JoinableTaskContext.Factory.SwitchToMainThreadAsync();
+
+ var currentState = this.State;
+ if (currentState != QuickInfoSessionState.Dismissed)
+ {
+ this.CancelComputations();
+
+ // Update object state.
+ Volatile.Write(ref this.content, ImmutableList<object>.Empty);
+ Volatile.Write(ref this.applicableToSpan, null);
+
+ // Alert subscribers on the UI thread.
+ this.TransitionTo(QuickInfoSessionState.Dismissed);
+ }
+ }
+
+ public ITrackingPoint GetTriggerPoint(ITextBuffer textBuffer)
+ {
+ var mappedTriggerPoint = GetTriggerPoint(textBuffer.CurrentSnapshot);
+
+ if (!mappedTriggerPoint.HasValue)
+ {
+ return null;
+ }
+
+ return mappedTriggerPoint.Value.Snapshot.CreateTrackingPoint(mappedTriggerPoint.Value, PointTrackingMode.Negative);
+ }
+
+ public SnapshotPoint? GetTriggerPoint(ITextSnapshot textSnapshot)
+ {
+ var triggerSnapshotPoint = this.triggerPoint.GetPoint(this.TextView.TextSnapshot);
+ var triggerSpan = new SnapshotSpan(triggerSnapshotPoint, 0);
+
+ var mappedSpans = new FrugalList<SnapshotSpan>();
+ MappingHelper.MapDownToBufferNoTrack(triggerSpan, textSnapshot.TextBuffer, mappedSpans);
+
+ if (mappedSpans.Count == 0)
+ {
+ return null;
+ }
+ else
+ {
+ return mappedSpans[0].Start;
+ }
+ }
+
+ #endregion
+
+ #region Internal Impl
+
+ internal virtual async Task UpdateAsync(bool allowUpdate, CancellationToken cancellationToken)
+ {
+ if ((this.State != QuickInfoSessionState.Created) && !allowUpdate)
+ {
+ throw new InvalidOperationException($"Session must be in the {QuickInfoSessionState.Created} state to be started");
+ }
+
+ // Ensure we have the UI thread.
+ await this.JoinableTaskContext.Factory.SwitchToMainThreadAsync();
+
+ // Read current state.
+ var initialState = this.State;
+
+ this.CancelComputations();
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Create a linked cancellation token and store this in the class so we can be canceled by calls to DismissAsync()
+ // without impacting the caller's cancellation token.
+ using (var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
+ {
+ try
+ {
+ this.uiThreadOnlyLinkedCancellationTokenSource = linkedCancellationTokenSource;
+
+ var failures = await this.ComputeContentAndUpdateAsync(
+ initialState,
+ allowUpdate,
+ this.uiThreadOnlyLinkedCancellationTokenSource.Token).ConfigureAwait(true);
+
+ if (failures?.Any() ?? false)
+ {
+ await this.DismissAsync().ConfigureAwait(false);
+ throw new AggregateException(failures);
+ }
+ }
+ finally
+ {
+ this.uiThreadOnlyLinkedCancellationTokenSource = null;
+ }
+ }
+ }
+
+ #endregion
+
+ #region Private Impl
+
+ private void CancelComputations()
+ {
+ // Cancel any running computations.
+ this.uiThreadOnlyLinkedCancellationTokenSource?.Cancel();
+ this.uiThreadOnlyLinkedCancellationTokenSource = null;
+ }
+
+ private async Task<IList<Exception>> ComputeContentAndUpdateAsync(QuickInfoSessionState initialState, bool allowUpdate, CancellationToken cancellationToken)
+ {
+ //IntellisenseUtilities.ThrowIfNotOnMainThread(this.JoinableTaskContext);
+
+ // Alert subscribers on the UI thread.
+ this.TransitionTo(QuickInfoSessionState.Calculating, allowUpdate);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var failures = new FrugalList<Exception>();
+
+ // Find and create the sources. Sources cache is smart enough to
+ // invalidate on content-type changed and free on view close.
+ var sources = this.GetOrCreateSources(failures);
+
+ // Compute quick info items. This method switches off the UI thread.
+ // From here on out we're on an arbitrary thread.
+ (IList<object> items, IList<ITrackingSpan> applicableToSpans)? results
+ = await ComputeContentAsync(sources, failures, cancellationToken).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Update our content, or put the empty list if there is none.
+ Volatile.Write(
+ ref this.content,
+ results != null ? ImmutableList.CreateRange(results.Value.items) : ImmutableList<object>.Empty);
+
+ await StartUIThreadEpilogueAsync(initialState, results?.applicableToSpans, cancellationToken).ConfigureAwait(false);
+
+ return failures;
+ }
+
+ private IEnumerable<OrderedSource> GetOrCreateSources(IList<Exception> failures)
+ {
+ var joinableTaskContext = this.JoinableTaskContext;
+ var orderedSourceProviders = this.orderedSourceProviders;
+
+ // Bug #543960: we use a lambda with explicit capturing of the 'CreateSources'
+ // arguments to prevent the compiler from generating a lambda that captures a
+ // reference to 'this'. Doing so would cause AsyncQuickInfoSession to be kept
+ // alive by the IntellisenseSourceCache and leaked until the view closes.
+ return IntellisenseSourceCache.GetSources(
+ this.TextView,
+ GetBuffersForTriggerPoint().ToList(),
+ (textBuffer) => CreateSources(
+ joinableTaskContext,
+ orderedSourceProviders,
+ textBuffer,
+ failures));
+ }
+
+ private async Task StartUIThreadEpilogueAsync(QuickInfoSessionState initialState, IList<ITrackingSpan> applicableToSpans, CancellationToken cancellationToken)
+ {
+ // Ensure we're back on the UI thread.
+ await this.JoinableTaskContext.Factory.SwitchToMainThreadAsync();
+
+ if (applicableToSpans != null)
+ {
+ // Update the applicable-to span.
+ this.ComputeApplicableToSpan(applicableToSpans);
+ }
+
+ // Check if any of our content is interactive and cache that so it's not done on mouse move.
+ this.ComputeHasInteractiveContent();
+
+ // If we have results and a span for which to show them and we aren't cancelled update the tip.
+ if ((initialState == QuickInfoSessionState.Dismissed)
+ || !this.Content.Any()
+ || (this.ApplicableToSpan == null)
+ || cancellationToken.IsCancellationRequested)
+ {
+ // If we were unable to await some computation task and don't end up with
+ // a visible presenter + content, ensure that we cleanup and change our state appropriately.
+ await this.DismissAsync().ConfigureAwait(false);
+ }
+ }
+
+ private async Task<(IList<object> items, IList<ITrackingSpan> applicableToSpans)> ComputeContentAsync(
+ IEnumerable<OrderedSource> unorderedSources,
+ IList<Exception> failures,
+ CancellationToken cancellationToken)
+ {
+ // Ensure we're off the UI thread.
+ await TaskScheduler.Default;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var items = new FrugalList<object>();
+ var applicableToSpans = new FrugalList<ITrackingSpan>();
+
+ // Sources from the cache are from the flattened projection buffer graph
+ // so they're initially out of order. We recorded their MEF ordering in
+ // a property though so we can reorder them now.
+ foreach (var source in unorderedSources.OrderBy(source => source.Order))
+ {
+ // This code is sequential to enable back-compat with the IQuickInfo* APIs,
+ // but when the shims are removed, consider parallelizing as a potential optimization.
+ await this.ComputeSourceContentAsync(
+ source.Source,
+ items,
+ applicableToSpans,
+ failures,
+ cancellationToken).ConfigureAwait(false);
+ }
+
+ return (items, applicableToSpans);
+ }
+
+ private void ComputeHasInteractiveContent()
+ {
+ foreach (var result in this.Content)
+ {
+ if (result is IInteractiveQuickInfoContent)
+ {
+ Volatile.Write(ref this.uiThreadWritableHasInteractiveContent, true);
+ break;
+ }
+ }
+ }
+
+ private async Task ComputeSourceContentAsync(
+ IAsyncQuickInfoSource source,
+ IList<object> items,
+ IList<ITrackingSpan> applicableToSpans,
+ IList<Exception> failures,
+ CancellationToken cancellationToken)
+ {
+ Debug.Assert(!this.JoinableTaskContext.IsOnMainThread);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ if (!await this.TryComputeContentFromLegacySourceAsync(source, items, applicableToSpans).ConfigureAwait(false))
+ {
+ var result = await source.GetQuickInfoItemAsync(this, cancellationToken).ConfigureAwait(false);
+ if (result != null)
+ {
+ items.Add(result.Item);
+ if (result.ApplicableToSpan != null)
+ {
+ applicableToSpans.Add(result.ApplicableToSpan);
+ }
+ }
+ }
+ }
+ catch (Exception ex) when (ex.GetType() != typeof(OperationCanceledException))
+ {
+ failures.Add(ex);
+ }
+ }
+
+ private void ComputeApplicableToSpan(IEnumerable<ITrackingSpan> applicableToSpans)
+ {
+ // Requires UI thread for access to BufferGraph.
+ //IntellisenseUtilities.ThrowIfNotOnMainThread(this.JoinableTaskContext);
+
+ ITrackingSpan newApplicableToSpan = Volatile.Read(ref this.applicableToSpan);
+
+ foreach (var result in applicableToSpans)
+ {
+ var applicableToSpan = result;
+
+ if (applicableToSpan != null)
+ {
+ SnapshotSpan subjectAppSnapSpan = applicableToSpan.GetSpan(applicableToSpan.TextBuffer.CurrentSnapshot);
+
+ var surfaceAppSpans = this.TextView.BufferGraph.MapUpToBuffer(
+ subjectAppSnapSpan,
+ applicableToSpan.TrackingMode,
+ this.TextView.TextBuffer);
+
+ if (surfaceAppSpans.Count >= 1)
+ {
+ applicableToSpan = surfaceAppSpans[0].Snapshot.CreateTrackingSpan(surfaceAppSpans[0], applicableToSpan.TrackingMode);
+
+ newApplicableToSpan = IntellisenseUtilities.GetEncapsulatingSpan(
+ this.TextView,
+ newApplicableToSpan,
+ applicableToSpan);
+ }
+ }
+ }
+
+ Volatile.Write(ref this.applicableToSpan, newApplicableToSpan);
+ }
+
+#pragma warning disable 618
+ // Bug #512117: Remove compatibility shims for 2nd gen. Quick Info APIs.
+ // ILegacyQuickInfoMetadata should be removed and switched out for IOrderableContentTypeMetadata.
+ private static IReadOnlyCollection<OrderedSource> CreateSources(
+ JoinableTaskContext joinableTaskContext,
+ IEnumerable<Lazy<IAsyncQuickInfoSourceProvider, IOrderableContentTypeMetadata>> orderedSourceProviders,
+ ITextBuffer textBuffer,
+ IList<Exception> failures)
+ {
+#pragma warning restore 618
+ //IntellisenseUtilities.ThrowIfNotOnMainThread(joinableTaskContext);
+
+ int i = 0;
+ var sourcesList = new List<OrderedSource>();
+
+ foreach (var sourceProvider in orderedSourceProviders)
+ {
+ foreach (var contentType in sourceProvider.Metadata.ContentTypes)
+ {
+ if (textBuffer.ContentType.IsOfType(contentType))
+ {
+ try
+ {
+ var source = sourceProvider.Value.TryCreateQuickInfoSource(textBuffer);
+ if (source != null)
+ {
+ sourcesList.Add(new OrderedSource(i, source));
+ }
+ }
+ catch (Exception ex)
+ {
+ failures.Add(ex);
+ }
+ }
+ }
+
+ ++i;
+ }
+
+ return sourcesList;
+ }
+
+ private Collection<ITextBuffer> GetBuffersForTriggerPoint()
+ {
+ //IntellisenseUtilities.ThrowIfNotOnMainThread(this.JoinableTaskContext);
+
+ return this.TextView.BufferGraph.GetTextBuffers(
+ buffer => this.GetTriggerPoint(buffer.CurrentSnapshot) != null);
+ }
+
+ protected void TransitionTo(QuickInfoSessionState newState, bool allowUpdate = false)
+ {
+ //IntellisenseUtilities.ThrowIfNotOnMainThread(this.JoinableTaskContext);
+
+ var oldState = this.State;
+ bool isValid = false;
+
+ switch (newState)
+ {
+ case QuickInfoSessionState.Created:
+ isValid = false;
+ break;
+ case QuickInfoSessionState.Calculating:
+ isValid = oldState == QuickInfoSessionState.Created ||
+ oldState == QuickInfoSessionState.Visible ||
+ (allowUpdate && (oldState == QuickInfoSessionState.Calculating));
+ break;
+ case QuickInfoSessionState.Dismissed:
+ isValid = oldState == QuickInfoSessionState.Visible || oldState == QuickInfoSessionState.Calculating;
+ break;
+ case QuickInfoSessionState.Visible:
+ isValid = oldState == QuickInfoSessionState.Calculating;
+ break;
+ }
+
+ if (!isValid)
+ {
+ throw new InvalidOperationException(FormattableString.Invariant($"Invalid {nameof(IAsyncQuickInfoSession)} state transition from {oldState} to {newState}"));
+ }
+
+ Volatile.Write(ref this.uiThreadWritableState, (int)newState);
+ this.StateChanged?.Invoke(this, new QuickInfoSessionStateChangedEventArgs(oldState, newState));
+ }
+
+ private sealed class OrderedSource : IDisposable
+ {
+ public OrderedSource(int order, IAsyncQuickInfoSource source)
+ {
+ this.Order = order;
+ this.Source = source ?? throw new ArgumentNullException(nameof(source));
+ }
+
+ public IAsyncQuickInfoSource Source { get; }
+
+ public int Order { get; }
+
+ public void Dispose()
+ {
+ this.Source.Dispose();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Language/Impl/Language/QuickInfo/QuickInfoController.cs b/src/Language/Impl/Language/QuickInfo/QuickInfoController.cs
new file mode 100644
index 0000000..a6416f0
--- /dev/null
+++ b/src/Language/Impl/Language/QuickInfo/QuickInfoController.cs
@@ -0,0 +1,165 @@
+namespace Microsoft.VisualStudio.Language.Intellisense.Implementation
+{
+ using System;
+ using System.Diagnostics;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.Language.Utilities;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Threading;
+
+ internal sealed class QuickInfoController
+ {
+ private readonly IAsyncQuickInfoBroker quickInfoBroker;
+ private readonly JoinableTaskContext joinableTaskContext;
+ private readonly ITextView textView;
+ private CancellationTokenSource cancellationTokenSource;
+
+ internal QuickInfoController(
+ IAsyncQuickInfoBroker quickInfoBroker,
+ JoinableTaskContext joinableTaskContext,
+ ITextView textView)
+ {
+ this.quickInfoBroker = quickInfoBroker ?? throw new ArgumentNullException(nameof(quickInfoBroker));
+ this.joinableTaskContext = joinableTaskContext ?? throw new ArgumentNullException(nameof(joinableTaskContext));
+ this.textView = textView ?? throw new ArgumentNullException(nameof(textView));
+
+ IntellisenseUtilities.ThrowIfNotOnMainThread(joinableTaskContext);
+
+ this.textView.MouseHover += this.OnMouseHover;
+ this.textView.Closed += this.OnTextViewClosed;
+ }
+
+ // Internal for unit test.
+ internal void OnTextViewClosed(object sender, EventArgs e)
+ {
+ IntellisenseUtilities.ThrowIfNotOnMainThread(this.joinableTaskContext);
+
+ this.textView.Closed -= this.OnTextViewClosed;
+
+ // Cancel any calculating sessions and dispose the token.
+ this.CancelAndDisposeToken();
+
+ // Terminate any open quick info sessions.
+ this.joinableTaskContext.Factory.RunAsync(async delegate
+ {
+ var session = this.quickInfoBroker.GetSession(this.textView);
+ if (session != null)
+ {
+ await session.DismissAsync().ConfigureAwait(true);
+ }
+ });
+
+ this.textView.MouseHover -= this.OnMouseHover;
+ }
+
+ private void OnMouseHover(object sender, MouseHoverEventArgs e)
+ {
+ IntellisenseUtilities.ThrowIfNotOnMainThread(this.joinableTaskContext);
+
+ SnapshotPoint? surfaceHoverPointNullable = e.TextPosition.GetPoint(
+ this.textView.TextBuffer,
+ PositionAffinity.Predecessor);
+
+ // Does hover correspond to actual position in document or
+ // is there already a session around that is valid?
+ if (!surfaceHoverPointNullable.HasValue || this.IsSessionStillValid(surfaceHoverPointNullable.Value))
+ {
+ return;
+ }
+
+ // Cancel last queued quick info update, if there is one.
+ CancelAndDisposeToken();
+
+ this.cancellationTokenSource = new CancellationTokenSource();
+
+ // Start quick info session async on the UI thread.
+ this.joinableTaskContext.Factory.RunAsync(async delegate
+ {
+ await UpdateSessionStateAsync(surfaceHoverPointNullable.Value, this.cancellationTokenSource.Token).ConfigureAwait(true);
+
+ // Clean up the cancellation token source.
+ Debug.Assert(this.joinableTaskContext.IsOnMainThread);
+ this.cancellationTokenSource?.Dispose();
+ this.cancellationTokenSource = null;
+ });
+ }
+
+ private async Task UpdateSessionStateAsync(SnapshotPoint surfaceHoverPoint, CancellationToken cancellationToken)
+ {
+ // If we were cancelled while queued, do nothing.
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ ITrackingPoint triggerPoint = surfaceHoverPoint.Snapshot.CreateTrackingPoint(
+ surfaceHoverPoint.Position,
+ PointTrackingMode.Negative);
+
+ try
+ {
+ await this.quickInfoBroker.TriggerQuickInfoAsync(
+ this.textView,
+ triggerPoint,
+ QuickInfoSessionOptions.TrackMouse,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) { /* swallow exception */ }
+ }
+
+ /// <summary>
+ /// Ensures that the specified session is still valid given the specified point. If the point is within the applicability
+ /// span of the session, the session will be left alone and the method will return true. If the point is outside of the
+ /// sessions applicability span, the session will be dismissed and the method will return false.
+ /// </summary>
+ private bool IsSessionStillValid(SnapshotPoint point)
+ {
+ // Make sure we're being called with a surface snapshot point.
+ Debug.Assert(point.Snapshot.TextBuffer == this.textView.TextBuffer);
+
+ var session = this.quickInfoBroker.GetSession(this.textView);
+
+ if (session != null)
+ {
+ // First check that the point and applicable span are from the same subject buffer,
+ // and then that they intersect.
+ if ((session.ApplicableToSpan != null) &&
+ (session.ApplicableToSpan.TextBuffer == point.Snapshot.TextBuffer) &&
+ (session.ApplicableToSpan.GetSpan(point.Snapshot).IntersectsWith(new Span(point.Position, 0))))
+ {
+ return true;
+ }
+
+ // If this session has an interactive content give it a chance to keep the session alive.
+ if (session.HasInteractiveContent)
+ {
+ foreach (var content in session.Content)
+ {
+ foreach (var result in session.Content)
+ {
+ if (result is IInteractiveQuickInfoContent interactiveContent
+ && (interactiveContent.KeepQuickInfoOpen || interactiveContent.IsMouseOverAggregated))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private void CancelAndDisposeToken()
+ {
+ if (this.cancellationTokenSource != null)
+ {
+ this.cancellationTokenSource.Cancel();
+ this.cancellationTokenSource.Dispose();
+ this.cancellationTokenSource = null;
+ }
+ }
+ }
+}
diff --git a/src/Language/Impl/Language/QuickInfo/QuickInfoTextViewCreationListener.cs b/src/Language/Impl/Language/QuickInfo/QuickInfoTextViewCreationListener.cs
new file mode 100644
index 0000000..c387e97
--- /dev/null
+++ b/src/Language/Impl/Language/QuickInfo/QuickInfoTextViewCreationListener.cs
@@ -0,0 +1,42 @@
+namespace Microsoft.VisualStudio.Language.Intellisense.Implementation
+{
+ using System;
+ using System.ComponentModel.Composition;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Threading;
+ using Microsoft.VisualStudio.Utilities;
+
+ [Export(typeof(ITextViewCreationListener))]
+ [ContentType("any")]
+ [TextViewRole(PredefinedTextViewRoles.Editable)]
+ [TextViewRole(PredefinedTextViewRoles.EmbeddedPeekTextView)]
+ [TextViewRole(PredefinedTextViewRoles.CodeDefinitionView)]
+ internal sealed class QuickInfoTextViewCreationListener : ITextViewCreationListener
+ {
+ private readonly IAsyncQuickInfoBroker quickInfoBroker;
+ private readonly JoinableTaskContext joinableTaskContext;
+
+ [ImportingConstructor]
+ public QuickInfoTextViewCreationListener(
+ IAsyncQuickInfoBroker quickInfoBroker,
+ JoinableTaskContext joinableTaskContext)
+ {
+ this.quickInfoBroker = quickInfoBroker
+ ?? throw new ArgumentNullException(nameof(quickInfoBroker));
+ this.joinableTaskContext = joinableTaskContext
+ ?? throw new ArgumentNullException(nameof(joinableTaskContext));
+ }
+
+ public void TextViewCreated(ITextView textView)
+ {
+#pragma warning disable CA1806
+ // No need to do anything further, this type hooks up events to the
+ // text view and tracks its own life cycle.
+ new QuickInfoController(
+ this.quickInfoBroker,
+ this.joinableTaskContext,
+ textView);
+#pragma warning restore CA1806
+ }
+ }
+}
diff --git a/src/Language/Impl/Language/QuickInfo/SquiggleQuickInfoSource.cs b/src/Language/Impl/Language/QuickInfo/SquiggleQuickInfoSource.cs
new file mode 100644
index 0000000..f2c22e5
--- /dev/null
+++ b/src/Language/Impl/Language/QuickInfo/SquiggleQuickInfoSource.cs
@@ -0,0 +1,143 @@
+namespace Microsoft.VisualStudio.Language.Intellisense.Implementation
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.VisualStudio.Language.Utilities;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Adornments;
+ using Microsoft.VisualStudio.Text.Editor;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ internal sealed class SquiggleQuickInfoSource : IAsyncQuickInfoSource
+ {
+ private bool disposed = false;
+ private SquiggleQuickInfoSourceProvider componentContext;
+ private ITextBuffer textBuffer;
+ private ITextView tagAggregatorTextView;
+ private ITagAggregator<IErrorTag> tagAggregator;
+
+ public SquiggleQuickInfoSource(
+ SquiggleQuickInfoSourceProvider componentContext,
+ ITextBuffer textBuffer)
+ {
+ this.componentContext = componentContext
+ ?? throw new ArgumentNullException(nameof(componentContext));
+ this.textBuffer = textBuffer
+ ?? throw new ArgumentNullException(nameof(textBuffer));
+ }
+
+ public async Task<QuickInfoItem> GetQuickInfoItemAsync(
+ IAsyncQuickInfoSession session,
+ CancellationToken cancellationToken)
+ {
+ if (this.disposed)
+ {
+ throw new ObjectDisposedException("SquiggleQuickInfoSource");
+ }
+
+ if (session.TextView.TextBuffer != this.textBuffer)
+ {
+ return null;
+ }
+
+ // TagAggregators must be used exclusively on the UI thread.
+ await this.componentContext.JoinableTaskContext.Factory.SwitchToMainThreadAsync();
+
+ ITrackingSpan applicableToSpan = null;
+ var quickInfoContent = new FrugalList<object>();
+ ITagAggregator<IErrorTag> tagAggregator = GetTagAggregator(session.TextView);
+
+ Debug.Assert(tagAggregator != null, "Couldn't create a tag aggregator for error tags");
+ if (tagAggregator != null)
+ {
+ // Put together the span over which tags are to be discovered. This will be the zero-length span at the trigger
+ // point of the session.
+ SnapshotPoint? subjectTriggerPoint = session.GetTriggerPoint(this.textBuffer.CurrentSnapshot);
+ if (!subjectTriggerPoint.HasValue)
+ {
+ Debug.Fail("The squiggle QuickInfo source is being called when it shouldn't be.");
+ return null;
+ }
+
+ ITextSnapshot currentSnapshot = subjectTriggerPoint.Value.Snapshot;
+ var querySpan = new SnapshotSpan(subjectTriggerPoint.Value, 0);
+
+ // Ask for all of the error tags that intersect our query span. We'll get back a list of mapping tag spans.
+ // The first of these is what we'll use for our quick info.
+ IEnumerable<IMappingTagSpan<IErrorTag>> tags = tagAggregator.GetTags(querySpan);
+ ITrackingSpan appToSpan = null;
+ foreach (MappingTagSpan<IErrorTag> tag in tags)
+ {
+ NormalizedSnapshotSpanCollection applicableToSpans = tag.Span.GetSpans(currentSnapshot);
+ if ((applicableToSpans.Count > 0) && (tag.Tag.ToolTipContent != null))
+ {
+ // We've found a error tag at the right location with a tag span that maps to our subject buffer.
+ // Return the applicability span as well as the tooltip content.
+ appToSpan = IntellisenseUtilities.GetEncapsulatingSpan(
+ session.TextView,
+ appToSpan,
+ currentSnapshot.CreateTrackingSpan(
+ applicableToSpans[0].Span,
+ SpanTrackingMode.EdgeInclusive));
+
+ quickInfoContent.Add(tag.Tag.ToolTipContent);
+ }
+ }
+
+ if (quickInfoContent.Count > 0)
+ {
+ applicableToSpan = appToSpan;
+ return new QuickInfoItem(
+ applicableToSpan,
+ new ContainerElement(
+ ContainerElementStyle.Stacked | ContainerElementStyle.VerticalPadding,
+ quickInfoContent));
+ }
+ }
+
+ return null;
+ }
+
+ private ITagAggregator<IErrorTag> GetTagAggregator(ITextView textView)
+ {
+ if (this.tagAggregator == null)
+ {
+ this.tagAggregatorTextView = textView;
+ this.tagAggregator = this.componentContext.TagAggregatorFactoryService.CreateTagAggregator<IErrorTag>(textView);
+ }
+ else if (this.tagAggregatorTextView != textView)
+ {
+ throw new ArgumentException ("The SquiggleQuickInfoSource cannot be shared between TextViews.");
+ }
+
+ return this.tagAggregator;
+ }
+
+ public void Dispose()
+ {
+ if (this.disposed)
+ {
+ return;
+ }
+
+ this.disposed = true;
+
+ Debug.Assert(this.componentContext.JoinableTaskContext.IsOnMainThread);
+
+ // Get rid of the tag aggregator, if we created it.
+ if (this.tagAggregator != null)
+ {
+ this.tagAggregator.Dispose();
+ this.tagAggregator = null;
+ this.tagAggregatorTextView = null;
+ }
+
+ this.componentContext = null;
+ this.textBuffer = null;
+ }
+ }
+}
diff --git a/src/Language/Impl/Language/QuickInfo/SquiggleQuickInfoSourceProvider.cs b/src/Language/Impl/Language/QuickInfo/SquiggleQuickInfoSourceProvider.cs
new file mode 100644
index 0000000..668c037
--- /dev/null
+++ b/src/Language/Impl/Language/QuickInfo/SquiggleQuickInfoSourceProvider.cs
@@ -0,0 +1,26 @@
+namespace Microsoft.VisualStudio.Language.Intellisense.Implementation
+{
+ using System.ComponentModel.Composition;
+ using Microsoft.VisualStudio.Text;
+ using Microsoft.VisualStudio.Text.Tagging;
+ using Microsoft.VisualStudio.Threading;
+ using Microsoft.VisualStudio.Utilities;
+
+ [Export(typeof(IAsyncQuickInfoSourceProvider))]
+ [Name("squiggle")]
+ [Order]
+ [ContentType("any")]
+ internal sealed class SquiggleQuickInfoSourceProvider : IAsyncQuickInfoSourceProvider
+ {
+ [Import]
+ internal IViewTagAggregatorFactoryService TagAggregatorFactoryService { get; set; }
+
+ [Import]
+ internal JoinableTaskContext JoinableTaskContext { get; set; }
+
+ public IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer)
+ {
+ return new SquiggleQuickInfoSource(this, textBuffer);
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Text.Implementation.csproj b/src/Microsoft.VisualStudio.Text.Implementation.csproj
index 0644a58..0ca89ae 100644
--- a/src/Microsoft.VisualStudio.Text.Implementation.csproj
+++ b/src/Microsoft.VisualStudio.Text.Implementation.csproj
@@ -126,10 +126,13 @@
<ItemGroup>
<Compile Remove="Core\Def\**\*" />
+ <Compile Remove="Language\Def\**\*" />
<Compile Remove="Text\Def\Text*\**\*" />
<EmbeddedResource Remove="Core\Def\**\*" />
+ <EmbeddedResource Remove="Language\Def\**\*" />
<EmbeddedResource Remove="Text\Def\Text*\**\*" />
<None Remove="Core\Def\**\*" />
+ <None Remove="Language\Def\**\*" />
<None Remove="Text\Def\Text*\**\*" />
</ItemGroup>
diff --git a/src/Text/Impl/XPlat/MultiCaretImpl/Strings.Designer.cs b/src/Text/Impl/XPlat/MultiCaretImpl/Strings.Designer.cs
index 4e023f8..7455ad8 100644
--- a/src/Text/Impl/XPlat/MultiCaretImpl/Strings.Designer.cs
+++ b/src/Text/Impl/XPlat/MultiCaretImpl/Strings.Designer.cs
@@ -39,7 +39,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation {
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Text.Implementation.MultiSelection.Implementation.Strings", typeof(Strings).Assembly);
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Text.Implementation.Text.Impl.XPlat.MultiCaretImpl.Strings", typeof(Strings).Assembly);
resourceMan = temp;
}
return resourceMan;