| 1 | #nullable enable |
| 2 | using System.Collections.ObjectModel; |
| 3 | using CascadeIDE.Contracts; |
| 4 | using CascadeIDE.Features.Shell.Application; |
| 5 | using CascadeIDE.Features.UiChrome; |
| 6 | using CascadeIDE.Features.Workspace.Application; |
| 7 | using CascadeIDE.Models; |
| 8 | using CascadeIDE.Services; |
| 9 | using CascadeIDE.ViewModels; |
| 10 | |
| 11 | namespace CascadeIDE.Features.Search.Application; |
| 12 | |
| 13 | /// <summary>Фильтрация и наполнение списка палитры команд (каталог, melody, go-to).</summary> |
| 14 | [ApplicationOrchestrator("ide-command-palette-filter")] |
| 15 | internal 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 | |