Walkthrough: Displaying QuickInfo Tooltips

QuickInfo is an IntelliSense feature that displays method signatures and descriptions when a user moves the pointer over a method name. You can implement language-based features such as QuickInfo by defining the identifiers for which you want to provide QuickInfo descriptions, and then creating a tooltip in which to display the content. You can define QuickInfo in the context of a language service, or you can define your own file name extension and content type and display the QuickInfo for just that type, or you can display QuickInfo for an existing content type (such as "text"). This walkthrough shows how to display QuickInfo for the "text" content type.

The QuickInfo example in this walkthrough displays the tooltips when a user moves the pointer over a method name. This design requires you to implement these four interfaces:

  • source interface

  • source provider interface

  • controller interface

  • controller provider interface

    The source and controller providers are Managed Extensibility Framework (MEF) component parts, and are responsible for exporting the source and controller classes and importing services and brokers such as the ITextBufferFactoryService, which creates the tooltip text buffer, and the IQuickInfoBroker, which triggers the QuickInfo session.

    In this example, the QuickInfo source uses a hard-coded list of method names and descriptions, but in full implementations, the language service and the language documentation are responsible for providing that content.

Prerequisites

Starting in Visual Studio 2015, you do not install the Visual Studio SDK from the download center. It is included as an optional feature in Visual Studio setup. You can also install the VS SDK later on. For more information, see Installing the Visual Studio SDK.

Creating a MEF Project

To create a MEF project

  1. Create a C# VSIX project. (In the New Project dialog, select Visual C# / Extensibility, then VSIX Project.) Name the solution QuickInfoTest.

  2. Add an Editor Classifier item template to the project. For more information, see Creating an Extension with an Editor Item Template.

  3. Delete the existing class files.

Implementing the QuickInfo Source

The QuickInfo source is responsible for collecting the set of identifiers and their descriptions and adding the content to the tooltip text buffer when one of the identifiers is encountered. In this example, the identifiers and their descriptions are just added in the source constructor.

