Forge
csharpdeeb25a2
1#nullable enable
2
3using System.Collections.Concurrent;
4using System.Text;
5using Microsoft.CodeAnalysis;
6using Microsoft.CodeAnalysis.CSharp;
7using Microsoft.CodeAnalysis.CSharp.Syntax;
8using Microsoft.CodeAnalysis.Text;
9
10namespace CascadeIDE.Features.Chat;
11
12/// <summary>Члены типов в .cs для оси <c>M:</c> bracket-autocomplete.</summary>
13public static class BracketMemberCompletionProvider
14{
15 public sealed record Match(string Name, string Help);
16
17 private sealed record FileIndexCache(long MtimeUtcTicks, IReadOnlyList<Match> Members);
18
19 private static readonly ConcurrentDictionary<string, FileIndexCache> IndexByAbsolutePath =
20 new(StringComparer.OrdinalIgnoreCase);
21
22 /// <summary>Проактивно построить индекс членов для файла (ADR 0141 P0).</summary>
23 public static void WarmIndex(string? filePath, string? workspaceRoot)
24 {
25 if (string.IsNullOrWhiteSpace(filePath))
26 return;
27
28 if (!TryResolveAbsolute(filePath, workspaceRoot, out var absolute))
29 return;
30
31 if (!absolute.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
32 return;
33
34 long mtimeTicks;
35 try
36 {
37 mtimeTicks = File.GetLastWriteTimeUtc(absolute).Ticks;
38 }
39 catch
40 {
41 return;
42 }
43
44 _ = loadOrBuildIndex(absolute, mtimeTicks);
45 }
46
47 public static IReadOnlyList<Match> GetMatches(
48 string? filePath,
49 string? workspaceRoot,
50 string namePrefix,
51 int limit)
52 {
53 if (limit <= 0 || string.IsNullOrWhiteSpace(filePath))
54 return [];
55
56 if (!TryResolveAbsolute(filePath, workspaceRoot, out var absolute))
57 return [];
58
59 if (!absolute.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
60 return [];
61
62 long mtimeTicks;
63 try
64 {
65 mtimeTicks = File.GetLastWriteTimeUtc(absolute).Ticks;
66 }
67 catch
68 {
69 return [];
70 }
71
72 var all = loadOrBuildIndex(absolute, mtimeTicks);
73 var prefix = namePrefix.Trim();
74
75 IEnumerable<Match> ranked = all;
76 if (prefix.Length > 0)
77 ranked = ranked.Where(m => m.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
78
79 return ranked
80 .OrderBy(m => m.Name, StringComparer.OrdinalIgnoreCase)
81 .Take(limit)
82 .ToList();
83 }
84
85 private static IReadOnlyList<Match> loadOrBuildIndex(string absolute, long mtimeTicks)
86 {
87 if (IndexByAbsolutePath.TryGetValue(absolute, out var cached) && cached.MtimeUtcTicks == mtimeTicks)
88 return cached.Members;
89
90 var built = buildIndex(absolute);
91 IndexByAbsolutePath[absolute] = new FileIndexCache(mtimeTicks, built);
92 return built;
93 }
94
95 private static IReadOnlyList<Match> buildIndex(string absolute)
96 {
97 string text;
98 try
99 {
100 text = File.ReadAllText(absolute);
101 }
102 catch
103 {
104 return [];
105 }
106
107 var tree = CSharpSyntaxTree.ParseText(SourceText.From(text, Encoding.UTF8), path: absolute);
108 var root = tree.GetCompilationUnitRoot();
109 if (root is null)
110 return [];
111
112 var names = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
113 foreach (var node in root.DescendantNodes())
114 {
115 switch (node)
116 {
117 case MethodDeclarationSyntax m:
118 add(names, m.Identifier.Text, "method");
119 break;
120 case PropertyDeclarationSyntax p:
121 add(names, p.Identifier.Text, "property");
122 break;
123 case ConstructorDeclarationSyntax c:
124 add(names, c.Identifier.Text, "ctor");
125 break;
126 case IndexerDeclarationSyntax:
127 add(names, "this", "indexer");
128 break;
129 case EventDeclarationSyntax ev:
130 add(names, ev.Identifier.Text, "event");
131 break;
132 case FieldDeclarationSyntax f:
133 foreach (var v in f.Declaration.Variables)
134 add(names, v.Identifier.Text, "field");
135 break;
136 case ClassDeclarationSyntax:
137 case StructDeclarationSyntax:
138 case InterfaceDeclarationSyntax:
139 case RecordDeclarationSyntax:
140 var typeName = node switch
141 {
142 ClassDeclarationSyntax c => c.Identifier.Text,
143 StructDeclarationSyntax s => s.Identifier.Text,
144 InterfaceDeclarationSyntax i => i.Identifier.Text,
145 RecordDeclarationSyntax rec => rec.Identifier.Text,
146 _ => "",
147 };
148 if (!string.IsNullOrWhiteSpace(typeName))
149 add(names, typeName, "type");
150 break;
151 }
152 }
153
154 return names
155 .OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase)
156 .Select(p => new Match(p.Key, p.Value))
157 .ToList();
158 }
159
160 private static void add(Dictionary<string, string> names, string name, string kind)
161 {
162 if (string.IsNullOrWhiteSpace(name))
163 return;
164 names.TryAdd(name, kind);
165 }
166
167 private static bool TryResolveAbsolute(string filePath, string? workspaceRoot, out string absolute)
168 {
169 absolute = filePath.Trim();
170 if (Path.IsPathRooted(absolute))
171 return File.Exists(absolute);
172
173 if (string.IsNullOrWhiteSpace(workspaceRoot))
174 return false;
175
176 var combined = Path.Combine(workspaceRoot.Trim(), absolute.Replace('/', Path.DirectorySeparatorChar));
177 absolute = Path.GetFullPath(combined);
178 return File.Exists(absolute);
179 }
180}
181
View only · write via MCP/CIDE