mirror of
https://github.com/bitsycore/Allofit.git
synced 2026-10-05 12:27:26 +00:00
Fix - An open window with a path query (term containing "/") re-ran the search over the whole index on every FSEvents update, keeping several cores busy while files changed. Index-driven refilters are now throttled to one per second, skipped while the window is closed, minimized or covered, and replayed once when it becomes visible again. Typing stays instant. - Path terms no longer build and lowercase the full path of every record: the folded folder path is precomputed once per folder (interned like parentPath) and matched together with the folded name without allocating the joined string. Path-only queries are about 3x faster. Cleanup - Remove dead code: FSChange.isDir, FileWatcher.latestEventId, IndexState.record(atPath:), the never-set lastQuery preference, AccessManager.reset(), SearchField.onSubmit, an unreachable return. - Merge duplicates: the two Settings size formatters become Formatters.sizeOrDash, the shellQuote wrapper is replaced by AdminShell.quote. - Naming conventions: "in"-prefixed parameter names everywhere (delegate methods, binary helpers, initializers), camelCase impl helpers. - Comment every function and type; fix stale comments. - Debug and release builds are warning-free.
139 lines
5.1 KiB
Swift
139 lines
5.1 KiB
Swift
import Foundation
|
|
|
|
// FileRecord stores the minimum metadata needed to display and search a single
|
|
// filesystem entry. Keeping only names and small fixed-size fields lets the
|
|
// in-memory index scale to hundreds of thousands of entries.
|
|
struct FileRecord: Identifiable, Hashable, Sendable {
|
|
|
|
// stable identifier derived from the absolute path (see pathHash). Also
|
|
// the key of IndexState's lookup table, and the SwiftUI row identity -
|
|
// being deterministic, it survives updates and relaunches so the
|
|
// selection doesn't jump when a file changes under it.
|
|
let id: UInt64
|
|
// last path component (file or directory name)
|
|
let name: String
|
|
// parent directory absolute path (interned: siblings share one buffer)
|
|
let parentPath: String
|
|
// size in bytes (0 for directories)
|
|
let size: Int64
|
|
// creation timestamp
|
|
let dateCreated: Date
|
|
// last modification timestamp
|
|
let dateModified: Date
|
|
// true if this entry is a directory
|
|
let isDirectory: Bool
|
|
// lowercased, NFC-normalized name used for case-insensitive matching.
|
|
// Shares the name's storage when the name is already in that form.
|
|
let nameLower: String
|
|
// lowercased, NFC-normalized parent path for path terms. Interned like
|
|
// parentPath when built through a PathInterner, so a folder's entries
|
|
// share one copy and path searches do no per-record string work.
|
|
let parentLower: String
|
|
|
|
// builds a record; inParentLower is the precomputed search form of the
|
|
// parent path (computed here when nil)
|
|
init(name inName: String,
|
|
parentPath inParentPath: String,
|
|
parentLower inParentLower: String? = nil,
|
|
size inSize: Int64,
|
|
dateCreated inDateCreated: Date,
|
|
dateModified inDateModified: Date,
|
|
isDirectory inIsDirectory: Bool) {
|
|
self.id = FileRecord.pathHash(inParent: inParentPath, inName: inName)
|
|
self.name = inName
|
|
self.parentPath = inParentPath
|
|
self.parentLower = inParentLower ?? FileRecord.sharedSearchForm(of: inParentPath)
|
|
self.size = inSize
|
|
self.dateCreated = inDateCreated
|
|
self.dateModified = inDateModified
|
|
self.isDirectory = inIsDirectory
|
|
self.nameLower = FileRecord.sharedSearchForm(of: inName)
|
|
}
|
|
|
|
// returns the absolute full path computed from parent and name
|
|
var fullPath: String {
|
|
if parentPath.isEmpty { return name }
|
|
if parentPath == "/" { return "/" + name }
|
|
return parentPath + "/" + name
|
|
}
|
|
|
|
// ===========================
|
|
// MARK: Normalization / hashing
|
|
// ===========================
|
|
|
|
// lowercases and canonically composes a string so byte-level matching
|
|
// behaves like a case-insensitive, normalization-insensitive compare.
|
|
// Pure-ASCII strings (the vast majority) skip the NFC pass.
|
|
static func searchForm(of inString: String) -> String {
|
|
let vLower = inString.lowercased()
|
|
if vLower.utf8.allSatisfy({ $0 < 0x80 }) { return vLower }
|
|
return vLower.precomposedStringWithCanonicalMapping
|
|
}
|
|
|
|
// search form that reuses the input's storage when nothing changes
|
|
static func sharedSearchForm(of inString: String) -> String {
|
|
let vForm = searchForm(of: inString)
|
|
return vForm == inString ? inString : vForm
|
|
}
|
|
|
|
// 64-bit FNV-1a offset basis and prime
|
|
private static let kFnvOffset: UInt64 = 0xcbf2_9ce4_8422_2325
|
|
private static let kFnvPrime: UInt64 = 0x0000_0100_0000_01b3
|
|
|
|
// FNV-1a hash of an absolute path's UTF-8 bytes. Deterministic across
|
|
// processes (unlike Hasher, which is randomly seeded per launch).
|
|
// Non-ASCII paths are hashed in composed (NFC) form: APFS accepts both
|
|
// "é" and "e + combining accent" for the same file, and FSEvents and the
|
|
// directory enumerator don't always report the same form.
|
|
static func pathHash(_ inPath: String) -> UInt64 {
|
|
var vHash = kFnvOffset
|
|
if !isASCII(inPath) {
|
|
for vByte in inPath.precomposedStringWithCanonicalMapping.utf8 {
|
|
vHash = (vHash ^ UInt64(vByte)) &* kFnvPrime
|
|
}
|
|
return vHash
|
|
}
|
|
for vByte in inPath.utf8 {
|
|
vHash = (vHash ^ UInt64(vByte)) &* kFnvPrime
|
|
}
|
|
return vHash
|
|
}
|
|
|
|
// true when the string is plain ASCII (no normalization ambiguity)
|
|
private static func isASCII(_ inString: String) -> Bool {
|
|
return inString.utf8.allSatisfy { $0 < 0x80 }
|
|
}
|
|
|
|
// same value as pathHash(fullPath) but computed from the parent + name
|
|
// without allocating the joined string (ASCII fast path)
|
|
static func pathHash(inParent: String, inName: String) -> UInt64 {
|
|
if inParent.isEmpty { return pathHash(inName) }
|
|
if !isASCII(inParent) || !isASCII(inName) {
|
|
return pathHash(inParent == "/" ? "/" + inName : inParent + "/" + inName)
|
|
}
|
|
var vHash = kFnvOffset
|
|
for vByte in inParent.utf8 {
|
|
vHash = (vHash ^ UInt64(vByte)) &* kFnvPrime
|
|
}
|
|
if inParent != "/" {
|
|
vHash = (vHash ^ UInt64(UInt8(ascii: "/"))) &* kFnvPrime
|
|
}
|
|
for vByte in inName.utf8 {
|
|
vHash = (vHash ^ UInt64(vByte)) &* kFnvPrime
|
|
}
|
|
return vHash
|
|
}
|
|
}
|
|
|
|
// Sort modes for FileRecord lists. Defined at file scope so both Preferences
|
|
// (which persists the last choice) and AppModel can reference it without a
|
|
// circular dependency.
|
|
enum FileSortDescriptor: String, CaseIterable, Identifiable {
|
|
case nameAscending, nameDescending
|
|
case sizeAscending, sizeDescending
|
|
case createdAscending, createdDescending
|
|
case modifiedAscending, modifiedDescending
|
|
case pathAscending, pathDescending
|
|
var id: String { rawValue }
|
|
}
|