Using the standard template I managed to make a custom highlighter which turns all occurrences of the string "Archive?????Key" (where ???? is any collection of characters that are allowed in variable names) pink. However what I would really like is for the "Archive" and "Key" portions to become pink and for the "????" portion to become maroon. As far as I understand VSIX highlighters (and I really don't) this means defining two ClassificationFormatDefinition
s, but every time I try I just break the project.
My GetClassificationSpans
method (which is the only significant deviation from the standard template) looks like:
public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
{
List<ClassificationSpan> spans = new List<ClassificationSpan>();
string text = span.GetText();
int idx0 = 0;
int idx1;
while (true)
{
idx0 = text.IndexOf(keyPrefix, idx0);
if (idx0 < 0)
break;
idx1 = text.IndexOf(keySuffix, idx0 + 6);
if (idx1 < 0)
break;
// TODO: make sure the prefix and suffix are part of the same object identifier.
string name = text.Substring(idx0 + lengthPrefix, idx1 - idx0 - lengthPrefix);
string full = text.Substring(idx0, idx1 - idx0 + keySuffix.Length);
SnapshotSpan span0 = new SnapshotSpan(span.Start + idx0, idx1 - idx0 + lengthSuffix);
SnapshotSpan span1 = new SnapshotSpan(span.Start + idx0 + lengthPrefix, idx1 - idx0 - lengthPrefix);
SnapshotSpan span2 = new SnapshotSpan(span.Start + idx1, lengthSuffix);
spans.Add(new ClassificationSpan(span0, classificationType));
spans.Add(new ClassificationSpan(span1, classificationType)); // I'd like to assign a different IClassificationType to this span.
spans.Add(new ClassificationSpan(span2, classificationType));
idx0 = idx1 + 5;
}
return spans;
}
And span1
is where I want to assign a different style. I do not understand how the Classifier, Format, Provider, and Definition classes needed to do this one (!) thing relate to each other and which ones can be made aware of multiple styles.
The templates are OK for getting started, but usually it's simpler to reimplement everything more directly once you know what direction you're going in.
Here's how all the pieces fit together:
IClassificationTag
tagger) yields classification tag-spans for a given section of a text buffer on demand.ClassificationFormatDefinition
s) are exported via MEF (as EditorFormatDefinition
s) so that VS can discover them and use them to colour spans that have the associated classification type. They also (optionally) appear in the Fonts & Colors options.So, what you're after is code that defines and exports two classification format definitions associated to two classification types, respectively. Then your classifier needs to produce tags of both types accordingly. Here's an example (untested):
public static class Classifications
{
// These are the strings that will be used to form the classification types
// and bind those types to formats
public const string ArchiveKey = "MyProject/ArchiveKey";
public const string ArchiveKeyVar = "MyProject/ArchiveKeyVar";
// These MEF exports define the types themselves
[Export]
[Name(ArchiveKey)]
private static ClassificationTypeDefinition ArchiveKeyType = null;
[Export]
[Name(ArchiveKeyVar)]
private static ClassificationTypeDefinition ArchiveKeyVarType = null;
// These are the format definitions that specify how things will look
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = ArchiveKey)]
[UserVisible(true)] // Controls whether it appears in Fonts & Colors options for user configuration
[Name(ArchiveKey)] // This could be anything but I like to reuse the classification type name
[Order(After = Priority.Default, Before = Priority.High)] // Optionally include this attribute if your classification should
// take precedence over some of the builtin ones like keywords
public sealed class ArchiveKeyFormatDefinition : ClassificationFormatDefinition
{
public ArchiveKeyFormatDefinition()
{
ForegroundColor = Color.FromRgb(0xFF, 0x69, 0xB4); // pink!
DisplayName = "This will display in Fonts & Colors";
}
}
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = ArchiveKeyVar)]
[UserVisible(true)]
[Name(ArchiveKeyVar)]
[Order(After = Priority.Default, Before = Priority.High)]
public sealed class ArchiveKeyVarFormatDefinition : ClassificationFormatDefinition
{
public ArchiveKeyVarFormatDefinition()
{
ForegroundColor = Color.FromRgb(0xB0, 0x30, 0x60); // maroon
DisplayName = "This too will display in Fonts & Colors";
}
}
}
The provider:
[Export(typeof(ITaggerProvider))]
[ContentType("text")] // or whatever content type your tagger applies to
[TagType(typeof(ClassificationTag))]
public class ArchiveKeyClassifierProvider : ITaggerProvider
{
[Import]
public IClassificationTypeRegistryService ClassificationTypeRegistry { get; set; }
public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
{
return buffer.Properties.GetOrCreateSingletonProperty(() =>
new ArchiveKeyClassifier(buffer, ClassificationTypeRegistry)) as ITagger<T>;
}
}
Finally, the tagger itself:
public class ArchiveKeyClassifier : ITagger<ClassificationTag>
{
public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
private Dictionary<string, ClassificationTag> _tags;
public ArchiveKeyClassifier(ITextBuffer subjectBuffer, IClassificationTypeRegistryService classificationRegistry)
{
// Build the tags that correspond to each of the possible classifications
_tags = new Dictionary<string, ClassificationTag> {
{ Classifications.ArchiveKey, BuildTag(classificationRegistry, Classifications.ArchiveKey) },
{ Classifications.ArchiveKeyVar, BuildTag(classificationRegistry, Classifications.ArchiveKeyVar) }
};
}
public IEnumerable<ITagSpan<ClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans)
{
if (spans.Count == 0)
yield break;
foreach (var span in spans) {
if (span.IsEmpty)
continue;
foreach (var identSpan in LexIdentifiers(span)) {
var ident = identSpan.GetText();
if (!ident.StartsWith("Archive") || !ident.EndsWith("Key"))
continue;
var varSpan = new SnapshotSpan(
identSpan.Start + "Archive".Length,
identSpan.End - "Key".Length);
yield return new TagSpan<ClassificationTag>(new SnapshotSpan(identSpan.Start, varSpan.Start), _tags[Classifications.ArchiveKey]);
yield return new TagSpan<ClassificationTag>(varSpan, _tags[Classifications.ArchiveKeyVar]);
yield return new TagSpan<ClassificationTag>(new SnapshotSpan(varSpan.End, identSpan.End), _tags[Classifications.ArchiveKey]);
}
}
}
private static IEnumerable<SnapshotSpan> LexIdentifiers(SnapshotSpan span)
{
// Tokenize the string into identifiers and numbers, returning only the identifiers
var s = span.GetText();
for (int i = 0; i < s.Length; ) {
if (char.IsLetter(s[i])) {
var start = i;
for (++i; i < s.Length && IsTokenChar(s[i]); ++i);
yield return new SnapshotSpan(span.Start + start, i - start);
continue;
}
if (char.IsDigit(s[i])) {
for (++i; i < s.Length && IsTokenChar(s[i]); ++i);
continue;
}
++i;
}
}
private static bool IsTokenChar(char c)
{
return char.IsLetterOrDigit(c) || c == '_';
}
private static ClassificationTag BuildTag(IClassificationTypeRegistryService classificationRegistry, string typeName)
{
return new ClassificationTag(classificationRegistry.GetClassificationType(typeName));
}
}
One more note: In order to accelerate startup, VS keeps a cache of MEF exports. However, this cache is often not invalidated when it should be. Additionally, if you change the default colour of an existing classification format definition, there's a good chance your change won't get picked up because VS saves the previous values in the registry. To mitigate this, it's best to run a batch script in between compiles when anything MEF- or format-related changes to clear things. Here's an example for VS2013 and the Exp root suffix (used by default when testing VSIXes):
@echo off
del "%LOCALAPPDATA%\Microsoft\VisualStudio\12.0Exp\ComponentModelCache\Microsoft.VisualStudio.Default.cache" 2> nul
rmdir /S /Q "%LOCALAPPDATA%\Microsoft\VisualStudio\12.0Exp\ComponentModelCache" 2> nul
reg delete HKCU\Software\Microsoft\VisualStudio\12.0Exp\FontAndColors\Cache\{75A05685-00A8-4DED-BAE5-E7A50BFA929A} /f