Udostępnij za pośrednictwem


Przewodnik: wyświetlanie pasujących nawiasów klamrowych

Zaimplementuj funkcje oparte na języku, takie jak dopasowywanie nawiasów klamrowych, definiując nawiasy klamrowe, które chcesz dopasować, i dodając tag znacznika tekstu do pasujących nawiasów klamrowych, gdy daszek znajduje się na jednym z nawiasów klamrowych. Nawiasy klamrowe można zdefiniować w kontekście języka, zdefiniować rozszerzenie nazwy pliku i typ zawartości oraz zastosować tagi tylko do tego typu lub zastosować tagi do istniejącego typu zawartości (na przykład "text"). Poniższy przewodnik przedstawia sposób stosowania pasujących tagów nawiasów klamrowych do typu zawartości "text".

Tworzenie projektu zarządzanej struktury rozszerzalności (MEF)

Aby utworzyć projekt MEF

  1. Utwórz projekt Klasyfikatora edytora. Nadaj rozwiązaniu BraceMatchingTestnazwę .

  2. Dodaj szablon elementu Klasyfikator edytora do projektu. Aby uzyskać więcej informacji, zobacz Tworzenie rozszerzenia za pomocą szablonu elementu edytora.

  3. Usuń istniejące pliki klas.

Implementowanie pasującego nawiasu klamrowego

Aby uzyskać efekt wyróżniania nawiasów klamrowych przypominający ten, który jest używany w programie Visual Studio, możesz zaimplementować sztylet typu TextMarkerTag. Poniższy kod pokazuje, jak zdefiniować sztylet dla par nawiasów klamrowych na dowolnym poziomie zagnieżdżania. W tym przykładzie pary nawiasów klamrowych [] i {} są zdefiniowane w konstruktorze tagger, ale w pełnej implementacji języka odpowiednie pary nawiasów klamrowych zostaną zdefiniowane w specyfikacji języka.

Aby zaimplementować pasujący nawias klamrowy

  1. Dodaj plik klasy i nadaj mu nazwę BraceMatching.

  2. Zaimportuj następujące przestrzenie nazw.

    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. Zdefiniuj klasę BraceMatchingTagger dziedziczą z ITagger<T> typu TextMarkerTag.

    internal class BraceMatchingTagger : ITagger<TextMarkerTag>
    
  4. Dodaj właściwości widoku tekstu, bufor źródłowy, bieżący punkt migawki, a także zestaw par nawiasów klamrowych.

    ITextView View { get; set; }
    ITextBuffer SourceBuffer { get; set; }
    SnapshotPoint? CurrentChar { get; set; }
    private Dictionary<char, char> m_braceList;
    
  5. W konstruktorze modułu tagger ustaw właściwości i zasubskrybuj zdarzenia PositionChanged zmiany widoku i LayoutChanged. W tym przykładzie dla celów ilustracyjnych pasujące pary są również zdefiniowane w konstruktorze.

    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. W ramach implementacji ITagger<T> zadeklaruj zdarzenie TagsChanged.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  7. Programy obsługi zdarzeń aktualizują bieżące położenie CurrentChar karetki właściwości i zgłaszają zdarzenie 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. Zaimplementuj metodę GetTags , aby dopasować nawiasy klamrowe, gdy bieżący znak jest otwartym nawiasem klamrowym lub gdy poprzedni znak jest bliskim nawiasem klamrowym, tak jak w programie Visual Studio. Po znalezieniu dopasowania ta metoda tworzy wystąpienie dwóch tagów, jeden dla otwartego nawiasu klamrowego i jeden dla zamykanego nawiasu klamrowego.

    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. Poniższe metody prywatne znajdują pasujący nawias klamrowy na dowolnym poziomie zagnieżdżania. Pierwsza metoda znajduje znak zamknięcia zgodny z otwartym znakiem:

    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. Następująca metoda pomocnika znajduje otwarty znak, który pasuje do bliskiego znaku:

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

Implementowanie dostawcy dopasowywania nawiasów klamrowych

Oprócz implementacji sztyletu należy również zaimplementować i wyeksportować dostawcę taggera. W takim przypadku typ zawartości dostawcy to "tekst". Dlatego dopasowanie nawiasów klamrowych będzie wyświetlane we wszystkich typach plików tekstowych, ale pełną implementację stosuje dopasowywanie nawiasów klamrowych tylko do określonego typu zawartości.

Aby zaimplementować dostawcę dopasowywania nawiasów klamrowych

  1. Zadeklaruj dostawcę tagger, który dziedziczy z IViewTaggerProvider, nadaj mu nazwę BraceMatchingTaggerProvider i wyeksportuj go za pomocą ContentTypeAttribute ciągu "text" i elementu TagTypeAttribute TextMarkerTag.

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class BraceMatchingTaggerProvider : IViewTaggerProvider
    
  2. Zaimplementuj metodę CreateTagger , aby utworzyć wystąpienie elementu 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>;
    }
    

Kompilowanie i testowanie kodu

Aby przetestować ten kod, skompiluj rozwiązanie BraceMatchingTest i uruchom je w wystąpieniu eksperymentalnym.

Aby skompilować i przetestować rozwiązanie BraceMatchingTest

  1. Stwórz rozwiązanie.

  2. Po uruchomieniu tego projektu w debugerze zostanie uruchomione drugie wystąpienie programu Visual Studio.

  3. Utwórz plik tekstowy i wpisz tekst zawierający pasujące nawiasy klamrowe.

    hello {
    goodbye}
    
    {}
    
    {hello}
    
  4. Po ustawieniu karetki przed otwartym nawiasem klamrowym należy zaznaczyć zarówno ten nawias klamrowy, jak i pasujący nawias klamrowy zamykający. Po ustawieniu kursora tuż po zamknięciu nawiasu klamrowego należy zaznaczyć zarówno ten nawias klamrowy, jak i pasujący otwarty nawias klamrowy.