Let the user control recursion using the search pattern

This commit is contained in:
Kovid Goyal
2025-06-01 12:23:18 +05:30
parent 219a6fbd3e
commit 8d3b3f527c
3 changed files with 116 additions and 126 deletions

View File

@@ -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))

View File

@@ -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.

View File

@@ -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()