Files
kitty/kittens/choose_files/scan.go
2025-05-22 23:06:28 +05:30

165 lines
4.3 KiB
Go

package choose_files
import (
"cmp"
"fmt"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"sync"
"github.com/kovidgoyal/kitty/tools/tui/subseq"
"github.com/kovidgoyal/kitty/tools/utils"
)
var _ = fmt.Print
type ResultItem struct {
text, abspath string
dir_entry os.DirEntry
positions []int // may be nil
}
func (r ResultItem) String() string {
return fmt.Sprintf("{text: %#v, abspath: %#v, is_dir: %v, positions: %#v}", r.text, r.abspath, r.dir_entry.IsDir(), r.positions)
}
type dir_cache map[string][]ResultItem
type ScanCache struct {
dir_entries dir_cache
mutex sync.Mutex
root_dir, search_text string
in_progress bool
matches []ResultItem
}
func (sc *ScanCache) get_cached_entries(root_dir string) (ans []ResultItem, found bool) {
sc.mutex.Lock()
defer sc.mutex.Unlock()
ans, found = sc.dir_entries[root_dir]
return
}
func (sc *ScanCache) set_cached_entries(root_dir string, e []ResultItem) {
sc.mutex.Lock()
defer sc.mutex.Unlock()
sc.dir_entries[root_dir] = e
}
func scan_dir(path, root_dir string) []ResultItem {
if ans, err := os.ReadDir(path); err == nil {
if rel, err := filepath.Rel(root_dir, path); err == nil {
return utils.Map(func(x os.DirEntry) ResultItem {
path := filepath.Join(path, x.Name())
r := filepath.Join(rel, x.Name())
if root_dir == "/" {
r = "/" + r
}
return ResultItem{dir_entry: x, abspath: path, text: r}
}, ans)
}
}
return []ResultItem{}
}
func is_excluded(path string, exclude_patterns []*regexp.Regexp) bool {
for _, pattern := range exclude_patterns {
if pattern.MatchString(path) {
return true
}
}
return false
}
func (sc *ScanCache) fs_scan(root_dir, current_dir string, max_depth int, exclude_patterns []*regexp.Regexp, seen map[string]bool) (ans []ResultItem) {
var found bool
if ans, found = sc.get_cached_entries(current_dir); !found {
ans = scan_dir(current_dir, root_dir)
sc.set_cached_entries(current_dir, ans)
}
ans = slices.Clone(ans)
// now recurse into directories
if max_depth > 0 {
for _, x := range ans {
if x.dir_entry.IsDir() && !seen[x.abspath] && !is_excluded(x.abspath, exclude_patterns) {
seen[x.abspath] = true
ans = append(ans, sc.fs_scan(root_dir, x.abspath, max_depth-1, exclude_patterns, seen)...)
}
}
}
return
}
func (sc *ScanCache) scan(root_dir, search_text string, max_depth int, exclude_patterns []*regexp.Regexp) (ans []ResultItem) {
seen := make(map[string]bool, 1024)
ans = sc.fs_scan(root_dir, root_dir, max_depth, exclude_patterns, seen)
if search_text == "" {
slices.SortFunc(ans, func(a, b ResultItem) int {
switch a.dir_entry.IsDir() {
case true:
switch b.dir_entry.IsDir() {
case true:
return strings.Compare(strings.ToLower(a.text), strings.ToLower(b.text))
case false:
return -1
}
case false:
switch b.dir_entry.IsDir() {
case true:
return 1
case false:
return strings.Compare(strings.ToLower(a.text), strings.ToLower(b.text))
}
}
return 0
})
} else {
pm := make(map[string]ResultItem, len(ans))
for _, x := range ans {
pm[x.text] = x
}
matches := utils.Filter(subseq.ScoreItems(search_text, utils.Keys(pm), subseq.Options{}), func(x *subseq.Match) bool {
return x.Score > 0
})
slices.SortFunc(matches, func(a, b *subseq.Match) int { return cmp.Compare(b.Score, a.Score) })
ans = utils.Map(func(m *subseq.Match) ResultItem {
x := pm[m.Text]
x.positions = m.Positions
return x
}, matches)
}
return ans
}
func (h *Handler) get_results() (ans []ResultItem, in_progress bool) {
sc := &h.scan_cache
sc.mutex.Lock()
defer sc.mutex.Unlock()
if sc.dir_entries == nil {
sc.dir_entries = make(dir_cache, 512)
}
if sc.root_dir == h.state.CurrentDir() && sc.search_text == h.state.SearchText() {
return sc.matches, sc.in_progress
}
sc.in_progress = true
sc.matches = nil
root_dir := h.state.CurrentDir()
search_text := h.state.SearchText()
sc.root_dir = root_dir
sc.search_text = search_text
go func() {
results := sc.scan(root_dir, search_text, h.state.MaxDepth(), h.state.ExcludePatterns())
sc.mutex.Lock()
defer sc.mutex.Unlock()
if root_dir == sc.root_dir && search_text == sc.search_text {
sc.matches = results
sc.in_progress = false
h.lp.WakeupMainThread()
}
}()
return sc.matches, sc.in_progress
}