Explains how to add custom margins (gutter, side panel, bottom bar) to the Visual Studio text editor via VS SDK, VSIX Toolkit, or VisualStudio.Extensibility. Includes MEF configuration and margin vs. adornment guidance.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vs-extensibility-skills:adding-editor-marginsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Editor margins are UI panels attached to the edges of the text editor — left (gutter), right, top, or bottom. Common scenarios:
Editor margins are UI panels attached to the edges of the text editor — left (gutter), right, top, or bottom. Common scenarios:
Margins are the natural location for UI that tracks editor content line-by-line (gutters) or provides persistent supplementary information (bottom/top panels). Unlike adornments which draw over text, margins have their own reserved space and don't interfere with text selection or scrolling. The VisualStudio.Extensibility model now supports margins via Remote UI, while the VSSDK/Toolkit approach uses WPF directly.
When to use margins vs. alternatives:
IGlyphFactory (this skill)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 margin 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%|" />
Note: The VisualStudio.Extensibility approach does NOT require this MEF asset entry.
The VisualStudio.Extensibility SDK supports editor margins via VisualStudioContribution and Remote UI (XAML).
NuGet package: Microsoft.VisualStudio.Extensibility
Key namespace: Microsoft.VisualStudio.Extensibility.Editor
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Editor;
using Microsoft.VisualStudio.RpcContracts.RemoteUI;
namespace MyExtension;
[VisualStudioContribution]
internal sealed class MyEditorMargin : ExtensionPart, ITextViewMarginProvider
{
public TextViewMarginProviderConfiguration TextViewMarginProviderConfiguration => new(
marginContainer: ContainerMarginPlacement.KnownValues.BottomControl)
{
Before = new[] { MarginPlacement.KnownValues.RowMargin },
};
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = new[]
{
DocumentFilter.FromDocumentType("CSharp"),
},
};
public Task<IRemoteUserControl> CreateVisualElementAsync(
ITextViewSnapshot textView,
CancellationToken cancellationToken)
{
return Task.FromResult<IRemoteUserControl>(
new MyMarginControl(textView));
}
}
using Microsoft.VisualStudio.Extensibility.UI;
namespace MyExtension;
internal sealed class MyMarginControl : RemoteUserControl
{
private readonly ITextViewSnapshot _textView;
public MyMarginControl(ITextViewSnapshot textView)
: base(new MyMarginData())
{
_textView = textView;
}
public override Task<string> GetXamlAsync(CancellationToken cancellationToken)
{
return Task.FromResult("""
<DataTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Border Background="#1E1E1E" Padding="4">
<TextBlock Text="{Binding Message}"
Foreground="LightGray"
FontSize="11" />
</Border>
</DataTemplate>
""");
}
}
internal sealed class MyMarginData : NotifyPropertyChangedObject
{
private string _message = "Custom margin loaded";
public string Message
{
get => _message;
set => SetProperty(ref _message, value);
}
}
| Placement | Description |
|---|---|
ContainerMarginPlacement.KnownValues.BottomControl | Below the editor |
ContainerMarginPlacement.KnownValues.TopControl | Above the editor |
ContainerMarginPlacement.KnownValues.LeftControl | Left of the editor (gutter area) |
ContainerMarginPlacement.KnownValues.RightControl | Right of the editor |
The Community Toolkit does not wrap the margin API — margins use the same MEF-based VSSDK pattern described below.
NuGet packages: Microsoft.VisualStudio.SDK, Microsoft.VisualStudio.Editor, Microsoft.VisualStudio.Text.UI.Wpf
Key namespaces: Microsoft.VisualStudio.Text.Editor, Microsoft.VisualStudio.Utilities
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Editor;
namespace MyExtension;
internal sealed class MyMargin : Canvas, IWpfTextViewMargin
{
public const string MarginName = "MyCustomMargin";
private readonly IWpfTextView _textView;
private bool _disposed;
private readonly TextBlock _label;
public MyMargin(IWpfTextView textView)
{
_textView = textView;
Height = 25;
ClipToBounds = true;
Background = new SolidColorBrush(Color.FromRgb(0x2D, 0x2D, 0x2D));
_label = new TextBlock
{
Text = "Custom Margin",
Foreground = Brushes.LightGray,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(8, 4, 0, 0),
FontSize = 11,
};
Children.Add(_label);
_textView.Caret.PositionChanged += OnCaretPositionChanged;
}
private void OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
{
var line = _textView.Caret.Position.BufferPosition.GetContainingLine();
_label.Text = $"Line {line.LineNumber + 1}, Col {_textView.Caret.Position.BufferPosition.Position - line.Start.Position + 1}";
}
// IWpfTextViewMargin
public FrameworkElement VisualElement => this;
public double MarginSize => ActualHeight;
public bool Enabled => true;
public ITextViewMargin GetTextViewMargin(string marginName)
{
return string.Equals(marginName, MarginName, StringComparison.OrdinalIgnoreCase)
? this
: null;
}
public void Dispose()
{
if (!_disposed)
{
_textView.Caret.PositionChanged -= OnCaretPositionChanged;
_disposed = true;
}
}
}
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
namespace MyExtension;
[Export(typeof(IWpfTextViewMarginProvider))]
[Name(MyMargin.MarginName)]
[Order(After = PredefinedMarginNames.HorizontalScrollBar)]
[MarginContainer(PredefinedMarginNames.Bottom)]
[ContentType("text")]
[TextViewRole(PredefinedTextViewRoles.Interactive)]
internal sealed class MyMarginProvider : IWpfTextViewMarginProvider
{
public IWpfTextViewMargin CreateMargin(
IWpfTextViewHost wpfTextViewHost,
IWpfTextViewMargin marginContainer)
{
return new MyMargin(wpfTextViewHost.TextView);
}
}
For a left-side gutter with per-line glyphs, use IGlyphFactory and IGlyphFactoryProvider:
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Formatting;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
namespace MyExtension;
[Export(typeof(IGlyphFactoryProvider))]
[Name("MyGlyphFactory")]
[Order(After = "VsTextMarker")]
[ContentType("text")]
[TagType(typeof(MyGlyphTag))]
internal sealed class MyGlyphFactoryProvider : IGlyphFactoryProvider
{
public IGlyphFactory GetGlyphFactory(IWpfTextView view, IWpfTextViewMargin margin)
{
return new MyGlyphFactory();
}
}
internal sealed class MyGlyphFactory : IGlyphFactory
{
public UIElement GenerateGlyph(IWpfTextViewLine line, ITag tag)
{
if (tag is not MyGlyphTag)
return null;
return new Ellipse
{
Width = 12,
Height = 12,
Fill = Brushes.OrangeRed,
};
}
}
// Custom tag to trigger glyph display — use with ITaggerProvider
internal sealed class MyGlyphTag : IGlyphTag { }
You'll also need a tagger that produces MyGlyphTag spans — see the vs-editor-tagger skill for tagger implementation details.
| Container | Description |
|---|---|
PredefinedMarginNames.Bottom | Below the horizontal scrollbar |
PredefinedMarginNames.Top | Above the editor |
PredefinedMarginNames.Left | Left gutter area |
PredefinedMarginNames.Right | Right of the editor |
PredefinedMarginNames.BottomControl | Below editor, above bottom margin |
PredefinedMarginNames.LeftSelection | Left of selection margin |
PredefinedMarginNames.Glyph | The glyph margin (breakpoints, etc.) |
PredefinedMarginNames.LineNumber | Line number margin |
FrameworkElement instances — you have full WPF control.Height. For left/right margins, set Width.Dispose to clean up event subscriptions.[MarginContainer] to specify where the margin is placed.[Order] to control position relative to other margins.IGlyphFactory with a custom tag..vsixmanifest for the VSSDK approach.ITextViewMarginProvider with Remote UI for out-of-process margins. Supports bottom, top, left, and right placements.IWpfTextViewMarginProvider via MEF. Implement IWpfTextViewMargin. For gutter glyphs, use IGlyphFactoryProvider.source.extension.vsixmanifest for the VSSDK approach.Dispose to avoid memory leaks..vsixmanifest. Verify [MarginContainer] uses a valid PredefinedMarginNames value. Verify [ContentType] matches the file type.Height (top/bottom margins) or Width (left/right margins) on your margin control.IGlyphFactoryProvider exports [TagType(typeof(YourTag))] and that a tagger is producing YourTag instances for the correct spans.LayoutChanged handler. Pre-compute on a background thread.IDisposable on the margin or not unsubscribing from text buffer events in Dispose.Do NOT forget to clean up event subscriptions in
Dispose()— margins are created/destroyed as views open/close. Leaked subscriptions cause memory leaks.
Do NOT forget to set
Height(bottom/top) orWidth(left/right) on your margin control — without explicit sizing, the margin renders with zero size.
Do NOT use the wrong
[MarginContainer]value — a typo or wrongPredefinedMarginNamesconstant causes the margin to silently not appear.
Do NOT forget the
MefComponentasset type in.vsixmanifestfor VSSDK/Toolkit. Not required for VisualStudio.Extensibility.
Do NOT do heavy work in the margin constructor or
LayoutChangedhandler — slow margins cause visible lag when scrolling and editing.
npx claudepluginhub madskristensen/vs-agent-plugins --plugin vs-extensibility-skillsAdds visual decorations, overlays, and highlights to the Visual Studio text editor. Covers VSIX, VSSDK, and VisualStudio.Extensibility approaches.
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.