Forge
csharpdeeb25a2
1using Microsoft.CodeAnalysis;
2using Microsoft.CodeAnalysis.CSharp;
3using Microsoft.CodeAnalysis.CSharp.Syntax;
4using Microsoft.CodeAnalysis.Text;
5
6namespace CascadeIDE.Services;
7
8public sealed partial class CSharpLanguageService
9{
10 /// <summary>Возвращает предложения автодополнения в позиции (1-based line, column). Выполнять в фоне.</summary>
11 public IReadOnlyList<CompletionItem> GetCompletionItems(string filePath, string sourceText, int line, int column, CancellationToken ct = default)
12 {
13 if (string.IsNullOrEmpty(filePath) || line < 1 || column < 1)
14 return [];
15
16 var text = SourceText.From(sourceText);
17 var textHash = GetStableHash(text);
18 var cacheKey = (filePath, textHash, line, column);
19 if (_completionCache.TryGetValue(cacheKey, out var cached))
20 return cached;
21
22 try
23 {
24 var model = GetOrCreateModel(filePath, text, ct);
25 var lines = text.Lines;
26 if (line > lines.Count)
27 return [];
28
29 var lineInfo = lines[line - 1];
30 var colIndex = column - 1;
31 var position = lineInfo.Start + Math.Min(Math.Max(0, colIndex), lineInfo.Span.Length);
32
33 var root = model.SyntaxTree.GetRoot(ct);
34 var token = root.FindToken(position, findInsideTrivia: true);
35 var prefix = GetCompletionPrefix(token, position);
36
37 List<CompletionItem> list;
38 if (TryIsNamespaceDeclarationNamePosition(root, model.SyntaxTree, position))
39 {
40 list = CollectNamespaceNameCompletions(model, root, position, prefix, ct);
41 }
42 else if (TryGetMemberAccessExpression(root, position, out var memberTarget))
43 {
44 list = CollectMemberCompletions(model, memberTarget, position, prefix, ct);
45 }
46 else
47 {
48 list = CollectScopeCompletions(model, position, prefix, ct);
49 }
50
51 TrimCaches(_completionCache);
52 _completionCache[cacheKey] = list;
53 return list;
54 }
55 catch
56 {
57 return [];
58 }
59 }
60
61 private static string GetCompletionPrefix(SyntaxToken token, int position)
62 {
63 if (token.IsKind(SyntaxKind.IdentifierToken) && token.Span.Contains(position))
64 return token.Text[..Math.Min(token.Text.Length, Math.Max(0, position - token.Span.Start))];
65 return "";
66 }
67
68 private static bool MatchesPrefix(string name, string prefix) =>
69 CSharpCompletionMatcher.Matches(name, prefix);
70
71 private static int CompareCompletionNames(string nameA, string nameB, string prefix) =>
72 CSharpCompletionMatcher.CompareByRelevance(nameA, nameB, prefix);
73
74 private static bool TryGetMemberAccessExpression(SyntaxNode root, int position, out ExpressionSyntax expression)
75 {
76 expression = null!;
77 var token = root.FindToken(position, findInsideTrivia: true);
78 if (token.IsKind(SyntaxKind.DotToken) && token.Parent is MemberAccessExpressionSyntax dotAccess)
79 {
80 expression = dotAccess.Expression;
81 return true;
82 }
83
84 var node = root.FindNode(TextSpan.FromBounds(position, position), getInnermostNodeForTie: true);
85 while (node is not null)
86 {
87 if (node is MemberAccessExpressionSyntax memberAccess
88 && memberAccess.OperatorToken.IsKind(SyntaxKind.DotToken)
89 && memberAccess.OperatorToken.Span.End <= position)
90 {
91 expression = memberAccess.Expression;
92 return true;
93 }
94
95 node = node.Parent;
96 }
97
98 return false;
99 }
100
101 private static bool TryIsNamespaceDeclarationNamePosition(SyntaxNode root, SyntaxTree tree, int position)
102 {
103 var node = root.FindNode(TextSpan.FromBounds(position, position), getInnermostNodeForTie: true);
104 for (var current = node; current is not null; current = current.Parent)
105 {
106 if (current is not BaseNamespaceDeclarationSyntax nsDecl)
107 continue;
108
109 if (nsDecl is NamespaceDeclarationSyntax block
110 && position >= block.OpenBraceToken.SpanStart)
111 return false;
112
113 return position >= nsDecl.NamespaceKeyword.Span.End;
114 }
115
116 return IsIncompleteNamespaceDeclarationLine(tree, position);
117 }
118
119 private static bool IsIncompleteNamespaceDeclarationLine(SyntaxTree tree, int position)
120 {
121 var line = tree.GetText().Lines.GetLineFromPosition(position);
122 var offset = position - line.Start;
123 if (offset < 0 || offset > line.Span.Length)
124 return false;
125
126 var beforeCursor = line.ToString()[..offset].TrimStart();
127 if (!beforeCursor.StartsWith("namespace", StringComparison.Ordinal))
128 return false;
129
130 if (beforeCursor.Length == "namespace".Length)
131 return true;
132
133 if (beforeCursor.Length <= "namespace".Length || beforeCursor["namespace".Length] != ' ')
134 return false;
135
136 var namePart = beforeCursor["namespace".Length..].TrimStart();
137 var terminator = namePart.IndexOfAny(['{', ';']);
138 if (terminator >= 0)
139 namePart = namePart[..terminator].TrimEnd();
140
141 if (namePart.Length == 0)
142 return true;
143
144 foreach (var ch in namePart)
145 {
146 if (ch != '.' && ch != '_' && !char.IsLetterOrDigit(ch))
147 return false;
148 }
149
150 return true;
151 }
152
153 private static List<CompletionItem> CollectNamespaceNameCompletions(
154 SemanticModel model,
155 SyntaxNode root,
156 int position,
157 string prefix,
158 CancellationToken ct)
159 {
160 var list = new List<CompletionItem>();
161 var seen = new HashSet<string>(StringComparer.Ordinal);
162 var searchRoot = ResolveNamespaceContainer(model, root, position, ct);
163
164 foreach (var child in searchRoot.GetNamespaceMembers())
165 {
166 if (!MatchesPrefix(child.Name, prefix) || !seen.Add(child.Name))
167 continue;
168
169 list.Add(new CompletionItem(
170 child.Name,
171 child.Name,
172 child.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
173 CSharpCompletionKind.Other));
174 }
175
176 list.Sort((a, b) => CompareCompletionNames(a.DisplayText, b.DisplayText, prefix));
177 return list;
178 }
179
180 private static INamespaceSymbol ResolveNamespaceContainer(
181 SemanticModel model,
182 SyntaxNode root,
183 int position,
184 CancellationToken ct)
185 {
186 var token = root.FindToken(position, findInsideTrivia: true);
187 if (token.IsKind(SyntaxKind.DotToken) && token.Parent is QualifiedNameSyntax dotParent)
188 return ResolveNamespaceFromQualifiedLeft(model, dotParent.Left, position, ct)
189 ?? model.Compilation.GlobalNamespace;
190
191 var node = root.FindNode(TextSpan.FromBounds(position, position), getInnermostNodeForTie: true);
192 for (var current = node; current is not null; current = current.Parent)
193 {
194 if (current is QualifiedNameSyntax qualified
195 && position > qualified.DotToken.Span.End)
196 {
197 return ResolveNamespaceFromQualifiedLeft(model, qualified.Left, position, ct)
198 ?? model.Compilation.GlobalNamespace;
199 }
200 }
201
202 return model.Compilation.GlobalNamespace;
203 }
204
205 private static INamespaceSymbol? ResolveNamespaceFromQualifiedLeft(
206 SemanticModel model,
207 NameSyntax left,
208 int position,
209 CancellationToken ct)
210 {
211 if (model.GetSymbolInfo(left, ct).Symbol is INamespaceSymbol ns)
212 return ns;
213
214 if (left is IdentifierNameSyntax id)
215 {
216 foreach (var symbol in model.LookupSymbols(position, name: id.Identifier.Text))
217 {
218 if (symbol is INamespaceSymbol lookupNs)
219 return lookupNs;
220 }
221 }
222
223 return null;
224 }
225
226 private static List<CompletionItem> CollectMemberCompletions(
227 SemanticModel model,
228 ExpressionSyntax targetExpression,
229 int position,
230 string prefix,
231 CancellationToken ct)
232 {
233 var list = new List<CompletionItem>();
234 var typeInfo = model.GetTypeInfo(targetExpression, ct);
235 var type = typeInfo.Type ?? typeInfo.ConvertedType;
236 if (type is null || type.TypeKind == TypeKind.Error)
237 return list;
238
239 var seen = new HashSet<string>(StringComparer.Ordinal);
240 foreach (var member in type.GetMembers().Where(m => m.CanBeReferencedByName && !m.IsImplicitlyDeclared))
241 {
242 if (!model.IsAccessible(position, member))
243 continue;
244 if (!seen.Add(member.Name))
245 continue;
246 if (!MatchesPrefix(member.Name, prefix))
247 continue;
248
249 var item = CreateCompletionItem(member);
250 if (item is not null)
251 list.Add(item);
252 }
253
254 list.Sort((a, b) => CompareCompletionNames(a.DisplayText, b.DisplayText, prefix));
255 return list;
256 }
257
258 private static List<CompletionItem> CollectScopeCompletions(
259 SemanticModel model,
260 int position,
261 string prefix,
262 CancellationToken ct)
263 {
264 var list = new List<CompletionItem>();
265 var seen = new HashSet<string>(StringComparer.Ordinal);
266
267 foreach (var symbol in model.LookupSymbols(position, name: null, includeReducedExtensionMethods: true))
268 {
269 if (!IsScopeCompletionSymbol(symbol))
270 continue;
271 if (!seen.Add(symbol.Name) || !MatchesPrefix(symbol.Name, prefix))
272 continue;
273
274 var item = CreateCompletionItem(symbol);
275 if (item is not null)
276 list.Add(item);
277 }
278
279 foreach (var typeSymbol in model.LookupNamespacesAndTypes(position, name: null))
280 {
281 if (!seen.Add(typeSymbol.Name) || !MatchesPrefix(typeSymbol.Name, prefix))
282 continue;
283
284 var item = CreateCompletionItem(typeSymbol);
285 if (item is not null)
286 list.Add(item);
287 }
288
289 foreach (var keyword in CSharpCompletionKeywords.All)
290 {
291 if (!MatchesPrefix(keyword, prefix) || !seen.Add(keyword))
292 continue;
293 list.Add(new CompletionItem(keyword, keyword, "keyword", CSharpCompletionKind.Keyword));
294 }
295
296 list.Sort((a, b) => CompareCompletionNames(a.DisplayText, b.DisplayText, prefix));
297 return list;
298 }
299
300 private static CompletionItem? CreateCompletionItem(ISymbol member)
301 {
302 switch (member)
303 {
304 case IMethodSymbol method when method.MethodKind is MethodKind.Ordinary or MethodKind.LocalFunction:
305 {
306 var insert = method.Parameters.Length > 0 ? $"{method.Name}(" : method.Name;
307 return new CompletionItem(method.Name, insert, method.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), CSharpCompletionKind.Method);
308 }
309 case IPropertySymbol property:
310 return new CompletionItem(property.Name, property.Name, property.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), CSharpCompletionKind.Property);
311 case IFieldSymbol field when field.ContainingType?.TypeKind == TypeKind.Enum:
312 return new CompletionItem(field.Name, field.Name, field.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), CSharpCompletionKind.EnumMember);
313 case IFieldSymbol field:
314 return new CompletionItem(field.Name, field.Name, field.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), CSharpCompletionKind.Field);
315 case IEventSymbol ev:
316 return new CompletionItem(ev.Name, ev.Name, ev.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), CSharpCompletionKind.Event);
317 case INamedTypeSymbol typeSymbol:
318 return new CompletionItem(typeSymbol.Name, typeSymbol.Name, typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), MapTypeKind(typeSymbol.TypeKind));
319 case ILocalSymbol local:
320 return new CompletionItem(local.Name, local.Name, local.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), CSharpCompletionKind.Variable);
321 case IParameterSymbol parameter:
322 return new CompletionItem(parameter.Name, parameter.Name, parameter.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), CSharpCompletionKind.Variable);
323 case IRangeVariableSymbol range:
324 return new CompletionItem(range.Name, range.Name, range.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), CSharpCompletionKind.Variable);
325 default:
326 return null;
327 }
328 }
329
330 private static bool IsScopeCompletionSymbol(ISymbol symbol) =>
331 symbol switch
332 {
333 ILocalSymbol or IParameterSymbol or IRangeVariableSymbol => true,
334 IFieldSymbol or IPropertySymbol or IEventSymbol => true,
335 IMethodSymbol { MethodKind: MethodKind.AnonymousFunction or MethodKind.LocalFunction } => true,
336 _ => false,
337 };
338
339 private static CSharpCompletionKind MapTypeKind(TypeKind kind) =>
340 kind switch
341 {
342 TypeKind.Class => CSharpCompletionKind.Class,
343 TypeKind.Interface => CSharpCompletionKind.Interface,
344 TypeKind.Struct => CSharpCompletionKind.Struct,
345 TypeKind.Delegate => CSharpCompletionKind.Delegate,
346 TypeKind.Enum => CSharpCompletionKind.Enum,
347 _ => CSharpCompletionKind.Other,
348 };
349}
350
View only · write via MCP/CIDE