Forge
csharpdeeb25a2
1#nullable enable
2
3using Avalonia.Controls;
4using CascadeIDE.Cockpit.Cds;
5using CascadeIDE.Cockpit.Composition.TraceFlow;
6using CascadeIDE.Cockpit.Graph;
7using CascadeIDE.Features.Editor;
8using CascadeIDE.Features.HybridIndex.Application;
9using CascadeIDE.Features.Workspace;
10using CascadeIDE.Features.Workspace.DataAcquisition;
11using CascadeIDE.Features.WorkspaceNavigation.Application;
12using CascadeIDE.Models;
13using CascadeIDE.Services;
14using CascadeIDE.ViewModels;
15
16namespace CascadeIDE.Features.WorkspaceNavigation.Presentation;
17
18/// <summary>Срез карты workspace: перезапрос refresh и сборка через <see cref="WorkspaceNavigationMapRefreshComposer"/>.</summary>
19public sealed partial class WorkspaceNavigationMapViewModel
20{
21 internal void ScheduleWorkspaceNavigationMapRefresh()
22 {
23 _workspaceNavigationMapRefreshCts?.Cancel();
24 var cts = new CancellationTokenSource();
25 _workspaceNavigationMapRefreshCts = cts;
26 _ = RunWorkspaceNavigationMapRefreshAsync(cts.Token);
27 }
28
29 /// <summary>Вызывается из <c>WorkspaceNavigationMapView</c> при изменении ширины мини-карты.</summary>
30 internal void NotifyCodeNavigationMapGraphViewportWidthChanged(double width)
31 {
32 if (!CodeNavigationMapViewportPolicy.ShouldApplyMeasuredWidth(width, CodeNavigationMapGraphWidth, out var clamped))
33 return;
34
35 CodeNavigationMapGraphWidth = clamped;
36 ScheduleWorkspaceNavigationMapRefresh();
37 }
38
39 private async Task RunWorkspaceNavigationMapRefreshAsync(CancellationToken ct)
40 {
41 try
42 {
43 await Task.Delay(280, ct).ConfigureAwait(false);
44 }
45 catch
46 {
47 return;
48 }
49
50 List<string> rawPaths = [];
51 List<string> openDocumentPaths = [];
52 string? currentPath = null;
53 string? anchorPath = null;
54 string? solutionPath = null;
55 string? editorText = null;
56 int? cursorLine = null;
57 int? cursorColumn = null;
58 int? caretOffset = null;
59 string? cfGraphNavigatePath = null;
60 int? cfGraphNavigateLine = null;
61 int? cfGraphNavigateColumn = null;
62 CodeNavigationSettings? navSettings = null;
63 string controlFlowGrain = CodeNavigationMapControlFlowGrainKind.Intent;
64 var wantList = false;
65 var wantGraph = false;
66 var level = CodeNavigationMapLevelKind.File;
67 CockpitSurfaceState? cockpitSurfaceCapturedOnUi = null;
68 await UiScheduler.Default.InvokeAsync(() =>
69 {
70 rawPaths = McpSolutionTree.CollectFileEntries(_host.Workspace.SolutionRoots).Select(e => e.FullPath).ToList();
71 openDocumentPaths = _host.Documents.OpenDocuments
72 .Select(d => d.FilePath)
73 .Where(fp => !string.IsNullOrEmpty(fp))
74 .Select(fp => fp!)
75 .ToList();
76 currentPath = _host.CurrentFilePath;
77 var curOrder = currentPath ?? "";
78 openDocumentPaths.Sort((a, b) =>
79 {
80 bool Match(string d) =>
81 !string.IsNullOrEmpty(curOrder) && EditorTextCoordinateUtilities.PathsReferToSameFile(d, curOrder);
82 var am = Match(a);
83 var bm = Match(b);
84 if (am == bm)
85 return 0;
86 return am ? -1 : 1;
87 });
88 anchorPath = WorkspaceNavigationMapAnchorResolver.Resolve(currentPath, openDocumentPaths, rawPaths);
89 solutionPath = _host.Workspace.SolutionPath;
90 editorText = _host.EditorText;
91 caretOffset = _editorCaretOffset
92 ?? _host.EditorSelectionStart;
93 if (string.Equals(currentPath, _host.CurrentFilePath, StringComparison.OrdinalIgnoreCase)
94 && !string.IsNullOrEmpty(_host.EditorText))
95 {
96 editorText = _host.EditorText;
97 }
98 navSettings = _host.Settings.CodeNavigation;
99 var sm = _host.Settings.CodeNavigationMap;
100 wantList = sm.WantsCodeNavigationMapList;
101 wantGraph = sm.WantsCodeNavigationMapGraph;
102 level = CodeNavigationMapLevelKind.Normalize(CodeNavigationMapLevel);
103 controlFlowGrain = CodeNavigationMapControlFlowGrainKind.Normalize(sm.ControlFlowGrain);
104 if (level == CodeNavigationMapLevelKind.ControlFlow)
105 {
106 cockpitSurfaceCapturedOnUi = CockpitSurfaceSnapshotBuilder.Build(_host.Shell);
107 cfGraphNavigatePath = _controlFlowGraphNavigatePath;
108 cfGraphNavigateLine = _controlFlowGraphNavigateLine;
109 cfGraphNavigateColumn = _controlFlowGraphNavigateColumn;
110 }
111 });
112
113 var navigationPath = WorkspaceNavigationMapOrchestrator.ResolveNavigationPathForGraphJson(
114 level,
115 currentPath,
116 anchorPath,
117 rawPaths);
118
119 if (!string.IsNullOrEmpty(navigationPath)
120 && !EditorTextCoordinateUtilities.PathsReferToSameFile(navigationPath, currentPath ?? ""))
121 {
122 try
123 {
124 if (WorkspaceTextFileReader.TryReadAllText(navigationPath, out var navText))
125 editorText = navText;
126 }
127 catch
128 {
129 // keep editor text as-is
130 }
131 }
132
133 if (level == CodeNavigationMapLevelKind.ControlFlow)
134 {
135 int? navigateLine = null;
136 int? navigateColumn = null;
137 if (cfGraphNavigateLine is > 0
138 && EditorTextCoordinateUtilities.PathsReferToSameFile(cfGraphNavigatePath, navigationPath))
139 {
140 navigateLine = cfGraphNavigateLine;
141 navigateColumn = cfGraphNavigateColumn ?? 1;
142 }
143
144 (cursorLine, cursorColumn) = WorkspaceNavigationMapOrchestrator.ResolveControlFlowCursorForRefresh(
145 navigationPath,
146 currentPath,
147 editorText,
148 caretOffset,
149 navigateLine,
150 navigateColumn);
151 }
152
153 if (ct.IsCancellationRequested)
154 return;
155
156 var useSubgraphMode = level == CodeNavigationMapLevelKind.ControlFlow || wantGraph;
157
158 string json;
159 try
160 {
161 json = await Task.Run(
162 () => _codeNavigationMapGraphDataSource.BuildNavigationJson(
163 new GraphNavigationJsonRequest(
164 level,
165 wantGraph,
166 navigationPath,
167 editorText,
168 cursorLine,
169 cursorColumn,
170 rawPaths,
171 solutionPath,
172 navSettings,
173 controlFlowGrain)),
174 ct)
175 .ConfigureAwait(false);
176 }
177 catch (OperationCanceledException)
178 {
179 return;
180 }
181 catch (Exception ex)
182 {
183 await UiScheduler.Default.InvokeAsync(() =>
184 {
185 if (ct.IsCancellationRequested)
186 return;
187 WorkspaceNavigationMapStatus = $"Не удалось построить контекст карты: {ex.Message}";
188 CodeNavigationMapGraphScene = null;
189 WorkspaceNavigationMapItems.Clear();
190 WorkspaceNavigationMapRelatedCount = 0;
191 });
192 return;
193 }
194
195 if (ct.IsCancellationRequested)
196 return;
197
198 var deps = WorkspaceNavigationMapGraphComposition.CreateRefreshDependencies(
199 _traceFlowChannelCoordinator,
200 _traceFlowCdsRouter,
201 _traceFlowSurfaceCompositor);
202 var graphWidth = SanitizeMapViewportDimension(CodeNavigationMapGraphWidth, CodeNavigationMapCompositor.DefaultWidth);
203 var graphHeight = SanitizeMapViewportDimension(CodeNavigationMapGraphHeight, CodeNavigationMapCompositor.DefaultHeightFile);
204
205 var dry = WorkspaceNavigationMapRefreshComposer.Compose(
206 deps,
207 json,
208 useSubgraphMode,
209 wantList,
210 navigationPath,
211 solutionPath,
212 level,
213 graphWidth,
214 graphHeight,
215 _host.Settings.CodeNavigationMap.NormalizedDetailLevel,
216 _host.Settings.CodeNavigationMap.NormalizedRelatedGraphLayout,
217 _host.Settings.CodeNavigationMap.NormalizedControlFlowMainAxis,
218 _host.Settings.CodeNavigationMap,
219 new WorkspaceNavigationMapRefreshComposer.TraceSignals(_host.ImpactedTestsBadge, _host.LastTestSummary),
220 cockpitSurfaceCapturedOnUi);
221
222 var workspaceRoot = _host.GetWorkspacePath();
223 SemanticMapHciOrientationSnapshot? hciSnap = null;
224 if (!ct.IsCancellationRequested)
225 {
226 try
227 {
228 hciSnap = await SemanticMapHciOrientationAcquirer.TryAcquireAsync(
229 _host.HybridIndex,
230 _host.Settings.HybridIndex,
231 workspaceRoot,
232 solutionPath,
233 navigationPath,
234 ct)
235 .ConfigureAwait(false);
236 }
237 catch (OperationCanceledException)
238 {
239 // refresh superseded
240 }
241 catch
242 {
243 hciSnap = new SemanticMapHciOrientationSnapshot([], "", "запрос HCI не выполнен");
244 }
245 }
246
247 var hciLine = SemanticMapHciOrientationFormatting.ToStatusLine(hciSnap);
248 var (featureLine, featureDocPaths, docsCoverageLine, adrLine, adrFirstDocPath, adrDocPaths) =
249 TryResolveWorkspaceCorrespondence(workspaceRoot, navigationPath);
250
251 await UiScheduler.Default.InvokeAsync(() =>
252 {
253 try
254 {
255 if (ct.IsCancellationRequested)
256 return;
257 WorkspaceNavigationMapAnchorLabel = dry.AnchorLabel;
258 WorkspaceNavigationMapStatus = dry.Status;
259 WorkspaceNavigationMapRelatedCount = dry.AccentCount;
260 CodeNavigationMapGraphScene = dry.Scene;
261 CodeNavigationMapGraphHeight = dry.GraphHeight;
262 WorkspaceNavigationMapCfAnchorFullPath = dry.CfAnchorFullPath;
263 WorkspaceNavigationMapHciOrientationLine = hciLine;
264 WorkspaceFeatureLine = featureLine;
265 WorkspaceFeatureDocPaths = featureDocPaths;
266 WorkspaceAdrCorrespondenceLine = adrLine;
267 WorkspaceAdrCorrespondenceFirstDocPath = adrFirstDocPath;
268 WorkspaceDocsCoverageLine = docsCoverageLine;
269 ApplyWorkspaceCorrespondenceLayers(hciLine, featureLine, adrLine, dry);
270 ApplyCorrespondenceDocAndReverseLists(workspaceRoot, navigationPath, adrDocPaths);
271 WorkspaceNavigationMapItems.Clear();
272 foreach (var parsed in dry.ListRows)
273 {
274 WorkspaceNavigationMapItems.Add(new WorkspaceNavigationMapItemVm
275 {
276 FullPath = parsed.FullPath,
277 RelativePath = parsed.RelativePath,
278 Kind = parsed.Kind,
279 Rationale = parsed.Rationale
280 });
281 }
282 }
283 finally
284 {
285 if (cfGraphNavigateLine is > 0)
286 ClearControlFlowGraphNodeNavigationAnchor();
287 }
288 });
289 }
290
291 private void ApplyWorkspaceCorrespondenceLayers(
292 string hciLine,
293 string featureLine,
294 string adrLine,
295 WorkspaceNavigationMapRefreshComposer.DryResult dry)
296 {
297 var hasCodeGraph = dry.AccentCount > 0
298 || dry.ListRows.Count > 0
299 || dry.Scene?.Nodes.Count > 0;
300
301 var signals = CorrespondenceLayersProjection.FromCorrespondence(
302 hasHciOrientation: !string.IsNullOrWhiteSpace(hciLine),
303 hasFeature: !string.IsNullOrWhiteSpace(featureLine),
304 hasAdrDocs: !string.IsNullOrWhiteSpace(adrLine),
305 hasCodeGraph: hasCodeGraph);
306
307 WorkspaceCorrespondenceLayersLine = CorrespondenceLayersProjection.BuildLayersBadge(signals);
308 WorkspaceCorrespondenceLayersTooltip = CorrespondenceLayersProjection.BuildLayersTooltip(signals);
309 }
310
311 private static (string FeatureLine, string[] FeatureDocPaths, string DocsCoverageLine, string AdrLine, string? AdrFirstDocPath, string[] AdrDocPaths)
312 TryResolveWorkspaceCorrespondence(string? workspaceRoot, string? navigationPath)
313 {
314 var r = WorkspaceCorrespondenceResolver.Resolve(workspaceRoot, navigationPath);
315 return (r.FeatureLine, r.FeatureDocPaths, r.DocsCoverageLine, r.AdrLine, r.AdrFirstDocPath, r.AdrDocPaths);
316 }
317
318 private static double SanitizeMapViewportDimension(double value, double fallback) =>
319 double.IsFinite(value) && value > 0 ? value : fallback;
320}
321
View only · write via MCP/CIDE