mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-17 22:14:53 +02:00
165 lines
4.3 KiB
Go
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
|
|
}
|