Condividi tramite


Procedura dettagliata: Visualizzare parentesi graffe corrispondenti

Implementare funzionalità basate sul linguaggio, ad esempio la corrispondenza tra parentesi graffe definendo le parentesi graffe che si desidera associare e aggiungendo un tag di marcatore di testo alle parentesi graffe corrispondenti quando il cursore si trova su una delle parentesi graffe. È possibile definire parentesi graffe nel contesto di una lingua, definire il proprio tipo di contenuto e estensione del nome file e applicare i tag solo a tale tipo o applicare i tag a un tipo di contenuto esistente ,ad esempio "text". La procedura dettagliata seguente illustra come applicare tag di corrispondenza tra parentesi graffe al tipo di contenuto "text".

Creare un progetto MEF (Managed Extensibility Framework)

Per creare un progetto MEF

  1. Creare un progetto di classificatore editor. Assegnare alla soluzione il nome BraceMatchingTest.

  2. Aggiungere un modello di elemento classificatore dell'editor al progetto. Per altre informazioni, vedere Creare un'estensione con un modello di elemento dell'editor.

  3. Eliminare i file di classe esistenti.

Implementare un tagger corrispondente tra parentesi graffe

Per ottenere un effetto di evidenziazione parentesi graffa simile a quello usato in Visual Studio, è possibile implementare un tagger di tipo TextMarkerTag. Il codice seguente illustra come definire il tagger per le coppie di parentesi graffe a qualsiasi livello di annidamento. In questo esempio, le coppie di parentesi graffe di [] e {} sono definite nel costruttore tagger, ma in un'implementazione completa del linguaggio, le coppie di parentesi graffe pertinenti verranno definite nella specifica del linguaggio.

Per implementare un tagger corrispondente tra parentesi graffe

  1. Aggiungere un file di classe e denominarlo BraceMatching.

  2. Importare gli spazi dei nomi seguenti.

    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. Definire una classe BraceMatchingTagger che eredita da ITagger<T> di tipo TextMarkerTag.

    internal class BraceMatchingTagger : ITagger<TextMarkerTag>
    
  4. Aggiungere proprietà per la visualizzazione testo, il buffer di origine, il punto di snapshot corrente e anche un set di coppie di parentesi graffe.

    ITextView View { get; set; }
    ITextBuffer SourceBuffer { get; set; }
    SnapshotPoint? CurrentChar { get; set; }
    private Dictionary<char, char> m_braceList;
    
  5. Nel costruttore tagger impostare le proprietà e sottoscrivere gli eventi PositionChanged di modifica della visualizzazione e LayoutChanged. In questo esempio, a scopo illustrativo, le coppie di corrispondenza vengono definite anche nel costruttore.

    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. Come parte dell'implementazione ITagger<T> , dichiarare un evento TagsChanged.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  7. I gestori eventi aggiornano la posizione corrente del cursore della CurrentChar proprietà e generano l'evento 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. Implementare il GetTags metodo in modo che corrisponda alle parentesi graffe quando il carattere corrente è una parentesi graffa aperta o quando il carattere precedente è una parentesi graffa di chiusura, come in Visual Studio. Quando viene trovata la corrispondenza, questo metodo crea un'istanza di due tag, uno per la parentesi graffa aperta e uno per la parentesi graffa di chiusura.

    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. I metodi privati seguenti trovano la parentesi graffa corrispondente a qualsiasi livello di annidamento. Il primo metodo trova il carattere di chiusura corrispondente al carattere aperto:

    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. Il metodo helper seguente trova il carattere aperto che corrisponde a un carattere di chiusura:

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

Implementare un provider tagger corrispondente tra parentesi graffe

Oltre all'implementazione di un tagger, è necessario implementare ed esportare anche un provider di tagger. In questo caso, il tipo di contenuto del provider è "text". Pertanto, la corrispondenza tra parentesi graffe verrà visualizzata in tutti i tipi di file di testo, ma un'implementazione più completa applica la corrispondenza tra parentesi graffe solo a un tipo di contenuto specifico.

Per implementare un provider tagger corrispondente tra parentesi graffe

  1. Dichiarare un provider tagger che eredita da IViewTaggerProvider, denominarlo BraceMatchingTaggerProvider ed esportarlo con un ContentTypeAttribute valore di "text" e un TagTypeAttribute di TextMarkerTag.

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class BraceMatchingTaggerProvider : IViewTaggerProvider
    
  2. Implementare il metodo per creare un'istanza CreateTagger di 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>;
    }
    

Compilare e testare il codice

Per testare questo codice, compilare la soluzione BraceMatchingTest ed eseguirla nell'istanza sperimentale.

Per compilare e testare la soluzione BraceMatchingTest

  1. Compilare la soluzione.

  2. Quando si esegue questo progetto nel debugger, viene avviata una seconda istanza di Visual Studio.

  3. Creare un file di testo e digitare un testo che includa parentesi graffe corrispondenti.

    hello {
    goodbye}
    
    {}
    
    {hello}
    
  4. Quando si posiziona il cursore prima di una parentesi graffa aperta, entrambe le parentesi graffe e la parentesi graffa di chiusura corrispondente devono essere evidenziate. Quando si posiziona il cursore subito dopo la parentesi graffa chiusa, entrambe le parentesi graffe e le parentesi graffe aperte corrispondenti devono essere evidenziate.