Procédure pas à pas : afficher les accolades correspondantes

Implémentez des fonctionnalités basées sur la langue, telles que les accolades correspondantes en définissant les accolades que vous souhaitez mettre en correspondance, et en ajoutant une balise de marqueur de texte aux accolades correspondantes lorsque le trait est sur l’une des accolades. Vous pouvez définir des accolades dans le contexte d’une langue, définir votre propre extension de nom de fichier et le type de contenu, et appliquer les balises à ce type ou appliquer les balises à un type de contenu existant (par exemple, « text »). La procédure pas à pas suivante montre comment appliquer des balises de correspondance d’accolades au type de contenu « text ».

Créer un projet MEF (Managed Extensibility Framework)

Pour créer un projet MEF

  1. Créez un projet Classifieur d’éditeur. Nommez la solution BraceMatchingTest.

  2. Ajoutez un modèle d’élément Classifieur d’éditeur au projet. Pour plus d’informations, consultez Créer une extension avec un modèle d’élément d’éditeur.

  3. Supprimez les fichiers de classe existants.

Implémenter un balisage correspondant à l’accolade

Pour obtenir un effet de mise en surbrillance de l’accolades qui ressemble à celui utilisé dans Visual Studio, vous pouvez implémenter un balisage de type TextMarkerTag. Le code suivant montre comment définir le balisage pour les paires d’accolades à n’importe quel niveau d’imbrication. Dans cet exemple, les paires d’accolades de [] et {} sont définies dans le constructeur du balisage, mais dans une implémentation de langage complète, les paires d’accolades pertinentes sont définies dans la spécification du langage.

Pour implémenter un balisage correspondant à l’accolade

  1. Ajoutez un fichier de classe et nommez-le BraceMatching.

  2. Importez les espaces de noms suivants.

    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. Définissez une classe BraceMatchingTagger qui hérite du ITagger<T> type TextMarkerTag.

    internal class BraceMatchingTagger : ITagger<TextMarkerTag>
    
  4. Ajoutez des propriétés pour la vue de texte, la mémoire tampon source, le point de instantané actuel, ainsi qu’un ensemble de paires d’accolades.

    ITextView View { get; set; }
    ITextBuffer SourceBuffer { get; set; }
    SnapshotPoint? CurrentChar { get; set; }
    private Dictionary<char, char> m_braceList;
    
  5. Dans le constructeur du balisage, définissez les propriétés et abonnez-vous aux événements PositionChanged de modification d’affichage et LayoutChanged. Dans cet exemple, à des fins d’illustration, les paires correspondantes sont également définies dans le constructeur.

    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. Dans le cadre de l’implémentation ITagger<T> , déclarez un événement TagsChanged.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  7. Les gestionnaires d’événements mettent à jour la position d’insertion actuelle de la CurrentChar propriété et déclenchent l’événement 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. Implémentez la GetTags méthode pour faire correspondre les accolades lorsque le caractère actuel est une accolade ouverte ou lorsque le caractère précédent est une accolade proche, comme dans Visual Studio. Lorsque la correspondance est trouvée, cette méthode instancie deux balises, une pour l’accolade ouverte et une pour l’accolades fermées.

    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. Les méthodes privées suivantes recherchent l’accolade correspondante à n’importe quel niveau d’imbrication. La première méthode recherche le caractère de fermeture qui correspond au caractère ouvert :

    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. La méthode d’assistance suivante recherche le caractère ouvert qui correspond à un caractère de fermeture :

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

Implémenter un fournisseur de balisage correspondant à l’accolade

Outre l’implémentation d’un taggeur, vous devez également implémenter et exporter un fournisseur de balisage. Dans ce cas, le type de contenu du fournisseur est « text ». Par conséquent, la correspondance entre accolades s’affiche dans tous les types de fichiers texte, mais une implémentation complète applique uniquement la correspondance d’accolades à un type de contenu spécifique.

Pour implémenter un fournisseur de balisage correspondant à l’accolade

  1. Déclarez un fournisseur de balisage qui hérite de IViewTaggerProvider, nommez-le BraceMatchingTaggerProvider, puis exportez-le avec un ContentTypeAttribute « texte » et un TagTypeAttribute de TextMarkerTag.

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class BraceMatchingTaggerProvider : IViewTaggerProvider
    
  2. Implémentez la CreateTagger méthode pour instancier un 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>;
    }
    

Générer et tester le code

Pour tester ce code, générez la solution BraceMatchingTest et exécutez-la dans l’instance expérimentale.

Pour générer et tester la solution BraceMatchingTest

  1. Générez la solution.

  2. Lorsque vous exécutez ce projet dans le débogueur, une deuxième instance de Visual Studio est démarrée.

  3. Créez un fichier texte et tapez du texte incluant des accolades correspondantes.

    hello {
    goodbye}
    
    {}
    
    {hello}
    
  4. Lorsque vous positionnez le caret avant une accolade ouverte, cette accolade et l’accolades correspondantes doivent être mises en surbrillance. Lorsque vous positionnez le curseur juste après l’accolade de fermeture, cette accolade et l’accolade ouverte correspondante doivent être mises en surbrillance.