Skip to content

Commit 779b8b8

Browse files
committed
Changed the default format for command table code.
1 parent 4a3b0c7 commit 779b8b8

30 files changed

+1326
-638
lines changed

Community.VisualStudio.SourceGenerators.sln

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1616
EndProjectSection
1717
EndProject
1818
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{589EC942-90CD-4CBF-AEBF-1F21C7EEE3B0}"
19+
ProjectSection(SolutionItems) = preProject
20+
test\.editorconfig = test\.editorconfig
21+
EndProjectSection
1922
EndProject
2023
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.VisualStudio.SourceGenerators.UnitTests", "test\Community.VisualStudio.SourceGenerators.UnitTests\Community.VisualStudio.SourceGenerators.UnitTests.csproj", "{9D7B8767-CF76-4A59-A5A3-3F0B1053DE5F}"
2124
EndProject
22-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Community.VisualStudio.SourceGenerators.EndToEndTests", "test\Community.VisualStudio.SourceGenerators.EndToEndTests\Community.VisualStudio.SourceGenerators.EndToEndTests.csproj", "{6C0DC0B8-45EB-4100-B2D1-A691C1ADA474}"
25+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.VisualStudio.SourceGenerators.EndToEndTests", "test\Community.VisualStudio.SourceGenerators.EndToEndTests\Community.VisualStudio.SourceGenerators.EndToEndTests.csproj", "{6C0DC0B8-45EB-4100-B2D1-A691C1ADA474}"
2326
EndProject
2427
Global
2528
GlobalSection(SolutionConfigurationPlatforms) = preSolution

README.md

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ The `Vsix` class will be generated in the root namespace of the project. If you
3535

3636
## Command Table Files
3737

38-
The source generator will create a class called `PackageGuids`. This class will contain a `string` constant and `Guid` field for each `<GUIDSymbol>` in any `.vsct` files.
38+
The source generator will create a container class that is named after the `.vsct` file. Within that container class, a class will be created for each `<GUIDSymbol>`.
3939

40-
A class called `PackageIds` will also be created that contains a constant for each `IDSymbol` in any `.vsct` files.
40+
The class for a `<GUIDSymbol>` contains a `Guid` and `GuidString` field that defines the GUID value, and each `<IDSymbol>` is defined as a constant.
4141

42-
For example, this `.vsct` file:
42+
For example, a `VSCommandTable.vsct` file that looks like this:
4343

4444
```xml
4545
<CommandTable xmlns='http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
@@ -52,25 +52,31 @@ For example, this `.vsct` file:
5252
</CommandTable>
5353
```
5454

55-
Will result in these classes:
55+
Will result in this:
5656

