Поделиться через


Пошаговое руководство. Выделение текста

Вы можете добавить в редактор различные визуальные эффекты, создав части компонента Managed Extensibility Framework (MEF). В этом пошаговом руководстве показано, как выделить каждое вхождение текущего слова в текстовом файле. Если слово происходит несколько раз в текстовом файле, и вы размещаете курсор в одном вхождлении, каждое вхождение выделено.

Создание проекта MEF

  1. Создайте проект VSIX на C#. (В Диалоговое окно "Новый проект" , выберите Visual C# / Расширяемость, а затем ПРОЕКТ VSIX.) Назовите решение HighlightWordTest.

  2. Добавьте в проект шаблон элемента классификатора редактора. Дополнительные сведения: Создание расширения с помощью шаблона элемента редактора.

  3. Удалите файлы существующих классов.

Определение тега TextMarkerTag

Первым шагом в выделении текста является подкласс TextMarkerTag и определение его внешнего вида.

Определение TextMarkerTag и МаркерFormatDefinition

  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. Чтобы использовать это определение формата для тега, необходимо экспортировать его со следующими атрибутами:

    • NameAttribute: теги используют это для ссылки на этот формат

    • UserVisibleAttribute: это приводит к отображению формата в пользовательском интерфейсе

    
    [Export(typeof(EditorFormatDefinition))]
    [Name("MarkerFormatDefinition/HighlightWordFormatDefinition")]
    [UserVisible(true)]
    internal class HighlightWordFormatDefinition : MarkerFormatDefinition
    {
    
    }
    
  6. В конструкторе для HighlightWordFormatDefinition определите отображаемое имя и внешний вид. Свойство Background определяет цвет заливки, а свойство переднего плана определяет цвет границы.

    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. Создайте класс, реализующий ITagger<T> тип HighlightWordTag, и назовите его HighlightWordTagger.

    internal class HighlightWordTagger : ITagger<HighlightWordTag>
    {
    
    }
    
  2. Добавьте в класс следующие частные поля и свойства:

    • Значение 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();
    
    
  3. Добавьте конструктор, который инициализирует свойства, перечисленные ранее, и добавляет и 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;
    }
    
    
  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 объектов, соответствующих вхождениям слова. Затем 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));
    }
    
    
  7. Выполняет 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)));
        }
    }
    
  8. Необходимо реализовать 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).

Создание поставщика тегов

  1. Создайте класс с именем, реализующий и экспортируйте его с ContentTypeAttribute помощью текста и а TagTypeAttributeTextMarkerTag.IViewTaggerProviderHighlightWordTaggerProvider

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class HighlightWordTaggerProvider : IViewTaggerProvider
    { }
    
  2. Необходимо импортировать два службы редактора, ITextSearchService а также ITextStructureNavigatorSelectorServiceэкземпляр тега.

    [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.

  4. Поместите курсор в одно из вхождений "hello". Каждое вхождение должно быть выделено синим цветом.