Forge
csharpdeeb25a2
1using CascadeIDE.Models;
2using CascadeIDE.Contracts;
3
4namespace CascadeIDE.Features.Workspace.DataAcquisition;
5
6/// <summary>
7/// Дерево обозревателя для режима «папка как workspace» (без .sln): каталоги и файлы с отсечением по глубине/числу узлов.
8/// </summary>
9[IoBoundary]
10public static class FolderWorkspaceTreeBuilder
11{
12 private const int MaxDepth = 14;
13 private const int MaxNodes = 8000;
14
15 private static readonly HashSet<string> ExcludedDirectoryNames = new(StringComparer.OrdinalIgnoreCase)
16 {
17 ".git", "bin", "obj", "node_modules", ".vs", "packages", ".idea", "__pycache__",
18 ".venv", "venv", "dist", "build", ".turbo", ".next"
19 };
20
21 /// <summary>
22 /// Корень с <see cref="SolutionItem.FullPath"/> = нормализованный каталог; дочерние папки — через <see cref="SolutionItem.CreateFolder"/> (без пути).
23 /// </summary>
24 public static SolutionItem? TryBuild(string folderPath, out string? error)
25 {
26 error = null;
27 folderPath = folderPath?.Trim() ?? "";
28 if (folderPath.Length == 0)
29 {
30 error = "Путь пустой.";
31 return null;
32 }
33
34 if (Uri.TryCreate(folderPath, UriKind.Absolute, out var uri) && uri.IsFile)
35 folderPath = uri.LocalPath;
36
37 string normalized;
38 try
39 {
40 normalized = CanonicalFilePath.Normalize(folderPath);
41 }
42 catch (Exception ex)
43 {
44 error = "Путь: " + ex.Message;
45 return null;
46 }
47
48 if (!Directory.Exists(normalized))
49 {
50 error = "Каталог не найден: " + normalized;
51 return null;
52 }
53
54 var title = Path.GetFileName(normalized.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
55 if (string.IsNullOrEmpty(title))
56 title = normalized;
57
58 var root = SolutionItem.CreateFolderWorkspaceRoot(title, normalized);
59 var repoRoot = WorkspaceIgnoreMatcher.ResolveRepositoryRoot(normalized);
60 var ignore = WorkspaceIgnoreMatcher.GetOrCreate(repoRoot);
61 var count = 0;
62 AddChildren(root, normalized, MaxDepth, ref count, ignore);
63 ProjectFileTreeBuilder.SortSolutionItemChildren(root, StringComparer.OrdinalIgnoreCase);
64 return root;
65 }
66
67 private static void AddChildren(SolutionItem parent, string dir, int depthRemaining, ref int nodeCount, WorkspaceIgnoreMatcher ignore)
68 {
69 if (depthRemaining <= 0 || nodeCount >= MaxNodes)
70 return;
71
72 List<string> dirs = [];
73 List<string> files = [];
74 try
75 {
76 foreach (var entry in Directory.EnumerateFileSystemEntries(dir))
77 {
78 if (nodeCount >= MaxNodes)
79 break;
80 var name = Path.GetFileName(entry);
81 if (name.Length == 0)
82 continue;
83 if (Directory.Exists(entry))
84 {
85 if (ExcludedDirectoryNames.Contains(name))
86 continue;
87 if (ignore.IsIgnored(entry))
88 continue;
89 dirs.Add(entry);
90 }
91 else if (File.Exists(entry))
92 {
93 if (ignore.IsIgnored(entry))
94 continue;
95 files.Add(entry);
96 }
97 }
98 }
99 catch
100 {
101 return;
102 }
103
104 dirs.Sort(StringComparer.OrdinalIgnoreCase);
105 files.Sort(StringComparer.OrdinalIgnoreCase);
106
107 foreach (var d in dirs)
108 {
109 if (nodeCount >= MaxNodes)
110 break;
111 var folderNode = SolutionItem.CreateFolder(Path.GetFileName(d));
112 parent.Children.Add(folderNode);
113 nodeCount++;
114 AddChildren(folderNode, d, depthRemaining - 1, ref nodeCount, ignore);
115 }
116
117 foreach (var f in files)
118 {
119 if (nodeCount >= MaxNodes)
120 break;
121 parent.Children.Add(SolutionItem.CreateFile(Path.GetFileName(f), f));
122 nodeCount++;
123 }
124 }
125}
126
View only · write via MCP/CIDE