Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

Modern Quick Info API

Christian Gunderman edited this page Jan 26, 2018 · 5 revisions
  • Feature Team: Editor
  • Owner: Christian Gunderman
  • Document Type: Dev Design Doc
  • Release: 15.6

NOTE: This information is subject to change without notice.

Overview

This document captures the steps taken to produce a portable Quick Info API for use as part of a common editor extensibility surface on Visual Studio proper and Visual Studio for Mac.

Goals

  • Promote growth of the Visual Studio family by providing a single converged API for Quick Info.
  • Reduce cost for extenders targeting both platforms.
  • Reduce duplication of efforts.
  • Help establish general patterns for a modern generation of async Intellisense APIs.
  • Improve feature parity between VS proper and VS for Mac.
  • Remove Windows and WPF specific dependencies from Quick Info API.
  • Introduce async execution into our APIs to reduce impact on responsiveness.

Non-Goals

  • Reduction in Quick Info flexibility. The new APIs should eventually be able to supersede the old.
  • Direct platformization of Roslyn Quick Info infrastructure. Much of this infrastructure was designed to achieve async behavior in the classic synchronous Quick Info world and does not satisfy all of our needs.

Tasks

  • Remove WPF dependencies from Quick Info API.
  • Create new async APIs to reduce impact on responsiveness.
  • Reorganize VS Quick Info implementation into WPF and non-WPF specific layers.
  • Implement an abstraction for ToolTip content that is UI independent and define a pattern for specific IDEs to translate this abstraction to view components for their UI framework.
  • Provide way for providers to abstractly classify content (e.g.: keyword, type, etc). such that content can be formatted/themed per-IDE without platform specific code.
  • Presentation concepts should be extensible to support existing Visual Studio Quick Info scenarios (Light Bulb, interactive ToolTips, etc.)

These changes are being implemented in collaboration with the Roslyn, Web Tools, and VS for Mac team whom are looking to bring web experiences to Mac, and reduce the overhead of maintaining cross platform infrastructure for both IDEs.

  • Roslyn Editor Features Team
  • VS Web Tools Team
  • VS Editor Team
  • VS for Mac Team

Design

Classic Quick Info Design (IQuickInfo*)

The Visual Studio Quick Info API is a specialization of the Intellisense family of APIs, which are targeted at surfacing metadata and error information to the user. IIntellisenseController is the entry point to the feature, and it is mapped to a text view by content type. It listens to events on the view as well as those provided by MEF. When the right conditions arise, the IIntellisenseController pops up the quick info via APIs on the IQuickInfoBroker. The QuickInfo broker creates an IQuickInfoSession and a list of extender-provided IQuickInfoSources and passes the session to the sources, allowing each source to synchronously add and remove data to be displayed in the tip before it is shown.

Classic Quick Info Usage Scenarios (IQuickInfo*)

  • CSS Editor: Hosts a UI element (WPF).
  • LightBulb: Hosts a initially collapsed control that expands to include an icon asynchronously after the tip is displayed.
  • Python: Gives the API one or more raw strings to format and render.
  • R: Gives the API one or more raw strings to format and render.
  • Razor: Adds a user control (WPF).
  • Web Editor: Uses a custom control (WPF) that displays quick info and a link to documentation.

Classic Quick Info Design (IQuickInfo*)

  • IIntellisenseControllerProvider: Instantiates the Intellisense controller and imports any required MEF services (namely, the IQuickInfoBroker). This interface is implemented by extenders and there is one per IIntellisense controller implementation.
  • IQuickInfoBroker: Provides APIs for triggering, expanding, collapsing, and enumerating quick info sessions. The Platform implements this interface and there is one per process.
  • IQuickInfoSession(2): Represents a session with a visible, collapsed, or previously visible QuickInfo tip. The Platform’s Quick Info feature implements this interface, and there is only one implementation, but there is one instance per visible or previously visible QuickInfo tip. This type unfortunately has a dependency on WPF via a BulkObservableCollection QuickInfoContent member.
  • IQuickInfoSource: When a new session is started, or an existing session is recalculated, this interface is called to add information to the QuickInfo tip. Data in the QuickInfo tips take the form of a list of objects, which can be raw strings or WPF elements. This interface is implemented by extenders and there can be multiple for a single ITextView, even if they are the same content type.
  • IQuickInfoSourceProvider: Instantiates an IQuickInfoSource and imports any MEF services that it requires. There is one per IQuickInfoSource implementation.

