| 1 | #nullable enable |
| 2 | using CascadeIDE.Cockpit.Graph; |
| 3 | |
| 4 | namespace CascadeIDE.Services; |
| 5 | |
| 6 | /// <summary> |
| 7 | /// Короткий сценарный API для <b>карты намерений</b> (CodeNavigation) поверх <see cref="GraphDocumentBlueprint"/> (те же виды шагов, что и у |
| 8 | /// <see cref="CascadeIDE.Services.CodeNavigation.CodeNavigationControlFlowSubgraphBuilder"/>): |
| 9 | /// <list type="bullet"> |
| 10 | /// <item><description><see cref="DrawFunction"/> — <c>call_step</c>, ребро <c>Call</c>;</description></item> |
| 11 | /// <item><description><see cref="DrawLoopCall"/> — <c>call_step</c>, ребро <c>LoopCall</c> (вызов в теле цикла);</description></item> |
| 12 | /// <item><description><see cref="DrawCondition"/> — <c>condition_step</c>, рёбра <c>ConditionalCall</c>;</description></item> |
| 13 | /// <item><description><see cref="DrawReturn"/> — <c>exit_step</c>, рёбра <c>Exit</c> (выход из метода).</description></item> |
| 14 | /// </list> |
| 15 | /// В wire-формате ещё бывают <c>Merge</c>, <c>MultiBranch</c> и т.д.; для них по-прежнему можно собрать граф руками через blueprint или JSON. |
| 16 | /// </summary> |
| 17 | public sealed class ControlFlowSceneSketch |
| 18 | { |
| 19 | private const string DefaultAnchorLabel = "file"; |
| 20 | |
| 21 | private readonly GraphDocumentBlueprint _blueprint; |
| 22 | private readonly Dictionary<string, string> _stepKeyToNodeId = new(StringComparer.Ordinal); |
| 23 | |
| 24 | public ControlFlowSceneSketch( |
| 25 | string anchorPath, |
| 26 | string anchorRationale = "sketch", |
| 27 | int maxNodes = 64, |
| 28 | int maxEdges = 128) |
| 29 | { |
| 30 | AnchorPath = anchorPath; |
| 31 | _blueprint = new GraphDocumentBlueprint( |
| 32 | anchorPath, |
| 33 | maxNodes, |
| 34 | maxEdges, |
| 35 | DefaultAnchorLabel, |
| 36 | anchorRationale, |
| 37 | GraphKind.CodeIntent); |
| 38 | } |
| 39 | |
| 40 | public string AnchorPath { get; } |
| 41 | |
| 42 | /// <summary>Якорь subgraph (id узла <c>n0</c>).</summary> |
| 43 | public string AnchorNodeId => _blueprint.AnchorNodeId; |
| 44 | |
| 45 | /// <summary> |
| 46 | /// Последовательный шаг вызова: <paramref name="fromStepKey"/> — ключ уже добавленного шага или <c>null</c> (от якоря). |
| 47 | /// </summary> |
| 48 | public string DrawFunction(string? fromStepKey, string toStepKey) => |
| 49 | DrawCallStep(fromStepKey, toStepKey, "Call", "Call"); |
| 50 | |
| 51 | /// <summary> |
| 52 | /// Вызов в контексте цикла (<c>LoopCall</c>), как вызов внутри <c>for</c>/<c>while</c> в билдере по AST. |
| 53 | /// </summary> |
| 54 | public string DrawLoopCall(string? fromStepKey, string toStepKey) => |
| 55 | DrawCallStep(fromStepKey, toStepKey, "LoopCall", "LoopCall"); |
| 56 | |
| 57 | private string DrawCallStep(string? fromStepKey, string toStepKey, string edgeKind, string relatedKind) |
| 58 | { |
| 59 | ArgumentException.ThrowIfNullOrWhiteSpace(toStepKey); |
| 60 | if (_stepKeyToNodeId.ContainsKey(toStepKey)) |
| 61 | throw new InvalidOperationException($"Step key '{toStepKey}' is already used."); |
| 62 | |
| 63 | var fromId = ResolveFromKey(fromStepKey); |
| 64 | var id = _blueprint.TryAddNode( |
| 65 | "call_step", |
| 66 | AnchorPath, |
| 67 | toStepKey, |
| 68 | "", |
| 69 | $"call {toStepKey}", |
| 70 | toStepKey, |
| 71 | assignControlFlowLegendIndex: true); |
| 72 | if (id is null) |
| 73 | throw new InvalidOperationException("Node cap reached."); |
| 74 | |
| 75 | _blueprint.AddEdges([fromId], id, edgeKind, relatedKind); |
| 76 | _stepKeyToNodeId[toStepKey] = id; |
| 77 | return id; |
| 78 | } |
| 79 | |
| 80 | /// <summary> |
| 81 | /// Ветвление: от шага <paramref name="fromStepKey"/> — узел условия, затем два шага <paramref name="thenStepKey"/> и <paramref name="elseStepKey"/>. |
| 82 | /// Текст условия для легенды — <paramref name="conditionLegend"/> (например предикат исходного <c>if</c>). |
| 83 | /// </summary> |
| 84 | public string DrawCondition( |
| 85 | string fromStepKey, |
| 86 | string thenStepKey, |
| 87 | string elseStepKey, |
| 88 | string? conditionLegend = null) |
| 89 | { |
| 90 | ArgumentException.ThrowIfNullOrWhiteSpace(fromStepKey); |
| 91 | ArgumentException.ThrowIfNullOrWhiteSpace(thenStepKey); |
| 92 | ArgumentException.ThrowIfNullOrWhiteSpace(elseStepKey); |
| 93 | if (string.Equals(thenStepKey, elseStepKey, StringComparison.Ordinal)) |
| 94 | throw new InvalidOperationException("Then and else step keys must differ."); |
| 95 | |
| 96 | foreach (var k in new[] { thenStepKey, elseStepKey }) |
| 97 | { |
| 98 | if (_stepKeyToNodeId.ContainsKey(k)) |
| 99 | throw new InvalidOperationException($"Step key '{k}' is already used."); |
| 100 | } |
| 101 | |
| 102 | var fromId = ResolveFromKey(fromStepKey); |
| 103 | var leg = string.IsNullOrWhiteSpace(conditionLegend) ? "if condition" : conditionLegend.Trim(); |
| 104 | |
| 105 | var condId = _blueprint.TryAddNode( |
| 106 | "condition_step", |
| 107 | AnchorPath, |
| 108 | "IF", |
| 109 | "", |
| 110 | "if condition", |
| 111 | leg, |
| 112 | assignControlFlowLegendIndex: true); |
| 113 | if (condId is null) |
| 114 | throw new InvalidOperationException("Node cap reached."); |
| 115 | |
| 116 | _blueprint.AddEdges([fromId], condId, "ConditionalCall", "ConditionalCall"); |
| 117 | |
| 118 | var thenId = AddCallBranch(thenStepKey); |
| 119 | var elseId = AddCallBranch(elseStepKey); |
| 120 | _blueprint.AddEdges([condId], thenId, "ConditionalCall", "ConditionalCall"); |
| 121 | _blueprint.AddEdges([condId], elseId, "ConditionalCall", "ConditionalCall"); |
| 122 | |
| 123 | _stepKeyToNodeId[thenStepKey] = thenId; |
| 124 | _stepKeyToNodeId[elseStepKey] = elseId; |
| 125 | return condId; |
| 126 | } |
| 127 | |
| 128 | /// <summary> |
| 129 | /// Выход из метода (<c>exit_step</c>): узел «return», рёбра <c>Exit</c>. Опционально зарегистрировать <paramref name="exitStepKey"/>, если на этот шаг ссылаются дальше (редко). |
| 130 | /// </summary> |
| 131 | public string DrawReturn(string fromStepKey, string? exitStepKey = null) |
| 132 | { |
| 133 | ArgumentException.ThrowIfNullOrWhiteSpace(fromStepKey); |
| 134 | if (exitStepKey is not null && _stepKeyToNodeId.ContainsKey(exitStepKey)) |
| 135 | throw new InvalidOperationException($"Step key '{exitStepKey}' is already used."); |
| 136 | |
| 137 | var fromId = ResolveFromKey(fromStepKey); |
| 138 | var id = _blueprint.TryAddNode( |
| 139 | "exit_step", |
| 140 | AnchorPath, |
| 141 | "RET", |
| 142 | "", |
| 143 | "return", |
| 144 | "return", |
| 145 | assignControlFlowLegendIndex: true); |
| 146 | if (id is null) |
| 147 | throw new InvalidOperationException("Node cap reached."); |
| 148 | |
| 149 | _blueprint.AddEdges([fromId], id, "Exit", "Exit"); |
| 150 | if (!string.IsNullOrWhiteSpace(exitStepKey)) |
| 151 | _stepKeyToNodeId[exitStepKey!] = id; |
| 152 | return id; |
| 153 | } |
| 154 | |
| 155 | /// <summary> |
| 156 | /// Документ для <see cref="CascadeIDE.Features.WorkspaceNavigation.Application.CodeNavigationMapCompositor"/> (<see cref="GraphDocument"/> + <c>CodeNavigationMapLevelKind.ControlFlow</c>): |
| 157 | /// раскладка идёт через Intent → Declutter → Layout, без обхода пайплайна (ADR 0055). |
| 158 | /// </summary> |
| 159 | public GraphDocument ToDocument() => _blueprint.ToDocument(); |
| 160 | |
| 161 | private string AddCallBranch(string stepKey) |
| 162 | { |
| 163 | var id = _blueprint.TryAddNode( |
| 164 | "call_step", |
| 165 | AnchorPath, |
| 166 | stepKey, |
| 167 | "", |
| 168 | $"call {stepKey}", |
| 169 | stepKey, |
| 170 | assignControlFlowLegendIndex: true); |
| 171 | if (id is null) |
| 172 | throw new InvalidOperationException("Node cap reached."); |
| 173 | return id; |
| 174 | } |
| 175 | |
| 176 | private string ResolveFromKey(string? fromStepKey) |
| 177 | { |
| 178 | if (fromStepKey is null) |
| 179 | return _blueprint.AnchorNodeId; |
| 180 | if (_stepKeyToNodeId.TryGetValue(fromStepKey, out var id)) |
| 181 | return id; |
| 182 | throw new InvalidOperationException($"Unknown step key '{fromStepKey}'."); |
| 183 | } |
| 184 | } |
| 185 | |