| 1 | #nullable enable |
| 2 | using System.Collections.Generic; |
| 3 | using System.Globalization; |
| 4 | using System.Linq; |
| 5 | using CascadeIDE.Contracts; |
| 6 | using CascadeIDE.ViewModels; |
| 7 | |
| 8 | namespace CascadeIDE.Features.WorkspaceNavigation.Application; |
| 9 | |
| 10 | /// <summary>Визуальный язык узла CF совпадает с <see cref="Views.SkiaKit.Graph.SkiaGraphSceneDrawing"/> BuildNodeGlyph / фигуры.</summary> |
| 11 | public enum ControlFlowNodeVisualKind |
| 12 | { |
| 13 | Anchor, |
| 14 | Circle, |
| 15 | Diamond, |
| 16 | Exit |
| 17 | } |
| 18 | |
| 19 | /// <summary>Одна метка в gutter / virtual spacing lane: логическая строка 1-based.</summary> |
| 20 | public sealed record ControlFlowLineVisual( |
| 21 | int LineOneBased, |
| 22 | ControlFlowNodeVisualKind VisualKind, |
| 23 | string NodeKind, |
| 24 | string TextGlyph, |
| 25 | bool ShowExitArrow, |
| 26 | string? ToolTip); |
| 27 | |
| 28 | /// <summary>Сборка глифов control-flow для редактора (без дублирования логики Skia).</summary> |
| 29 | [ComputingUnit] |
| 30 | public static class CodeNavigationControlFlowGlyphComposer |
| 31 | { |
| 32 | private const string ExitStepKind = "exit_step"; |
| 33 | private const string ConditionStepKind = "condition_step"; |
| 34 | |
| 35 | public static ControlFlowNodeVisualKind ResolveVisualKind(CodeNavigationMapGraphNodeLayout n) |
| 36 | { |
| 37 | if (n.IsAnchor) |
| 38 | return ControlFlowNodeVisualKind.Anchor; |
| 39 | if (IsExitNode(n)) |
| 40 | return ControlFlowNodeVisualKind.Exit; |
| 41 | if (IsConditionNode(n) || n.Shape == CodeNavigationMapNodeShape.Condition) |
| 42 | return ControlFlowNodeVisualKind.Diamond; |
| 43 | return ControlFlowNodeVisualKind.Circle; |
| 44 | } |
| 45 | |
| 46 | /// <summary>Текст внутри узла (пусто для exit — стрелка рисуется отдельно).</summary> |
| 47 | public static (string TextGlyph, bool ShowExitArrow) BuildTextGlyph( |
| 48 | CodeNavigationMapGraphNodeLayout n, |
| 49 | CodeNavigationMapGraphSceneVm scene) |
| 50 | { |
| 51 | var useLegend = scene.UseLegendColumn; |
| 52 | var showGlyphs = scene.ShowNodeLegendGlyphs; |
| 53 | |
| 54 | if (n.IsAnchor) |
| 55 | return ("A", false); |
| 56 | if (string.Equals(n.Kind, "protected_step", StringComparison.OrdinalIgnoreCase)) |
| 57 | return ("T", false); |
| 58 | if (IsExitNode(n)) |
| 59 | return ("", true); |
| 60 | if (n.LegendIndex is int idxLegend && idxLegend > 0 && (useLegend || showGlyphs)) |
| 61 | return (idxLegend.ToString(CultureInfo.InvariantCulture), false); |
| 62 | if (IsConditionNode(n) || n.Shape == CodeNavigationMapNodeShape.Condition) |
| 63 | return ("?", false); |
| 64 | if (IsHandlerNode(n)) |
| 65 | return ("!", false); |
| 66 | return ("•", false); |
| 67 | } |
| 68 | |
| 69 | public static void GetNodeVisual( |
| 70 | CodeNavigationMapGraphNodeLayout n, |
| 71 | CodeNavigationMapGraphSceneVm scene, |
| 72 | out ControlFlowNodeVisualKind kind, |
| 73 | out string textGlyph, |
| 74 | out bool showExitArrow) |
| 75 | { |
| 76 | kind = ResolveVisualKind(n); |
| 77 | (textGlyph, showExitArrow) = BuildTextGlyph(n, scene); |
| 78 | } |
| 79 | |
| 80 | /// <summary>Строка для gutter: <see cref="LineStart"/>, иначе для якоря без строки — первая строка файла.</summary> |
| 81 | public static int? TryGetGutterLine(CodeNavigationMapGraphNodeLayout n) |
| 82 | { |
| 83 | if (n.LineStart is int ls and > 0) |
| 84 | return ls; |
| 85 | if (n.IsAnchor) |
| 86 | return 1; |
| 87 | return null; |
| 88 | } |
| 89 | |
| 90 | public static IReadOnlyList<ControlFlowLineVisual> BuildGutterLineVisuals(CodeNavigationMapGraphSceneVm scene) |
| 91 | { |
| 92 | var byLine = new Dictionary<int, ControlFlowLineVisual>(); |
| 93 | |
| 94 | foreach (var n in scene.Nodes) |
| 95 | { |
| 96 | var line = TryGetGutterLine(n); |
| 97 | if (line is null) |
| 98 | continue; |
| 99 | GetNodeVisual(n, scene, out var kind, out var text, out var arrow); |
| 100 | var tip = TooltipForNode(n); |
| 101 | byLine[line.Value] = new ControlFlowLineVisual(line.Value, kind, n.Kind, text, arrow, tip); |
| 102 | } |
| 103 | |
| 104 | return byLine.Keys.OrderBy(k => k).Select(k => byLine[k]).ToList(); |
| 105 | } |
| 106 | |
| 107 | /// <summary>Текст для ToolTip узла на HUD глифах.</summary> |
| 108 | public static string? TooltipForNode(CodeNavigationMapGraphNodeLayout n) |
| 109 | { |
| 110 | if (!string.IsNullOrWhiteSpace(n.LegendLine)) |
| 111 | return n.LegendLine.Trim(); |
| 112 | if (!string.IsNullOrWhiteSpace(n.Label)) |
| 113 | return n.Label.Trim(); |
| 114 | return null; |
| 115 | } |
| 116 | |
| 117 | private static bool IsExitNode(CodeNavigationMapGraphNodeLayout n) => |
| 118 | string.Equals(n.Kind, ExitStepKind, StringComparison.OrdinalIgnoreCase); |
| 119 | |
| 120 | private static bool IsConditionNode(CodeNavigationMapGraphNodeLayout n) => |
| 121 | string.Equals(n.Kind, ConditionStepKind, StringComparison.OrdinalIgnoreCase); |
| 122 | |
| 123 | private static bool IsHandlerNode(CodeNavigationMapGraphNodeLayout n) => |
| 124 | string.Equals(n.Kind, "handler_step", StringComparison.OrdinalIgnoreCase); |
| 125 | } |
| 126 | |