Export (0) Print
Expand All

Walkthrough: Highlighting Text

You can add different visual effects to the editor by creating Managed Extensibility Framework (MEF) component parts. This walkthrough shows how to highlight every occurrence of the current word in a text file. If a word occurs more than one time in a text file, and you position the caret in one occurrence, every occurrence is highlighted.

To follow this walkthrough, you must install the Visual Studio 2013 SDK. For more information, see Visual Studio Software Development Kit (SDK).

To create a MEF project

  1. Create an Editor Classifier project. Name the solution HighlightWordTest.

  2. Open the source.extension.vsixmanifest file in the VSIX Manifest Editor.

  3. Make sure that the Content heading contains a MEF Component content type and that the Path is set to HighlightWordTest.dll.

  4. Save and close source.extension.vsixmanifest.

  5. Delete the existing class files.

The first step in highlighting text is to subclass TextMarkerTag and define its appearance.

To define a TextMarkerTag and a MarkerFormatDefinition

  1. Add a class file and name it HighlightWordTag.

  2. Import the following namespaces.

    Imports System
    Imports System.Collections.Generic
    Imports System.ComponentModel.Composition
    Imports System.Linq
    Imports System.Threading
    Imports System.Windows.Media
    Imports Microsoft.VisualStudio.Text
    Imports Microsoft.VisualStudio.Text.Classification
    Imports Microsoft.VisualStudio.Text.Editor
    Imports Microsoft.VisualStudio.Text.Operations
    Imports Microsoft.VisualStudio.Text.Tagging
    Imports Microsoft.VisualStudio.Utilities
  3. Create a class that inherits from TextMarkerTag and name it HighlightWordTag.

    Friend Class HighlightWordTag
        Inherits TextMarkerTag
  4. Create a second class that inherits from MarkerFormatDefinition, and name it HighlightWordFormatDefinition. In order to use this format definition for your tag, you must export it with the following attributes:

    Friend Class HighlightWordFormatDefinition
        Inherits MarkerFormatDefinition
  5. In the constructor for HighlightWordFormatDefinition, define its display name and appearance. The Background property defines the fill color, while the Foreground property defines the border color.

    Public Sub New()
        Me.BackgroundColor = Colors.LightBlue
        Me.ForegroundColor = Colors.DarkBlue
        Me.DisplayName = "Highlight Word" 
        Me.ZOrder = 5
    End Sub
  6. In the constructor for HighlightWordTag, pass in the name of the format definition you just created.

    Public Sub New()
    End Sub

The next step is to implement the ITagger(Of T) interface. This interface assigns, to a given text buffer, tags that provide text highlighting and other visual effects.

