Procedura dettagliata: Visualizzare le parentesi graffe corrispondenti

Implementare funzionalità basate sul linguaggio, ad esempio la corrispondenza delle parentesi graffe definendo le parentesi graffe di cui si vuole trovare la corrispondenza e aggiungendo un tag indicatore di testo alle parentesi graffe corrispondenti quando il punto di inserimento si trova su una delle parentesi graffe. È possibile definire le parentesi graffe nel contesto di un linguaggio, definire l'estensione e il tipo di contenuto del nome file e applicare i tag solo a quel tipo o applicare i tag a un tipo di contenuto esistente,ad esempio "text". La procedura dettagliata seguente illustra come applicare tag corrispondenti alle parentesi graffe al tipo di contenuto "text".

Prerequisiti

A partire Visual Studio 2015, non si installa Visual Studio SDK dall'Area download. È incluso come funzionalità facoltativa nella configurazione Visual Studio configurazione. È anche possibile installare VS SDK in un secondo momento. Per altre informazioni, vedere Installare Visual Studio SDK.

Creare un Managed Extensibility Framework (MEF)

Per creare un progetto MEF

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

  2. Aggiungere un modello di elemento Editor Classifier 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 alle parentesi graffe

Per ottenere un effetto di evidenziazione della 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 del tagger, ma in un'implementazione completa del linguaggio le coppie di parentesi graffe pertinenti vengono definite nella specifica del {} linguaggio.

Per implementare un tagger corrispondente alle parentesi graffe

  1. Aggiungere un file di classe e assegnare al file il nome 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;
    
    Imports System.ComponentModel.Composition
    Imports Microsoft.VisualStudio.Text
    Imports Microsoft.VisualStudio.Text.Editor
    Imports Microsoft.VisualStudio.Text.Tagging
    Imports Microsoft.VisualStudio.Utilities
    
  3. Definire una classe BraceMatchingTagger che eredita da di tipo ITagger<T> TextMarkerTag .

    internal class BraceMatchingTagger : ITagger<TextMarkerTag>
    
    Friend Class BraceMatchingTagger
        Implements ITagger(Of 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;
    
    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. Nel costruttore del tagger impostare le proprietà e sottoscrivere gli eventi di modifica della visualizzazione PositionChanged e LayoutChanged . In questo esempio, a scopo illustrativo, anche le coppie corrispondenti vengono definite 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;
    }
    
    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. Come parte ITagger<T> dell'implementazione, dichiarare un evento TagsChanged.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
    Public Event TagsChanged As EventHandler(Of SnapshotSpanEventArgs) _
        Implements ITagger(Of TextMarkerTag).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)));
    }
    
    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. Implementare il metodo in modo che corrisponda alle parentesi graffe quando il carattere corrente è una parentesi graffa aperta o quando il carattere precedente è una parentesi graffa chiusa, come GetTags 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"));
            }
        }
    }
    
    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. I metodi privati seguenti trovano la parentesi graffa corrispondente a qualsiasi livello di annidamento. Il primo metodo trova il carattere di chiusura che corrisponde 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;
    }
    
    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. 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;
    }
    
    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
    

Implementare un provider di tagger corrispondente alle parentesi graffe

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

Per implementare un provider di tagger corrispondente alle parentesi graffe

  1. Dichiarare un provider di tagger che eredita da , assegnare al provider il nome BraceMatchingTaggerProvider ed esportarlo con IViewTaggerProvider ContentTypeAttribute "text" e TagTypeAttribute con 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. Implementare il CreateTagger metodo per creare un'istanza 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>;
    }
    
    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
    

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 Visual Studio di .

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

    hello {
    goodbye}
    
    {}
    
    {hello}
    
  4. Quando si posiziona il cursore prima di una parentesi graffa aperta, sia la parentesi graffa che la parentesi graffa di chiusura corrispondente devono essere evidenziate. Quando si posiziona il cursore subito dopo la parentesi graffa di chiusura, sia la parentesi graffa aperta che la parentesi graffa aperta corrispondente devono essere evidenziate.

Vedi anche