57-
```cs
58-
internal sealed class PackageGuids
57+
```csharp
58+
internal sealed partial class VSCommandTable
5959
{
60-
public const string MyPackageString = "e5d94a98-30f6-47da-88bb-1bdf3b4157ff";
61-
public static readonly Guid MyPackage = new Guid(MyPackageString);
60+
internal sealed partial class MyPackage
61+
{
62+
public const string GuidString = "e5d94a98-30f6-47da-88bb-1bdf3b4157ff";
63+
public static readonly Guid Guid = new Guid(GuidString);
64+
65+
public const int MyFirstCommand = 1;
66+
public const int MySecondCommand = 2;
67+
}
6268
}
69+
```
6370

64-
internal sealed class PackageIds
65-
{
66-
public const int MyFirstCommand = 1;
67-
public const int MySecondCommand = 2;
68-
}
71+
You can then access the `Guid` and IDs like this:
72+
73+
```csharp
74+
[GuidAttribute(VSCommandTable.MyPackage.GuidString)]
6975
```
7076

7177
#### Use a custom namespace
7278

73-
The `PackageGuids` and `PackageIds` classes will be generated in the root namespace of the project. If you would like to generate the code into a different namespace, you can specify the namespace by defining the `Namespace` metadata for the `VSCTCompile` item like this:
79+
The classes will be generated in the root namespace of the project. If you would like to generate the code into a different namespace, you can specify the namespace by defining the `Namespace` metadata for the `VSCTCompile` item like this:
7480

7581
```xml
7682
<ItemGroup>
@@ -80,3 +86,32 @@ The `PackageGuids` and `PackageIds` classes will be generated in the root namesp
8086
</VSCTCompile>
8187
</ItemGroup>
8288
```
89+
90+
#### Migrating from the Vsix Synchronizer extension
91+
92+
If you are migrating from the [Vsix Synchronizer](https://github.com/madskristensen/VsixSynchronizer) extension and would like to continue to use the `PackageGuids` and `PackageIds` classes that it generates, you can change the output format by defining the `Format` metadata for the `VSCTCompile` item like this:
93+
94+
```xml
95+
<ItemGroup>
96+
<VSCTCompile Include="MyCommandTable.vsct">
97+
<ResourceName>Menus.ctmenu</ResourceName>
98+
<Format>VsixSynchronizer</Format>
99+
</VSCTCompile>
100+
</ItemGroup>
101+
```
102+
103+
This will result in classes like this:
104+
105+
```csharp
106+
internal sealed partial class PackageGuids
107+
{
108+
public const string MyPackageString = "e5d94a98-30f6-47da-88bb-1bdf3b4157ff";
109+
public static readonly Guid MyPackage = new Guid(MyPackageString);
110+
}
111+
112+
internal sealed partial class PackageIds
113+
{
114+
public const int MyFirstCommand = 1;
115+
public const int MySecondCommand = 2;
116+
}
117+
```
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System.Text;
2+
using System.Text.RegularExpressions;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
5+
namespace Community.VisualStudio.SourceGenerators;
6+
7+
internal abstract class CommandTableCodeWriter : WriterBase
8+
{
9+
public abstract string Format { get; }
10+
11+
public abstract IEnumerable<GeneratedFile> Write(CommandTable commandTable, string codeNamespace, string langVersion);
12+
13+
protected private static string GetGuidName(string name)
14+
{
15+
// If the name starts with "guid", then trim it off because
16+
// that prefix doesn't provide any additional information
17+
// since all symbols defined in the class are GUIDs.
18+
if (Regex.IsMatch(name, "^guid[A-Z]"))
19+
{
20+
name = name.Substring(4);
21+
}
22+
23+
return name;
24+
}
25+
26+
protected static string SafeIdentifierName(string name)
27+
{
28+
// Most of the time the identifier name will be fine,
29+
// so rather than always copying the name, we'll
30+
// check if the name needs to be altered first.
31+
if (!name.All(SyntaxFacts.IsIdentifierPartCharacter))
32+
{
33+
StringBuilder buffer = new(name.Length);
34+
foreach (char ch in name)
35+
{
36+
if (SyntaxFacts.IsIdentifierPartCharacter(ch))
37+
{
38+
buffer.Append(ch);
39+
}
40+
else
41+
{
42+
buffer.Append('_');
43+
}
44+
}
45+
46+
name = buffer.ToString();
47+
}
48+
49+
// Make sure the name starts with a valid character.
50+
// If it doesn't, then prepend an underscore.
51+
if (name.Length == 0 || !SyntaxFacts.IsIdentifierStartCharacter(name[0]))
52+
{
53+
name = "_" + name;
54+
};
55+
56+
return name;
57+
}
58+
59+
protected static string SafeHintName(string name)
60+
{
61+
// Most of the time the name will be fine,
62+
// so rather than always copying the name, we'll
63+
// check if the name needs to be altered first.
64+
if (name.All(IsValidHintNameCharacter))
65+
{
66+
return name;
67+
}
68+
69+
StringBuilder buffer = new(name.Length);
70+
foreach (char ch in name)
71+
{
72+
if (IsValidHintNameCharacter(ch))
73+
{
74+
buffer.Append(ch);
75+
}
76+
else
77+
{
78+
buffer.Append('_');
79+
}
80+
}
81+
82+
return buffer.ToString();
83+
}
84+
85+
private static bool IsValidHintNameCharacter(char ch)
86+
{
87+
// This check is taken from the validation
88+
// in Roslyn's `AdditionalSourcesCollection`.
89+
///
90+
// Allow any identifier character or any of these characters:
91+
// [.,-_ ()[]{}]
92+
//
93+
// Note that the latest version also allows + and `, but we want to be compatible
94+
// with earlier versions, so we'll consider those two characters to be invalid.
95+
return SyntaxFacts.IsIdentifierPartCharacter(ch)
96+
|| ch == '.'
97+
|| ch == ','
98+
|| ch == '-'
99+
|| ch == '_'
100+
|| ch == ' '
101+
|| ch == '('
102+
|| ch == ')'
103+
|| ch == '['
104+
|| ch == ']'
105+
|| ch == '{'
106+
|| ch == '}';
107+
}
108+
}
Lines changed: 26 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
using System.Text;
2-
using System.Text.RegularExpressions;
3-
using Microsoft.CodeAnalysis;
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.Diagnostics;
43

