Пошаговое руководство. Отображение парных скобок

Реализуйте языковые функции, такие как Парные фигурные скобки, путем определения фигурных скобок, которые необходимо сопоставить, и добавления тега текстового маркера к парным фигурным скобкам, если курсор находится на одной из фигурных скобок. Можно определить фигурные скобки в контексте языка, определить собственное расширение имени файла и тип содержимого, применить теги только к этому типу или применить теги к существующему типу содержимого (например, "Text"). В следующем пошаговом руководстве показано, как применить теги сопоставления фигурных скобок к типу содержимого text.

Предварительные требования

начиная с Visual Studio 2015, пакет SDK для Visual Studio не устанавливается из центра загрузки. он входит в состав Visual Studio установки в качестве дополнительного компонента. Пакет SDK для VS можно установить и позже. дополнительные сведения см. в статье установка пакета SDK для Visual Studio.

создание проекта Managed Extensibility Framework (MEF)

Создание проекта MEF

  1. Создайте проект классификатора редактора. Назовите решение BraceMatchingTest.

  2. Добавьте шаблон элемента классификатора редактора в проект. Дополнительные сведения: Создание расширения с помощью шаблона элемента редактора.

  3. Удалите файлы существующих классов.

Реализация тегов для сопоставления фигурных скобок

чтобы получить маркер выделения фигурных скобок, похожий на тот, который используется в Visual Studio, можно реализовать средство создания тегов типа TextMarkerTag . В следующем коде показано, как определить средство создания тегов для пар скобок на любом уровне вложенности. В этом примере пары фигурных скобок [] и {} определены в конструкторе тегов, но в полной реализации языка соответствующие пары фигурных скобок будут определены в спецификации языка.

