Tutorial: Resaltar texto

Puede agregar diferentes efectos visuales al editor mediante la creación de partes de componentes de Managed Extensibility Framework (MEF). En este tutorial se muestra cómo resaltar cada aparición de la palabra actual en un archivo de texto. Si una palabra tiene lugar más de una vez en un archivo de texto y coloca el símbolo de intercalación en una repetición, se resalta cada repetición.

Creación de un proyecto MEF

  1. Cree un proyecto VSIX de C#. (En Cuadro de diálogo Nuevo proyecto , seleccione Visual C# / Extensibilidad y, después , Proyecto VSIX). Asigne un nombre a la solución HighlightWordTest.

  2. Agregue una plantilla de elemento clasificador del editor al proyecto. Para obtener más información, vea Creación de una extensión con una plantilla de elemento de editor.

  3. Elimine los archivos de clase existentes.

Definir un TextMarkerTag

El primer paso para resaltar texto es subclase TextMarkerTag y definir su apariencia.

Para definir textMarkerTag y markerFormatDefinition

  1. Agregue un archivo de clase y asígnele el nombre HighlightWordTag.

  2. Agregue las siguientes referencias:

    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. Importe los siguientes espacios de nombres.

    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. Cree una clase que herede de TextMarkerTag y asígnela HighlightWordTagel nombre .

    internal class HighlightWordTag : TextMarkerTag
    {
    
    }
    
  5. Cree una segunda clase que herede de MarkerFormatDefinitiony asígnela HighlightWordFormatDefinitionel nombre . Para poder usar esta definición de formato para la etiqueta, debe exportarla con los atributos siguientes:

    
    [Export(typeof(EditorFormatDefinition))]
    [Name("MarkerFormatDefinition/HighlightWordFormatDefinition")]
    [UserVisible(true)]
    internal class HighlightWordFormatDefinition : MarkerFormatDefinition
    {
    
    }
    
  6. En el constructor de HighlightWordFormatDefinition, defina su nombre para mostrar y apariencia. La propiedad Background define el color de relleno, mientras que la propiedad Foreground define el color del borde.

    public HighlightWordFormatDefinition()
    {
        this.BackgroundColor = Colors.LightBlue;
        this.ForegroundColor = Colors.DarkBlue;
        this.DisplayName = "Highlight Word";
        this.ZOrder = 5;
    }
    
  7. En el constructor de HighlightWordTag, pase el nombre de la definición de formato que creó.

    public HighlightWordTag() : base("MarkerFormatDefinition/HighlightWordFormatDefinition") { }
    

Implementación de un ITagger

El siguiente paso es implementar la ITagger<T> interfaz. Esta interfaz asigna, a un búfer de texto determinado, etiquetas que proporcionan resaltado de texto y otros efectos visuales.

Para implementar un tagger

  1. Cree una clase que implemente ITagger<T> de tipo HighlightWordTagy asígnela HighlightWordTaggerel nombre .

    internal class HighlightWordTagger : ITagger<HighlightWordTag>
    {
    
    }
    
  2. Agregue los siguientes campos privados y propiedades a la clase :

    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. Agregue un constructor que inicialice las propiedades enumeradas anteriormente y agregue LayoutChanged controladores de eventos y 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;
    }
    
    
  4. Los controladores de eventos llaman al UpdateAtCaretPosition método .

    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. También debe agregar un TagsChanged evento al que llama el método update.

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  6. El UpdateAtCaretPosition() método busca todas las palabras del búfer de texto idénticas a la palabra donde se coloca el cursor y construye una lista de SnapshotSpan objetos que corresponden a las apariciones de la palabra. A continuación, llama a SynchronousUpdate, que genera el TagsChanged evento .

    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 realiza una actualización sincrónica en las WordSpans propiedades y CurrentWord y genera el TagsChanged evento .

    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. Debe implementar el GetTags método . Este método toma una colección de SnapshotSpan objetos y devuelve una enumeración de intervalos de etiquetas.

    En C#, implemente este método como iterador de rendimiento, que habilita la evaluación diferida (es decir, la evaluación del conjunto solo cuando se accede a elementos individuales) de las etiquetas. En Visual Basic, agregue las etiquetas a una lista y devuelva la lista.

    Aquí el método devuelve un TagSpan<T> objeto que tiene un "azul", TextMarkerTagque proporciona un fondo azul.

    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());
        }
    }
    

Creación de un proveedor de tagger

Para crear el tagger, debe implementar un IViewTaggerProvider. Esta clase es una parte del componente MEF, por lo que debe establecer los atributos correctos para que se reconozca esta extensión.

Nota:

Para obtener más información sobre MEF, vea Managed Extensibility Framework (MEF).

Para crear un proveedor de etiquetas

  1. Cree una clase denominada HighlightWordTaggerProvider que implemente IViewTaggerProvidery expórtela con un ContentTypeAttribute de "texto" y un TagTypeAttribute de TextMarkerTag.

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class HighlightWordTaggerProvider : IViewTaggerProvider
    { }
    
  2. Debe importar dos servicios de editor, y ITextSearchService ITextStructureNavigatorSelectorService, para crear una instancia del tagger.

    [Import]
    internal ITextSearchService TextSearchService { get; set; }
    
    [Import]
    internal ITextStructureNavigatorSelectorService TextStructureNavigatorSelector { get; set; }
    
    
  3. Implemente el CreateTagger método para devolver una instancia de 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>;
    }
    

Compilación y prueba del código

Para probar este código, compile la solución HighlightWordTest y ejecútela en la instancia experimental.

Para compilar y probar la solución HighlightWordTest

  1. Compile la solución.

  2. Al ejecutar este proyecto en el depurador, se inicia una segunda instancia de Visual Studio.

  3. Cree un archivo de texto y escriba texto en el que se repiten las palabras, por ejemplo, "hello hello hello".

  4. Coloque el cursor en una de las apariciones de "hello". Cada aparición debe resaltarse en azul.