Пошаговое руководство. Выделение текста
Вы можете добавить в редактор различные визуальные эффекты, создав части компонента Managed Extensibility Framework (MEF). В этом пошаговом руководстве показано, как выделить каждое вхождение текущего слова в текстовом файле. Если слово происходит несколько раз в текстовом файле, и вы размещаете курсор в одном вхождлении, каждое вхождение выделено.
Создание проекта MEF
Создайте проект VSIX на C#. (В Диалоговое окно "Новый проект" , выберите Visual C# / Расширяемость, а затем ПРОЕКТ VSIX.) Назовите решение
HighlightWordTest
.Добавьте в проект шаблон элемента классификатора редактора. Дополнительные сведения: Создание расширения с помощью шаблона элемента редактора.
Удалите файлы существующих классов.
Определение тега TextMarkerTag
Первым шагом в выделении текста является подкласс TextMarkerTag и определение его внешнего вида.
Определение TextMarkerTag и МаркерFormatDefinition
Добавьте файл класса и назовите его 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: это приводит к отображению формата в пользовательском интерфейсе
[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содержащий набор слов для выделения.
Значение , 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();
Добавьте конструктор, который инициализирует свойства, перечисленные ранее, и добавляет и PositionChanged обработчики LayoutChanged событий.
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
синхронное обновление для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()); } }
Создание поставщика Tagger
Чтобы создать тег, необходимо реализовать .IViewTaggerProvider Этот класс является частью компонента MEF, поэтому необходимо задать правильные атрибуты, чтобы это расширение было распознано.
Примечание.
Дополнительные сведения о MEF см. в статье Об управляемой платформе расширяемости (MEF).
Создание поставщика тегов
Создайте класс с именем, реализующий и экспортируйте его с ContentTypeAttribute помощью текста и а TagTypeAttribute TextMarkerTag.IViewTaggerProvider
HighlightWordTaggerProvider
[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". Каждое вхождение должно быть выделено синим цветом.