To implement a tagger

  1. Create a class that implements ITagger(Of T) of type HighlightWordTag, and name it HighlightWordTagger.

    Friend Class HighlightWordTagger
        Implements ITagger(Of HighlightWordTag)
  2. Add the following private fields and properties to the class:

    Private _View As ITextView
    Private Property View() As ITextView
            Return _View
        End Get 
        Set(ByVal value As ITextView)
            _View = value
        End Set 
    End Property 
    Private _SourceBuffer As ITextBuffer
    Private Property SourceBuffer() As ITextBuffer
            Return _SourceBuffer
        End Get 
        Set(ByVal value As ITextBuffer)
            _SourceBuffer = value
        End Set 
    End Property 
    Private _TextSearchService As ITextSearchService
    Private Property TextSearchService() As ITextSearchService
            Return _TextSearchService
        End Get 
        Set(ByVal value As ITextSearchService)
            _TextSearchService = value
        End Set 
    End Property 
    Private _TextStructureNavigator As ITextStructureNavigator
    Private Property TextStructureNavigator() As ITextStructureNavigator
            Return _TextStructureNavigator
        End Get 
        Set(ByVal value As ITextStructureNavigator)
            _TextStructureNavigator = value
        End Set 
    End Property 
    Private _WordSpans As NormalizedSnapshotSpanCollection
    Private Property WordSpans() As NormalizedSnapshotSpanCollection
            Return _WordSpans
        End Get 
        Set(ByVal value As NormalizedSnapshotSpanCollection)
            _WordSpans = value
        End Set 
    End Property 
    Private _CurrentWord As System.Nullable(Of SnapshotSpan)
    Private Property CurrentWord() As System.Nullable(Of SnapshotSpan)
            Return _CurrentWord
        End Get 
        Set(ByVal value As System.Nullable(Of SnapshotSpan))
            _CurrentWord = value
        End Set 
    End Property 
    Private _RequestedPoint As SnapshotPoint
    Private Property RequestedPoint() As SnapshotPoint
            Return _RequestedPoint
        End Get 
        Set(ByVal value As SnapshotPoint)
            _RequestedPoint = value
        End Set 
    End Property 
    Private updateLock As New Object()
  3. Add a constructor that initializes the properties listed earlier and adds LayoutChanged and PositionChanged event handlers.

    Public Sub New(ByVal view As ITextView, ByVal sourceBuffer As ITextBuffer, ByVal textSearchService As ITextSearchService, ByVal textStructureNavigator As ITextStructureNavigator)
        Me.View = view
        Me.SourceBuffer = sourceBuffer
        Me.TextSearchService = textSearchService
        Me.TextStructureNavigator = textStructureNavigator
        Me.WordSpans = New NormalizedSnapshotSpanCollection()
        Me.CurrentWord = Nothing 
        AddHandler Me.View.Caret.PositionChanged, AddressOf CaretPositionChanged
        AddHandler Me.View.LayoutChanged, AddressOf ViewLayoutChanged
    End Sub
  4. The event handlers both call the UpdateAtCaretPosition method.

    Private Sub ViewLayoutChanged(ByVal sender As Object, ByVal e As TextViewLayoutChangedEventArgs)
        ' If a new snapshot wasn't generated, then skip this layout 
        If e.NewSnapshot IsNot e.OldSnapshot Then
        End If 
    End Sub 
    Private Sub CaretPositionChanged(ByVal sender As Object, ByVal e As CaretPositionChangedEventArgs)
    End Sub
  5. You must also add a TagsChanged event that will be called by the update method.

    Public Event TagsChanged(ByVal sender As Object, ByVal e As SnapshotSpanEventArgs) _
        Implements ITagger(Of HighlightWordTag).TagsChanged
  6. The UpdateAtCaretPosition() method finds every word in the text buffer that is identical to the word where the cursor is positioned and constructs a list of SnapshotSpan objects that correspond to the occurrences of the word. It then calls SynchronousUpdate, which raises the TagsChanged event.

    Private Sub UpdateAtCaretPosition(ByVal caretPosition As CaretPosition)
        Dim point As System.Nullable(Of SnapshotPoint) = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity)
        If Not point.HasValue Then 
            Exit Sub 
        End If 
        ' If the new caret position is still within the current word (and on the same snapshot), we don't need to check it 
        If CurrentWord.HasValue AndAlso CurrentWord.Value.Snapshot Is View.TextSnapshot AndAlso point.Value > CurrentWord.Value.Start AndAlso point.Value < CurrentWord.Value.[End] Then 
            Exit Sub 
        End If
        RequestedPoint = point.Value
    End Sub 
    Private Sub UpdateWordAdornments()
        Dim currentRequest As SnapshotPoint = RequestedPoint
        Dim wordSpans As New List(Of SnapshotSpan)()
        'Find all words in the buffer like the one the caret is on 
        Dim word As TextExtent = TextStructureNavigator.GetExtentOfWord(currentRequest)
        Dim foundWord As Boolean = True 
        'If we've selected something not worth highlighting, we might have missed a "word" by a little bit 
        If Not WordExtentIsValid(currentRequest, word) Then 
            'Before we retry, make sure it is worthwhile 
            If word.Span.Start <> currentRequest OrElse currentRequest = currentRequest.GetContainingLine().Start OrElse Char.IsWhiteSpace((currentRequest - 1).GetChar()) Then
                foundWord = False 
                ' Try again, one character previous.  
                'If the caret is at the end of a word, pick up the word.
                word = TextStructureNavigator.GetExtentOfWord(currentRequest - 1)
                'If the word still isn't valid, we're done 
                If Not WordExtentIsValid(currentRequest, word) Then
                    foundWord = False 
                End If 
            End If 
        End If 
        If Not foundWord Then 
            'If we couldn't find a word, clear out the existing markers
            SynchronousUpdate(currentRequest, New NormalizedSnapshotSpanCollection(), Nothing)
            Exit Sub 
        End If 
        Dim currentWord__1 As SnapshotSpan = word.Span
        'If this is the current word, and the caret moved within a word, we're done. 
        If CurrentWord.HasValue AndAlso currentWord__1 = CurrentWord Then 
            Exit Sub 
        End If 
        'Find the new spans 
        Dim findData As New FindData(currentWord__1.GetText(), currentWord__1.Snapshot)
        findData.FindOptions = FindOptions.WholeWord Or FindOptions.MatchCase
        'If another change hasn't happened, do a real update 
        If currentRequest = RequestedPoint Then
            SynchronousUpdate(currentRequest, New NormalizedSnapshotSpanCollection(wordSpans), currentWord__1)
        End If 
    End Sub 
    Private Shared Function WordExtentIsValid(ByVal currentRequest As SnapshotPoint, ByVal word As TextExtent) As Boolean 
        Return word.IsSignificant AndAlso currentRequest.Snapshot.GetText(word.Span).Any(Function(c) Char.IsLetter(c))
    End Function
  7. The SynchronousUpdate performs a synchronous update on the WordSpans and CurrentWord properties, and raises the TagsChanged event.

    Private Sub SynchronousUpdate(ByVal currentRequest As SnapshotPoint, ByVal newSpans As NormalizedSnapshotSpanCollection, ByVal newCurrentWord As System.Nullable(Of SnapshotSpan))
        SyncLock updateLock
            If currentRequest <> RequestedPoint Then 
                Exit Sub 
            End If
            WordSpans = newSpans
            CurrentWord = newCurrentWord
            RaiseEvent TagsChanged(Me, New SnapshotSpanEventArgs(New SnapshotSpan(SourceBuffer.CurrentSnapshot, 0, SourceBuffer.CurrentSnapshot.Length)))
        End SyncLock 
    End Sub
  8. You must implement the GetTags method. This method takes a collection of SnapshotSpan objects and returns an enumeration of tag spans.

    In C#, implement this method as a yield iterator, which enables lazy evaluation (that is, evaluation of the set only when individual items are accessed) of the tags. In Visual Basic, add the tags to a list and return the list.

    Here the method returns a TagSpan(Of T) object that has a "blue" TextMarkerTag, which provides a blue background.

    Public Function GetTags(ByVal spans As NormalizedSnapshotSpanCollection) As IEnumerable(Of ITagSpan(Of HighlightWordTag)) Implements ITagger(Of HighlightWordTag).GetTags
        If CurrentWord Is Nothing Then 
            Return Nothing 
            Exit Function 
        End If 
        ' Hold on to a "snapshot" of the word spans and current word, so that we maintain the same 
        ' collection throughout 
        Dim currentWord__1 As SnapshotSpan = CurrentWord.Value
        Dim wordSpans__2 As NormalizedSnapshotSpanCollection = WordSpans
        If spans.Count = 0 OrElse WordSpans.Count = 0 Then 
            Return Nothing 
            Exit Function 
        End If 
        ' If the requested snapshot isn't the same as the one our words are on, translate our spans to the expected snapshot 
        If spans(0).Snapshot IsNot wordSpans__2(0).Snapshot Then
            wordSpans__2 = New NormalizedSnapshotSpanCollection(wordSpans__2.[Select](Function(span) span.TranslateTo(spans(0).Snapshot, SpanTrackingMode.EdgeExclusive)))
            currentWord__1 = currentWord__1.TranslateTo(spans(0).Snapshot, SpanTrackingMode.EdgeExclusive)
        End If 
        'in order to emulate the C# yield return functionality, 
        'create a list and add all the relevant spans to it, then return the list 
        Dim list As List(Of TagSpan(Of HighlightWordTag))
        list = New List(Of TagSpan(Of HighlightWordTag))()
        If spans.OverlapsWith(New NormalizedSnapshotSpanCollection(currentWord__1)) Then
            list.Add(New TagSpan(Of HighlightWordTag)(CurrentWord, New HighlightWordTag()))
        End If 
        For Each span As SnapshotSpan In NormalizedSnapshotSpanCollection.Overlap(spans, wordSpans__2)
            list.Add(New TagSpan(Of HighlightWordTag)(span, New HighlightWordTag()))
        Return List
    End Function