Реализация тегов для сопоставления фигурных скобок

  1. Добавьте файл класса и назовите его цветность.

  2. Импортируйте следующие пространства имен.

    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
    
    Imports System.ComponentModel.Composition
    Imports Microsoft.VisualStudio.Text
    Imports Microsoft.VisualStudio.Text.Editor
    Imports Microsoft.VisualStudio.Text.Tagging
    Imports Microsoft.VisualStudio.Utilities
    
  3. Определите класс BraceMatchingTagger , наследующий от ITagger<T> типа TextMarkerTag .

    internal class BraceMatchingTagger : ITagger<TextMarkerTag>
    
    Friend Class BraceMatchingTagger
        Implements ITagger(Of TextMarkerTag)
    
  4. Добавление свойств для текстового представления, исходного буфера, текущей точки моментального снимка, а также набора пар фигурных скобок.

    ITextView View { get; set; }
    ITextBuffer SourceBuffer { get; set; }
    SnapshotPoint? CurrentChar { get; set; }
    private Dictionary<char, char> m_braceList;
    
    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 _CurrentChar As System.Nullable(Of SnapshotPoint)
    Private Property CurrentChar() As System.Nullable(Of SnapshotPoint)
        Get
            Return _CurrentChar
        End Get
        Set(ByVal value As System.Nullable(Of SnapshotPoint))
            _CurrentChar = value
        End Set
    End Property
    Private m_braceList As Dictionary(Of Char, Char)
    
  5. В конструкторе тегов укажите свойства и подпишитесь на события изменения представления PositionChanged и LayoutChanged . В этом примере для демонстрационных целей в конструкторе также определяются совпадающие пары.

    internal BraceMatchingTagger(ITextView view, ITextBuffer sourceBuffer)
    {
        //here the keys are the open braces, and the values are the close braces
        m_braceList = new Dictionary<char, char>();
        m_braceList.Add('{', '}');
        m_braceList.Add('[', ']');
        m_braceList.Add('(', ')');
        this.View = view;
        this.SourceBuffer = sourceBuffer;
        this.CurrentChar = null;
    
        this.View.Caret.PositionChanged += CaretPositionChanged;
        this.View.LayoutChanged += ViewLayoutChanged;
    }
    
    Friend Sub New(ByVal view As ITextView, ByVal sourceBuffer As ITextBuffer)
        'here the keys are the open braces, and the values are the close braces
        m_braceList = New Dictionary(Of Char, Char)()
        m_braceList.Add("{"c, "}"c)
        m_braceList.Add("["c, "]"c)
        m_braceList.Add("("c, ")"c)
        Me.View = view
        Me.SourceBuffer = sourceBuffer
        Me.CurrentChar = Nothing
    
        AddHandler Me.View.Caret.PositionChanged, AddressOf Me.CaretPositionChanged
        AddHandler Me.View.LayoutChanged, AddressOf Me.ViewLayoutChanged
    End Sub
    
  6. В рамках ITagger<T> реализации объявите событие тагсчанжед.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
    Public Event TagsChanged As EventHandler(Of SnapshotSpanEventArgs) _
        Implements ITagger(Of TextMarkerTag).TagsChanged
    
  7. Обработчики событий обновляют текущую позиции курсора CurrentChar Свойства и вызывают событие тагсчанжед.

    void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
    {
        if (e.NewSnapshot != e.OldSnapshot) //make sure that there has really been a change
        {
            UpdateAtCaretPosition(View.Caret.Position);
        }
    }
    
    void CaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
    {
        UpdateAtCaretPosition(e.NewPosition);
    }
    void UpdateAtCaretPosition(CaretPosition caretPosition)
    {
        CurrentChar = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity);
    
        if (!CurrentChar.HasValue)
            return;
    
        var tempEvent = TagsChanged;
        if (tempEvent != null)
            tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(SourceBuffer.CurrentSnapshot, 0,
                SourceBuffer.CurrentSnapshot.Length)));
    }
    
    Private Sub ViewLayoutChanged(ByVal sender As Object, ByVal e As TextViewLayoutChangedEventArgs)
        If e.NewSnapshot IsNot e.OldSnapshot Then
            'make sure that there has really been a change
            UpdateAtCaretPosition(View.Caret.Position)
        End If
    End Sub
    
    Private Sub CaretPositionChanged(ByVal sender As Object, ByVal e As CaretPositionChangedEventArgs)
        UpdateAtCaretPosition(e.NewPosition)
    End Sub
    
    Private Sub UpdateAtCaretPosition(ByVal caretPosition As CaretPosition)
        CurrentChar = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity)
    
        If Not CurrentChar.HasValue Then
            Exit Sub
        End If
    
        RaiseEvent TagsChanged(Me, New SnapshotSpanEventArgs(New SnapshotSpan(SourceBuffer.CurrentSnapshot, 0, SourceBuffer.CurrentSnapshot.Length)))
    End Sub
    
  8. Реализуйте GetTags метод для сопоставления фигурных скобок, если текущий символ является открывающей фигурной скобкой или если предыдущий символ является закрывающей фигурной скобкой, как в Visual Studio. При обнаружении совпадения этот метод создает два тега: один для открывающей фигурной скобки, а другой — для закрывающей.

    public IEnumerable<ITagSpan<TextMarkerTag>> GetTags(NormalizedSnapshotSpanCollection spans)
    {
        if (spans.Count == 0)   //there is no content in the buffer
            yield break;
    
        //don't do anything if the current SnapshotPoint is not initialized or at the end of the buffer
        if (!CurrentChar.HasValue || CurrentChar.Value.Position >= CurrentChar.Value.Snapshot.Length)
            yield break;
    
        //hold on to a snapshot of the current character
        SnapshotPoint currentChar = CurrentChar.Value;
    
        //if the requested snapshot isn't the same as the one the brace is on, translate our spans to the expected snapshot
        if (spans[0].Snapshot != currentChar.Snapshot)
        {
            currentChar = currentChar.TranslateTo(spans[0].Snapshot, PointTrackingMode.Positive);
        }
    
        //get the current char and the previous char
        char currentText = currentChar.GetChar();
        SnapshotPoint lastChar = currentChar == 0 ? currentChar : currentChar - 1; //if currentChar is 0 (beginning of buffer), don't move it back
        char lastText = lastChar.GetChar();
        SnapshotSpan pairSpan = new SnapshotSpan();
    
        if (m_braceList.ContainsKey(currentText))   //the key is the open brace
        {
            char closeChar;
            m_braceList.TryGetValue(currentText, out closeChar);
            if (BraceMatchingTagger.FindMatchingCloseChar(currentChar, currentText, closeChar, View.TextViewLines.Count, out pairSpan) == true)
            {
                yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(currentChar, 1), new TextMarkerTag("blue"));
                yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("blue"));
            }
        }
        else if (m_braceList.ContainsValue(lastText))    //the value is the close brace, which is the *previous* character 
        {
            var open = from n in m_braceList
                       where n.Value.Equals(lastText)
                       select n.Key;
            if (BraceMatchingTagger.FindMatchingOpenChar(lastChar, (char)open.ElementAt<char>(0), lastText, View.TextViewLines.Count, out pairSpan) == true)
            {
                yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(lastChar, 1), new TextMarkerTag("blue"));
                yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("blue"));
            }
        }
    }
    
    Public Function GetTags(ByVal spans As NormalizedSnapshotSpanCollection) As IEnumerable(Of ITagSpan(Of TextMarkerTag)) Implements ITagger(Of Microsoft.VisualStudio.Text.Tagging.TextMarkerTag).GetTags
        If spans.Count = 0 Then
            'there is no content in the buffer
            Exit Function
        End If
    
        'don't do anything if the current SnapshotPoint is not initialized or at the end of the buffer
        If Not CurrentChar.HasValue OrElse CurrentChar.Value.Position >= CurrentChar.Value.Snapshot.Length Then
            Exit Function
        End If
    
        'hold on to a snapshot of the current character
        Dim currentChar__1 As SnapshotPoint = CurrentChar.Value
    
        'if the requested snapshot isn't the same as the one the brace is on, translate our spans to the expected snapshot
        If spans(0).Snapshot IsNot currentChar__1.Snapshot Then
            currentChar__1 = currentChar__1.TranslateTo(spans(0).Snapshot, PointTrackingMode.Positive)
        End If
    
        'get the current char and the previous char
        Dim currentText As Char = currentChar__1.GetChar()
        Dim lastChar As SnapshotPoint = If(CInt(currentChar__1) = 0, currentChar__1, currentChar__1 - 1)
        'if currentChar is 0 (beginning of buffer), don't move it back
        Dim lastText As Char = lastChar.GetChar()
        Dim pairSpan As New SnapshotSpan()
    
        If m_braceList.ContainsKey(currentText) Then
            'the key is the open brace
            Dim closeChar As Char
            m_braceList.TryGetValue(currentText, closeChar)
            If BraceMatchingTagger.FindMatchingCloseChar(currentChar__1, currentText, closeChar, View.TextViewLines.Count, pairSpan) = True Then
                Exit Function
            End If
        ElseIf m_braceList.ContainsValue(lastText) Then
            'the value is the close brace, which is the *previous* character 
            Dim open = From n In m_braceList _
                Where n.Value.Equals(lastText) _
                Select n.Key
            If BraceMatchingTagger.FindMatchingOpenChar(lastChar, CChar(open.ElementAt(0)), lastText, View.TextViewLines.Count, pairSpan) = True Then
                Exit Function
            End If
        End If
    End Function
    
  9. Следующие закрытые методы находят парную фигурную скобку на любом уровне вложенности. Первый метод находит символ закрытия, соответствующий открывающему символу:

    private static bool FindMatchingCloseChar(SnapshotPoint startPoint, char open, char close, int maxLines, out SnapshotSpan pairSpan)
    {
        pairSpan = new SnapshotSpan(startPoint.Snapshot, 1, 1);
        ITextSnapshotLine line = startPoint.GetContainingLine();
        string lineText = line.GetText();
        int lineNumber = line.LineNumber;
        int offset = startPoint.Position - line.Start.Position + 1;
    
        int stopLineNumber = startPoint.Snapshot.LineCount - 1;
        if (maxLines > 0)
            stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines);
    
        int openCount = 0;
        while (true)
        {
            //walk the entire line
            while (offset < line.Length)
            {
                char currentChar = lineText[offset];
                if (currentChar == close) //found the close character
                {
                    if (openCount > 0)
                    {
                        openCount--;
                    }
                    else    //found the matching close
                    {
                        pairSpan = new SnapshotSpan(startPoint.Snapshot, line.Start + offset, 1);
                        return true;
                    }
                }
                else if (currentChar == open) // this is another open
                {
                    openCount++;
                }
                offset++;
            }
    
            //move on to the next line
            if (++lineNumber > stopLineNumber)
                break;
    
            line = line.Snapshot.GetLineFromLineNumber(lineNumber);
            lineText = line.GetText();
            offset = 0;
        }
    
        return false;
    }
    
    Private Shared Function FindMatchingCloseChar(ByVal startPoint As SnapshotPoint, ByVal open As Char, ByVal close As Char, ByVal maxLines As Integer, ByRef pairSpan As SnapshotSpan) As Boolean
        pairSpan = New SnapshotSpan(startPoint.Snapshot, 1, 1)
        Dim line As ITextSnapshotLine = startPoint.GetContainingLine()
        Dim lineText As String = line.GetText()
        Dim lineNumber As Integer = line.LineNumber
        Dim offset As Integer = startPoint.Position - line.Start.Position + 1
    
        Dim stopLineNumber As Integer = startPoint.Snapshot.LineCount - 1
        If maxLines > 0 Then
            stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines)
        End If
    
        Dim openCount As Integer = 0
        While True
            'walk the entire line
            While offset < line.Length
                Dim currentChar As Char = lineText(offset)
                If currentChar = close Then
                    'found the close character
                    If openCount > 0 Then
                        openCount -= 1
                    Else
                        'found the matching close
                        pairSpan = New SnapshotSpan(startPoint.Snapshot, line.Start + offset, 1)
                        Return True
                    End If
                ElseIf currentChar = open Then
                    ' this is another open
                    openCount += 1
                End If
                offset += 1
            End While
    
            'move on to the next line
            If System.Threading.Interlocked.Increment(lineNumber) > stopLineNumber Then
                Exit While
            End If
    
            line = line.Snapshot.GetLineFromLineNumber(lineNumber)
            lineText = line.GetText()
            offset = 0
        End While
    
        Return False
    End Function
    
  10. Следующий вспомогательный метод находит открытый символ, соответствующий закрывающему символу:

    private static bool FindMatchingOpenChar(SnapshotPoint startPoint, char open, char close, int maxLines, out SnapshotSpan pairSpan)
    {
        pairSpan = new SnapshotSpan(startPoint, startPoint);
    
        ITextSnapshotLine line = startPoint.GetContainingLine();
    
        int lineNumber = line.LineNumber;
        int offset = startPoint - line.Start - 1; //move the offset to the character before this one
    
        //if the offset is negative, move to the previous line
        if (offset < 0)
        {
            line = line.Snapshot.GetLineFromLineNumber(--lineNumber);
            offset = line.Length - 1;
        }
    
        string lineText = line.GetText();
    
        int stopLineNumber = 0;
        if (maxLines > 0)
            stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines);
    
        int closeCount = 0;
    
        while (true)
        {
            // Walk the entire line
            while (offset >= 0)
            {
                char currentChar = lineText[offset];
    
                if (currentChar == open)
                {
                    if (closeCount > 0)
                    {
                        closeCount--;
                    }
                    else // We've found the open character
                    {
                        pairSpan = new SnapshotSpan(line.Start + offset, 1); //we just want the character itself
                        return true;
                    }
                }
                else if (currentChar == close)
                {
                    closeCount++;
                }
                offset--;
            }
    
            // Move to the previous line
            if (--lineNumber < stopLineNumber)
                break;
    
            line = line.Snapshot.GetLineFromLineNumber(lineNumber);
            lineText = line.GetText();
            offset = line.Length - 1;
        }
        return false;
    }
    
    Private Shared Function FindMatchingOpenChar(ByVal startPoint As SnapshotPoint, ByVal open As Char, ByVal close As Char, ByVal maxLines As Integer, ByRef pairSpan As SnapshotSpan) As Boolean
        pairSpan = New SnapshotSpan(startPoint, startPoint)
    
        Dim line As ITextSnapshotLine = startPoint.GetContainingLine()
    
        Dim lineNumber As Integer = line.LineNumber
        Dim offset As Integer = startPoint - line.Start - 1
        'move the offset to the character before this one
        'if the offset is negative, move to the previous line
        If offset < 0 Then
            line = line.Snapshot.GetLineFromLineNumber(System.Threading.Interlocked.Decrement(lineNumber))
            offset = line.Length - 1
        End If
    
        Dim lineText As String = line.GetText()
    
        Dim stopLineNumber As Integer = 0
        If maxLines > 0 Then
            stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines)
        End If
    
        Dim closeCount As Integer = 0
    
        While True
            ' Walk the entire line
            While offset >= 0
                Dim currentChar As Char = lineText(offset)
    
                If currentChar = open Then
                    If closeCount > 0 Then
                        closeCount -= 1
                    Else
                        ' We've found the open character
                        pairSpan = New SnapshotSpan(line.Start + offset, 1)
                        'we just want the character itself
                        Return True
                    End If
                ElseIf currentChar = close Then
                    closeCount += 1
                End If
                offset -= 1
            End While
    
            ' Move to the previous line
            If System.Threading.Interlocked.Decrement(lineNumber) < stopLineNumber Then
                Exit While
            End If
    
            line = line.Snapshot.GetLineFromLineNumber(lineNumber)
            lineText = line.GetText()
            offset = line.Length - 1
        End While
        Return False
    End Function
    

