| name | writing-macros |
| description | How to write .csx macros for Visual Studio automation in the Macros repo |
| domain | visual-studio-macros |
| confidence | high |
| source | derived from Macros.Engine codegen, helpers, globals, and docs |
Writing .csx macros
Write or edit Macros .csx files that automate Visual Studio from inside the Macros extension.
When to use this skill
Use this skill when the user wants a new macro, wants to edit an existing macro, or asks what a valid .csx macro file should look like in this repo.
Do not use this skill for VSIX authoring, MEF parts, tool windows, commands, or other Visual Studio extensibility patterns.
Canonical macro shape
The generator emits this header shape before the script body:
#load ".intellisense/Macros.Intellisense.csx"
await TypeAsync("Hello from a macro");
await ExecuteCommandAsync("Edit.FormatDocument");
Keep trigger comments in the top block. Put executable code after #load.
The #load directive
Always keep this line near the top:
#load ".intellisense/Macros.Intellisense.csx"
What it does:
- Enables IntelliSense for helpers, DTE, Toolkit APIs,
Context, and Trigger
- Supplies editor-time references and common imports
- Keeps the macro file small and readable
Runtime behavior:
- The player intercepts this
#load and returns empty source
- The shim is editor-only; real globals are injected when the macro runs
No #r or using lines needed
Do not add standard #r directives for EnvDTE, EnvDTE80, Toolkit, or Macros.Engine.
Do not add standard using statements just to get macro basics working.
The shim and script host already provide the common references/imports.
Helpers available in every macro
These helpers are available without extra imports:
await TypeAsync("text");
await MoveCaretAsync(3, 1);
await SelectAsync(3, 1, 3, 10);
await ExecuteCommandAsync("Edit.FormatDocument");
await RunCommandAsync(new Guid("00000000-0000-0000-0000-000000000000"), 42);
await OpenFileAsync(@"C:\Repos\MySolution\Program.cs");
await WaitAsync(250);
Use them as the default vocabulary for text insertion, selection, command replay, file opening, and timing.
Globals available in every macro
The macro host exposes these names directly:
DTE — EnvDTE80.DTE2
VS — the Community.VisualStudio.Toolkit facade
Context — IMacroContext
Trigger — the current macro trigger object
if (DTE.ActiveDocument != null)
{
await VS.StatusBar.ShowMessageAsync($"Running {Context.MacroName}");
}
Common patterns
Insert text
await TypeAsync("// TODO: follow up\n");
Run a Visual Studio command
await ExecuteCommandAsync("Edit.FormatDocument");
await ExecuteCommandAsync("File.SaveSelectedItems");
Open a file, then work in it
await OpenFileAsync(@"C:\Repos\MySolution\README.md");
await MoveCaretAsync(1, 1);
await TypeAsync("# Notes\n\n");
Read and replace selected text with DTE
var sel = DTE.ActiveDocument?.Selection as EnvDTE.TextSelection;
if (sel?.IsEmpty == false)
{
sel.Insert(sel.Text.ToUpperInvariant(), (int)EnvDTE.vsInsertFlags.vsInsertFlagsInsertAtStart);
}
Authoring rules
- Write top-level code only; no class or
Main() needed
- Use
await with every async helper call
- Prefer helpers first, then drop to
DTE or VS
- Guard against missing state such as no active document or empty selection
- Keep macros focused on IDE automation, not extension authoring
How the player runs macros
Macros run in-process inside Visual Studio through Roslyn C# scripting.
await Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync(
source,
globals: new MacroGlobals(dte, context));
That means top-level await works, real globals are injected at runtime, and helper methods can marshal to the UI thread before touching the IDE.
Good defaults for Copilot
- Start with the canonical header block
- Keep the
#load line
- Skip
#r and standard using lines
- Prefer
TypeAsync, ExecuteCommandAsync, OpenFileAsync, and WaitAsync
- Use
DTE and VS only when the helper vocabulary is not enough