Adds light bulb code actions, quick fixes, and suggested actions to Visual Studio editor. Covers VisualStudio.Extensibility, VSIX Community Toolkit, and legacy VSSDK approaches.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vs-extensibility-skills:adding-lightbulb-actionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Light bulb actions appear when the user clicks the light bulb icon or presses Ctrl+. — they offer quick fixes, refactorings, and suggestions. Common scenarios:
Light bulb actions appear when the user clicks the light bulb icon or presses Ctrl+. — they offer quick fixes, refactorings, and suggestions. Common scenarios:
The light bulb is VS's primary mechanism for presenting actionable code suggestions at the cursor position — users learn to press Ctrl+. as a reflex.
When to use light bulb actions vs. alternatives:
.vsixmanifestISuggestedAction)ISuggestedActionsSource)ISuggestedActionsSourceProvider)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 suggested actions will not appear.
Add this inside the <Assets> element of source.extension.vsixmanifest:
<Asset Type="Microsoft.VisualStudio.MefComponent"
d:Source="Project"
d:ProjectName="%CurrentProject%"
Path="|%CurrentProject%|" />
The VisualStudio.Extensibility SDK does not directly support suggested actions / light bulb actions through its own APIs.
However, for Roslyn-based languages (C#, VB), you can create Roslyn code fix providers and code refactoring providers which appear as light bulb actions. These use the Microsoft.CodeAnalysis APIs and are the recommended approach for C#/VB light bulb actions:
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MyCodeFixProvider))]
[Shared]
public sealed class MyCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create("MY001");
public override FixAllProvider GetFixAllProvider() =>
WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics[0];
var diagnosticSpan = diagnostic.Location.SourceSpan;
context.RegisterCodeFix(
CodeAction.Create(
title: "Fix this issue",
createChangedDocument: ct => FixDocumentAsync(context.Document, diagnosticSpan, ct),
equivalenceKey: "MyFix"),
diagnostic);
}
private async Task<Document> FixDocumentAsync(
Document document, TextSpan span, CancellationToken cancellationToken)
{
// Implement the fix by modifying the syntax tree
var root = await document.GetSyntaxRootAsync(cancellationToken);
// ... modify root ...
return document.WithSyntaxRoot(root);
}
}
For non-Roslyn languages, use the VSSDK in-process approach.
The Community Toolkit does not wrap the suggested actions API — it uses the same MEF-based VSSDK pattern described below.
Light bulb actions use the ISuggestedActionsSourceProvider and ISuggestedAction APIs.
NuGet packages: Microsoft.VisualStudio.SDK, Microsoft.VisualStudio.Language.Intellisense
Key namespace: Microsoft.VisualStudio.Language.Intellisense
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Imaging.Interop;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
namespace MyExtension;
internal sealed class ConvertToUpperCaseAction : ISuggestedAction
{
private readonly ITrackingSpan _span;
private readonly string _display;
public ConvertToUpperCaseAction(ITrackingSpan span)
{
_span = span;
_display = $"Convert '{span.GetText(span.TextBuffer.CurrentSnapshot)}' to UPPER CASE";
}
public string DisplayText => _display;
public string IconAutomationText => null;
public ImageMoniker IconMoniker => KnownMonikers.Transform;
public string InputGestureText => null;
public bool HasActionSets => false;
public bool HasPreview => true;
public Task<IEnumerable<SuggestedActionSet>> GetActionSetsAsync(CancellationToken cancellationToken)
{
return Task.FromResult<IEnumerable<SuggestedActionSet>>(null);
}
public Task<object> GetPreviewAsync(CancellationToken cancellationToken)
{
string currentText = _span.GetText(_span.TextBuffer.CurrentSnapshot);
return Task.FromResult<object>(currentText.ToUpperInvariant());
}
public void Invoke(CancellationToken cancellationToken)
{
var snapshot = _span.TextBuffer.CurrentSnapshot;
var spanToReplace = _span.GetSpan(snapshot);
string upperText = spanToReplace.GetText().ToUpperInvariant();
_span.TextBuffer.Replace(spanToReplace, upperText);
}
public bool TryGetTelemetryId(out Guid telemetryId)
{
telemetryId = Guid.Empty;
return false;
}
public void Dispose() { }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
namespace MyExtension;
internal sealed class MyActionsSource : ISuggestedActionsSource
{
private readonly ITextStructureNavigatorSelectorService _navigatorService;
private readonly ITextView _textView;
private readonly ITextBuffer _textBuffer;
public MyActionsSource(
ITextStructureNavigatorSelectorService navigatorService,
ITextView textView,
ITextBuffer textBuffer)
{
_navigatorService = navigatorService;
_textView = textView;
_textBuffer = textBuffer;
}
public event EventHandler<EventArgs> SuggestedActionsChanged;
public IEnumerable<SuggestedActionSet> GetSuggestedActions(
ISuggestedActionCategorySet requestedActionCategories,
SnapshotSpan range,
CancellationToken cancellationToken)
{
if (TryGetWordUnderCaret(out var extent) && extent.IsSignificant)
{
var trackingSpan = range.Snapshot.CreateTrackingSpan(
extent.Span, SpanTrackingMode.EdgeInclusive);
var upperCaseAction = new ConvertToUpperCaseAction(trackingSpan);
return new[]
{
new SuggestedActionSet(
categoryName: PredefinedSuggestedActionCategoryNames.Refactoring,
actions: new[] { upperCaseAction })
};
}
return Enumerable.Empty<SuggestedActionSet>();
}
public Task<bool> HasSuggestedActionsAsync(
ISuggestedActionCategorySet requestedActionCategories,
SnapshotSpan range,
CancellationToken cancellationToken)
{
return Task.FromResult(
TryGetWordUnderCaret(out var extent) && extent.IsSignificant);
}
private bool TryGetWordUnderCaret(out TextExtent wordExtent)
{
var caret = _textView.Caret.Position;
var point = caret.Point.GetPoint(_textBuffer, caret.Affinity);
if (point.HasValue)
{
var navigator = _navigatorService.GetTextStructureNavigator(_textBuffer);
wordExtent = navigator.GetExtentOfWord(point.Value);
return true;
}
wordExtent = default;
return false;
}
public bool TryGetTelemetryId(out Guid telemetryId)
{
telemetryId = Guid.Empty;
return false;
}
public void Dispose() { }
}
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
namespace MyExtension;
[Export(typeof(ISuggestedActionsSourceProvider))]
[Name("My Suggested Actions")]
[ContentType("text")]
internal sealed class MyActionsSourceProvider : ISuggestedActionsSourceProvider
{
[Import]
internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
public ISuggestedActionsSource CreateSuggestedActionsSource(
ITextView textView, ITextBuffer textBuffer)
{
if (textBuffer == null || textView == null)
return null;
return new MyActionsSource(NavigatorService, textView, textBuffer);
}
}
Use categories to control where your actions appear:
| Category | Purpose |
|---|---|
PredefinedSuggestedActionCategoryNames.CodeFix | Error/warning fixes |
PredefinedSuggestedActionCategoryNames.Refactoring | Refactoring operations |
PredefinedSuggestedActionCategoryNames.Any | All categories |
You can group related actions under a parent:
public Task<IEnumerable<SuggestedActionSet>> GetActionSetsAsync(
CancellationToken cancellationToken)
{
var childActions = new ISuggestedAction[]
{
new ConvertToUpperCaseAction(_span),
new ConvertToLowerCaseAction(_span),
};
return Task.FromResult<IEnumerable<SuggestedActionSet>>(
new[] { new SuggestedActionSet(actions: childActions) });
}
Set HasActionSets => true on the parent action.
HasSuggestedActionsAsync is called frequently as the caret moves — keep it fast.GetSuggestedActions returns the actual action objects — also should be fast.Invoke is where the actual code modification happens.HasPreview = true and implement GetPreviewAsync to show a preview diff.KnownMonikers for action icons.CodeFixProvider / CodeRefactoringProvider over ISuggestedAction..vsixmanifest — see the top of this document.CodeFixProvider / CodeRefactoringProvider for C#/VB. Not supported for other languages via the new model.ISuggestedActionsSourceProvider via MEF. Implement ISuggestedAction for each action. Use ISuggestedActionsSource to decide when actions are available.source.extension.vsixmanifest.HasSuggestedActionsAsync lightweight — it runs on every caret movement..vsixmanifest. Verify [ContentType] matches the file type. Also check that HasSuggestedActionsAsync returns true for the current caret position.Invoke doesn't modify the document: Ensure you're applying edits via ITextBuffer.Replace or Roslyn workspace APIs. If using Roslyn, verify your CodeAction returns non-empty change sets.HasSuggestedActionsAsync causes editor lag: This method fires on every caret movement. It must be very fast. Move any parsing or analysis to a background thread and cache the result; just check the cache in HasSuggestedActionsAsync.HasPreview returns true and GetPreviewAsync returns a valid Task<object>. For Roslyn code fixes, preview is automatic.[ContentType] attribute on the provider.Do NOT do expensive work in
HasSuggestedActionsAsync— it runs on every caret movement. Pre-compute availability on a background thread and return a cached result.
Do NOT use the
ISuggestedActionMEF API for C#/VB when RoslynCodeFixProvider/CodeRefactoringProvideris available — Roslyn providers integrate with the analyzer pipeline and supportFixAllautomatically.
Do NOT forget the
MefComponentasset type in.vsixmanifest— without it, your suggested actions provider is silently ignored.
Do NOT forget to implement
IDisposableon yourISuggestedActionsSource— VS creates and disposes sources as the caret moves; leaked sources accumulate memory.
npx claudepluginhub madskristensen/vs-agent-plugins --plugin vs-extensibility-skillsExtends the Visual Studio editor lightbulb with custom code fixes and refactorings via the MEF-based ISuggestedAction API. Covers legacy, category-based, and async streaming APIs for non-Roslyn languages.
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.