Forge
csharp4405de34
1#nullable enable
2
3using System.Text;
4using System.Text.Json;
5using CascadeIDE.Models.Intercom;
6using Microsoft.CodeAnalysis;
7using Microsoft.CodeAnalysis.CSharp;
8using Microsoft.CodeAnalysis.CSharp.Syntax;
9using Microsoft.CodeAnalysis.Text;
10
11namespace CascadeIDE.Services.Intercom;
12
13/// <summary>H0b: innermost syntax scope @ caret → <see cref="AttachmentAnchor"/> (ADR 0128).</summary>
14public static class AttachmentAnchorCaretScopeResolver
15{
16 public static bool TryResolveAtCaret(
17 string? filePath,
18 string? editorText,
19 int? caretOffset,
20 string? workspaceRoot,
21 out AttachmentAnchor anchor,
22 out string error)
23 {
24 anchor = new AttachmentAnchor();
25 error = "";
26
27 if (string.IsNullOrWhiteSpace(filePath) || !filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
28 {
29 error = "Attach scope доступен для .cs в активном редакторе.";
30 return false;
31 }
32
33 var text = editorText ?? "";
34 var offset = Math.Clamp(caretOffset ?? 0, 0, text.Length);
35 if (!AttachmentAnchorPaths.TryResolveAbsolute(
36 AttachmentAnchorPaths.ToWorkspaceRelative(filePath, workspaceRoot) ?? filePath,
37 workspaceRoot,
38 out var absolute,
39 out var pathErr))
40 {
41 error = pathErr;
42 return false;
43 }
44
45 if (!string.Equals(absolute, filePath, StringComparison.OrdinalIgnoreCase)
46 && File.Exists(filePath))
47 {
48 absolute = filePath;
49 }
50
51 if (!File.Exists(absolute))
52 {
53 error = "Файл не найден на диске.";
54 return false;
55 }
56
57 string diskText;
58 try
59 {
60 diskText = File.ReadAllText(absolute);
61 }
62 catch (Exception ex)
63 {
64 error = "Не удалось прочитать файл: " + ex.Message;
65 return false;
66 }
67
68 var tree = CSharpSyntaxTree.ParseText(SourceText.From(diskText, Encoding.UTF8), path: absolute);
69 var root = tree.GetCompilationUnitRoot();
70 var pos = Math.Clamp(offset, 0, Math.Max(0, diskText.Length - 1));
71 var node = root.FindToken(pos).Parent;
72 if (node is null)
73 {
74 error = "Не найден синтаксический узел @ caret.";
75 return false;
76 }
77
78 var scopeNode = findInnermostScope(node);
79 if (scopeNode is null)
80 {
81 error = "Под кареткой нет for/if/while/switch — используй /attach selection.";
82 return false;
83 }
84
85 var memberNode = scopeNode.Ancestors().OfType<MemberDeclarationSyntax>().FirstOrDefault();
86 var memberName = memberNode switch
87 {
88 MethodDeclarationSyntax m => m.Identifier.Text,
89 PropertyDeclarationSyntax p => p.Identifier.Text,
90 ConstructorDeclarationSyntax c => c.Identifier.Text,
91 _ => null,
92 };
93
94 var kind = scopeKindName(scopeNode);
95 if (kind is null)
96 {
97 error = "Неподдерживаемый синтаксический узел.";
98 return false;
99 }
100
101 var memberBody = memberNode?.ChildNodes().FirstOrDefault(n => n is BlockSyntax or ArrowExpressionClauseSyntax);
102 var searchRoot = memberBody ?? root;
103 var matches = collectScopeNodes(searchRoot, kind);
104 var index = matches.FindIndex(n => n == scopeNode) + 1;
105 if (index < 1)
106 index = 1;
107
108 var rel = AttachmentAnchorPaths.ToWorkspaceRelative(absolute, workspaceRoot) ?? absolute.Replace('\\', '/');
109 var syntaxScope = JsonSerializer.SerializeToElement(new Dictionary<string, object?>
110 {
111 ["kind"] = kind,
112 ["indexInParent"] = index,
113 ["parentMemberKey"] = memberName,
114 });
115
116 var label = memberName is not null
117 ? $"{memberName} › {kind} ({index})"
118 : $"{Path.GetFileName(rel)} › {kind} ({index})";
119
120 anchor = new AttachmentAnchor
121 {
122 AttachmentShape = "syntax-scope",
123 File = rel.Replace('\\', '/'),
124 MemberKey = memberName,
125 SyntaxScope = syntaxScope,
126 DisplayLabel = label,
127 };
128 return true;
129 }
130
131 private static SyntaxNode? findInnermostScope(SyntaxNode node)
132 {
133 for (var cur = node; cur is not null; cur = cur.Parent)
134 {
135 if (matchesScopeKind(cur, "for")
136 || matchesScopeKind(cur, "if")
137 || matchesScopeKind(cur, "while")
138 || matchesScopeKind(cur, "switch")
139 || matchesScopeKind(cur, "foreach")
140 || matchesScopeKind(cur, "try")
141 || matchesScopeKind(cur, "lock")
142 || matchesScopeKind(cur, "using"))
143 {
144 return cur;
145 }
146 }
147
148 return null;
149 }
150
151 private static string? scopeKindName(SyntaxNode node) =>
152 node switch
153 {
154 ForStatementSyntax => "for",
155 ForEachStatementSyntax => "foreach",
156 IfStatementSyntax => "if",
157 WhileStatementSyntax => "while",
158 SwitchStatementSyntax => "switch",
159 TryStatementSyntax => "try",
160 LockStatementSyntax => "lock",
161 UsingStatementSyntax => "using",
162 _ => null,
163 };
164
165 private static List<SyntaxNode> collectScopeNodes(SyntaxNode root, string kind)
166 {
167 var list = new List<SyntaxNode>();
168 foreach (var node in root.DescendantNodes())
169 {
170 if (matchesScopeKind(node, kind))
171 list.Add(node);
172 }
173
174 return list;
175 }
176
177 private static bool matchesScopeKind(SyntaxNode node, string kind) =>
178 kind switch
179 {
180 "for" => node is ForStatementSyntax,
181 "foreach" => node is ForEachStatementSyntax,
182 "if" => node is IfStatementSyntax,
183 "while" => node is WhileStatementSyntax,
184 "switch" => node is SwitchStatementSyntax,
185 "try" => node is TryStatementSyntax,
186 "lock" => node is LockStatementSyntax,
187 "using" => node is UsingStatementSyntax,
188 _ => false,
189 };
190}
191
View only · write via MCP/CIDE