Forge
csharp4405de34
1#nullable enable
2
3using CascadeIDE.Models.Editor;
4using CascadeIDE.Services;
5using Microsoft.CodeAnalysis;
6using Microsoft.CodeAnalysis.CSharp;
7using Microsoft.CodeAnalysis.CSharp.Syntax;
8
9namespace CascadeIDE.Services.Intercom;
10
11/// <summary>Сбор symbol sidecar из синтаксического дерева (ADR 0135).</summary>
12public static class IntercomSymbolLineIndexBuilder
13{
14 public static IReadOnlyList<IntercomSymbolLineEntry> CollectFromFile(string absolutePath)
15 {
16 if (!absolutePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) || !File.Exists(absolutePath))
17 return [];
18
19 string text;
20 try
21 {
22 text = File.ReadAllText(absolutePath);
23 }
24 catch
25 {
26 return [];
27 }
28
29 return CollectFromText(text, absolutePath);
30 }
31
32 /// <summary>Собрать symbol entries из уже прочитанного текста (HCI reindex observer, ADR 0135).</summary>
33 public static IReadOnlyList<IntercomSymbolLineEntry> CollectFromText(string text, string absolutePath)
34 {
35 if (!absolutePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)
36 || string.IsNullOrEmpty(text))
37 {
38 return [];
39 }
40
41 var tree = CSharpSyntaxTree.ParseText(text, path: absolutePath);
42 if (tree.GetCompilationUnitRoot() is not { } root)
43 return [];
44
45 var compilation = buildCompilation(absolutePath, tree);
46 var model = compilation.GetSemanticModel(tree);
47 var entries = new List<IntercomSymbolLineEntry>();
48 var simpleCounts = new Dictionary<string, int>(StringComparer.Ordinal);
49
50 foreach (var candidate in root.DescendantNodes())
51 {
52 if (!tryGetDeclaredSymbol(candidate, model, out var sym) || sym is null)
53 continue;
54
55 if (!tryLineRangeFromNode(tree, candidate, out var start, out var end))
56 continue;
57
58 var docId = sym.GetDocumentationCommentId();
59 if (!string.IsNullOrWhiteSpace(docId))
60 {
61 entries.Add(new IntercomSymbolLineEntry("docid", docId, start, end));
62 }
63
64 var simple = sym.Name;
65 if (!string.IsNullOrWhiteSpace(simple))
66 simpleCounts[simple] = simpleCounts.GetValueOrDefault(simple) + 1;
67 }
68
69 foreach (var candidate in root.DescendantNodes())
70 {
71 if (!tryGetDeclaredSymbol(candidate, model, out var sym) || sym is null)
72 continue;
73
74 if (!tryLineRangeFromNode(tree, candidate, out var start, out var end))
75 continue;
76
77 var simple = sym.Name;
78 if (string.IsNullOrWhiteSpace(simple) || simpleCounts.GetValueOrDefault(simple) != 1)
79 continue;
80
81 entries.Add(new IntercomSymbolLineEntry("simple", simple, start, end));
82 }
83
84 return entries;
85 }
86
87 public static void IndexFile(in IntercomAttachResolveCacheContext cache, string absolutePath, string relativePath)
88 {
89 IndexFile(cache, absolutePath, relativePath, text: null, lastWriteUtcTicks: null);
90 }
91
92 public static void IndexFile(
93 in IntercomAttachResolveCacheContext cache,
94 string absolutePath,
95 string relativePath,
96 string? text,
97 long? lastWriteUtcTicks)
98 {
99 if (!File.Exists(absolutePath))
100 return;
101
102 long mtime;
103 try
104 {
105 mtime = lastWriteUtcTicks ?? File.GetLastWriteTimeUtc(absolutePath).Ticks;
106 }
107 catch
108 {
109 return;
110 }
111
112 var entries = text is null
113 ? CollectFromFile(absolutePath)
114 : CollectFromText(text, absolutePath);
115 IntercomSymbolLineIndex.ReplaceFileSymbols(cache, relativePath, mtime, entries);
116 }
117
118 private static bool tryGetDeclaredSymbol(SyntaxNode candidate, SemanticModel model, out ISymbol? symbol)
119 {
120 symbol = candidate switch
121 {
122 MethodDeclarationSyntax m => model.GetDeclaredSymbol(m),
123 PropertyDeclarationSyntax p => model.GetDeclaredSymbol(p),
124 FieldDeclarationSyntax f => model.GetDeclaredSymbol(f.Declaration.Variables.First()),
125 VariableDeclaratorSyntax v => model.GetDeclaredSymbol(v),
126 ConstructorDeclarationSyntax c => model.GetDeclaredSymbol(c),
127 IndexerDeclarationSyntax i => model.GetDeclaredSymbol(i),
128 EventDeclarationSyntax e => model.GetDeclaredSymbol(e),
129 ClassDeclarationSyntax cl => model.GetDeclaredSymbol(cl),
130 StructDeclarationSyntax st => model.GetDeclaredSymbol(st),
131 InterfaceDeclarationSyntax it => model.GetDeclaredSymbol(it),
132 RecordDeclarationSyntax r => model.GetDeclaredSymbol(r),
133 _ => null,
134 };
135 return symbol is not null;
136 }
137
138 private static bool tryLineRangeFromNode(SyntaxTree tree, SyntaxNode node, out int start, out int end)
139 {
140 start = 0;
141 end = 0;
142 var span = node.Span;
143 if (span.IsEmpty)
144 return false;
145
146 var startLine = RoslynLinePositionMapper.ToEditorLineNumber(tree.GetLineSpan(span).StartLinePosition);
147 var endSpan = span.Length > 0 ? new Microsoft.CodeAnalysis.Text.TextSpan(Math.Max(span.Start, span.End - 1), 1) : span;
148 var endLine = RoslynLinePositionMapper.ToEditorLineNumber(tree.GetLineSpan(endSpan).StartLinePosition);
149
150 start = startLine.Value;
151 end = endLine.Value;
152 return end >= start;
153 }
154
155 private static CSharpCompilation buildCompilation(string filePath, SyntaxTree tree)
156 {
157 var refs = new List<MetadataReference>();
158 void addRef(string? path)
159 {
160 if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
161 return;
162 refs.Add(MetadataReference.CreateFromFile(path));
163 }
164
165 addRef(typeof(object).Assembly.Location);
166 addRef(typeof(Enumerable).Assembly.Location);
167
168 return CSharpCompilation.Create(
169 "CascadeSymbolIndex_" + Guid.NewGuid().ToString("N"),
170 new[] { tree },
171 refs,
172 new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
173 }
174}
175
View only · write via MCP/CIDE