To implement the QuickInfo source

  1. Add a class file and name it TestQuickInfoSource.

  2. Add a reference to Microsoft.VisualStudio.Language.IntelliSense.

  3. Add the following imports.

    Imports System
    Imports System.Collections.Generic
    Imports System.Linq
    Imports System.Text
    Imports System.Collections.ObjectModel
    Imports System.ComponentModel.Composition
    Imports Microsoft.VisualStudio.Language.Intellisense
    Imports Microsoft.VisualStudio.Text
    Imports Microsoft.VisualStudio.Text.Editor
    Imports Microsoft.VisualStudio.Text.Operations
    Imports Microsoft.VisualStudio.Text.Tagging
    Imports Microsoft.VisualStudio.Utilities
    
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Language.Intellisense;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Operations;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
    
  4. Declare a class that implements IQuickInfoSource, and name it TestQuickInfoSource.

    Friend Class TestQuickInfoSource
        Implements IQuickInfoSource
    
    internal class TestQuickInfoSource : IQuickInfoSource
    
  5. Add fields for the QuickInfo source provider, the text buffer, and a set of method names and method signatures. In this example, the method names and signatures are initialized in the TestQuickInfoSource constructor.

    Private m_provider As TestQuickInfoSourceProvider
    Private m_subjectBuffer As ITextBuffer
    Private m_dictionary As Dictionary(Of String, String)
    
    private TestQuickInfoSourceProvider m_provider;
    private ITextBuffer m_subjectBuffer;
    private Dictionary<string, string> m_dictionary;
    
  6. Add a constructor that sets the QuickInfo source provider and the text buffer, and populates the set of method names, and method signatures and descriptions.

    Public Sub New(ByVal provider As TestQuickInfoSourceProvider, ByVal subjectBuffer As ITextBuffer)
        m_provider = provider
        m_subjectBuffer = subjectBuffer
    
        'these are the method names and their descriptions
        m_dictionary = New Dictionary(Of String, String)()
        m_dictionary.Add("add", "int add(int firstInt, int secondInt)" & vbLf & "Adds one integer to another.")
        m_dictionary.Add("subtract", "int subtract(int firstInt, int secondInt)" & vbLf & "Subtracts one integer from another.")
        m_dictionary.Add("multiply", "int multiply(int firstInt, int secondInt)" & vbLf & "Multiplies one integer by another.")
        m_dictionary.Add("divide", "int divide(int firstInt, int secondInt)" & vbLf & "Divides one integer by another.")
    End Sub
    
    public TestQuickInfoSource(TestQuickInfoSourceProvider provider, ITextBuffer subjectBuffer)
    {
        m_provider = provider;
        m_subjectBuffer = subjectBuffer;
    
        //these are the method names and their descriptions
        m_dictionary = new Dictionary<string, string>();
        m_dictionary.Add("add", "int add(int firstInt, int secondInt)\nAdds one integer to another.");
        m_dictionary.Add("subtract", "int subtract(int firstInt, int secondInt)\nSubtracts one integer from another.");
        m_dictionary.Add("multiply", "int multiply(int firstInt, int secondInt)\nMultiplies one integer by another.");
        m_dictionary.Add("divide", "int divide(int firstInt, int secondInt)\nDivides one integer by another.");
    }
    
  7. Implement the AugmentQuickInfoSession method. In this example, the method finds the current word, or the previous word if the cursor is at the end of a line or a text buffer. If the word is one of the method names, the description for that method name is added to the QuickInfo content.

    Public Sub AugmentQuickInfoSession(ByVal session As IQuickInfoSession, ByVal qiContent As IList(Of Object), ByRef applicableToSpan As ITrackingSpan) Implements IQuickInfoSource.AugmentQuickInfoSession
        ' Map the trigger point down to our buffer.
        Dim subjectTriggerPoint As System.Nullable(Of SnapshotPoint) = session.GetTriggerPoint(m_subjectBuffer.CurrentSnapshot)
        If Not subjectTriggerPoint.HasValue Then
            applicableToSpan = Nothing
            Exit Sub
        End If
    
        Dim currentSnapshot As ITextSnapshot = subjectTriggerPoint.Value.Snapshot
        Dim querySpan As New SnapshotSpan(subjectTriggerPoint.Value, 0)
    
        'look for occurrences of our QuickInfo words in the span
        Dim navigator As ITextStructureNavigator = m_provider.NavigatorService.GetTextStructureNavigator(m_subjectBuffer)
        Dim extent As TextExtent = navigator.GetExtentOfWord(subjectTriggerPoint.Value)
        Dim searchText As String = extent.Span.GetText()
    
        For Each key As String In m_dictionary.Keys
            Dim foundIndex As Integer = searchText.IndexOf(key, StringComparison.CurrentCultureIgnoreCase)
            If foundIndex > -1 Then
                'applicableToSpan = currentSnapshot.CreateTrackingSpan(querySpan.Start.Add(foundIndex).Position, 9, SpanTrackingMode.EdgeInclusive)
    
                applicableToSpan = currentSnapshot.CreateTrackingSpan(extent.Span.Start + foundIndex, key.Length, SpanTrackingMode.EdgeInclusive)
                Dim value As String = ""
                m_dictionary.TryGetValue(key, value)
                If value IsNot Nothing Then
                    qiContent.Add(value)
                Else
                    qiContent.Add("")
                End If
    
                Exit Sub
            End If
        Next
    
        applicableToSpan = Nothing
    End Sub
    
    public void AugmentQuickInfoSession(IQuickInfoSession session, IList<object> qiContent, out ITrackingSpan applicableToSpan)
    {
        // Map the trigger point down to our buffer.
        SnapshotPoint? subjectTriggerPoint = session.GetTriggerPoint(m_subjectBuffer.CurrentSnapshot);
        if (!subjectTriggerPoint.HasValue)
        {
            applicableToSpan = null;
            return;
        }
    
        ITextSnapshot currentSnapshot = subjectTriggerPoint.Value.Snapshot;
        SnapshotSpan querySpan = new SnapshotSpan(subjectTriggerPoint.Value, 0);
    
        //look for occurrences of our QuickInfo words in the span
        ITextStructureNavigator navigator = m_provider.NavigatorService.GetTextStructureNavigator(m_subjectBuffer);
        TextExtent extent = navigator.GetExtentOfWord(subjectTriggerPoint.Value);
        string searchText = extent.Span.GetText();
    
        foreach (string key in m_dictionary.Keys)
        {
            int foundIndex = searchText.IndexOf(key, StringComparison.CurrentCultureIgnoreCase);
            if (foundIndex > -1)
            {
                applicableToSpan = currentSnapshot.CreateTrackingSpan
                    (
                    //querySpan.Start.Add(foundIndex).Position, 9, SpanTrackingMode.EdgeInclusive
                                            extent.Span.Start + foundIndex, key.Length, SpanTrackingMode.EdgeInclusive
                    );
    
                string value;
                m_dictionary.TryGetValue(key, out value);
                if (value != null)
                    qiContent.Add(value);
                else
                    qiContent.Add("");
    
                return;
            }
        }
    
        applicableToSpan = null;
    }
    
  8. You must also implement a Dispose() method, since IQuickInfoSource implements IDisposable:

    Private m_isDisposed As Boolean
    Public Sub Dispose() Implements IDisposable.Dispose
        If Not m_isDisposed Then
            GC.SuppressFinalize(Me)
            m_isDisposed = True
        End If
    End Sub
    
    private bool m_isDisposed;
    public void Dispose()
    {
        if (!m_isDisposed)
        {
            GC.SuppressFinalize(this);
            m_isDisposed = true;
        }
    }
    

