| description | Guidelines for Visual Studio extension (VSIX) development using Community.VisualStudio.Toolkit |
|---|---|
| applyTo | **/*.cs, **/*.vsct, **/*.xaml, **/source.extension.vsixmanifest |
These instructions apply ONLY to Visual Studio extensions using Community.VisualStudio.Toolkit.
Verify the project uses the toolkit by checking for:
Community.VisualStudio.Toolkit.*NuGet package referenceToolkitPackagebase class (not rawAsyncPackage)BaseCommand<T>pattern for commands
If the project uses raw VSSDK (AsyncPackage directly) or the new VisualStudio.Extensibility model, do not apply these instructions.
- Generate async-first, thread-safe extension code
- Use toolkit abstractions (
VS.*helpers,BaseCommand<T>,BaseOptionModel<T>) - Ensure all UI respects Visual Studio themes
- Follow VSSDK and VSTHRD analyzer rules
- Produce testable, maintainable extension code
- Adhere to
.editorconfigsettings when present in the repository
If an .editorconfig file exists in the repository, all generated and modified code MUST follow its rules.
This includes but is not limited to:
- Indentation style (tabs vs spaces) and size
- Line endings and final newline requirements
- Naming conventions (fields, properties, methods, etc.)
- Code style preferences (
varusage, expression bodies, braces, etc.) - Analyzer severity levels and suppressions
Before generating code, check for .editorconfig in the repository root and apply its settings. When in doubt, match the style of surrounding code in the file being edited.
Visual Studio extensions target .NET Framework 4.8 but can use modern C# syntax (up to C# 14) with constraints imposed by the .NET Framework runtime.
- Primary constructors
- File-scoped namespaces
- Global usings
- Pattern matching (all forms)
- Records (with limitations)
initaccessors- Target-typed
new - Nullable reference types (annotations only)
- Raw string literals
- Collection expressions
Span<T>,ReadOnlySpan<T>,Memory<T>(no runtime support)IAsyncEnumerable<T>(without polyfill packages)- Default interface implementations
IndexandRangetypes (no runtime support for^and..operators)init-only setters on structs (runtime limitation)- Some
System.Text.Jsonfeatures
When writing code, prefer APIs available in .NET Framework 4.8. If a modern API is needed, check if a polyfill NuGet package exists (e.g., Microsoft.Bcl.AsyncInterfaces for IAsyncEnumerable<T>).
- "Create a command that opens the current file's containing folder using
BaseCommand<T>" - "Add an options page with a boolean setting using
BaseOptionModel<T>" - "Write a tagger provider for C# files that highlights TODO comments"
- "Show a status bar progress indicator while processing files"
- Suggesting raw
AsyncPackageinstead ofToolkitPackage - Using
OleMenuCommandServicedirectly instead ofBaseCommand<T> - Creating WPF elements without switching to UI thread first
- Using
.Result,.Wait(), orTask.Runfor UI work - Hardcoding colors instead of using VS theme colors
src/
├── Commands/ # Command handlers (menu items, toolbar buttons)
├── Options/ # Settings/options pages
├── Services/ # Business logic and services
├── Tagging/ # ITagger implementations (syntax highlighting, outlining)
├── Adornments/ # Editor adornments (IntraTextAdornment, margins)
├── QuickInfo/ # QuickInfo/tooltip providers
├── SuggestedActions/ # Light bulb actions
├── Handlers/ # Event handlers (format document, paste, etc.)
├── Resources/ # Images, icons, license files
├── source.extension.vsixmanifest # Extension manifest
├── VSCommandTable.vsct # Command definitions (menus, buttons)
├── VSCommandTable.cs # Auto-generated command IDs
└── *Package.cs # Main package class
Extensions using the toolkit should have these global usings in the Package file:
global using System;
global using Community.VisualStudio.Toolkit;
global using Microsoft.VisualStudio.Shell;
global using Task = System.Threading.Tasks.Task;[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(PackageGuids.YourExtensionString)]
[ProvideOptionPage(typeof(OptionsProvider.GeneralOptions), Vsix.Name, "General", 0, 0, true, SupportsProfiles = true)]
public sealed class YourPackage : ToolkitPackage
{
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await this.RegisterCommandsAsync();
}
}Commands use the [Command] attribute and inherit from BaseCommand<T>:
[Command(PackageIds.YourCommandId)]
internal sealed class YourCommand : BaseCommand<YourCommand>
{
protected override async Task ExecuteAsync(OleMenuCmdEventArgs e)
{
// Command implementation
}
// Optional: Control command state (enabled, checked, visible)
protected override void BeforeQueryStatus(EventArgs e)
{
Command.Checked = someCondition;
Command.Enabled = anotherCondition;
}
}internal partial class OptionsProvider
{
[ComVisible(true)]
public class GeneralOptions : BaseOptionPage<General> { }
}
public class General : BaseOptionModel<General>
{
[Category("Category Name")]
[DisplayName("Setting Name")]
[Description("Description of the setting.")]
[DefaultValue(true)]
public bool MySetting { get; set; } = true;
}Use [Export] and appropriate [ContentType] attributes:
[Export(typeof(IViewTaggerProvider))]
[ContentType("CSharp")]
[ContentType("Basic")]
[TagType(typeof(IntraTextAdornmentTag))]
[TextViewRole(PredefinedTextViewRoles.Document)]
internal sealed class YourTaggerProvider : IViewTaggerProvider
{
[Import]
internal IOutliningManagerService OutliningManagerService { get; set; }
public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
{
if (textView == null || !(textView is IWpfTextView wpfTextView))
return null;
if (textView.TextBuffer != buffer)
return null;
return wpfTextView.Properties.GetOrCreateSingletonProperty(
() => new YourTagger(wpfTextView)) as ITagger<T>;
}
}[Export(typeof(IAsyncQuickInfoSourceProvider))]
[Name("YourQuickInfo")]
[ContentType("code")]
[Order(Before = "Default Quick Info Presenter")]
internal sealed class YourQuickInfoSourceProvider : IAsyncQuickInfoSourceProvider
{
public IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer)
{
return textBuffer.Properties.GetOrCreateSingletonProperty(
() => new YourQuickInfoSource(textBuffer));
}
}[Export(typeof(ISuggestedActionsSourceProvider))]
[Name("Your Suggested Actions")]
[ContentType("text")]
internal sealed class YourSuggestedActionsSourceProvider : ISuggestedActionsSourceProvider
{
public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, ITextBuffer textBuffer)
{
return new YourSuggestedActionsSource(textView, textBuffer);
}
}await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
// Now safe to create/modify WPF elementsThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
await VS.Commands.ExecuteAsync("View.TaskList");
});Extensions should enforce these analyzer rules. Add to .editorconfig:
dotnet_diagnostic.VSSDK*.severity = error
dotnet_diagnostic.VSTHRD*.severity = error| ID | Rule | Fix |
|---|---|---|
| VSSDK001 | Derive from AsyncPackage |
Use ToolkitPackage (derives from AsyncPackage) |
| VSSDK002 | AllowsBackgroundLoading = true |
Add to [PackageRegistration] |
| ID | Rule | Fix |
|---|---|---|
| VSTHRD001 | Avoid .Wait() |
Use await |
| VSTHRD002 | Avoid JoinableTaskFactory.Run |
Use RunAsync or await |
| VSTHRD010 | COM calls require UI thread | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync() |
| VSTHRD100 | No async void |
Use async Task |
| VSTHRD110 | Observe async results | await task; or suppress with pragma |
All UI must respect VS themes (Light, Dark, Blue, High Contrast)
<!-- MyControl.xaml -->
<UserControl x:Class="MyExt.MyControl"
xmlns:vsui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0">
<Grid Background="{DynamicResource {x:Static vsui:EnvironmentColors.ToolWindowBackgroundBrushKey}}">
<TextBlock Foreground="{DynamicResource {x:Static vsui:EnvironmentColors.ToolWindowTextBrushKey}}"
Text="Hello, themed world!" />
</Grid>
</UserControl>The toolkit provides automatic theming for WPF UserControls:
<UserControl x:Class="MyExt.MyUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:toolkit="clr-namespace:Community.VisualStudio.Toolkit;assembly=Community.VisualStudio.Toolkit"
toolkit:Themes.UseVsTheme="True">
<!-- Controls automatically get VS styling -->
</UserControl>For dialog windows, use DialogWindow:
<platform:DialogWindow
x:Class="MyExt.MyDialog"
xmlns:platform="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0"
xmlns:toolkit="clr-namespace:Community.VisualStudio.Toolkit;assembly=Community.VisualStudio.Toolkit"
toolkit:Themes.UseVsTheme="True">
</platform:DialogWindow>| Category | Token | Usage |
|---|---|---|
| Background | EnvironmentColors.ToolWindowBackgroundBrushKey |
Window/panel background |
| Foreground | EnvironmentColors.ToolWindowTextBrushKey |
Text |
| Command Bar | EnvironmentColors.CommandBarTextActiveBrushKey |
Menu items |
| Links | EnvironmentColors.ControlLinkTextBrushKey |
Hyperlinks |
Use KnownMonikers from the VS Image Catalog for theme-aware icons:
public ImageMoniker IconMoniker => KnownMonikers.Settings;In VSCT:
<Icon guid="ImageCatalogGuid" id="Settings"/>
<CommandFlag>IconIsMoniker</CommandFlag>// Status bar
await VS.StatusBar.ShowMessageAsync("Message");
await VS.StatusBar.ShowProgressAsync("Working...", currentStep, totalSteps);
// Solution/Projects
Solution solution = await VS.Solutions.GetCurrentSolutionAsync();
IEnumerable<SolutionItem> items = await VS.Solutions.GetActiveItemsAsync();
bool isOpen = await VS.Solutions.IsOpenAsync();
// Documents
DocumentView docView = await VS.Documents.GetActiveDocumentViewAsync();
string text = docView?.TextBuffer?.CurrentSnapshot.GetText();
await VS.Documents.OpenAsync(fileName);
await VS.Documents.OpenInPreviewTabAsync(fileName);
// Commands
await VS.Commands.ExecuteAsync("View.TaskList");
// Settings
await VS.Settings.OpenAsync<OptionsProvider.GeneralOptions>();
// Messages
await VS.MessageBox.ShowAsync("Title", "Message");
await VS.MessageBox.ShowErrorAsync("Extension Name", ex.ToString());
// Events
VS.Events.SolutionEvents.OnAfterOpenProject += OnAfterOpenProject;
VS.Events.DocumentEvents.Saved += OnDocumentSaved;// Read settings synchronously
var value = General.Instance.MyOption;
// Read settings asynchronously
var general = await General.GetLiveInstanceAsync();
var value = general.MyOption;
// Write settings
General.Instance.MyOption = newValue;
General.Instance.Save();
// Or async
general.MyOption = newValue;
await general.SaveAsync();
// Listen for settings changes
General.Saved += OnSettingsSaved;// Get snapshot
ITextSnapshot snapshot = textBuffer.CurrentSnapshot;
// Get line
ITextSnapshotLine line = snapshot.GetLineFromLineNumber(lineNumber);
string lineText = line.GetText();
// Create tracking span
ITrackingSpan trackingSpan = snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeInclusive);
// Edit buffer
using (ITextEdit edit = textBuffer.CreateEdit())
{
edit.Replace(span, newText);
edit.Apply();
}
// Insert at caret position
DocumentView docView = await VS.Documents.GetActiveDocumentViewAsync();
if (docView?.TextView != null)
{
SnapshotPoint position = docView.TextView.Caret.Position.BufferPosition;
docView.TextBuffer?.Insert(position, "text to insert");
}<Commands package="YourPackage">
<Menus>
<Menu guid="YourPackage" id="SubMenu" type="Menu">
<Parent guid="YourPackage" id="MenuGroup"/>
<Strings>
<ButtonText>Menu Name</ButtonText>
<CommandName>Menu Name</CommandName>
<CanonicalName>.YourExtension.MenuName</CanonicalName>
</Strings>
</Menu>
</Menus>
<Groups>
<Group guid="YourPackage" id="MenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN"/>
</Group>
</Groups>
<Buttons>
<Button guid="YourPackage" id="CommandId" type="Button">
<Parent guid="YourPackage" id="MenuGroup"/>
<Icon guid="ImageCatalogGuid" id="Settings"/>
<CommandFlag>IconIsMoniker</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
<Strings>
<ButtonText>Command Name</ButtonText>
<CanonicalName>.YourExtension.CommandName</CanonicalName>
</Strings>
</Button>
</Buttons>
</Commands>
<Symbols>
<GuidSymbol name="YourPackage" value="{guid-here}">
<IDSymbol name="MenuGroup" value="0x0001"/>
<IDSymbol name="CommandId" value="0x0100"/>
</GuidSymbol>
</Symbols>- Check file/buffer size before processing large documents
- Use
NormalizedSnapshotSpanCollectionfor efficient span operations - Cache parsed results when possible
- Use
ConfigureAwait(false)in library code
// Skip large files
if (buffer.CurrentSnapshot.Length > 150000)
return null;- Wrap external operations in try-catch
- Log errors appropriately
- Never let exceptions crash VS
try
{
// Operation
}
catch (Exception ex)
{
await ex.LogAsync();
}- Implement
IDisposableon taggers and other long-lived objects - Unsubscribe from events in Dispose
public void Dispose()
{
if (!_isDisposed)
{
_buffer.Changed -= OnBufferChanged;
_isDisposed = true;
}
}Common content types for [ContentType] attribute:
"text"- All text files"code"- All code files"CSharp"- C# files"Basic"- VB.NET files"CSS","LESS","SCSS"- Style files"TypeScript","JavaScript"- Script files"HTML","HTMLX"- HTML files"XML"- XML files"JSON"- JSON files
Use KnownMonikers from the VS Image Catalog:
public ImageMoniker IconMoniker => KnownMonikers.Settings;In VSCT:
<Icon guid="ImageCatalogGuid" id="Settings"/>
<CommandFlag>IconIsMoniker</CommandFlag>- Use
[VsTestMethod]for tests requiring VS context - Mock VS services when possible
- Test business logic separately from VS integration
| Pitfall | Solution |
|---|---|
| Blocking UI thread | Always use async/await |
| Creating WPF on background thread | Call SwitchToMainThreadAsync() first |
| Ignoring cancellation tokens | Pass them through async chains |
| VSCommandTable.cs mismatch | Regenerate after VSCT changes |
| Hardcoded GUIDs | Use PackageGuids and PackageIds constants |
| Swallowing exceptions | Log with await ex.LogAsync() |
| Missing DynamicVisibility | Required for BeforeQueryStatus to work |
Using .Result, .Wait() |
Causes deadlocks; always await |
| Hardcoded colors | Use VS theme colors (EnvironmentColors) |
async void methods |
Use async Task instead |
Build and verify the extension:
msbuild /t:rebuildEnsure analyzers are enabled in .editorconfig:
dotnet_diagnostic.VSSDK*.severity = error
dotnet_diagnostic.VSTHRD*.severity = errorTest in VS Experimental Instance before release.
| Package | Purpose |
|---|---|
Community.VisualStudio.Toolkit.17 |
Simplifies VS extension development |
Microsoft.VisualStudio.SDK |
Core VS SDK |
Microsoft.VSSDK.BuildTools |
Build tools for VSIX |
Microsoft.VisualStudio.Threading.Analyzers |
Threading analyzers |
Microsoft.VisualStudio.SDK.Analyzers |
VSSDK analyzers |
A good README works on both GitHub and the VS Marketplace. The Marketplace uses the README.md as the extension's description page.
[marketplace]: https://marketplace.visualstudio.com/items?itemName=Publisher.ExtensionName
[repo]: https://github.com/user/repo
# Extension Name
[](...)
[][marketplace]
[][marketplace]
Download this extension from the [Visual Studio Marketplace][marketplace]
or get the [CI build](http://vsixgallery.com/extension/ExtensionId/).
--------------------------------------
**Hook line that sells the extension in one sentence.**

## Features
### Feature 1
Description with screenshot...
## How to Use
...
## License
[Apache 2.0](LICENSE)| Element | Guideline |
|---|---|
| Title | Use the same name as DisplayName in vsixmanifest |
| Hook line | Bold, one-sentence value proposition immediately after badges |
| Screenshots | Place in /art folder, use relative paths (art/image.png) |
| Image sizes | Keep under 1MB, 800-1200px wide for clarity |
| Badges | Version, downloads, rating, build status |
| Feature sections | Use H3 (###) with screenshots for each major feature |
| Keyboard shortcuts | Format as Ctrl+M, Ctrl+C (bold) |
| Tables | Great for comparing options or listing features |
| Links | Use reference-style links at top for cleaner markdown |
<Metadata>
<Identity Id="ExtensionName.guid-here" Version="1.0.0" Language="en-US" Publisher="Your Name" />
<DisplayName>Extension Name</DisplayName>
<Description xml:space="preserve">Short, compelling description under 200 chars. This appears in search results and the extension tile.</Description>
<MoreInfo>https://github.com/user/repo</MoreInfo>
<License>Resources\LICENSE.txt</License>
<Icon>Resources\Icon.png</Icon>
<PreviewImage>Resources\Preview.png</PreviewImage>
<Tags>keyword1, keyword2, keyword3</Tags>
</Metadata>| Element | Guideline |
|---|---|
| DisplayName | 3-5 words, no "for Visual Studio" (implied) |
| Description | Under 200 chars, focus on value not features. Appears in search tiles |
| Tags | 5-10 relevant keywords, comma-separated, helps discoverability |
| Icon | 128x128 or 256x256 PNG, simple design visible at small sizes |
| PreviewImage | 200x200 PNG, can be same as Icon or a feature screenshot |
| MoreInfo | Link to GitHub repo for documentation and issues |
- Lead with benefits, not features - "Stop wrestling with XML comments" beats "XML comment formatter"
- Show, don't tell - Screenshots are more convincing than descriptions
- Use consistent terminology - Match terms between README, manifest, and UI
- Keep the description scannable - Short paragraphs, bullet points, tables
- Include keyboard shortcuts - Users love productivity tips
- Add a "Why" section - Explain the problem before the solution