逐步解說︰醒目提示文字
透過建立 Managed Extensibility Framework (MEF) 元件部分,將不同的視覺效果新增至編輯器。 本逐步解說示範如何醒目提示文字檔中游標所在處單字的每次出現。 如果一個單字在文件檔中出現一次以上,而且您在一次出現時放置插入號,則在每次出現時都會醒目提示。
建立 MEF 專案
建立 C# VXIS 專案。 (在新增專案對話框中,選取 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# 中,將這個方法實作為暫止迭代器,這樣可以啟用標籤的延遲評估 (即僅在存取單個項目時進行評估)。 在 Visual Basic 中,將標籤新增至清單並傳回清單。
在這裡,該方法會 TagSpan<T> 傳回具有「藍色」的 TextMarkerTag 物件,該物件提供藍色背景。
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 的詳細資訊,請參閱 Managed Extensibility Framework (MEF)。
建立標籤器提供者
建立實作 IViewTaggerProvider,名為
HighlightWordTaggerProvider
的類別,並以「文字」的 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」。 每次出現都應該以藍色醒目提示。