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 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 Search( IndexStore store, string query, int limit, bool caseSensitive, List excludedDirs) { if (string.IsNullOrEmpty(query)) return new List(); 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(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 SearchByExtension( IndexStore store, string ext, List excludedDirs) { string dotExt = "." + ext.ToLowerInvariant(); var results = new List(); 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; } }