54
namespace Community.VisualStudio.SourceGenerators;
65

76
[Generator]
8-
public class CommandTableGenerator : GeneratorBase, IIncrementalGenerator
7+
public class CommandTableGenerator : IIncrementalGenerator
98
{
109
private static readonly DiagnosticDescriptor _invalidCommandTableFile = new(
1110
DiagnosticIds.CVSSG003_InvalidCommandTableFile,
@@ -16,18 +15,21 @@ public class CommandTableGenerator : GeneratorBase, IIncrementalGenerator
1615
true
1716
);
1817

18+
private readonly DefaultCommandTableCodeWriter _defaultWriter = new();
19+
private readonly VsixSynchronizerCommandTableCodeWriter _vsxiSynchronizerWriter = new();
20+
1921
public void Initialize(IncrementalGeneratorInitializationContext context)
2022
{
21-
IncrementalValuesProvider<(string FilePath, string FileContents, string Namespace, string LangVersion)> values = context
23+
IncrementalValuesProvider<(string FilePath, string FileContents, string Namespace, string LangVersion, string Format)> values = context
2224
.AdditionalTextsProvider
2325
.Where(static (file) => Path.GetExtension(file.Path).Equals(".vsct", StringComparison.OrdinalIgnoreCase))
24-
.Where(static (file) => Path.GetExtension(file.Path).Equals(".vsct", StringComparison.OrdinalIgnoreCase))
2526
.Combine(context.AnalyzerConfigOptionsProvider)
2627
.Select(static (item, cancellationToken) => (
2728
FilePath: item.Left.Path,
2829
FileContents: item.Left.GetText(cancellationToken)?.ToString() ?? "",
2930
Namespace: item.Right.GetNamespace(item.Left),
30-
LangVersion: item.Right.GetLangVersion()
31+
LangVersion: item.Right.GetLangVersion(),
32+
Format: GetFormat(item.Right, item.Left)
3133
));
3234

3335
context.RegisterSourceOutput(
@@ -51,80 +53,27 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5153
return;
5254
}
5355

54-
GeneratePackageGuids(commandTable, data.Namespace, data.LangVersion, context);
55-
GeneratePackageIds(commandTable, data.Namespace, data.LangVersion, context);
56+
CommandTableCodeWriter writer;
57+
if (string.Equals(data.Format, _vsxiSynchronizerWriter.Format, StringComparison.OrdinalIgnoreCase))
58+
{
59+
writer = _vsxiSynchronizerWriter;
60+
}
61+
else
62+
{
63+
writer = _defaultWriter;
64+
}
65+
66+
foreach (GeneratedFile file in writer.Write(commandTable, data.Namespace, data.LangVersion))
67+
{
68+
context.AddSource(file.FileName, file.Code);
69+
}
5670
}
5771
);
5872
}
5973

