Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Extensions can contribute new taggers to Visual Studio. Taggers are used to associate data with ranges of text. Then other Visual Studio features (for example, CodeLens) can consume the data that the taggers provided.
VisualStudio.Extensibility
supports tag types that are only provided by the Microsoft.VisualStudio.Extensibility package and implement the ITag interface:
CodeLensTag
is used together with an ICodeLensProvider to add CodeLens to documents.TextMarkerTag
is used to highlight portions of documents.VisualStudio.Extensibility
doesn't support defining new Text Marker styles yet. For now, you can use only styles that are built into Visual Studio or provided by a Visual Studio SDK extension. (A VisualStudio.Extensibility in-proc extension can create Text Marker styles with[Export(typeof(EditorFormatDefinition))]
.)ClassificationTag
is used to classify a document's syntax, which allows for the text to be colorized accordingly.
To generate tags, the extension must contribute an extension part that implements ITextViewTaggerProvider<>
for the type (or types) of tags provided. The extension part also needs to implement ITextViewChangedListener
to react to document changes:
[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)
{
...
}
}
The tagger provider must keep track of the active taggers to dispatch the TextViewChangedAsync
notifications to them. The following code snippet is a full implementation:
[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);
}
}
}
}
}
The tagger itself is a class that implements 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);
}
}
Both the TextViewChangedAsync
and RequestTagsAsync
methods should call UpdateTagsAsync
to provide the ranges for which the tags are being updated and the new tags themselves. The TextViewTagger<>
base class holds a cache of previously generated tags. Calling UpdateTagsAsync
invalidates all existing tags for the provided ranges and replaces them with the newly provided ones.
Although generating tags for the entire document is a possible strategy, it's preferable to generate tags only for the requested ranges (in RequestTagsAsync
) and the edited ranges (in TextViewChangedAsync
). It's also common to have to extend such ranges to cover meaningful spans of the document syntax (for example, entire statements or entire lines of code).
Handling text view changes in particular requires some extra code. The following code snippet is an example:
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);
}
If generating tags requires significant computation (for example, it's necessary to parse the entire document, or large portions of it, to generate tags), the tagger should have extra synchronization logic to avoid calculating tags for every text view change or call to RequestTagsAsync
. Instead, RequestTagsAsync
and TextViewChangedAsync
should quickly return a completed task, multiple requests should be batched together, and UpdateTagsAsync
should be called when the batched tag generation is completed. The tagger sample extension contains a complete example of this approach.