Пошаговое руководство. Структурирование

Настройте функции на основе языка, например структурирование, определив типы текстовых областей, которые требуется развернуть или свернуть. Можно определить регионы в контексте языковой службы или определить собственное расширение имени файла и тип контента и применить определение региона только к такому типу или применить определения регионов к существующему типу контента (например, "текст"). В этом пошаговом руководстве показано, как определить и отобразить области определения и отображения областей.

Создание проекта Managed Extensibility Framework (MEF)

Создание проекта MEF

  1. Создайте проект VSIX. Назовите решение OutlineRegionTest.

  2. Добавьте в проект шаблон элемента классификатора редактора. Дополнительные сведения: Создание расширения с помощью шаблона элемента редактора.

  3. Удалите файлы существующих классов.

Реализация выстраивного тега

Выстраивание регионов помечается типом тега (OutliningRegionTag). Этот тег обеспечивает стандартное поведение структурирования. Указанный регион можно развернуть или свернуть. Контурная область отмечена знаком "Плюс" (), если он свернут или знак минуса (+-), если он развернут, и развернутый регион демаркатируется вертикальной линией.

В следующих шагах показано, как определить тег, создающий выстраивание регионов для всех регионов, разделенных квадратными скобками ([,]).

Реализация выстраивного тега

  1. Добавьте файл класса с именем OutliningTagger.

  2. Импортируйте следующие пространства имен.

    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. Создайте класс с именем OutliningTaggerи реализуйте ITagger<T>:

    internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>
    
  4. Добавьте некоторые поля для отслеживания текстового буфера и моментального снимка, а также для накапливания наборов строк, которые должны быть помечены как области выделения. Этот код содержит список объектов Region (для определения позже), представляющих области.

    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. Добавьте конструктор тега, который инициализирует поля, анализирует буфер и добавляет обработчик событий в Changed событие.

    public OutliningTagger(ITextBuffer buffer)
    {
        this.buffer = buffer;
        this.snapshot = buffer.CurrentSnapshot;
        this.regions = new List<Region>();
        this.ReParse();
        this.buffer.Changed += BufferChanged;
    }
    
  6. GetTags Реализуйте метод, который создает экземпляр диапазонов тегов. В этом примере предполагается, что диапазоны NormalizedSpanCollection , переданные в метод, являются смежными, хотя это может быть не всегда так. Этот метод создает экземпляр нового диапазона тегов для каждого из областей подготовки.

    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. Объявите TagsChanged обработчик событий.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  8. BufferChanged Добавьте обработчик событий, который отвечает на Changed события путем анализа текстового буфера.

    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. Добавьте метод, который анализирует буфер. Пример, приведенный здесь, предназначен только для иллюстрации. Он синхронно анализирует буфер в вложенные области структурирования.

    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. Следующий вспомогательный метод получает целое число, представляющее уровень структурирования, таким образом, что 1 является самой левой парой фигурных скобок.

    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. Следующий вспомогательный метод преобразует регион (определенный далее в этой статье) в моментальный снимок.

    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. Следующий код предназначен только для иллюстрации. Он определяет класс PartialRegion, содержащий номер строки и смещение начала области линии, а также ссылку на родительский регион (если таковой имеется). Этот код позволяет средство синтаксического анализа настроить вложенные области структуры. Класс производного региона содержит ссылку на номер строки конца области.

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

Реализация поставщика тегов

Экспорт поставщика тегов для теггера. Поставщик тегов создает OutliningTagger буфер типа контента "text" или возвращает OutliningTagger значение, если буфер уже имеет один.

Реализация поставщика тегов

  1. Создайте класс с именем OutliningTaggerProvider , реализующий ITaggerProviderи экспортируйте его с помощью атрибутов ContentType и TagType.

    [Export(typeof(ITaggerProvider))]
    [TagType(typeof(IOutliningRegionTag))]
    [ContentType("text")]
    internal sealed class OutliningTaggerProvider : ITaggerProvider
    
  2. CreateTagger Реализуйте метод, добавив OutliningTagger его в свойства буфера.

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

Сборка и проверка кода

Чтобы протестировать этот код, создайте решение OutlineRegionTest и запустите его в экспериментальном экземпляре.

Создание и тестирование решения OutlineRegionTest

  1. Постройте решение.

  2. При запуске этого проекта в отладчике запускается второй экземпляр Visual Studio.

  3. Создание текстового файла. Введите текст, включающий как открывающие скобки, так и закрывающие скобки.

    [
       Hello
    ]
    
  4. Должен быть выстраивание области, включающей обе скобки. Чтобы свернуть область выстраивание, щелкните знак "Минус" слева от открытой скобки. Когда регион свернут, символ многоточия (...) должен отображаться слева от свернутой области, а всплывающее окно, содержащее текст наведения текста, должно отображаться при перемещении указателя на многоточие.