Forge
csharpdeeb25a2
1#nullable enable
2using System.Text.Json;
3using CascadeIDE.Cockpit.Cds;
4using CascadeIDE.Cockpit.Channels.TraceFlow;
5using CascadeIDE.Cockpit.Graph;
6using CascadeIDE.Cockpit.Composition.TraceFlow;
7using CascadeIDE.Contracts;
8using CascadeIDE.Features.Documents;
9using CascadeIDE.Models;
10using CascadeIDE.Services.SkiaInstruments;
11using CascadeIDE.ViewModels;
12
13namespace CascadeIDE.Features.WorkspaceNavigation.Application;
14
15/// <summary>
16/// Разбор JSON refresh карты намерений и сборка сцены/related-строк (до маршала на UI).
17/// </summary>
18[ComputingUnit]
19public static class WorkspaceNavigationMapRefreshComposer
20{
21 public sealed record Dependencies(
22 CodeNavigationMapCompositor MapCompositor,
23 TraceFlowChannelCoordinator TraceFlowCoordinator,
24 ITraceFlowCdsRouter TraceFlowCdsRouter,
25 ITraceFlowSurfaceCompositor TraceFlowSurfaceCompositor);
26
27 public sealed record TraceSignals(int ImpactedTestsBadge, string? LastTestSummary);
28
29 public sealed record DryResult(
30 string Status,
31 string AnchorLabel,
32 CodeNavigationMapGraphSceneVm? Scene,
33 double GraphHeight,
34 int AccentCount,
35 IReadOnlyList<WorkspaceNavigationMapOrchestrator.RelatedRow> ListRows,
36 string? CfAnchorFullPath = null);
37
38 /// <summary>Разбирает <paramref name="json"/> и собирает сцену или related-список; при сбое — статус ошибки без исключений наружу.</summary>
39 /// <param name="cockpitSurfaceCapturedOnUi">Снимок CDS с UI-потока; обязателен, если ветка control-flow с подграфом (см. вызывающий refresh).</param>
40 public static DryResult Compose(
41 Dependencies deps,
42 string json,
43 bool useSubgraphMode,
44 bool wantList,
45 string? currentPath,
46 string? solutionPath,
47 string normalizedLevel,
48 double graphWidth,
49 double graphHeight,
50 CodeNavigationMapDetailLevel mapDetailLevel,
51 string relatedGraphLayout,
52 string controlFlowMainAxis,
53 CodeNavigationMapSettings mapSettings,
54 TraceSignals trace,
55 CockpitSurfaceState? cockpitSurfaceCapturedOnUi)
56 {
57 var rows = new List<WorkspaceNavigationMapOrchestrator.RelatedRow>();
58 var status = string.Empty;
59 var anchorLabel = "—";
60 CodeNavigationMapGraphSceneVm? scene = null;
61 var graphPreferredHeight = CodeNavigationMapCompositor.DefaultHeightFile;
62 var accentCount = 0;
63 string? cfAnchorFullPath = null;
64 string? wireError = null;
65
66 try
67 {
68 using var doc = JsonDocument.Parse(json);
69 var root = doc.RootElement;
70 if (root.TryGetProperty("error", out _))
71 {
72 status = WorkspaceNavigationMapOrchestrator.ResolveErrorStatus(root, currentPath);
73 }
74 else if (useSubgraphMode && GraphDocumentJson.TryParseRoot(root, out var subgraph, out wireError))
75 {
76 if (string.Equals(normalizedLevel, CodeNavigationMapLevelKind.ControlFlow, StringComparison.Ordinal)
77 && SolutionTreePath.TryGetFullPath(subgraph!.AnchorPath, out var anchorNorm))
78 cfAnchorFullPath = anchorNorm;
79
80 var viewport = new SkiaInstrumentViewport(graphWidth, graphHeight);
81 var composed = deps.MapCompositor.Compose(
82 new CodeNavigationMapCompositionIntent(
83 subgraph!,
84 normalizedLevel,
85 mapDetailLevel,
86 relatedGraphLayout,
87 controlFlowMainAxis),
88 viewport);
89 if (normalizedLevel == CodeNavigationMapLevelKind.ControlFlow)
90 {
91 ArgumentNullException.ThrowIfNull(cockpitSurfaceCapturedOnUi);
92 var channelPayload = deps.TraceFlowCoordinator.Build(new TraceFlowChannelContext(
93 subgraph!,
94 trace.ImpactedTestsBadge,
95 trace.LastTestSummary ?? ""));
96 var cdsDecision = deps.TraceFlowCdsRouter.Route(
97 new TraceFlowCdsRouteInput(cockpitSurfaceCapturedOnUi, normalizedLevel));
98 var baseScene = composed.ToSceneVm(graphWidth, composed.PreferredHeight, mapSettings, solutionPath);
99 scene = deps.TraceFlowSurfaceCompositor.Compose(baseScene, channelPayload, cdsDecision);
100 }
101 else
102 {
103 scene = composed.ToSceneVm(graphWidth, composed.PreferredHeight, mapSettings, solutionPath);
104 }
105
106 graphPreferredHeight = composed.PreferredHeight;
107 var satCount = Math.Max(0, scene!.Nodes.Count - 1);
108 accentCount = satCount;
109 anchorLabel = WorkspaceNavigationMapOrchestrator.ResolveAnchorLabelFromSubgraph(subgraph!);
110
111 if (wantList)
112 {
113 rows.AddRange(WorkspaceNavigationMapOrchestrator.BuildRowsFromSubgraph(subgraph!, solutionPath));
114 accentCount = Math.Max(accentCount, rows.Count);
115 status = WorkspaceNavigationMapOrchestrator.ResolveEmptyStatus(rows, status, wantList: true);
116 }
117 }
118 else if (wantList)
119 {
120 anchorLabel = WorkspaceNavigationMapOrchestrator.ResolveAnchorLabelFromRelatedRoot(root);
121 rows.AddRange(WorkspaceNavigationMapOrchestrator.BuildRowsFromRelatedRoot(root));
122 accentCount = rows.Count;
123 status = WorkspaceNavigationMapOrchestrator.ResolveEmptyStatus(rows, status, wantList: true);
124 }
125 else if (useSubgraphMode && !string.IsNullOrEmpty(wireError))
126 {
127 status = FormatWireParseStatus(wireError, root);
128 }
129
130 return new DryResult(status, anchorLabel, scene, graphPreferredHeight, accentCount, rows, cfAnchorFullPath);
131 }
132 catch (ArgumentNullException)
133 {
134 throw;
135 }
136 catch (Exception ex)
137 {
138 return new DryResult(
139 FormatComposeExceptionStatus(ex),
140 "—",
141 null,
142 CodeNavigationMapCompositor.DefaultHeightFile,
143 0,
144 [],
145 null);
146 }
147 }
148
149 private static string FormatWireParseStatus(string wireError, JsonElement root)
150 {
151 if (wireError is "error" or "bad_mode" or "no_anchor" or "no_items")
152 {
153 var msg = root.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String
154 ? m.GetString()
155 : null;
156 if (!string.IsNullOrEmpty(msg))
157 return msg;
158 }
159
160 return wireError switch
161 {
162 "no_items" => "Нет связанных файлов по текущим эвристикам.",
163 "no_anchor" => "Не задан якорный файл для карты.",
164 "bad_mode" => "Неподдерживаемый режим ответа навигации.",
165 _ => $"Не удалось разобрать ответ навигации ({wireError})."
166 };
167 }
168
169 private static string FormatComposeExceptionStatus(Exception ex) =>
170 string.IsNullOrWhiteSpace(ex.Message)
171 ? "Не удалось разобрать ответ навигации."
172 : $"Не удалось разобрать ответ навигации: {ex.Message}";
173}
174
View only · write via MCP/CIDE