Реализация поставщика тегов для сопоставления фигурных скобок

Помимо реализации средства создания тегов, необходимо также реализовать и экспортировать поставщик тегов. В этом случае тип содержимого поставщика — Text. Таким образом, сопоставление фигурных скобок будет отображаться во всех типах текстовых файлов, но более полное применение применяет сопоставление фигурных скобок только к конкретному типу содержимого.

Реализация поставщика тегов для сопоставления фигурных скобок

  1. Объявите поставщик тегов, наследующий от IViewTaggerProvider , назовите его брацематчингтагжерпровидер и экспортируйте его с помощью ContentTypeAttribute "Text" и объекта TagTypeAttribute TextMarkerTag .

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class BraceMatchingTaggerProvider : IViewTaggerProvider
    
    <Export(GetType(IViewTaggerProvider))> _
    <ContentType("text")> _
    <TagType(GetType(TextMarkerTag))> _
    Friend Class BraceMatchingTaggerProvider
        Implements IViewTaggerProvider
    
  2. Реализуйте CreateTagger метод для создания экземпляра брацематчингтагжер.

    public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
    {
        if (textView == null)
            return null;
    
        //provide highlighting only on the top-level buffer
        if (textView.TextBuffer != buffer)
            return null;
    
        return new BraceMatchingTagger(textView, buffer) as ITagger<T>;
    }
    
    Public Function CreateTagger(Of T As ITag)(ByVal textView As ITextView, ByVal buffer As ITextBuffer) As ITagger(Of T) Implements IViewTaggerProvider.CreateTagger
        If textView Is Nothing Then
            Return Nothing
        End If
    
        'provide highlighting only on the top-level buffer
        If textView.TextBuffer IsNot buffer Then
            Return Nothing
        End If
    
        Return TryCast(New BraceMatchingTagger(textView, buffer), ITagger(Of T))
    End Function
    

Сборка и тестирование кода

Чтобы протестировать этот код, создайте решение Брацематчингтест и запустите его в экспериментальном экземпляре.

Создание и тестирование решения Брацематчингтест

  1. Постройте решение.

  2. при запуске этого проекта в отладчике запускается второй экземпляр Visual Studio.

  3. Создайте текстовый файл и введите текст, содержащий Парные фигурные скобки.

    hello {
    goodbye}
    
    {}
    
    {hello}
    
  4. При размещении курсора перед открывающей фигурной скобкой следует выделить обе фигурные скобки и соответствующую закрывающую фигурную скобку. При размещении курсора сразу после закрывающей скобки должны быть выделены как фигурные скобки, так и парные открывающие скобки.

См. также раздел