Forge
csharpdeeb25a2
1using System.Collections.Concurrent;
2using System.Text;
3using Microsoft.CodeAnalysis;
4using Microsoft.CodeAnalysis.CSharp;
5
6namespace CascadeIDE.Services;
7
8/// <summary>
9/// Resolves global usings for ad-hoc Roslyn compilations: MSBuild <c>GlobalUsings.g.cs</c>,
10/// project <c>GlobalUsings.cs</c>, then SDK implicit + csproj <c>Using</c> items.
11/// </summary>
12public static class CSharpProjectGlobalUsingsResolver
13{
14 private sealed record CacheEntry(long ProjectTicks, long GeneratedTicks, SyntaxTree Tree);
15
16 private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new(StringComparer.OrdinalIgnoreCase);
17
18 public static SyntaxTree ResolveGlobalUsingsTree(string sourceFilePath)
19 {
20 var projectPath = FindOwningProjectFile(sourceFilePath);
21 if (projectPath is null)
22 return CreateTree(CSharpSdkImplicitUsingsCatalog.ForProjectSdk("Microsoft.NET.Sdk", implicitUsingsEnabled: true), "__cascade_global_usings__.g.cs");
23
24 var projectTicks = File.GetLastWriteTimeUtc(projectPath).Ticks;
25 var generatedPath = TryFindGeneratedGlobalUsingsPath(projectPath);
26 var generatedTicks = generatedPath is not null && File.Exists(generatedPath)
27 ? File.GetLastWriteTimeUtc(generatedPath).Ticks
28 : 0L;
29
30 if (Cache.TryGetValue(projectPath, out var cached)
31 && cached.ProjectTicks == projectTicks
32 && cached.GeneratedTicks == generatedTicks)
33 {
34 return cached.Tree;
35 }
36
37 var tree = BuildTree(projectPath, generatedPath);
38 Cache[projectPath] = new CacheEntry(projectTicks, generatedTicks, tree);
39 return tree;
40 }
41
42 public static void ClearCache() => Cache.Clear();
43
44 internal static string? FindOwningProjectFile(string sourceFilePath)
45 {
46 var dir = Path.GetDirectoryName(sourceFilePath);
47 while (!string.IsNullOrWhiteSpace(dir))
48 {
49 try
50 {
51 if (Directory.Exists(dir))
52 {
53 var projects = Directory.EnumerateFiles(dir, "*.csproj", SearchOption.TopDirectoryOnly).ToList();
54 if (projects.Count == 1)
55 return projects[0];
56 if (projects.Count > 1)
57 {
58 var fileName = Path.GetFileNameWithoutExtension(sourceFilePath);
59 var match = projects.FirstOrDefault(p =>
60 string.Equals(Path.GetFileNameWithoutExtension(p), fileName, StringComparison.OrdinalIgnoreCase));
61 if (match is not null)
62 return match;
63 return projects.OrderBy(static p => p.Length).First();
64 }
65 }
66
67 dir = Path.GetDirectoryName(dir);
68 }
69 catch
70 {
71 dir = Path.GetDirectoryName(dir);
72 }
73 }
74
75 return null;
76 }
77
78 internal static string? TryFindGeneratedGlobalUsingsPath(string projectPath)
79 {
80 var projectDir = Path.GetDirectoryName(projectPath);
81 if (string.IsNullOrWhiteSpace(projectDir))
82 return null;
83
84 var objDir = Path.Combine(projectDir, "obj");
85 if (!Directory.Exists(objDir))
86 return null;
87
88 try
89 {
90 return Directory.EnumerateFiles(objDir, "GlobalUsings.g.cs", SearchOption.AllDirectories)
91 .Select(path => new FileInfo(path))
92 .OrderByDescending(info => info.LastWriteTimeUtc)
93 .Select(info => info.FullName)
94 .FirstOrDefault();
95 }
96 catch
97 {
98 return null;
99 }
100 }
101
102 private static SyntaxTree BuildTree(string projectPath, string? generatedPath)
103 {
104 if (!string.IsNullOrWhiteSpace(generatedPath) && File.Exists(generatedPath))
105 {
106 try
107 {
108 return CSharpSyntaxTree.ParseText(File.ReadAllText(generatedPath), path: generatedPath);
109 }
110 catch
111 {
112 // Fall through to synthesis.
113 }
114 }
115
116 var projectDir = Path.GetDirectoryName(projectPath)!;
117 var authoredPath = Path.Combine(projectDir, "GlobalUsings.cs");
118 if (File.Exists(authoredPath))
119 {
120 try
121 {
122 return CSharpSyntaxTree.ParseText(File.ReadAllText(authoredPath), path: authoredPath);
123 }
124 catch
125 {
126 // Fall through to synthesis.
127 }
128 }
129
130 var profile = CSharpProjectUsingsProfile.TryLoad(projectPath);
131 if (profile is null)
132 return CreateTree([], "__cascade_global_usings__.g.cs");
133
134 var merged = profile.ResolveNamespaces();
135 var virtualPath = Path.Combine(projectDir, "obj", "__cascade_synthesized_global_usings__.g.cs");
136 return CreateTree(merged, virtualPath);
137 }
138
139 private static SyntaxTree CreateTree(IReadOnlyList<string> namespaces, string path)
140 {
141 var sb = new StringBuilder();
142 sb.AppendLine("// <auto-generated by CascadeIDE — SDK/csproj global usings />");
143 foreach (var ns in namespaces)
144 sb.AppendLine($"global using {ns};");
145
146 return CSharpSyntaxTree.ParseText(sb.ToString(), path: path);
147 }
148}
149
View only · write via MCP/CIDE