다음을 통해 공유


연습: 일치하는 중괄호 표시

일치시킬 중괄호를 정의하고 중괄호 중 하나에 캐럿이 있으면 일치하는 중괄호에 텍스트 마커 태그를 추가하여 중괄호 일치와 같은 언어 기반 기능을 구현합니다. 언어 컨텍스트에서 중괄호를 정의하고 고유한 파일 이름 확장명과 콘텐츠 형식을 정의하고 해당 형식에만 태그를 적용하거나 기존 콘텐츠 형식(예: "text")에 태그를 적용할 수 있습니다. 다음 연습에서는 "text" 콘텐츠 형식에 중괄호 일치 태그를 적용하는 방법을 설명합니다.

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

MEF 프로젝트를 만들려면

  1. 편집기 분류자 프로젝트를 만듭니다. 솔루션의 이름을 BraceMatchingTest로 지정합니다.

  2. 프로젝트에 편집기 분류자 항목 템플릿을 추가합니다. 자세한 내용은 편집기 항목 템플릿을 사용하여 확장 만들기를 참조하세요.

  3. 기존 클래스 파일을 삭제합니다.

중괄호 일치 태거 구현

Visual Studio에서 사용되는 것과 유사한 중괄호 강조 표시 효과를 얻으려면 TextMarkerTag 형식 태거를 구현하면 됩니다. 다음 코드에서는 중첩 수준에서 중괄호 쌍에 대한 태거를 정의하는 방법을 보여 줍니다. 이 예제에서는 [] 및 {}의 중괄호 쌍이 태거 생성자에서 정의되지만 전체 언어 구현에서는 관련 중괄호 쌍이 언어 사양에서 정의됩니다.

중괄호 일치 태거 구현하기

  1. 클래스 파일을 추가하고 이름을 BraceMatching으로 지정합니다.

  2. 다음 네임스페이스를 가져옵니다.

    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. TextMarkerTag 형식의 ITagger<T>에서 상속되는 BraceMatchingTagger 클래스를 정의합니다.

    internal class BraceMatchingTagger : ITagger<TextMarkerTag>
    
  4. 텍스트 뷰, 원본 버퍼, 현재 스냅샷 포인트 및 중괄호 쌍 세트에 대한 속성을 추가합니다.

    ITextView View { get; set; }
    ITextBuffer SourceBuffer { get; set; }
    SnapshotPoint? CurrentChar { get; set; }
    private Dictionary<char, char> m_braceList;
    
  5. 태거 생성자에서 속성을 설정하고 뷰 변경 이벤트 PositionChangedLayoutChanged를 구독합니다. 이 예제에서는 설명을 위해 일치하는 쌍도 생성자에서 정의됩니다.

    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. ITagger<T> 구현의 일부로 TagsChanged 이벤트를 선언합니다.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  7. 이벤트 처리기는 CurrentChar 속성의 현재 캐럿 위치를 업데이트하고 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. 현재 문자가 GetTags 여는 중괄호이거나 이전 문자가 닫는 중괄호인 경우 Visual Studio에서와 같이 중괄호가 일치하도록 메서드를 구현합니다. 일치 항목이 발견되면 이 메서드는 여는 중괄호와 닫는 중괄호에 각각 하나씩 태그 두 개를 인스턴스화합니다.

    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. 다음 프라이빗 메서드는 중첩 수준에서 일치하는 중괄호를 찾습니다. 첫 번째 메서드는 열린 문자와 일치하는 닫힌 문자를 찾습니다.

    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. 다음 도우미 메서드는 가까운 문자와 일치하는 열린 문자를 찾습니다.

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

중괄호 일치 태거 공급자 구현

태거 구현 외에도 태거 공급자를 구현하고 내보내야 합니다. 이 경우 공급자의 콘텐츠 형식은 "text"입니다. 따라서 중괄호 일치는 모든 형식의 텍스트 파일에 나타나지만 보다 완전한 구현은 특정 콘텐츠 형식에만 중괄호 일치를 적용합니다.

중괄호 일치 태거 공급자 구현하기

  1. IViewTaggerProvider에서 상속되는 태거 공급자를 선언하고 이름을 BraceMatchingTaggerProvider로 지정하며 “text”의 ContentTypeAttributeTextMarkerTagTagTypeAttribute를 사용하여 내보냅니다.

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class BraceMatchingTaggerProvider : IViewTaggerProvider
    
  2. CreateTagger 메서드를 구현하여 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>;
    }
    

코드 빌드 및 테스트

이 코드를 테스트하려면 BraceMatchingTest 솔루션을 빌드하고 실험적 인스턴스에서 실행합니다.

BraceMatchingTest 솔루션 빌드 및 테스트하기

  1. 솔루션을 빌드합니다.

  2. 디버거에서 이 프로젝트를 실행하면 Visual Studio의 두 번째 인스턴스가 시작됩니다.

  3. 텍스트 파일을 만들고 일치하는 중괄호가 포함된 텍스트 일부를 입력합니다.

    hello {
    goodbye}
    
    {}
    
    {hello}
    
  4. 여는 중괄호 앞에 캐럿을 배치하면 해당 중괄호와 일치하는 닫는 중괄호 모두 강조 표시되어야 합니다. 닫는 중괄호 뒤에 커서를 배치하면 해당 중괄호와 일치하는 여는 중괄호 모두 강조 표시되어야 합니다.