Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Расширения могут способствовать созданию новых тегов в 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
следует вызывать при завершении создания пакетного тега. Пример расширения теггера содержит полный пример этого подхода.