Forge
csharp4405de34
1using Microsoft.CodeAnalysis;
2using Microsoft.CodeAnalysis.CSharp.Syntax;
3using Microsoft.CodeAnalysis.Operations;
4using Microsoft.CodeAnalysis.Text;
5using System.Collections.Immutable;
6
7namespace CascadeIDE.Services;
8
9public sealed partial class CSharpLanguageService
10{
11 private static readonly SymbolDisplayFormat InlayTypeDisplayFormat = new(
12 globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining,
13 typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
14 genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
15 miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
16
17 /// <summary>
18 /// Inlay-подсказки в буфер: <c>var → тип</c> (0085/0103), при необходимости имена аргументов
19 /// (<c> paramName: </c> у позиционных) и у индексаторов, как type/parameter hints в IDE.
20 /// Позиционные аргументы: не дублировать имя, если аргумент — простой идентификатор с тем же имени, что и параметр;
21 /// для <c>params</c> — одна подсказка на сгруппированные значения (не на каждый элемент).
22 /// Кэш по (path, text); вызывать с актуальным <paramref name="sourceText"/>.
23 /// </summary>
24 public IReadOnlyList<EditorTrailingInlayPart> GetVarInlayHintsForFile(string filePath, string sourceText, CancellationToken ct = default)
25 {
26 if (string.IsNullOrEmpty(filePath) || !filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
27 return [];
28 var text = SourceText.From(sourceText);
29 var textHash = GetStableHash(text);
30 var fileKey = (filePath, textHash);
31 if (_inlayHintCache.TryGetValue(fileKey, out var cached))
32 return cached;
33 try
34 {
35 var model = GetOrCreateModel(filePath, text, ct);
36 var root = model.SyntaxTree.GetRoot(ct);
37 var list = new List<EditorTrailingInlayPart>(32);
38 foreach (var node in root.DescendantNodes())
39 {
40 ct.ThrowIfCancellationRequested();
41 switch (node)
42 {
43 case LocalDeclarationStatementSyntax lds
44 when lds.Declaration is { Type: IdentifierNameSyntax { Identifier.Text: "var" } } && lds.Declaration.Variables.Count > 0:
45 {
46 var v0 = lds.Declaration.Variables[0];
47 if (model.GetDeclaredSymbol(v0, ct) is not ILocalSymbol local)
48 break;
49 var label = " " + local.Type.ToDisplayString(InlayTypeDisplayFormat);
50 var anchor = lds.Declaration.Type.Span.End;
51 list.Add(new EditorTrailingInlayPart(anchor, label));
52 break;
53 }
54 case ForEachStatementSyntax fes
55 when fes.Type is IdentifierNameSyntax { Identifier.Text: "var" }:
56 {
57 if (model.GetDeclaredSymbol(fes, ct) is not ILocalSymbol le)
58 break;
59 var label2 = " " + le.Type.ToDisplayString(InlayTypeDisplayFormat);
60 var anchor2 = fes.Type.Span.End;
61 list.Add(new EditorTrailingInlayPart(anchor2, label2));
62 break;
63 }
64 case InvocationExpressionSyntax inv:
65 AddParameterNameInlaysForParenthesizedList(model, inv.ArgumentList, list, ct);
66 break;
67 case ObjectCreationExpressionSyntax oce:
68 AddParameterNameInlaysForParenthesizedList(model, oce.ArgumentList, list, ct);
69 break;
70 case ImplicitObjectCreationExpressionSyntax ioc:
71 AddParameterNameInlaysForParenthesizedList(model, ioc.ArgumentList, list, ct);
72 break;
73 case ElementAccessExpressionSyntax eae:
74 AddParameterNameInlaysForBracketedList(model, eae.ArgumentList, list, ct);
75 break;
76 }
77 }
78 // Normalize hints per anchor: drop blank labels and keep the most informative one.
79 // This prevents first-seen empty placeholders from hiding useful labels like "args:"/"message:".
80 var bestByAnchor = new Dictionary<int, string>();
81 foreach (var p in list)
82 {
83 if (string.IsNullOrWhiteSpace(p.Label))
84 continue;
85 if (!bestByAnchor.TryGetValue(p.AnchorOffset, out var existing) || p.Label.Length > existing.Length)
86 bestByAnchor[p.AnchorOffset] = p.Label;
87 }
88 IReadOnlyList<EditorTrailingInlayPart> result = bestByAnchor
89 .OrderBy(static kv => kv.Key)
90 .Select(static kv => new EditorTrailingInlayPart(kv.Key, kv.Value))
91 .ToList();
92 if (InlayHintTrace.IsEnabled)
93 {
94 const int cap = 64;
95 static string OneLineLabel(string? s) =>
96 (s ?? "").ReplaceLineEndings(" ").Replace('\r', ' ').Replace('\n', ' ');
97 var preview = result.Count == 0
98 ? ""
99 : string.Join(" | ", result.Take(cap).Select(p => $"{p.AnchorOffset}→{OneLineLabel(p.Label)}"));
100 if (result.Count > cap)
101 preview += $" | ...+{result.Count - cap}";
102 InlayHintTrace.Log($"GetVarInlayHints count={result.Count} file={filePath} {preview}");
103 }
104 TrimInlayCache();
105 _inlayHintCache[fileKey] = result;
106 return result;
107 }
108 catch
109 {
110 return [];
111 }
112 }
113
114 private static void AddParameterNameInlaysForParenthesizedList(
115 SemanticModel model,
116 ArgumentListSyntax? list,
117 List<EditorTrailingInlayPart> outList,
118 CancellationToken ct)
119 {
120 if (list is null)
121 return;
122 AddParameterNameInlaysForArguments(model, list.Arguments, outList, ct);
123 }
124
125 private static void AddParameterNameInlaysForBracketedList(
126 SemanticModel model,
127 BracketedArgumentListSyntax? list,
128 List<EditorTrailingInlayPart> outList,
129 CancellationToken ct)
130 {
131 if (list is null)
132 return;
133 AddParameterNameInlaysForArguments(model, list.Arguments, outList, ct);
134 }
135
136 private static IArgumentOperation? TryGetArgumentOperation(SemanticModel model, ArgumentSyntax arg, CancellationToken ct)
137 {
138 IOperation? op;
139 try
140 {
141 op = model.GetOperation(arg, ct);
142 }
143 catch
144 {
145 return null;
146 }
147 while (op is IConversionOperation conv)
148 op = conv.Operand;
149 if (op is IArgumentOperation direct)
150 return direct;
151 // Иногда с аргументом связана операция над выражением (ILiteralOperation и т.д.) — поднимаемся по Parent.
152 var walk = op;
153 while (walk is not null && walk is not IArgumentOperation)
154 walk = walk.Parent;
155 if (walk is IArgumentOperation fromParent)
156 return fromParent;
157
158 // Roslyn иногда не возвращает IArgumentOperation с узла (внутренняя конверсия/значение);
159 // привязываемся к IInvocationOperation / IObjectCreationOperation по Syntax аргумента.
160 if (arg.Parent is not BaseArgumentListSyntax alist || alist.Parent is null)
161 return null;
162 IOperation? parentOp;
163 try
164 {
165 parentOp = model.GetOperation(alist.Parent, ct);
166 }
167 catch
168 {
169 return null;
170 }
171 if (parentOp is IInvocationOperation invOp)
172 {
173 foreach (var a in invOp.Arguments)
174 {
175 if (a.Syntax == arg)
176 return a;
177 }
178 // params: IInvocationOperation.Arguments часто == 1; IArgumentOperation.Syntax не равен каждому ArgumentSyntax. Любой синтаксический arg из того же списка относится к тому же params.
179 if (alist is ArgumentListSyntax al3)
180 {
181 foreach (var a in invOp.Arguments)
182 {
183 if (a.Parameter is not { IsParams: true })
184 continue;
185 foreach (var syn in al3.Arguments)
186 {
187 if (syn == arg)
188 return a;
189 }
190 }
191 }
192 }
193 if (parentOp is IObjectCreationOperation ocr)
194 {
195 foreach (var a in ocr.Arguments)
196 {
197 if (a.Syntax == arg)
198 return a;
199 }
200 }
201 return null;
202 }
203
204 private static ISymbol? GetBestSymbol(SymbolInfo info) =>
205 info.Symbol ?? info.CandidateSymbols.FirstOrDefault();
206
207 private static IParameterSymbol? TryResolveParameterBySymbolInfo(
208 SemanticModel model,
209 ArgumentSyntax arg,
210 CancellationToken ct)
211 {
212 if (arg.Parent is not BaseArgumentListSyntax list)
213 return null;
214
215 var index = list.Arguments.IndexOf(arg);
216 if (index < 0)
217 return null;
218
219 static IParameterSymbol? SelectParameter(ImmutableArray<IParameterSymbol> parameters, int argIndex)
220 {
221 if (parameters.IsDefaultOrEmpty)
222 return null;
223 var last = parameters.Length - 1;
224 if (parameters[last].IsParams && argIndex >= last)
225 return parameters[last];
226 return argIndex < parameters.Length ? parameters[argIndex] : null;
227 }
228
229 var parent = list.Parent;
230 ISymbol? symbol = parent switch
231 {
232 InvocationExpressionSyntax inv => GetBestSymbol(model.GetSymbolInfo(inv, ct)),
233 ObjectCreationExpressionSyntax oce => GetBestSymbol(model.GetSymbolInfo(oce, ct)),
234 ImplicitObjectCreationExpressionSyntax ioc => GetBestSymbol(model.GetSymbolInfo(ioc, ct)),
235 ElementAccessExpressionSyntax eae => GetBestSymbol(model.GetSymbolInfo(eae, ct)),
236 _ => null
237 };
238
239 var parameter = symbol switch
240 {
241 IMethodSymbol method => SelectParameter(method.Parameters, index),
242 IPropertySymbol { IsIndexer: true } property => SelectParameter(property.Parameters, index),
243 _ => null
244 };
245 if (parameter is not null)
246 return parameter;
247
248 // If constructor symbol isn't bound directly (e.g., reduced semantic model),
249 // resolve by created type and pick a matching ctor by argument count/optionality.
250 if (parent is ObjectCreationExpressionSyntax oce2 || parent is ImplicitObjectCreationExpressionSyntax)
251 {
252 INamedTypeSymbol? createdType = parent switch
253 {
254 ObjectCreationExpressionSyntax ocex => model.GetTypeInfo(ocex.Type, ct).Type as INamedTypeSymbol,
255 ImplicitObjectCreationExpressionSyntax iocx => (model.GetTypeInfo(iocx, ct).Type
256 ?? model.GetTypeInfo(iocx, ct).ConvertedType) as INamedTypeSymbol,
257 _ => null
258 };
259 if (createdType is null)
260 return null;
261 var argCount = list.Arguments.Count;
262 IMethodSymbol? bestCtor = null;
263 var bestScore = int.MinValue;
264 foreach (var ctor in createdType.InstanceConstructors)
265 {
266 if (ctor.Parameters.IsDefaultOrEmpty)
267 continue;
268
269 var parameters = ctor.Parameters;
270 var hasParams = parameters[^1].IsParams;
271 var required = parameters.Count(p => !p.IsOptional && !p.IsParams);
272 if (argCount < required)
273 continue;
274 if (!hasParams && argCount > parameters.Length)
275 continue;
276
277 // Prefer exact arity, then the closest non-params shape.
278 var score = parameters.Length == argCount ? 1000 : 0;
279 if (hasParams)
280 score -= 100;
281 score -= Math.Abs(parameters.Length - argCount);
282 if (score <= bestScore)
283 continue;
284 bestScore = score;
285 bestCtor = ctor;
286 }
287
288 return bestCtor is null ? null : SelectParameter(bestCtor.Parameters, index);
289 }
290
291 return null;
292 }
293
294 private static IParameterSymbol? TryGetArgumentParameter(
295 SemanticModel model,
296 ArgumentSyntax arg,
297 CancellationToken ct)
298 {
299 return TryGetArgumentOperation(model, arg, ct)?.Parameter
300 ?? TryResolveParameterBySymbolInfo(model, arg, ct);
301 }
302
303 private static void AddParameterNameInlaysForArguments(
304 SemanticModel model,
305 SeparatedSyntaxList<ArgumentSyntax> args,
306 List<EditorTrailingInlayPart> outList,
307 CancellationToken ct)
308 {
309 // For params, Roslyn gives one IParameterSymbol per element — show a single inlay (first arg), not "items: " on every value.
310 IParameterSymbol? paramsHintEmittedFor = null;
311 foreach (var arg in args)
312 {
313 ct.ThrowIfCancellationRequested();
314 if (arg.NameColon is not null)
315 continue;
316 var resolvedParameter = TryGetArgumentParameter(model, arg, ct);
317 if (resolvedParameter is not IParameterSymbol p)
318 continue;
319 if (string.IsNullOrEmpty(p.Name))
320 continue;
321 var parameterName = p.Name;
322 var isParams = p.IsParams;
323
324 if (isParams)
325 {
326 if (resolvedParameter is IParameterSymbol p2 &&
327 paramsHintEmittedFor is not null &&
328 SymbolEqualityComparer.Default.Equals(paramsHintEmittedFor, p2))
329 {
330 continue;
331 }
332 if (resolvedParameter is IParameterSymbol p3)
333 paramsHintEmittedFor = p3;
334 }
335 else
336 {
337 paramsHintEmittedFor = null;
338 }
339
340 // Drop redundant "args: " when the argument is just `args` (same as VS inlay policy).
341 if (arg.Expression is IdentifierNameSyntax idArg &&
342 string.Equals(idArg.Identifier.ValueText, parameterName, StringComparison.Ordinal))
343 continue;
344
345 var label = " " + parameterName + ": ";
346 outList.Add(new EditorTrailingInlayPart(arg.Expression.SpanStart, label));
347 }
348 }
349}
350
View only · write via MCP/CIDE