Forge
csharp551a3611
1using Microsoft.CodeAnalysis;
2using Microsoft.CodeAnalysis.CSharp;
3using Microsoft.CodeAnalysis.CSharp.Syntax;
4using Microsoft.CodeAnalysis.MSBuild;
5using Microsoft.CodeAnalysis.Text;
6
7namespace RoslynMcp.ServiceLayer;
8
9/// <summary>Символ в позиции (строка, столбец). Без solution — по синтаксису; с solution_or_project_path — семантика и квалифицированное имя. 1-based.</summary>
10public static class SymbolAtPosition
11{
12 /// <summary>Семантический вариант: загружает solution, возвращает kind, name, location и Qualified (ToDisplayString).</summary>
13 public static async Task<string> GetSymbolAtPositionAsync(
14 string filePath,
15 int line,
16 int column,
17 string? solutionOrProjectPath,
18 CancellationToken cancellationToken = default)
19 {
20 if (!string.IsNullOrWhiteSpace(solutionOrProjectPath) && File.Exists(solutionOrProjectPath))
21 {
22 var result = await GetSymbolWithSemanticAsync(filePath, line, column, solutionOrProjectPath!.Trim(), cancellationToken).ConfigureAwait(false);
23 if (result is not null)
24 return result;
25 }
26 return GetSymbolAtPosition(filePath, line, column, cancellationToken);
27 }
28
29 private static async Task<string?> GetSymbolWithSemanticAsync(string filePath, int line, int column, string solutionOrProjectPath, CancellationToken ct)
30 {
31 var targetPath = NormalizePath(filePath);
32 Solution? solution = null;
33 try
34 {
35 var workspace = MSBuildWorkspace.Create(RoslynMcpWorkspaceProperties.MsBuild);
36 solution = await WorkspaceOpen.OpenSolutionOrProjectAsync(workspace, solutionOrProjectPath, ct).ConfigureAwait(false);
37 if (solution is null) return null;
38
39 var document = solution.Projects
40 .SelectMany(p => p.Documents)
41 .FirstOrDefault(d => string.Equals(NormalizePath(d.FilePath ?? ""), targetPath, StringComparison.OrdinalIgnoreCase));
42 if (document is null) return null;
43
44 var root = await document.GetSyntaxRootAsync(ct).ConfigureAwait(false);
45 var semanticModel = await document.GetSemanticModelAsync(ct).ConfigureAwait(false);
46 if (root is null || semanticModel is null) return null;
47
48 var sourceText = await document.GetTextAsync(ct).ConfigureAwait(false);
49 var lines = sourceText.Lines;
50 if (line < 1 || line > lines.Count) return null;
51 var lineInfo = lines[line - 1];
52 var columnIndex = column - 1;
53 if (columnIndex < 0) return null;
54 var lineLen = lineInfo.Span.Length;
55 var position = lineLen == 0 ? lineInfo.Start : lineInfo.Start + Math.Min(columnIndex, lineLen);
56
57 var node = root.FindToken(position, findInsideTrivia: true).Parent;
58 ISymbol? symbol = null;
59 while (node != null)
60 {
61 ct.ThrowIfCancellationRequested();
62 symbol = semanticModel.GetDeclaredSymbol(node, ct) ?? semanticModel.GetSymbolInfo(node, ct).Symbol;
63 if (symbol != null) break;
64 node = node.Parent;
65 }
66 if (symbol is null) return null;
67
68 var kind = symbol.Kind.ToString().ToLowerInvariant();
69 var name = symbol.Name;
70 var format = new SymbolDisplayFormat(
71 typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
72 memberOptions: SymbolDisplayMemberOptions.IncludeContainingType);
73 var qualified = symbol.ToDisplayString(format);
74 var docPath = document.FilePath ?? filePath;
75 return $"# Document: {docPath} (total_lines={lines.Count}, line_{line}_length={lineLen})\n{kind}\t{name}\t{filePath}:{line}:{column}\tQualified: {qualified}";
76 }
77 catch (InvalidOperationException) { return null; }
78 finally { solution?.Workspace.Dispose(); }
79 }
80
81 private static string NormalizePath(string path)
82 {
83 var p = Path.GetFullPath(path.Trim());
84 if (p.EndsWith(Path.DirectorySeparatorChar)) p = p.TrimEnd(Path.DirectorySeparatorChar);
85 return p;
86 }
87
88 public static string GetSymbolAtPosition(string filePath, int line, int column, CancellationToken cancellationToken = default)
89 {
90 if (!File.Exists(filePath))
91 return $"Error: file not found: {filePath}";
92
93 var sourceText = SourceText.From(File.ReadAllText(filePath));
94 var tree = CSharpSyntaxTree.ParseText(sourceText, cancellationToken: cancellationToken);
95 var root = tree.GetRoot(cancellationToken);
96 var lines = sourceText.Lines;
97
98 if (line < 1 || line > lines.Count)
99 return $"Error: line {line} out of range (1..{lines.Count}).";
100 var lineInfo = lines[line - 1];
101 var columnIndex = column - 1;
102 if (columnIndex < 0)
103 return $"Error: column {column} must be >= 1.";
104 var lineLen = lineInfo.Span.Length;
105 var position = lineLen == 0 ? lineInfo.Start : lineInfo.Start + Math.Min(columnIndex, lineLen);
106
107 var token = root.FindToken(position, findInsideTrivia: true);
108 var node = token.Parent;
109 while (node != null)
110 {
111 cancellationToken.ThrowIfCancellationRequested();
112 var (kind, name) = GetKindAndName(node);
113 if (kind != null && name != null)
114 return $"{kind}\t{name}\t{filePath}:{line}:{column}";
115 node = node.Parent;
116 }
117
118 return $"No symbol at {filePath}:{line}:{column} (token: {token.Kind()}).";
119 }
120
121 private static (string? kind, string? name) GetKindAndName(SyntaxNode node) => node switch
122 {
123 NamespaceDeclarationSyntax n => ("namespace", n.Name.ToString()),
124 FileScopedNamespaceDeclarationSyntax n => ("namespace", n.Name.ToString()),
125 ClassDeclarationSyntax n => ("class", n.Identifier.Text),
126 StructDeclarationSyntax n => ("struct", n.Identifier.Text),
127 InterfaceDeclarationSyntax n => ("interface", n.Identifier.Text),
128 RecordDeclarationSyntax n => ("record", n.Identifier.Text),
129 MethodDeclarationSyntax n => ("method", n.Identifier.Text),
130 ConstructorDeclarationSyntax n => ("constructor", n.Identifier.Text),
131 PropertyDeclarationSyntax n => ("property", n.Identifier.Text),
132 FieldDeclarationSyntax n => ("field", n.Declaration.Variables.FirstOrDefault()?.Identifier.Text ?? ""),
133 EventDeclarationSyntax n => ("event", n.Identifier.Text),
134 EnumDeclarationSyntax n => ("enum", n.Identifier.Text),
135 DelegateDeclarationSyntax n => ("delegate", n.Identifier.Text),
136 VariableDeclaratorSyntax n => ("local", n.Identifier.Text),
137 ParameterSyntax n => ("parameter", n.Identifier.Text),
138 TypeParameterSyntax n => ("type_parameter", n.Identifier.Text),
139 _ => (null, null)
140 };
141}
142
View only · write via MCP/CIDE