Implementing a QuickInfo Source Provider

The provider of the QuickInfo source serves primarily to export itself as a MEF component part and instantiate the QuickInfo source. Because it is a MEF component part, it can import other MEF component parts.

To implement a QuickInfo source provider

  1. Declare a QuickInfo source provider named TestQuickInfoSourceProvider that implements IQuickInfoSourceProvider, and export it with a NameAttribute of "ToolTip QuickInfo Source", an OrderAttribute of Before="default", and a ContentTypeAttribute of "text".

    <Export(GetType(IQuickInfoSourceProvider))> _
    <Name("ToolTip QuickInfo Source")> _
    <Order(Before:=" Default Quick Info Presenter")> _
    <ContentType("text")> _
    Friend Class TestQuickInfoSourceProvider
        Implements IQuickInfoSourceProvider
    
    [Export(typeof(IQuickInfoSourceProvider))]
    [Name("ToolTip QuickInfo Source")]
    [Order(Before = "Default Quick Info Presenter")]
    [ContentType("text")]
    internal class TestQuickInfoSourceProvider : IQuickInfoSourceProvider
    
  2. Import two editor services, ITextStructureNavigatorSelectorService and ITextBufferFactoryService, as properties of TestQuickInfoSourceProvider.

    Private _NavigatorService As ITextStructureNavigatorSelectorService
    <Import()> _
    Friend Property NavigatorService() As ITextStructureNavigatorSelectorService
        Get
            Return _NavigatorService
        End Get
        Set(ByVal value As ITextStructureNavigatorSelectorService)
            _NavigatorService = value
        End Set
    End Property
    
    Private _TextBufferFactoryService As ITextBufferFactoryService
    <Import()> _
    Friend Property TextBufferFactoryService() As ITextBufferFactoryService
        Get
            Return _TextBufferFactoryService
        End Get
        Set(ByVal value As ITextBufferFactoryService)
            _TextBufferFactoryService = value
        End Set
    End Property
    
    [Import]
    internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
    
    [Import]
    internal ITextBufferFactoryService TextBufferFactoryService { get; set; }
    
  3. Implement TryCreateQuickInfoSource to return a new TestQuickInfoSource.

    Public Function TryCreateQuickInfoSource(ByVal textBuffer As ITextBuffer) As IQuickInfoSource Implements IQuickInfoSourceProvider.TryCreateQuickInfoSource
        Return New TestQuickInfoSource(Me, textBuffer)
    End Function
    
    public IQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer)
    {
        return new TestQuickInfoSource(this, textBuffer);
    }
    

