演练:突出显示文本
可以通过创建托管扩展性框架(MEF)组件部件向编辑器添加不同的视觉效果。 本演练演示如何突出显示文本文件中当前单词的每个匹配项。 如果在文本文件中多次出现单词,并且将插入符号置于一个匹配项中,则会突出显示每个匹配项。
创建 MEF 项目
创建 C# VSIX 项目。 (在 “新建项目 ”对话框,选择 Visual C# /扩展性,然后选择 VSIX Project。)将解决方案
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 的构造函数中,定义其显示名称和外观。 Background 属性定义填充颜色,而前台属性定义边框颜色。
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> 接口。 此接口将给定文本缓冲区分配给提供文本突出显示和其他视觉效果的标记。
实现标记器
创建实现 ITagger<T> 类型的
HighlightWordTag
类,并将其命名HighlightWordTagger
。internal class HighlightWordTagger : ITagger<HighlightWordTag> { }
将以下私有字段和属性添加到类:
一个 ITextView,对应于当前文本视图。
一个 ITextBuffer,它对应于文本视图下的文本缓冲区。
一个 ITextSearchService,用于查找文本。
一个 ITextStructureNavigator,它具有在文本跨度内导航的方法。
一个 NormalizedSnapshotSpanCollection,其中包含要突出显示的单词集。
A SnapshotSpan,对应于当前单词。
A 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 。 然后,它会调用SynchronousUpdate
引发该TagsChanged
事件。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
属性CurrentWord
执行同步更新WordSpans
并引发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> 具有“blue” 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)。
创建标记器提供程序
创建一个名为
HighlightWordTaggerProvider
实现IViewTaggerProvider的类,并使用“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”的其中一个匹配项中。 应以蓝色突出显示每个匹配项。