Walkthrough: Creating a Margin Glyph

 

You can customize the appearance of editor margins by using custom editor extensions. This walkthrough puts a custom glyph on the indicator margin whenever the word "todo" appears in a code comment.

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.

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

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

  3. Delete the existing class files.

Define a glyph by implementing the IGlyphFactory interface.

To define the glyph

  1. Add a class file and name it TodoGlyphFactory.

  2. Add the following using declarations.

    using System.ComponentModel.Composition;
    using System.Windows;
    using System.Windows.Shapes;
    using System.Windows.Media;
    using System.Windows.Controls;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Formatting;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
    
  3. Add a class named TodoGlyphFactory that implements IGlyphFactory.

    internal class TodoGlyphFactory : IGlyphFactory
    
  4. Add a private field that defines the dimensions of the glyph.

    const double m_glyphSize = 16.0;
    

  5. Implement GenerateGlyph by defining the glyph user interface (UI) element. TodoTag is defined later in this walkthrough.

    public UIElement GenerateGlyph(IWpfTextViewLine line, IGlyphTag tag)
    {
        // Ensure we can draw a glyph for this marker.
        if (tag == null || !(tag is TodoTag))
        {
            return null;
        }
    
        System.Windows.Shapes.Ellipse ellipse = new Ellipse();
        ellipse.Fill = Brushes.LightBlue;
        ellipse.StrokeThickness = 2;
        ellipse.Stroke = Brushes.DarkBlue;
        ellipse.Height = m_glyphSize;
        ellipse.Width = m_glyphSize;
    
        return ellipse;
    }
    
  6. Add a class named TodoGlyphFactoryProvider that implements IGlyphFactoryProvider. Export this class with a NameAttribute of "TodoGlyph", an OrderAttribute of After VsTextMarker, a ContentTypeAttribute of "code", and a TagTypeAttribute of TodoTag.

    [Export(typeof(IGlyphFactoryProvider))]
    [Name("TodoGlyph")]
    [Order(After = "VsTextMarker")]
    [ContentType("code")]
    [TagType(typeof(TodoTag))]
    internal sealed class TodoGlyphFactoryProvider : IGlyphFactoryProvider
    
  7. Implement the GetGlyphFactory method by instantiating the TodoGlyphFactory.

    public IGlyphFactory GetGlyphFactory(IWpfTextView view, IWpfTextViewMargin margin)
    {
        return new TodoGlyphFactory();
    }
    

Define the relationship between the UI element that you defined in the previous steps and the indicator margin by creating a tag type and tagger, and exporting it by using a tagger provider.

To define a todo tag and tagger

  1. Add a new class file to the project and name it TodoTagger.

  2. Add the following imports.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Classification;
    using Microsoft.VisualStudio.Utilities;
    
  3. Add a class named TodoTag.

    internal class TodoTag : IGlyphTag
    
  4. Modify the class named TodoTagger that implements ITagger<T> of type TodoTag.

    internal class TodoTagger : ITagger<TodoTag>
    
  5. To the TodoTagger class, add private fields for an IClassifier and for the text to find in the classification spans.

    private IClassifier m_classifier;
    private const string m_searchText = "todo";
    
  6. Add a constructor that sets the classifier.

    internal TodoTagger(IClassifier classifier)
    {
        m_classifier = classifier;
    }
    
  7. Implement the GetTags method by finding all the classification spans whose names include the word "comment" and whose text includes the search text. Whenever the search text is found, yield back a new TagSpan<T> of type TodoTag.

    IEnumerable<ITagSpan<TodoTag>> ITagger<TodoTag>.GetTags(NormalizedSnapshotSpanCollection spans)
    {
        foreach (SnapshotSpan span in spans)
        {
            //look at each classification span \
            foreach (ClassificationSpan classification in m_classifier.GetClassificationSpans(span))
            {
                //if the classification is a comment
                if (classification.ClassificationType.Classification.ToLower().Contains("comment"))
                {
                    //if the word "todo" is in the comment,
                    //create a new TodoTag TagSpan
                    int index = classification.Span.GetText().ToLower().IndexOf(m_searchText);
                    if (index != -1)
                    {
                        yield return new TagSpan<TodoTag>(new SnapshotSpan(classification.Span.Start + index, m_searchText.Length), new TodoTag());
                    }
                }
            }
        }
    }
    
  8. Declare a TagsChanged event.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  9. Add a class named TodoTaggerProvider that implements ITaggerProvider, and export it with a ContentTypeAttribute of "code" and a TagTypeAttribute of TodoTag.

    [Export(typeof(ITaggerProvider))]
    [ContentType("code")]
    [TagType(typeof(TodoTag))]
    class TodoTaggerProvider : ITaggerProvider
    
  10. Import the IClassifierAggregatorService.

    [Import]
    internal IClassifierAggregatorService AggregatorService;
    
  11. Implement the CreateTagger<T> method by instantiating the TodoTagger.

    public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
    {
        if (buffer == null)
        {
            throw new ArgumentNullException("buffer");
        }
    
        return new TodoTagger(AggregatorService.GetClassifier(buffer)) as ITagger<T>;
    }
    

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

To build and test the TodoGlyphTest solution

  1. Build the solution.

  2. Run the project by pressing F5. A second instance of Visual Studio is instantiated.

  3. Make sure that the indicator margin is showing. (On the Tools menu, click Options. On the Text Editor page, make sure that Indicator margin is selected.)

  4. Open a code file that has comments. Add the word "todo" to one of the comment sections.

  5. A light blue circle that has a dark blue outline should appear in the indicator margin to the left of the code window.

Show: