Muokkaa

Jaa


Walkthrough: Display Signature Help

Signature Help (also known as Parameter Info) displays the signature of a method in a tooltip when a user types the parameter list start character (typically an opening parenthesis). As a parameter and parameter separator (typically a comma) are typed, the tooltip is updated to show the next parameter in bold. You can define Signature Help in the following ways: in the context of a language service, define your own file name extension and content type and display Signature Help for just that type, or display Signature Help for an existing content type (for example, "text"). This walkthrough shows how to display Signature Help for the "text" content type.

Signature Help is typically triggered by typing a specific character, for example, "(" (opening parenthesis), and dismissed by typing another character, for example, ")" (closing parenthesis). IntelliSense features that are triggered by typing a character can be implemented by using a command handler for the keystrokes (the IOleCommandTarget interface) and a handler provider that implements the IVsTextViewCreationListener interface. To create the Signature Help source, which is the list of signatures that participate in Signature Help, implement the ISignatureHelpSource interface and a source provider that runs the ISignatureHelpSourceProvider interface. The providers are Managed Extensibility Framework (MEF) component parts, and are responsible for exporting the source and controller classes and importing services and brokers, for example, the ITextStructureNavigatorSelectorService, which lets you navigate in the text buffer, and the ISignatureHelpBroker, which triggers the Signature Help session.

This walkthrough shows how to set up Signature Help for a hard-coded set of identifiers. In full implementations, the language is responsible for providing that content.

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 SignatureHelpTest.

  2. Add an Editor Classifier item template to the project. For more information, see Create an extension with an editor item template.

  3. Delete the existing class files.

  4. Add the following references to the project, and make sure CopyLocal is set to false:

    Microsoft.VisualStudio.Editor

    Microsoft.VisualStudio.Language.Intellisense

    Microsoft.VisualStudio.OLE.Interop

    Microsoft.VisualStudio.Shell.14.0

    Microsoft.VisualStudio.TextManager.Interop

Implement Signature Help signatures and parameters

The Signature Help source is based on signatures that implement ISignature, each of which contains parameters that implement IParameter. In a full implementation, this information would be obtained from the language documentation, but in this example, the signatures are hard-coded.

