diff --git a/src/CodingWithCalvin.ProjectRenamifier/CodingWithCalvin.ProjectRenamifier.csproj b/src/CodingWithCalvin.ProjectRenamifier/CodingWithCalvin.ProjectRenamifier.csproj index e85f232..e409917 100644 --- a/src/CodingWithCalvin.ProjectRenamifier/CodingWithCalvin.ProjectRenamifier.csproj +++ b/src/CodingWithCalvin.ProjectRenamifier/CodingWithCalvin.ProjectRenamifier.csproj @@ -71,6 +71,7 @@ + True diff --git a/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs b/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs index dd6fef9..37c07df 100644 --- a/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs +++ b/src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs @@ -80,6 +80,10 @@ private void RenameProject(Project project, DTE2 dte) var newName = dialog.NewProjectName; var projectFilePath = project.FullName; + // Collect projects that reference this project before removal + var referencingProjects = ProjectReferenceService.FindProjectsReferencingTarget(dte.Solution, projectFilePath); + var oldProjectFilePath = projectFilePath; + // Remove project from solution before file operations dte.Solution.Remove(project); @@ -95,12 +99,14 @@ private void RenameProject(Project project, DTE2 dte) // Rename parent directory if it matches the old project name projectFilePath = ProjectFileService.RenameParentDirectoryIfMatches(projectFilePath, currentName, newName); + // Update references in projects that referenced this project + ProjectReferenceService.UpdateProjectReferences(referencingProjects, oldProjectFilePath, projectFilePath); + // Re-add project to solution with new path dte.Solution.AddFromFile(projectFilePath); // TODO: Implement remaining rename operations // See open issues for requirements: - // - #23: Update project references // - #9: Update using statements across solution // - #11: Solution folder support // - #12: Progress indication diff --git a/src/CodingWithCalvin.ProjectRenamifier/Services/ProjectReferenceService.cs b/src/CodingWithCalvin.ProjectRenamifier/Services/ProjectReferenceService.cs new file mode 100644 index 0000000..5c03ae5 --- /dev/null +++ b/src/CodingWithCalvin.ProjectRenamifier/Services/ProjectReferenceService.cs @@ -0,0 +1,207 @@ +using System.Collections.Generic; +using System.IO; +using System.Xml; +using EnvDTE; + +namespace CodingWithCalvin.ProjectRenamifier.Services +{ + /// + /// Service for managing project references during rename operations. + /// + internal static class ProjectReferenceService + { + /// + /// Finds all projects in the solution that reference the specified project. + /// + /// The solution to search. + /// The full path to the project being renamed. + /// A list of project paths that reference the target project. + public static List FindProjectsReferencingTarget(Solution solution, string targetProjectPath) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + var referencingProjects = new List(); + var targetFileName = Path.GetFileName(targetProjectPath); + + foreach (Project project in solution.Projects) + { + FindReferencesInProject(project, targetProjectPath, targetFileName, referencingProjects); + } + + return referencingProjects; + } + + /// + /// Recursively searches a project (and solution folders) for references to the target. + /// + private static void FindReferencesInProject(Project project, string targetProjectPath, string targetFileName, List referencingProjects) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (project == null) + { + return; + } + + // Handle solution folders + if (project.Kind == EnvDTE.Constants.vsProjectKindSolutionItems) + { + foreach (ProjectItem item in project.ProjectItems) + { + if (item.SubProject != null) + { + FindReferencesInProject(item.SubProject, targetProjectPath, targetFileName, referencingProjects); + } + } + return; + } + + // Skip the target project itself + if (string.Equals(project.FullName, targetProjectPath, System.StringComparison.OrdinalIgnoreCase)) + { + return; + } + + // Check if this project references the target + if (!string.IsNullOrEmpty(project.FullName) && File.Exists(project.FullName)) + { + if (ProjectReferencesTarget(project.FullName, targetFileName)) + { + referencingProjects.Add(project.FullName); + } + } + } + + /// + /// Checks if a project file contains a reference to the target project. + /// + private static bool ProjectReferencesTarget(string projectFilePath, string targetFileName) + { + var doc = new XmlDocument(); + doc.Load(projectFilePath); + + var namespaceManager = new XmlNamespaceManager(doc.NameTable); + var msbuildNs = "http://schemas.microsoft.com/developer/msbuild/2003"; + var hasNamespace = doc.DocumentElement?.NamespaceURI == msbuildNs; + + XmlNodeList nodes; + if (hasNamespace) + { + namespaceManager.AddNamespace("ms", msbuildNs); + nodes = doc.SelectNodes("//ms:ProjectReference", namespaceManager); + } + else + { + nodes = doc.SelectNodes("//ProjectReference"); + } + + if (nodes == null) + { + return false; + } + + foreach (XmlNode node in nodes) + { + var includeAttr = node.Attributes?["Include"]?.Value; + if (!string.IsNullOrEmpty(includeAttr)) + { + var referencedFileName = Path.GetFileName(includeAttr); + if (string.Equals(referencedFileName, targetFileName, System.StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + /// + /// Updates project references in all projects that referenced the old project path. + /// + /// Projects that need their references updated. + /// The old path to the renamed project. + /// The new path to the renamed project. + public static void UpdateProjectReferences(List referencingProjectPaths, string oldProjectPath, string newProjectPath) + { + var oldFileName = Path.GetFileName(oldProjectPath); + + foreach (var projectPath in referencingProjectPaths) + { + UpdateReferencesInProject(projectPath, oldFileName, oldProjectPath, newProjectPath); + } + } + + /// + /// Updates references in a single project file. + /// + private static void UpdateReferencesInProject(string projectFilePath, string oldFileName, string oldProjectPath, string newProjectPath) + { + var doc = new XmlDocument(); + doc.PreserveWhitespace = true; + doc.Load(projectFilePath); + + var namespaceManager = new XmlNamespaceManager(doc.NameTable); + var msbuildNs = "http://schemas.microsoft.com/developer/msbuild/2003"; + var hasNamespace = doc.DocumentElement?.NamespaceURI == msbuildNs; + + XmlNodeList nodes; + if (hasNamespace) + { + namespaceManager.AddNamespace("ms", msbuildNs); + nodes = doc.SelectNodes("//ms:ProjectReference", namespaceManager); + } + else + { + nodes = doc.SelectNodes("//ProjectReference"); + } + + if (nodes == null) + { + return; + } + + var modified = false; + var projectDirectory = Path.GetDirectoryName(projectFilePath); + + foreach (XmlNode node in nodes) + { + var includeAttr = node.Attributes?["Include"]; + if (includeAttr == null || string.IsNullOrEmpty(includeAttr.Value)) + { + continue; + } + + var referencedFileName = Path.GetFileName(includeAttr.Value); + if (!string.Equals(referencedFileName, oldFileName, System.StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // Calculate new relative path from referencing project to renamed project + var newRelativePath = GetRelativePath(projectDirectory, newProjectPath); + includeAttr.Value = newRelativePath; + modified = true; + } + + if (modified) + { + doc.Save(projectFilePath); + } + } + + /// + /// Gets a relative path from one directory to a file. + /// + private static string GetRelativePath(string fromDirectory, string toFile) + { + var fromUri = new System.Uri(fromDirectory.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar); + var toUri = new System.Uri(toFile); + + var relativeUri = fromUri.MakeRelativeUri(toUri); + var relativePath = System.Uri.UnescapeDataString(relativeUri.ToString()); + + return relativePath.Replace('/', Path.DirectorySeparatorChar); + } + } +}