New Design

The new Quick Info API design must be cross platform capable and async but must not reduce the flexibility of the feature for our existing customers. Quick Info ToolTips are invoked via the Modern ToolTip API and its abstractions.

Requirements

  • Mostly backwards compatible with the existing APIs. Some behaviors, such as UI thread affinity, modification and reordering of other providers items, and custom presenters will no longer be supported.
  • Can host information from multiple providers in a single Quick Info session.
  • xPlat API surfaces.
  • Should allow proffering custom UI controls in a cross-platform way.
  • Colorization, indentation, spacing, and code formatting in a xPlat way.
  • Allows platform specific interactive controls, such as ‘Show Potential Fixes’ link for providers that are platform aware.
  • Asynchronous
  • Logic/UI split for code sharing in VS for Mac and VS

Deprecation and Backcompat

  • Existing Quick Info APIs (both IVsTextTipData and IQuickInfo*) should be supported as a shim to the new APIs and be isolated such that they can be deprecated and removed in Dev16 for VS proper, and excluded all together on VS for Mac.
  • New APIs should not limit the flexibility of the feature and should offer a feature-complete replacement for the existing APIs.
  • Some behaviors, such as class Quick Info Intellisense presenters, the IntellisenseSessionStack, etc. are no longer supported.

Design Proposal