To implement the Signature Help signatures and parameters

  1. Add a class file and name it SignatureHelpSource.

  2. Add the following imports.

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel.Composition;
    using System.Runtime.InteropServices;
    using Microsoft.VisualStudio.Language.Intellisense;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Utilities;
    using Microsoft.VisualStudio.Editor;
    using Microsoft.VisualStudio.Text.Operations;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.TextManager.Interop;
    using Microsoft.VisualStudio.OLE.Interop;
    
  3. Add a class named TestParameter that implements IParameter.

    internal class TestParameter : IParameter
    
  4. Add a constructor that sets all the properties.

    public TestParameter(string documentation, Span locus, string name, ISignature signature)
    {
        Documentation = documentation;
        Locus = locus;
        Name = name;
        Signature = signature;
    }
    
  5. Add the properties of IParameter.

    public string Documentation { get; private set; }
    public Span Locus { get; private set; }
    public string Name { get; private set; }
    public ISignature Signature { get; private set; }
    public Span PrettyPrintedLocus { get; private set; }
    
  6. Add a class named TestSignature that implements ISignature.

    internal class TestSignature : ISignature
    
  7. Add some private fields.

    private ITextBuffer m_subjectBuffer;
    private IParameter m_currentParameter;
    private string m_content;
    private string m_documentation;
    private ITrackingSpan m_applicableToSpan;
    private ReadOnlyCollection<IParameter> m_parameters;
    private string m_printContent;
    
  8. Add a constructor that sets the fields and subscribes to the Changed event.

    internal TestSignature(ITextBuffer subjectBuffer, string content, string doc, ReadOnlyCollection<IParameter> parameters)
    {
        m_subjectBuffer = subjectBuffer;
        m_content = content;
        m_documentation = doc;
        m_parameters = parameters;
        m_subjectBuffer.Changed += new EventHandler<TextContentChangedEventArgs>(OnSubjectBufferChanged);
    }
    
  9. Declare a CurrentParameterChanged event. This event is raised when the user fills in one of the parameters in the signature.

    public event EventHandler<CurrentParameterChangedEventArgs> CurrentParameterChanged;
    
  10. Implement the CurrentParameter property so that it raises the CurrentParameterChanged event when the property value is changed.

    public IParameter CurrentParameter
    {
        get { return m_currentParameter; }
        internal set
        {
            if (m_currentParameter != value)
            {
                IParameter prevCurrentParameter = m_currentParameter;
                m_currentParameter = value;
                this.RaiseCurrentParameterChanged(prevCurrentParameter, m_currentParameter);
            }
        }
    }
    
  11. Add a method that raises the CurrentParameterChanged event.

    private void RaiseCurrentParameterChanged(IParameter prevCurrentParameter, IParameter newCurrentParameter)
    {
        EventHandler<CurrentParameterChangedEventArgs> tempHandler = this.CurrentParameterChanged;
        if (tempHandler != null)
        {
            tempHandler(this, new CurrentParameterChangedEventArgs(prevCurrentParameter, newCurrentParameter));
        }
    }
    
  12. Add a method that computes the current parameter by comparing the number of commas in the ApplicableToSpan to the number of commas in the signature.

    internal void ComputeCurrentParameter()
    {
        if (Parameters.Count == 0)
        {
            this.CurrentParameter = null;
            return;
        }
    
        //the number of commas in the string is the index of the current parameter
        string sigText = ApplicableToSpan.GetText(m_subjectBuffer.CurrentSnapshot);
    
        int currentIndex = 0;
        int commaCount = 0;
        while (currentIndex < sigText.Length)
        {
            int commaIndex = sigText.IndexOf(',', currentIndex);
            if (commaIndex == -1)
            {
                break;
            }
            commaCount++;
            currentIndex = commaIndex + 1;
        }
    
        if (commaCount < Parameters.Count)
        {
            this.CurrentParameter = Parameters[commaCount];
        }
        else
        {
            //too many commas, so use the last parameter as the current one.
            this.CurrentParameter = Parameters[Parameters.Count - 1];
        }
    }
    
  13. Add an event handler for the Changed event that calls the ComputeCurrentParameter() method.

    internal void OnSubjectBufferChanged(object sender, TextContentChangedEventArgs e)
    {
        this.ComputeCurrentParameter();
    }
    
  14. Implement the ApplicableToSpan property. This property holds an ITrackingSpan that corresponds to the span of text in the buffer to which the signature applies.

    public ITrackingSpan ApplicableToSpan
    {
        get { return (m_applicableToSpan); }
        internal set { m_applicableToSpan = value; }
    }
    
  15. Implement the other parameters.

    public string Content
    {
        get { return (m_content); }
        internal set { m_content = value; }
    }
    
    public string Documentation
    {
        get { return (m_documentation); }
        internal set { m_documentation = value; }
    }
    
    public ReadOnlyCollection<IParameter> Parameters
    {
        get { return (m_parameters); }
        internal set { m_parameters = value; }
    }
    
    public string PrettyPrintedContent
    {
        get { return (m_printContent); }
        internal set { m_printContent = value; }
    }
    

Implement the Signature Help source

The Signature Help source is the set of signatures for which you provide information.

