다음을 통해 공유


연습: 텍스트 강조 표시

MEF(Managed Extensibility Framework) 구성 요소 부분을 만들어 편집기에서 다양한 시각 효과를 추가할 수 있습니다. 이 연습에서는 텍스트 파일에서 현재 단어가 나오는 모든 부분을 강조 표시하는 방법을 보여 줍니다. 텍스트 파일에서 단어가 두 번 이상 나오고 한 번 나올 때마다 캐럿을 배치하면 단어가 나올 때마다 강조 표시됩니다.

MEF 프로젝트 만들기

  1. C# VSIX 프로젝트를 만듭니다. (새 프로젝트 대화 상자에서 Visual C#/확장성, VSIX 프로젝트를 차례로 선택합니다.) 솔루션 이름을 HighlightWordTest로 지정합니다.

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

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

TextMarkerTag 정의

텍스트를 강조 표시하는 첫 번째 단계는 TextMarkerTag를 서브클래싱하고 그 모양을 정의하는 것입니다.

TextMarkerTag 및 MarkerFormatDefinition을 정의하려면

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

  2. 다음 참조를 추가합니다.

    1. Microsoft.VisualStudio.CoreUtility

    2. Microsoft.VisualStudio.Text.Data

    3. Microsoft.VisualStudio.Text.Logic

    4. Microsoft.VisualStudio.Text.UI

    5. Microsoft.VisualStudio.Text.UI.Wpf

    6. System.ComponentModel.Composition

    7. Presentation.Core

    8. Presentation.Framework

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

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Linq;
    using System.Threading;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Classification;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Operations;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
    using System.Windows.Media;
    
  4. TextMarkerTag에서 상속되는 클래스를 만들고 이름을 HighlightWordTag로 지정합니다.

    internal class HighlightWordTag : TextMarkerTag
    {
    
    }
    
  5. MarkerFormatDefinition에서 상속되는 두 번째 클래스를 만들고 이름을 HighlightWordFormatDefinition으로 지정합니다. 태그에 이 형식 정의를 사용하려면 다음 특성과 함께 내보내야 합니다.

    
    [Export(typeof(EditorFormatDefinition))]
    [Name("MarkerFormatDefinition/HighlightWordFormatDefinition")]
    [UserVisible(true)]
    internal class HighlightWordFormatDefinition : MarkerFormatDefinition
    {
    
    }
    
  6. HighlightWordFormatDefinition 생성자에서 표시 이름과 모양을 정의합니다. 배경 속성은 채우기 색을 정의하고 전경 속성은 테두리 색을 정의합니다.

    public HighlightWordFormatDefinition()
    {
        this.BackgroundColor = Colors.LightBlue;
        this.ForegroundColor = Colors.DarkBlue;
        this.DisplayName = "Highlight Word";
        this.ZOrder = 5;
    }
    
  7. HighlightWordTag 생성자에서 만든 형식 정의의 이름을 전달합니다.

    public HighlightWordTag() : base("MarkerFormatDefinition/HighlightWordFormatDefinition") { }
    

ITagger 구현

다음 단계에는 ITagger<T> 인터페이스를 구현합니다. 이 인터페이스는 지정된 텍스트 버퍼에 텍스트 강조 표시 및 기타 시각 효과를 제공하는 태그를 할당합니다.

