Search code examples
c#visual-studio-2015vsix

VSIX IClassifier assigning multiple ClassificationTypes


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 ClassificationFormatDefinitions, 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.


Solution

  • 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:

    • A classifier (really, an IClassificationTag tagger) yields classification tag-spans for a given section of a text buffer on demand.
    • Classification tag-spans consist of the span in the buffer that the tag applies to, and the classification tag itself. The classification tag simply specifies a classification type to apply.
    • Classification types are used to relate tags of that classification to a given format.
    • Formats (specifically, ClassificationFormatDefinitions) are exported via MEF (as EditorFormatDefinitions) 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.
    • A classifier provider is exported via MEF in order for VS to discover it; it gives VS a means of instantiating your classifier for each open buffer (and thus discovering the tags in it).

    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