Forge
csharpdeeb25a2
1using Microsoft.CodeAnalysis;
2using Microsoft.CodeAnalysis.CSharp.Syntax;
3using Microsoft.CodeAnalysis.Text;
4
5namespace CascadeIDE.Services;
6
7public sealed partial class CSharpLanguageService
8{
9 public sealed record ReferenceLocation(string FilePath, int Line, int Column);
10
11 /// <summary>Find references in the current file (Roslyn fast path).</summary>
12 public IReadOnlyList<ReferenceLocation> FindReferencesInFile(
13 string filePath,
14 string sourceText,
15 int line,
16 int column,
17 CancellationToken ct = default)
18 {
19 if (string.IsNullOrEmpty(filePath) || line < 1 || column < 1)
20 return [];
21
22 try
23 {
24 var text = SourceText.From(sourceText);
25 var model = GetOrCreateModel(filePath, text, ct);
26 var lines = text.Lines;
27 if (line > lines.Count)
28 return [];
29
30 var lineInfo = lines[line - 1];
31 var position = lineInfo.Start + Math.Min(Math.Max(0, column - 1), lineInfo.Span.Length);
32 var root = model.SyntaxTree.GetRoot(ct);
33 var symbol = TryResolveSymbolAtPosition(model, root, position, ct);
34 if (symbol is null)
35 return [];
36
37 var seen = new HashSet<(int line, int col)>();
38 var list = new List<ReferenceLocation>();
39 foreach (var node in root.DescendantNodes())
40 {
41 if (!TryGetBoundSymbol(model, node, ct, out var nodeSymbol))
42 continue;
43 if (!SymbolEqualityComparer.Default.Equals(nodeSymbol, symbol))
44 continue;
45
46 var span = GetReferenceSpan(node);
47 if (span.Length == 0)
48 continue;
49
50 var pos = text.Lines.GetLinePosition(span.Start);
51 var key = (pos.Line, pos.Character);
52 if (!seen.Add(key))
53 continue;
54
55 list.Add(new ReferenceLocation(filePath, pos.Line + 1, pos.Character + 1));
56 }
57
58 list.Sort(static (a, b) =>
59 {
60 var lineCmp = a.Line.CompareTo(b.Line);
61 return lineCmp != 0 ? lineCmp : a.Column.CompareTo(b.Column);
62 });
63 return list;
64 }
65 catch
66 {
67 return [];
68 }
69 }
70
71 private static TextSpan GetReferenceSpan(SyntaxNode node) =>
72 node switch
73 {
74 IdentifierNameSyntax id => id.Identifier.Span,
75 GenericNameSyntax generic => generic.Identifier.Span,
76 QualifiedNameSyntax qualified => qualified.Right.Identifier.Span,
77 MemberAccessExpressionSyntax member => member.Name.Identifier.Span,
78 TypeDeclarationSyntax typeDecl => typeDecl.Identifier.Span,
79 EnumDeclarationSyntax enumDecl => enumDecl.Identifier.Span,
80 DelegateDeclarationSyntax delegateDecl => delegateDecl.Identifier.Span,
81 _ => default,
82 };
83
84 private static bool TryGetBoundSymbol(
85 SemanticModel model,
86 SyntaxNode node,
87 CancellationToken ct,
88 out ISymbol? symbol)
89 {
90 symbol = node switch
91 {
92 BaseTypeDeclarationSyntax or DelegateDeclarationSyntax
93 => model.GetDeclaredSymbol(node, ct),
94 _ => model.GetSymbolInfo(node, ct).Symbol ?? model.GetSymbolInfo(node, ct).CandidateSymbols.FirstOrDefault(),
95 };
96 return symbol is not null;
97 }
98
99 private static ISymbol? TryResolveSymbolAtPosition(
100 SemanticModel model,
101 SyntaxNode root,
102 int position,
103 CancellationToken ct)
104 {
105 var node = root.FindNode(TextSpan.FromBounds(position, position), getInnermostNodeForTie: true);
106 for (var current = node; current is not null; current = current.Parent)
107 {
108 if (TryGetBoundSymbol(model, current, ct, out var symbol))
109 return symbol;
110 }
111
112 return null;
113 }
114}
115
View only · write via MCP/CIDE