태거를 구현하려면

  1. HighlightWordTag 형식의 ITagger<T>을 구현하는 클래스를 만들고 이름을 HighlightWordTagger로 지정합니다.

    internal class HighlightWordTagger : ITagger<HighlightWordTag>
    {
    
    }
    
  2. 클래스에 다음 전용 필드와 속성을 추가합니다.

    ITextView View { get; set; }
    ITextBuffer SourceBuffer { get; set; }
    ITextSearchService TextSearchService { get; set; }
    ITextStructureNavigator TextStructureNavigator { get; set; }
    NormalizedSnapshotSpanCollection WordSpans { get; set; }
    SnapshotSpan? CurrentWord { get; set; }
    SnapshotPoint RequestedPoint { get; set; }
    object updateLock = new object();
    
    
  3. 이전에 나열된 속성을 초기화하고 LayoutChangedPositionChanged 이벤트 처리기를 추가하는 생성자를 추가합니다.

    public HighlightWordTagger(ITextView view, ITextBuffer sourceBuffer, ITextSearchService textSearchService,
    ITextStructureNavigator textStructureNavigator)
    {
        this.View = view;
        this.SourceBuffer = sourceBuffer;
        this.TextSearchService = textSearchService;
        this.TextStructureNavigator = textStructureNavigator;
        this.WordSpans = new NormalizedSnapshotSpanCollection();
        this.CurrentWord = null;
        this.View.Caret.PositionChanged += CaretPositionChanged;
        this.View.LayoutChanged += ViewLayoutChanged;
    }
    
    
  4. 두 이벤트 처리기 모두 UpdateAtCaretPosition 메서드를 호출합니다.

    void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
    {
        // If a new snapshot wasn't generated, then skip this layout 
        if (e.NewSnapshot != e.OldSnapshot)
        {
            UpdateAtCaretPosition(View.Caret.Position);
        }
    }
    
    void CaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
    {
        UpdateAtCaretPosition(e.NewPosition);
    }
    
  5. 또한 업데이트 메서드가 호출하는 TagsChanged 이벤트를 추가해야 합니다.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  6. UpdateAtCaretPosition() 메서드는 커서가 배치된 단어와 동일한 텍스트 버퍼의 모든 단어를 찾고 단어가 나오는 부분에 해당하는 SnapshotSpan 개체의 목록을 생성합니다. 그런 다음 TagsChanged 이벤트를 발생시키는 SynchronousUpdate를 호출합니다.

    void UpdateAtCaretPosition(CaretPosition caretPosition)
    {
        SnapshotPoint? point = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity);
    
        if (!point.HasValue)
            return;
    
        // If the new caret position is still within the current word (and on the same snapshot), we don't need to check it 
        if (CurrentWord.HasValue
            && CurrentWord.Value.Snapshot == View.TextSnapshot
            && point.Value >= CurrentWord.Value.Start
            && point.Value <= CurrentWord.Value.End)
        {
            return;
        }
    
        RequestedPoint = point.Value;
        UpdateWordAdornments();
    }
    
    void UpdateWordAdornments()
    {
        SnapshotPoint currentRequest = RequestedPoint;
        List<SnapshotSpan> wordSpans = new List<SnapshotSpan>();
        //Find all words in the buffer like the one the caret is on
        TextExtent word = TextStructureNavigator.GetExtentOfWord(currentRequest);
        bool foundWord = true;
        //If we've selected something not worth highlighting, we might have missed a "word" by a little bit
        if (!WordExtentIsValid(currentRequest, word))
        {
            //Before we retry, make sure it is worthwhile 
            if (word.Span.Start != currentRequest
                 || currentRequest == currentRequest.GetContainingLine().Start
                 || char.IsWhiteSpace((currentRequest - 1).GetChar()))
            {
                foundWord = false;
            }
            else
            {
                // Try again, one character previous.  
                //If the caret is at the end of a word, pick up the word.
                word = TextStructureNavigator.GetExtentOfWord(currentRequest - 1);
    
                //If the word still isn't valid, we're done 
                if (!WordExtentIsValid(currentRequest, word))
                    foundWord = false;
            }
        }
    
        if (!foundWord)
        {
            //If we couldn't find a word, clear out the existing markers
            SynchronousUpdate(currentRequest, new NormalizedSnapshotSpanCollection(), null);
            return;
        }
    
        SnapshotSpan currentWord = word.Span;
        //If this is the current word, and the caret moved within a word, we're done. 
        if (CurrentWord.HasValue && currentWord == CurrentWord)
            return;
    
        //Find the new spans
        FindData findData = new FindData(currentWord.GetText(), currentWord.Snapshot);
        findData.FindOptions = FindOptions.WholeWord | FindOptions.MatchCase;
    
        wordSpans.AddRange(TextSearchService.FindAll(findData));
    
        //If another change hasn't happened, do a real update 
        if (currentRequest == RequestedPoint)
            SynchronousUpdate(currentRequest, new NormalizedSnapshotSpanCollection(wordSpans), currentWord);
    }
    static bool WordExtentIsValid(SnapshotPoint currentRequest, TextExtent word)
    {
        return word.IsSignificant
            && currentRequest.Snapshot.GetText(word.Span).Any(c => char.IsLetter(c));
    }
    
    
  7. SynchronousUpdateWordSpansCurrentWord 속성에 대한 동기 업데이트를 수행하고 TagsChanged 이벤트를 발생시킵니다.

    void SynchronousUpdate(SnapshotPoint currentRequest, NormalizedSnapshotSpanCollection newSpans, SnapshotSpan? newCurrentWord)
    {
        lock (updateLock)
        {
            if (currentRequest != RequestedPoint)
                return;
    
            WordSpans = newSpans;
            CurrentWord = newCurrentWord;
    
            var tempEvent = TagsChanged;
            if (tempEvent != null)
                tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(SourceBuffer.CurrentSnapshot, 0, SourceBuffer.CurrentSnapshot.Length)));
        }
    }
    
  8. GetTags 메서드를 구현해야 합니다. 이 메서드는 SnapshotSpan 개체 컬렉션을 사용하고 태그 범위의 열거형을 반환합니다.

    C#에서 이 메서드를 yield 반복기로 구현하여 태그의 지연 계산(즉, 개별 항목에 액세스할 때만 집합을 계산)할 수 있습니다. Visual Basic에서 목록에 태그를 추가하고 목록을 반환합니다.

    여기서 메서드는 파란색 배경을 제공하는 "파란색" TextMarkerTag가 있는 TagSpan<T> 개체를 반환합니다.

    public IEnumerable<ITagSpan<HighlightWordTag>> GetTags(NormalizedSnapshotSpanCollection spans)
    {
        if (CurrentWord == null)
            yield break;
    
        // Hold on to a "snapshot" of the word spans and current word, so that we maintain the same
        // collection throughout
        SnapshotSpan currentWord = CurrentWord.Value;
        NormalizedSnapshotSpanCollection wordSpans = WordSpans;
    
        if (spans.Count == 0 || wordSpans.Count == 0)
            yield break;
    
        // If the requested snapshot isn't the same as the one our words are on, translate our spans to the expected snapshot 
        if (spans[0].Snapshot != wordSpans[0].Snapshot)
        {
            wordSpans = new NormalizedSnapshotSpanCollection(
                wordSpans.Select(span => span.TranslateTo(spans[0].Snapshot, SpanTrackingMode.EdgeExclusive)));
    
            currentWord = currentWord.TranslateTo(spans[0].Snapshot, SpanTrackingMode.EdgeExclusive);
        }
    
        // First, yield back the word the cursor is under (if it overlaps) 
        // Note that we'll yield back the same word again in the wordspans collection; 
        // the duplication here is expected. 
        if (spans.OverlapsWith(new NormalizedSnapshotSpanCollection(currentWord)))
            yield return new TagSpan<HighlightWordTag>(currentWord, new HighlightWordTag());
    
        // Second, yield all the other words in the file 
        foreach (SnapshotSpan span in NormalizedSnapshotSpanCollection.Overlap(spans, wordSpans))
        {
            yield return new TagSpan<HighlightWordTag>(span, new HighlightWordTag());
        }
    }
    

