Procédure pas à pas : mode Plan

Configurez des fonctionnalités linguistiques telles que le plan en définissant les types de régions de texte que vous souhaitez développer ou réduire. Vous pouvez définir des régions dans le contexte d’un service de langage, ou définir votre propre extension de nom de fichier et type de contenu et appliquer la définition de région à ce type uniquement, ou appliquer les définitions de région à un type de contenu existant (par exemple, « text »). Cette procédure pas à pas montre comment définir et afficher des zones de plan.

Créer un projet MEF (Managed Extensibility Framework)

Pour créer un projet MEF

  1. Créez un projet VSIX. Nommez la solution OutlineRegionTest.

  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 de plan

Les zones de plan sont marquées par un type de balise (OutliningRegionTag). Cette balise fournit le comportement standard du plan. La région décrite peut être développée ou réduite. La région décrite est marquée par un signe Plus (+) s’il est réduit ou moins (-) s’il est développé et la région développée est délimitée par une ligne verticale.

Les étapes suivantes montrent comment définir un balisage qui crée des zones de plan pour toutes les régions délimitées par les crochets ([,]).

Pour implémenter un balisage de plan

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

  2. Importez les espaces de noms suivants.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Text.Outlining;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
    using Microsoft.VisualStudio.Text;
    
  3. Créez une classe nommée OutliningTaggeret mettez-la en œuvre ITagger<T>:

    internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
    
  4. Ajoutez certains champs pour suivre la mémoire tampon de texte et instantané et accumuler les ensembles de lignes qui doivent être étiquetés comme des zones de plan. Ce code inclut une liste d’objets Region (à définir ultérieurement) qui représentent les régions de plan.

    string startHide = "[";     //the characters that start the outlining region
    string endHide = "]";       //the characters that end the outlining region
    string ellipsis = "...";    //the characters that are displayed when the region is collapsed
    string hoverText = "hover text"; //the contents of the tooltip for the collapsed span
    ITextBuffer buffer;
    ITextSnapshot snapshot;
    List<Region> regions;
    
  5. Ajoutez un constructeur de balisage qui initialise les champs, analyse la mémoire tampon et ajoute un gestionnaire d’événements à l’événement Changed .

    public OutliningTagger(ITextBuffer buffer)
    {
        this.buffer = buffer;
        this.snapshot = buffer.CurrentSnapshot;
        this.regions = new List<Region>();
        this.ReParse();
        this.buffer.Changed += BufferChanged;
    }
    
  6. Implémentez la GetTags méthode, qui instancie les étendues de balise. Cet exemple part du principe que les étendues de la NormalizedSpanCollection méthode passées sont contiguës, bien qu’il ne soit pas toujours le cas. Cette méthode instancie une nouvelle étendue de balise pour chacune des régions de plan.

    public IEnumerable<ITagSpan<IOutliningRegionTag>> GetTags(NormalizedSnapshotSpanCollection spans)
    {
        if (spans.Count == 0)
            yield break;
        List<Region> currentRegions = this.regions;
        ITextSnapshot currentSnapshot = this.snapshot;
        SnapshotSpan entire = new SnapshotSpan(spans[0].Start, spans[spans.Count - 1].End).TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive);
        int startLineNumber = entire.Start.GetContainingLine().LineNumber;
        int endLineNumber = entire.End.GetContainingLine().LineNumber;
        foreach (var region in currentRegions)
        {
            if (region.StartLine <= endLineNumber &&
                region.EndLine >= startLineNumber)
            {
                var startLine = currentSnapshot.GetLineFromLineNumber(region.StartLine);
                var endLine = currentSnapshot.GetLineFromLineNumber(region.EndLine);
    
                //the region starts at the beginning of the "[", and goes until the *end* of the line that contains the "]".
                yield return new TagSpan<IOutliningRegionTag>(
                    new SnapshotSpan(startLine.Start + region.StartOffset,
                    endLine.End),
                    new OutliningRegionTag(false, false, ellipsis, hoverText));
            }
        }
    }
    
  7. Déclarez un gestionnaire d’événements TagsChanged .

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  8. Ajoutez un gestionnaire d’événements BufferChanged qui répond aux événements en Changed analysant la mémoire tampon de texte.

    void BufferChanged(object sender, TextContentChangedEventArgs e)
    {
        // If this isn't the most up-to-date version of the buffer, then ignore it for now (we'll eventually get another change event).
        if (e.After != buffer.CurrentSnapshot)
            return;
        this.ReParse();
    }
    
  9. Ajoutez une méthode qui analyse la mémoire tampon. L’exemple donné ici est destiné uniquement à l’illustration. Il analyse de manière synchrone la mémoire tampon dans des régions de plan imbriquées.

    void ReParse()
    {
        ITextSnapshot newSnapshot = buffer.CurrentSnapshot;
        List<Region> newRegions = new List<Region>();
    
        //keep the current (deepest) partial region, which will have
        // references to any parent partial regions.
        PartialRegion currentRegion = null;
    
        foreach (var line in newSnapshot.Lines)
        {
            int regionStart = -1;
            string text = line.GetText();
    
            //lines that contain a "[" denote the start of a new region.
            if ((regionStart = text.IndexOf(startHide, StringComparison.Ordinal)) != -1)
            {
                int currentLevel = (currentRegion != null) ? currentRegion.Level : 1;
                int newLevel;
                if (!TryGetLevel(text, regionStart, out newLevel))
                    newLevel = currentLevel + 1;
    
                //levels are the same and we have an existing region;
                //end the current region and start the next
                if (currentLevel == newLevel && currentRegion != null)
                {
                    newRegions.Add(new Region()
                    {
                        Level = currentRegion.Level,
                        StartLine = currentRegion.StartLine,
                        StartOffset = currentRegion.StartOffset,
                        EndLine = line.LineNumber
                    });
    
                    currentRegion = new PartialRegion()
                    {
                        Level = newLevel,
                        StartLine = line.LineNumber,
                        StartOffset = regionStart,
                        PartialParent = currentRegion.PartialParent
                    };
                }
                //this is a new (sub)region
                else
                {
                    currentRegion = new PartialRegion()
                    {
                        Level = newLevel,
                        StartLine = line.LineNumber,
                        StartOffset = regionStart,
                        PartialParent = currentRegion
                    };
                }
            }
            //lines that contain "]" denote the end of a region
            else if ((regionStart = text.IndexOf(endHide, StringComparison.Ordinal)) != -1)
            {
                int currentLevel = (currentRegion != null) ? currentRegion.Level : 1;
                int closingLevel;
                if (!TryGetLevel(text, regionStart, out closingLevel))
                    closingLevel = currentLevel;
    
                //the regions match
                if (currentRegion != null &&
                    currentLevel == closingLevel)
                {
                    newRegions.Add(new Region()
                    {
                        Level = currentLevel,
                        StartLine = currentRegion.StartLine,
                        StartOffset = currentRegion.StartOffset,
                        EndLine = line.LineNumber
                    });
    
                    currentRegion = currentRegion.PartialParent;
                }
            }
        }
    
        //determine the changed span, and send a changed event with the new spans
        List<Span> oldSpans =
            new List<Span>(this.regions.Select(r => AsSnapshotSpan(r, this.snapshot)
                .TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive)
                .Span));
        List<Span> newSpans =
                new List<Span>(newRegions.Select(r => AsSnapshotSpan(r, newSnapshot).Span));
    
        NormalizedSpanCollection oldSpanCollection = new NormalizedSpanCollection(oldSpans);
        NormalizedSpanCollection newSpanCollection = new NormalizedSpanCollection(newSpans);
    
        //the changed regions are regions that appear in one set or the other, but not both.
        NormalizedSpanCollection removed =
        NormalizedSpanCollection.Difference(oldSpanCollection, newSpanCollection);
    
        int changeStart = int.MaxValue;
        int changeEnd = -1;
    
        if (removed.Count > 0)
        {
            changeStart = removed[0].Start;
            changeEnd = removed[removed.Count - 1].End;
        }
    
        if (newSpans.Count > 0)
        {
            changeStart = Math.Min(changeStart, newSpans[0].Start);
            changeEnd = Math.Max(changeEnd, newSpans[newSpans.Count - 1].End);
        }
    
        this.snapshot = newSnapshot;
        this.regions = newRegions;
    
        if (changeStart <= changeEnd)
        {
            ITextSnapshot snap = this.snapshot;
            if (this.TagsChanged != null)
                this.TagsChanged(this, new SnapshotSpanEventArgs(
                    new SnapshotSpan(this.snapshot, Span.FromBounds(changeStart, changeEnd))));
        }
    }
    
  10. La méthode d’assistance suivante obtient un entier qui représente le niveau du plan, de sorte que 1 est la paire d’accolades la plus à gauche.

    static bool TryGetLevel(string text, int startIndex, out int level)
    {
        level = -1;
        if (text.Length > startIndex + 3)
        {
            if (int.TryParse(text.Substring(startIndex + 1), out level))
                return true;
        }
    
        return false;
    }
    
  11. La méthode d’assistance suivante traduit une région (définie plus loin dans cet article) en un composant SnapshotSpan.

    static SnapshotSpan AsSnapshotSpan(Region region, ITextSnapshot snapshot)
    {
        var startLine = snapshot.GetLineFromLineNumber(region.StartLine);
        var endLine = (region.StartLine == region.EndLine) ? startLine
             : snapshot.GetLineFromLineNumber(region.EndLine);
        return new SnapshotSpan(startLine.Start + region.StartOffset, endLine.End);
    }
    
  12. Le code suivant est destiné uniquement à l’illustration. Il définit une classe PartialRegion qui contient le numéro de ligne et le décalage du début d’une région de plan et une référence à la région parente (le cas échéant). Ce code permet à l’analyseur de configurer des régions de plan imbriquées. Une classe Region dérivée contient une référence au numéro de ligne de la fin d’une région de plan.

    class PartialRegion
    {
        public int StartLine { get; set; }
        public int StartOffset { get; set; }
        public int Level { get; set; }
        public PartialRegion PartialParent { get; set; }
    }
    
    class Region : PartialRegion
    {
        public int EndLine { get; set; }
    }
    

