SearchEngine: Update to use IndexStore.Name()/NameLower()/BuildPath() APIs matching Rust. Remove NameAt/NameLowerAt references that no longer exist.
ca34d91 verified | using System; | |
| using System.Collections.Generic; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Text; | |
| namespace FastSeekWpf.Core; | |
| public enum ResultKind | |
| { | |
| App, Document, Image, Video, Audio, Archive, Folder, Other | |
| } | |
| public class SearchResult | |
| { | |
| public string FullPath { get; set; } = string.Empty; | |
| public string Name { get; set; } = string.Empty; | |
| public byte Rank { get; set; } | |
| public bool IsDir { get; set; } | |
| public ResultKind Kind { get; set; } | |
| } | |
| public static class SearchEngine | |
| { | |
| private static readonly string[] AppExtensions = { "exe", "lnk", "msi", "appx", "msix" }; | |
| private static readonly string[] AppPathMarkers = { | |
| "\\program files\\", "\\program files (x86)\\", | |
| "\\start menu\\", "\\desktop\\", "\\appdata\\" | |
| }; | |
| private static readonly Dictionary<string, ResultKind> ExtensionMap = new(StringComparer.OrdinalIgnoreCase) | |
| { | |
| ["exe"] = ResultKind.App, ["lnk"] = ResultKind.App, ["msi"] = ResultKind.App, | |
| ["doc"] = ResultKind.Document, ["docx"] = ResultKind.Document, ["pdf"] = ResultKind.Document, | |
| ["txt"] = ResultKind.Document, ["xlsx"] = ResultKind.Document, ["xls"] = ResultKind.Document, | |
| ["pptx"] = ResultKind.Document, ["ppt"] = ResultKind.Document, ["odt"] = ResultKind.Document, | |
| ["ods"] = ResultKind.Document, ["odp"] = ResultKind.Document, ["rtf"] = ResultKind.Document, | |
| ["md"] = ResultKind.Document, ["csv"] = ResultKind.Document, ["json"] = ResultKind.Document, | |
| ["xml"] = ResultKind.Document, ["yaml"] = ResultKind.Document, ["toml"] = ResultKind.Document, | |
| ["ini"] = ResultKind.Document, ["log"] = ResultKind.Document, | |
| ["png"] = ResultKind.Image, ["jpg"] = ResultKind.Image, ["jpeg"] = ResultKind.Image, | |
| ["gif"] = ResultKind.Image, ["bmp"] = ResultKind.Image, ["webp"] = ResultKind.Image, | |
| ["svg"] = ResultKind.Image, ["ico"] = ResultKind.Image, ["tiff"] = ResultKind.Image, | |
| ["heic"] = ResultKind.Image, ["raw"] = ResultKind.Image, ["psd"] = ResultKind.Image, | |
| ["mp4"] = ResultKind.Video, ["mkv"] = ResultKind.Video, ["avi"] = ResultKind.Video, | |
| ["mov"] = ResultKind.Video, ["wmv"] = ResultKind.Video, ["flv"] = ResultKind.Video, | |
| ["webm"] = ResultKind.Video, ["m4v"] = ResultKind.Video, | |
| ["mp3"] = ResultKind.Audio, ["flac"] = ResultKind.Audio, ["wav"] = ResultKind.Audio, | |
| ["aac"] = ResultKind.Audio, ["ogg"] = ResultKind.Audio, ["m4a"] = ResultKind.Audio, | |
| ["wma"] = ResultKind.Audio, ["opus"] = ResultKind.Audio, | |
| ["zip"] = ResultKind.Archive, ["rar"] = ResultKind.Archive, ["7z"] = ResultKind.Archive, | |
| ["tar"] = ResultKind.Archive, ["gz"] = ResultKind.Archive, ["bz2"] = ResultKind.Archive, | |
| ["xz"] = ResultKind.Archive, ["zst"] = ResultKind.Archive, | |
| }; | |
| public static ResultKind GetKind(string? ext, bool isDir, string fullPathLower) | |
| { | |
| if (isDir) return ResultKind.Folder; | |
| if (ext == null) return ResultKind.Other; | |
| if (ExtensionMap.TryGetValue(ext, out var kind)) | |
| { | |
| if (kind == ResultKind.App && ContainsAny(fullPathLower, AppPathMarkers)) | |
| return ResultKind.App; | |
| return kind; | |
| } | |
| return ResultKind.Other; | |
| } | |
| private static bool ContainsAny(string s, string[] markers) | |
| { | |
| foreach (var m in markers) | |
| if (s.Contains(m, StringComparison.Ordinal)) return true; | |
| return false; | |
| } | |
| // Search — matches Rust search() exactly. Uses IndexStore.Name() / NameLower() / BuildPath() | |
| public static List<SearchResult> Search( | |
| IndexStore store, | |
| string query, | |
| int limit, | |
| bool caseSensitive, | |
| List<string> excludedDirs) | |
| { | |
| if (string.IsNullOrEmpty(query)) | |
| return new List<SearchResult>(); | |
| string q = caseSensitive ? query : query.ToLowerInvariant(); | |
| // Phase 1: lightweight name-only matching | |
| var candidates = new List<(int idx, byte rank)>(); | |
| for (int i = 0; i < store.Entries.Count; i++) | |
| { | |
| var entry = store.Entries[i]; | |
| string nameCmp = caseSensitive ? store.Name(entry) : store.NameLower(entry); | |
| byte rank; | |
| if (nameCmp == q) rank = 1; | |
| else if (nameCmp.StartsWith(q, StringComparison.Ordinal)) rank = 2; | |
| else if (nameCmp.Contains(q, StringComparison.Ordinal)) rank = 3; | |
| else continue; | |
| candidates.Add((i, rank)); | |
| } | |
| // Phase 2: sort by rank, keep overshoot buffer | |
| candidates.Sort((a, b) => a.rank.CompareTo(b.rank)); | |
| int overshoot = Math.Max(limit * 5, 1000); | |
| if (candidates.Count > overshoot) | |
| candidates.RemoveRange(overshoot, candidates.Count - overshoot); | |
| // Phase 3: build paths + exclusions + app promotion | |
| var results = new List<SearchResult>(limit); | |
| foreach (var (idx, baseRank) in candidates) | |
| { | |
| var entry = store.Entries[idx]; | |
| string fullPath = store.BuildPath(entry.FileRef); | |
| if (excludedDirs.Count > 0) | |
| { | |
| string pathLower = fullPath.ToLowerInvariant(); | |
| if (excludedDirs.Any(ex => pathLower.StartsWith(ex, StringComparison.Ordinal))) | |
| continue; | |
| } | |
| string nameLower = store.NameLower(entry); | |
| byte rank = baseRank; | |
| if (baseRank <= 2) | |
| { | |
| string? ext = nameLower.Contains('.') ? nameLower[(nameLower.LastIndexOf('.') + 1)..] : null; | |
| if (ext != null && Array.IndexOf(AppExtensions, ext) >= 0) | |
| { | |
| string pathLower = fullPath.ToLowerInvariant(); | |
| if (ContainsAny(pathLower, AppPathMarkers)) | |
| rank = 0; | |
| } | |
| } | |
| string? fileExt = null; | |
| if (!entry.IsDir) | |
| { | |
| string name = store.Name(entry); | |
| int dot = name.LastIndexOf('.'); | |
| if (dot >= 0) fileExt = name[(dot + 1)..].ToLowerInvariant(); | |
| } | |
| results.Add(new SearchResult | |
| { | |
| FullPath = fullPath, | |
| Name = store.Name(entry), | |
| Rank = rank, | |
| IsDir = entry.IsDir, | |
| Kind = GetKind(fileExt, entry.IsDir, fullPath.ToLowerInvariant()) | |
| }); | |
| } | |
| results.Sort((a, b) => a.Rank.CompareTo(b.Rank)); | |
| if (results.Count > limit) | |
| results.RemoveRange(limit, results.Count - limit); | |
| return results; | |
| } | |
| // Extension search — matches Rust extension filtering in main.rs | |
| public static List<SearchResult> SearchByExtension( | |
| IndexStore store, | |
| string ext, | |
| List<string> excludedDirs) | |
| { | |
| string dotExt = "." + ext.ToLowerInvariant(); | |
| var results = new List<SearchResult>(); | |
| for (int i = 0; i < store.Entries.Count; i++) | |
| { | |
| var entry = store.Entries[i]; | |
| string name = store.NameLower(entry); | |
| if (!name.EndsWith(dotExt)) continue; | |
| string fullPath = store.BuildPath(entry.FileRef); | |
| string fullPathLower = fullPath.ToLowerInvariant(); | |
| bool excluded = false; | |
| foreach (var ex in excludedDirs) | |
| { | |
| if (fullPathLower.StartsWith(ex, StringComparison.Ordinal)) | |
| { excluded = true; break; } | |
| } | |
| if (excluded) continue; | |
| string? fileExt = null; | |
| int dot = name.LastIndexOf('.'); | |
| if (dot >= 0) fileExt = name[(dot + 1)..]; | |
| results.Add(new SearchResult | |
| { | |
| FullPath = fullPath, | |
| Name = store.Name(entry), | |
| Rank = 0, | |
| IsDir = entry.IsDir, | |
| Kind = GetKind(fileExt, entry.IsDir, fullPathLower) | |
| }); | |
| if (results.Count >= 50) break; | |
| } | |
| return results; | |
| } | |
| } | |