Partager via


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.

Prerequisites

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

Creating a MEF Project

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 Assets tab contains a MEF Component content type and that Project is set to the name of the project.

  4. Save and close source.extension.vsixmanifest.

  5. Delete the existing class files.

Defining a TextMarkerTag

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
    
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Linq;
    using System.Threading;
    using System.Windows.Media;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Classification;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Operations;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
    
  3. Create a class that inherits from TextMarkerTag and name it HighlightWordTag.

    Friend Class HighlightWordTag
        Inherits TextMarkerTag
    
    internal class HighlightWordTag : 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:

    <Export(GetType(EditorFormatDefinition))>
    <Name("MarkerFormatDefinition/HighlightWordFormatDefinition")>
    <UserVisible(True)>
    Friend Class HighlightWordFormatDefinition
        Inherits MarkerFormatDefinition
    
    [Export(typeof(EditorFormatDefinition))]
    [Name("MarkerFormatDefinition/HighlightWordFormatDefinition")]
    [UserVisible(true)]
    internal class HighlightWordFormatDefinition : 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
    
    public HighlightWordFormatDefinition()
    {
        this.BackgroundColor = Colors.LightBlue;
        this.ForegroundColor = Colors.DarkBlue;
        this.DisplayName = "Highlight Word";
        this.ZOrder = 5;
    }
    
  6. In the constructor for HighlightWordTag, pass in the name of the format definition you just created.

    Public Sub New()
        MyBase.New("MarkerFormatDefinition/HighlightWordFormatDefinition")
    End Sub
    
    public HighlightWordTag() : base("MarkerFormatDefinition/HighlightWordFormatDefinition") { }
    

Implementing an ITagger

