Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@
<Label Grid.Row="1" Grid.Column="0" Content="Source:" VerticalAlignment="Center" />
<ComboBox Grid.Row="1" Grid.Column="1" x:Name="SourceComboBox" Margin="0,0,0,10"
SelectionChanged="SourceComboBox_SelectionChanged">
<ComboBoxItem Content="Project" Tag="Project" />
<ComboBoxItem Content="File" Tag="File" />
<ComboBoxItem Content="This Project" Tag="CurrentProject" />
<ComboBoxItem Content="Project in Solution" Tag="Project" />
<ComboBoxItem Content="File on Disk" Tag="File" />
</ComboBox>

<!-- Project (shown when Source is Project) -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public AddAssetDialog(IServiceProvider serviceProvider, Asset asset, string? man
Source = asset.Source,
Path = asset.Path,
ProjectName = asset.ProjectName,
ProjectFullPath = asset.ProjectFullPath,
TargetPath = asset.TargetPath,
VsixSubPath = asset.VsixSubPath,
Addressable = asset.Addressable
Expand Down Expand Up @@ -87,28 +88,46 @@ private void TypeComboBox_SelectionChanged(object sender, SelectionChangedEventA

private void SourceComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Clear source-specific fields when source changes
_selectedProject = null;
if (ProjectTextBox != null)
{
ProjectTextBox.Text = string.Empty;
}
if (PathTextBox != null)
{
PathTextBox.Text = string.Empty;
}

UpdateSourceVisibility();
UpdateWarningVisibility();
}

private void UpdateSourceVisibility()
{
var isProjectSource = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() == "Project";
var source = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString();
var isProjectSource = source == "Project";
var isFileSource = source == "File";

// Show project picker only for Project source
ProjectLabel.Visibility = isProjectSource ? Visibility.Visible : Visibility.Collapsed;
ProjectGrid.Visibility = isProjectSource ? Visibility.Visible : Visibility.Collapsed;
PathLabel.Visibility = isProjectSource ? Visibility.Collapsed : Visibility.Visible;
PathGrid.Visibility = isProjectSource ? Visibility.Collapsed : Visibility.Visible;

// Show path picker only for File source
PathLabel.Visibility = isFileSource ? Visibility.Visible : Visibility.Collapsed;
PathGrid.Visibility = isFileSource ? Visibility.Visible : Visibility.Collapsed;

// CurrentProject source shows nothing additional
}

private void UpdateWarningVisibility()
{
var selectedType = TypeComboBox.SelectedItem as string;
var isTemplateType = AssetTypes.IsTemplate(selectedType ?? string.Empty);
var isProjectSource = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() == "Project";
var source = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString();

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

private void OkButton_Click(object sender, RoutedEventArgs e)
{
var source = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Project";
var source = (SourceComboBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "CurrentProject";
var isProjectSource = source == "Project";
var isCurrentProjectSource = source == "CurrentProject";
var isFileSource = source == "File";

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

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

if (isProjectSource)
if (isCurrentProjectSource)
{
// CurrentProject uses %CurrentProject% token - no ProjectReference needed
Asset.Path = "%CurrentProject%";
Asset.ProjectName = null;
Asset.ProjectFullPath = null;
Asset.TargetPath = null;
}
else if (isProjectSource)
{
Asset.ProjectName = ProjectTextBox.Text;
Asset.Path = $"|{ProjectTextBox.Text}|";
Asset.ProjectFullPath = _selectedProject?.FullPath;

// Template assets use Path for the required folder and TargetPath for the project token
var requiredFolder = AssetTypes.GetRequiredFolder(Asset.Type);
if (!string.IsNullOrEmpty(requiredFolder))
{
Asset.Path = requiredFolder;
Asset.TargetPath = AssetTypes.GenerateProjectToken(ProjectTextBox.Text, Asset.Type);
}
else
{
Asset.Path = AssetTypes.GenerateProjectToken(ProjectTextBox.Text, Asset.Type);
Asset.TargetPath = null;
}
}
else
{
Asset.Path = PathTextBox.Text;
Asset.ProjectName = null;
Asset.ProjectFullPath = null;
Asset.TargetPath = null;
}

Asset.VsixSubPath = string.IsNullOrWhiteSpace(VsixSubPathTextBox.Text) ? null : VsixSubPathTextBox.Text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public AddDependencyDialog(IServiceProvider serviceProvider, Dependency dependen
DisplayName = dependency.DisplayName,
Version = dependency.Version,
Source = dependency.Source,
Location = dependency.Location
Location = dependency.Location,
ProjectFullPath = dependency.ProjectFullPath
};

InitializeComponent();
Expand Down Expand Up @@ -77,6 +78,21 @@ public AddDependencyDialog(IServiceProvider serviceProvider, Dependency dependen

private void SourceComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Clear source-specific fields when source changes
_selectedProject = null;
if (ProjectTextBox != null)
{
ProjectTextBox.Text = string.Empty;
}
if (LocationTextBox != null)
{
LocationTextBox.Text = string.Empty;
}
if (IdTextBox != null)
{
IdTextBox.Text = string.Empty;
}

UpdateSourceVisibility();
}

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

DialogResult = true;
Close();
Expand Down
110 changes: 109 additions & 1 deletion src/CodingWithCalvin.VsixManifestDesigner/Models/Asset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@ public sealed class Asset
/// </summary>
public string? ProjectName { get; set; }

/// <summary>
/// Gets or sets the full path to the referenced project file.
/// Used for adding ProjectReference to the VSIX project.
/// </summary>
public string? ProjectFullPath { get; set; }

/// <summary>
/// Gets or sets the target path (design-time attribute).
/// Used for template assets where Path is the required folder.
/// </summary>
public string? TargetPath { get; set; }

Expand Down Expand Up @@ -55,6 +62,26 @@ public static class AssetTypes
public const string Analyzer = "Microsoft.VisualStudio.Analyzer";
public const string CodeLensComponent = "Microsoft.VisualStudio.CodeLensComponent";

/// <summary>
/// Standard output groups for most assets.
/// </summary>
public const string StandardOutputGroups = "BuiltProjectOutputGroup;BuiltProjectOutputGroupDependencies;GetCopyToOutputDirectoryItems;SatelliteDllsProjectOutputGroup";

/// <summary>
/// Debug output groups (local only).
/// </summary>
public const string DebugOutputGroups = "DebugSymbolsProjectOutputGroup";

/// <summary>
/// Output groups for template assets.
/// </summary>
public const string TemplateOutputGroups = "TemplateProjectOutputGroup";

/// <summary>
/// Additional output group for VsPackage and ToolboxControl.
/// </summary>
public const string PkgdefOutputGroup = "PkgdefProjectOutputGroup";

/// <summary>
/// Gets all well-known asset types.
/// </summary>
Expand All @@ -73,8 +100,89 @@ public static class AssetTypes
/// <summary>
/// Determines if the asset type is a template type.
/// </summary>
public static bool IsTemplate(string assetType)
public static bool IsTemplate(string? assetType)
{
return assetType == ProjectTemplate || assetType == ItemTemplate;
}

/// <summary>
/// Determines if the asset type requires PkgdefProjectOutputGroup.
/// </summary>
public static bool RequiresPkgdef(string? assetType)
{
return assetType == VsPackage || assetType == ToolboxControl;
}

/// <summary>
/// Gets the output groups for the specified asset type.
/// </summary>
public static string GetOutputGroups(string? assetType)
{
if (IsTemplate(assetType))
{
return TemplateOutputGroups;
}

if (RequiresPkgdef(assetType))
{
return StandardOutputGroups + ";" + PkgdefOutputGroup;
}

return StandardOutputGroups;
}

/// <summary>
/// Gets the target output group for the specified asset type.
/// Used in project tokens like |ProjectName;OutputGroup|.
/// </summary>
public static string? GetTargetOutputGroup(string? assetType)
{
if (IsTemplate(assetType))
{
return TemplateOutputGroups;
}

if (RequiresPkgdef(assetType))
{
return PkgdefOutputGroup;
}

return null;
}

/// <summary>
/// Gets the required folder for the specified asset type.
/// Template assets must be placed in specific folders.
/// </summary>
public static string? GetRequiredFolder(string? assetType)
{
return assetType switch
{
ProjectTemplate => "ProjectTemplates",
ItemTemplate => "ItemTemplates",
_ => null
};
}

/// <summary>
/// Determines if the asset type should set ReferenceOutputAssembly to false.
/// </summary>
public static bool ShouldDisableReferenceOutputAssembly(string? assetType)
{
return IsTemplate(assetType);
}

/// <summary>
/// Generates the project token for the specified project name and asset type.
/// </summary>
public static string GenerateProjectToken(string projectName, string? assetType)
{
var targetGroup = GetTargetOutputGroup(assetType);
if (!string.IsNullOrEmpty(targetGroup))
{
return $"|{projectName};{targetGroup}|";
}

return $"|{projectName}|";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@ public sealed class Dependency
/// Gets or sets the location (URL or relative path).
/// </summary>
public string? Location { get; set; }

/// <summary>
/// Gets or sets the full path to the referenced project file.
/// Used for adding ProjectReference to the VSIX project when Source is "Project".
/// </summary>
public string? ProjectFullPath { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Threading.Tasks;

namespace CodingWithCalvin.VsixManifestDesigner.Services;

/// <summary>
/// Service for integrating manifest changes with the VSIX project file.
/// Handles adding/removing ProjectReferences and file items.
/// </summary>
public interface IProjectIntegrationService
{
/// <summary>
/// Adds a ProjectReference to the VSIX project with appropriate metadata.
/// </summary>
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
/// <param name="referencedProjectPath">Path to the project being referenced.</param>
/// <param name="assetType">The asset type being added (affects output groups).</param>
/// <returns>True if the reference was added; false if it already existed.</returns>
Task<bool> AddProjectReferenceAsync(string vsixProjectPath, string referencedProjectPath, string? assetType = null);

/// <summary>
/// Removes a ProjectReference from the VSIX project.
/// </summary>
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
/// <param name="referencedProjectPath">Path to the project being dereferenced.</param>
/// <returns>True if the reference was removed; false if it didn't exist.</returns>
Task<bool> RemoveProjectReferenceAsync(string vsixProjectPath, string referencedProjectPath);

/// <summary>
/// Checks if a ProjectReference exists in the VSIX project.
/// </summary>
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
/// <param name="referencedProjectPath">Path to the project to check.</param>
/// <returns>True if the reference exists.</returns>
Task<bool> HasProjectReferenceAsync(string vsixProjectPath, string referencedProjectPath);

/// <summary>
/// Adds a file to the VSIX project with Include in VSIX enabled.
/// </summary>
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
/// <param name="sourceFilePath">Path to the source file to add.</param>
/// <param name="includeInVsix">Whether to set IncludeInVSIX=true.</param>
/// <returns>The project-relative path of the added file.</returns>
Task<string?> AddFileToProjectAsync(string vsixProjectPath, string sourceFilePath, bool includeInVsix = true);

/// <summary>
/// Removes a file from the VSIX project.
/// </summary>
/// <param name="vsixProjectPath">Path to the VSIX project file (.csproj).</param>
/// <param name="projectRelativePath">The project-relative path of the file to remove.</param>
/// <param name="deleteFromDisk">Whether to also delete the file from disk.</param>
/// <returns>True if the file was removed.</returns>
Task<bool> RemoveFileFromProjectAsync(string vsixProjectPath, string projectRelativePath, bool deleteFromDisk = false);

/// <summary>
/// Gets the VSIX project path from a manifest file path.
/// </summary>
/// <param name="manifestFilePath">Path to the .vsixmanifest file.</param>
/// <returns>Path to the containing .csproj file, or null if not found.</returns>
string? GetVsixProjectPath(string manifestFilePath);
}
Loading