Поделиться через


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

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

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

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

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

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

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

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

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

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

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

  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;
    
  3. Определите класс BraceMatchingTagger , наследующий от ITagger<T> типа TextMarkerTag.

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

    ITextView View { get; set; }
    ITextBuffer SourceBuffer { get; set; }
    SnapshotPoint? CurrentChar { get; set; }
    private Dictionary<char, char> m_braceList;
    
  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;
    }
    
  6. В рамках реализации объявите ITagger<T> событие TagsChanged.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  7. Обработчики событий обновляют текущее положение CurrentChar свойства и вызывают событие TagsChanged.

    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)));
    }
    
  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"));
            }
        }
    }
    
  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;
    }
    
  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;
    }
    

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

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

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

  1. Объявите поставщик тегов, наследующий от IViewTaggerProvider, присвойте ему имя BraceMatchingTaggerProvider и экспортируйте его с текстом ContentTypeAttribute и элементом TagTypeAttribute TextMarkerTag.

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class BraceMatchingTaggerProvider : IViewTaggerProvider
    
  2. CreateTagger Реализуйте метод для создания экземпляра BraceMatchingTagger.

    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>;
    }
    

Сборка и проверка кода

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

Создание и тестирование решения BraceMatchingTest

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

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

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

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