Add custom syntax highlighting and text classification to the Visual Studio editor. Covers VisualStudio.Extensibility, VSIX Community Toolkit, and VSSDK approaches.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vs-extensibility-skills:adding-editor-classifiersThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Classifiers assign classification types (keyword, comment, string, etc.) to spans of text. Visual Studio maps these types to colors and styles. Common scenarios:
Classifiers assign classification types (keyword, comment, string, etc.) to spans of text. Visual Studio maps these types to colors and styles. Common scenarios:
Classifiers are the foundation of syntax highlighting in VS. They run on every editor render pass, so performance is critical — a slow classifier makes typing feel laggy across the entire editor. Classifiers also integrate with the VS Fonts & Colors system, allowing users to customize your classification colors through Tools > Options.
When to use classifiers vs. alternatives:
Any extension that uses MEF editor exports must declare the MEF asset type in the .vsixmanifest file. Without this, Visual Studio will not discover your MEF components and your classifier will not load.
Add this inside the <Assets> element of source.extension.vsixmanifest:
<Asset Type="Microsoft.VisualStudio.MefComponent"
d:Source="Project"
d:ProjectName="%CurrentProject%"
Path="|%CurrentProject%|" />
If you are using a multi-project solution, replace %CurrentProject% with the project name that contains the MEF exports.
The VisualStudio.Extensibility SDK does not currently support custom classifiers or classification types. Text classification relies on MEF-exported IClassifier components which must run in-process.
If you need custom syntax highlighting, use the VSSDK (in-process) MEF approach.
The Community Toolkit does not add a separate classification API — classifiers use the same MEF-based VSSDK APIs. The code below is the standard VSSDK approach, which works in a Community Toolkit extension.
Custom classification involves three MEF exports:
IClassifier instances for each text bufferNuGet packages: Microsoft.VisualStudio.SDK, Microsoft.VisualStudio.Editor, Microsoft.VisualStudio.Language.StandardClassification
Key namespaces: Microsoft.VisualStudio.Text.Classification, Microsoft.VisualStudio.Utilities
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;
namespace MyExtension;
internal static class ClassificationTypes
{
[Export(typeof(ClassificationTypeDefinition))]
[Name("MyKeyword")]
internal static ClassificationTypeDefinition MyKeywordType = null;
[Export(typeof(ClassificationTypeDefinition))]
[Name("MyComment")]
internal static ClassificationTypeDefinition MyCommentType = null;
}
using System.ComponentModel.Composition;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;
namespace MyExtension;
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = "MyKeyword")]
[Name("MyKeyword")]
[UserVisible(true)]
[Order(Before = Priority.Default)]
internal sealed class MyKeywordFormat : ClassificationFormatDefinition
{
public MyKeywordFormat()
{
DisplayName = "My Keyword";
ForegroundColor = Colors.CornflowerBlue;
IsBold = true;
}
}
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = "MyComment")]
[Name("MyComment")]
[UserVisible(true)]
[Order(Before = Priority.Default)]
internal sealed class MyCommentFormat : ClassificationFormatDefinition
{
public MyCommentFormat()
{
DisplayName = "My Comment";
ForegroundColor = Colors.Green;
IsItalic = true;
}
}
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;
namespace MyExtension;
[Export(typeof(IClassifierProvider))]
[ContentType("text")] // or your custom content type
internal sealed class MyClassifierProvider : IClassifierProvider
{
[Import]
internal IClassificationTypeRegistryService ClassificationRegistry { get; set; }
public IClassifier GetClassifier(ITextBuffer textBuffer)
{
return textBuffer.Properties.GetOrCreateSingletonProperty(
() => new MyClassifier(textBuffer, ClassificationRegistry));
}
}
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
namespace MyExtension;
internal sealed class MyClassifier : IClassifier
{
private readonly ITextBuffer _buffer;
private readonly IClassificationType _keywordType;
private readonly IClassificationType _commentType;
// Keywords to highlight
private static readonly HashSet<string> Keywords = new(StringComparer.OrdinalIgnoreCase)
{
"SELECT", "FROM", "WHERE", "INSERT", "UPDATE", "DELETE", "JOIN", "ON"
};
public MyClassifier(ITextBuffer buffer, IClassificationTypeRegistryService registry)
{
_buffer = buffer;
_keywordType = registry.GetClassificationType("MyKeyword");
_commentType = registry.GetClassificationType("MyComment");
}
public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;
public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
{
var classifications = new List<ClassificationSpan>();
string text = span.GetText();
int lineStart = span.Start.Position;
// Simple line-based classification
foreach (var line in span.Snapshot.Lines)
{
if (line.Start < span.Start || line.Start >= span.End)
continue;
string lineText = line.GetText();
// Classify comments (lines starting with --)
if (lineText.TrimStart().StartsWith("--"))
{
classifications.Add(new ClassificationSpan(
line.Extent, _commentType));
continue;
}
// Classify keywords
var words = lineText.Split(new[] { ' ', '\t', ',', '(', ')' },
StringSplitOptions.RemoveEmptyEntries);
int pos = 0;
foreach (string word in words)
{
int idx = lineText.IndexOf(word, pos, StringComparison.OrdinalIgnoreCase);
if (idx >= 0 && Keywords.Contains(word))
{
var wordSpan = new SnapshotSpan(
span.Snapshot, line.Start + idx, word.Length);
classifications.Add(new ClassificationSpan(wordSpan, _keywordType));
}
pos = idx + word.Length;
}
}
return classifications;
}
}
If you're creating a classifier for a new file type, define a content type and file extension:
internal static class MyContentType
{
[Export]
[Name("myLanguage")]
[BaseDefinition("code")]
internal static ContentTypeDefinition MyLanguageContentType = null;
[Export]
[FileExtension(".mylang")]
[ContentType("myLanguage")]
internal static FileExtensionToContentTypeDefinition MyLanguageFileExtension = null;
}
Then change your classifier provider's [ContentType("text")] to [ContentType("myLanguage")].
To add classifications on top of an existing language (e.g., highlight custom identifiers in C#), use [ContentType("CSharp")] on your provider. Your classifier's spans will merge with the built-in C# classifier.
GetClassificationSpans is called frequently — keep it fast. Avoid allocations and complex parsing.ClassificationChanged when your classification data changes (e.g., after a background parse completes).IClassificationTypeRegistryService to look up classification types.[UserVisible(true)] on the format definition makes the classification appear in Tools > Options > Fonts and Colors.[Order] on the format to control priority when multiple classifiers produce overlapping spans..vsixmanifest — see the top of this document.ClassificationTypeDefinition, ClassificationFormatDefinition, and IClassifierProvider via MEF. Implement IClassifier.GetClassificationSpans to return classified text spans.source.extension.vsixmanifest.GetClassificationSpans fast — it runs on every render pass..vsixmanifest. Also verify [ContentType] matches the file type you're testing with.[UserVisible(true)] is set on the ClassificationFormatDefinition. Without it, the classification exists internally but isn't exposed in Tools > Options.ClassificationFormatDefinition properties that respect VS theme tokens, or define separate colors for Light/Dark/High Contrast. See vs-theming.GetClassificationSpans is doing too much parsing. Pre-parse on a background thread (triggered by ITextBuffer.Changed) and cache results; return from cache in GetClassificationSpans.ClassificationChanged event causes infinite loop: Your ClassificationChanged handler is triggering a re-classification. Guard against re-entrancy.Do NOT do heavy parsing or I/O in
GetClassificationSpans— it runs synchronously on every render pass. Parse on a background thread and cache results.
Do NOT hard-code colors in
ClassificationFormatDefinitionwithout considering Dark and High Contrast themes — use VS theme-aware color tokens or per-theme overrides.
Do NOT forget the
MefComponentasset type in.vsixmanifest— without it, your classifier provider and format definitions are silently ignored.
Do NOT forget to fire
ClassificationChangedwhen cached parse results update — without it, stale highlighting persists until the user scrolls.
npx claudepluginhub madskristensen/vs-agent-plugins --plugin vs-extensibility-skillsAdds syntax highlighting, bracket matching, and basic editor support for custom languages in Visual Studio by bundling TextMate grammar files (.tmlanguage) with a VSIX and pkgdef registration. Use when porting grammars from VS Code or Sublime Text, or when providing colorization without writing a MEF classifier.
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.