태거 공급자 만들기

태거를 만들려면 IViewTaggerProvider를 구현해야 합니다. 이 클래스는 MEF 구성 요소 파트이므로 이 확장을 인식할 수 있도록 올바른 특성을 설정해야 합니다.

참고 항목

MEF에 대한 자세한 내용은 MEF(Managed Extensibility Framework)를 참조하세요.

태거 공급자를 만들려면

  1. IViewTaggerProvider를 구현하는 HighlightWordTaggerProvider라는 클래스를 만들고, "text"의 ContentTypeAttributeTextMarkerTagTagTypeAttribute와 함께 내보냅니다.

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class HighlightWordTaggerProvider : IViewTaggerProvider
    { }
    
  2. 태그를 인스턴스화하려면 두 개의 편집기 서비스(ITextSearchServiceITextStructureNavigatorSelectorService)를 가져와야 합니다.

    [Import]
    internal ITextSearchService TextSearchService { get; set; }
    
    [Import]
    internal ITextStructureNavigatorSelectorService TextStructureNavigatorSelector { get; set; }
    
    
  3. CreateTagger 메서드를 구현하여 HighlightWordTagger의 인스턴스를 반환합니다.

    public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
    {
        //provide highlighting only on the top buffer 
        if (textView.TextBuffer != buffer)
            return null;
    
        ITextStructureNavigator textStructureNavigator =
            TextStructureNavigatorSelector.GetTextStructureNavigator(buffer);
    
        return new HighlightWordTagger(textView, buffer, TextSearchService, textStructureNavigator) as ITagger<T>;
    }
    

코드 빌드 및 테스트

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

HighlightWordTest 솔루션을 빌드하고 테스트하려면

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

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

  3. 텍스트 파일을 만들고 단어가 반복되는 일부 텍스트(예: "hello hello hello")를 입력합니다.

  4. 커서를 "hello"가 나오는 부분 중 하나에 배치합니다. 단어가 나오는 모든 부분이 파란색으로 강조 표시되어야 합니다.