To create your tagger, you must implement a IViewTaggerProvider. This class is a MEF component part, so you must set the correct attributes so that this extension is recognized.

Note Note

For more information about MEF, see Managed Extensibility Framework (MEF).

To create a tagger provider

  1. Create a class named HighlightWordTaggerProvider that implements IViewTaggerProvider, and export it with a ContentTypeAttribute of "text" and a TagTypeAttribute of TextMarkerTag.

    Friend Class HighlightWordTaggerProvider
        Implements IViewTaggerProvider
  2. You must import two editor services, the ITextSearchService and the ITextStructureNavigatorSelectorService, to instantiate the tagger.

    Private _TextSearchService As ITextSearchService
    <Import()> _
    Friend Property TextSearchService() As ITextSearchService
            Return _TextSearchService
        End Get 
        Set(ByVal value As ITextSearchService)
            _TextSearchService = value
        End Set 
    End Property 
    Private _TextStructureNavigatorSelector As ITextStructureNavigatorSelectorService
    Friend Property TextStructureNavigatorSelector() As ITextStructureNavigatorSelectorService
            Return _TextStructureNavigatorSelector
        End Get 
        Set(ByVal value As ITextStructureNavigatorSelectorService)
            _TextStructureNavigatorSelector = value
        End Set 
    End Property
  3. Implement the CreateTagger(Of T) method to return an instance of HighlightWordTagger.

    Public Function CreateTagger(Of T As ITag)(ByVal textView As ITextView, ByVal buffer As ITextBuffer) As ITagger(Of T) Implements IViewTaggerProvider.CreateTagger
        'provide highlighting only on the top buffer 
        If textView.TextBuffer IsNot buffer Then 
            Return Nothing 
        End If 
        Dim textStructureNavigator As ITextStructureNavigator = TextStructureNavigatorSelector.GetTextStructureNavigator(buffer)
        Return TryCast(New HighlightWordTagger(textView, buffer, TextSearchService, textStructureNavigator), ITagger(Of T))
    End Function

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

To build and test the HighlightWordTest 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 in which the words are repeated, for example, "hello hello hello".

  4. Position the cursor in one of the occurrences of "hello". Every occurrence should be highlighted in blue.

© 2014 Microsoft