finder-wpf / FastSeekWpf /Core /SearchEngine.cs
anshdadhich's picture
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;
}
}