mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
244 lines
5.9 KiB
Go
244 lines
5.9 KiB
Go
package choose_files
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"unicode"
|
|
|
|
"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
|
|
score float64
|
|
}
|
|
|
|
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][]os.DirEntry
|
|
|
|
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 []os.DirEntry, 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 []os.DirEntry) {
|
|
sc.mutex.Lock()
|
|
defer sc.mutex.Unlock()
|
|
sc.dir_entries[root_dir] = e
|
|
}
|
|
|
|
func (sc *ScanCache) readdir(current_dir string) (ans []os.DirEntry) {
|
|
var found bool
|
|
if ans, found = sc.get_cached_entries(current_dir); !found {
|
|
ans, _ = os.ReadDir(current_dir)
|
|
sc.set_cached_entries(current_dir, ans)
|
|
}
|
|
return
|
|
}
|
|
|
|
func sort_items_without_search_text(items []*ResultItem) (ans []*ResultItem) {
|
|
type s struct {
|
|
ltext string
|
|
num_of_slashes int
|
|
is_dir bool
|
|
is_hidden bool
|
|
r *ResultItem
|
|
}
|
|
hidden_pat := regexp.MustCompile(`(^|/)\.[^/]+(/|$)`)
|
|
d := utils.Map(func(x *ResultItem) s {
|
|
return s{strings.ToLower(x.text), strings.Count(x.text, "/"), x.dir_entry.IsDir(), hidden_pat.MatchString(x.abspath), x}
|
|
}, items)
|
|
sort.SliceStable(d, func(i, j int) bool {
|
|
a, b := d[i], d[j]
|
|
if a.num_of_slashes == b.num_of_slashes {
|
|
if a.is_dir == b.is_dir {
|
|
if a.is_hidden == b.is_hidden {
|
|
if a.ltext == b.ltext {
|
|
return count_uppercase(a.r.text) < count_uppercase(b.r.text)
|
|
}
|
|
return a.ltext < b.ltext
|
|
}
|
|
return b.is_hidden
|
|
}
|
|
return a.is_dir
|
|
}
|
|
return a.num_of_slashes < b.num_of_slashes
|
|
})
|
|
return utils.Map(func(s s) *ResultItem { return s.r }, d)
|
|
}
|
|
|
|
func get_modified_score(abspath string, score float64, score_patterns []ScorePattern) float64 {
|
|
for _, sp := range score_patterns {
|
|
if sp.pat.MatchString(abspath) {
|
|
score = sp.op(score, sp.val)
|
|
}
|
|
}
|
|
return score
|
|
}
|
|
|
|
func count_uppercase(s string) int {
|
|
count := 0
|
|
for _, r := range s {
|
|
if unicode.IsUpper(r) {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
type pos_in_name struct {
|
|
name string
|
|
positions []int
|
|
}
|
|
|
|
func (r *ResultItem) finalize(positions []pos_in_name) {
|
|
buf := strings.Builder{}
|
|
buf.Grow(256)
|
|
pos := 0
|
|
for i, x := range positions {
|
|
before := buf.Len()
|
|
buf.WriteString(x.name)
|
|
if i != len(positions)-1 {
|
|
buf.WriteRune(os.PathSeparator)
|
|
}
|
|
for _, p := range x.positions {
|
|
r.positions = append(r.positions, p+pos)
|
|
}
|
|
pos += buf.Len() - before
|
|
}
|
|
r.text = buf.String()
|
|
if r.text == "" {
|
|
r.text = string(os.PathSeparator)
|
|
}
|
|
}
|
|
|
|
func (sc *ScanCache) scan_dir(abspath string, patterns []string, positions []pos_in_name, score float64) (ans []*ResultItem) {
|
|
if entries := sc.readdir(abspath); len(entries) > 0 {
|
|
npos := make([]pos_in_name, len(positions)+1)
|
|
copy(npos, positions)
|
|
names := make([]string, len(entries))
|
|
for i, e := range entries {
|
|
names[i] = e.Name()
|
|
}
|
|
var scores []*subseq.Match
|
|
pattern := ""
|
|
if len(patterns) > 0 {
|
|
pattern = patterns[0]
|
|
}
|
|
if pattern != "" {
|
|
scores = subseq.ScoreItems(pattern, names, subseq.Options{})
|
|
} else {
|
|
null := subseq.Match{}
|
|
scores = slices.Repeat([]*subseq.Match{&null}, len(names))
|
|
}
|
|
is_last := pattern == "" || len(patterns) <= 1
|
|
for i, n := range names {
|
|
child_abspath := filepath.Join(abspath, n)
|
|
if pattern == "" || scores[i].Score > 0 {
|
|
npos[len(positions)] = pos_in_name{name: n, positions: scores[i].Positions}
|
|
if is_last {
|
|
r := &ResultItem{score: score + scores[i].Score, dir_entry: entries[i], abspath: child_abspath}
|
|
r.finalize(npos)
|
|
ans = append(ans, r)
|
|
} else if entries[i].IsDir() {
|
|
ans = append(ans, sc.scan_dir(child_abspath, patterns[1:], npos, scores[i].Score+score)...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (sc *ScanCache) scan(root_dir, search_text string, score_patterns []ScorePattern) (ans []*ResultItem) {
|
|
var patterns []string
|
|
switch search_text {
|
|
case "", "/":
|
|
default:
|
|
patterns = strings.Split(filepath.Clean(search_text), string(os.PathSeparator))
|
|
}
|
|
if strings.HasPrefix(search_text, "/") {
|
|
root_dir = "/"
|
|
if len(patterns) > 0 {
|
|
patterns = patterns[1:]
|
|
}
|
|
}
|
|
matches := sc.scan_dir(root_dir, patterns, nil, 0)
|
|
for _, ri := range matches {
|
|
ri.score = get_modified_score(ri.abspath, ri.score, score_patterns)
|
|
}
|
|
if search_text == "" {
|
|
ans = sort_items_without_search_text(matches)
|
|
return
|
|
}
|
|
slices.SortStableFunc(matches, func(a, b *ResultItem) int {
|
|
ans := cmp.Compare(b.score, a.score)
|
|
if ans == 0 {
|
|
ans = cmp.Compare(len(a.text), len(b.text))
|
|
if ans == 0 {
|
|
ans = cmp.Compare(count_uppercase(a.text), count_uppercase(b.text))
|
|
}
|
|
}
|
|
return ans
|
|
})
|
|
return matches
|
|
}
|
|
|
|
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)
|
|
}
|
|
cd := h.state.CurrentDir()
|
|
st := h.state.SearchText()
|
|
if st != "" {
|
|
st = filepath.Clean(st)
|
|
}
|
|
if sc.root_dir == cd && sc.search_text == st {
|
|
return sc.matches, sc.in_progress
|
|
}
|
|
sc.in_progress = true
|
|
sc.matches = nil
|
|
sc.root_dir = cd
|
|
sc.search_text = st
|
|
sp := h.state.ScorePatterns()
|
|
go func() {
|
|
defer h.lp.RecoverFromPanicInGoRoutine()
|
|
results := sc.scan(cd, st, sp)
|
|
sc.mutex.Lock()
|
|
defer sc.mutex.Unlock()
|
|
if cd == sc.root_dir && st == sc.search_text {
|
|
sc.matches = results
|
|
sc.in_progress = false
|
|
h.lp.WakeupMainThread()
|
|
}
|
|
}()
|
|
return sc.matches, sc.in_progress
|
|
}
|