To implement the Signature Help source

  1. Add a class named TestSignatureHelpSource that implements ISignatureHelpSource.

    internal class TestSignatureHelpSource : ISignatureHelpSource
    
  2. Add a reference to the text buffer.

    private ITextBuffer m_textBuffer;
    
  3. Add a constructor that sets the text buffer and the Signature Help source provider.

    public TestSignatureHelpSource(ITextBuffer textBuffer)
    {
        m_textBuffer = textBuffer;
    }
    
  4. Implement the AugmentSignatureHelpSession method. In this example, the signatures are hard-coded, but in a full implementation you would get this information from the language documentation.

    public void AugmentSignatureHelpSession(ISignatureHelpSession session, IList<ISignature> signatures)
    {
        ITextSnapshot snapshot = m_textBuffer.CurrentSnapshot;
        int position = session.GetTriggerPoint(m_textBuffer).GetPosition(snapshot);
    
        ITrackingSpan applicableToSpan = m_textBuffer.CurrentSnapshot.CreateTrackingSpan(
         new Span(position, 0), SpanTrackingMode.EdgeInclusive, 0);
    
        signatures.Add(CreateSignature(m_textBuffer, "add(int firstInt, int secondInt)", "Documentation for adding integers.", applicableToSpan));
        signatures.Add(CreateSignature(m_textBuffer, "add(double firstDouble, double secondDouble)", "Documentation for adding doubles.", applicableToSpan));
    
    }
    
  5. The helper method CreateSignature() is provided just for illustration.

    private TestSignature CreateSignature(ITextBuffer textBuffer, string methodSig, string methodDoc, ITrackingSpan span)
    {
        TestSignature sig = new TestSignature(textBuffer, methodSig, methodDoc, null);
        textBuffer.Changed += new EventHandler<TextContentChangedEventArgs>(sig.OnSubjectBufferChanged);
    
        //find the parameters in the method signature (expect methodname(one, two)
        string[] pars = methodSig.Split(new char[] { '(', ',', ')' });
        List<IParameter> paramList = new List<IParameter>();
    
        int locusSearchStart = 0;
        for (int i = 1; i < pars.Length; i++)
        {
            string param = pars[i].Trim();
    
            if (string.IsNullOrEmpty(param))
                continue;
    
            //find where this parameter is located in the method signature
            int locusStart = methodSig.IndexOf(param, locusSearchStart);
            if (locusStart >= 0)
            {
                Span locus = new Span(locusStart, param.Length);
                locusSearchStart = locusStart + param.Length;
                paramList.Add(new TestParameter("Documentation for the parameter.", locus, param, sig));
            }
        }
    
        sig.Parameters = new ReadOnlyCollection<IParameter>(paramList);
        sig.ApplicableToSpan = span;
        sig.ComputeCurrentParameter();
        return sig;
    }
    
  6. Implement the GetBestMatch method. In this example, there are just two signatures, each of which has two parameters. Therefore, this method is not required. In a fuller implementation, in which more than one Signature Help source is available, this method is used to decide whether the highest-priority Signature Help source can supply a matching signature. If it can't, the method returns null and the next-highest-priority source is asked to supply a match.

    public ISignature GetBestMatch(ISignatureHelpSession session)
    {
        if (session.Signatures.Count > 0)
        {
            ITrackingSpan applicableToSpan = session.Signatures[0].ApplicableToSpan;
            string text = applicableToSpan.GetText(applicableToSpan.TextBuffer.CurrentSnapshot);
    
            if (text.Trim().Equals("add"))  //get only "add" 
                return session.Signatures[0];
        }
        return null;
    }
    
  7. Implement the Dispose() method:

    private bool m_isDisposed;
    public void Dispose()
    {
        if (!m_isDisposed)
        {
            GC.SuppressFinalize(this);
            m_isDisposed = true;
        }
    }
    

Implement the Signature Help source provider

The Signature Help source provider is responsible for exporting the Managed Extensibility Framework (MEF) component part and for instantiating the Signature Help source.

To implement the Signature Help source provider

  1. Add a class named TestSignatureHelpSourceProvider that implements ISignatureHelpSourceProvider, and export it with a NameAttribute, a ContentTypeAttribute of "text", and an OrderAttribute of Before="default".

    [Export(typeof(ISignatureHelpSourceProvider))]
    [Name("Signature Help source")]
    [Order(Before = "default")]
    [ContentType("text")]
    internal class TestSignatureHelpSourceProvider : ISignatureHelpSourceProvider
    
  2. Implement TryCreateSignatureHelpSource by instantiating the TestSignatureHelpSource.

    public ISignatureHelpSource TryCreateSignatureHelpSource(ITextBuffer textBuffer)
    {
        return new TestSignatureHelpSource(textBuffer);
    }
    

Implement the command handler

Signature Help is typically triggered by an opening parenthesis "(" character and dismissed by a closing parenthesis ")" character. You can handle these keystrokes by running a IOleCommandTarget so that it triggers a Signature Help session when it receives an opening parenthesis character preceded by a known method name, and dismisses the session when it receives a closing parenthesis character.

