Walkthrough: Outlining

 

The new home for Visual Studio documentation is Visual Studio 2017 Documentation on docs.microsoft.com.

The latest version of this topic can be found at Walkthrough: Outlining.

You can implement language-based features such as outlining by defining the kinds of text regions you want to expand or collapse. You can define regions in the context of a language service, or you can define your own file name extension and content type and apply the region definition to only that type, or you can apply the region definitions to an existing content type (such as "text"). This walkthrough shows how to define and display outlining regions.

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.

To create a MEF project

  1. Create an VSIX project. Name the solution OutlineRegionTest.

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

  3. Delete the existing class files.

Outlining regions are marked by a kind of tag (OutliningRegionTag). This tag provides the standard outlining behavior. The outlined region can be expanded or collapsed. The outlined region is marked by a PLUS SIGN if it is collapsed or a MINUS SIGN if it is expanded, and the expanded region is demarcated by a vertical line.

The following steps show how to define a tagger that creates outlining regions for all the regions that are delimited by "[" and "]".

To implement an outlining tagger

  1. Add a class file and name it OutliningTagger.

  2. Import the following namespaces.

    Imports System
    Imports System.Collections.Generic
    Imports System.Linq
    Imports System.Text
    Imports System.ComponentModel.Composition
    Imports Microsoft.VisualStudio.Text.Outlining
    Imports Microsoft.VisualStudio.Text.Tagging
    Imports Microsoft.VisualStudio.Utilities
    Imports Microsoft.VisualStudio.Text
    

  3. Create a class named OutliningTagger, and have it implement ITagger<T>:

    Friend NotInheritable Class OutliningTagger
        Implements ITagger(Of IOutliningRegionTag)
    

  4. Add some fields to track the text buffer and snapshot and to accumulate the sets of lines that should be tagged as outlining regions. This code includes a list of Region objects (to be defined later) that represent the outlining regions.

        'the characters that start the outlining region
        Private startHide As String = "["
        'the characters that end the outlining region
        Private endHide As String = "]"
        'the characters that are displayed when the region is collapsed
        Private ellipsis As String = "..."
        'the contents of the tooltip for the collapsed span
        Private hoverText As String = "hover text"
        Private buffer As ITextBuffer
        Private snapshot As ITextSnapshot
        Private regions As List(Of Region)
    

  5. Add a tagger constructor that initializes the fields, parses the buffer, and adds an event handler to the Changed event.

        Public Sub New(ByVal buffer As ITextBuffer)
            Me.buffer = buffer
            Me.snapshot = buffer.CurrentSnapshot
            Me.regions = New List(Of Region)()
            Me.ReParse()
            AddHandler Me.buffer.Changed, AddressOf BufferChanged
        End Sub
    

  6. Implement the GetTags method, which instantiates the tag spans. This example assumes that the spans in the NormalizedSpanCollection passed in to the method are contiguous, although this may not always be the case. This method instantiates a new tag span for each of the outlining regions.

        Public Function GetTags(ByVal spans As NormalizedSnapshotSpanCollection) As IEnumerable(Of ITagSpan(Of IOutliningRegionTag)) Implements ITagger(Of Microsoft.VisualStudio.Text.Tagging.IOutliningRegionTag).GetTags
            If spans.Count = 0 Then
                Return Nothing
                Exit Function
            End If
            Dim currentRegions As List(Of Region) = Me.regions
            Dim currentSnapshot As ITextSnapshot = Me.snapshot
            Dim entire As SnapshotSpan = New SnapshotSpan(spans(0).Start, spans(spans.Count - 1).[End]).TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive)
            Dim startLineNumber As Integer = entire.Start.GetContainingLine().LineNumber
            Dim endLineNumber As Integer = entire.[End].GetContainingLine().LineNumber
    
            Dim list As List(Of ITagSpan(Of IOutliningRegionTag))
            list = New List(Of ITagSpan(Of IOutliningRegionTag))()
    
            For Each region In currentRegions
                If region.StartLine <= endLineNumber AndAlso region.EndLine >= startLineNumber Then
                    Dim startLine = currentSnapshot.GetLineFromLineNumber(region.StartLine)
                    Dim endLine = currentSnapshot.GetLineFromLineNumber(region.EndLine)
    
                    'the region starts at the beginning of the "[", and goes until the *end* of the line that contains the "]".
                    list.Add(New TagSpan(Of IOutliningRegionTag)(New SnapshotSpan(startLine.Start + region.StartOffset, endLine.End),
                    New OutliningRegionTag(False, False, ellipsis, hoverText)))
                End If
            Next
            Return list
        End Function
    

  7. Declare a TagsChanged event handler.

        Public Event TagsChanged As EventHandler(Of SnapshotSpanEventArgs) Implements ITagger(Of IOutliningRegionTag).TagsChanged
    

  8. Add a BufferChanged event handler that responds to Changed events by parsing the text buffer.

        Private Sub BufferChanged(ByVal sender As Object, ByVal e As TextContentChangedEventArgs)
            ' If this isn't the most up-to-date version of the buffer, then ignore it for now (we'll eventually get another change event).
            If e.After IsNot buffer.CurrentSnapshot Then
                Exit Sub
            End If
            Me.ReParse()
        End Sub
    

  9. Add a method that parses the buffer. The example given here is for illustration only. It synchronously parses the buffer into nested outlining regions.

        Private Sub ReParse()
            Dim newSnapshot As ITextSnapshot = buffer.CurrentSnapshot
            Dim newRegions As New List(Of Region)()
    
            'keep the current (deepest) partial region, which will have
            ' references to any parent partial regions.
            Dim currentRegion As PartialRegion = Nothing
    
            For Each line In newSnapshot.Lines
                Dim regionStart As Integer = -1
                Dim text As String = line.GetText()
    
                'lines that contain a "[" denote the start of a new region.
                If text.IndexOf(startHide, StringComparison.Ordinal) <> -1 Then
                    regionStart = text.IndexOf(startHide, StringComparison.Ordinal)
                    Dim currentLevel As Integer = If((currentRegion IsNot Nothing), currentRegion.Level, 1)
                    Dim newLevel As Integer
                    If Not TryGetLevel(text, regionStart, newLevel) Then
                        newLevel = currentLevel + 1
                    End If
    
                    'levels are the same and we have an existing region;
                    'end the current region and start the next
                    If currentLevel = newLevel AndAlso currentRegion IsNot Nothing Then
                        Dim newRegion = New Region()
                        newRegion.Level = currentRegion.Level
                        newRegion.StartLine = currentRegion.StartLine
                        newRegion.StartOffset = currentRegion.StartOffset
                        newRegion.EndLine = line.LineNumber
                        newRegions.Add(newRegion)
    
                        currentRegion = New PartialRegion()
                        currentRegion.Level = newLevel
                        currentRegion.StartLine = line.LineNumber
                        currentRegion.StartOffset = regionStart
                        currentRegion.PartialParent = currentRegion.PartialParent
    
                    Else
                        'this is a new (sub)region
                        currentRegion = New PartialRegion()
                        currentRegion.Level = newLevel
                        currentRegion.StartLine = line.LineNumber
                        currentRegion.StartOffset = regionStart
                        currentRegion.PartialParent = currentRegion
                    End If
                    'lines that contain "]" denote the end of a region
                ElseIf (text.IndexOf(endHide, StringComparison.Ordinal)) <> -1 Then
                    regionStart = text.IndexOf(endHide, StringComparison.Ordinal)
                    Dim currentLevel As Integer = If((currentRegion IsNot Nothing), currentRegion.Level, 1)
                    Dim closingLevel As Integer
                    If Not TryGetLevel(text, regionStart, closingLevel) Then
                        closingLevel = currentLevel
                    End If
    
                    'the regions match
                    If currentRegion IsNot Nothing AndAlso currentLevel = closingLevel Then
                        Dim newRegion As Region
                        newRegion = New Region()
                        newRegion.Level = currentLevel
                        newRegion.StartLine = currentRegion.StartLine
                        newRegion.StartOffset = currentRegion.StartOffset
                        newRegion.EndLine = line.LineNumber
                        newRegions.Add(newRegion)
    
                        currentRegion = currentRegion.PartialParent
                    End If
                End If
            Next
            'determine the changed span, and send a changed event with the new spans
            Dim oldSpans As New List(Of Span)(Me.regions.[Select](Function(r) AsSnapshotSpan(r, Me.snapshot).TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive).Span))
            Dim newSpans As New List(Of Span)(newRegions.[Select](Function(r) AsSnapshotSpan(r, newSnapshot).Span))
    
            Dim oldSpanCollection As New NormalizedSpanCollection(oldSpans)
            Dim newSpanCollection As New NormalizedSpanCollection(newSpans)
    
            'the changed regions are regions that appear in one set or the other, but not both.
            Dim removed As NormalizedSpanCollection = NormalizedSpanCollection.Difference(oldSpanCollection, newSpanCollection)
    
            Dim changeStart As Integer = Integer.MaxValue
            Dim changeEnd As Integer = -1
    
            If removed.Count > 0 Then
                changeStart = removed(0).Start
                changeEnd = removed(removed.Count - 1).[End]
            End If
    
            If newSpans.Count > 0 Then
                changeStart = Math.Min(changeStart, newSpans(0).Start)
                changeEnd = Math.Max(changeEnd, newSpans(newSpans.Count - 1).[End])
            End If
    
            Me.snapshot = newSnapshot
            Me.regions = newRegions
    
            If changeStart <= changeEnd Then
                Dim snap As ITextSnapshot = Me.snapshot
                RaiseEvent TagsChanged(Me, New SnapshotSpanEventArgs(New SnapshotSpan(Me.snapshot, Span.FromBounds(changeStart, changeEnd))))
            End If
        End Sub
    

  10. The following helper method gets an integer that represents the level of the outlining, such that 1 is the leftmost brace pair.

        Private Shared Function TryGetLevel(ByVal text As String, ByVal startIndex As Integer, ByRef level As Integer) As Boolean
            level = -1
            If text.Length > startIndex + 3 Then
                If Integer.TryParse(text.Substring(startIndex + 1), level) Then
                    Return True
                End If
            End If
    
            Return False
        End Function
    

  11. The following helper method translates a Region (defined later in this topic) into a SnapshotSpan.

        Private Shared Function AsSnapshotSpan(ByVal region As Region, ByVal snapshot As ITextSnapshot) As SnapshotSpan
            Dim startLine = snapshot.GetLineFromLineNumber(region.StartLine)
            Dim endLine = If((region.StartLine = region.EndLine), startLine, snapshot.GetLineFromLineNumber(region.EndLine))
            Return New SnapshotSpan(startLine.Start + region.StartOffset, endLine.[End])
        End Function
    

  12. The following code is for illustration only. It defines a PartialRegion class that contains the line number and offset of the start of an outlining region, and also a reference to the parent region (if any). This enables the parser to set up nested outlining regions. A derived Region class contains a reference to the line number of the end of an outlining region.

        Private Class PartialRegion
            Private _StartLine As Integer
            Public Property StartLine() As Integer
                Get
                    Return _StartLine
                End Get
                Set(ByVal value As Integer)
                    _StartLine = value
                End Set
            End Property
            Private _StartOffset As Integer
            Public Property StartOffset() As Integer
                Get
                    Return _StartOffset
                End Get
                Set(ByVal value As Integer)
                    _StartOffset = value
                End Set
            End Property
            Private _Level As Integer
            Public Property Level() As Integer
                Get
                    Return _Level
                End Get
                Set(ByVal value As Integer)
                    _Level = value
                End Set
            End Property
            Private _PartialParent As PartialRegion
            Public Property PartialParent() As PartialRegion
                Get
                    Return _PartialParent
                End Get
                Set(ByVal value As PartialRegion)
                    _PartialParent = value
                End Set
            End Property
        End Class
    
        Private Class Region
            Inherits PartialRegion
            Private _EndLine As Integer
            Public Property EndLine() As Integer
                Get
                    Return _EndLine
                End Get
                Set(ByVal value As Integer)
                    _EndLine = value
                End Set
            End Property
        End Class
    

