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


Расширение редактора Visual Studio с помощью нового тега

Расширения могут способствовать созданию новых тегов в Visual Studio. Теги используются для связывания данных с диапазонами текста. Затем другие функции Visual Studio (например, CodeLens) могут использовать данные, предоставленные тегами.

VisualStudio.Extensibility поддерживает типы тегов, предоставляемые только пакетом Microsoft.VisualStudio.Extensibility и реализующие интерфейс ITag :

  • CodeLensTag используется вместе с ICodeLensProvider для добавления CodeLens в документы.
  • TextMarkerTag используется для выделения частей документов. VisualStudio.Extensibility пока не поддерживает определение новых стилей маркеров текста. Теперь можно использовать только стили, встроенные в Visual Studio или предоставляемые расширением пакета SDK Для Visual Studio. (Расширение VisualStudio.Extensibility in-proc может создавать стили текстовых маркеров с [Export(typeof(EditorFormatDefinition))]).
  • ClassificationTag используется для классификации синтаксиса документа, который позволяет цветировать текст соответствующим образом.

Чтобы создать теги, расширение должно представить компонент расширения, который реализует ITextViewTaggerProvider<> для предоставляемого типа (или типов) тегов. Часть расширения также должна реализовать ITextViewChangedListener для реагирования на изменения документа:

[VisualStudioContribution]
internal class MarkdownCodeLensTaggerProvider : ExtensionPart, ITextViewTaggerProvider<CodeLensTag>, ITextViewChangedListener
{
    public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
    {
        AppliesTo = [DocumentFilter.FromDocumentType("vs-markdown")],
    };

    public async Task TextViewChangedAsync(TextViewChangedArgs args, CancellationToken cancellationToken)
    {
        ...
    }

    public Task<TextViewTagger<CodeLensTag>> CreateTaggerAsync(ITextViewSnapshot textView, CancellationToken cancellationToken)
    {
        ...
    }
}

Поставщик тегов должен отслеживать активные теги для отправки TextViewChangedAsync уведомлений в них. Следующий фрагмент кода является полной реализацией:

[VisualStudioContribution]
internal class MarkdownCodeLensTaggerProvider : ExtensionPart, ITextViewTaggerProvider<CodeLensTag>, ITextViewChangedListener
{
    private readonly object lockObject = new();
    private readonly Dictionary<Uri, List<MarkdownCodeLensTagger>> taggers = new();

    public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
    {
        AppliesTo = [DocumentFilter.FromDocumentType("vs-markdown")],
    };

    public async Task TextViewChangedAsync(TextViewChangedArgs args, CancellationToken cancellationToken)
    {
        List<Task> tasks = new();
        lock (this.lockObject)
        {
            if (this.taggers.TryGetValue(args.AfterTextView.Uri, out var textMarkerTaggers))
            {
                foreach (var textMarkerTagger in textMarkerTaggers)
                {
                    tasks.Add(textMarkerTagger.TextViewChangedAsync(args.AfterTextView, args.Edits, cancellationToken));
                }
            }
        }

        await Task.WhenAll(tasks);
    }

    public Task<TextViewTagger<CodeLensTag>> CreateTaggerAsync(ITextViewSnapshot textView, CancellationToken cancellationToken)
    {
        var tagger = new MarkdownCodeLensTagger(this, textView.Document.Uri);
        lock (this.lockObject)
        {
            if (!this.taggers.TryGetValue(textView.Document.Uri, out var taggers))
            {
                taggers = new();
                this.taggers[textView.Document.Uri] = taggers;
            }

            taggers.Add(tagger);
        }

        return Task.FromResult<TextViewTagger<CodeLensTag>>(tagger);
    }

    internal void RemoveTagger(Uri documentUri, MarkdownCodeLensTagger toBeRemoved)
    {
        lock (this.lockObject)
        {
            if (this.taggers.TryGetValue(documentUri, out var taggers))
            {
                taggers.Remove(toBeRemoved);
                if (taggers.Count == 0)
                {
                    this.taggers.Remove(documentUri);
                }
            }
        }
    }
}

Сам теггер является классом, реализующим TextViewTagger<>:

