feat: prioritize word starts and app acronyms in search

This commit is contained in:
2026-10-11 21:16:18 +02:00
parent e9ac609884
commit 3714fc516b
3 changed files with 73 additions and 8 deletions
+31 -3
View File
@@ -6,6 +6,7 @@ public struct Application: Identifiable, Sendable {
public let name: String
fileprivate let searchableName: String
fileprivate let searchableCharacters: [Character]
fileprivate let searchableWordStarts: [Int]
public init(url: URL, name: String) {
self.init(canonicalURL: url.standardizedFileURL.resolvingSymlinksInPath(), name: name)
@@ -16,7 +17,16 @@ public struct Application: Identifiable, Sendable {
? canonicalURL : URL(fileURLWithPath: canonicalURL.path, isDirectory: true)
self.name = name
searchableName = folded(name)
searchableCharacters = Array(searchableName)
let characters = Array(searchableName)
searchableCharacters = characters
var starts: [Int] = []
var previousWasWordCharacter = false
for index in characters.indices {
let isWordCharacter = characters[index].isLetter || characters[index].isNumber
if isWordCharacter && !previousWasWordCharacter { starts.append(index) }
previousWasWordCharacter = isWordCharacter
}
searchableWordStarts = starts
}
}
@@ -86,7 +96,15 @@ public enum ApplicationSearch {
let name = application.searchableName
if name == query { return (application, Rank(category: 0)) }
if name.hasPrefix(query) { return (application, Rank(category: 1)) }
if name.contains(query) { return (application, Rank(category: 2)) }
if application.searchableWordStarts.contains(where: {
$0 > 0 && application.searchableCharacters[$0...].starts(with: characters)
}) {
return (application, Rank(category: 2))
}
if matchesWordInitials(characters, in: application) {
return (application, Rank(category: 3))
}
if name.contains(query) { return (application, Rank(category: 4)) }
guard let rank = fuzzyRank(characters, in: application.searchableCharacters) else { return nil }
return (application, rank)
}
@@ -107,6 +125,16 @@ public enum ApplicationSearch {
}
}
private static func matchesWordInitials(_ query: [Character], in application: Application) -> Bool {
var matched = 0
for start in application.searchableWordStarts {
guard application.searchableCharacters[start] == query[matched] else { continue }
matched += 1
if matched == query.count { return true }
}
return false
}
// ponytail: quadratic in app-name length; use a linear matcher if long command text becomes searchable.
private static func fuzzyRank(_ query: [Character], in name: [Character]) -> Rank? {
guard query.count <= name.count else { return nil }
@@ -119,7 +147,7 @@ public enum ApplicationSearch {
if name[end] == query[matched] { matched += 1 }
}
guard matched == query.count else { continue }
let rank = Rank(category: 3, gaps: end - start + 1 - query.count, start: start)
let rank = Rank(category: 5, gaps: end - start + 1 - query.count, start: start)
if let best, best <= rank { continue }
best = rank
}