Implementing a QuickInfo Controller

QuickInfo controllers determine when QuickInfo should be displayed. In this example, QuickInfo is displayed when the pointer is over a word that corresponds to one of the method names. The QuickInfo controller implements a mouse hover event handler that triggers a QuickInfo session.

To implement a QuickInfo controller

  1. Declare a class that implements IIntellisenseController, and name it TestQuickInfoController.

    Friend Class TestQuickInfoController
        Implements IIntellisenseController
    
    internal class TestQuickInfoController : IIntellisenseController
    
  2. Add private fields for the text view, the text buffers represented in the text view, the QuickInfo session, and the QuickInfo controller provider.

    Private m_textView As ITextView
    Private m_subjectBuffers As IList(Of ITextBuffer)
    Private m_provider As TestQuickInfoControllerProvider
    Private m_session As IQuickInfoSession
    
    private ITextView m_textView;
    private IList<ITextBuffer> m_subjectBuffers;
    private TestQuickInfoControllerProvider m_provider;
    private IQuickInfoSession m_session;
    
  3. Add a constructor that sets the fields and adds the mouse hover event handler.

    Friend Sub New(ByVal textView As ITextView, ByVal subjectBuffers As IList(Of ITextBuffer), ByVal provider As TestQuickInfoControllerProvider)
        m_textView = textView
        m_subjectBuffers = subjectBuffers
        m_provider = provider
    
        AddHandler m_textView.MouseHover, AddressOf Me.OnTextViewMouseHover
    End Sub
    
    internal TestQuickInfoController(ITextView textView, IList<ITextBuffer> subjectBuffers, TestQuickInfoControllerProvider provider)
    {
        m_textView = textView;
        m_subjectBuffers = subjectBuffers;
        m_provider = provider;
    
        m_textView.MouseHover += this.OnTextViewMouseHover;
    }
    
  4. Add the mouse hover event handler that triggers the QuickInfo session.

    Private Sub OnTextViewMouseHover(ByVal sender As Object, ByVal e As MouseHoverEventArgs)
        'find the mouse position by mapping down to the subject buffer
        Dim point As System.Nullable(Of SnapshotPoint) = m_textView.BufferGraph.MapDownToFirstMatch(New SnapshotPoint(m_textView.TextSnapshot, e.Position), PointTrackingMode.Positive, Function(snapshot) m_subjectBuffers.Contains(snapshot.TextBuffer), PositionAffinity.Predecessor)
    
        If point IsNot Nothing Then
            Dim triggerPoint As ITrackingPoint = point.Value.Snapshot.CreateTrackingPoint(point.Value.Position, PointTrackingMode.Positive)
    
            If Not m_provider.QuickInfoBroker.IsQuickInfoActive(m_textView) Then
                m_session = m_provider.QuickInfoBroker.TriggerQuickInfo(m_textView, triggerPoint, True)
            End If
        End If
    End Sub
    
    private void OnTextViewMouseHover(object sender, MouseHoverEventArgs e)
    {
        //find the mouse position by mapping down to the subject buffer
        SnapshotPoint? point = m_textView.BufferGraph.MapDownToFirstMatch
             (new SnapshotPoint(m_textView.TextSnapshot, e.Position),
            PointTrackingMode.Positive,
            snapshot => m_subjectBuffers.Contains(snapshot.TextBuffer),
            PositionAffinity.Predecessor);
    
        if (point != null)
        {
            ITrackingPoint triggerPoint = point.Value.Snapshot.CreateTrackingPoint(point.Value.Position,
            PointTrackingMode.Positive);
    
            if (!m_provider.QuickInfoBroker.IsQuickInfoActive(m_textView))
            {
                m_session = m_provider.QuickInfoBroker.TriggerQuickInfo(m_textView, triggerPoint, true);
            }
        }
    }
    
  5. Implement the Detach method so that it removes the mouse hover event handler when the controller is detached from the text view.

    Public Sub Detach(ByVal textView As ITextView) Implements IIntellisenseController.Detach
        If m_textView Is textView Then
            AddHandler m_textView.MouseHover, AddressOf Me.OnTextViewMouseHover
            m_textView = Nothing
        End If
    End Sub
    
    public void Detach(ITextView textView)
    {
        if (m_textView == textView)
        {
            m_textView.MouseHover -= this.OnTextViewMouseHover;
            m_textView = null;
        }
    }
    
  6. Implement the ConnectSubjectBuffer method and the DisconnectSubjectBuffer method as empty methods for this example.

    Public Sub ConnectSubjectBuffer(ByVal subjectBuffer As ITextBuffer) Implements IIntellisenseController.ConnectSubjectBuffer
    
    End Sub
    
    Public Sub DisconnectSubjectBuffer(ByVal subjectBuffer As ITextBuffer) Implements IIntellisenseController.DisconnectSubjectBuffer
    
    End Sub
    
    public void ConnectSubjectBuffer(ITextBuffer subjectBuffer)
    {
    }
    
    public void DisconnectSubjectBuffer(ITextBuffer subjectBuffer)
    {
    }
    