internal class MarkdownCodeLensTagger : TextViewTagger<CodeLensTag>
{
    private readonly MarkdownCodeLensTaggerProvider provider;
    private readonly Uri documentUri;

    public MarkdownCodeLensTagger(MarkdownCodeLensTaggerProvider provider, Uri documentUri)
    {
        this.provider = provider;
        this.documentUri = documentUri;
    }

    public override void Dispose()
    {
        this.provider.RemoveTagger(this.documentUri, this);
        base.Dispose();
    }

    public async Task TextViewChangedAsync(ITextViewSnapshot textView, IReadOnlyList<TextEdit> edits, CancellationToken cancellationToken)
    {
        ...
        await this.UpdateTagsAsync(ranges, tags, cancellationToken);
    }

    protected override async Task RequestTagsAsync(NormalizedTextRangeCollection requestedRanges, bool recalculateAll, CancellationToken cancellationToken)
    {
        ...
        await this.UpdateTagsAsync(ranges, tags, cancellationToken);
    }
}

Оба метода TextViewChangedAsync и RequestTagsAsync должны вызывать UpdateTagsAsync, чтобы определить диапазоны, для которых обновляются теги, а также сами новые теги. Базовый TextViewTagger<> класс содержит кэш ранее созданных тегов. Вызов UpdateTagsAsync делает недействительными все существующие теги для предоставленных диапазонов и заменяет их новыми.

Хотя создание тегов для всего документа является одной из возможных стратегий, предпочтительно создавать теги только для запрошенных диапазонов (в RequestTagsAsync) и измененных диапазонов (в TextViewChangedAsync). Кроме того, обычно необходимо расширить такие диапазоны, чтобы охватывать значимые диапазоны синтаксиса документа (например, целые операторы или целые строки кода).

Для обработки изменений в представлении текста, в частности, требуется дополнительный код. Следующий фрагмент кода является примером:

public async Task TextViewChangedAsync(ITextViewSnapshot textView, IReadOnlyList<TextEdit> edits, CancellationToken cancellationToken)
{
    // GetAllRequestedRangesAsync returns all ranges that Visual Studio has requested
    // tags for so far.
    var allRequestedRanges = await this.GetAllRequestedRangesAsync(textView.Document, cancellationToken);

    // Translate edited ranges to the current document snapshot
    var editedRanges = edits.Select(e => e.Range.TranslateTo(textView.Document, TextRangeTrackingMode.EdgeInclusive));

    // Extend 0-length ranges to be at least 1 character so that they are not ignored
    // when passed to `Intersect`
    var fixedEditedRanges = editedRanges.Select(e => EnsureNotEmpty(editedRanges));

    // Intersect the edited ranges with the requested ranges to avoid generating tags
    // for ranges that Visual Studio is not interested in (for example, non-visible portions
    // of the document)
    var rangesOfInterest = allRequestedRanges.Intersect(fixedEditedRanges);

    // Extend ranges to match meaningful portions of the document's syntax
    var rangesToCalculateTagsFor = ExtendToMatchSyntax(rangesOfInterest);

    // Calculate tags
    await this.CreateTagsAsync(textView.Document, rangesToCalculateTagsFor);
}

private static TextRange EnsureNotEmpty(TextRange range)
{
    ...
}

private static IEnumerable<TextRange> ExtendToMatchSyntax(IEnumerable<TextRange> range)
{
    ...
}

private async Task CreateTagsAsync(ITextDocumentSnapshot document, IEnumerable<TextRange> requestedRanges)
{
    ...
    await this.UpdateTagsAsync(ranges, tags, cancellationToken);
}

Если для создания тегов требуется значительное вычисление (например, необходимо проанализировать весь документ или большие части, чтобы создать теги), теггер должен иметь дополнительную логику синхронизации, чтобы избежать вычисления тегов для каждого изменения представления текста или вызова RequestTagsAsync. Вместо этого RequestTagsAsync и TextViewChangedAsync должны быстро возвращать завершенную задачу, несколько запросов должны быть пакетированы вместе, и UpdateTagsAsync следует вызывать при завершении создания пакетного тега. Пример расширения теггера содержит полный пример этого подхода.