IQuickInfoSession’s WPF dependency has made it impossible to reuse any of the existing Quick Info specific interfaces, so I propose that we create an entirely new set of QuickInfo APIs, distinguished by the prefix ‘Async’ or ‘Portable’.

  • IAsyncQuickInfoBroker: Interface for programmatically invoking Quick Info at a point in a text view. This code is primarily called by the editor itself, but it is exposed for extenders that want to invoke Quick Info explicitly. Now allows invoking Quick Info in an async fashion to avoid loss of responsiveness. Multiple quick info sessions, though supported by the classic Quick Info APIs signature, was felt to be an unneeded complexity and was removed.
    /// <summary>
    /// Controls invocation and dismissal of Quick Info tooltips for <see cref="ITextView"/> instances.
    /// </summary>
    /// <remarks>
    /// This type can be called from any thread and will marshal its work to the UI thread as required.
    /// This is a MEF component part and can be acquired in a MEF exported class with:
    /// <example>
    /// [Import] private IAsyncQuickInfoBroker quickInfoBroker;
    /// </example>
    /// </remarks>
    public interface IAsyncQuickInfoBroker
    {
        /// <summary>
        /// Determines whether there is at least one active Quick Info session in the specified <see cref="ITextView" />.
        /// </summary>
        /// <remarks>
        /// Quick info is considered to be active if there is a visible, calculating, or recalculating quick info session.
        /// </remarks>
        /// <param name="textView">The <see cref="ITextView" /> for which Quick Info session status is to be determined.</</param>
        /// <returns>
        /// <c>true</c> if there is at least one active or calculating Quick Info session over the specified <see cref="ITextView" />, <c>false</c>
        /// otherwise.
        /// </returns>
        bool IsQuickInfoActive(ITextView textView);

        /// <summary>
        /// Triggers Quick Info tooltip in the specified <see cref="ITextView"/> at the caret or optional <paramref name="triggerPoint"/>.
        /// </summary>
        /// <exception cref="OperationCanceledException">
        /// <paramref name="cancellationToken"/> was canceled by the caller or the operation was interrupted by another call to
        /// <see cref="TriggerQuickInfoAsync(ITextView, ITrackingPoint, QuickInfoSessionOptions, CancellationToken)"/>
        /// </exception>
        /// <param name="cancellationToken">If canceled before the method returns, cancels any computations in progress.</param>
        /// <param name="textView">
        /// The <see cref="ITextView" /> for which Quick Info is to be triggered.
        /// </param>
        /// <param name="triggerPoint">
        /// The <see cref="ITrackingPoint" /> in the view's text buffer at which Quick Info should be triggered.
        /// </param>
        /// <param name="options">Options for customizing Quick Info behavior.</param>
        /// <returns>
        /// An <see cref="IAsyncQuickInfoSession"/> tracking the state of the session or null if there are no items.
        /// </returns>
        Task<IAsyncQuickInfoSession> TriggerQuickInfoAsync(
            ITextView textView,
            ITrackingPoint triggerPoint = null,
            QuickInfoSessionOptions options = QuickInfoSessionOptions.None,
            CancellationToken cancellationToken = default(CancellationToken));

        /// <summary>
        /// Gets the current <see cref="IAsyncQuickInfoSession"/> for the <see cref="ITextView"/>.
        /// </summary>
        /// <remarks>
        /// Quick info is considered to be active if there is a visible, calculating, or recalculating quick info session.
        /// </remarks>
        /// <param name="textView">The <see cref="ITextView" /> for which to lookup the session.</param>
        /// <returns>The session, or <c>null</c> if there is no active session.</returns>
        IAsyncQuickInfoSession GetSession(ITextView textView);
    }
  • IAsyncQuickInfoSession: Implemented by the editor, this interface exposes the state of a computing, visible, or dismissed quick info session. This iteration removes the WPF dependency, introduces async calling patterns, and isolates providers. The session can no longer be modified directly. All changes to the session must be additive in nature and provided via an IAsyncQuickInfoSource. Features that wish to display only their content to the user can do so via the cross platform IToolTipService instead of using quick info. All members are safe to call from any thread, however, the TextView is subject to the typical ITextView threading requirements.
    /// <summary>
    /// Tracks state of a visible or calculating Quick Info session.
    /// </summary>
    public interface IAsyncQuickInfoSession : IPropertyOwner
    {
        /// <summary>
        /// Dispatched on the UI thread whenever the Quick Info Session changes state.
        /// </summary>
        event EventHandler<QuickInfoSessionStateChangedEventArgs> StateChanged;

        /// <summary>
        /// The span of text to which this Quick Info session applies.
        /// </summary>
        ITrackingSpan ApplicableToSpan { get; }

        /// <summary>
        /// The ordered, merged collection of content to be displayed in the Quick Info.
        /// </summary>
        /// <remarks>
        /// This field is originally null and is updated with the content once the session has
        /// finished querying the providers.
        /// </remarks>
        IEnumerable<object> Content { get; }

        /// <summary>
        /// Indicates that this Quick Info has interactive content that can request to stay open.
        /// </summary>
        bool HasInteractiveContent { get; }

        /// <summary>
        /// Specifies attributes of the Quick Info session and Quick Info session presentation.
        /// </summary>
        QuickInfoSessionOptions Options { get; }

        /// <summary>
        /// The current state of the Quick Info session.
        /// </summary>
        QuickInfoSessionState State { get; }

        /// <summary>
        /// The <see cref="ITextView"/> for which this Quick Info session was created.
        /// </summary>
        ITextView TextView { get; }

        /// <summary>
        /// Gets the point at which the Quick Info tip was triggered in the <see cref="ITextView"/>.
        /// </summary>
        /// <remarks>
        /// Returned <see cref="ITrackingPoint"/> is on the buffer requested by the caller.
        /// </remarks>
        /// <param name="textBuffer">The <see cref="ITextBuffer"/> relative to which to obtain the point.</param>
        /// <returns>A <see cref="ITrackingPoint"/> indicating the point over which Quick Info was invoked.</returns>
        ITrackingPoint GetTriggerPoint(ITextBuffer textBuffer);

        /// <summary>
        /// Gets the point at which the Quick Info tip was triggered in the <see cref="ITextView"/>.
        /// </summary>
        /// <remarks>
        /// Returned point is on the buffer requested by the caller.
        /// </remarks>
        /// <param name="snapshot">The <see cref="ITextSnapshot"/> relative to which to obtain the point.</param>
        /// <returns>The point over which Quick Info was invoked or <c>null</c> if it does not exist in <paramref name="snapshot"/>.</returns>
        SnapshotPoint? GetTriggerPoint(ITextSnapshot snapshot);

        /// <summary>
        /// Dismisses the Quick Info session, if applicable. If the session is already dismissed,
        /// this method no-ops.
        /// </summary>
        /// <returns>A task tracking the completion of the operation.</returns>
        Task DismissAsync();
    }
  • IAsyncQuickInfoSource: Extender provided object that interfaces with the language service and provides content for Quick Info at a specific point. This iteration removes the WPF dependency and isolates providers so that they can be queried in parallel.
    /// <summary>
    /// Source of Quick Info tooltip content item, proffered to the IDE by a <see cref="IAsyncQuickInfoSourceProvider"/>.
    /// </summary>
    /// <remarks>
    /// This class is always constructed and disposed on the UI thread and called on
    /// a non-UI thread.
    /// Content objects are resolved into UI constructs via the <see cref="IViewElementFactoryService"/>.
    /// </remarks>
    public interface IAsyncQuickInfoSource : IDisposable
    {
        /// <summary>
        /// Gets Quick Info item and tracking span via a <see cref="QuickInfoItem"/>.
        /// </summary>
        /// <remarks>
        /// This method is always called on a background thread. Multiple elements can be
        /// be returned by a single source by wrapping them in a <see cref="ContainerElement"/>.
        /// </remarks>
        /// <param name="session">An object tracking the current state of the Quick Info.</param>
        /// <param name="cancellationToken">Cancels an in-progress computation.</param>
        /// <returns>item and a tracking span for which these item are applicable.</returns>
        Task<QuickInfoItem> GetQuickInfoItemAsync(
            IAsyncQuickInfoSession session,
            CancellationToken cancellationToken);
    }
  • IAsyncQuickInfoSourceProvider: Instantiates an IAsyncQuickInfoSource and imports any MEF services that it requires. There is one per IAsyncQuickInfoSource implementation. Quick Info items from various providers are ordered in the ToolTip content by their MEF Order attributes.
    /// <summary>
    /// A MEF component part that is proffered to the IDE to construct an <see cref="IAsyncQuickInfoSource"/>.
    /// </summary>
    /// <remarks>
    /// This class is always constructed and called on the UI thread.
    /// </remarks>
    /// <example>
    /// [Export(typeof(IAsyncQuickInfoSourceProvider))]
    /// [Name("Foo QuickInfo Provider")]
    /// [Order(After = "default")]
    /// [ContentType("text")]
    /// </example>
    public interface IAsyncQuickInfoSourceProvider
    {
        /// <summary>
        /// Creates an <see cref="IAsyncQuickInfoSource"/> for the specified <see cref="ITextBuffer"/>.
        /// </summary>
        /// <param name="textBuffer">The <see cref="ITextBuffer"/> for which this source produces items.</param>
        /// <returns>
        /// An instance of <see cref="IAsyncQuickInfoSource"/> for <paramref name="textBuffer"/>
        /// or null if no source could be created.
        /// </returns>
        IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer);
    }
  • QuickInfoItem: A data transfer object defining a object to display in the Quick Info and an ITrackingSpan of text on any buffer in the view's BufferGraph to which it applies. object is resolved to a platform-specific UI element on the UI thread when the tooltip is constructed via the IViewElementFactoryService.
        /// <summary>
        /// Constructs a new instance of <see cref="QuickInfoItem"/>.
        /// </summary>
        /// <exception cref="ArgumentNullException">Thrown if item is null.</exception>
        /// <param name="applicableToSpan">The span to which <paramref name="item"/> is applicable.</param>
        /// <param name="item">The Quick Info item.</param>
        public QuickInfoItem(ITrackingSpan applicableToSpan, object item)
        {
        }
Clone this wiki locally