| 1 | #nullable enable |
| 2 | using System.Collections.ObjectModel; |
| 3 | using CascadeIDE.Models; |
| 4 | |
| 5 | namespace CascadeIDE.Features.Workspace.Application; |
| 6 | |
| 7 | /// <summary> |
| 8 | /// Фильтр SE: пересборка видимого дерева (Avalonia TreeView не умеет prune через IsVisible). |
| 9 | /// </summary> |
| 10 | public static class SolutionExplorerTreeFilter |
| 11 | { |
| 12 | public static void RebuildDisplayRoots( |
| 13 | ObservableCollection<SolutionItem> sourceRoots, |
| 14 | ObservableCollection<SolutionItem> displayRoots, |
| 15 | string filterText, |
| 16 | WorkspaceFileIndex index) |
| 17 | { |
| 18 | displayRoots.Clear(); |
| 19 | var term = filterText.Trim(); |
| 20 | if (term.Length == 0) |
| 21 | { |
| 22 | foreach (var root in sourceRoots) |
| 23 | displayRoots.Add(root); |
| 24 | return; |
| 25 | } |
| 26 | |
| 27 | var matches = index.MatchingFullPaths(term); |
| 28 | foreach (var root in sourceRoots) |
| 29 | { |
| 30 | var filtered = FilterNode(root, term, matches); |
| 31 | if (filtered is not null) |
| 32 | displayRoots.Add(filtered); |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | internal static SolutionItem? FilterNode(SolutionItem node, string term, HashSet<string> matchingPaths) |
| 37 | { |
| 38 | var selfMatches = NodeMatches(node, matchingPaths, term); |
| 39 | var filteredChildren = new List<SolutionItem>(); |
| 40 | foreach (var child in node.Children) |
| 41 | { |
| 42 | var filteredChild = FilterNode(child, term, matchingPaths); |
| 43 | if (filteredChild is not null) |
| 44 | filteredChildren.Add(filteredChild); |
| 45 | } |
| 46 | |
| 47 | if (!selfMatches && filteredChildren.Count == 0) |
| 48 | return null; |
| 49 | |
| 50 | var result = CloneForFilter(node); |
| 51 | foreach (var child in filteredChildren) |
| 52 | result.Children.Add(child); |
| 53 | return result; |
| 54 | } |
| 55 | |
| 56 | private static SolutionItem CloneForFilter(SolutionItem source) |
| 57 | { |
| 58 | SolutionItem clone = source.FullPath switch |
| 59 | { |
| 60 | null => SolutionItem.CreateFolder(source.Title), |
| 61 | { } path when source.IconKey.Equals("solution", StringComparison.OrdinalIgnoreCase) |
| 62 | => SolutionItem.CreateSolution(source.Title, path), |
| 63 | { } path when source.IconKey.Equals("project", StringComparison.OrdinalIgnoreCase) |
| 64 | => SolutionItem.CreateProject(source.Title, path), |
| 65 | { } path when Directory.Exists(path) |
| 66 | => SolutionItem.CreateFolderWorkspaceRoot(source.Title, path), |
| 67 | { } path => SolutionItem.CreateFile(source.Title, path), |
| 68 | }; |
| 69 | clone.IsExpanded = true; |
| 70 | return clone; |
| 71 | } |
| 72 | |
| 73 | private static bool NodeMatches(SolutionItem node, HashSet<string> matchingPaths, string term) |
| 74 | { |
| 75 | if (node.FullPath is { } path) |
| 76 | { |
| 77 | if (matchingPaths.Contains(path)) |
| 78 | return true; |
| 79 | |
| 80 | return node.Title.Contains(term, StringComparison.OrdinalIgnoreCase) |
| 81 | || path.Contains(term, StringComparison.OrdinalIgnoreCase); |
| 82 | } |
| 83 | |
| 84 | return node.Title.Contains(term, StringComparison.OrdinalIgnoreCase); |
| 85 | } |
| 86 | } |
| 87 | |