Forge
csharpdeeb25a2
1using System.Collections.Concurrent;
2using System.Diagnostics;
3using System.Runtime.CompilerServices;
4using Microsoft.CodeAnalysis;
5using Microsoft.CodeAnalysis.CSharp;
6using Microsoft.CodeAnalysis.Text;
7
8namespace CascadeIDE.Services;
9
10/// <summary>
11/// Сервис языка C#: автодополнение, подсказки по параметрам, Quick Info, подсветка вхождений.
12/// Реализация разнесена по partial в каталоге <c>Services/CSharp/</c>. Работает в фоне, с кэшем.
13/// </summary>
14public sealed partial class CSharpLanguageService
15{
16 private static readonly MetadataReference[] DefaultReferences = BuildDefaultReferences();
17
18 private const int CacheMaxEntries = 128;
19 private const int TextHashCacheMaxEntries = 16;
20
21 private readonly ConcurrentDictionary<(string path, int textHash), (CSharpCompilation comp, SyntaxTree tree)> _modelCache = new();
22 private readonly ConcurrentDictionary<(string path, int textHash, int line, int col), IReadOnlyList<CompletionItem>> _completionCache = new();
23 private readonly ConcurrentDictionary<(string path, int textHash, int line, int col), string?> _signatureCache = new();
24 private readonly ConcurrentDictionary<(string path, int textHash, int line, int col), IReadOnlyList<TextSpan>> _highlightCache = new();
25 private readonly ConcurrentDictionary<(string path, int textHash, int line, int col), string?> _quickInfoCache = new();
26 private readonly ConcurrentDictionary<(string path, int textHash), IReadOnlyList<EditorTrailingInlayPart>> _inlayHintCache = new();
27 private readonly LinkedList<(string path, int textHash)> _modelCacheOrder = new();
28 private readonly object _modelCacheLock = new();
29
30 private static int GetStableHash(SourceText text)
31 {
32 var s = text.ToString();
33 unchecked
34 {
35 int h = 0;
36 foreach (var c in s)
37 h = (h * 31) + c;
38 return h;
39 }
40 }
41
42 private static MetadataReference[] BuildDefaultReferences()
43 {
44 var refs = new List<MetadataReference>(64);
45 var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
46 void AddRef(string? path)
47 {
48 if (string.IsNullOrWhiteSpace(path))
49 return;
50 if (!File.Exists(path))
51 return;
52 if (!seen.Add(path))
53 return;
54 refs.Add(MetadataReference.CreateFromFile(path));
55 }
56
57 try
58 {
59 // Core references (works even if TPA list is unavailable).
60 AddRef(typeof(object).Assembly.Location);
61 AddRef(typeof(Enumerable).Assembly.Location);
62 AddRef(typeof(RuntimeHelpers).Assembly.Location);
63 AddRef(typeof(Exception).Assembly.Location);
64 AddRef(typeof(FileNotFoundException).Assembly.Location);
65 AddRef(typeof(Process).Assembly.Location);
66
67 // Richer semantic model for BCL constructor/parameter inlays (e.g., message:).
68 if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa && !string.IsNullOrWhiteSpace(tpa))
69 {
70 foreach (var path in tpa.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
71 {
72 var name = Path.GetFileNameWithoutExtension(path);
73 if (name is null)
74 continue;
75 if (name.StartsWith("System.", StringComparison.OrdinalIgnoreCase) ||
76 name.StartsWith("Microsoft.", StringComparison.OrdinalIgnoreCase) ||
77 string.Equals(name, "mscorlib", StringComparison.OrdinalIgnoreCase) ||
78 string.Equals(name, "netstandard", StringComparison.OrdinalIgnoreCase))
79 {
80 AddRef(path);
81 }
82 }
83 }
84 }
85 catch
86 {
87 // Минимальный набор при ошибке
88 }
89 return refs.ToArray();
90 }
91
92 private static MetadataReference[] GetDefaultReferences() => DefaultReferences;
93
94 private static SyntaxTree GetGlobalUsingsTreeForFile(string filePath) =>
95 CSharpProjectGlobalUsingsResolver.ResolveGlobalUsingsTree(filePath);
96
97 private (CSharpCompilation comp, SyntaxTree tree) GetOrCreateCompilationAndTree(string filePath, SourceText sourceText, CancellationToken ct)
98 {
99 var textHash = GetStableHash(sourceText);
100 if (_modelCache.TryGetValue((filePath, textHash), out var cached))
101 return cached;
102
103 lock (_modelCacheLock)
104 {
105 if (_modelCache.TryGetValue((filePath, textHash), out cached))
106 return cached;
107
108 while (_modelCache.Count >= TextHashCacheMaxEntries && _modelCacheOrder.Count > 0)
109 {
110 var oldest = _modelCacheOrder.First!.Value;
111 _modelCacheOrder.RemoveFirst();
112 _modelCache.TryRemove(oldest, out _);
113 }
114
115 var tree = CSharpSyntaxTree.ParseText(sourceText, path: filePath, cancellationToken: ct);
116 var globalUsingsTree = GetGlobalUsingsTreeForFile(filePath);
117 var compilation = CSharpCompilation.Create(
118 "Temp",
119 [globalUsingsTree, tree],
120 GetDefaultReferences(),
121 new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
122 var entry = (compilation, tree);
123 _modelCache[(filePath, textHash)] = entry;
124 _modelCacheOrder.AddLast((filePath, textHash));
125 return entry;
126 }
127 }
128
129 private SemanticModel GetOrCreateModel(string filePath, SourceText sourceText, CancellationToken ct)
130 {
131 var (comp, tree) = GetOrCreateCompilationAndTree(filePath, sourceText, ct);
132 return comp.GetSemanticModel(tree, ignoreAccessibility: true);
133 }
134
135 private static void TrimCaches<T>(ConcurrentDictionary<(string, int, int, int), T> cache)
136 {
137 if (cache.Count <= CacheMaxEntries) return;
138 var keys = cache.Keys.ToList();
139 foreach (var k in keys.Take(keys.Count - CacheMaxEntries))
140 cache.TryRemove(k, out _);
141 }
142
143 private void TrimInlayCache()
144 {
145 if (_inlayHintCache.Count <= 128) return;
146 var keys = _inlayHintCache.Keys.ToList();
147 foreach (var k in keys.Take(keys.Count - 64))
148 _inlayHintCache.TryRemove(k, out _);
149 }
150
151 /// <summary>Элемент автодополнения.</summary>
152 public enum CSharpCompletionKind
153 {
154 Keyword,
155 Method,
156 Property,
157 Field,
158 Event,
159 EnumMember,
160 Enum,
161 Class,
162 Interface,
163 Struct,
164 Delegate,
165 Variable,
166 Other,
167 }
168
169 public sealed record CompletionItem(
170 string DisplayText,
171 string InsertText,
172 string? Description = null,
173 CSharpCompletionKind Kind = CSharpCompletionKind.Other);
174
175 /// <summary>Сбросить кэш при смене решения/файла (опционально).</summary>
176 public void InvalidateCache()
177 {
178 _modelCache.Clear();
179 _completionCache.Clear();
180 _signatureCache.Clear();
181 _highlightCache.Clear();
182 _quickInfoCache.Clear();
183 _inlayHintCache.Clear();
184 CSharpProjectGlobalUsingsResolver.ClearCache();
185 lock (_modelCacheLock)
186 {
187 _modelCacheOrder.Clear();
188 }
189 }
190}
191
View only · write via MCP/CIDE