Forge
csharpdeeb25a2
1#nullable enable
2
3using System.Collections.ObjectModel;
4using CascadeIDE.Services;
5using CommunityToolkit.Mvvm.ComponentModel;
6
7namespace CascadeIDE.Features.Chat;
8
9public partial class ChatPanelViewModel
10{
11 private const int BracketAutocompleteDebounceMs = 80;
12
13 private CancellationTokenSource? _bracketAutocompleteDebounceCts;
14 /// <summary>Строки popup (slash или bracket) — один список для Skia.</summary>
15 public ObservableCollection<ChatSlashSuggestionItem> ComposerPopupSuggestions { get; } = [];
16
17 public ObservableCollection<ChatBracketSuggestionItem> ChatBracketSuggestions { get; } = [];
18
19 [ObservableProperty]
20 private bool _isChatBracketAutocompleteVisible;
21
22 [ObservableProperty]
23 private int _selectedChatBracketSuggestionIndex = -1;
24
25 partial void OnSelectedChatSlashSuggestionIndexChanged(int value)
26 {
27 _ = value;
28 NotifyComposerAutocompleteSelectionChanged();
29 }
30
31 partial void OnSelectedChatBracketSuggestionIndexChanged(int value)
32 {
33 _ = value;
34 NotifyComposerAutocompleteSelectionChanged();
35 }
36
37 private void NotifyComposerAutocompleteSelectionChanged()
38 {
39 OnPropertyChanged(nameof(ComposerAutocompleteSelectionIndex));
40 OnPropertyChanged(nameof(SelectedComposerAutocompleteIndex));
41 }
42
43 /// <summary>Slash или bracket — для UI popup и клавиш.</summary>
44 public bool IsComposerAutocompleteVisible =>
45 IsChatSlashAutocompleteVisible || IsChatBracketAutocompleteVisible;
46
47 public int ComposerAutocompleteSelectionIndex =>
48 IsChatBracketAutocompleteVisible
49 ? SelectedChatBracketSuggestionIndex
50 : SelectedChatSlashSuggestionIndex;
51
52 public int SelectedComposerAutocompleteIndex
53 {
54 get => ComposerAutocompleteSelectionIndex;
55 set
56 {
57 if (IsChatBracketAutocompleteVisible)
58 SelectedChatBracketSuggestionIndex = value;
59 else
60 SelectedChatSlashSuggestionIndex = value;
61 }
62 }
63
64 partial void OnChatComposerCaretIndexChanged(int value)
65 {
66 RefreshComposerAutocomplete();
67 RefreshComposerSlashPreview();
68 }
69
70 /// <summary>Slash popup для Cockpit Command Line (тот же каталог, что у composer).</summary>
71 public void RefreshCockpitCommandLineAutocomplete(string? inputOverride = null, int? caretOverride = null)
72 {
73 if (!IsCockpitCommandLineOpen)
74 return;
75
76 IsChatBracketAutocompleteVisible = false;
77 ChatBracketSuggestions.Clear();
78 SelectedChatBracketSuggestionIndex = -1;
79 RefreshChatSlashAutocomplete(
80 inputOverride ?? CockpitCommandLineText,
81 caretOverride ?? CockpitCommandLineCaretIndex);
82 }
83
84 public void RefreshComposerAutocomplete(string? inputOverride = null, int? caretOverride = null)
85 {
86 if (IsCockpitCommandLineOpen)
87 {
88 RefreshCockpitCommandLineAutocomplete(inputOverride, caretOverride);
89 return;
90 }
91
92 var text = inputOverride ?? ChatInput;
93 var caret = Math.Clamp(caretOverride ?? ChatComposerCaretIndex, 0, text.Length);
94
95 if (ChatBracketAutocomplete.TryGetEditState(text, caret, out var bracketState))
96 {
97 scheduleAnchorDraftPreview(text, caret);
98 var syncBracketRefresh = bracketState.ActiveAxis == ChatBracketAutocomplete.Axis.Start
99 && bracketState.AxisPrefix.Length == 0;
100 if (syncBracketRefresh)
101 RefreshChatBracketAutocomplete(text, caret);
102 else
103 scheduleBracketAutocompleteRefresh(text, caret);
104
105 if (syncBracketRefresh && IsChatBracketAutocompleteVisible)
106 {
107 IsChatSlashAutocompleteVisible = false;
108 ChatSlashSuggestions.Clear();
109 rebuildComposerPopup();
110 return;
111 }
112
113 if (!syncBracketRefresh)
114 {
115 IsChatSlashAutocompleteVisible = false;
116 ChatSlashSuggestions.Clear();
117 rebuildComposerPopup();
118 return;
119 }
120 }
121 else
122 {
123 _bracketAutocompleteDebounceCts?.Cancel();
124 IsChatBracketAutocompleteVisible = false;
125 ChatBracketSuggestions.Clear();
126 SelectedChatBracketSuggestionIndex = -1;
127 clearAnchorDraftPreview();
128 }
129
130 RefreshChatSlashAutocomplete(text, caretOverride: caret);
131 rebuildComposerPopup();
132 RefreshComposerSlashPreview(text, caret);
133 }
134
135 private void rebuildComposerPopup()
136 {
137 ComposerPopupSuggestions.Clear();
138 if (IsChatBracketAutocompleteVisible)
139 {
140 foreach (var b in ChatBracketSuggestions)
141 {
142 ComposerPopupSuggestions.Add(new ChatSlashSuggestionItem(
143 new ChatSlashSuggestion(b.Display, b.Display, b.Help, b.Group)));
144 }
145 }
146 else if (IsChatSlashAutocompleteVisible)
147 {
148 foreach (var s in ChatSlashSuggestions)
149 ComposerPopupSuggestions.Add(s);
150 }
151
152 OnPropertyChanged(nameof(IsComposerAutocompleteVisible));
153 NotifyComposerAutocompleteSelectionChanged();
154 }
155
156 private void scheduleBracketAutocompleteRefresh(string text, int caret)
157 {
158 _bracketAutocompleteDebounceCts?.Cancel();
159 _bracketAutocompleteDebounceCts = new CancellationTokenSource();
160 var cts = _bracketAutocompleteDebounceCts;
161 var capturedText = text;
162 var capturedCaret = caret;
163 _ = refreshBracketAutocompleteDebouncedAsync(capturedText, capturedCaret, cts);
164 }
165
166 private async Task refreshBracketAutocompleteDebouncedAsync(
167 string text,
168 int caret,
169 CancellationTokenSource cts)
170 {
171 try
172 {
173 await Task.Delay(BracketAutocompleteDebounceMs, cts.Token).ConfigureAwait(false);
174 }
175 catch (OperationCanceledException)
176 {
177 return;
178 }
179
180 await UiScheduler.Default.InvokeAsync(() =>
181 {
182 if (cts.IsCancellationRequested)
183 return;
184
185 if (!ChatBracketAutocomplete.TryGetEditState(text, caret, out _))
186 {
187 IsChatBracketAutocompleteVisible = false;
188 ChatBracketSuggestions.Clear();
189 return;
190 }
191
192 RefreshChatBracketAutocomplete(text, caret);
193 rebuildComposerPopup();
194 scheduleAnchorDraftPreview(text, caret);
195 }).ConfigureAwait(false);
196 }
197
198 private void RefreshChatBracketAutocomplete(string text, int caret)
199 {
200 var suggestions = ChatBracketAutocomplete.GetSuggestions(
201 text,
202 caret,
203 _getCurrentFilePath?.Invoke(),
204 _getWorkspaceRoot(),
205 _workspaceFileSlashCompletion);
206
207 ChatBracketSuggestions.Clear();
208 if (!ChatBracketAutocomplete.TryGetEditState(text, caret, out var state))
209 {
210 IsChatBracketAutocompleteVisible = false;
211 SelectedChatBracketSuggestionIndex = -1;
212 return;
213 }
214
215 foreach (var s in suggestions)
216 ChatBracketSuggestions.Add(new ChatBracketSuggestionItem(s, state.BracketStart, state.CaretIndex));
217
218 var visible = ChatBracketSuggestions.Count > 0;
219 if (IsChatBracketAutocompleteVisible != visible)
220 IsChatBracketAutocompleteVisible = visible;
221 else
222 OnPropertyChanged(nameof(IsChatBracketAutocompleteVisible));
223
224 SelectedChatBracketSuggestionIndex = visible ? 0 : -1;
225 }
226
227 public void MoveChatBracketSuggestionSelection(int delta)
228 {
229 if (ChatBracketSuggestions.Count == 0)
230 return;
231
232 if (SelectedChatBracketSuggestionIndex < 0)
233 SelectedChatBracketSuggestionIndex = 0;
234 else
235 {
236 var next = SelectedChatBracketSuggestionIndex + delta;
237 if (next < 0)
238 next = ChatBracketSuggestions.Count - 1;
239 else if (next >= ChatBracketSuggestions.Count)
240 next = 0;
241 SelectedChatBracketSuggestionIndex = next;
242 }
243 }
244
245 public bool TryCommitSelectedBracketSuggestion()
246 {
247 if (ChatBracketSuggestions.Count == 0)
248 return false;
249
250 var idx = SelectedChatBracketSuggestionIndex < 0 ? 0 : SelectedChatBracketSuggestionIndex;
251 if (idx >= ChatBracketSuggestions.Count)
252 idx = 0;
253
254 var item = ChatBracketSuggestions[idx];
255 var tail = item.AddClosingBracket ? "]" : "";
256 var newText = ChatInput[..item.BracketStart]
257 + "["
258 + item.NewBracketInner
259 + tail
260 + ChatInput[item.ReplaceEnd..];
261
262 ChatComposerCaretIndex = item.BracketStart + 1 + item.NewBracketInner.Length + tail.Length;
263 ChatInput = newText;
264 IsChatBracketAutocompleteVisible = false;
265 ChatBracketSuggestions.Clear();
266 OnPropertyChanged(nameof(IsComposerAutocompleteVisible));
267 RefreshComposerAutocomplete();
268 scheduleAnchorDraftPreview(newText, ChatComposerCaretIndex);
269 return true;
270 }
271
272 public void DismissChatBracketAutocomplete()
273 {
274 IsChatBracketAutocompleteVisible = false;
275 ChatBracketSuggestions.Clear();
276 clearAnchorDraftPreview();
277 OnPropertyChanged(nameof(IsComposerAutocompleteVisible));
278 }
279
280 public void MoveComposerAutocompleteSelection(int delta)
281 {
282 if (!IsComposerAutocompleteVisible)
283 return;
284
285 if (IsChatBracketAutocompleteVisible)
286 MoveChatBracketSuggestionSelection(delta);
287 else
288 MoveChatSlashSuggestionSelection(delta);
289
290 NotifyComposerAutocompleteSelectionChanged();
291 }
292
293 public bool TryCommitSelectedComposerSuggestion(out bool shouldAutoExecute)
294 {
295 shouldAutoExecute = false;
296 if (IsChatBracketAutocompleteVisible)
297 return TryCommitSelectedBracketSuggestion();
298
299 return TryCommitSelectedChatSlashSuggestion(out shouldAutoExecute);
300 }
301}
302
View only · write via MCP/CIDE