다음을 통해 공유


연습: 개요

확장하거나 축소하려는 텍스트 영역의 종류를 정의하여 개요와 같은 언어 기반 기능을 설정합니다. 언어 서비스의 컨텍스트에서 영역을 정의하거나 사용자 고유의 파일 이름 확장명 및 콘텐츠 형식을 정의하고 해당 형식에만 영역 정의를 적용하거나 영역 정의를 기존 콘텐츠 형식(예: “text”)에 적용할 수 있습니다. 이 연습에서는 개요 영역을 정의하고 표시하는 방법을 보여 줍니다.

MEF(Managed Extensibility Framework) 프로젝트 만들기

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. 일부 필드를 추가하여 텍스트 버퍼 및 스냅샷을 추적하고 개요 영역으로 태그가 지정되어야 하는 줄 집합을 누적합니다. 이 코드에는 개요 영역을 나타내는 영역 개체(나중에 정의됨)의 목록이 포함됩니다.

    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. 텍스트 버퍼를 구문 분석하여 Changed 이벤트에 응답하는 BufferChanged 이벤트 처리기를 추가합니다.

    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. 다음 도우미 메서드는 영역(이 문서의 뒷부분에서 정의됨)을 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. 다음 코드는 설명을 돕기 위해 제공된 것입니다. 개요 영역 시작의 줄 번호와 오프셋, 부모 영역(있는 경우)에 대한 참조를 포함하는 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를 생성하거나 버퍼에 이미 있는 경우 OutliningTagger를 반환합니다.

태거 공급자를 구현하려면

  1. ITaggerProvider를 구현하는 OutliningTaggerProvider라는 클래스를 만들고 이를 ContentType 및 TagType 속성과 함께 내보냅니다.

    [Export(typeof(ITaggerProvider))]
    [TagType(typeof(IOutliningRegionTag))]
    [ContentType("text")]
    internal sealed class OutliningTaggerProvider : ITaggerProvider
    
  2. 버퍼의 속성에 OutliningTagger를 추가하여 CreateTagger 메서드를 구현합니다.

    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. 두 대괄호를 모두 포함하는 개요 영역이 있어야 합니다. 열린 대괄호 왼쪽에 있는 빼기 기호를 클릭하여 개요 영역을 축소할 수 있습니다. 영역이 축소되면 줄임표 기호(...)가 축소된 영역의 왼쪽에 나타나야 하며, 줄임표 위로 포인터를 이동할 때 텍스트 텍스트 가리키기가 포함된 팝업이 나타납니다.