diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aa294f3..5e1b458 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,14 +19,14 @@ jobs:
with:
dotnet-version: 8.0.x
- - name: Restore dependencies
- run: dotnet restore
+ - name: Create artifacts directory
+ run: New-Item -ItemType Directory -Force -Path artifacts/packages
- name: Build SDK
- run: dotnet build src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj -c Release --no-restore
+ run: dotnet build src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj -c Release
- name: Build Templates
- run: dotnet pack src/CodingWithCalvin.VsixSdk.Templates/CodingWithCalvin.VsixSdk.Templates.csproj -c Release --no-restore
+ run: dotnet pack src/CodingWithCalvin.VsixSdk.Templates/CodingWithCalvin.VsixSdk.Templates.csproj -c Release
- name: Build Sample Extension
run: dotnet build samples/SampleExtension/SampleExtension.csproj -c Release
diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml
new file mode 100644
index 0000000..78918a7
--- /dev/null
+++ b/.github/workflows/contributors.yml
@@ -0,0 +1,40 @@
+name: Update Contributors
+
+on:
+ schedule:
+ - cron: '0 0 * * *' # Run daily at midnight UTC
+ workflow_dispatch: # Allow manual trigger
+
+jobs:
+ contributors:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.CONTRIBUTORS_TOKEN }}
+
+ - name: Update contributors
+ env:
+ GH_TOKEN: ${{ secrets.CONTRIBUTORS_TOKEN }}
+ run: |
+ # Fetch contributors from GitHub API (exclude bots) - markdown format
+ contributors=$(gh api repos/CodingWithCalvin/VsixSdk/contributors --paginate --jq '.[] | select(.type != "Bot") | select(.login | test("\\[bot\\]$") | not) | "[&s=64)](\(.html_url))"' | tr '\n' ' ')
+
+ # Build the contributors section
+ contrib_section="
+$contributors
+"
+
+ # Update README between the markers
+ awk -v contrib="$contrib_section" '
+ //{found=1; print contrib; next}
+ //{found=0; next}
+ !found{print}
+ ' README.md > README.tmp && mv README.tmp README.md
+
+ - name: Commit and push
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add README.md
+ git diff --staged --quiet || (git commit -m "docs: update contributors [skip ci]" && git push)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5836309..9fcc5ff 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,6 +12,8 @@ jobs:
release:
needs: changelog
runs-on: windows-latest
+ outputs:
+ version: ${{ steps.version.outputs.VERSION }}
permissions:
contents: write
@@ -64,3 +66,43 @@ jobs:
files: |
artifacts/packages/CodingWithCalvin.VsixSdk.${{ steps.version.outputs.VERSION }}.nupkg
artifacts/packages/CodingWithCalvin.VsixSdk.Templates.${{ steps.version.outputs.VERSION }}.nupkg
+
+ notify-bluesky:
+ needs: release
+ uses: CodingWithCalvin/.github/.github/workflows/bluesky-post.yml@main
+ with:
+ post_text: |
+ 🚀 CodingWithCalvin.VsixSdk v${{ needs.release.outputs.version }} has been released!
+
+ Build modern SDK-style Visual Studio extensions with ease.
+
+ [📋 Release Notes](https://github.com/${{ github.repository }}/releases/tag/v${{ needs.release.outputs.version }})
+ [📦 NuGet](https://www.nuget.org/packages/CodingWithCalvin.VsixSdk)
+
+ #dotnet #csharp #visualstudio #nuget
+ embed_url: https://www.nuget.org/packages/CodingWithCalvin.VsixSdk
+ embed_title: CodingWithCalvin.VsixSdk
+ embed_description: An MSBuild SDK for modern SDK-style Visual Studio extension development
+ secrets:
+ BLUESKY_USERNAME: ${{ secrets.BLUESKY_USERNAME }}
+ BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }}
+
+ notify-linkedin:
+ needs: release
+ uses: CodingWithCalvin/.github/.github/workflows/linkedin-post.yml@main
+ with:
+ post_text: |
+ 🚀 CodingWithCalvin.VsixSdk v${{ needs.release.outputs.version }} has been released!
+
+ Build modern SDK-style Visual Studio extensions with ease.
+
+ 📋 Release Notes: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.release.outputs.version }}
+ 📦 NuGet: https://www.nuget.org/packages/CodingWithCalvin.VsixSdk
+
+ #dotnet #csharp #visualstudio #nuget
+ article_url: https://www.nuget.org/packages/CodingWithCalvin.VsixSdk
+ article_title: CodingWithCalvin.VsixSdk
+ article_description: An MSBuild SDK for modern SDK-style Visual Studio extension development
+ secrets:
+ LINKEDIN_ACCESS_TOKEN: ${{ secrets.LINKEDIN_ACCESS_TOKEN }}
+ LINKEDIN_CLIENT_ID: ${{ secrets.LINKEDIN_CLIENT_ID }}
diff --git a/.gitignore b/.gitignore
index 90fec5f..469802b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,8 @@ Desktop.ini
CLAUDE.local.md
.claude/
+
+# Generated files from SDK
+Generated/
+VsixInfo.g.cs
+*Vsct.g.cs
diff --git a/README.md b/README.md
index 2652373..52bf847 100644
--- a/README.md
+++ b/README.md
@@ -1,48 +1,71 @@
-# 🚀 CodingWithCalvin.VsixSdk
+# CodingWithCalvin.VsixSdk
[](https://www.nuget.org/packages/CodingWithCalvin.VsixSdk)
[](https://opensource.org/licenses/MIT)
-> ✨ **Finally!** Modern SDK-style projects for Visual Studio extensions!
+An MSBuild SDK that brings modern SDK-style `.csproj` files to Visual Studio extension development. No more XML soup!
-An MSBuild SDK that brings the clean, modern `.csproj` format to VSIX development. No more XML soup! 🎉
+## Why This Exists
----
+Visual Studio extension projects are stuck in the past. They still use the old, verbose project format while the rest of .NET has moved on to clean SDK-style projects.
-## 🤔 Why This Exists
+**This SDK fixes that.**
-Visual Studio extension projects are stuck in 2010. They still use the old, verbose project format while the rest of .NET has moved on to beautiful SDK-style projects.
+Write clean `.csproj` files. Get source generators for compile-time constants. Ship fully functional VSIX packages.
-**This SDK fixes that.**
+## Getting Started
-Write clean `.csproj` files. Get all the modern tooling. Ship fully functional VSIX packages. 💪
+### Using the Template (Recommended)
----
+The easiest way to create a new VSIX project is with the dotnet template:
-## 📦 Installation
+```bash
+# Install the template
+dotnet new install CodingWithCalvin.VsixSdk.Templates
-```
-dotnet add package CodingWithCalvin.VsixSdk
+# Create a new extension
+dotnet new vsix -n MyExtension --publisher "Your Name" --description "My awesome extension"
+
+# Build it
+cd MyExtension
+dotnet build
```
-Or reference it directly in your project file (recommended):
+#### Template Parameters
-```xml
-
-```
+| Parameter | Short | Description | Default |
+|-----------|-------|-------------|---------|
+| `--extensionName` | `-e` | Display name in VS extension manager | Project name |
+| `--publisher` | `-p` | Publisher name in VSIX manifest | MyPublisher |
+| `--description` | `-de` | Extension description | A Visual Studio extension |
+| `--tags` | `-ta` | Comma-separated tags for discoverability | extension |
----
+**Examples:**
+
+```bash
+# Basic - uses project name as display name
+dotnet new vsix -n MyExtension
+
+# With custom extension name (different from project name)
+dotnet new vsix -n MyExtension.Vsix --extensionName "My Cool Extension"
+
+# With all parameters
+dotnet new vsix -n MyExtension \
+ --extensionName "My Cool Extension" \
+ --publisher "Acme Corp" \
+ --description "Adds productivity features to Visual Studio" \
+ --tags "productivity, tools, editor"
+```
-## ⚡ Quick Start
+### Manual Setup
-### 1️⃣ Create the Project File
+If you prefer to set up manually, create a `.csproj` file:
```xml
net472
- 1.0.0
@@ -53,17 +76,13 @@ Or reference it directly in your project file (recommended):
```
-That's it. Seriously. 😎
-
-### 2️⃣ Create the VSIX Manifest
-
-Create `source.extension.vsixmanifest`:
+Then create `source.extension.vsixmanifest`:
```xml
-
+
My Extension
Description of your extension
your, tags, here
@@ -85,81 +104,158 @@ Create `source.extension.vsixmanifest`:
```
-> 💡 **Pro tip:** The `Version="|%CurrentProject%;GetVsixVersion|"` syntax automatically syncs with your project's `Version` property!
+## Features
+
+### Source Generators
+
+The SDK includes source generators that create compile-time constants from your manifest files.
-### 3️⃣ Create Your Package Class
+#### VsixInfo - VSIX Manifest Constants
+
+A `VsixInfo` class is automatically generated from your `source.extension.vsixmanifest`:
```csharp
-using System;
-using System.Runtime.InteropServices;
-using System.Threading;
-using Microsoft.VisualStudio.Shell;
-using Task = System.Threading.Tasks.Task;
+// Auto-generated from your manifest
+public static class VsixInfo
+{
+ public const string Id = "MyExtension.a1b2c3d4-...";
+ public const string Version = "1.0.0";
+ public const string Publisher = "Your Name";
+ public const string DisplayName = "My Extension";
+ public const string Description = "Description of your extension";
+ public const string Tags = "your, tags, here";
+ // ... and more
+}
+```
+
+Use it in your code:
+
+```csharp
+// Display version in your extension
+MessageBox.Show($"Version: {VsixInfo.Version}");
+
+// Use in attributes
+[Guid(VsixInfo.Id)]
+public sealed class MyPackage : AsyncPackage { }
+```
-namespace MyExtension
+#### VSCT GUIDs and IDs
+
+If you have `.vsct` files, constants are generated for your GUIDs and command IDs:
+
+```csharp
+// Auto-generated from MyCommands.vsct
+public static class MyCommandsVsct
{
- [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
- [Guid("YOUR-GUID-HERE")]
- public sealed class MyExtensionPackage : AsyncPackage
- {
- protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress)
- {
- await base.InitializeAsync(cancellationToken, progress);
- await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
+ public static readonly Guid guidMyPackage = new Guid("...");
- // Your initialization code here 🎨
- }
+ public static class guidMyCommandSet
+ {
+ public const string GuidString = "...";
+ public static readonly Guid Guid = new Guid(GuidString);
+ public const int MyCommandId = 0x0100;
+ public const int MyMenuGroup = 0x1020;
}
}
```
-### 4️⃣ Build and Debug
+#### Generated Files Location
-| Action | Command |
-|--------|---------|
-| 🔨 Build | `dotnet build` or build in Visual Studio |
-| 🐛 Debug | Press **F5** → launches the Experimental Instance |
+Generated source files are written to the `Generated/` folder in your project and are visible in Solution Explorer. They're marked as auto-generated so you know not to edit them directly.
----
+### Version Override
+
+Update the VSIX version at build time without manually editing the manifest:
-## ✅ Features
+```bash
+dotnet build -p:SetVsixVersion=2.0.0
+```
-| Feature | Description |
-|---------|-------------|
-| 📝 **SDK-style projects** | Clean, minimal `.csproj` files |
-| 🐛 **F5 debugging** | Works out of the box with VS Experimental Instance |
-| 📁 **Auto-inclusion** | VSCT, VSIX manifests, and VSPackage resources included automatically |
-| 🔄 **Version sync** | VSIX version derived from project `Version` property |
-| ⚙️ **Sensible defaults** | Correct settings for VS 2022+ (x64, .NET Framework 4.7.2+) |
-| 🚀 **Smart deployment** | Only deploys to Experimental Instance when building in VS |
+This updates the `source.extension.vsixmanifest` file with the new version, rebuilds with the correct version in all outputs (including the generated `VsixInfo.Version` constant), and produces the VSIX with the specified version.
----
+This is useful for CI/CD pipelines:
-## 📋 Requirements
+```yaml
+# GitHub Actions example
+- name: Build Release
+ run: dotnet build -c Release -p:SetVsixVersion=${{ github.ref_name }}
+```
-- 🖥️ Visual Studio 2022 or later
-- 🎯 .NET Framework 4.7.2+ target framework
+### Auto-Inclusion
----
+The SDK automatically includes common VSIX files:
+
+- `*.vsct` files as `VSCTCompile` items
+- `VSPackage.resx` files with proper metadata
+- `source.extension.vsixmanifest` as an `AdditionalFile` for source generators
+
+### F5 Debugging
+
+Press F5 to launch the Visual Studio Experimental Instance with your extension loaded. This works automatically when:
+- Building in Debug configuration
+- Building inside Visual Studio (not `dotnet build`)
+
+### Smart Deployment
+
+Extensions are only deployed to the Experimental Instance when building inside Visual Studio. This prevents errors when building from the command line.
+
+### Publish Manifest Generation
+
+The SDK automatically generates a `publish.manifest.json` file for publishing to the VS Marketplace. All values are extracted from your VSIX manifest:
+
+```json
+{
+ "$schema": "http://json.schemastore.org/vsix-publish",
+ "categories": [
+ "your", "tags", "here"
+ ],
+ "identity": {
+ "internalName": "MyExtension"
+ },
+ "overview": "README.md",
+ "publisher": "Your Name",
+ "qna": true,
+ "repo": "https://github.com/you/your-repo"
+}
+```
+
+| JSON Field | Source |
+|------------|--------|
+| `publisher` | `Identity/@Publisher` in VSIX manifest |
+| `categories` | `Tags` element in VSIX manifest |
+| `repo` | `MoreInfo` element in VSIX manifest |
+| `internalName` | Project name |
+| `overview` | Configurable via `VsixPublishOverview` property (default: `README.md`) |
+| `qna` | Configurable via `VsixPublishQnA` property (default: `true`) |
+
+To disable publish manifest generation:
+
+```xml
+
+ false
+
+```
-## 🔧 Configuration
+## Configuration
### Properties
| Property | Default | Description |
|----------|---------|-------------|
| `TargetFramework` | `net472` | Target framework (must be .NET Framework 4.7.2+) |
-| `Platform` | `x64` | Target platform (VS 2022+ is 64-bit) |
-| `UseCodebase` | `true` | Use codebase for assembly loading |
+| `Platform` | `AnyCPU` | Target platform |
| `GeneratePkgDefFile` | `true` | Generate .pkgdef registration file |
-| `VsixVersion` | `$(Version)` | VSIX manifest version |
| `DeployExtension` | `true`* | Deploy to experimental instance |
| `EnableDefaultVsixItems` | `true` | Auto-include VSIX-related files |
-| `EnableDefaultVsixDebugging` | `true` | Configure F5 debugging |
+| `EmitCompilerGeneratedFiles` | `true` | Write generated source files to disk |
+| `CompilerGeneratedFilesOutputPath` | `Generated/` | Location for generated source files |
+| `GeneratePublishManifest` | `true` | Generate `publish.manifest.json` for VS Marketplace |
+| `VsixPublishOverview` | `README.md` | Path to README for marketplace overview |
+| `VsixPublishQnA` | `true` | Enable Q&A on marketplace listing |
> \* Only when `Configuration=Debug` AND building inside Visual Studio
-### 🎛️ Disabling Auto-Inclusion
+### Disabling Auto-Inclusion
Take full control over which files are included:
@@ -174,12 +270,11 @@ Or disable specific categories:
```xml
false
- false
false
```
-### 🐛 Custom Debugging Arguments
+### Custom Debugging Arguments
```xml
@@ -187,21 +282,122 @@ Or disable specific categories:
```
----
+## Migrating from Legacy Projects
-## 🔄 Migration from Legacy Projects
+If you have an existing non-SDK style VSIX project, follow these steps to convert it.
-Migrating from the old project format? Here's how:
+### Step 1: Back Up Your Project
-1. 📝 Replace your old `.csproj` content with the SDK-style format above
-2. 🗑️ Remove unnecessary `` statements — the SDK handles them
-3. 📁 Keep your `source.extension.vsixmanifest`, `.vsct`, and resource files
-4. ➕ Add `amd64` to your manifest for VS 2022+
-5. 🔨 Build and test!
+Before making changes, ensure your project is committed to source control or backed up.
----
+### Step 2: Replace the .csproj Content
+
+Replace your entire `.csproj` file with the SDK-style format:
+
+**Before (Legacy):**
+```xml
+
+
+
+
+ Debug
+ AnyCPU
+ {YOUR-GUID}
+ Library
+ Properties
+ MyExtension
+ MyExtension
+ v4.7.2
+
+
+
+
+
+
+```
+
+**After (SDK-style):**
+```xml
+
+
+
+ net472
+ MyExtension
+ MyExtension
+
+
+
+
+
+
+
+
+```
+
+### Step 3: Update the VSIX Manifest for VS 2022+
+
+Add `amd64` to each `InstallationTarget`:
+
+```xml
+
+
+
+
+```
+
+### Step 4: Remove Unnecessary Files
+
+Delete these files if they exist (the SDK handles them automatically):
+- `packages.config` - Use `PackageReference` instead
+- `Properties/AssemblyInfo.cs` - SDK generates this automatically
+- `app.config` - Usually not needed
+
+### Step 5: Update Package References
+
+Convert from `packages.config` to `PackageReference` format in your `.csproj`:
+
+```xml
+
+
+
+
+
+```
+
+### Step 6: Handle VSCT Files
-## 🏗️ Building from Source
+If you have `.vsct` files, they're automatically included. Remove any explicit `` items unless you need custom metadata.
+
+### Step 7: Build and Test
+
+```bash
+dotnet build
+```
+
+Fix any errors that arise. Common issues:
+- **Missing types**: Add the appropriate `PackageReference`
+- **Duplicate files**: Remove explicit includes that conflict with auto-inclusion
+- **Resource files**: Ensure `VSPackage.resx` files are in the project
+
+### Migration Checklist
+
+- [ ] Replaced `.csproj` with SDK-style format
+- [ ] Added `amd64` to manifest
+- [ ] Converted `packages.config` to `PackageReference`
+- [ ] Removed `Properties/AssemblyInfo.cs`
+- [ ] Removed explicit file includes (now auto-included)
+- [ ] Updated version range to `[17.0, 19.0)` for VS 2022+
+- [ ] Build succeeds with `dotnet build`
+- [ ] F5 debugging works in Visual Studio
+
+## Requirements
+
+- Visual Studio 2022 or later
+- .NET Framework 4.7.2+ target framework
+
+## Building from Source
```bash
# Clone the repository
@@ -211,12 +407,18 @@ cd VsixSdk
# Build the SDK package
dotnet build src/CodingWithCalvin.VsixSdk/CodingWithCalvin.VsixSdk.csproj -c Release
-# 📦 Package outputs to artifacts/packages/
+# Build the template package
+dotnet pack src/CodingWithCalvin.VsixSdk.Templates/CodingWithCalvin.VsixSdk.Templates.csproj -c Release
+
+# Packages output to artifacts/packages/
```
----
+## Contributors
+
+
+
-## 📄 License
+## License
MIT License - see [LICENSE](LICENSE) for details.
diff --git a/samples/Directory.Build.targets b/samples/Directory.Build.targets
index 487fe61..d30ea9c 100644
--- a/samples/Directory.Build.targets
+++ b/samples/Directory.Build.targets
@@ -7,4 +7,9 @@
+
+
+
+
+
diff --git a/samples/SampleExtension/SampleCommands.vsct b/samples/SampleExtension/SampleCommands.vsct
new file mode 100644
index 0000000..0c62cff
--- /dev/null
+++ b/samples/SampleExtension/SampleCommands.vsct
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/SampleExtension/publish.manifest.json b/samples/SampleExtension/publish.manifest.json
new file mode 100644
index 0000000..fc4ef1a
--- /dev/null
+++ b/samples/SampleExtension/publish.manifest.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/vsix-publish",
+ "categories": [
+ "sample", "test"
+ ],
+ "identity": {
+ "internalName": "SampleExtension"
+ },
+ "overview": "README.md",
+ "publisher": "Coding With Calvin",
+ "qna": true,
+ "repo": "https://github.com/CodingWithCalvin/VsixSdk"
+}
diff --git a/samples/SampleExtension/source.extension.vsixmanifest b/samples/SampleExtension/source.extension.vsixmanifest
index 88d23b8..fcfba1a 100644
--- a/samples/SampleExtension/source.extension.vsixmanifest
+++ b/samples/SampleExtension/source.extension.vsixmanifest
@@ -1,9 +1,10 @@
-
+
-
+
Sample Extension
- A sample Visual Studio extension built with CodingWithCalvin.VsixSdk
+ A sample Visual Studio extension built with CodingWithCalvin.VsixSdk
+ https://github.com/CodingWithCalvin/VsixSdk
sample, test
@@ -26,4 +27,4 @@
-
+
\ No newline at end of file
diff --git a/src/CodingWithCalvin.VsixSdk.Generators/CodingWithCalvin.VsixSdk.Generators.csproj b/src/CodingWithCalvin.VsixSdk.Generators/CodingWithCalvin.VsixSdk.Generators.csproj
new file mode 100644
index 0000000..0dd9428
--- /dev/null
+++ b/src/CodingWithCalvin.VsixSdk.Generators/CodingWithCalvin.VsixSdk.Generators.csproj
@@ -0,0 +1,35 @@
+
+
+
+ netstandard2.0
+ latest
+ enable
+ true
+
+
+ true
+ false
+ true
+ true
+
+
+ CodingWithCalvin.VsixSdk.Generators
+ Source generators for CodingWithCalvin.VsixSdk - generates code from VSIX manifest and VSCT files
+ Calvin A. Allen
+ vsix;visualstudio;extension;sdk;sourcegenerator;roslyn
+ MIT
+ https://github.com/CodingWithCalvin/VsixSdk
+ git
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CodingWithCalvin.VsixSdk.Generators/VsctGuidsGenerator.cs b/src/CodingWithCalvin.VsixSdk.Generators/VsctGuidsGenerator.cs
new file mode 100644
index 0000000..5f731a1
--- /dev/null
+++ b/src/CodingWithCalvin.VsixSdk.Generators/VsctGuidsGenerator.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace CodingWithCalvin.VsixSdk.Generators;
+
+///
+/// Source generator that creates static classes with GUIDs and IDs from VSCT files.
+///
+[Generator]
+public class VsctGuidsGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Find all .vsct files in AdditionalFiles
+ var vsctFiles = context.AdditionalTextsProvider
+ .Where(file => file.Path.EndsWith(".vsct", StringComparison.OrdinalIgnoreCase));
+
+ // Combine with compilation to get namespace
+ var compilationAndVscts = context.CompilationProvider
+ .Combine(vsctFiles.Collect());
+
+ context.RegisterSourceOutput(compilationAndVscts, (ctx, source) =>
+ {
+ var (compilation, vscts) = source;
+
+ foreach (var vsct in vscts)
+ {
+ GenerateVsctGuids(ctx, compilation, vsct);
+ }
+ });
+ }
+
+ private static void GenerateVsctGuids(
+ SourceProductionContext context,
+ Compilation compilation,
+ AdditionalText vsctFile)
+ {
+ var text = vsctFile.GetText(context.CancellationToken);
+ if (text == null) return;
+
+ var fileName = Path.GetFileNameWithoutExtension(vsctFile.Path);
+ var className = $"{fileName}Vsct";
+
+ try
+ {
+ var doc = new XmlDocument();
+ doc.LoadXml(text.ToString());
+
+ var nsmgr = new XmlNamespaceManager(doc.NameTable);
+ nsmgr.AddNamespace("vsct", "http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable");
+
+ // Extract all GuidSymbols
+ var guidSymbols = ExtractGuidSymbols(doc, nsmgr);
+
+ if (guidSymbols.Count == 0) return;
+
+ // Get namespace from compilation
+ var rootNamespace = compilation.AssemblyName ?? "GeneratedCode";
+
+ var source = GenerateSource(rootNamespace, className, fileName, guidSymbols);
+ context.AddSource($"{className}.g.cs", SourceText.From(source, Encoding.UTF8));
+ }
+ catch (Exception ex)
+ {
+ // Report diagnostic on error
+ var descriptor = new DiagnosticDescriptor(
+ "VSIXSDK002",
+ "Failed to parse VSCT file",
+ "Failed to parse VSCT file '{0}': {1}",
+ "VsixSdk",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ context.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None, vsctFile.Path, ex.Message));
+ }
+ }
+
+ private static List ExtractGuidSymbols(XmlDocument doc, XmlNamespaceManager nsmgr)
+ {
+ var result = new List();
+
+ // Try both namespaced and non-namespaced queries
+ var guidSymbolNodes = doc.SelectNodes("//vsct:GuidSymbol", nsmgr);
+ if (guidSymbolNodes == null || guidSymbolNodes.Count == 0)
+ {
+ guidSymbolNodes = doc.SelectNodes("//GuidSymbol");
+ }
+
+ if (guidSymbolNodes == null) return result;
+
+ foreach (XmlNode node in guidSymbolNodes)
+ {
+ var name = node.Attributes?["name"]?.Value;
+ var value = node.Attributes?["value"]?.Value;
+
+ if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value))
+ continue;
+
+ var guidSymbol = new GuidSymbol
+ {
+ Name = name!,
+ Value = value!.Trim('{', '}')
+ };
+
+ // Extract nested IDSymbols
+ var idSymbolNodes = node.SelectNodes("vsct:IDSymbol", nsmgr);
+ if (idSymbolNodes == null || idSymbolNodes.Count == 0)
+ {
+ idSymbolNodes = node.SelectNodes("IDSymbol");
+ }
+
+ if (idSymbolNodes != null)
+ {
+ foreach (XmlNode idNode in idSymbolNodes)
+ {
+ var idName = idNode.Attributes?["name"]?.Value;
+ var idValue = idNode.Attributes?["value"]?.Value;
+
+ if (!string.IsNullOrEmpty(idName) && !string.IsNullOrEmpty(idValue))
+ {
+ guidSymbol.IdSymbols.Add(new IdSymbol { Name = idName!, Value = idValue! });
+ }
+ }
+ }
+
+ result.Add(guidSymbol);
+ }
+
+ return result;
+ }
+
+ private static string GenerateSource(string rootNamespace, string className, string fileName, List guidSymbols)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("//------------------------------------------------------------------------------");
+ sb.AppendLine("// ");
+ sb.AppendLine("// This code was generated by CodingWithCalvin.VsixSdk from the VSCT file.");
+ sb.AppendLine("// Changes to this file may cause incorrect behavior and will be lost if");
+ sb.AppendLine("// the code is regenerated.");
+ sb.AppendLine("// ");
+ sb.AppendLine("//------------------------------------------------------------------------------");
+ sb.AppendLine();
+ sb.AppendLine("using System;");
+ sb.AppendLine();
+ sb.AppendLine($"namespace {rootNamespace}");
+ sb.AppendLine("{");
+ sb.AppendLine(" /// ");
+ sb.AppendLine($" /// GUIDs and IDs from {fileName}.vsct");
+ sb.AppendLine(" /// ");
+ sb.AppendLine($" internal static class {className}");
+ sb.AppendLine(" {");
+
+ foreach (var guidSymbol in guidSymbols)
+ {
+ if (guidSymbol.IdSymbols.Count == 0)
+ {
+ // Simple GUID constant
+ sb.AppendLine($" /// GUID: {{{guidSymbol.Value}}}");
+ sb.AppendLine($" public static readonly Guid {guidSymbol.Name} = new Guid(\"{guidSymbol.Value}\");");
+ sb.AppendLine();
+ }
+ else
+ {
+ // Nested class with GUID and IDs
+ sb.AppendLine($" /// Command set GUID: {{{guidSymbol.Value}}}");
+ sb.AppendLine($" public static class {guidSymbol.Name}");
+ sb.AppendLine(" {");
+ sb.AppendLine($" public const string GuidString = \"{guidSymbol.Value}\";");
+ sb.AppendLine($" public static readonly Guid Guid = new Guid(GuidString);");
+
+ foreach (var idSymbol in guidSymbol.IdSymbols)
+ {
+ sb.AppendLine($" public const int {idSymbol.Name} = {idSymbol.Value};");
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ }
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ private class GuidSymbol
+ {
+ public string Name { get; set; } = string.Empty;
+ public string Value { get; set; } = string.Empty;
+ public List IdSymbols { get; } = new List();
+ }
+
+ private class IdSymbol
+ {
+ public string Name { get; set; } = string.Empty;
+ public string Value { get; set; } = string.Empty;
+ }
+}
diff --git a/src/CodingWithCalvin.VsixSdk.Generators/VsixInfoGenerator.cs b/src/CodingWithCalvin.VsixSdk.Generators/VsixInfoGenerator.cs
new file mode 100644
index 0000000..9aca016
--- /dev/null
+++ b/src/CodingWithCalvin.VsixSdk.Generators/VsixInfoGenerator.cs
@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace CodingWithCalvin.VsixSdk.Generators;
+
+///
+/// Source generator that creates a VsixInfo class from the VSIX manifest file.
+///
+[Generator]
+public class VsixInfoGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Find all .vsixmanifest files in AdditionalFiles
+ var manifestFiles = context.AdditionalTextsProvider
+ .Where(file => file.Path.EndsWith(".vsixmanifest", StringComparison.OrdinalIgnoreCase));
+
+ // Combine with compilation to get namespace
+ var compilationAndManifests = context.CompilationProvider
+ .Combine(manifestFiles.Collect());
+
+ context.RegisterSourceOutput(compilationAndManifests, (ctx, source) =>
+ {
+ var (compilation, manifests) = source;
+
+ foreach (var manifest in manifests)
+ {
+ GenerateVsixInfo(ctx, compilation, manifest);
+ }
+ });
+ }
+
+ private static void GenerateVsixInfo(
+ SourceProductionContext context,
+ Compilation compilation,
+ AdditionalText manifestFile)
+ {
+ var text = manifestFile.GetText(context.CancellationToken);
+ if (text == null) return;
+
+ try
+ {
+ var doc = new XmlDocument();
+ doc.LoadXml(text.ToString());
+
+ var nsmgr = new XmlNamespaceManager(doc.NameTable);
+ nsmgr.AddNamespace("vsix", "http://schemas.microsoft.com/developer/vsx-schema/2011");
+
+ // Extract metadata
+ var metadata = new VsixMetadata
+ {
+ Id = GetAttributeValue(doc, "//vsix:Identity/@Id", nsmgr),
+ Version = GetAttributeValue(doc, "//vsix:Identity/@Version", nsmgr),
+ Language = GetAttributeValue(doc, "//vsix:Identity/@Language", nsmgr),
+ Publisher = GetAttributeValue(doc, "//vsix:Identity/@Publisher", nsmgr),
+ DisplayName = GetElementText(doc, "//vsix:DisplayName", nsmgr),
+ Description = GetElementText(doc, "//vsix:Description", nsmgr),
+ MoreInfo = GetElementText(doc, "//vsix:MoreInfo", nsmgr),
+ License = GetElementText(doc, "//vsix:License", nsmgr),
+ GettingStartedGuide = GetElementText(doc, "//vsix:GettingStartedGuide", nsmgr),
+ ReleaseNotes = GetElementText(doc, "//vsix:ReleaseNotes", nsmgr),
+ Icon = GetElementText(doc, "//vsix:Icon", nsmgr),
+ PreviewImage = GetElementText(doc, "//vsix:PreviewImage", nsmgr),
+ Tags = GetElementText(doc, "//vsix:Tags", nsmgr),
+ IsPreview = GetElementText(doc, "//vsix:Preview", nsmgr)?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false
+ };
+
+ // Get namespace from compilation
+ var rootNamespace = compilation.AssemblyName ?? "GeneratedCode";
+
+ var source = GenerateSource(rootNamespace, metadata);
+ context.AddSource("VsixInfo.g.cs", SourceText.From(source, Encoding.UTF8));
+ }
+ catch (Exception ex)
+ {
+ // Report diagnostic on error
+ var descriptor = new DiagnosticDescriptor(
+ "VSIXSDK001",
+ "Failed to parse VSIX manifest",
+ "Failed to parse VSIX manifest: {0}",
+ "VsixSdk",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ context.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None, ex.Message));
+ }
+ }
+
+ private static string? GetAttributeValue(XmlDocument doc, string xpath, XmlNamespaceManager nsmgr)
+ {
+ var node = doc.SelectSingleNode(xpath, nsmgr);
+ return node?.Value;
+ }
+
+ private static string? GetElementText(XmlDocument doc, string xpath, XmlNamespaceManager nsmgr)
+ {
+ var node = doc.SelectSingleNode(xpath, nsmgr);
+ return node?.InnerText;
+ }
+
+ private static string GenerateSource(string rootNamespace, VsixMetadata metadata)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("//------------------------------------------------------------------------------");
+ sb.AppendLine("// ");
+ sb.AppendLine("// This code was generated by CodingWithCalvin.VsixSdk from the VSIX manifest.");
+ sb.AppendLine("// Changes to this file may cause incorrect behavior and will be lost if");
+ sb.AppendLine("// the code is regenerated.");
+ sb.AppendLine("// ");
+ sb.AppendLine("//------------------------------------------------------------------------------");
+ sb.AppendLine();
+ sb.AppendLine($"namespace {rootNamespace}");
+ sb.AppendLine("{");
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" /// Provides compile-time constants from the VSIX manifest metadata.");
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" internal static class VsixInfo");
+ sb.AppendLine(" {");
+ sb.AppendLine($" /// The unique identifier of the extension.");
+ sb.AppendLine($" public const string Id = \"{EscapeString(metadata.Id)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// The version of the extension.");
+ sb.AppendLine($" public const string Version = \"{EscapeString(metadata.Version)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// The language/locale of the extension.");
+ sb.AppendLine($" public const string Language = \"{EscapeString(metadata.Language)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// The publisher/author of the extension.");
+ sb.AppendLine($" public const string Publisher = \"{EscapeString(metadata.Publisher)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// The display name of the extension.");
+ sb.AppendLine($" public const string DisplayName = \"{EscapeString(metadata.DisplayName)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// The description of the extension.");
+ sb.AppendLine($" public const string Description = \"{EscapeString(metadata.Description)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// URL for more information about the extension.");
+ sb.AppendLine($" public const string MoreInfo = \"{EscapeString(metadata.MoreInfo)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// Path to the license file.");
+ sb.AppendLine($" public const string License = \"{EscapeString(metadata.License)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// URL to the getting started guide.");
+ sb.AppendLine($" public const string GettingStartedGuide = \"{EscapeString(metadata.GettingStartedGuide)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// URL or path to release notes.");
+ sb.AppendLine($" public const string ReleaseNotes = \"{EscapeString(metadata.ReleaseNotes)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// Path to the extension icon.");
+ sb.AppendLine($" public const string Icon = \"{EscapeString(metadata.Icon)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// Path to the preview image.");
+ sb.AppendLine($" public const string PreviewImage = \"{EscapeString(metadata.PreviewImage)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// Comma-separated tags for the extension.");
+ sb.AppendLine($" public const string Tags = \"{EscapeString(metadata.Tags)}\";");
+ sb.AppendLine();
+ sb.AppendLine($" /// Whether the extension is marked as a preview release.");
+ sb.AppendLine($" public const bool IsPreview = {(metadata.IsPreview ? "true" : "false")};");
+ sb.AppendLine(" }");
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ private static string EscapeString(string? value)
+ {
+ if (string.IsNullOrEmpty(value)) return string.Empty;
+ return value!.Replace("\\", "\\\\").Replace("\"", "\\\"");
+ }
+
+ private class VsixMetadata
+ {
+ public string? Id { get; set; }
+ public string? Version { get; set; }
+ public string? Language { get; set; }
+ public string? Publisher { get; set; }
+ public string? DisplayName { get; set; }
+ public string? Description { get; set; }
+ public string? MoreInfo { get; set; }
+ public string? License { get; set; }
+ public string? GettingStartedGuide { get; set; }
+ public string? ReleaseNotes { get; set; }
+ public string? Icon { get; set; }
+ public string? PreviewImage { get; set; }
+ public string? Tags { get; set; }
+ public bool IsPreview { get; set; }
+ }
+}
diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/.template.config/template.json b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/.template.config/template.json
index 09ef7ab..cd62d93 100644
--- a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/.template.config/template.json
+++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/.template.config/template.json
@@ -12,25 +12,56 @@
"description": "A Visual Studio extension project using SDK-style format with CodingWithCalvin.VsixSdk",
"tags": {
"language": "C#",
- "type": "project"
+ "type": "solution"
},
"sourceName": "VsixExtension",
"preferNameDirectory": true,
"defaultName": "VsixExtension",
"symbols": {
+ "extensionName": {
+ "type": "parameter",
+ "datatype": "string",
+ "displayName": "Extension Name",
+ "description": "The display name shown in Visual Studio's extension manager (defaults to project name)",
+ "defaultValue": "",
+ "replaces": "TEMPLATE_EXTENSION_NAME",
+ "isRequired": false
+ },
"publisher": {
"type": "parameter",
"datatype": "string",
+ "displayName": "Publisher",
+ "description": "Publisher name for the VSIX manifest (shown in extension manager)",
"defaultValue": "MyPublisher",
"replaces": "TEMPLATE_PUBLISHER",
- "description": "Publisher name for the VSIX manifest"
+ "isRequired": false
},
"description": {
"type": "parameter",
"datatype": "string",
+ "displayName": "Description",
+ "description": "Description of what your extension does",
"defaultValue": "A Visual Studio extension",
"replaces": "TEMPLATE_DESCRIPTION",
- "description": "Description for the extension"
+ "isRequired": false
+ },
+ "tags": {
+ "type": "parameter",
+ "datatype": "string",
+ "displayName": "Tags",
+ "description": "Comma-separated tags for discoverability (e.g., productivity, editor)",
+ "defaultValue": "extension",
+ "replaces": "TEMPLATE_TAGS",
+ "isRequired": false
+ },
+ "extensionNameResolved": {
+ "type": "generated",
+ "generator": "coalesce",
+ "parameters": {
+ "sourceVariableName": "extensionName",
+ "fallbackVariableName": "name"
+ },
+ "replaces": "TEMPLATE_EXTENSION_NAME_RESOLVED"
},
"guid1": {
"type": "generated",
@@ -65,7 +96,7 @@
],
"primaryOutputs": [
{
- "path": "VsixExtension.csproj"
+ "path": "VsixExtension.slnx"
}
],
"postActions": [
diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/README.md b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/README.md
new file mode 100644
index 0000000..8bd286e
--- /dev/null
+++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/README.md
@@ -0,0 +1,31 @@
+# TEMPLATE_EXTENSION_NAME_RESOLVED
+
+TEMPLATE_DESCRIPTION
+
+## Features
+
+- Feature 1
+- Feature 2
+
+## Installation
+
+1. Download from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/)
+2. Or install directly from Visual Studio: **Extensions > Manage Extensions**
+
+## Usage
+
+Describe how to use your extension here.
+
+## Requirements
+
+- Visual Studio 2022 or later
+
+## Release Notes
+
+### 1.0.0
+
+- Initial release
+
+## License
+
+MIT
diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.csproj b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.csproj
index 61d4d5e..9d1baa9 100644
--- a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.csproj
+++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.csproj
@@ -2,6 +2,10 @@
net472
+ Debug;Release
+ AnyCPU
+ AnyCPU
+ AnyCPU
1.0.0
VsixExtension
VsixExtension
diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.slnx b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.slnx
new file mode 100644
index 0000000..acc0de5
--- /dev/null
+++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/VsixExtension.slnx
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/source.extension.vsixmanifest b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/source.extension.vsixmanifest
index ac40876..710ead2 100644
--- a/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/source.extension.vsixmanifest
+++ b/src/CodingWithCalvin.VsixSdk.Templates/templates/vsix-extension/source.extension.vsixmanifest
@@ -1,10 +1,10 @@
-
- VsixExtension
+
+ TEMPLATE_EXTENSION_NAME_RESOLVED
TEMPLATE_DESCRIPTION
- extension
+ TEMPLATE_TAGS
+
+
+
+ false
+ true
+ all
+
+
+
+
+
+
diff --git a/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.props b/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.props
index 05baaa2..cd826f1 100644
--- a/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.props
+++ b/src/CodingWithCalvin.VsixSdk/Sdk/Sdk.Vsix.props
@@ -9,9 +9,12 @@
true
-
- x64
- x64
+
+ VsixInfo
+
+
+ AnyCPU
+ AnyCPU
net472
@@ -43,6 +46,19 @@
disable
+
+
+ true
+ $(MSBuildProjectDirectory)\Generated
+
+
+ $(DefaultItemExcludes);Generated\**
@@ -62,6 +78,25 @@
1.0.0
+
+
+
+ true
+
+
+ README.md
+
+
+ true
+
+
-
-
- Designer
- PreserveNewest
-
-
+
+
+ <_SourceVsixManifestPath Condition="'$(_SourceVsixManifestPath)' == '' and Exists('$(MSBuildProjectDirectory)\source.extension.vsixmanifest')">$(MSBuildProjectDirectory)\source.extension.vsixmanifest
+ <_SourceVsixManifestPath Condition="'$(_SourceVsixManifestPath)' == '' and Exists('$(MSBuildProjectDirectory)\$(MSBuildProjectName).vsixmanifest')">$(MSBuildProjectDirectory)\$(MSBuildProjectName).vsixmanifest
+
+
-
+
+
+
+
+
+
+
+
+
+ Designer
+
+
+
+
+
+ <_VsixVersionSentinel>$(IntermediateOutputPath)_vsix_version_build.sentinel
+
+
+
+
- $(Version)
- 1.0.0
+ <_VsixNsForPoke><Namespace Prefix='vsix' Uri='http://schemas.microsoft.com/developer/vsx-schema/2011'/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_VsixNsForPeek><Namespace Prefix='vsix' Uri='http://schemas.microsoft.com/developer/vsx-schema/2011'/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_VsixTagsNormalized>$(_VsixTags.Replace(', ', ','))
+ <_CategoriesJson>$(_VsixTagsNormalized.Replace(',', '", "'))
+ <_CategoriesJson>"$(_CategoriesJson)"
+ <_QnAValue Condition="'$(VsixPublishQnA)' == 'true'">true
+ <_QnAValue Condition="'$(VsixPublishQnA)' != 'true'">false
+ <_RepoLine Condition="'$(_VsixMoreInfo)' != ''">,
+ "repo": "$(_VsixMoreInfo)"
+ <_RepoLine Condition="'$(_VsixMoreInfo)' == ''">
+
+
+
+
+ <_PublishManifestContent>
+
+
+
+
+
+ Text="Visual Studio 2022 is 64-bit. Consider changing PlatformTarget to 'AnyCPU'." />
+