| 1 | using System.ComponentModel; |
| 2 | using Avalonia; |
| 3 | using Avalonia.Controls; |
| 4 | using Avalonia.Threading; |
| 5 | using Avalonia.VisualTree; |
| 6 | using CascadeIDE.Features.Documents; |
| 7 | using CascadeIDE.Features.Editor.Application; |
| 8 | using CascadeIDE.Features.WorkspaceNavigation.Presentation; |
| 9 | using CascadeIDE.ViewModels; |
| 10 | using Microsoft.CodeAnalysis; |
| 11 | using Microsoft.CodeAnalysis.CSharp; |
| 12 | using Microsoft.CodeAnalysis.CSharp.Syntax; |
| 13 | |
| 14 | namespace CascadeIDE.Views; |
| 15 | |
| 16 | public partial class DockDocumentView : UserControl |
| 17 | { |
| 18 | private MainWindowViewModel? _vm; |
| 19 | private DockDocumentViewModel? _docVm; |
| 20 | private PropertyChangedEventHandler? _vmHandler; |
| 21 | private PropertyChangedEventHandler? _navigationMapHandler; |
| 22 | private PropertyChangedEventHandler? _documentsHandler; |
| 23 | |
| 24 | private Border? _stickyScrollHost; |
| 25 | private TextBlock? _stickyScrollText; |
| 26 | |
| 27 | private IEditorSurfaceAdapter? _editorSurface; |
| 28 | private readonly EditorDocumentHudLayer _documentHudLayer = new(); |
| 29 | private Action<EditorInputDelta>? _stabilizedHudAction; |
| 30 | |
| 31 | public DockDocumentView() |
| 32 | { |
| 33 | InitializeComponent(); |
| 34 | DataContextChanged += (_, _) => TrySetup(); |
| 35 | AttachedToVisualTree += (_, _) => TrySetup(); |
| 36 | UiScheduler.Default.Post(TrySetup); |
| 37 | } |
| 38 | |
| 39 | private void TrySetup() |
| 40 | { |
| 41 | Teardown(); |
| 42 | |
| 43 | _docVm = DataContext as DockDocumentViewModel; |
| 44 | if (_docVm is null) |
| 45 | return; |
| 46 | |
| 47 | var top = TopLevel.GetTopLevel(this); |
| 48 | _vm = top?.DataContext as MainWindowViewModel; |
| 49 | if (_vm is null) |
| 50 | { |
| 51 | for (Visual? v = this; v is not null; v = v.GetVisualParent()) |
| 52 | { |
| 53 | if (v is MainWindow mw) |
| 54 | { |
| 55 | _vm = mw.DataContext as MainWindowViewModel; |
| 56 | break; |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | if (_vm is null) |
| 62 | return; |
| 63 | |
| 64 | _stickyScrollHost = this.FindControl<Border>("StickyScrollHost"); |
| 65 | _stickyScrollText = this.FindControl<TextBlock>("StickyScrollText"); |
| 66 | TrySetupMonacoForward(); |
| 67 | } |
| 68 | |
| 69 | private Action<EditorInputDelta> StabilizedHudAction => |
| 70 | _stabilizedHudAction ??= OnStabilizedHud; |
| 71 | |
| 72 | private void OnStabilizedHud(EditorInputDelta d) => |
| 73 | _vm?.SetStabilizedEditorHudContext(_documentHudLayer.BuildStabilizedContext(d)); |
| 74 | |
| 75 | internal void UpdateStabilizedHudRegistration() |
| 76 | { |
| 77 | if (_vm is null) |
| 78 | return; |
| 79 | if (IsActive()) |
| 80 | _vm.SetActiveEditorStabilizedHudHandler(StabilizedHudAction); |
| 81 | else |
| 82 | _vm.ClearActiveEditorStabilizedHudHandlerIfEquals(StabilizedHudAction); |
| 83 | } |
| 84 | |
| 85 | private void Teardown() |
| 86 | { |
| 87 | TeardownMonacoForward(); |
| 88 | |
| 89 | if (_vm is not null) |
| 90 | { |
| 91 | _vm.ClearActiveEditorStabilizedHudHandlerIfEquals(StabilizedHudAction); |
| 92 | _vm.SetStabilizedEditorHudContext(null); |
| 93 | if (_vmHandler is not null) |
| 94 | _vm.PropertyChanged -= _vmHandler; |
| 95 | if (_navigationMapHandler is not null) |
| 96 | { |
| 97 | _vm.NavigationMap.PropertyChanged -= _navigationMapHandler; |
| 98 | _navigationMapHandler = null; |
| 99 | } |
| 100 | |
| 101 | if (_documentsHandler is not null) |
| 102 | _vm.Documents.PropertyChanged -= _documentsHandler; |
| 103 | } |
| 104 | |
| 105 | _documentHudLayer.ConfigureDiagnostics(null); |
| 106 | _editorSurface = null; |
| 107 | |
| 108 | _vm = null; |
| 109 | _docVm = null; |
| 110 | _vmHandler = null; |
| 111 | _documentsHandler = null; |
| 112 | _stickyScrollHost = null; |
| 113 | _stickyScrollText = null; |
| 114 | } |
| 115 | |
| 116 | internal bool IsActive() |
| 117 | { |
| 118 | if (_vm is null || _docVm is null) |
| 119 | return false; |
| 120 | |
| 121 | return ReferenceEquals(_vm.Documents.DockActiveDocument, _docVm) |
| 122 | || string.Equals(_vm.CurrentFilePath, _docVm.Doc.FilePath, StringComparison.OrdinalIgnoreCase); |
| 123 | } |
| 124 | |
| 125 | internal void PostStabilizedEditorInputIfActive(EditorInputDeltaKind kind) |
| 126 | { |
| 127 | if (!IsActive() || _vm is null || _editorSurface is null) |
| 128 | return; |
| 129 | _editorSurface.GetSelection(out var selStart, out var selLen); |
| 130 | var d = new EditorInputDelta(_docVm?.Doc.FilePath, _editorSurface.CaretOffset, selStart, selLen, kind); |
| 131 | _vm.TryPostEditorStabilizedInput(d); |
| 132 | } |
| 133 | |
| 134 | internal static string? BuildStickyScrollLabel(string sourceText, int topLineOneBased) |
| 135 | { |
| 136 | if (topLineOneBased <= 1 || string.IsNullOrWhiteSpace(sourceText)) |
| 137 | return null; |
| 138 | |
| 139 | try |
| 140 | { |
| 141 | var tree = CSharpSyntaxTree.ParseText(sourceText); |
| 142 | var root = tree.GetRoot(); |
| 143 | var line = tree.GetText().Lines[Math.Max(0, Math.Min(topLineOneBased - 1, tree.GetText().Lines.Count - 1))]; |
| 144 | var token = root.FindToken(line.Start); |
| 145 | if (token.RawKind == 0) |
| 146 | return null; |
| 147 | |
| 148 | var parts = token.Parent? |
| 149 | .AncestorsAndSelf() |
| 150 | .Reverse() |
| 151 | .Select(n => ToStickyPart(n, tree, topLineOneBased)) |
| 152 | .Where(static s => !string.IsNullOrWhiteSpace(s)) |
| 153 | .Distinct(StringComparer.Ordinal) |
| 154 | .ToList(); |
| 155 | |
| 156 | if (parts is null || parts.Count == 0) |
| 157 | return null; |
| 158 | |
| 159 | return string.Join(" > ", parts); |
| 160 | } |
| 161 | catch |
| 162 | { |
| 163 | return null; |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | private static string? ToStickyPart(SyntaxNode node, SyntaxTree tree, int topLineOneBased) |
| 168 | { |
| 169 | var startLine = RoslynLinePositionMapper.ToEditorLineNumber(tree.GetLineSpan(node.Span).StartLinePosition).Value; |
| 170 | if (startLine >= topLineOneBased) |
| 171 | return null; |
| 172 | |
| 173 | return node switch |
| 174 | { |
| 175 | BaseNamespaceDeclarationSyntax n => $"namespace {n.Name}", |
| 176 | ClassDeclarationSyntax c => $"class {c.Identifier.Text}", |
| 177 | StructDeclarationSyntax s => $"struct {s.Identifier.Text}", |
| 178 | InterfaceDeclarationSyntax i => $"interface {i.Identifier.Text}", |
| 179 | RecordDeclarationSyntax r => $"record {r.Identifier.Text}", |
| 180 | EnumDeclarationSyntax e => $"enum {e.Identifier.Text}", |
| 181 | DelegateDeclarationSyntax d => $"delegate {d.Identifier.Text}", |
| 182 | MethodDeclarationSyntax m => $"{m.Identifier.Text}()", |
| 183 | ConstructorDeclarationSyntax c => $"{c.Identifier.Text}()", |
| 184 | PropertyDeclarationSyntax p => p.Identifier.Text, |
| 185 | IndexerDeclarationSyntax => "this[]", |
| 186 | LocalFunctionStatementSyntax f => $"{f.Identifier.Text}()", |
| 187 | _ => null |
| 188 | }; |
| 189 | } |
| 190 | |
| 191 | /// <summary>Reveal строк из карты намерений / MCP (ADR 0130).</summary> |
| 192 | public bool TryRevealEditorRange(int startLine, int endLine, int? durationMs) |
| 193 | { |
| 194 | if (_docVm is null || _vm is null || _monacoHost is null) |
| 195 | return false; |
| 196 | |
| 197 | if (durationMs is > 0) |
| 198 | { |
| 199 | _ = _monacoHost.PushAgentRevealAsync(startLine, endLine, persistent: false, durationMs); |
| 200 | return true; |
| 201 | } |
| 202 | |
| 203 | _ = _monacoHost.PushRevealRangeAsync(startLine, endLine); |
| 204 | return true; |
| 205 | } |
| 206 | |
| 207 | public void FocusMonacoEditor() => _monacoHost?.Focus(); |
| 208 | |
| 209 | public Task GotoLineColumnAsync(int line, int column, bool select = true) => |
| 210 | _monacoHost is { IsReady: true } host |
| 211 | ? host.PushRevealRangeAsync(line, line, column, select) |
| 212 | : Task.CompletedTask; |
| 213 | |
| 214 | public Task SetSelectionAsync(int start, int length) => |
| 215 | _monacoHost is { IsReady: true } host |
| 216 | ? host.PushSetSelectionAsync(start, length) |
| 217 | : Task.CompletedTask; |
| 218 | |
| 219 | public Task SetEpochDimAsync(bool dimmed) => |
| 220 | _monacoHost is { IsReady: true } host |
| 221 | ? host.PushEpochDimAsync(dimmed) |
| 222 | : Task.CompletedTask; |
| 223 | |
| 224 | public Task RevealAgentRangeAsync(int startLine, int endLine, bool persistent, int? durationMs = null) => |
| 225 | _monacoHost is { IsReady: true } host |
| 226 | ? host.PushAgentRevealAsync(startLine, endLine, persistent, durationMs: persistent ? null : (durationMs ?? 3000)) |
| 227 | : Task.CompletedTask; |
| 228 | |
| 229 | public void ClearAgentReveal() => _ = ClearAgentRevealAsync(); |
| 230 | |
| 231 | internal Task PushMonacoDebugVisualsAsync() => PushMonacoDebugOverlayAsync(); |
| 232 | |
| 233 | internal string GetEditorTextSnapshot() |
| 234 | { |
| 235 | if (_monacoHost is null) |
| 236 | return _vm?.EditorText ?? _docVm?.Doc.Content ?? ""; |
| 237 | _monacoHost.Session.ReadSnapshot(out _, out var text, out _, out _, out _); |
| 238 | return text; |
| 239 | } |
| 240 | } |
| 241 | |