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); }
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)); }
private Collection <ITextBuffer> GetBuffersForTriggerPoint() { IntellisenseUtilities.ThrowIfNotOnMainThread(this.JoinableTaskContext); return(this.TextView.BufferGraph.GetTextBuffers( buffer => this.GetTriggerPoint(buffer.CurrentSnapshot) != null)); }
private void TransitionTo(QuickInfoSessionState newState) { 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; 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($"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 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 void OnMouseHover(object sender, MouseHoverEventArgs e) { IntellisenseUtilities.ThrowIfNotOnMainThread(this.joinableTaskContext); SnapshotPoint?surfaceHoverPointNullable = e.TextPosition.GetPoint( this.textView.TextViewModel.DataBuffer, 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); // Clean up the cancellation token source. Debug.Assert(this.joinableTaskContext.IsOnMainThread); this.cancellationTokenSource.Dispose(); this.cancellationTokenSource = null; }); }
private IEnumerable <OrderedSource> CreateSources(ITextBuffer textBuffer) { IntellisenseUtilities.ThrowIfNotOnMainThread(this.joinableTaskContext); int i = 0; foreach (var sourceProvider in this.orderedSourceProviders) { foreach (var contentType in sourceProvider.Metadata.ContentTypes) { if (textBuffer.ContentType.IsOfType(contentType)) { var source = this.guardedOperations.InstantiateExtension( this, sourceProvider, provider => provider.TryCreateQuickInfoSource(textBuffer)); if (source != null) { yield return(new OrderedSource(i, source)); } } } ++i; } }
private void OnDismissed(object sender, EventArgs e) { IntellisenseUtilities.ThrowIfNotOnMainThread(this.joinableTaskContext); this.joinableTaskContext.Factory.RunAsync(async delegate { await this.DismissAsync().ConfigureAwait(false); }); }
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); } } } // Scope the applicableToSpan down to just the current line to ensure that interactions // with interactive controls such as lightbulb are not impeded by the tip appearing // far away from the mouse. if (newApplicableToSpan != null) { var currentSnapshot = newApplicableToSpan.TextBuffer.CurrentSnapshot; var spanStart = newApplicableToSpan.GetStartPoint(currentSnapshot); var spanEnd = newApplicableToSpan.GetEndPoint(currentSnapshot); var triggerPointLine = this.triggerPoint.GetPoint(currentSnapshot).GetContainingLine(); var triggerPointLineExtent = triggerPointLine.Extent; var newStart = Math.Max(triggerPointLineExtent.Start, spanStart); var newEnd = Math.Min(triggerPointLineExtent.End, spanEnd); if (newStart <= newEnd) { newApplicableToSpan = currentSnapshot.CreateTrackingSpan(Span.FromBounds(newStart, newEnd), SpanTrackingMode.EdgeInclusive); } } Volatile.Write(ref this.applicableToSpan, newApplicableToSpan); }
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; }
#pragma warning restore 618 // 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 AsyncQuickInfoSession session) { session.TextView.Properties.RemoveProperty(typeof(AsyncQuickInfoSession)); session.StateChanged -= this.OnStateChanged; return; } Debug.Fail("Unexpected sender type"); } }
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); }
// 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(); } }); this.textView.MouseHover -= this.OnMouseHover; }
private void CreateAndStartPresenter() { IntellisenseUtilities.ThrowIfNotOnMainThread(this.joinableTaskContext); Debug.Assert(this.uiThreadOnlyPresenter == null); // Configure presenter behavior. var parameters = new ToolTipParameters( this.Options.HasFlag(QuickInfoSessionOptions.TrackMouse), keepOpenFunc: this.ContentRequestsKeepOpen); // Create and show presenter. this.uiThreadOnlyPresenter = this.toolTipService.CreatePresenter(this.TextView, parameters); this.uiThreadOnlyPresenter.Dismissed += this.OnDismissed; 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); } }
#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 void ComputePresentationSpan() { if ((_session == null) || (_session.IsDismissed)) { return; } // Save off the old presentation span so we can tell if it changes. ITrackingSpan oldPresentationSpan = _presentationSpan; if (_session.Signatures.Count > 0) { _presentationSpan = null; foreach (ISignature signature in _session.Signatures) { _presentationSpan = IntellisenseUtilities.GetEncapsulatingSpan(_session.TextView, _presentationSpan, signature.ApplicableToSpan); } } // For now, we're only interested in letting consumers know about presentation span changes if the START of our // presentation span changed. That's where our display position gets calculated. // TODO : Develop a better story around presentation span changes in popup presenters. If the presentation location // (coordinates) doesn't really change, we shouldn't have a flicker... ITextSnapshot viewSnapshot = _session.TextView.TextSnapshot; SnapshotSpan newSpan = _presentationSpan.GetSpan(viewSnapshot); SnapshotSpan? oldSpan = null; if (oldPresentationSpan != null) { oldSpan = oldPresentationSpan.GetSpan(viewSnapshot); } if (!oldSpan.HasValue || (newSpan.Start != oldSpan.Value.Start)) { this.FirePresentationSpanChanged(); } }
private async Task <IList <Exception> > ComputeContentAndStartPresenterAsync(CancellationToken cancellationToken) { IntellisenseUtilities.ThrowIfNotOnMainThread(this.joinableTaskContext); // Read current state. var originalState = this.State; (IList <object> items, IList <ITrackingSpan> applicableToSpans, IList <Exception> failures)? results = null; // Alert subscribers on the UI thread. this.TransitionTo(QuickInfoSessionState.Calculating); cancellationToken.ThrowIfCancellationRequested(); // Find and create the sources. Sources cache is smart enough to // invalidate on content-type changed and free on view close. var sources = IntellisenseSourceCache.GetSources( this.TextView, GetBuffersForTriggerPoint().ToList(), this.CreateSources); // Compute quick info items. This method switches off the UI thread. // From here on out we're on an arbitrary thread. results = await ComputeContentAsync(sources, 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(originalState, results?.applicableToSpans, cancellationToken).ConfigureAwait(false); return(results?.failures); }
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); }