To implement the command handler

  1. Add a class named TestSignatureHelpCommand that implements IOleCommandTarget.

    internal sealed class TestSignatureHelpCommandHandler : IOleCommandTarget
    
  2. Add private fields for the IVsTextView adapter (which lets you add the command handler to the chain of command handlers), the text view, the Signature Help broker and session, a ITextStructureNavigator, and the next IOleCommandTarget.

    IOleCommandTarget m_nextCommandHandler;
    ITextView m_textView;
    ISignatureHelpBroker m_broker;
    ISignatureHelpSession m_session;
    ITextStructureNavigator m_navigator;
    
  3. Add a constructor to initialize these fields and to add the command filter to the chain of command filters.

    internal TestSignatureHelpCommandHandler(IVsTextView textViewAdapter, ITextView textView, ITextStructureNavigator nav, ISignatureHelpBroker broker)
    {
        this.m_textView = textView;
        this.m_broker = broker;
        this.m_navigator = nav;
    
        //add this to the filter chain
        textViewAdapter.AddCommandFilter(this, out m_nextCommandHandler);
    }
    
  4. Implement the Exec method to trigger the Signature Help session when the command filter receives an opening parenthesis "(" character after one of the known method names, and to dismiss the session when it receives a closing parenthesis ")" character while the session is still active. In every case, the command is forwarded.

    public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
    {
        char typedChar = char.MinValue;
    
        if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR)
        {
            typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);
            if (typedChar.Equals('('))
            {
                //move the point back so it's in the preceding word
                SnapshotPoint point = m_textView.Caret.Position.BufferPosition - 1;
                TextExtent extent = m_navigator.GetExtentOfWord(point);
                string word = extent.Span.GetText();
                if (word.Equals("add"))
                    m_session = m_broker.TriggerSignatureHelp(m_textView);
    
            }
            else if (typedChar.Equals(')') && m_session != null)
            {
                m_session.Dismiss();
                m_session = null;
            }
        }
        return m_nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);
    }
    
  5. Implement the QueryStatus method so that it always forwards the command.

    public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
    {
        return m_nextCommandHandler.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);
    }
    

Implement the Signature Help command provider

You can provide the Signature Help command by implementing the IVsTextViewCreationListener to instantiate the command handler when the text view is created.

To implement the Signature Help command provider

  1. Add a class named TestSignatureHelpController that implements IVsTextViewCreationListener and export it with the NameAttribute, ContentTypeAttribute, and TextViewRoleAttribute.

    [Export(typeof(IVsTextViewCreationListener))]
    [Name("Signature Help controller")]
    [TextViewRole(PredefinedTextViewRoles.Editable)]
    [ContentType("text")]
    internal class TestSignatureHelpCommandProvider : IVsTextViewCreationListener
    
  2. Import the IVsEditorAdaptersFactoryService (used to get the ITextView, given the IVsTextView object), the ITextStructureNavigatorSelectorService (used to find the current word), and the ISignatureHelpBroker (to trigger the Signature Help session).

    [Import]
    internal IVsEditorAdaptersFactoryService AdapterService;
    
    [Import]
    internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
    
    [Import]
    internal ISignatureHelpBroker SignatureHelpBroker;
    
  3. Implement the VsTextViewCreated method by instantiating the TestSignatureCommandHandler.

    public void VsTextViewCreated(IVsTextView textViewAdapter)
    {
        ITextView textView = AdapterService.GetWpfTextView(textViewAdapter);
        if (textView == null)
            return;
    
        textView.Properties.GetOrCreateSingletonProperty(
             () => new TestSignatureHelpCommandHandler(textViewAdapter,
                textView,
                NavigatorService.GetTextStructureNavigator(textView.TextBuffer),
                SignatureHelpBroker));
    }
    

Build and Test the Code

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

To build and test the SignatureHelpTest solution

  1. Build the solution.

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

  3. Create a text file and type some text that includes the word "add" plus an opening parenthesis.

  4. After you type the opening parenthesis, you should see a tooltip that displays a list of the two signatures for the add() method.