Forge
csharpdeeb25a2
1#nullable enable
2using System.Text.Json;
3using CascadeIDE.Cockpit.Graph;
4using CascadeIDE.Contracts;
5using CascadeIDE.Features.Documents;
6using CascadeIDE.Features.IdeMcp.Application;
7using CascadeIDE.Models;
8using CascadeIDE.Services;
9using CascadeIDE.Services.CodeNavigation;
10
11namespace CascadeIDE.Features.WorkspaceNavigation.Application;
12
13/// <summary>
14/// Application-level helpers for workspace navigation map refresh.
15/// Keeps status/row shaping and caret position normalization out of MainWindowViewModel.
16/// </summary>
17[ApplicationOrchestrator]
18public static class WorkspaceNavigationMapOrchestrator
19{
20 public sealed record RelatedRow(string FullPath, string RelativePath, string Kind, string Rationale);
21
22 public static (int line, int column) ComputeLineColumn(string? text, int? offset)
23 {
24 var source = text ?? string.Empty;
25 var pos = Math.Clamp(offset ?? 0, 0, source.Length);
26 var line = 1;
27 var col = 1;
28 for (var i = 0; i < pos; i++)
29 {
30 if (source[i] == '\n')
31 {
32 line++;
33 col = 1;
34 }
35 else
36 {
37 col++;
38 }
39 }
40
41 return (line, col);
42 }
43
44 /// <summary>
45 /// CF refresh: только позиция каретки в текущем открытом <c>.cs</c>; без подстановки «первого метода» файла.
46 /// </summary>
47 public static (int? line, int? column) ResolveControlFlowCursorForRefresh(
48 string? navigationPath,
49 string? currentPath,
50 string? sourceText,
51 int? caretOrSelectionOffset,
52 int? navigateToLine = null,
53 int? navigateToColumn = null)
54 {
55 if (string.IsNullOrWhiteSpace(currentPath)
56 || !currentPath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
57 return (null, null);
58
59 if (!EditorTextCoordinateUtilities.PathsReferToSameFile(navigationPath ?? "", currentPath))
60 return (null, null);
61
62 if (string.IsNullOrEmpty(sourceText))
63 return (null, null);
64
65 return IdeMcpNavigationOrchestrator.ResolveControlFlowLineColumn(
66 navigateToLine,
67 navigateToColumn,
68 sourceText,
69 caretOrSelectionOffset);
70 }
71
72 /// <summary>1-based line → offset в <paramref name="text"/> (для якоря клика по узлу CF).</summary>
73 public static int? TryOffsetForLine(string? text, int lineOneBased, int columnOneBased = 1)
74 {
75 if (string.IsNullOrEmpty(text) || lineOneBased < 1)
76 return null;
77
78 var line = 1;
79 var i = 0;
80 while (line < lineOneBased && i < text.Length)
81 {
82 if (text[i++] == '\n')
83 line++;
84 }
85
86 if (line != lineOneBased)
87 return null;
88
89 var col = Math.Max(1, columnOneBased) - 1;
90 while (col > 0 && i < text.Length && text[i] != '\n')
91 {
92 i++;
93 col--;
94 }
95
96 return Math.Clamp(i, 0, text.Length);
97 }
98
99 /// <summary>
100 /// Путь для JSON подграфа карты: CF — всегда активный <c>.cs</c> редактора, если он задан (без замены на Program.cs /
101 /// «первый открытый» из дерева решения); иначе эвристический якорь.
102 /// </summary>
103 public static string? ResolveNavigationPathForGraphJson(
104 string normalizedLevel,
105 string? currentPath,
106 string? anchorPath,
107 IReadOnlyList<string> _)
108 {
109 var fallback = anchorPath ?? currentPath;
110 if (!string.Equals(normalizedLevel, CodeNavigationMapLevelKind.ControlFlow, StringComparison.Ordinal))
111 return fallback;
112
113 if (string.IsNullOrWhiteSpace(currentPath)
114 || !currentPath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
115 return fallback;
116
117 return SolutionTreePath.TryGetFullPath(currentPath, out var full) ? full : currentPath;
118 }
119
120 public static string ResolveErrorStatus(JsonElement root, string? currentPath)
121 {
122 if (!root.TryGetProperty("error", out var errEl))
123 return string.Empty;
124
125 var code = errEl.GetString() ?? string.Empty;
126 var msg = root.TryGetProperty("message", out var mEl) ? mEl.GetString() ?? string.Empty : string.Empty;
127 if (code == "no_file" && string.IsNullOrEmpty(currentPath))
128 return "Откройте файл из дерева решения — здесь появятся связанные.";
129 if (code == "no_control_flow_scope")
130 return string.IsNullOrEmpty(msg)
131 ? "Поставьте курсор в метод или top-level оператор."
132 : msg;
133 return string.IsNullOrEmpty(msg) ? code : msg;
134 }
135
136 public static List<RelatedRow> BuildRowsFromSubgraph(GraphDocument subgraph, string? solutionPath)
137 {
138 var rows = new List<RelatedRow>();
139 foreach (var n in subgraph.Nodes)
140 {
141 if (string.Equals(n.Kind, "anchor", StringComparison.OrdinalIgnoreCase))
142 continue;
143
144 var rel = string.IsNullOrEmpty(n.RelativePath)
145 ? McpSolutionTree.GetRelativePath(solutionPath, n.Path)
146 : n.RelativePath!;
147
148 rows.Add(new RelatedRow(
149 n.Path,
150 rel ?? n.Path,
151 n.Kind,
152 n.Rationale ?? string.Empty));
153 }
154
155 return rows;
156 }
157
158 public static string ResolveAnchorLabelFromSubgraph(GraphDocument subgraph) =>
159 string.IsNullOrEmpty(subgraph.AnchorPath)
160 ? "—"
161 : Path.GetFileName(subgraph.AnchorPath);
162
163 public static string ResolveAnchorLabelFromRelatedRoot(JsonElement root)
164 {
165 if (root.TryGetProperty("anchor_path", out var ap) && ap.ValueKind == JsonValueKind.String)
166 {
167 var apStr = ap.GetString();
168 if (!string.IsNullOrEmpty(apStr))
169 return Path.GetFileName(apStr);
170 }
171
172 return "—";
173 }
174
175 public static List<RelatedRow> BuildRowsFromRelatedRoot(JsonElement root)
176 {
177 var rows = new List<RelatedRow>();
178 if (!root.TryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array)
179 return rows;
180
181 foreach (var el in items.EnumerateArray())
182 {
183 var fp = el.TryGetProperty("path", out var pEl) ? pEl.GetString() ?? string.Empty : string.Empty;
184 if (string.IsNullOrEmpty(fp))
185 continue;
186
187 var rel = el.TryGetProperty("relative_path", out var rEl) ? rEl.GetString() ?? string.Empty : string.Empty;
188 var kind = el.TryGetProperty("kind", out var kEl) ? kEl.GetString() ?? string.Empty : string.Empty;
189 var rationale = el.TryGetProperty("rationale", out var raEl) ? raEl.GetString() ?? string.Empty : string.Empty;
190 rows.Add(new RelatedRow(
191 fp,
192 string.IsNullOrEmpty(rel) ? fp : rel,
193 kind,
194 rationale));
195 }
196
197 return rows;
198 }
199
200 public static string ResolveEmptyStatus(IReadOnlyCollection<RelatedRow> rows, string currentStatus, bool wantList) =>
201 rows.Count == 0 && string.IsNullOrEmpty(currentStatus) && wantList
202 ? "Нет связанных файлов по текущим эвристикам."
203 : currentStatus;
204}
205
View only · write via MCP/CIDE