Taggers and Tagger Providers
Taggers are objects that can return tagged snapshot ranges for any requested snapshot range. They are generally created by tagger providers, which are language service. Tagger results are then consumed by tag aggregators.
The ITagger Interface
Taggers must implement the ITaggerITagger<IClassificationTag>
.
Returning Tagged Snapshot Ranges
The main focus of this interface is its GetTags method. That method is called when some other object, such as a tag aggregator, needs to know the tagged snapshot ranges provided by the tagger for one or more snapshot ranges. The return value of the method is an IEnumerable<TagSnapshotRange<T>>
. This allows the C# yield keyword to be used when implementing a GetTags method.
Lifecycle
Since taggers generally attach to various events on a document or other objects, they have the notion of a lifecycle. The lifecycle starts when the tagger is created and ends when the tagger is closed.
The ITagger
Notification of Tag Changes
Sometimes the data that a tagger monitors is updated in such a way that its tag results are affected. The tagger's TagsChanged event can be raised to notify any watchers, such as tag aggregators, that the tag data that can be retrieved has been updated.
The event arguments should indicate the snapshot range that was updated. If the entire document was updated, pass a snapshot range that starts at offset 0
and goes through the snapshot length.
Ordering Taggers
Most of the time, the order of taggers isn't important. However when working in an editor and layering IClassificationTag taggers, order is important since that governs how the highlighting style data related to one classification type is merged into the highlighting style data from another classification type.
Taggers must implement the IOrderable interface, which allows them to be ordered as desired.
Token Taggers
Token taggers are a special built-in implementation of taggers that return both ITokenTag and IClassificationTag objects. The tokens used are generated by the language's lexer. These tokens are returned in token tags, and are also used to generate related classification tags as appropriate.
Since in an editor, the syntax highlighting is based off of classifications, token taggers provide the mechanism by which a lexer indirectly provides the base syntax highlighting layer for code. Because token tags also report tokens for a given range, they are used to enable the functionality of scanning a snapshot by tokens.
Note
Only one token tagger should ever be associated with a document at any given time. Therefore only one tagger provider service should ever be registered on a language.
The TokenTagger Class
The TokenTagger class wraps all this functionality up into a handy package. For languages that have a mergable lexer, the TokenTagger class is all you need, and can be used by your language by adding a TokenTaggerProvider
Note
If your language is loaded from a language definition file, a token tagger provider service will be automatically registered with your language. If your language is created in code-behind and all the code was generated by the Language Designer tool, it will make a custom token tagger provider class that can be used.
The standard TokenTagger works for languages with mergable lexers since the tokens created by those lexers already include references to the classification type that should be used for each individual token.
For languages with non-mergable lexers, tokens are smaller and more simplistic. They do not know the related classification type to use. Therefore for languages that use these types of lexers, you must create a class that inherits the TokenTagger class and overrides the ClassifyToken method. This method is passed a token and you return the IClassificationType, if any, to use for that token. The code in the base TokenTagger class takes care of the rest.
The TokenTagger class has a built-in mechanism to support incremental lexing from any point in a document, which is why it is considered a special case tagger type. It completely virtualizes the caching of token data for a document, and allows token data to be generated incrementally on-demand when requested. Whereas most parsing frameworks store all tokens for a document in memory (which can be really bad for large documents), this class instead uses a minimal amount of memory by only caching data at certain intervals and regenerates token data as needed. The net result is that a miniscule amount of memory is used to persist token data for incremental parsing vs. what most other products use, and performance is still good.
Sample: Adding a Token Tagger Provider Service
Assume that we are making an ECMAScript language and have an EcmaScriptTokenTagger
class inherits TokenTagger and has a ClassifyToken override. This is how a provider for it could be installed:
language.RegisterService(new TokenTaggerProvider<EcmaScriptTokenTagger>());
Note
The example above only works for token taggers. For all other taggers, see the documentation on tagger providers below.
Collection Taggers
The ICollectionTagger
Collection Items
The collection tagger stores tagged version ranges (TagVersionRange
A TagVersionRange
Modifying the Collection
The tagger works just like a collection, where tagged version ranges (TagVersionRange
Two additional overloads for the Add method allows for adding a TagVersionRange
An additional overload of the Remove method allows for indirectly removing a TagVersionRange
Two Toggle method overloads toggle whether a tagged range appears over a certain text range. Just like the additional Add method overloads, one accepts a TextSnapshotRange and ITag, and the other an ITextSnapshotLine and ITag. If any tags (within the same tagger) are located in the covered range, they will be removed. If there are no tags within that range, the result is the same as calling the related Add method.
Batch Updates
The tagger will fire off a TagsChanged event any time the collection is modified. When adding multiple tagged ranges, this is not efficient. Therefore, batches are supported. A batch can be created by calling the CreateBatch method. This returns an IDisposable
object. When the object gets disposed, the batch is ended and the TagsChanged event is raised. Therefore this disposible object is ideal for use within a C# using
block.
This code shows how to wrap tagger modification code in a batch update:
using (IDisposable batch = tagger.CreateBatch()) {
// Make batch updates here
}
Finding Tags
The indexer of the ICollectionTagger
ICollectionTagger
One overload is line-based and accepts a start ITextSnapshotLine argument. The other is offset-based and accepts a start TextSnapshotOffset argument. Both overloads also take an optional ITagSearchOptions
The options have properties for setting whether the search should search upwards instead of downward (the default), if the search can wrap if it reaches the start/end of the document with no match, and a filter predicate that can allow for a lambda to decide whether a potential match is valid.
Parse Error Taggers
A ParseErrorTagger class may be available, which is a tagger for ISquiggleTag. When this tagger is provided to a document, it will watch for updates to the document's ParseData property. It will see if the parse data implements the IParseErrorProvider interface. If it does, this tagger will automatically provide related squiggle tags for all parse errors, and when in an editor, will automatically render squiggles under the appropriate text ranges.
For performance reasons, the tagger limits the number of parse errors it can tag. This limit defaults to 5,000
, but can be adjusted by changing the ParseErrorTagger.MaximumParseErrorCount property to another value.
This tagger can be attached to a document via a document-oriented tagger provider, described below.
Tagger Base Classes
There are several classes that can be used as base classes for tagger implementations.
Type | Description |
---|---|
This is an abstract base class for a tagger that implements nearly all of the ITagger In most scenarios, this is the ideal base class to use when writing a tagger. The TaggerBase |
|
This class is used for the special token tagger functionality that is described above. For languages with mergable lexers, an instance of this class can be installed as-is. However for languages with non-mergable lexers, you can make class that inherits TokenTagger and overrides its ClassifyToken method, which returns the IClassificationType to use for an IToken. |
Sample: ITagger Implementation
In this sample we have implemented a tagger that uses the .NET regular expression engine to search for a text pattern. When a match is found, it is tagged with an ISquiggleTag. Assuming this is being used with an editor, a squiggle line will appear under the related text.
/// <summary>
/// Provides <see cref="ISquiggleTag"/> objects over text ranges that contain the specified regex pattern text.
/// </summary>
public class CustomSquiggleTagger : TaggerBase<ISquiggleTag> {
/// <summary>
/// Initializes a new instance of the <c>CustomSquiggleTagger</c> class.
/// </summary>
/// <param name="document">The document to which this manager is attached.</param>
public CustomSquiggleTagger(ICodeDocument document) :
base("ActiproPatternBasedSquiggle", null, document, true) {}
/// <summary>
/// Returns the tag ranges that intersect with the specified normalized snapshot ranges.
/// </summary>
/// <param name="snapshotRanges">The collection of normalized snapshot ranges.</param>
/// <param name="parameter">An optional parameter that provides contextual information about the tag request.</param>
/// <returns>The tag ranges that intersect with the specified normalized snapshot ranges.</returns>
public override IEnumerable<TagSnapshotRange<ISquiggleTag>> GetTags(
NormalizedTextSnapshotRangeCollection snapshotRanges, object parameter) {
if (snapshotRanges != null) {
// Loop through the snapshot ranges
foreach (TextSnapshotRange snapshotRange in snapshotRanges) {
// Get the text of the snapshot range
string text = snapshotRange.Text;
// Look for a regex pattern match
MatchCollection matches = Regex.Matches(text, @"\bActipro\b", RegexOptions.IgnoreCase);
if (matches.Count > 0) {
// Loop through the matches
foreach (Match match in matches) {
// Create a tag
SquiggleTag tag = new SquiggleTag();
// Yield the tag
yield return new TagSnapshotRange<ISquiggleTag>(
TextSnapshotRange.FromSpan(snapshotRange.Snapshot,
snapshotRange.StartOffset + match.Index, match.Length), tag);
}
}
}
}
}
}
Tagger Providers
Tagger providers are objects that can create/retrieve taggers for a particular document or view. Taggers can be created for either a document or a view and their providers are called on-demand when a related tagger is needed.
The general rule is that if a tagger is going to provide tags for that document that should show up in any view that uses the document, a document-oriented tagger provider should be used. Most taggers fall into this category. An example of this would be a tagger that provides syntax error tags. Document-oriented tagger providers are represented by the ICodeDocumentTaggerProvider interface.
For other cases where a tagger should be for a specific view, a view-oriented tagger provider should be used. An example of this would be a tagger that relies on the current selection in a view. View-oriented tagger providers are represented by the ITextViewTaggerProvider interface.
Both tagger provider interfaces have two members: a TagTypes property that returns an enumerable of the Type
of tags that are returned by provided taggers, and a GetTagger method that returns a tagger instance to use for the specified document/view.
Built-In Implementation Classes
These types are built-in classes that can be registered as services on syntax languages and used to provide taggers.
Type | Description |
---|---|
Creates a tagger is associated with an ICodeDocument and any view that uses it. A constructor overload accepts a "singleton" object key that can be used to persist any created tagger into the ICodeDocument.Properties dictionary using the key. In this case, if the document's language changes, the tagger is automatically closed and removed from the properties dictionary. The tagger associated with the document can be retrieved via the properties dictionary while it is active. If no "singleton" object is is passed to the tagger provider, a new tagger class instance will be created each time a tag aggregator requests one. |
|
Creates a tagger is associated with a specific text view. This type is only available when an editor is used. A constructor overload accepts a "singleton" object key that can be used to persist any created tagger into the ITextView.Properties dictionary using the key. In this case, if the view's document or the document's language changes, the tagger is automatically closed and removed from the properties dictionary. The tagger associated with the view can be retrieved via the properties dictionary while it is active. If no "singleton" object is is passed to the tagger provider, a new tagger class instance will be created each time a tag aggregator requests one. |
|
Creates a token tagger (see above for more info) that is associated with an ICodeDocument and any views that use it. Only one token tagger provider should ever be registered as a service on the same language at a time. |
Registering with a Syntax Language
As described above, CodeDocumentTaggerProvider
This code shows how to register a document-oriented tagger provider language service that provides ParseErrorTagger objects for documents that use the language. Note that we are also passing a "singleton" key so that the tagger that is created for any document using the language is persisted in the document's Properties dictionary while it is active.
language.RegisterService(new CodeDocumentTaggerProvider<ParseErrorTagger>(typeof(ParseErrorTagger)));