Implémenter un fournisseur de balisage

Exportez un fournisseur de balisage pour votre taggeur. Le fournisseur de balisage crée une OutliningTagger mémoire tampon du type de contenu « text » ou retourne une OutliningTagger valeur si la mémoire tampon en a déjà une.

Pour implémenter un fournisseur de balisage

  1. Créez une classe nommée OutliningTaggerProvider qui implémente ITaggerProvider, puis exportez-la avec les attributs ContentType et TagType.

    [Export(typeof(ITaggerProvider))]
    [TagType(typeof(IOutliningRegionTag))]
    [ContentType("text")]
    internal sealed class OutliningTaggerProvider : ITaggerProvider
    
  2. Implémentez la CreateTagger méthode en ajoutant une OutliningTagger aux propriétés de la mémoire tampon.

    public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
    {
        //create a single tagger for each buffer.
        Func<ITagger<T>> sc = delegate() { return new OutliningTagger(buffer) as ITagger<T>; };
        return buffer.Properties.GetOrCreateSingletonProperty<ITagger<T>>(sc);
    }
    

Générer et tester le code

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

Pour générer et tester la solution OutlineRegionTest

  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éer un fichier texte. Tapez du texte incluant les crochets ouvrants et les crochets fermants.

    [
       Hello
    ]
    
  4. Il doit y avoir une zone de plan qui inclut les deux crochets. Vous devez être en mesure de cliquer sur le signe Moins à gauche du crochet ouvert pour réduire la zone de plan. Lorsque la région est réduite, le symbole de points de suspension (...) doit apparaître à gauche de la région réduite, et une fenêtre contextuelle contenant le texte de pointage du texte doit apparaître lorsque vous déplacez le pointeur sur les points de suspension.