Forge
csharpdeeb25a2
1#nullable enable
2using System.Collections.ObjectModel;
3using System.Collections.Generic;
4using CascadeIDE.Contracts;
5using CascadeIDE.Models;
6
7namespace CascadeIDE.Features.Workspace.Application;
8
9/// <summary>
10/// Обход дерева <see cref="SolutionItem"/> для MCP: проекты, файлы, относительные пути, дерево для JSON.
11/// Без привязки к UI.
12/// </summary>
13[ComputingUnit]
14public static class McpSolutionTree
15{
16 public static IEnumerable<string> CollectProjectPaths(ObservableCollection<SolutionItem> roots)
17 {
18 foreach (var item in roots)
19 {
20 if (item.FullPath is { } p && (p.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) || p.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)))
21 yield return p;
22 foreach (var child in CollectProjectPaths(item.Children))
23 yield return child;
24 }
25 }
26
27 /// <summary>Нормализованные пути <c>.csproj</c>/<c>.fsproj</c> из дерева (без .sln).</summary>
28 public static List<string> CollectDistinctManagedProjectPaths(ObservableCollection<SolutionItem> roots) =>
29 CollectProjectPaths(roots)
30 .Where(static p => p.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) ||
31 p.EndsWith(".fsproj", StringComparison.OrdinalIgnoreCase))
32 .Select(static p => CanonicalFilePath.Normalize(p))
33 .Distinct(StringComparer.OrdinalIgnoreCase)
34 .ToList();
35
36 public static IEnumerable<(string Title, string FullPath)> CollectFileEntries(ObservableCollection<SolutionItem> roots)
37 {
38 foreach (var item in roots)
39 {
40 if (item.FullPath is { } p && !p.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) && !p.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) && !p.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase))
41 yield return (item.Title, p);
42 foreach (var child in CollectFileEntries(item.Children))
43 yield return child;
44 }
45 }
46
47 /// <summary>
48 /// Полный путь файла → путь владеющего <c>.csproj</c> (или <c>null</c>, если файл вне проекта в дереве).
49 /// Обход совпадает с обозревателем решения: узел с <c>.csproj</c> задаёт контекст для потомков.
50 /// Для «реального» MSBuild-проекта файла при плоском SDK-glob см. <see cref="ResolveOwningProjectPath"/>.
51 /// </summary>
52 public static Dictionary<string, string?> MapFileToProject(ObservableCollection<SolutionItem> roots)
53 {
54 var map = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
55
56 void Walk(SolutionItem node, string? projectPath)
57 {
58 if (node.FullPath is { } p)
59 {
60 if (p.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
61 {
62 string proj;
63 try
64 {
65 proj = CanonicalFilePath.Normalize(p);
66 }
67 catch
68 {
69 return;
70 }
71
72 foreach (var child in node.Children)
73 Walk(child, proj);
74 return;
75 }
76
77 if (!p.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)
78 && !p.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase))
79 {
80 try
81 {
82 // Первое вхождение пути в DFS определяет проект; не перезаписывать —
83 // иначе последний узел в дереве «перетягивает» общие/линкованные пути на чужой .csproj.
84 map.TryAdd(CanonicalFilePath.Normalize(p), projectPath);
85 }
86 catch
87 {
88 // ignore
89 }
90 }
91 }
92
93 if (node.FullPath is { } fp && fp.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
94 return;
95
96 foreach (var child in node.Children)
97 Walk(child, projectPath);
98 }
99
100 foreach (var root in roots)
101 Walk(root, null);
102
103 return map;
104 }
105
106 /// <summary>
107 /// Ближайший вверх по диску <c>.csproj</c>, которому по соглашению принадлежит файл (папка проекта = каталог с .csproj).
108 /// Нужен для <c>project_peer</c>: узел дерева решения может подвешивать все <c>.cs</c> под одним корневым .csproj из-за SDK-glob по каталогу.
109 /// </summary>
110 public static string? ResolveOwningProjectPath(string fileFullPath)
111 {
112 if (string.IsNullOrWhiteSpace(fileFullPath))
113 return null;
114 string full;
115 try
116 {
117 full = CanonicalFilePath.Normalize(fileFullPath.Trim());
118 }
119 catch
120 {
121 return null;
122 }
123
124 var dir = Path.GetDirectoryName(full);
125 while (!string.IsNullOrEmpty(dir))
126 {
127 string[] csprojs;
128 try
129 {
130 csprojs = Directory.GetFiles(dir, "*.csproj");
131 }
132 catch
133 {
134 break;
135 }
136
137 if (csprojs.Length > 0)
138 {
139 if (csprojs.Length == 1)
140 return CanonicalFilePath.Normalize(csprojs[0]);
141 var folderName = Path.GetFileName(dir);
142 var match = csprojs.FirstOrDefault(p =>
143 string.Equals(Path.GetFileNameWithoutExtension(p), folderName, StringComparison.OrdinalIgnoreCase));
144 return CanonicalFilePath.Normalize(match ?? csprojs[0]);
145 }
146
147 try
148 {
149 dir = Path.GetDirectoryName(dir);
150 }
151 catch
152 {
153 break;
154 }
155 }
156
157 return null;
158 }
159
160 /// <summary>
161 /// Артефакты сборки (obj/bin) — не участвуют в семантической навигации и часто попадают в дерево из-за явных Include в .csproj.
162 /// </summary>
163 public static bool IsBuildArtifactPath(string fullPath)
164 {
165 if (string.IsNullOrEmpty(fullPath))
166 return false;
167 return fullPath.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)
168 || fullPath.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)
169 || fullPath.Contains("/obj/", StringComparison.OrdinalIgnoreCase)
170 || fullPath.Contains("/bin/", StringComparison.OrdinalIgnoreCase);
171 }
172
173 public static string? GetRelativePath(string? solutionPath, string? fullPath)
174 {
175 if (string.IsNullOrEmpty(solutionPath) || string.IsNullOrEmpty(fullPath))
176 return null;
177 var solutionDir = Path.GetDirectoryName(solutionPath);
178 if (string.IsNullOrEmpty(solutionDir))
179 return null;
180 try
181 {
182 return Path.GetRelativePath(solutionDir, fullPath);
183 }
184 catch
185 {
186 return null;
187 }
188 }
189
190 public static object BuildSolutionTreeNode(SolutionItem item, string? solutionPath)
191 {
192 var relative = GetRelativePath(solutionPath, item.FullPath);
193 var path = item.FullPath;
194 var title = item.Title;
195 if (item.Children.Count == 0)
196 return new { title, path, relative_path = relative };
197 var children = item.Children.Select(c => BuildSolutionTreeNode(c, solutionPath)).ToList();
198 return new { title, path, relative_path = relative, children };
199 }
200}
201
View only · write via MCP/CIDE