Move filtering into file system scanner

Greatest reduction in work, changing filtering is rare
therefore, rescan is not too bad.
This commit is contained in:
Kovid Goyal
2025-07-04 14:24:15 +05:30
parent 76d0991b76
commit 0c017e3c9b
2 changed files with 51 additions and 24 deletions

View File

@@ -63,14 +63,16 @@ type FileSystemScanner struct {
mutex sync.Mutex
collection *ResultCollection
dir_reader func(path string) ([]fs.DirEntry, error)
filter_func func(filename string) bool
err error
}
func NewFileSystemScanner(root_dir string, notify chan bool) (fss *FileSystemScanner) {
func NewFileSystemScanner(root_dir string, notify chan bool, filter_func func(string) bool) (fss *FileSystemScanner) {
ans := &FileSystemScanner{root_dir: root_dir, listeners: []chan bool{notify}, collection: NewResultCollection(4096)}
ans.in_progress.Store(true)
ans.keep_going.Store(true)
ans.dir_reader = os.ReadDir
ans.filter_func = utils.IfElse(filter_func == nil, accept_all, filter_func)
return ans
}
@@ -184,6 +186,8 @@ func as_lower(s string, output []byte) int {
return pos
}
func accept_all(filename string) bool { return true }
func (fss *FileSystemScanner) worker() {
defer func() {
fss.lock()
@@ -228,24 +232,29 @@ func (fss *FileSystemScanner) worker() {
sortable = make([]*sortable_dir_entry, 0, cap(arena))
}
arena = arena[:len(entries)]
sortable = sortable[:len(entries)]
sortable = sortable[:0]
for i, e := range entries {
arena[i].name = e.Name()
name := e.Name()
ftype := e.Type()
is_dir := ftype&fs.ModeDir != 0
if !is_dir && !fss.filter_func(name) {
continue
}
arena[i].name = name
if ftype&fs.ModeSymlink != 0 {
if st, serr := os.Stat(dir + arena[i].name); serr == nil && st.IsDir() {
ftype |= SymlinkToDir
}
}
arena[i].ftype = ftype
if ftype&fs.ModeDir != 0 {
if is_dir {
arena[i].buf[0] = '0'
} else {
arena[i].buf[0] = '1'
}
n := as_lower(arena[i].name, arena[i].buf[1:])
arena[i].sort_key = arena[i].buf[:1+n]
sortable[i] = &arena[i]
sortable = append(sortable, &arena[i])
}
slices.SortFunc(sortable, func(a, b *sortable_dir_entry) int { return bytes.Compare(a.sort_key, b.sort_key) })
fss.lock()
@@ -285,6 +294,7 @@ type FileSystemScorer struct {
on_results func(error, bool)
current_worker_wait *sync.WaitGroup
scorer *fzf.FuzzyMatcher
dir_reader func(path string) ([]fs.DirEntry, error)
}
func NewFileSystemScorer(root_dir, query string, filter Filter, only_dirs bool, on_results func(error, bool)) (ans *FileSystemScorer) {
@@ -302,7 +312,9 @@ func (fss *FileSystemScorer) Start() {
fss.is_complete.Store(false)
fss.keep_going.Store(true)
if fss.scanner == nil {
fss.scanner = NewFileSystemScanner(fss.root_dir, on_results)
sc := NewFileSystemScanner(fss.root_dir, on_results, fss.filter.Match)
sc.dir_reader = fss.dir_reader
fss.scanner = sc
fss.scanner.Start()
} else {
fss.scanner.AddListener(on_results)
@@ -318,6 +330,9 @@ func (fss *FileSystemScorer) Change_query(query string) {
}
fss.keep_going.Store(false)
if fss.current_worker_wait != nil {
if fss.scanner != nil {
fss.scanner.Cancel()
}
fss.current_worker_wait.Wait()
}
fss.lock()
@@ -333,11 +348,15 @@ func (fss *FileSystemScorer) Change_filter(filter Filter) {
}
fss.keep_going.Store(false)
if fss.current_worker_wait != nil {
if fss.scanner != nil {
fss.scanner.Cancel()
}
fss.current_worker_wait.Wait()
}
fss.lock()
fss.filter = filter
fss.sorted_results.Clear()
fss.scanner = nil
fss.unlock()
fss.Start()
}

View File

@@ -107,9 +107,9 @@ func (n node) ReadDir(name string) ([]fs.DirEntry, error) {
func TestChooseFilesScoring(t *testing.T) {
root := node{name: string(os.PathSeparator), children: map[string]*node{
"b": {name: "b"},
"a": {name: "a"},
"c": {name: "c"},
"b": {name: "b"},
"a": {name: "a"},
"c.png": {name: "c.png"},
"x": {name: "x", children: map[string]*node{
"1": {"1", nil}, "2": {"2", nil}, "3": {"3", nil},
"s": {"s", map[string]*node{
@@ -122,15 +122,12 @@ func TestChooseFilesScoring(t *testing.T) {
}}
wg := sync.WaitGroup{}
wg.Add(1)
s := NewFileSystemScorer(string(os.PathSeparator), "", false, func(err error, is_complete bool) {
s := NewFileSystemScorer(string(os.PathSeparator), "", Filter{}, false, func(err error, is_complete bool) {
if is_complete {
wg.Done()
}
})
sc := NewFileSystemScanner(s.root_dir, make(chan bool))
s.scanner = sc
sc.dir_reader = root.ReadDir
s.scanner.Start()
s.dir_reader = root.ReadDir
s.Start()
wg.Wait()
results := func() (ans []string) {
@@ -141,20 +138,33 @@ func TestChooseFilesScoring(t *testing.T) {
return
}
ae := func(query string, expected ...string) {
if query != "" {
wg.Add(1)
s.Change_query(query)
wg.Wait()
}
wg.Add(1)
s.Change_query(query)
wg.Wait()
if diff := cmp.Diff(expected, results()); diff != "" {
t.Fatalf("Query less scoring failed\n%s", diff)
}
}
ae("", "x", "y", "a", "b", "c", "x/s", "x/1", "x/2", "x/3", "y/3", "y/4", "y/5", "x/s/m", "x/s/n")
ae("a", "a")
ae("3", "x/3", "y/3")
ae("s", "x/s", "x/s/m", "x/s/n")
ae("sn", "x/s/n")
ae("", "x", "y", "a", "b", "c.png", "x/s", "x/1", "x/2", "x/3", "y/3", "y/4", "y/5", "x/s/m", "x/s/n")
af := func(filter string, expected ...string) {
f, _ := NewFilter(filter)
wg.Add(1)
s.Change_filter(*f)
wg.Wait()
if diff := cmp.Diff(expected, results()); diff != "" {
t.Fatalf("filter %s failed\n%s", filter, diff)
}
}
af("glob:a:A", "x", "y", "a", "x/s")
af("glob:[ab]:A", "x", "y", "a", "b", "x/s")
af("mime:image/png:A", "x", "y", "c.png", "x/s")
af("mime:image/*:A", "x", "y", "c.png", "x/s")
af("glob:*:All", "x", "y", "a", "b", "c.png", "x/s", "x/1", "x/2", "x/3", "y/3", "y/4", "y/5", "x/s/m", "x/s/n")
}
func TestSortedResults(t *testing.T) {
@@ -258,14 +268,12 @@ func run_scoring(b *testing.B, depth, breadth int, query string) {
b.StopTimer()
wg := sync.WaitGroup{}
wg.Add(1)
s := NewFileSystemScorer(string(os.PathSeparator), query, false, func(err error, is_complete bool) {
s := NewFileSystemScorer(string(os.PathSeparator), query, Filter{}, false, func(err error, is_complete bool) {
if is_complete {
wg.Done()
}
})
sc := NewFileSystemScanner(s.root_dir, make(chan bool))
s.scanner = sc
sc.dir_reader = root.ReadDir
s.dir_reader = root.ReadDir
b.StartTimer()
s.scanner.Start()
s.Start()