You must export a tagger provider for your tagger. The tagger provider creates an OutliningTagger for a buffer of the "text" content type, or else returns an OutliningTagger if the buffer already has one.

To implement a tagger provider

  1. Create a class named OutliningTaggerProvider that implements ITaggerProvider, and export it with the ContentType and TagType attributes.

    <Export(GetType(ITaggerProvider))> _
    <TagType(GetType(IOutliningRegionTag))> _
    <ContentType("text")> _
    Friend NotInheritable Class OutliningTaggerProvider
        Implements ITaggerProvider
    

  2. Implement the CreateTagger<T> method by adding an OutliningTagger to the properties of the buffer.

        Public Function CreateTagger(Of T As ITag)(ByVal buffer As ITextBuffer) As ITagger(Of T) Implements ITaggerProvider.CreateTagger
            'create a single tagger for each buffer.
            Dim sc As Func(Of ITagger(Of T)) = Function() TryCast(New OutliningTagger(buffer), ITagger(Of T))
            Return buffer.Properties.GetOrCreateSingletonProperty(Of ITagger(Of T))(sc)
        End Function
    

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

To build and test the OutlineRegionTest 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. Type some text that includes both the opening brace and the closing brace.

    [  
       Hello  
    ]  
    
    
  4. There should be an outlining region that includes both braces. You should be able to click the MINUS SIGN to the left of the open brace to collapse the outlining region. When the region is collapsed, the ellipsis symbol (...) should appear to the left of the collapsed region, and a popup containing the text hover text should appear when you move the pointer over the ellipsis.

Walkthrough: Linking a Content Type to a File Name Extension

Show: