Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Services;
4using CascadeIDE.Services.Intercom;
5
6namespace CascadeIDE.Features.Chat;
7
8/// <summary>
9/// Debounced ghost-рамка в редакторе для незакрытого bracket в composer/CCL (ADR 0138 фаза B).
10/// </summary>
11public sealed class AnchorDraftPreviewCoordinator
12{
13 public const int DefaultDebounceMs = 200;
14
15 private readonly Func<string?> _getActiveFilePath;
16 private readonly Func<string> _getWorkspaceRoot;
17 private readonly Func<string?> _getSolutionPath;
18 private readonly Func<string?> _getIndexDirectoryRelative;
19 private readonly Func<string?, int, int, Task>? _revealAgentRange;
20 private readonly Action<string?>? _clearAgentReveal;
21
22 private CancellationTokenSource? _debounceCts;
23 private string? _lastRevealPath;
24 private int _generation;
25
26 public AnchorDraftPreviewCoordinator(
27 Func<string?> getActiveFilePath,
28 Func<string> getWorkspaceRoot,
29 Func<string?> getSolutionPath,
30 Func<string?> getIndexDirectoryRelative,
31 Func<string?, int, int, Task>? revealAgentRange,
32 Action<string?>? clearAgentReveal)
33 {
34 _getActiveFilePath = getActiveFilePath;
35 _getWorkspaceRoot = getWorkspaceRoot;
36 _getSolutionPath = getSolutionPath;
37 _getIndexDirectoryRelative = getIndexDirectoryRelative;
38 _revealAgentRange = revealAgentRange;
39 _clearAgentReveal = clearAgentReveal;
40 }
41
42 public void Schedule(string? composerText, int caretIndex, int debounceMs = DefaultDebounceMs)
43 {
44 if (_revealAgentRange is null)
45 return;
46
47 _debounceCts?.Cancel();
48 _debounceCts = new CancellationTokenSource();
49 var cts = _debounceCts;
50 var generation = ++_generation;
51 var capturedText = composerText ?? "";
52 var capturedCaret = caretIndex;
53 _ = refreshDebouncedAsync(capturedText, capturedCaret, generation, debounceMs, cts);
54 }
55
56 public void Clear()
57 {
58 _debounceCts?.Cancel();
59 _generation++;
60 clearEditorHighlight();
61 }
62
63 private async Task refreshDebouncedAsync(
64 string text,
65 int caret,
66 int generation,
67 int debounceMs,
68 CancellationTokenSource cts)
69 {
70 try
71 {
72 await Task.Delay(debounceMs, cts.Token).ConfigureAwait(false);
73 }
74 catch (OperationCanceledException)
75 {
76 return;
77 }
78
79 if (!tryBuildBracketText(text, caret, out var bracketText))
80 {
81 await UiScheduler.Default.InvokeAsync(() =>
82 {
83 if (generation != _generation || cts.IsCancellationRequested)
84 return;
85
86 clearEditorHighlight();
87 }).ConfigureAwait(false);
88 return;
89 }
90
91 var activeFile = _getActiveFilePath();
92 var workspace = _getWorkspaceRoot();
93 var solution = _getSolutionPath();
94 var indexDir = _getIndexDirectoryRelative();
95
96 var preview = await Task.Run(
97 () =>
98 {
99 if (!AnchorDraftPreviewResolver.TryResolve(
100 bracketText,
101 activeFile,
102 workspace,
103 solution,
104 indexDir,
105 out var resolved,
106 out _))
107 {
108 return (AnchorDraftPreviewResolver.ResolvedPreview?)null;
109 }
110
111 return resolved;
112 },
113 cts.Token).ConfigureAwait(false);
114
115 if (preview is null)
116 {
117 await UiScheduler.Default.InvokeAsync(() =>
118 {
119 if (generation != _generation || cts.IsCancellationRequested)
120 return;
121
122 clearEditorHighlight();
123 }).ConfigureAwait(false);
124 return;
125 }
126
127 await UiScheduler.Default.InvokeAsync(async () =>
128 {
129 if (generation != _generation || cts.IsCancellationRequested)
130 return;
131
132 if (!ChatBracketAutocomplete.TryGetEditState(text, caret, out _))
133 {
134 clearEditorHighlight();
135 return;
136 }
137
138 if (_revealAgentRange is null)
139 {
140 clearEditorHighlight();
141 return;
142 }
143
144 if (_lastRevealPath is not null
145 && !string.Equals(_lastRevealPath, preview.AbsoluteFilePath, StringComparison.OrdinalIgnoreCase))
146 {
147 _clearAgentReveal?.Invoke(_lastRevealPath);
148 }
149
150 _lastRevealPath = preview.AbsoluteFilePath;
151 await _revealAgentRange(preview.AbsoluteFilePath, preview.StartLine, preview.EndLine).ConfigureAwait(true);
152 }).ConfigureAwait(false);
153 }
154
155 private static bool tryBuildBracketText(string text, int caret, out string bracketText)
156 {
157 bracketText = "";
158 if (!ChatBracketAutocomplete.TryGetEditState(text, caret, out var state))
159 return false;
160
161 var inner = text[(state.BracketStart + 1)..state.CaretIndex];
162 if (inner.Trim().Length == 0 && state.ActiveAxis == ChatBracketAutocomplete.Axis.Start)
163 return false;
164
165 bracketText = $"[{inner}]";
166 return true;
167 }
168
169 private void clearEditorHighlight()
170 {
171 if (_lastRevealPath is null)
172 return;
173
174 _clearAgentReveal?.Invoke(_lastRevealPath);
175 _lastRevealPath = null;
176 }
177}
178
View only · write via MCP/CIDE