diff options
author | Kirill Osenkov <github@osenkov.com> | 2018-08-16 02:14:18 +0300 |
---|---|---|
committer | Kirill Osenkov <github@osenkov.com> | 2018-08-16 02:14:18 +0300 |
commit | 193f2ac081338d0034a2b2874ba59c8343db56a7 (patch) | |
tree | 20b7b010bfbb71f9d15f183fb7329dcf4881c45f | |
parent | 21b22d2687687c4013d8e7873dd515518b06b386 (diff) |
Add Async Quick Info.
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; |