Skip to content

Commit 22efe75

Browse files
authored
feat(references): update project references after rename (#29)
1 parent cbe7381 commit 22efe75

File tree

3 files changed

+215
-1
lines changed

3 files changed

+215
-1
lines changed

src/CodingWithCalvin.ProjectRenamifier/CodingWithCalvin.ProjectRenamifier.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
</Compile>
7272
<Compile Include="ProjectRenamifierPackage.cs" />
7373
<Compile Include="Services\ProjectFileService.cs" />
74+
<Compile Include="Services\ProjectReferenceService.cs" />
7475
<Compile Include="Services\SourceFileService.cs" />
7576
<Compile Include="source.extension.cs">
7677
<AutoGen>True</AutoGen>

src/CodingWithCalvin.ProjectRenamifier/Commands/RenamifyProjectCommand.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ private void RenameProject(Project project, DTE2 dte)
8080
var newName = dialog.NewProjectName;
8181
var projectFilePath = project.FullName;
8282

83+
// Collect projects that reference this project before removal
84+
var referencingProjects = ProjectReferenceService.FindProjectsReferencingTarget(dte.Solution, projectFilePath);
85+
var oldProjectFilePath = projectFilePath;
86+
8387
// Remove project from solution before file operations
8488
dte.Solution.Remove(project);
8589

@@ -95,12 +99,14 @@ private void RenameProject(Project project, DTE2 dte)
9599
// Rename parent directory if it matches the old project name
96100
projectFilePath = ProjectFileService.RenameParentDirectoryIfMatches(projectFilePath, currentName, newName);
97101

102+
// Update references in projects that referenced this project
103+
ProjectReferenceService.UpdateProjectReferences(referencingProjects, oldProjectFilePath, projectFilePath);
104+
98105
// Re-add project to solution with new path
99106
dte.Solution.AddFromFile(projectFilePath);
100107

101108
// TODO: Implement remaining rename operations
102109
// See open issues for requirements:
103-
// - #23: Update project references
104110
// - #9: Update using statements across solution
105111
// - #11: Solution folder support
106112
// - #12: Progress indication
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Xml;
4+
using EnvDTE;
5+
6+
namespace CodingWithCalvin.ProjectRenamifier.Services
7+
{
8+
/// <summary>
9+
/// Service for managing project references during rename operations.
10+
/// </summary>
11+
internal static class ProjectReferenceService
12+
{
13+
/// <summary>
14+
/// Finds all projects in the solution that reference the specified project.
15+
/// </summary>
16+
/// <param name="solution">The solution to search.</param>
17+
/// <param name="targetProjectPath">The full path to the project being renamed.</param>
18+
/// <returns>A list of project paths that reference the target project.</returns>
19+
public static List<string> FindProjectsReferencingTarget(Solution solution, string targetProjectPath)
20+
{
21+
ThreadHelper.ThrowIfNotOnUIThread();
22+
23+
var referencingProjects = new List<string>();
24+
var targetFileName = Path.GetFileName(targetProjectPath);
25+
26+
foreach (Project project in solution.Projects)
27+
{
28+
FindReferencesInProject(project, targetProjectPath, targetFileName, referencingProjects);
29+
}
30+
31+
return referencingProjects;
32+
}
33+
34+
/// <summary>
35+
/// Recursively searches a project (and solution folders) for references to the target.
36+
/// </summary>
37+
private static void FindReferencesInProject(Project project, string targetProjectPath, string targetFileName, List<string> referencingProjects)
38+
{
39+
ThreadHelper.ThrowIfNotOnUIThread();
40+
41+
if (project == null)
42+
{
43+
return;
44+
}
45+
46+
// Handle solution folders
47+
if (project.Kind == EnvDTE.Constants.vsProjectKindSolutionItems)
48+
{
49+
foreach (ProjectItem item in project.ProjectItems)
50+
{
51+
if (item.SubProject != null)
52+
{
53+
FindReferencesInProject(item.SubProject, targetProjectPath, targetFileName, referencingProjects);
54+
}
55+
}
56+
return;
57+
}
58+
59+
// Skip the target project itself
60+
if (string.Equals(project.FullName, targetProjectPath, System.StringComparison.OrdinalIgnoreCase))
61+
{
62+
return;
63+
}
64+
65+
// Check if this project references the target
66+
if (!string.IsNullOrEmpty(project.FullName) && File.Exists(project.FullName))
67+
{
68+
if (ProjectReferencesTarget(project.FullName, targetFileName))
69+
{
70+
referencingProjects.Add(project.FullName);
71+
}
72+
}
73+
}
74+
75+
/// <summary>
76+
/// Checks if a project file contains a reference to the target project.
77+
/// </summary>
78+
private static bool ProjectReferencesTarget(string projectFilePath, string targetFileName)
79+
{
80+
var doc = new XmlDocument();
81+
doc.Load(projectFilePath);
82+
83+
var namespaceManager = new XmlNamespaceManager(doc.NameTable);
84+
var msbuildNs = "http://schemas.microsoft.com/developer/msbuild/2003";
85+
var hasNamespace = doc.DocumentElement?.NamespaceURI == msbuildNs;
86+
87+
XmlNodeList nodes;
88+
if (hasNamespace)
89+
{
90+
namespaceManager.AddNamespace("ms", msbuildNs);
91+
nodes = doc.SelectNodes("//ms:ProjectReference", namespaceManager);
92+
}
93+
else
94+
{
95+
nodes = doc.SelectNodes("//ProjectReference");
96+
}
97+
98+
if (nodes == null)
99+
{
100+
return false;
101+
}
102+
103+
foreach (XmlNode node in nodes)
104+
{
105+
var includeAttr = node.Attributes?["Include"]?.Value;
106+
if (!string.IsNullOrEmpty(includeAttr))
107+
{
108+
var referencedFileName = Path.GetFileName(includeAttr);
109+
if (string.Equals(referencedFileName, targetFileName, System.StringComparison.OrdinalIgnoreCase))
110+
{
111+
return true;
112+
}
113+
}
114+
}
115+
116+
return false;
117+
}
118+
119+
/// <summary>
120+
/// Updates project references in all projects that referenced the old project path.
121+
/// </summary>
122+
/// <param name="referencingProjectPaths">Projects that need their references updated.</param>
123+
/// <param name="oldProjectPath">The old path to the renamed project.</param>
124+
/// <param name="newProjectPath">The new path to the renamed project.</param>
125+
public static void UpdateProjectReferences(List<string> referencingProjectPaths, string oldProjectPath, string newProjectPath)
126+
{
127+
var oldFileName = Path.GetFileName(oldProjectPath);
128+
129+
foreach (var projectPath in referencingProjectPaths)
130+
{
131+
UpdateReferencesInProject(projectPath, oldFileName, oldProjectPath, newProjectPath);
132+
}
133+
}
134+
135+
/// <summary>
136+
/// Updates references in a single project file.
137+
/// </summary>
138+
private static void UpdateReferencesInProject(string projectFilePath, string oldFileName, string oldProjectPath, string newProjectPath)
139+
{
140+
var doc = new XmlDocument();
141+
doc.PreserveWhitespace = true;
142+
doc.Load(projectFilePath);
143+
144+
var namespaceManager = new XmlNamespaceManager(doc.NameTable);
145+
var msbuildNs = "http://schemas.microsoft.com/developer/msbuild/2003";
146+
var hasNamespace = doc.DocumentElement?.NamespaceURI == msbuildNs;
147+
148+
XmlNodeList nodes;
149+
if (hasNamespace)
150+
{
151+
namespaceManager.AddNamespace("ms", msbuildNs);
152+
nodes = doc.SelectNodes("//ms:ProjectReference", namespaceManager);
153+
}
154+
else
155+
{
156+
nodes = doc.SelectNodes("//ProjectReference");
157+
}
158+
159+
if (nodes == null)
160+
{
161+
return;
162+
}
163+
164+
var modified = false;
165+
var projectDirectory = Path.GetDirectoryName(projectFilePath);
166+
167+
foreach (XmlNode node in nodes)
168+
{
169+
var includeAttr = node.Attributes?["Include"];
170+
if (includeAttr == null || string.IsNullOrEmpty(includeAttr.Value))
171+
{
172+
continue;
173+
}
174+
175+
var referencedFileName = Path.GetFileName(includeAttr.Value);
176+
if (!string.Equals(referencedFileName, oldFileName, System.StringComparison.OrdinalIgnoreCase))
177+
{
178+
continue;
179+
}
180+
181+
// Calculate new relative path from referencing project to renamed project
182+
var newRelativePath = GetRelativePath(projectDirectory, newProjectPath);
183+
includeAttr.Value = newRelativePath;
184+
modified = true;
185+
}
186+
187+
if (modified)
188+
{
189+
doc.Save(projectFilePath);
190+
}
191+
}
192+
193+
/// <summary>
194+
/// Gets a relative path from one directory to a file.
195+
/// </summary>
196+
private static string GetRelativePath(string fromDirectory, string toFile)
197+
{
198+
var fromUri = new System.Uri(fromDirectory.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
199+
var toUri = new System.Uri(toFile);
200+
201+
var relativeUri = fromUri.MakeRelativeUri(toUri);
202+
var relativePath = System.Uri.UnescapeDataString(relativeUri.ToString());
203+
204+
return relativePath.Replace('/', Path.DirectorySeparatorChar);
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)