Skip to content

Commit b051653

Browse files
authored
feat(assets): add ProjectReference integration when adding project-based assets (#9)
* feat(assets): add ProjectReference when adding project-based assets Implement project and file integration to match the original VS manifest designer behavior: - Add ProjectIntegrationService to manage .csproj modifications - Add ProjectReference with IncludeOutputGroupsInVSIX metadata when adding project-sourced assets or dependencies - Use TemplateProjectOutputGroup for template asset types - Remove ProjectReference when removing assets (if not used elsewhere) - Add files to project with IncludeInVSIX when adding file-sourced assets - Prompt user about file removal when removing file-sourced assets - Track ProjectFullPath in Asset and Dependency models Closes #8 * feat(assets): add type-specific output groups and token handling - Use type-specific output groups (PkgdefProjectOutputGroup for VsPackage/ToolboxControl, TemplateProjectOutputGroup for templates) - Add ReferenceOutputAssembly=false for template assets - Generate correct project tokens with output groups (|ProjectName;OutputGroup|) - Add %CurrentProject% source option for self-referencing assets - Handle template RequiredFolder pattern (Path = folder, TargetPath = token) - Clear dialog fields when source type changes
1 parent 10f72ff commit b051653

9 files changed

Lines changed: 1020 additions & 15 deletions

File tree

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/AddAssetDialog.xaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@
4949
<Label Grid.Row="1" Grid.Column="0" Content="Source:" VerticalAlignment="Center" />
5050
<ComboBox Grid.Row="1" Grid.Column="1" x:Name="SourceComboBox" Margin="0,0,0,10"
5151
SelectionChanged="SourceComboBox_SelectionChanged">
52-
<ComboBoxItem Content="Project" Tag="Project" />
53-
<ComboBoxItem Content="File" Tag="File" />
52+
<ComboBoxItem Content="This Project" Tag="CurrentProject" />
53+
<ComboBoxItem Content="Project in Solution" Tag="Project" />
54+
<ComboBoxItem Content="File on Disk" Tag="File" />
5455
</ComboBox>
5556

5657
<!-- Project (shown when Source is Project) -->

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/AddAssetDialog.xaml.cs

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public AddAssetDialog(IServiceProvider serviceProvider, Asset asset, string? man
4848
Source = asset.Source,
4949
Path = asset.Path,
5050
ProjectName = asset.ProjectName,
51+
ProjectFullPath = asset.ProjectFullPath,
5152
TargetPath = asset.TargetPath,
5253
VsixSubPath = asset.VsixSubPath,
5354
Addressable = asset.Addressable
@@ -87,28 +88,46 @@ private void TypeComboBox_SelectionChanged(object sender, SelectionChangedEventA
8788

8889
private void SourceComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
8990
{
91+
// Clear source-specific fields when source changes
92+
_selectedProject = null;
93+
if (ProjectTextBox != null)
94+
{
95+
ProjectTextBox.Text = string.Empty;
96+
}
97+
if (PathTextBox != null)
98+
{
99+
PathTextBox.Text = string.Empty;
100+
}
101+
90102
UpdateSourceVisibility();
91103
UpdateWarningVisibility();
92104
}
93105

94106
private void UpdateSourceVisibility()
95107
{
96-
var isProjectSource = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() == "Project";
108+
var source = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString();
109+
var isProjectSource = source == "Project";
110+
var isFileSource = source == "File";
97111

112+
// Show project picker only for Project source
98113
ProjectLabel.Visibility = isProjectSource ? Visibility.Visible : Visibility.Collapsed;
99114
ProjectGrid.Visibility = isProjectSource ? Visibility.Visible : Visibility.Collapsed;
100-
PathLabel.Visibility = isProjectSource ? Visibility.Collapsed : Visibility.Visible;
101-
PathGrid.Visibility = isProjectSource ? Visibility.Collapsed : Visibility.Visible;
115+
116+
// Show path picker only for File source
117+
PathLabel.Visibility = isFileSource ? Visibility.Visible : Visibility.Collapsed;
118+
PathGrid.Visibility = isFileSource ? Visibility.Visible : Visibility.Collapsed;
119+
120+
// CurrentProject source shows nothing additional
102121
}
103122

104123
private void UpdateWarningVisibility()
105124
{
106125
var selectedType = TypeComboBox.SelectedItem as string;
107126
var isTemplateType = AssetTypes.IsTemplate(selectedType ?? string.Empty);
108-
var isProjectSource = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() == "Project";
127+
var source = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString();
109128

110129
// Show warning if template asset type + project source + SDK-style project without VsixSdk
111-
if (isTemplateType && isProjectSource && _selectedProject != null)
130+
if (isTemplateType && source == "Project" && _selectedProject != null)
112131
{
113132
if (_selectedProject.IsSdkStyle && !_selectedProject.UsesVsixSdk)
114133
{
@@ -149,16 +168,18 @@ private void BrowsePath_Click(object sender, RoutedEventArgs e)
149168

150169
private void OkButton_Click(object sender, RoutedEventArgs e)
151170
{
152-
var source = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Project";
171+
var source = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "CurrentProject";
153172
var isProjectSource = source == "Project";
173+
var isCurrentProjectSource = source == "CurrentProject";
174+
var isFileSource = source == "File";
154175

155176
if (isProjectSource && string.IsNullOrWhiteSpace(ProjectTextBox.Text))
156177
{
157178
MessageBox.Show("Please select a project.", "Validation Error", MessageBoxButton.OK, MessageBoxImage.Warning);
158179
return;
159180
}
160181

161-
if (!isProjectSource && string.IsNullOrWhiteSpace(PathTextBox.Text))
182+
if (isFileSource && string.IsNullOrWhiteSpace(PathTextBox.Text))
162183
{
163184
MessageBox.Show("Please specify a file path.", "Validation Error", MessageBoxButton.OK, MessageBoxImage.Warning);
164185
return;
@@ -167,15 +188,38 @@ private void OkButton_Click(object sender, RoutedEventArgs e)
167188
Asset.Type = TypeComboBox.SelectedItem as string ?? AssetTypes.VsPackage;
168189
Asset.Source = source;
169190

170-
if (isProjectSource)
191+
if (isCurrentProjectSource)
192+
{
193+
// CurrentProject uses %CurrentProject% token - no ProjectReference needed
194+
Asset.Path = "%CurrentProject%";
195+
Asset.ProjectName = null;
196+
Asset.ProjectFullPath = null;
197+
Asset.TargetPath = null;
198+
}
199+
else if (isProjectSource)
171200
{
172201
Asset.ProjectName = ProjectTextBox.Text;
173-
Asset.Path = $"|{ProjectTextBox.Text}|";
202+
Asset.ProjectFullPath = _selectedProject?.FullPath;
203+
204+
// Template assets use Path for the required folder and TargetPath for the project token
205+
var requiredFolder = AssetTypes.GetRequiredFolder(Asset.Type);
206+
if (!string.IsNullOrEmpty(requiredFolder))
207+
{
208+
Asset.Path = requiredFolder;
209+
Asset.TargetPath = AssetTypes.GenerateProjectToken(ProjectTextBox.Text, Asset.Type);
210+
}
211+
else
212+
{
213+
Asset.Path = AssetTypes.GenerateProjectToken(ProjectTextBox.Text, Asset.Type);
214+
Asset.TargetPath = null;
215+
}
174216
}
175217
else
176218
{
177219
Asset.Path = PathTextBox.Text;
178220
Asset.ProjectName = null;
221+
Asset.ProjectFullPath = null;
222+
Asset.TargetPath = null;
179223
}
180224

181225
Asset.VsixSubPath = string.IsNullOrWhiteSpace(VsixSubPathTextBox.Text) ? null : VsixSubPathTextBox.Text;

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/AddDependencyDialog.xaml.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public AddDependencyDialog(IServiceProvider serviceProvider, Dependency dependen
4848
DisplayName = dependency.DisplayName,
4949
Version = dependency.Version,
5050
Source = dependency.Source,
51-
Location = dependency.Location
51+
Location = dependency.Location,
52+
ProjectFullPath = dependency.ProjectFullPath
5253
};
5354

5455
InitializeComponent();
@@ -77,6 +78,21 @@ public AddDependencyDialog(IServiceProvider serviceProvider, Dependency dependen
7778

7879
private void SourceComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
7980
{
81+
// Clear source-specific fields when source changes
82+
_selectedProject = null;
83+
if (ProjectTextBox != null)
84+
{
85+
ProjectTextBox.Text = string.Empty;
86+
}
87+
if (LocationTextBox != null)
88+
{
89+
LocationTextBox.Text = string.Empty;
90+
}
91+
if (IdTextBox != null)
92+
{
93+
IdTextBox.Text = string.Empty;
94+
}
95+
8096
UpdateSourceVisibility();
8197
}
8298

@@ -158,6 +174,7 @@ private void OkButton_Click(object sender, RoutedEventArgs e)
158174
Dependency.Version = VersionTextBox.Text;
159175
Dependency.Source = source;
160176
Dependency.Location = source == "File" ? LocationTextBox.Text : null;
177+
Dependency.ProjectFullPath = source == "Project" ? _selectedProject?.FullPath : null;
161178

162179
DialogResult = true;
163180
Close();

src/CodingWithCalvin.VsixManifestDesigner/Models/Asset.cs

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,15 @@ public sealed class Asset
2525
/// </summary>
2626
public string? ProjectName { get; set; }
2727

28+
/// <summary>
29+
/// Gets or sets the full path to the referenced project file.
30+
/// Used for adding ProjectReference to the VSIX project.
31+
/// </summary>
32+
public string? ProjectFullPath { get; set; }
33+
2834
/// <summary>
2935
/// Gets or sets the target path (design-time attribute).
36+
/// Used for template assets where Path is the required folder.
3037
/// </summary>
3138
public string? TargetPath { get; set; }
3239

@@ -55,6 +62,26 @@ public static class AssetTypes
5562
public const string Analyzer = "Microsoft.VisualStudio.Analyzer";
5663
public const string CodeLensComponent = "Microsoft.VisualStudio.CodeLensComponent";
5764

65+
/// <summary>
66+
/// Standard output groups for most assets.
67+
/// </summary>
68+
public const string StandardOutputGroups = "BuiltProjectOutputGroup;BuiltProjectOutputGroupDependencies;GetCopyToOutputDirectoryItems;SatelliteDllsProjectOutputGroup";
69+
70+
/// <summary>
71+
/// Debug output groups (local only).
72+
/// </summary>
73+
public const string DebugOutputGroups = "DebugSymbolsProjectOutputGroup";
74+
75+
/// <summary>
76+
/// Output groups for template assets.
77+
/// </summary>
78+
public const string TemplateOutputGroups = "TemplateProjectOutputGroup";
79+
80+
/// <summary>
81+
/// Additional output group for VsPackage and ToolboxControl.
82+
/// </summary>
83+
public const string PkgdefOutputGroup = "PkgdefProjectOutputGroup";
84+
5885
/// <summary>
5986
/// Gets all well-known asset types.
6087
/// </summary>
@@ -73,8 +100,89 @@ public static class AssetTypes
73100
/// <summary>
74101
/// Determines if the asset type is a template type.
75102
/// </summary>
76-
public static bool IsTemplate(string assetType)
103+
public static bool IsTemplate(string? assetType)
77104
{
78105
return assetType == ProjectTemplate || assetType == ItemTemplate;
79106
}
107+
108+
/// <summary>
109+
/// Determines if the asset type requires PkgdefProjectOutputGroup.
110+
/// </summary>
111+
public static bool RequiresPkgdef(string? assetType)
112+
{
113+
return assetType == VsPackage || assetType == ToolboxControl;
114+
}
115+
116+
/// <summary>
117+
/// Gets the output groups for the specified asset type.
118+
/// </summary>
119+
public static string GetOutputGroups(string? assetType)
120+
{
121+
if (IsTemplate(assetType))
122+
{
123+
return TemplateOutputGroups;
124+
}
125+
126+
if (RequiresPkgdef(assetType))
127+
{
128+
return StandardOutputGroups + ";" + PkgdefOutputGroup;
129+
}
130+
131+
return StandardOutputGroups;
132+
}
133+
134+
/// <summary>
135+
/// Gets the target output group for the specified asset type.
136+
/// Used in project tokens like |ProjectName;OutputGroup|.
137+
/// </summary>
138+
public static string? GetTargetOutputGroup(string? assetType)
139+
{
140+
if (IsTemplate(assetType))
141+
{
142+
return TemplateOutputGroups;
143+
}
144+
145+
if (RequiresPkgdef(assetType))
146+
{
147+
return PkgdefOutputGroup;
148+
}
149+
150+
return null;
151+
}
152+
153+
/// <summary>
154+
/// Gets the required folder for the specified asset type.
155+
/// Template assets must be placed in specific folders.
156+
/// </summary>
157+
public static string? GetRequiredFolder(string? assetType)
158+
{
159+
return assetType switch
160+
{
161+
ProjectTemplate => "ProjectTemplates",
162+
ItemTemplate => "ItemTemplates",
163+
_ => null
164+
};
165+
}
166+
167+
/// <summary>
168+
/// Determines if the asset type should set ReferenceOutputAssembly to false.
169+
/// </summary>
170+
public static bool ShouldDisableReferenceOutputAssembly(string? assetType)
171+
{
172+
return IsTemplate(assetType);
173+
}
174+
175+
/// <summary>
176+
/// Generates the project token for the specified project name and asset type.
177+
/// </summary>
178+
public static string GenerateProjectToken(string projectName, string? assetType)
179+
{
180+
var targetGroup = GetTargetOutputGroup(assetType);
181+
if (!string.IsNullOrEmpty(targetGroup))
182+
{
183+
return $"|{projectName};{targetGroup}|";
184+
}
185+
186+
return $"|{projectName}|";
187+
}
80188
}

src/CodingWithCalvin.VsixManifestDesigner/Models/Dependency.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,10 @@ public sealed class Dependency
2929
/// Gets or sets the location (URL or relative path).
3030
/// </summary>
3131
public string? Location { get; set; }
32+
33+
/// <summary>
34+
/// Gets or sets the full path to the referenced project file.
35+
/// Used for adding ProjectReference to the VSIX project when Source is "Project".
36+
/// </summary>
37+
public string? ProjectFullPath { get; set; }
3238
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.Threading.Tasks;
2+
3+
namespace CodingWithCalvin.VsixManifestDesigner.Services;
4+
5+
/// <summary>
6+
/// Service for integrating manifest changes with the VSIX project file.
7+
/// Handles adding/removing ProjectReferences and file items.
8+
/// </summary>
9+
public interface IProjectIntegrationService
10+
{
11+
/// <summary>
12+
/// Adds a ProjectReference to the VSIX project with appropriate metadata.
13+
/// </summary>
14+
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
15+
/// <param name="referencedProjectPath">Path to the project being referenced.</param>
16+
/// <param name="assetType">The asset type being added (affects output groups).</param>
17+
/// <returns>True if the reference was added; false if it already existed.</returns>
18+
Task<bool> AddProjectReferenceAsync(string vsixProjectPath, string referencedProjectPath, string? assetType = null);
19+
20+
/// <summary>
21+
/// Removes a ProjectReference from the VSIX project.
22+
/// </summary>
23+
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
24+
/// <param name="referencedProjectPath">Path to the project being dereferenced.</param>
25+
/// <returns>True if the reference was removed; false if it didn't exist.</returns>
26+
Task<bool> RemoveProjectReferenceAsync(string vsixProjectPath, string referencedProjectPath);
27+
28+
/// <summary>
29+
/// Checks if a ProjectReference exists in the VSIX project.
30+
/// </summary>
31+
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
32+
/// <param name="referencedProjectPath">Path to the project to check.</param>
33+
/// <returns>True if the reference exists.</returns>
34+
Task<bool> HasProjectReferenceAsync(string vsixProjectPath, string referencedProjectPath);
35+
36+
/// <summary>
37+
/// Adds a file to the VSIX project with Include in VSIX enabled.
38+
/// </summary>
39+
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
40+
/// <param name="sourceFilePath">Path to the source file to add.</param>
41+
/// <param name="includeInVsix">Whether to set IncludeInVSIX=true.</param>
42+
/// <returns>The project-relative path of the added file.</returns>
43+
Task<string?> AddFileToProjectAsync(string vsixProjectPath, string sourceFilePath, bool includeInVsix = true);
44+
45+
/// <summary>
46+
/// Removes a file from the VSIX project.
47+
/// </summary>
48+
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
49+
/// <param name="projectRelativePath">The project-relative path of the file to remove.</param>
50+
/// <param name="deleteFromDisk">Whether to also delete the file from disk.</param>
51+
/// <returns>True if the file was removed.</returns>
52+
Task<bool> RemoveFileFromProjectAsync(string vsixProjectPath, string projectRelativePath, bool deleteFromDisk = false);
53+
54+
/// <summary>
55+
/// Gets the VSIX project path from a manifest file path.
56+
/// </summary>
57+
/// <param name="manifestFilePath">Path to the .vsixmanifest file.</param>
58+
/// <returns>Path to the containing .csproj file, or null if not found.</returns>
59+
string? GetVsixProjectPath(string manifestFilePath);
60+
}

0 commit comments

Comments
 (0)