Walkthrough: Using a Shortcut Key with an Editor Extension

Note

This article applies to Visual Studio 2015. If you're looking for the latest Visual Studio documentation, see Visual Studio documentation. We recommend upgrading to the latest version of Visual Studio. Download it here

You can respond to shortcut keys in your editor extension. The following walkthrough shows how to add a view adornment to a text view by using a shortcut key. This walkthrough is based on the viewport adornment editor template, and it allows you to add the adornment by using the + character.

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 Managed Extensibility Framework (MEF) Project

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

  2. Add an Editor Text Adornment item template to the project and name it KeyBindingTest. For more information, see Creating an Extension with an Editor Item Template.

  3. Add the following references and set CopyLocal to false:

    Microsoft.VisualStudio.Editor

    Microsoft.VisualStudio.OLE.Interop

    Microsoft.VisualStudio.Shell.14.0

    Microsoft.VisualStudio.TextManager.Interop

    In the KeyBindingTest class file, change the class name to PurpleCornerBox. Use the light bulb that appears in the left margin to make the other appropriate changes. Inside the constructor, change the name of the adornment layer from KeyBindingTest to PurpleCornerBox:

this.layer = view.GetAdornmentLayer("PurpleCornerBox");  

Defining the Command Filter

The command filter is an implementation of IOleCommandTarget, which handles the command by instantiating the adornment.

  1. Add a class file and name it KeyBindingCommandFilter.

  2. Add the following using statements.

    using System;  
    using System.Runtime.InteropServices;  
    using Microsoft.VisualStudio.OLE.Interop;  
    using Microsoft.VisualStudio;  
    using Microsoft.VisualStudio.Text.Editor;  
    
    
  3. The class named KeyBindingCommandFilter should inherit from IOleCommandTarget.

    internal class KeyBindingCommandFilter : IOleCommandTarget  
    
  4. Add private fields for the text view, the next command in the command chain, and a flag to represent whether the command filter has already been added.

    private IWpfTextView m_textView;  
    internal IOleCommandTarget m_nextTarget;  
    internal bool m_added;  
    internal bool m_adorned;  
    
  5. Add a constructor that sets the text view.

    public KeyBindingCommandFilter(IWpfTextView textView)  
    {  
        m_textView = textView;  
        m_adorned = false;  
    }  
    
  6. Implement the QueryStatus() method as follows.

    int IOleCommandTarget.QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)  
    {  
        return m_nextTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText);  
    }  
    
  7. Implement the Exec() method so that it adds a purple box to the view if a + character is typed.

    int IOleCommandTarget.Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)  
    {  
        if (m_adorned == false)  
        {  
            char typedChar = char.MinValue;  
    
            if (pguidCmdGroup == VSConstants.VSStd2K && nCmdID == (uint)VSConstants.VSStd2KCmdID.TYPECHAR)  
            {  
                typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);  
                if (typedChar.Equals('+'))  
                {  
                    new PurpleCornerBox(m_textView);  
                    m_adorned = true;  
                }  
            }  
        }  
        return m_nextTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut);  
    }  
    
    

Adding the Command Filter

The adornment provider must add a command filter to the text view. In this example, the provider implements IVsTextViewCreationListener to listen to text view creation events. This adornment provider also exports the adornment layer, which defines the Z-order of the adornment.

  1. In the KeyBindingTestTextViewCreationListener file, add the following using statements:

    using System;  
    using System.Collections.Generic;  
    using System.ComponentModel.Composition;  
    using Microsoft.VisualStudio;  
    using Microsoft.VisualStudio.OLE.Interop;  
    using Microsoft.VisualStudio.Utilities;  
    using Microsoft.VisualStudio.Editor;  
    using Microsoft.VisualStudio.Text.Editor;  
    using Microsoft.VisualStudio.TextManager.Interop;  
    
    
  2. In the adornment layer definition, change the name of the AdornmentLayer from KeyBindingTest to PurpleCornerBox.

    [Export(typeof(AdornmentLayerDefinition))]  
    [Name("PurpleCornerBox")]  
    [Order(After = PredefinedAdornmentLayers.Selection, Before = PredefinedAdornmentLayers.Text)]  
    public AdornmentLayerDefinition editorAdornmentLayer;  
    
  3. To get the text view adapter, you must import the IVsEditorAdaptersFactoryService.

    [Import(typeof(IVsEditorAdaptersFactoryService))]  
    internal IVsEditorAdaptersFactoryService editorFactory = null;  
    
    
  4. Change the TextViewCreated method so that it adds the KeyBindingCommandFilter.

    public void TextViewCreated(IWpfTextView textView)  
    {  
        AddCommandFilter(textView, new KeyBindingCommandFilter(textView));  
    }  
    
  5. The AddCommandFilter handler gets the text view adapter and adds the command filter.

    void AddCommandFilter(IWpfTextView textView, KeyBindingCommandFilter commandFilter)  
    {  
        if (commandFilter.m_added == false)  
        {  
            //get the view adapter from the editor factory  
            IOleCommandTarget next;   
            IVsTextView view = editorFactory.GetViewAdapter(textView);  
    
            int hr = view.AddCommandFilter(commandFilter, out next);  
    
            if (hr == VSConstants.S_OK)  
            {      
                commandFilter.m_added = true;  
                 //you'll need the next target for Exec and QueryStatus   
                if (next != null)  
                commandFilter.m_nextTarget = next;  
            }  
        }  
    }  
    

Making the Adornment Appear on Every Line

The original adornment appeared on every character ‘a’ in a text file. Now that we have changed the code to add the adornment in response to the ‘+’ character, it adds the adornment only on the line where the ‘+’ is typed. We can change the adornment code so that the adornment once more appears on every ‘a’.

In the KeyBindingTest.cs file, change the CreateVisuals() method to iterate through all the lines in the view to decorate the ‘a’ character.

private void CreateVisuals(ITextViewLine line)  
{  
    IWpfTextViewLineCollection textViewLines = this.view.TextViewLines;  
  
    foreach (ITextViewLine textViewLine in textViewLines)  
    {  
        if (textViewLine.ToString().Contains("a"))  
        {  
            // Loop through each character, and place a box around any 'a'   
            for (int charIndex = textViewLine.Start; charIndex < textViewLine.End; charIndex++)  
            {  
                if (this.view.TextSnapshot[charIndex] == 'a')  
                {  
                    SnapshotSpan span = new SnapshotSpan(this.view.TextSnapshot, Span.FromBounds(charIndex, charIndex + 1));  
                    Geometry geometry = textViewLines.GetMarkerGeometry(span);  
                    if (geometry != null)  
                    {  
                        var drawing = new GeometryDrawing(this.brush, this.pen, geometry);  
                        drawing.Freeze();  
  
                        var drawingImage = new DrawingImage(drawing);  
                        drawingImage.Freeze();  
  
                        var image = new Image  
                        {  
                            Source = drawingImage,  
                        };  
  
                        // Align the image with the top of the bounds of the text geometry  
                        Canvas.SetLeft(image, geometry.Bounds.Left);  
                        Canvas.SetTop(image, geometry.Bounds.Top);  
  
                        this.layer.AddAdornment(AdornmentPositioningBehavior.TextRelative, span, null, image, null);  
                    }  
                }  
            }  
        }  
    }  
}  

Building and Testing the Code

  1. Build the KeyBindingTest solution and run it in the experimental instance.

  2. Create or open a text file. Type some words containing the character ‘a’, and then type + anywhere in the text view.

    A purple square should appear on every ‘a’ character in the file.