연습: 텍스트 강조 표시
MEF(Managed Extensibility Framework) 구성 요소 부분을 만들어 편집기에서 다양한 시각 효과를 추가할 수 있습니다. 이 연습에서는 텍스트 파일에서 현재 단어가 나오는 모든 부분을 강조 표시하는 방법을 보여 줍니다. 텍스트 파일에서 단어가 두 번 이상 나오고 한 번 나올 때마다 캐럿을 배치하면 단어가 나올 때마다 강조 표시됩니다.
MEF 프로젝트 만들기
C# VSIX 프로젝트를 만듭니다. (새 프로젝트 대화 상자에서 Visual C#/확장성, VSIX 프로젝트를 차례로 선택합니다.) 솔루션 이름을
HighlightWordTest
로 지정합니다.프로젝트에 편집기 분류자 항목 템플릿을 추가합니다. 자세한 내용은 편집기 항목 템플릿을 사용하여 확장 만들기를 참조하세요.
기존 클래스 파일을 삭제합니다.
TextMarkerTag 정의
텍스트를 강조 표시하는 첫 번째 단계는 TextMarkerTag를 서브클래싱하고 그 모양을 정의하는 것입니다.
TextMarkerTag 및 MarkerFormatDefinition을 정의하려면
클래스 파일을 추가하고 이름을 HighlightWordTag로 지정합니다.
다음 참조를 추가합니다.
Microsoft.VisualStudio.CoreUtility
Microsoft.VisualStudio.Text.Data
Microsoft.VisualStudio.Text.Logic
Microsoft.VisualStudio.Text.UI
Microsoft.VisualStudio.Text.UI.Wpf
System.ComponentModel.Composition
Presentation.Core
Presentation.Framework
다음 네임스페이스를 가져옵니다.
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;
TextMarkerTag에서 상속되는 클래스를 만들고 이름을
HighlightWordTag
로 지정합니다.internal class HighlightWordTag : TextMarkerTag { }
MarkerFormatDefinition에서 상속되는 두 번째 클래스를 만들고 이름을
HighlightWordFormatDefinition
으로 지정합니다. 태그에 이 형식 정의를 사용하려면 다음 특성과 함께 내보내야 합니다.NameAttribute: 태그는 이 형식을 참조하는 데 사용합니다.
UserVisibleAttribute: 이렇게 하면 이 형식이 UI에 표시됩니다.
[Export(typeof(EditorFormatDefinition))] [Name("MarkerFormatDefinition/HighlightWordFormatDefinition")] [UserVisible(true)] internal class HighlightWordFormatDefinition : MarkerFormatDefinition { }
HighlightWordFormatDefinition 생성자에서 표시 이름과 모양을 정의합니다. 배경 속성은 채우기 색을 정의하고 전경 속성은 테두리 색을 정의합니다.
public HighlightWordFormatDefinition() { this.BackgroundColor = Colors.LightBlue; this.ForegroundColor = Colors.DarkBlue; this.DisplayName = "Highlight Word"; this.ZOrder = 5; }
HighlightWordTag 생성자에서 만든 형식 정의의 이름을 전달합니다.
public HighlightWordTag() : base("MarkerFormatDefinition/HighlightWordFormatDefinition") { }
ITagger 구현
다음 단계에는 ITagger<T> 인터페이스를 구현합니다. 이 인터페이스는 지정된 텍스트 버퍼에 텍스트 강조 표시 및 기타 시각 효과를 제공하는 태그를 할당합니다.
태거를 구현하려면
HighlightWordTag
형식의 ITagger<T>을 구현하는 클래스를 만들고 이름을HighlightWordTagger
로 지정합니다.internal class HighlightWordTagger : ITagger<HighlightWordTag> { }
클래스에 다음 전용 필드와 속성을 추가합니다.
현재 텍스트 보기에 해당하는 ITextView.
텍스트 보기의 기반이 되는 텍스트 버퍼에 해당하는 ITextBuffer.
텍스트를 찾는 데 사용되는 ITextSearchService.
텍스트 범위 내에서 탐색하는 메서드가 있는 ITextStructureNavigator.
강조 표시할 단어 집합이 포함된 NormalizedSnapshotSpanCollection.
현재 단어에 해당하는 SnapshotSpan.
캐럿의 현재 위치에 해당하는 SnapshotPoint.
잠금 개체.
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();
이전에 나열된 속성을 초기화하고 LayoutChanged 및 PositionChanged 이벤트 처리기를 추가하는 생성자를 추가합니다.
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; }
두 이벤트 처리기 모두
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); }
또한 업데이트 메서드가 호출하는
TagsChanged
이벤트를 추가해야 합니다.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)); }
SynchronousUpdate
는WordSpans
및CurrentWord
속성에 대한 동기 업데이트를 수행하고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))); } }
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)를 참조하세요.
태거 공급자를 만들려면
IViewTaggerProvider를 구현하는
HighlightWordTaggerProvider
라는 클래스를 만들고, "text"의 ContentTypeAttribute 및 TextMarkerTag의 TagTypeAttribute와 함께 내보냅니다.[Export(typeof(IViewTaggerProvider))] [ContentType("text")] [TagType(typeof(TextMarkerTag))] internal class HighlightWordTaggerProvider : IViewTaggerProvider { }
태그를 인스턴스화하려면 두 개의 편집기 서비스(ITextSearchService 및 ITextStructureNavigatorSelectorService)를 가져와야 합니다.
[Import] internal ITextSearchService TextSearchService { get; set; } [Import] internal ITextStructureNavigatorSelectorService TextStructureNavigatorSelector { get; set; }
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 솔루션을 빌드하고 테스트하려면
솔루션을 빌드합니다.
디버거에서 이 프로젝트를 실행하면 Visual Studio의 두 번째 인스턴스가 시작됩니다.
텍스트 파일을 만들고 단어가 반복되는 일부 텍스트(예: "hello hello hello")를 입력합니다.
커서를 "hello"가 나오는 부분 중 하나에 배치합니다. 단어가 나오는 모든 부분이 파란색으로 강조 표시되어야 합니다.