| 1 | #nullable enable |
| 2 | |
| 3 | using System.Collections.ObjectModel; |
| 4 | using System.IO; |
| 5 | using System.Linq; |
| 6 | using System.Text.Json; |
| 7 | using Avalonia.Controls; |
| 8 | using Avalonia.Threading; |
| 9 | using CascadeIDE.Cockpit.Cds; |
| 10 | using CascadeIDE.Cockpit.Channels.TraceFlow; |
| 11 | using CascadeIDE.Cockpit.Composition.TraceFlow; |
| 12 | using CascadeIDE.Cockpit.Graph; |
| 13 | using CascadeIDE.Features.Documents; |
| 14 | using CascadeIDE.Features.Editor; |
| 15 | using CascadeIDE.Features.Shell.Application; |
| 16 | using CascadeIDE.Features.HybridIndex.Application; |
| 17 | using CascadeIDE.Features.Markdown; |
| 18 | using CascadeIDE.Features.UiChrome; |
| 19 | using CascadeIDE.Features.Workspace; |
| 20 | using CascadeIDE.Features.Workspace.DataAcquisition; |
| 21 | using CascadeIDE.Features.WorkspaceNavigation.Application; |
| 22 | using CascadeIDE.Models; |
| 23 | using CascadeIDE.Services; |
| 24 | using CascadeIDE.ViewModels; |
| 25 | using CommunityToolkit.Mvvm.ComponentModel; |
| 26 | using CommunityToolkit.Mvvm.Input; |
| 27 | |
| 28 | namespace CascadeIDE.Features.WorkspaceNavigation.Presentation; |
| 29 | |
| 30 | /// <summary>PFD/MFD workspace navigation map (ADR 0039).</summary> |
| 31 | public sealed partial class WorkspaceNavigationMapViewModel : ObservableObject |
| 32 | { |
| 33 | private readonly IWorkspaceNavigationMapHost _host; |
| 34 | |
| 35 | public WorkspaceNavigationMapViewModel(IWorkspaceNavigationMapHost host) |
| 36 | { |
| 37 | _host = host; |
| 38 | _host.Shell.PropertyChanged += OnShellPropertyChanged; |
| 39 | } |
| 40 | |
| 41 | public bool IsPfdRegionExpanded => _host.IsPfdRegionExpanded; |
| 42 | |
| 43 | private void OnShellPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) |
| 44 | { |
| 45 | if (e.PropertyName is "IsPfdRegionExpanded" or null or "") |
| 46 | OnPropertyChanged(nameof(IsPfdRegionExpanded)); |
| 47 | } |
| 48 | |
| 49 | private readonly IGraphDataSource _codeNavigationMapGraphDataSource = new WorkspaceNavigationMapContextJsonDataSource(); |
| 50 | private readonly TraceFlowChannelCoordinator _traceFlowChannelCoordinator = new( |
| 51 | [ |
| 52 | new CodeFlowTraceChannel(), |
| 53 | new UnitTestTraceChannel() |
| 54 | ]); |
| 55 | private readonly ITraceFlowCdsRouter _traceFlowCdsRouter = new TraceFlowCdsRouter(); |
| 56 | private readonly ITraceFlowSurfaceCompositor _traceFlowSurfaceCompositor = new TraceFlowSurfaceCompositor(); |
| 57 | private int? _editorCaretOffset; |
| 58 | |
| 59 | /// <summary>Одноразовый якорь CF после клика по узлу графа (строка для refresh + reveal).</summary> |
| 60 | private string? _controlFlowGraphNavigatePath; |
| 61 | private int? _controlFlowGraphNavigateLine; |
| 62 | private int? _controlFlowGraphNavigateColumn; |
| 63 | |
| 64 | private CancellationTokenSource? _workspaceNavigationMapRefreshCts; |
| 65 | |
| 66 | internal int? EditorCaretOffset => _editorCaretOffset; |
| 67 | |
| 68 | internal void UpdateCodeNavigationMapCaretOffset(int? offset) |
| 69 | { |
| 70 | _editorCaretOffset = offset; |
| 71 | if (_host.Settings.CodeNavigationMap.IsControlFlowDepth) |
| 72 | ScheduleWorkspaceNavigationMapRefresh(); |
| 73 | _host.ScheduleEditorHudBannerRefresh(); |
| 74 | } |
| 75 | |
| 76 | /// <summary>Связанные файлы для текущего якоря (режим списка).</summary> |
| 77 | public ObservableCollection<WorkspaceNavigationMapItemVm> WorkspaceNavigationMapItems { get; } = new(); |
| 78 | |
| 79 | /// <summary>Варианты <see cref="CodeNavigationMapPresentationKind"/> для ComboBox.</summary> |
| 80 | public string[] CodeNavigationMapPresentationOptions { get; } = |
| 81 | [CodeNavigationMapPresentationKind.List, CodeNavigationMapPresentationKind.Graph, CodeNavigationMapPresentationKind.Both]; |
| 82 | |
| 83 | /// <summary>Варианты уровня карты: файловый и control flow.</summary> |
| 84 | public string[] CodeNavigationMapLevelOptions { get; } = |
| 85 | [CodeNavigationMapLevelKind.File, CodeNavigationMapLevelKind.ControlFlow]; |
| 86 | |
| 87 | /// <summary><c>auto</c> | <c>vertical</c> | <c>horizontal</c> для <c>[code_navigation_map].control_flow_main_axis</c>.</summary> |
| 88 | public string[] CodeNavigationMapControlFlowMainAxisOptions { get; } = |
| 89 | [ |
| 90 | CodeNavigationMapControlFlowMainAxisKind.Auto, |
| 91 | CodeNavigationMapControlFlowMainAxisKind.Vertical, |
| 92 | CodeNavigationMapControlFlowMainAxisKind.Horizontal, |
| 93 | ]; |
| 94 | |
| 95 | /// <summary> |
| 96 | /// Полный путь якоря подграфа control-flow (как в JSON <c>anchor_path</c>); для кликов и подписей в редакторе. |
| 97 | /// </summary> |
| 98 | [ObservableProperty] |
| 99 | private string? _workspaceNavigationMapCfAnchorFullPath; |
| 100 | |
| 101 | /// <summary>Сцена мини-карты (подграф + укладка по выбранному уровню карты).</summary> |
| 102 | [ObservableProperty] |
| 103 | [NotifyPropertyChangedFor(nameof(WorkspaceNavigationMapHasRelated))] |
| 104 | private CodeNavigationMapGraphSceneVm? _codeNavigationMapGraphScene; |
| 105 | |
| 106 | [ObservableProperty] |
| 107 | private double _codeNavigationMapGraphHeight = CodeNavigationMapCompositor.DefaultHeightFile; |
| 108 | |
| 109 | /// <summary>Ширина области компоновки (совпадает с фактической шириной мини-карты в PFD).</summary> |
| 110 | [ObservableProperty] |
| 111 | private double _codeNavigationMapGraphWidth = CodeNavigationMapCompositor.DefaultWidth; |
| 112 | |
| 113 | /// <summary><c>list</c> | <c>graph</c> | <c>both</c> — синхронизируется с <c>[code_navigation_map]</c>.</summary> |
| 114 | [ObservableProperty] |
| 115 | [NotifyPropertyChangedFor(nameof(ShowCodeNavigationMapList))] |
| 116 | [NotifyPropertyChangedFor(nameof(ShowCodeNavigationMapGraph))] |
| 117 | [NotifyPropertyChangedFor(nameof(WorkspaceNavigationMapHasRelated))] |
| 118 | [NotifyPropertyChangedFor(nameof(CodeNavigationMapListAreaRowHeight))] |
| 119 | [NotifyPropertyChangedFor(nameof(ShowCodeNavigationMapListOnPfd))] |
| 120 | [NotifyPropertyChangedFor(nameof(ShowCodeNavigationMapGraphClickHint))] |
| 121 | private string _codeNavigationMapPresentation = CodeNavigationMapPresentationKind.List; |
| 122 | |
| 123 | /// <summary><c>file</c> | <c>controlFlow</c> — уровень построения карты (секция <c>[code_navigation_map]</c>).</summary> |
| 124 | [ObservableProperty] |
| 125 | [NotifyPropertyChangedFor(nameof(ShowCodeNavigationMapGraph))] |
| 126 | [NotifyPropertyChangedFor(nameof(ShowCodeNavigationMapGraphClickHint))] |
| 127 | [NotifyPropertyChangedFor(nameof(CodeNavigationMapSettingsSummaryLine))] |
| 128 | private string _codeNavigationMapLevel = CodeNavigationMapLevelKind.File; |
| 129 | |
| 130 | /// <summary>Синхронизируется с <c>[code_navigation_map].control_flow_main_axis</c>.</summary> |
| 131 | [ObservableProperty] |
| 132 | [NotifyPropertyChangedFor(nameof(CodeNavigationMapSettingsSummaryLine))] |
| 133 | private string _codeNavigationMapControlFlowMainAxis = CodeNavigationMapControlFlowMainAxisKind.Auto; |
| 134 | |
| 135 | /// <summary>Сообщение об ошибке или пустом состоянии (не null).</summary> |
| 136 | [ObservableProperty] |
| 137 | private string _workspaceNavigationMapStatus = ""; |
| 138 | |
| 139 | /// <summary>Заголовок якоря: имя файла или «—».</summary> |
| 140 | [ObservableProperty] |
| 141 | private string _workspaceNavigationMapAnchorLabel = "—"; |
| 142 | |
| 143 | /// <summary>Число связей для бейджа и Skia accent.</summary> |
| 144 | [ObservableProperty] |
| 145 | [NotifyPropertyChangedFor(nameof(WorkspaceNavigationMapRelatedBadge))] |
| 146 | [NotifyPropertyChangedFor(nameof(WorkspaceNavigationMapHasRelated))] |
| 147 | private int _workspaceNavigationMapRelatedCount; |
| 148 | |
| 149 | /// <summary>Настройка <c>list</c>/<c>both</c>: список связанных ренерится на странице MFD <see cref="MfdShellPage.RelatedFiles"/>, не в колонке PFD.</summary> |
| 150 | public bool ShowCodeNavigationMapList => |
| 151 | CodeNavigationMapPresentationProjection.ShowCodeNavigationMapList(CodeNavigationMapPresentation); |
| 152 | |
| 153 | /// <summary>Списка related на PFD нет (см. <see cref="MfdShellPage.RelatedFiles"/>).</summary> |
| 154 | public bool ShowCodeNavigationMapListOnPfd => |
| 155 | CodeNavigationMapPresentationProjection.ShowCodeNavigationMapListOnPfd; |
| 156 | |
| 157 | /// <summary>Показать мини-карту подграфа.</summary> |
| 158 | public bool ShowCodeNavigationMapGraph => |
| 159 | CodeNavigationMapPresentationProjection.ShowCodeNavigationMapGraph(CodeNavigationMapPresentation, CodeNavigationMapLevel); |
| 160 | |
| 161 | /// <summary> |
| 162 | /// Высота нижней строки Grid под список на PFD: список перенесён в MFD — всегда 0. |
| 163 | /// </summary> |
| 164 | public GridLength CodeNavigationMapListAreaRowHeight => |
| 165 | CodeNavigationMapPresentationProjection.ListAreaRowUsesStar(ShowCodeNavigationMapList, ShowCodeNavigationMapListOnPfd) |
| 166 | ? new GridLength(1, GridUnitType.Star) |
| 167 | : new GridLength(0); |
| 168 | |
| 169 | /// <summary> |
| 170 | /// Режим <c>file</c>: подсказка «открыть файл» (в control flow клик ведёт к строке, не к файлу). |
| 171 | /// </summary> |
| 172 | public bool ShowCodeNavigationMapGraphClickHint => |
| 173 | CodeNavigationMapPresentationProjection.ShowCodeNavigationMapGraphClickHint( |
| 174 | ShowCodeNavigationMapGraph, |
| 175 | CodeNavigationMapLevel, |
| 176 | CodeNavigationMapPresentation); |
| 177 | |
| 178 | /// <summary>Короткая подпись к количеству связей для шапки SM.</summary> |
| 179 | public string WorkspaceNavigationMapRelatedBadge => |
| 180 | CodeNavigationMapPresentationProjection.WorkspaceNavigationMapRelatedBadge(WorkspaceNavigationMapRelatedCount); |
| 181 | |
| 182 | /// <summary>Есть ли контекст для accent (список или подграф с соседями).</summary> |
| 183 | public bool WorkspaceNavigationMapHasRelated => |
| 184 | CodeNavigationMapPresentationProjection.WorkspaceNavigationMapHasRelated( |
| 185 | WorkspaceNavigationMapRelatedCount, |
| 186 | CodeNavigationMapGraphScene?.Nodes.Count); |
| 187 | |
| 188 | public bool IsControlFlowEditorVirtualSpacingActiveForFile(string? filePath) => |
| 189 | EditorControlFlowLanePolicy.ShouldReserveLane( |
| 190 | CodeNavigationMapLevel, |
| 191 | WorkspaceNavigationMapCfAnchorFullPath, |
| 192 | filePath, |
| 193 | CodeNavigationMapGraphScene); |
| 194 | |
| 195 | /// <summary>Список глифов CF для строки gutter активного документа (<c>null</c> если не CF / не совпадает якорь).</summary> |
| 196 | public IReadOnlyList<ControlFlowLineVisual>? GetControlFlowGutterLineVisualsForFile(string? filePath) |
| 197 | { |
| 198 | if (!IsControlFlowEditorVirtualSpacingActiveForFile(filePath)) |
| 199 | return null; |
| 200 | |
| 201 | return CodeNavigationControlFlowGlyphComposer.BuildGutterLineVisuals(CodeNavigationMapGraphScene!); |
| 202 | } |
| 203 | |
| 204 | /// <summary>Открыть связанный файл / code anchor из карты намерений.</summary> |
| 205 | [RelayCommand] |
| 206 | private void OpenWorkspaceNavigationRelated(object? parameter) |
| 207 | { |
| 208 | switch (parameter) |
| 209 | { |
| 210 | case CodeNavigationMapNodeNavigatePayload payload: |
| 211 | NavigateWorkspaceNavigationMapNode(payload); |
| 212 | return; |
| 213 | case string path when !string.IsNullOrWhiteSpace(path): |
| 214 | _host.Documents.OpenOrActivateDocument(path); |
| 215 | return; |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | [RelayCommand] |
| 220 | private void OpenWorkspaceAdrCorrespondence() => ShowCorrespondencePage(); |
| 221 | |
| 222 | [RelayCommand] |
| 223 | private async Task OpenWorkspaceFeatureDocsAsync() |
| 224 | { |
| 225 | var docs = WorkspaceFeatureDocPaths ?? []; |
| 226 | if (docs.Length == 0) |
| 227 | return; |
| 228 | |
| 229 | var pick = docs.Length == 1 |
| 230 | ? docs[0] |
| 231 | : _host.RequestPickFeatureDocAsync is not null |
| 232 | ? await _host.RequestPickFeatureDocAsync("Документация фичи", docs).ConfigureAwait(true) |
| 233 | : docs[0]; |
| 234 | |
| 235 | if (string.IsNullOrWhiteSpace(pick)) |
| 236 | return; |
| 237 | |
| 238 | var wsRoot = _host.GetWorkspacePath(); |
| 239 | if (string.IsNullOrWhiteSpace(wsRoot)) |
| 240 | return; |
| 241 | |
| 242 | if (!WorkspaceMarkdownPreviewOpener.TryOpenRepoDocument( |
| 243 | wsRoot, |
| 244 | pick, |
| 245 | (title, content, source) => _host.MarkdownPreviewTool.SetContent(title, content, source), |
| 246 | out _)) |
| 247 | return; |
| 248 | |
| 249 | _host.ApplyMfdRegionExpanded(true); |
| 250 | _host.TryNavigateToMfdShellPage(MfdShellPage.MarkdownPreview); |
| 251 | } |
| 252 | |
| 253 | [RelayCommand] |
| 254 | private async Task OpenDocsTemplateAsync(object? parameter) |
| 255 | { |
| 256 | var wsRoot = _host.GetWorkspacePath(); |
| 257 | if (string.IsNullOrWhiteSpace(wsRoot)) |
| 258 | return; |
| 259 | |
| 260 | var workspaceToml = RepositoryWorkspaceTomlLoader.TryLoad(wsRoot); |
| 261 | var templates = DocsTemplatesCatalogResolver.ResolveTemplatesFromWorkspaceToml(workspaceToml, wsRoot); |
| 262 | if (templates.Count == 0) |
| 263 | return; |
| 264 | |
| 265 | var requested = parameter as string; |
| 266 | var selectedId = !string.IsNullOrWhiteSpace(requested) ? requested!.Trim() : null; |
| 267 | if (string.IsNullOrWhiteSpace(selectedId)) |
| 268 | { |
| 269 | var labels = templates.Select(t => $"{t.Id} — {t.Title}").ToArray(); |
| 270 | var pick = _host.RequestPickFeatureDocAsync is not null |
| 271 | ? await _host.RequestPickFeatureDocAsync("Шаблон документации", labels).ConfigureAwait(true) |
| 272 | : labels[0]; |
| 273 | |
| 274 | if (string.IsNullOrWhiteSpace(pick)) |
| 275 | return; |
| 276 | |
| 277 | var dash = pick.IndexOf("—", StringComparison.Ordinal); |
| 278 | selectedId = (dash > 0 ? pick[..dash] : pick).Trim(); |
| 279 | } |
| 280 | |
| 281 | if (string.IsNullOrWhiteSpace(selectedId)) |
| 282 | return; |
| 283 | |
| 284 | var entry = templates.FirstOrDefault(t => string.Equals(t.Id, selectedId, StringComparison.OrdinalIgnoreCase)); |
| 285 | if (entry is null) |
| 286 | return; |
| 287 | |
| 288 | if (entry.Source == "knowledge" && !string.IsNullOrWhiteSpace(entry.KnowledgeFilePath)) |
| 289 | { |
| 290 | try |
| 291 | { |
| 292 | var args = new Dictionary<string, System.Text.Json.JsonElement> |
| 293 | { |
| 294 | ["file_path"] = System.Text.Json.JsonDocument.Parse($"\"{entry.KnowledgeFilePath}\"").RootElement.Clone(), |
| 295 | }; |
| 296 | if (!string.IsNullOrWhiteSpace(entry.KnowledgeRootId)) |
| 297 | args["knowledge_root_id"] = System.Text.Json.JsonDocument.Parse($"\"{entry.KnowledgeRootId}\"").RootElement.Clone(); |
| 298 | |
| 299 | var content = await _host.IdeMcp.ExecuteCommandAsync(Services.IdeCommands.ReadKnowledgeFile, args, CancellationToken.None) |
| 300 | .ConfigureAwait(true); |
| 301 | |
| 302 | _host.MarkdownPreviewTool.SetContent($"KB template: {entry.Title}", content ?? "", entry.KnowledgeFilePath); |
| 303 | _host.ApplyMfdRegionExpanded(true); |
| 304 | _host.TryNavigateToMfdShellPage(MfdShellPage.MarkdownPreview); |
| 305 | } |
| 306 | catch |
| 307 | { |
| 308 | // ignore |
| 309 | } |
| 310 | return; |
| 311 | } |
| 312 | |
| 313 | if (!string.IsNullOrWhiteSpace(entry.RepoPath) |
| 314 | && WorkspaceMarkdownPreviewOpener.TryOpenRepoDocument( |
| 315 | wsRoot, |
| 316 | entry.RepoPath, |
| 317 | (title, content, source) => _host.MarkdownPreviewTool.SetContent(entry.Title, content, source), |
| 318 | out _)) |
| 319 | { |
| 320 | _host.ApplyMfdRegionExpanded(true); |
| 321 | _host.TryNavigateToMfdShellPage(MfdShellPage.MarkdownPreview); |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | private void BeginControlFlowGraphNodeNavigation(string fullPath, int lineOneBased) |
| 326 | { |
| 327 | _controlFlowGraphNavigatePath = fullPath; |
| 328 | _controlFlowGraphNavigateLine = lineOneBased; |
| 329 | _controlFlowGraphNavigateColumn = 1; |
| 330 | |
| 331 | if (!EditorTextCoordinateUtilities.PathsReferToSameFile(_host.CurrentFilePath, fullPath)) |
| 332 | return; |
| 333 | |
| 334 | var text = _host.EditorText; |
| 335 | if (WorkspaceNavigationMapOrchestrator.TryOffsetForLine(text, lineOneBased) is int offset) |
| 336 | _editorCaretOffset = offset; |
| 337 | } |
| 338 | |
| 339 | /// <summary>Не сбрасывать каретку при смене файла, если идёт навигация с CF-графа на ту же цель.</summary> |
| 340 | internal bool TryPreserveControlFlowNavigateCaretOnFileChange() |
| 341 | { |
| 342 | if (_controlFlowGraphNavigateLine is not int line || line < 1 |
| 343 | || string.IsNullOrEmpty(_controlFlowGraphNavigatePath)) |
| 344 | return false; |
| 345 | |
| 346 | if (!EditorTextCoordinateUtilities.PathsReferToSameFile(_host.CurrentFilePath, _controlFlowGraphNavigatePath)) |
| 347 | return false; |
| 348 | |
| 349 | if (WorkspaceNavigationMapOrchestrator.TryOffsetForLine(_host.EditorText, line) is int hostOffset) |
| 350 | { |
| 351 | _editorCaretOffset = hostOffset; |
| 352 | return true; |
| 353 | } |
| 354 | |
| 355 | return false; |
| 356 | } |
| 357 | |
| 358 | private void ClearControlFlowGraphNodeNavigationAnchor() |
| 359 | { |
| 360 | _controlFlowGraphNavigatePath = null; |
| 361 | _controlFlowGraphNavigateLine = null; |
| 362 | _controlFlowGraphNavigateColumn = null; |
| 363 | } |
| 364 | |
| 365 | private void NavigateWorkspaceNavigationMapNode(CodeNavigationMapNodeNavigatePayload payload) |
| 366 | { |
| 367 | try |
| 368 | { |
| 369 | if (string.IsNullOrWhiteSpace(payload.FullPath)) |
| 370 | return; |
| 371 | |
| 372 | if (!Features.Documents.SolutionTreePath.TryGetFullPath(payload.FullPath, out var path)) |
| 373 | { |
| 374 | WorkspaceNavigationMapStatus = "Не удалось нормализовать путь файла узла карты."; |
| 375 | return; |
| 376 | } |
| 377 | |
| 378 | var isCf = string.Equals( |
| 379 | CodeNavigationMapLevelKind.Normalize(CodeNavigationMapLevel), |
| 380 | CodeNavigationMapLevelKind.ControlFlow, |
| 381 | StringComparison.Ordinal); |
| 382 | |
| 383 | if (isCf) |
| 384 | { |
| 385 | var anchorCf = WorkspaceNavigationMapCfAnchorFullPath; |
| 386 | if (!string.IsNullOrEmpty(anchorCf) |
| 387 | && !EditorTextCoordinateUtilities.PathsReferToSameFile(path, anchorCf)) |
| 388 | { |
| 389 | WorkspaceNavigationMapStatus = |
| 390 | "Карта потока — другой файл; клик отменён (ожидается якорный .cs карты)."; |
| 391 | return; |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | if (isCf && payload.LineStart is > 0) |
| 396 | BeginControlFlowGraphNodeNavigation(path, payload.LineStart.Value); |
| 397 | |
| 398 | _host.Documents.ActivateDocumentForReveal(path); |
| 399 | |
| 400 | if (payload.LineStart is > 0) |
| 401 | { |
| 402 | var start = payload.LineStart.Value; |
| 403 | var end = payload.LineEnd is > 0 ? payload.LineEnd.Value : start; |
| 404 | var revealPath = path; |
| 405 | Avalonia.Threading.Dispatcher.UIThread.Post( |
| 406 | () => _host.RevealEditorRange(revealPath, start, end, null), |
| 407 | Avalonia.Threading.DispatcherPriority.Loaded); |
| 408 | if (isCf) |
| 409 | ScheduleWorkspaceNavigationMapRefresh(); |
| 410 | } |
| 411 | else if (isCf && string.Equals(payload.Kind, "anchor", StringComparison.OrdinalIgnoreCase)) |
| 412 | { |
| 413 | Avalonia.Threading.Dispatcher.UIThread.Post( |
| 414 | () => _host.RevealEditorRange(path, 1, 1, null), |
| 415 | Avalonia.Threading.DispatcherPriority.Loaded); |
| 416 | } |
| 417 | |
| 418 | if (!string.IsNullOrWhiteSpace(payload.LegendLine)) |
| 419 | WorkspaceNavigationMapStatus = payload.LegendLine.Trim(); |
| 420 | } |
| 421 | catch (Exception ex) |
| 422 | { |
| 423 | WorkspaceNavigationMapStatus = $"Навигация с карты: {ex.Message}"; |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | /// <summary>Сводка настроек карты намерений (без ComboBox; смена через палитру или MCP).</summary> |
| 428 | public string CodeNavigationMapSettingsSummaryLine => |
| 429 | CodeNavigationMapPresentationProjection.SettingsSummaryLine( |
| 430 | CodeNavigationMapPresentation, |
| 431 | CodeNavigationMapLevel, |
| 432 | _host.Settings.CodeNavigationMap.DetailLevel, |
| 433 | _host.Settings.CodeNavigationMap.RelatedGraphLayout, |
| 434 | _host.Settings.CodeNavigationMap.NormalizedControlFlowMainAxis); |
| 435 | |
| 436 | /// <summary>Краткая строка ориентации HCI (слой B) рядом с картой; не влияет на Roslyn-граф (ADR 0106).</summary> |
| 437 | [ObservableProperty] |
| 438 | private string _workspaceNavigationMapHciOrientationLine = ""; |
| 439 | |
| 440 | /// <summary>Активные слои correspondence L0–L4 (ADR 0155 §3).</summary> |
| 441 | [ObservableProperty] |
| 442 | private string _workspaceCorrespondenceLayersLine = ""; |
| 443 | |
| 444 | [ObservableProperty] |
| 445 | private string _workspaceCorrespondenceLayersTooltip = ""; |
| 446 | |
| 447 | /// <summary>Doc correspondence (ADR 0061): какие ADR относятся к текущему файлу по <c>[_host.Workspace.adr.map]</c>.</summary> |
| 448 | [ObservableProperty] |
| 449 | private string _workspaceAdrCorrespondenceLine = ""; |
| 450 | |
| 451 | [ObservableProperty] |
| 452 | private string? _workspaceAdrCorrespondenceFirstDocPath; |
| 453 | |
| 454 | [ObservableProperty] |
| 455 | private string _workspaceFeatureLine = ""; |
| 456 | |
| 457 | /// <summary>Мягкий сигнал “нет доков” (не ошибка): пусто, если всё ок.</summary> |
| 458 | [ObservableProperty] |
| 459 | private string _workspaceDocsCoverageLine = ""; |
| 460 | |
| 461 | [ObservableProperty] |
| 462 | private string[] _workspaceFeatureDocPaths = []; |
| 463 | |
| 464 | /// <summary>Команда палитры / MCP: list → graph → both.</summary> |
| 465 | public void CycleCodeNavigationMapPresentation() => |
| 466 | CodeNavigationMapPresentation = CodeNavigationMapPresentationProjection.NextPresentationViewAfter(CodeNavigationMapPresentation); |
| 467 | |
| 468 | /// <summary>Команда палитры / MCP: file ↔ controlFlow.</summary> |
| 469 | public void CycleCodeNavigationMapLevel() => |
| 470 | CodeNavigationMapLevel = CodeNavigationMapPresentationProjection.ToggledMapLevel(CodeNavigationMapLevel); |
| 471 | |
| 472 | /// <summary>Команда палитры / MCP / слэш: явно <c>file</c> или <c>controlFlow</c>.</summary> |
| 473 | public void SetCodeNavigationMapLevel(string level) => |
| 474 | CodeNavigationMapLevel = CodeNavigationMapLevelKind.Normalize(level); |
| 475 | |
| 476 | /// <summary>Команда палитры / MCP: radial → top_down → bottom_up.</summary> |
| 477 | public void CycleCodeNavigationMapRelatedGraphLayout() |
| 478 | { |
| 479 | _host.Settings.CodeNavigationMap.RelatedGraphLayout = |
| 480 | CodeNavigationMapPresentationProjection.NextRelatedGraphLayoutAfter(_host.Settings.CodeNavigationMap.RelatedGraphLayout); |
| 481 | _host.SaveSettingsIfChanged(); |
| 482 | ScheduleWorkspaceNavigationMapRefresh(); |
| 483 | OnPropertyChanged(nameof(CodeNavigationMapSettingsSummaryLine)); |
| 484 | } |
| 485 | |
| 486 | /// <summary>Команда палитры / MCP: glance → normal → inspect.</summary> |
| 487 | public void CycleCodeNavigationMapDetailLevel() |
| 488 | { |
| 489 | var (_, toml) = CodeNavigationMapPresentationProjection.NextDetailCycle(_host.Settings.CodeNavigationMap.NormalizedDetailLevel); |
| 490 | _host.Settings.CodeNavigationMap.DetailLevel = toml; |
| 491 | _host.SaveSettingsIfChanged(); |
| 492 | ScheduleWorkspaceNavigationMapRefresh(); |
| 493 | OnPropertyChanged(nameof(CodeNavigationMapSettingsSummaryLine)); |
| 494 | } |
| 495 | |
| 496 | partial void OnCodeNavigationMapPresentationChanged(string value) |
| 497 | { |
| 498 | var normalized = CodeNavigationMapPresentationKind.Normalize(value); |
| 499 | if (ShellSettingsPresentationProjection.ShouldRewriteWithNormalizedValue(value, normalized)) |
| 500 | { |
| 501 | CodeNavigationMapPresentation = normalized; |
| 502 | return; |
| 503 | } |
| 504 | |
| 505 | _host.Settings.CodeNavigationMap.View = normalized; |
| 506 | _host.SaveSettingsIfChanged(); |
| 507 | ScheduleWorkspaceNavigationMapRefresh(); |
| 508 | } |
| 509 | |
| 510 | partial void OnCodeNavigationMapLevelChanged(string value) |
| 511 | { |
| 512 | var normalized = CodeNavigationMapLevelKind.Normalize(value); |
| 513 | if (ShellSettingsPresentationProjection.ShouldRewriteWithNormalizedValue(value, normalized)) |
| 514 | { |
| 515 | CodeNavigationMapLevel = normalized; |
| 516 | return; |
| 517 | } |
| 518 | |
| 519 | _host.Settings.CodeNavigationMap.Depth = normalized; |
| 520 | _host.SaveSettingsIfChanged(); |
| 521 | ScheduleWorkspaceNavigationMapRefresh(); |
| 522 | } |
| 523 | |
| 524 | partial void OnCodeNavigationMapControlFlowMainAxisChanged(string value) |
| 525 | { |
| 526 | var normalized = CodeNavigationMapControlFlowMainAxisKind.Normalize(value); |
| 527 | if (ShellSettingsPresentationProjection.ShouldRewriteWithNormalizedValue(value, normalized)) |
| 528 | { |
| 529 | CodeNavigationMapControlFlowMainAxis = normalized; |
| 530 | return; |
| 531 | } |
| 532 | |
| 533 | _host.Settings.CodeNavigationMap.ControlFlowMainAxis = normalized; |
| 534 | _host.SaveSettingsIfChanged(); |
| 535 | ScheduleWorkspaceNavigationMapRefresh(); |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | |
| 540 | |