Forge
csharpdeeb25a2
1#nullable enable
2using System.Collections.ObjectModel;
3using CascadeIDE.Contracts;
4using CascadeIDE.Features.Shell.Application;
5using CascadeIDE.Features.UiChrome;
6using CascadeIDE.Features.Workspace.Application;
7using CascadeIDE.Models;
8using CascadeIDE.Services;
9using CascadeIDE.ViewModels;
10
11namespace CascadeIDE.Features.Search.Application;
12
13/// <summary>Фильтрация и наполнение списка палитры команд (каталог, melody, go-to).</summary>
14[ApplicationOrchestrator("ide-command-palette-filter")]
15internal static class IdeCommandPaletteFilterOrchestrator
16{
17 public static void RefreshCommandPaletteFilter(
18 Func<string> getCommandPaletteQuery,
19 ObservableCollection<IdeCommandPaletteRowViewModel> filteredEntries,
20 HotkeyGestureMap hotkeyGestureMap,
21 UiModeFamily uiModeFamily,
22 string? workspaceSolutionPath,
23 ObservableCollection<SolutionItem> solutionRoots,
24 string? currentFilePath,
25 string editorText,
26 CommandPaletteGoToAsyncHandle goToHandle,
27 Action<int> setSelectedIndex,
28 Func<int> getSelectedIndex,
29 Action refreshCommandPaletteSurfaceSnapshot,
30 Func<ICommandPaletteGoToSearchBackend> getWorkspaceGoToSearchBackend,
31 WorkspaceFileIndex workspaceFileIndex,
32 Action invalidateWorkspaceFileIndex)
33 {
34 switch (CommandPaletteParsedQueryParser.Parse(getCommandPaletteQuery()))
35 {
36 case CommandPaletteParsedQuery.Melody m:
37 goToHandle.Cancel();
38 RefreshMelodyPaletteFilter(
39 m.TailNormalized,
40 filteredEntries,
41 hotkeyGestureMap,
42 uiModeFamily,
43 currentFilePath,
44 editorText,
45 setSelectedIndex,
46 refreshCommandPaletteSurfaceSnapshot);
47 break;
48 case CommandPaletteParsedQuery.GoTo gt:
49 RefreshGoToPaletteFilter(
50 gt.Query,
51 filteredEntries,
52 workspaceSolutionPath,
53 solutionRoots,
54 goToHandle,
55 setSelectedIndex,
56 getSelectedIndex,
57 refreshCommandPaletteSurfaceSnapshot,
58 getCommandPaletteQuery,
59 getWorkspaceGoToSearchBackend,
60 workspaceFileIndex,
61 invalidateWorkspaceFileIndex);
62 break;
63 case CommandPaletteParsedQuery.Catalog c:
64 goToHandle.Cancel();
65 RefreshCommandCatalogPaletteFilter(
66 c.TrimmedRaw,
67 filteredEntries,
68 hotkeyGestureMap,
69 uiModeFamily,
70 setSelectedIndex,
71 refreshCommandPaletteSurfaceSnapshot);
72 break;
73 }
74 }
75
76 private static void RefreshCommandCatalogPaletteFilter(
77 string trimmedCatalogQuery,
78 ObservableCollection<IdeCommandPaletteRowViewModel> filteredEntries,
79 HotkeyGestureMap hotkeyGestureMap,
80 UiModeFamily uiModeFamily,
81 Action<int> setSelectedIndex,
82 Action refreshCommandPaletteSurfaceSnapshot)
83 {
84 var ranked = IdeCommandPaletteMatch.FilterAndRank(IdeCommandPaletteCatalog.All, trimmedCatalogQuery);
85
86 filteredEntries.Clear();
87 var hotkeys = hotkeyGestureMap;
88 var family = uiModeFamily;
89 foreach (var e in ranked)
90 filteredEntries.Add(new IdeCommandPaletteRowViewModel(e, hotkeys.GetDisplayHint(e.CommandId), family));
91
92 setSelectedIndex(CommandPaletteSelectionProjection.InitialSelectedIndex(filteredEntries.Count));
93 refreshCommandPaletteSurfaceSnapshot();
94 }
95
96 private static void RefreshMelodyPaletteFilter(
97 string tailNormalized,
98 ObservableCollection<IdeCommandPaletteRowViewModel> filteredEntries,
99 HotkeyGestureMap hotkeyGestureMap,
100 UiModeFamily uiModeFamily,
101 string? currentFilePath,
102 string editorText,
103 Action<int> setSelectedIndex,
104 Action refreshCommandPaletteSurfaceSnapshot)
105 {
106 filteredEntries.Clear();
107 var hotkeys = hotkeyGestureMap;
108 var family = uiModeFamily;
109 if (TryBuildParametricMelodyPlan(tailNormalized, currentFilePath, editorText) is { } paramPlan)
110 {
111 foreach (var line in paramPlan.Lines)
112 {
113 if (line.ToCommandPaletteRow(hotkeys, family) is { } row)
114 filteredEntries.Add(row);
115 }
116
117 setSelectedIndex(paramPlan.SelectedIndex);
118 refreshCommandPaletteSurfaceSnapshot();
119 return;
120 }
121
122 var plan = MelodyInterpreter.BuildPalette(tailNormalized);
123 foreach (var line in plan.Lines)
124 {
125 if (line.ToCommandPaletteRow(hotkeys, family) is { } row)
126 filteredEntries.Add(row);
127 }
128
129 setSelectedIndex(plan.SelectedIndex);
130 refreshCommandPaletteSurfaceSnapshot();
131 }
132
133 private static MelodyPalettePlan? TryBuildParametricMelodyPlan(
134 string tailNormalized,
135 string? currentFilePath,
136 string editorText)
137 {
138 if (ParametricIntentMelody.TryResolveParametricExecution(
139 tailNormalized,
140 currentFilePath,
141 editorText,
142 out var resolvedCommandId,
143 out var resolvedArgsJson,
144 out var displayTail))
145 return new MelodyPalettePlan([new MelodyPaletteCommand(displayTail, resolvedCommandId, resolvedArgsJson)], 0);
146
147 if (ParametricIntentMelody.TryParseLineRangeTail(tailNormalized, out var parsed) && parsed is not null)
148 {
149 if (ParametricIntentMelody.TryBuildExecutionArgs(
150 parsed,
151 currentFilePath,
152 editorText,
153 out var commandId,
154 out var argsJson,
155 out var error))
156 {
157 return new MelodyPalettePlan(
158 [new MelodyPaletteCommand(parsed.DisplayTail, commandId, argsJson)],
159 0);
160 }
161
162 return new MelodyPalettePlan(
163 [new MelodyPaletteHint(error, ParametricIntentMelody.BuildAliasUsageHintForPalette(parsed.Alias))],
164 0);
165 }
166
167 var aliasBeforeColon = ParametricIntentMelody.TryGetAliasPrefixBeforeColon(tailNormalized);
168 if (!string.IsNullOrEmpty(aliasBeforeColon) && ParametricIntentMelody.IsPaletteOnlyAlias(aliasBeforeColon))
169 {
170 return new MelodyPalettePlan(
171 [new MelodyPaletteHint(
172 ParametricIntentMelody.BuildAliasUsageHintForPalette(aliasBeforeColon),
173 ParametricIntentMelody.BuildAliasUsageCategoryForPalette(aliasBeforeColon))],
174 0);
175 }
176
177 return null;
178 }
179
180 private static void RefreshGoToPaletteFilter(
181 GoToAllQuery q,
182 ObservableCollection<IdeCommandPaletteRowViewModel> filteredEntries,
183 string? workspaceSolutionPath,
184 ObservableCollection<SolutionItem> solutionRoots,
185 CommandPaletteGoToAsyncHandle goToHandle,
186 Action<int> setSelectedIndex,
187 Func<int> getSelectedIndex,
188 Action refreshCommandPaletteSurfaceSnapshot,
189 Func<string> getCommandPaletteQuery,
190 Func<ICommandPaletteGoToSearchBackend> getWorkspaceGoToSearchBackend,
191 WorkspaceFileIndex workspaceFileIndex,
192 Action invalidateWorkspaceFileIndex)
193 {
194 goToHandle.Cancel();
195
196 filteredEntries.Clear();
197
198 var root = CommandPaletteGoToWorkspacePresentation.TryResolveRoot(workspaceSolutionPath);
199 if (root is null)
200 {
201 filteredEntries.Add(new IdeCommandPaletteRowViewModel(
202 "Нет открытого workspace",
203 "Открой решение или папку, затем повтори поиск."));
204 setSelectedIndex(0);
205 return;
206 }
207
208 switch (q.Prefix)
209 {
210 case 'f':
211 invalidateWorkspaceFileIndex();
212 FillGoToFileEntries(q.Term, filteredEntries, workspaceFileIndex, workspaceSolutionPath);
213 break;
214 case 't':
215 case 'm':
216 case 'x':
217 if (string.IsNullOrWhiteSpace(q.Term))
218 {
219 filteredEntries.Add(new IdeCommandPaletteRowViewModel(
220 "Введи запрос после префикса",
221 q.Prefix == 't' ? "t: тип (по .cs)" : q.Prefix == 'm' ? "m: член (эвристика по .cs)" : "x: текст"));
222 }
223 else
224 {
225 filteredEntries.Add(new IdeCommandPaletteRowViewModel("Поиск…", "Подожди результат"));
226 goToHandle.Cts = new CancellationTokenSource();
227 var ct = goToHandle.Cts.Token;
228 var seq = ++goToHandle.Seq;
229 _ = RunGoToWorkspaceSearchAsync(
230 q,
231 root,
232 workspaceSolutionPath,
233 seq,
234 () => goToHandle.Seq,
235 getCommandPaletteQuery,
236 getWorkspaceGoToSearchBackend(),
237 filteredEntries,
238 setSelectedIndex,
239 refreshCommandPaletteSurfaceSnapshot,
240 ct);
241 }
242
243 break;
244 }
245
246 setSelectedIndex(CommandPaletteSelectionProjection.ClampUpperOrKeep(
247 getSelectedIndex(),
248 filteredEntries.Count));
249 refreshCommandPaletteSurfaceSnapshot();
250 }
251
252 private static void FillGoToFileEntries(
253 string term,
254 ObservableCollection<IdeCommandPaletteRowViewModel> filteredEntries,
255 WorkspaceFileIndex workspaceFileIndex,
256 string? workspaceSolutionPath)
257 {
258 var workspaceRoot =
259 CommandPaletteGoToWorkspacePresentation.TryResolveRoot(workspaceSolutionPath)
260 ?? "";
261 foreach (var row in CommandPaletteGoToFileNavRowsProjection.EnumerateFiltered(
262 workspaceFileIndex,
263 term,
264 workspaceRoot,
265 CommandPaletteGoToLimits.MaxFiles))
266 {
267 filteredEntries.Add(new IdeCommandPaletteRowViewModel(
268 row.Title,
269 row.SubtitleCategory,
270 row.FullPath,
271 row.Line,
272 row.Column,
273 row.PrefixHint));
274 }
275
276 if (filteredEntries.Count == 0)
277 {
278 filteredEntries.Add(new IdeCommandPaletteRowViewModel(
279 "Нет файлов по фильтру",
280 "Проверь дерево решения или строку поиска."));
281 }
282 }
283
284 internal static async Task RunGoToWorkspaceSearchAsync(
285 GoToAllQuery query,
286 string workspaceRoot,
287 string? workspaceSolutionPath,
288 long seq,
289 Func<long> getLiveGoToSeq,
290 Func<string> getCommandPaletteQuery,
291 ICommandPaletteGoToSearchBackend workspaceBackend,
292 ObservableCollection<IdeCommandPaletteRowViewModel> filteredEntries,
293 Action<int> setSelectedIndex,
294 Action refreshCommandPaletteSurfaceSnapshot,
295 CancellationToken ct)
296 {
297 try
298 {
299 await Task.Delay(CommandPaletteGoToLimits.RipgrepDebounceMs, ct).ConfigureAwait(false);
300 var (matches, err) = await workspaceBackend
301 .SearchMatchesAsync(
302 query,
303 workspaceRoot,
304 workspaceSolutionPath,
305 CommandPaletteGoToLimits.MaxRipgrepMatches,
306 rgExecutable: null,
307 ct)
308 .ConfigureAwait(false);
309
310 await UiScheduler.Default.InvokeAsync(() =>
311 {
312 if (seq != getLiveGoToSeq())
313 return;
314 if (GoToAllQueryParser.TryParse(getCommandPaletteQuery()) is not { } cur
315 || cur.Prefix != query.Prefix
316 || !string.Equals(cur.Term, query.Term, StringComparison.Ordinal))
317 return;
318
319 filteredEntries.Clear();
320 if (err is not null)
321 {
322 filteredEntries.Add(new IdeCommandPaletteRowViewModel(err, "ripgrep"));
323 setSelectedIndex(0);
324 return;
325 }
326
327 var cat = query.Prefix == 't' ? "t: тип" : query.Prefix == 'm' ? "m: член" : "x: текст";
328 foreach (var row in CommandPaletteGoToRipgrepNavRowsProjection.FromMatches(matches, workspaceRoot, query))
329 {
330 filteredEntries.Add(new IdeCommandPaletteRowViewModel(
331 row.Title,
332 row.SubtitleCategory,
333 row.FullPath,
334 row.Line,
335 row.Column,
336 row.PrefixHint));
337 }
338
339 if (filteredEntries.Count == 0)
340 {
341 filteredEntries.Add(new IdeCommandPaletteRowViewModel(
342 "Ничего не найдено",
343 cat));
344 }
345
346 setSelectedIndex(CommandPaletteSelectionProjection.InitialSelectedIndex(filteredEntries.Count));
347 refreshCommandPaletteSurfaceSnapshot();
348 });
349 }
350 catch (OperationCanceledException)
351 {
352 // ignore
353 }
354 }
355}
356
View only · write via MCP/CIDE