Implementing the QuickInfo Controller Provider

The provider of the QuickInfo controller serves primarily to export itself as a MEF component part and instantiate the QuickInfo controller. Because it is a MEF component part, it can import other MEF component parts.

To implement the QuickInfo controller provider

  1. Declare a class named TestQuickInfoControllerProvider that implements IIntellisenseControllerProvider, and export it with a NameAttribute of "ToolTip QuickInfo Controller" and a ContentTypeAttribute of "text":

    <Export(GetType(IIntellisenseControllerProvider))> _
    <Name("ToolTip QuickInfo Controller")> _
    <ContentType("text")> _
    Friend Class TestQuickInfoControllerProvider
        Implements IIntellisenseControllerProvider
    
    [Export(typeof(IIntellisenseControllerProvider))]
    [Name("ToolTip QuickInfo Controller")]
    [ContentType("text")]
    internal class TestQuickInfoControllerProvider : IIntellisenseControllerProvider
    
  2. Import the IQuickInfoBroker as a property.

    Private _QuickInfoBroker As IQuickInfoBroker
    <Import()> _
    Friend Property QuickInfoBroker() As IQuickInfoBroker
        Get
            Return _QuickInfoBroker
        End Get
        Set(ByVal value As IQuickInfoBroker)
            _QuickInfoBroker = value
        End Set
    End Property
    
    [Import]
    internal IQuickInfoBroker QuickInfoBroker { get; set; }
    
  3. Implement the TryCreateIntellisenseController method by instantiating the QuickInfo controller.

    Public Function TryCreateIntellisenseController(ByVal textView As ITextView, ByVal subjectBuffers As IList(Of ITextBuffer)) As IIntellisenseController Implements IIntellisenseControllerProvider.TryCreateIntellisenseController
        Return New TestQuickInfoController(textView, subjectBuffers, Me)
    End Function
    
    public IIntellisenseController TryCreateIntellisenseController(ITextView textView, IList<ITextBuffer> subjectBuffers)
    {
        return new TestQuickInfoController(textView, subjectBuffers, this);
    }
    

Building and Testing the Code

To test this code, build the QuickInfoTest solution and run it in the experimental instance.

To build and test the QuickInfoTest solution

  1. Build the solution.

  2. When you run this project in the debugger, a second instance of Visual Studio is instantiated.

  3. Create a text file and type some text that includes the words "add" and "subtract".

  4. Move the pointer over one of the occurrences of "add". The signature and the description of the add method should be displayed.

See Also

Walkthrough: Linking a Content Type to a File Name Extension