Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Cockpit.Graph;
3using CascadeIDE.Cockpit.Graph.Layout;
4using CascadeIDE.Models;
5using CascadeIDE.Services.CodeNavigation;
6using CascadeIDE.Services.SkiaInstruments;
7
8namespace CascadeIDE.Features.WorkspaceNavigation.Application;
9
10public readonly record struct CodeNavigationMapPipelineContext(
11 GraphDocument Subgraph,
12 string MapLevel,
13 SkiaInstrumentViewport Viewport,
14 CodeNavigationMapDetailLevel DetailLevel = CodeNavigationMapDetailLevel.Normal,
15 string RelatedGraphLayout = CodeNavigationMapRelatedGraphLayoutKind.Radial,
16 string ControlFlowMainAxis = CodeNavigationMapControlFlowMainAxisKind.Auto);
17
18public readonly record struct CodeNavigationMapPipelineState(
19 GraphDocument Subgraph,
20 string MapLevel,
21 SkiaInstrumentViewport Viewport,
22 CodeNavigationMapDetailLevel DetailLevel,
23 string RelatedGraphLayout,
24 string NormalizedControlFlowMainAxis,
25 GraphControlFlowMainAxis? ControlFlowMainAxisOverride,
26 int EstimatedLevelCount,
27 int LoopEdgeCount,
28 int MultiBranchEdgeCount,
29 bool IsDense);
30
31public interface ICodeNavigationMapIntentStage
32{
33 CodeNavigationMapPipelineState Resolve(in CodeNavigationMapPipelineContext context);
34}
35
36public interface ICodeNavigationMapDeclutterStage
37{
38 CodeNavigationMapPipelineState Apply(in CodeNavigationMapPipelineState state);
39}
40
41public interface ICodeNavigationMapLayoutStage
42{
43 CodeNavigationMapCompositionResult Layout(in CodeNavigationMapPipelineState state);
44}
45
46public sealed class CodeNavigationMapIntentStage : ICodeNavigationMapIntentStage
47{
48 public CodeNavigationMapPipelineState Resolve(in CodeNavigationMapPipelineContext context)
49 {
50 var level = CodeNavigationMapLevelKind.Normalize(context.MapLevel);
51 var doc = context.Subgraph;
52 var loopEdgeCount = 0;
53 var multiBranchEdgeCount = 0;
54 foreach (var e in doc.Edges)
55 {
56 if (!string.IsNullOrWhiteSpace(e.Kind) && e.Kind.Contains("loop", StringComparison.OrdinalIgnoreCase))
57 loopEdgeCount++;
58 if (!string.IsNullOrWhiteSpace(e.Kind) && e.Kind.Contains("multibranch", StringComparison.OrdinalIgnoreCase))
59 multiBranchEdgeCount++;
60 }
61
62 var estimatedLevelCount = EstimateLevelCount(doc);
63 var isDense = doc.Nodes.Count > 8 || estimatedLevelCount > 6;
64 var cfAxis = CodeNavigationMapControlFlowMainAxisKind.Normalize(context.ControlFlowMainAxis);
65 return new CodeNavigationMapPipelineState(
66 doc,
67 level,
68 context.Viewport,
69 context.DetailLevel,
70 CodeNavigationMapRelatedGraphLayoutKind.Normalize(context.RelatedGraphLayout),
71 cfAxis,
72 GraphControlFlowLayoutMetrics.TryControlFlowMainAxisOverride(cfAxis),
73 estimatedLevelCount,
74 loopEdgeCount,
75 multiBranchEdgeCount,
76 isDense);
77 }
78
79 private static int EstimateLevelCount(GraphDocument doc)
80 {
81 if (doc.Nodes.Count <= 1)
82 return 1;
83
84 var anchor = doc.Nodes.FirstOrDefault(n => string.Equals(n.Kind, "anchor", StringComparison.OrdinalIgnoreCase))
85 ?? doc.Nodes.FirstOrDefault(n => n.Id.Equals("n0", StringComparison.OrdinalIgnoreCase))
86 ?? doc.Nodes[0];
87 var outgoing = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
88 foreach (var e in doc.Edges)
89 {
90 if (!outgoing.TryGetValue(e.FromId, out var list))
91 {
92 list = [];
93 outgoing[e.FromId] = list;
94 }
95
96 list.Add(e.ToId);
97 }
98
99 var depthById = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) { [anchor.Id] = 0 };
100 var queue = new Queue<string>();
101 queue.Enqueue(anchor.Id);
102
103 while (queue.Count > 0)
104 {
105 var id = queue.Dequeue();
106 var depth = depthById[id];
107 if (!outgoing.TryGetValue(id, out var next))
108 continue;
109
110 foreach (var to in next)
111 {
112 if (depthById.ContainsKey(to))
113 continue;
114 depthById[to] = depth + 1;
115 queue.Enqueue(to);
116 }
117 }
118
119 return Math.Max(1, depthById.Count == 0 ? 1 : depthById.Values.Max() + 1);
120 }
121}
122
123public sealed class CodeNavigationMapDeclutterStage(ICodeNavigationMapIntentStage intentStage) : ICodeNavigationMapDeclutterStage
124{
125 private readonly ICodeNavigationMapIntentStage _intentStage = intentStage;
126
127 public CodeNavigationMapPipelineState Apply(in CodeNavigationMapPipelineState state)
128 {
129 var filtered = CodeNavigationMapControlFlowDeclutter.TryTransform(state);
130 if (filtered is null)
131 return state;
132
133 var ctx = new CodeNavigationMapPipelineContext(
134 filtered,
135 state.MapLevel,
136 state.Viewport,
137 state.DetailLevel,
138 state.RelatedGraphLayout,
139 state.NormalizedControlFlowMainAxis);
140 return _intentStage.Resolve(ctx);
141 }
142}
143
144public sealed class CodeNavigationMapLayoutStage(
145 Func<string, IGraphLayoutEngine>? fileLayoutResolver = null,
146 IGraphLayoutEngine? controlFlowLayout = null) : ICodeNavigationMapLayoutStage
147{
148 private readonly Func<string, IGraphLayoutEngine> _fileLayoutResolver =
149 fileLayoutResolver ?? ResolveRelatedFileLayout;
150 private readonly IGraphLayoutEngine _controlFlowLayout = controlFlowLayout ?? new ControlFlowGraphLayoutEngine();
151
152 public CodeNavigationMapCompositionResult Layout(in CodeNavigationMapPipelineState state)
153 {
154 var viewport = state.Viewport;
155 var width = viewport.Width > 0 ? viewport.Width : CodeNavigationMapCompositor.DefaultWidth;
156 var presentation = GraphLayoutPresentationResolver.Resolve(state.Subgraph, state.MapLevel);
157 var isControlFlow = string.Equals(state.MapLevel, CodeNavigationMapLevelKind.ControlFlow, StringComparison.Ordinal);
158
159 if (!isControlFlow)
160 {
161 var satelliteCount = Math.Max(0, state.Subgraph.Nodes.Count - 1);
162 var intrinsicHeight = GraphFileLayoutMetrics.EstimatePreferredHeight(
163 satelliteCount,
164 state.DetailLevel,
165 state.RelatedGraphLayout);
166 var preferredHeight = viewport.Height > 0
167 ? Math.Max(viewport.Height, intrinsicHeight)
168 : intrinsicHeight;
169 var fileLayout = ResolveAutoRelatedFileLayout(state.RelatedGraphLayout, satelliteCount, width, preferredHeight, state.DetailLevel)
170 ?? _fileLayoutResolver(state.RelatedGraphLayout);
171 var layoutScene = fileLayout.Layout(state.Subgraph, width, preferredHeight, state.DetailLevel, controlFlowMainAxisOverride: null)
172 .WithPresentation(presentation);
173 return new CodeNavigationMapCompositionResult(
174 layoutScene,
175 preferredHeight,
176 Array.Empty<CodeNavigationMapInstrumentBlockDescriptor>());
177 }
178
179 var estimatedLevels = state.EstimatedLevelCount;
180 if (state.IsDense)
181 estimatedLevels += 2;
182 var computedHeight = GraphControlFlowLayoutMetrics.EstimatePreferredHeight(
183 estimatedLevels,
184 state.DetailLevel);
185 var preferredCfHeight = Math.Clamp(
186 computedHeight,
187 GraphViewportMetrics.DefaultHeightControlFlow,
188 GraphViewportMetrics.MaxHeightControlFlow);
189
190 var cfScene = _controlFlowLayout.Layout(
191 state.Subgraph,
192 width,
193 preferredCfHeight,
194 state.DetailLevel,
195 state.ControlFlowMainAxisOverride,
196 new GraphLayoutEngineOptions(state.IsDense))
197 .WithPresentation(presentation);
198 return new CodeNavigationMapCompositionResult(
199 cfScene,
200 preferredCfHeight,
201 Array.Empty<CodeNavigationMapInstrumentBlockDescriptor>());
202 }
203
204 private static IGraphLayoutEngine ResolveRelatedFileLayout(string? relatedLayout) =>
205 CodeNavigationMapRelatedGraphLayoutKind.Normalize(relatedLayout) switch
206 {
207 CodeNavigationMapRelatedGraphLayoutKind.TopDown => new GraphRelatedFileHierarchyLayoutEngine(anchorAtTop: true),
208 CodeNavigationMapRelatedGraphLayoutKind.BottomUp => new GraphRelatedFileHierarchyLayoutEngine(anchorAtTop: false),
209 _ => new StarGraphLayoutEngine()
210 };
211
212 private static IGraphLayoutEngine? ResolveAutoRelatedFileLayout(
213 string relatedLayout,
214 int satelliteCount,
215 double width,
216 double height,
217 CodeNavigationMapDetailLevel detailLevel)
218 {
219 if (CodeNavigationMapRelatedGraphLayoutKind.Normalize(relatedLayout) != CodeNavigationMapRelatedGraphLayoutKind.Auto)
220 return null;
221
222 // Heuristic: hierarchy is great when it has enough vertical space; otherwise radial is more readable.
223 if (satelliteCount <= 6)
224 return new GraphRelatedFileHierarchyLayoutEngine(anchorAtTop: true);
225
226 var desiredHierarchy = GraphFileLayoutMetrics.EstimateHierarchyHeight(satelliteCount, detailLevel);
227 var cramped = height < desiredHierarchy * 0.70 || height < 180 || width < 220;
228 return cramped ? new StarGraphLayoutEngine() : new GraphRelatedFileHierarchyLayoutEngine(anchorAtTop: true);
229 }
230}
231
View only · write via MCP/CIDE