mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-07 17:43:53 +02:00
Let the user control recursion using the search pattern
This commit is contained in:
@@ -28,14 +28,12 @@ type ScorePattern struct {
|
||||
}
|
||||
|
||||
type State struct {
|
||||
base_dir string
|
||||
current_dir string
|
||||
select_dirs bool
|
||||
multiselect bool
|
||||
max_depth int
|
||||
exclude_patterns []*regexp.Regexp
|
||||
score_patterns []ScorePattern
|
||||
search_text string
|
||||
base_dir string
|
||||
current_dir string
|
||||
select_dirs bool
|
||||
multiselect bool
|
||||
score_patterns []ScorePattern
|
||||
search_text string
|
||||
|
||||
current_idx int
|
||||
num_of_matches_at_last_render int
|
||||
@@ -45,7 +43,6 @@ type State struct {
|
||||
func (s State) BaseDir() string { return utils.IfElse(s.base_dir == "", default_cwd, s.base_dir) }
|
||||
func (s State) SelectDirs() bool { return s.select_dirs }
|
||||
func (s State) Multiselect() bool { return s.multiselect }
|
||||
func (s State) MaxDepth() int { return utils.IfElse(s.max_depth < 1, 4, s.max_depth) }
|
||||
func (s State) String() string { return utils.Repr(s) }
|
||||
func (s State) SearchText() string { return s.search_text }
|
||||
func (s *State) SetSearchText(val string) {
|
||||
@@ -64,10 +61,9 @@ func (s *State) SetCurrentDir(val string) {
|
||||
s.current_dir = val
|
||||
}
|
||||
}
|
||||
func (s State) ExcludePatterns() []*regexp.Regexp { return s.exclude_patterns }
|
||||
func (s State) ScorePatterns() []ScorePattern { return s.score_patterns }
|
||||
func (s State) CurrentIndex() int { return s.current_idx }
|
||||
func (s *State) SetCurrentIndex(val int) { s.current_idx = max(0, val) }
|
||||
func (s State) ScorePatterns() []ScorePattern { return s.score_patterns }
|
||||
func (s State) CurrentIndex() int { return s.current_idx }
|
||||
func (s *State) SetCurrentIndex(val int) { s.current_idx = max(0, val) }
|
||||
func (s State) CurrentDir() string {
|
||||
return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir)
|
||||
}
|
||||
@@ -184,21 +180,7 @@ func add(a, b float64) float64 { return a + b }
|
||||
func div(a, b float64) float64 { return a / b }
|
||||
|
||||
func (h *Handler) set_state_from_config(conf *Config) (err error) {
|
||||
h.state = State{max_depth: int(conf.Max_depth)}
|
||||
h.state.exclude_patterns = make([]*regexp.Regexp, 0, len(conf.Exclude_directory))
|
||||
seen := map[string]*regexp.Regexp{}
|
||||
for _, x := range conf.Exclude_directory {
|
||||
if strings.HasPrefix(x, "!") {
|
||||
delete(seen, x[1:])
|
||||
} else if seen[x] == nil {
|
||||
if pat, err := regexp.Compile(x); err == nil {
|
||||
seen[x] = pat
|
||||
} else {
|
||||
return fmt.Errorf("The exclude directory pattern %#v is invalid: %w", x, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
h.state.exclude_patterns = utils.Values(seen)
|
||||
h.state = State{}
|
||||
fmap := map[string]func(float64, float64) float64{
|
||||
"*=": mult, "+=": add, "-=": sub, "/=": div}
|
||||
h.state.score_patterns = make([]ScorePattern, len(conf.Modify_score))
|
||||
|
||||
@@ -18,27 +18,6 @@ map = definition.add_map
|
||||
mma = definition.add_mouse_map
|
||||
|
||||
agr('scan', 'Scanning the filesystem')
|
||||
opt(
|
||||
'+exclude_directory',
|
||||
'^/proc$',
|
||||
add_to_default=True,
|
||||
long_text='''
|
||||
Regular expression to exclude directories. Matching directories will not be recursed into, but
|
||||
you can still or change into them to inspect their contents. Can be specified multiple times.
|
||||
Matches against the absolute path to the directory.
|
||||
If the pattern starts with :code:`!`, the :code:`!` is removed and the remaining pattern is removed from the list of patterns. This
|
||||
can be used to remove the default excluded directory patterns.
|
||||
''',
|
||||
)
|
||||
opt('+exclude_directory', '^/dev$', add_to_default=True)
|
||||
opt('+exclude_directory', '^/sys$', add_to_default=True)
|
||||
opt('+exclude_directory', '/__pycache__$', add_to_default=True)
|
||||
|
||||
opt('max_depth', '1', option_type='positive_int', long_text='''
|
||||
The maximum depth to which to scan the filesystem for matches. Using large values will slow things down considerably. The better
|
||||
approach is to use a small value and first change to the directory of interest then actually select the file of interest.
|
||||
''')
|
||||
|
||||
opt('+modify_score', r'(^|/)\.[^/]+(/|$) *= 0.5', add_to_default=True, long_text='''
|
||||
Modify the score of items matching the specified regular expression (matches against the absolute path).
|
||||
Can be used to make certain files and directories less or more prominent in the results.
|
||||
|
||||
@@ -22,13 +22,14 @@ 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][]ResultItem
|
||||
type dir_cache map[string][]os.DirEntry
|
||||
|
||||
type ScanCache struct {
|
||||
dir_entries dir_cache
|
||||
@@ -38,64 +39,29 @@ type ScanCache struct {
|
||||
matches []*ResultItem
|
||||
}
|
||||
|
||||
func (sc *ScanCache) get_cached_entries(root_dir string) (ans []ResultItem, found bool) {
|
||||
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 []ResultItem) {
|
||||
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 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) {
|
||||
func (sc *ScanCache) readdir(current_dir string) (ans []os.DirEntry) {
|
||||
var found bool
|
||||
if ans, found = sc.get_cached_entries(current_dir); !found {
|
||||
ans = scan_dir(current_dir, root_dir)
|
||||
ans, _ = os.ReadDir(current_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 sort_items_without_search_text(items []ResultItem) (ans []*ResultItem) {
|
||||
func sort_items_without_search_text(items []*ResultItem) (ans []*ResultItem) {
|
||||
type s struct {
|
||||
ltext string
|
||||
num_of_slashes int
|
||||
@@ -104,8 +70,8 @@ func sort_items_without_search_text(items []ResultItem) (ans []*ResultItem) {
|
||||
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}
|
||||
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]
|
||||
@@ -126,9 +92,9 @@ func sort_items_without_search_text(items []ResultItem) (ans []*ResultItem) {
|
||||
return utils.Map(func(s s) *ResultItem { return s.r }, d)
|
||||
}
|
||||
|
||||
func get_modified_score(r *ResultItem, score float64, score_patterns []ScorePattern) float64 {
|
||||
func get_modified_score(abspath string, score float64, score_patterns []ScorePattern) float64 {
|
||||
for _, sp := range score_patterns {
|
||||
if sp.pat.MatchString(r.abspath) {
|
||||
if sp.pat.MatchString(abspath) {
|
||||
score = sp.op(score, sp.val)
|
||||
}
|
||||
}
|
||||
@@ -145,41 +111,101 @@ func count_uppercase(s string) int {
|
||||
return count
|
||||
}
|
||||
|
||||
func (sc *ScanCache) scan(root_dir, search_text string, max_depth int, exclude_patterns []*regexp.Regexp, score_patterns []ScorePattern) (ans []*ResultItem) {
|
||||
seen := make(map[string]bool, 1024)
|
||||
matches := sc.fs_scan(root_dir, root_dir, max_depth, exclude_patterns, seen)
|
||||
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
|
||||
}
|
||||
pm := make(map[string]*ResultItem, len(ans))
|
||||
for _, x := range matches {
|
||||
nx := x
|
||||
pm[x.text] = &nx
|
||||
}
|
||||
matches2 := utils.Filter(subseq.ScoreItems(search_text, utils.Keys(pm), subseq.Options{}), func(x *subseq.Match) bool {
|
||||
return x.Score > 0
|
||||
})
|
||||
type s struct {
|
||||
r *ResultItem
|
||||
score float64
|
||||
}
|
||||
ss := utils.Map(func(m *subseq.Match) s {
|
||||
x := pm[m.Text]
|
||||
x.positions = m.Positions
|
||||
return s{x, get_modified_score(x, m.Score, score_patterns)}
|
||||
}, matches2)
|
||||
slices.SortStableFunc(ss, func(a, b s) int {
|
||||
slices.SortStableFunc(matches, func(a, b *ResultItem) int {
|
||||
ans := cmp.Compare(b.score, a.score)
|
||||
if ans == 0 {
|
||||
ans = cmp.Compare(len(a.r.text), len(b.r.text))
|
||||
ans = cmp.Compare(len(a.text), len(b.text))
|
||||
if ans == 0 {
|
||||
ans = cmp.Compare(count_uppercase(a.r.text), count_uppercase(b.r.text))
|
||||
ans = cmp.Compare(count_uppercase(a.text), count_uppercase(b.text))
|
||||
}
|
||||
}
|
||||
return ans
|
||||
})
|
||||
return utils.Map(func(s s) *ResultItem { return s.r }, ss)
|
||||
return matches
|
||||
}
|
||||
|
||||
func (h *Handler) get_results() (ans []*ResultItem, in_progress bool) {
|
||||
@@ -189,21 +215,24 @@ func (h *Handler) get_results() (ans []*ResultItem, in_progress bool) {
|
||||
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() {
|
||||
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
|
||||
root_dir := h.state.CurrentDir()
|
||||
search_text := h.state.SearchText()
|
||||
sc.root_dir = root_dir
|
||||
sc.search_text = search_text
|
||||
md, ep, sp := max(0, h.state.MaxDepth()-1), h.state.ExcludePatterns(), h.state.ScorePatterns()
|
||||
sc.root_dir = cd
|
||||
sc.search_text = st
|
||||
sp := h.state.ScorePatterns()
|
||||
go func() {
|
||||
results := sc.scan(root_dir, search_text, md, ep, sp)
|
||||
results := sc.scan(cd, st, sp)
|
||||
sc.mutex.Lock()
|
||||
defer sc.mutex.Unlock()
|
||||
if root_dir == sc.root_dir && search_text == sc.search_text {
|
||||
if cd == sc.root_dir && st == sc.search_text {
|
||||
sc.matches = results
|
||||
sc.in_progress = false
|
||||
h.lp.WakeupMainThread()
|
||||
|
||||
Reference in New Issue
Block a user