Files
Allofit/sources/core/ExclusionMatcher.swift
Bitsy 147c4a9353 Fix memory blow-up on relaunch, Everything-style search, index correctness
Memory / relaunch
- FSEvents replay after a relaunch no longer copies the whole index per
  changed file: allRecords is no longer @Published (each element mutation
  emitted the full array and a buffering subscriber kept every copy alive,
  reaching 10+ GB). Changes are coalesced, stat'ed off-main and merged once.
- Save the cache when the history replay finishes and on quit, and save the
  event id actually merged, so the next launch doesn't replay days again.
- Views (App scene, ContentView, PreviewPane) no longer observe AppModel, so
  index updates don't re-render the table or reload the Quick Look preview.
- Break retain cycles in the FSEvents watcher callbacks.

Search (Everything syntax)
- Space = AND, | = OR, ! = NOT, quotes, ext:, file:, folder:.
- Terms with / match the full path; path wildcards are folder-aware
  (* within a folder name, ** across folders), e.g.
  "some/folder/**/path" IMG_????.heic
- Byte-level matching (memmem / native glob) instead of NSRegularExpression,
  parallel filter, top-N selection instead of a full sort, total match count
  in the status bar, near-instant typing debounce.

Index correctness
- New IndexState shared by the GUI and the service: stable path-derived ids,
  O(1) swap removal, folder removal drops its subtree (no ghost entries after
  moving a folder to the Trash), folders moved/renamed in are rescanned,
  parent-must-be-indexed rule matching the walker (hidden/package content).
- Unicode-normalized path ids (no duplicates for accented names).
- Reconcile on load and on settings change: prune removed roots, exclusions
  and orphans; scan new roots; rescan when the saved event id is unusable.
- Service resumes from its cache instead of rescanning everything on start.
- Shared parent-path strings, cheaper exclusion checks, deduped roots.

Tests
- Swift Testing target for the search syntax, index rules, top-N selection
  and cache round trip; run in CI.
2026-10-01 17:45:44 +02:00

40 lines
1.4 KiB
Swift

import Foundation
// ExclusionMatcher tests filesystem paths against a configured exclusion list.
// A path is excluded when it matches an exclusion exactly or sits beneath one
// of the configured prefix directories.
//
// Called for every entry during a walk and for every record during a prune,
// so the check is a plain UTF-8 prefix compare: the paths it receives come
// from URL.path / FSEvents and are already standardized.
struct ExclusionMatcher: Sendable {
// normalized exclusions (no trailing slash, tilde expanded)
private let paths: [String]
// the same exclusions with a trailing slash, as UTF-8 bytes
private let prefixes: [[UInt8]]
// builds a matcher from raw user-entered exclusion strings
init(inExclusions: [String]) {
paths = inExclusions.map { vRaw in
let vExpanded = (vRaw as NSString).expandingTildeInPath
let vStandard = (vExpanded as NSString).standardizingPath
if vStandard.hasSuffix("/") && vStandard.count > 1 {
return String(vStandard.dropLast())
}
return vStandard
}
prefixes = paths.map { Array(($0 == "/" ? "/" : $0 + "/").utf8) }
}
// returns true when inPath matches an exclusion exactly or is below one
func isExcluded(inPath: String) -> Bool {
if paths.isEmpty { return false }
for (vI, vEx) in paths.enumerated() {
if inPath == vEx { return true }
if inPath.utf8.starts(with: prefixes[vI]) { return true }
}
return false
}
}