Walkthrough: Creating a Margin Glyph
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 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.
Creating a MEF Project
Create a C# VSIX project. (In the New Project dialog, select Visual C# / Extensibility, then VSIX Project.) Name the solution
TodoGlyphTest
.Add an Editor Classifier project item. For more information, see Creating an Extension with an Editor Item Template.
Delete the existing class files.
Defining the Glyph
Define a glyph by implementing the IGlyphFactory interface.
To define the glyph
Add a class file and name it
TodoGlyphFactory
.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;
Imports System.ComponentModel.Composition Imports System.Windows Imports System.Windows.Shapes Imports System.Windows.Media Imports System.Windows.Controls Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Editor Imports Microsoft.VisualStudio.Text.Formatting Imports Microsoft.VisualStudio.Text.Tagging Imports Microsoft.VisualStudio.Utilities
Add a class named
TodoGlyphFactory
that implements IGlyphFactory.internal class TodoGlyphFactory : IGlyphFactory
Friend Class TodoGlyphFactory Implements IGlyphFactory
Add a private field that defines the dimensions of the glyph.
const double m_glyphSize = 16.0;
Const m_glyphSize As Double = 16.0
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; }
Public Function GenerateGlyph(ByVal line As IWpfTextViewLine, ByVal tag As IGlyphTag) As System.Windows.UIElement Implements IGlyphFactory.GenerateGlyph ' Ensure we can draw a glyph for this marker. If tag Is Nothing OrElse Not (TypeOf tag Is TodoTag) Then Return Nothing End If Dim ellipse As Ellipse = New Ellipse() ellipse.Fill = Brushes.LightBlue ellipse.StrokeThickness = 2 ellipse.Stroke = Brushes.DarkBlue ellipse.Height = m_glyphSize ellipse.Width = m_glyphSize Return ellipse End Function
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
<Export(GetType(IGlyphFactoryProvider)), Name("TodoGlyph"), Order(After:="VsTextMarker"), ContentType("code"), TagType(GetType(TodoTag))> Friend NotInheritable Class TodoGlyphFactoryProvider Implements IGlyphFactoryProvider
Implement the GetGlyphFactory method by instantiating the
TodoGlyphFactory
.public IGlyphFactory GetGlyphFactory(IWpfTextView view, IWpfTextViewMargin margin) { return new TodoGlyphFactory(); }
Public Function GetGlyphFactory(ByVal view As IWpfTextView, ByVal margin As IWpfTextViewMargin) As IGlyphFactory Implements IGlyphFactoryProvider.GetGlyphFactory Return New TodoGlyphFactory() End Function
Defining a Todo Tag and Tagger
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
Add a new class file to the project and name it
TodoTagger
.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;
Imports System Imports System.Collections.Generic Imports System.ComponentModel.Composition Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Tagging Imports Microsoft.VisualStudio.Text.Editor Imports Microsoft.VisualStudio.Text.Classification Imports Microsoft.VisualStudio.Utilities
Add a class named
TodoTag
.internal class TodoTag : IGlyphTag
Friend Class TodoTag Implements IGlyphTag Public Sub New() MyBase.New() End Sub End Class
Modify the class named
TodoTagger
that implements ITagger<T> of typeTodoTag
.internal class TodoTagger : ITagger<TodoTag>
Friend Class TodoTagger Implements ITagger(Of TodoTag)
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";
Private m_classifier As IClassifier Private Const m_searchText As String = "todo"
Add a constructor that sets the classifier.
internal TodoTagger(IClassifier classifier) { m_classifier = classifier; }
Friend Sub New(ByVal classifier As IClassifier) m_classifier = classifier End Sub
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()); } } } } }
Private Function GetTags(ByVal spans As NormalizedSnapshotSpanCollection) As IEnumerable(Of ITagSpan(Of TodoTag)) Implements ITagger(Of TodoTag).GetTags Dim list As List(Of ITagSpan(Of TodoTag)) list = New List(Of ITagSpan(Of TodoTag))() For Each span As SnapshotSpan In spans 'look at each classification span \ For Each classification As ClassificationSpan In m_classifier.GetClassificationSpans(span) 'if the classification is a comment If classification.ClassificationType.Classification.ToLower().Contains("comment") Then 'if the word "todo" is in the comment, 'create a new TodoTag TagSpan Dim index As Integer = classification.Span.GetText().ToLower().IndexOf(m_searchText) If index <> -1 Then list.Add(New TagSpan(Of TodoTag)(New SnapshotSpan(classification.Span.Start + index, m_searchText.Length), New TodoTag())) End If End If Next classification Next span Return list End Function
Declare a
TagsChanged
event.public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
Public Event TagsChanged(ByVal sender As Object, ByVal e As Microsoft.VisualStudio.Text.SnapshotSpanEventArgs) Implements Microsoft.VisualStudio.Text.Tagging.ITagger(Of TodoTag).TagsChanged
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
<Export(GetType(ITaggerProvider)), ContentType("code"), TagType(GetType(TodoTag))> Friend Class TodoTaggerProvider Implements ITaggerProvider
Import the IClassifierAggregatorService.
[Import] internal IClassifierAggregatorService AggregatorService;
<Import()> Friend AggregatorService As IClassifierAggregatorService
Implement the CreateTagger 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>; }
Public Function CreateTagger(Of T As Microsoft.VisualStudio.Text.Tagging.ITag)(ByVal buffer As Microsoft.VisualStudio.Text.ITextBuffer) As Microsoft.VisualStudio.Text.Tagging.ITagger(Of T) Implements Microsoft.VisualStudio.Text.Tagging.ITaggerProvider.CreateTagger If buffer Is Nothing Then Throw New ArgumentNullException("buffer") End If Return TryCast(New TodoTagger(AggregatorService.GetClassifier(buffer)), ITagger(Of T)) End Function
Building and Testing the Code
To test this code, build the TodoGlyphTest solution and run it in the experimental instance.
To build and test the TodoGlyphTest solution
Build the solution.
Run the project by pressing F5. A second instance of Visual Studio is instantiated.
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.)
Open a code file that has comments. Add the word "todo" to one of the comment sections.
A light blue circle that has a dark blue outline should appear in the indicator margin to the left of the code window.