60-
private static void GeneratePackageGuids(CommandTable commandTable, string containingNamespace, string langVersion, SourceProductionContext context)
74+
private static string GetFormat(AnalyzerConfigOptionsProvider options, AdditionalText file)
6175
{
62-
StringBuilder builder = new();
63-
WritePreamble(builder, langVersion);
64-
builder.AppendLine($"namespace {containingNamespace}");
65-
builder.AppendLine("{");
66-
builder.AppendLine(" /// <summary>Defines GUIDs from VSCT files.</summary>");
67-
builder.AppendLine(" internal sealed partial class PackageGuids");
68-
builder.AppendLine(" {");
69-
70-
foreach (GUIDSymbol symbol in commandTable.GUIDSymbols.OrderBy((x) => x.Name))
71-
{
72-
string guidName = SafeIdentifierName(GetGuidName(symbol.Name));
73-
builder.AppendLine($" public const string {guidName}String = \"{symbol.Value:D}\";");
74-
builder.AppendLine($" public static readonly System.Guid {guidName} = new System.Guid({guidName}String);");
75-
}
76-
77-
builder.AppendLine(" }");
78-
builder.AppendLine("}");
79-
80-
context.AddSource($"PackageGuids.{commandTable.Name}.g.cs", builder.ToString());
81-
}
82-
83-
private static void GeneratePackageIds(CommandTable commandTable, string containingNamespace, string langVersion, SourceProductionContext context)
84-
{
85-
StringBuilder builder = new();
86-
WritePreamble(builder, langVersion);
87-
builder.AppendLine($"namespace {containingNamespace}");
88-
builder.AppendLine("{");
89-
builder.AppendLine(" /// <summary>Defines IDs from VSCT files.</summary>");
90-
builder.AppendLine(" internal sealed partial class PackageIds");
91-
builder.AppendLine(" {");
92-
93-
foreach (IDSymbol symbol in commandTable.GUIDSymbols.SelectMany((x) => x.IDSymbols).OrderBy((x) => x.Name))
94-
{
95-
builder.AppendLine($" public const int {SafeIdentifierName(symbol.Name)} = 0x{symbol.Value:X4};");
96-
}
97-
98-
builder.AppendLine(" }");
99-
builder.AppendLine("}");
100-
101-
context.AddSource($"PackageIds.{commandTable.Name}.g.cs", builder.ToString());
102-
}
103-
104-
private static string GetGuidName(string name)
105-
{
106-
// If the name starts with "guid", then trim it off because
107-
// that prefix doesn't provide any additional information
108-
// since all symbols defined in the class are GUIDs.
109-
if (Regex.IsMatch(name, "^guid[A-Z]"))
110-
{
111-
name = name.Substring(4);
112-
}
113-
114-
return name;
115-
}
116-
117-
private static string SafeIdentifierName(string name)
118-
{
119-
// Replace all invalid characters with an underscore.
120-
name = Regex.Replace(name, "[^\\p{L}\\p{Nl}\\p{Mn}\\p{Mc}\\p{Nd}\\p{Pc}\\p{Cf}]", "_");
121-
122-
// Make sure the name starts with a letter or underscore.
123-
if (!Regex.IsMatch(name, "^[\\p{L}\\p{Nl}_]"))
124-
{
125-
name = "_" + name;
126-
};
127-
128-
return name;
76+
options.GetOptions(file).TryGetValue("build_metadata.AdditionalFiles.Format", out string? format);
77+
return format ?? "";
12978
}
13079
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Text;
2+
3+
namespace Community.VisualStudio.SourceGenerators;
4+
5+
/// <summary>
6+
/// Writes the generated code for command table in the default format.
7+
/// </summary>
8+
internal class DefaultCommandTableCodeWriter : CommandTableCodeWriter
9+
{
10+
public override string Format => "Default";
11+
12+
public override IEnumerable<GeneratedFile> Write(CommandTable commandTable, string codeNamespace, string langVersion)
13+
{
14+
StringBuilder builder = new();
15+
WritePreamble(builder, langVersion);
16+
builder.AppendLine($"namespace {codeNamespace}");
17+
builder.AppendLine("{");
18+
builder.AppendLine($" /// <summary>Defines symbols from the {commandTable.Name}.vsct file.</summary>");
19+
builder.AppendLine($" internal sealed partial class {SafeIdentifierName(commandTable.Name)}");
20+
builder.AppendLine(" {");
21+
22+
foreach (GUIDSymbol guidSymbol in commandTable.GUIDSymbols.OrderBy((x) => x.Name))
23+
{
24+
string guidName = GetGuidName(guidSymbol.Name);
25+
builder.AppendLine($" /// <summary>Defines the \"{guidName}\" GUIDSymbol and its IDSymbols.</summary>");
26+
builder.AppendLine($" internal sealed partial class {SafeIdentifierName(guidName)}");
27+
builder.AppendLine(" {");
28+
29+
builder.AppendLine($" public const string GuidString = \"{guidSymbol.Value:D}\";");
30+
builder.AppendLine($" public static readonly System.Guid Guid = new System.Guid(GuidString);");
31+
32+
foreach (IDSymbol idSymbol in guidSymbol.IDSymbols.OrderBy((x) => x.Name))
33+
{
34+
builder.AppendLine($" public const int {SafeIdentifierName(idSymbol.Name)} = 0x{idSymbol.Value:X4};");
35+
}
36+
37+
builder.AppendLine(" }");
38+
}
39+
40+
builder.AppendLine(" }");
41+
builder.AppendLine("}");
42+
43+
yield return new GeneratedFile(SafeHintName($"{commandTable.Name}.g.cs"), builder.ToString());
44+
}
45+
}

0 commit comments

Comments
 (0)