The next step is to implement the ITagger<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<T> of type HighlightWordTag, and name it HighlightWordTagger.

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

    Private _View As ITextView
    Private Property View() As ITextView
        Get 
            Return _View
        End Get 
        Set(ByVal value As ITextView)
            _View = value
        End Set 
    End Property 
    Private _SourceBuffer As ITextBuffer
    Private Property SourceBuffer() As ITextBuffer
        Get 
            Return _SourceBuffer
        End Get 
        Set(ByVal value As ITextBuffer)
            _SourceBuffer = value
        End Set 
    End Property 
    Private _TextSearchService As ITextSearchService
    Private Property TextSearchService() As ITextSearchService
        Get 
            Return _TextSearchService
        End Get 
        Set(ByVal value As ITextSearchService)
            _TextSearchService = value
        End Set 
    End Property 
    Private _TextStructureNavigator As ITextStructureNavigator
    Private Property TextStructureNavigator() As ITextStructureNavigator
        Get 
            Return _TextStructureNavigator
        End Get 
        Set(ByVal value As ITextStructureNavigator)
            _TextStructureNavigator = value
        End Set 
    End Property 
    Private _WordSpans As NormalizedSnapshotSpanCollection
    Private Property WordSpans() As NormalizedSnapshotSpanCollection
        Get 
            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)
        Get 
            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
        Get 
            Return _RequestedPoint
        End Get 
        Set(ByVal value As SnapshotPoint)
            _RequestedPoint = value
        End Set 
    End Property 
    Private updateLock As New Object()
    
    ITextView View { get; set; }
    ITextBuffer SourceBuffer { get; set; }
    ITextSearchService TextSearchService { get; set; }
    ITextStructureNavigator TextStructureNavigator { get; set; }
    NormalizedSnapshotSpanCollection WordSpans { get; set; }
    SnapshotSpan? CurrentWord { get; set; }
    SnapshotPoint RequestedPoint { get; set; }
    object updateLock = 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
    
    public HighlightWordTagger(ITextView view, ITextBuffer sourceBuffer, ITextSearchService textSearchService,
    ITextStructureNavigator textStructureNavigator)
    {
        this.View = view;
        this.SourceBuffer = sourceBuffer;
        this.TextSearchService = textSearchService;
        this.TextStructureNavigator = textStructureNavigator;
        this.WordSpans = new NormalizedSnapshotSpanCollection();
        this.CurrentWord = null;
        this.View.Caret.PositionChanged += CaretPositionChanged;
        this.View.LayoutChanged += ViewLayoutChanged;
    }
    
  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
            UpdateAtCaretPosition(View.Caret.Position)
        End If 
    End Sub 
    
    Private Sub CaretPositionChanged(ByVal sender As Object, ByVal e As CaretPositionChangedEventArgs)
        UpdateAtCaretPosition(e.NewPosition)
    End Sub
    
    void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
    {
        // If a new snapshot wasn't generated, then skip this layout 
        if (e.NewSnapshot != e.OldSnapshot)
        {
            UpdateAtCaretPosition(View.Caret.Position);
        }
    }
    
    void CaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
    {
        UpdateAtCaretPosition(e.NewPosition);
    }
    
  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
    
    public event EventHandler<SnapshotSpanEventArgs> 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
        UpdateWordAdornments()
    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 
            Else 
                ' 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
    
        wordSpans.AddRange(TextSearchService.FindAll(findData))
    
        '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
    
    void UpdateAtCaretPosition(CaretPosition caretPosition)
    {
        SnapshotPoint? point = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity);
    
        if (!point.HasValue)
            return;
    
        // 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
            && CurrentWord.Value.Snapshot == View.TextSnapshot
            && point.Value >= CurrentWord.Value.Start
            && point.Value <= CurrentWord.Value.End)
        {
            return;
        }
    
        RequestedPoint = point.Value;
        UpdateWordAdornments();
    }
    
    void UpdateWordAdornments()
    {
        SnapshotPoint currentRequest = RequestedPoint;
        List<SnapshotSpan> wordSpans = new List<SnapshotSpan>();
        //Find all words in the buffer like the one the caret is on
        TextExtent word = TextStructureNavigator.GetExtentOfWord(currentRequest);
        bool foundWord = true;
        //If we've selected something not worth highlighting, we might have missed a "word" by a little bit
        if (!WordExtentIsValid(currentRequest, word))
        {
            //Before we retry, make sure it is worthwhile 
            if (word.Span.Start != currentRequest
                 || currentRequest == currentRequest.GetContainingLine().Start
                 || char.IsWhiteSpace((currentRequest - 1).GetChar()))
            {
                foundWord = false;
            }
            else
            {
                // 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 (!WordExtentIsValid(currentRequest, word))
                    foundWord = false;
            }
        }
    
        if (!foundWord)
        {
            //If we couldn't find a word, clear out the existing markers
            SynchronousUpdate(currentRequest, new NormalizedSnapshotSpanCollection(), null);
            return;
        }
    
        SnapshotSpan currentWord = word.Span;
        //If this is the current word, and the caret moved within a word, we're done. 
        if (CurrentWord.HasValue && currentWord == CurrentWord)
            return;
    
        //Find the new spans
        FindData findData = new FindData(currentWord.GetText(), currentWord.Snapshot);
        findData.FindOptions = FindOptions.WholeWord | FindOptions.MatchCase;
    
        wordSpans.AddRange(TextSearchService.FindAll(findData));
    
        //If another change hasn't happened, do a real update 
        if (currentRequest == RequestedPoint)
            SynchronousUpdate(currentRequest, new NormalizedSnapshotSpanCollection(wordSpans), currentWord);
    }
    static bool WordExtentIsValid(SnapshotPoint currentRequest, TextExtent word)
    {
        return word.IsSignificant
            && currentRequest.Snapshot.GetText(word.Span).Any(c => char.IsLetter(c));
    }
    
  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
    
    void SynchronousUpdate(SnapshotPoint currentRequest, NormalizedSnapshotSpanCollection newSpans, SnapshotSpan? newCurrentWord)
    {
        lock (updateLock)
        {
            if (currentRequest != RequestedPoint)
                return;
    
            WordSpans = newSpans;
            CurrentWord = newCurrentWord;
    
            var tempEvent = TagsChanged;
            if (tempEvent != null)
                tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(SourceBuffer.CurrentSnapshot, 0, SourceBuffer.CurrentSnapshot.Length)));
        }
    }
    
  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<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()))
        Next 
        Return List
    End Function
    
    public IEnumerable<ITagSpan<HighlightWordTag>> GetTags(NormalizedSnapshotSpanCollection spans)
    {
        if (CurrentWord == null)
            yield break;
    
        // Hold on to a "snapshot" of the word spans and current word, so that we maintain the same
        // collection throughout
        SnapshotSpan currentWord = CurrentWord.Value;
        NormalizedSnapshotSpanCollection wordSpans = WordSpans;
    
        if (spans.Count == 0 || wordSpans.Count == 0)
            yield break;
    
        // 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 != wordSpans[0].Snapshot)
        {
            wordSpans = new NormalizedSnapshotSpanCollection(
                wordSpans.Select(span => span.TranslateTo(spans[0].Snapshot, SpanTrackingMode.EdgeExclusive)));
    
            currentWord = currentWord.TranslateTo(spans[0].Snapshot, SpanTrackingMode.EdgeExclusive);
        }
    
        // First, yield back the word the cursor is under (if it overlaps) 
        // Note that we'll yield back the same word again in the wordspans collection; 
        // the duplication here is expected. 
        if (spans.OverlapsWith(new NormalizedSnapshotSpanCollection(currentWord)))
            yield return new TagSpan<HighlightWordTag>(currentWord, new HighlightWordTag());
    
        // Second, yield all the other words in the file 
        foreach (SnapshotSpan span in NormalizedSnapshotSpanCollection.Overlap(spans, wordSpans))
        {
            yield return new TagSpan<HighlightWordTag>(span, new HighlightWordTag());
        }
    }
    

Creating a Tagger Provider

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

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.

    <Export(GetType(IViewTaggerProvider))>
    <ContentType("text")>
    <TagType(GetType(TextMarkerTag))>
    Friend Class HighlightWordTaggerProvider
        Implements IViewTaggerProvider
    
    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class HighlightWordTaggerProvider : 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
        Get 
            Return _TextSearchService
        End Get 
        Set(ByVal value As ITextSearchService)
            _TextSearchService = value
        End Set 
    End Property 
    
    Private _TextStructureNavigatorSelector As ITextStructureNavigatorSelectorService
    <Import()>
    Friend Property TextStructureNavigatorSelector() As ITextStructureNavigatorSelectorService
        Get 
            Return _TextStructureNavigatorSelector
        End Get 
        Set(ByVal value As ITextStructureNavigatorSelectorService)
            _TextStructureNavigatorSelector = value
        End Set 
    End Property
    
    [Import]
    internal ITextSearchService TextSearchService { get; set; }
    
    [Import]
    internal ITextStructureNavigatorSelectorService TextStructureNavigatorSelector { get; set; }
    
  3. Implement the CreateTagger<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
    
    public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
    {
        //provide highlighting only on the top buffer 
        if (textView.TextBuffer != buffer)
            return null;
    
        ITextStructureNavigator textStructureNavigator =
            TextStructureNavigatorSelector.GetTextStructureNavigator(buffer);
    
        return new HighlightWordTagger(textView, buffer, TextSearchService, textStructureNavigator) as ITagger<T>;
    }
    

Building and Testing the Code

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.

See Also

Tasks

Walkthrough: Linking a Content Type to a File Name Extension