Forge
csharpdeeb25a2
1using CascadeIDE.ViewModels;
2using CascadeIDE.Views;
3
4namespace CascadeIDE.Features.Editor.Application.Monaco;
5
6/// <summary>Unifies reveal/goto paths into Monaco (ADR 0163 §2.4).</summary>
7public sealed class EditorNavigationService : IEditorNavigationService
8{
9 private const int NavigationMapRevealDurationMs = 3000;
10
11 private readonly MainWindowViewModel _vm;
12
13 public EditorNavigationService(MainWindowViewModel vm) => _vm = vm;
14
15 public async Task<bool> NavigateAsync(EditorNavigationTarget target, CancellationToken cancellationToken = default)
16 {
17 if (string.IsNullOrWhiteSpace(target.FilePath))
18 return false;
19
20 await UiScheduler.Default.InvokeAsync(() =>
21 {
22 _vm.Documents.ActivateDocumentForReveal(target.FilePath);
23 return true;
24 }).ConfigureAwait(true);
25
26 var dock = await WaitForDockAsync(_vm, target.FilePath, cancellationToken).ConfigureAwait(true);
27 if (dock is null)
28 return false;
29
30 await WaitForMonacoReadyAsync(dock, cancellationToken).ConfigureAwait(true);
31
32 return await ApplyPresentationAsync(dock, target, cancellationToken).ConfigureAwait(true);
33 }
34
35 public bool TryNavigateReveal(
36 string? filePath,
37 int startLine,
38 int endLine,
39 int? durationMs,
40 EditorNavigationSource source = EditorNavigationSource.Mcp)
41 {
42 if (string.IsNullOrWhiteSpace(filePath))
43 return false;
44
45 var presentation = durationMs is > 0
46 ? EditorNavigationPresentation.RevealTransient
47 : EditorNavigationPresentation.SelectAndReveal;
48
49 _ = NavigateAsync(new EditorNavigationTarget(
50 filePath,
51 startLine,
52 endLine,
53 Presentation: presentation,
54 DurationMs: durationMs,
55 Source: source));
56
57 return true;
58 }
59
60 public bool TryNavigateGoTo(
61 string? filePath,
62 int line,
63 int column,
64 int? endLine = null,
65 int? endColumn = null,
66 EditorNavigationSource source = EditorNavigationSource.Mcp)
67 {
68 if (string.IsNullOrWhiteSpace(filePath))
69 return false;
70
71 var end = endLine ?? line;
72 _ = NavigateAsync(new EditorNavigationTarget(
73 filePath,
74 line,
75 end,
76 StartColumn: column,
77 EndColumn: endColumn,
78 Presentation: endLine is not null || endColumn is not null
79 ? EditorNavigationPresentation.SelectAndReveal
80 : EditorNavigationPresentation.ScrollOnly,
81 Source: source));
82
83 return true;
84 }
85
86 private static async Task<DockDocumentView?> WaitForDockAsync(
87 MainWindowViewModel vm,
88 string filePath,
89 CancellationToken cancellationToken)
90 {
91 for (var attempt = 0; attempt < 12; attempt++)
92 {
93 cancellationToken.ThrowIfCancellationRequested();
94 var dock = await UiScheduler.Default.InvokeAsync(() =>
95 EditorActiveDockResolver.TryGetDockDocumentView(vm, filePath)).ConfigureAwait(true);
96 if (dock is not null)
97 return dock;
98 await Task.Delay(50, cancellationToken).ConfigureAwait(false);
99 }
100
101 return null;
102 }
103
104 private static async Task WaitForMonacoReadyAsync(DockDocumentView dock, CancellationToken cancellationToken)
105 {
106 for (var attempt = 0; attempt < 24; attempt++)
107 {
108 cancellationToken.ThrowIfCancellationRequested();
109 if (await UiScheduler.Default.InvokeAsync(() => dock.IsMonacoReady).ConfigureAwait(true))
110 return;
111 await Task.Delay(50, cancellationToken).ConfigureAwait(false);
112 }
113 }
114
115 private static async Task<bool> ApplyPresentationAsync(
116 DockDocumentView dock,
117 EditorNavigationTarget target,
118 CancellationToken cancellationToken)
119 {
120 cancellationToken.ThrowIfCancellationRequested();
121
122 switch (target.Presentation)
123 {
124 case EditorNavigationPresentation.RevealTransient:
125 await dock.GotoLineColumnAsync(target.StartLine, target.StartColumn ?? 1, select: false)
126 .ConfigureAwait(true);
127 await dock.RevealAgentRangeAsync(
128 target.StartLine,
129 target.EndLine,
130 persistent: false,
131 durationMs: target.DurationMs ?? NavigationMapRevealDurationMs).ConfigureAwait(true);
132 return true;
133
134 case EditorNavigationPresentation.RevealPersistent:
135 await dock.GotoLineColumnAsync(target.StartLine, target.StartColumn ?? 1, select: false)
136 .ConfigureAwait(true);
137 await dock.RevealAgentRangeAsync(target.StartLine, target.EndLine, persistent: true).ConfigureAwait(true);
138 return true;
139
140 case EditorNavigationPresentation.ScrollOnly:
141 await dock.GotoLineColumnAsync(
142 target.StartLine,
143 target.StartColumn ?? 1,
144 select: false).ConfigureAwait(true);
145 return true;
146
147 case EditorNavigationPresentation.SelectAndReveal:
148 default:
149 if (target.Source == EditorNavigationSource.NavigationMap)
150 {
151 await dock.GotoLineColumnAsync(target.StartLine, target.StartColumn ?? 1, select: false)
152 .ConfigureAwait(true);
153 await dock.RevealAgentRangeAsync(
154 target.StartLine,
155 target.EndLine,
156 persistent: false,
157 durationMs: NavigationMapRevealDurationMs).ConfigureAwait(true);
158 return true;
159 }
160
161 if (target.DurationMs is > 0)
162 {
163 return dock.TryRevealEditorRange(target.StartLine, target.EndLine, target.DurationMs);
164 }
165
166 await dock.GotoLineColumnAsync(
167 target.StartLine,
168 target.StartColumn ?? 1).ConfigureAwait(true);
169
170 if (target.EndLine > target.StartLine || target.EndColumn is not null)
171 {
172 var text = dock.GetEditorTextSnapshot();
173 var (startOffset, length) = EditorNavigationLineMapping.SelectionOffsetsFromLines(
174 text,
175 target.StartLine,
176 target.StartColumn ?? 1,
177 target.EndLine,
178 target.EndColumn);
179 if (length > 0)
180 await dock.SetSelectionAsync(startOffset, length).ConfigureAwait(true);
181 }
182
183 return true;
184 }
185 }
186}
187
View only · write via MCP/CIDE