Allow modifying the score of matches using a regex

This commit is contained in:
Kovid Goyal
2025-05-23 11:31:50 +05:30
parent daea53ac6d
commit 96436f10f7
4 changed files with 114 additions and 47 deletions

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"strconv"
"strings" "strings"
"github.com/kovidgoyal/kitty/tools/cli" "github.com/kovidgoyal/kitty/tools/cli"
@@ -16,6 +17,12 @@ import (
var _ = fmt.Print var _ = fmt.Print
var debugprintln = tty.DebugPrintln var debugprintln = tty.DebugPrintln
type ScorePattern struct {
pat *regexp.Regexp
op func(float64, float64) float64
val float64
}
type State struct { type State struct {
base_dir string base_dir string
current_dir string current_dir string
@@ -23,6 +30,7 @@ type State struct {
multiselect bool multiselect bool
max_depth int max_depth int
exclude_patterns []*regexp.Regexp exclude_patterns []*regexp.Regexp
score_patterns []ScorePattern
search_text string search_text string
} }
@@ -33,6 +41,7 @@ func (s State) MaxDepth() int { return utils.IfElse(s.max_de
func (s State) String() string { return utils.Repr(s) } func (s State) String() string { return utils.Repr(s) }
func (s State) SearchText() string { return s.search_text } func (s State) SearchText() string { return s.search_text }
func (s State) ExcludePatterns() []*regexp.Regexp { return s.exclude_patterns } func (s State) ExcludePatterns() []*regexp.Regexp { return s.exclude_patterns }
func (s State) ScorePatterns() []ScorePattern { return s.score_patterns }
func (s State) CurrentDir() string { func (s State) CurrentDir() string {
return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir) return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir)
} }
@@ -114,8 +123,13 @@ func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (
return h.draw_screen() return h.draw_screen()
} }
func mult(a, b float64) float64 { return a * b }
func sub(a, b float64) float64 { return a - b }
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) { func (h *Handler) set_state_from_config(conf *Config) (err error) {
h.state.max_depth = int(conf.Max_depth) h.state = State{max_depth: int(conf.Max_depth)}
h.state.exclude_patterns = make([]*regexp.Regexp, 0, len(conf.Exclude_directory)) h.state.exclude_patterns = make([]*regexp.Regexp, 0, len(conf.Exclude_directory))
seen := map[string]*regexp.Regexp{} seen := map[string]*regexp.Regexp{}
for _, x := range conf.Exclude_directory { for _, x := range conf.Exclude_directory {
@@ -130,6 +144,25 @@ func (h *Handler) set_state_from_config(conf *Config) (err error) {
} }
} }
h.state.exclude_patterns = utils.Values(seen) h.state.exclude_patterns = utils.Values(seen)
fmap := map[string]func(float64, float64) float64{
"*=": mult, "+=": add, "-=": sub, "/=": div}
h.state.score_patterns = make([]ScorePattern, len(conf.Modify_score))
for i, x := range conf.Modify_score {
p, rest, _ := strings.Cut(x, " ")
if h.state.score_patterns[i].pat, err = regexp.Compile(p); err == nil {
op, val, _ := strings.Cut(rest, " ")
if h.state.score_patterns[i].val, err = strconv.ParseFloat(val, 64); err != nil {
return fmt.Errorf("The modify score value %#v is invalid: %w", val, err)
}
if h.state.score_patterns[i].op = fmap[op]; h.state.score_patterns[i].op == nil {
return fmt.Errorf("The modify score operator %#v is unknown", op)
}
} else {
return fmt.Errorf("The modify score pattern %#v is invalid: %w", x, err)
}
}
return return
} }

View File

@@ -24,7 +24,8 @@ opt(
add_to_default=True, add_to_default=True,
long_text=''' long_text='''
Regular expression to exclude directories. Matching directories will not be recursed into, but 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. 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 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. can be used to remove the default excluded directory patterns.
''', ''',
@@ -37,6 +38,13 @@ opt('max_depth', '4', 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 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. 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.
Can be specified multiple times. The default includes rules to reduce the score of hidden items.
The syntax is :code:`regular-expression operator value`. Supported operators are: :code:`*=, +=, -=, /=`.
''')
egr() egr()
def main(args: list[str]) -> None: def main(args: list[str]) -> None:

View File

@@ -56,7 +56,7 @@ func (h *Handler) draw_no_matches_message(in_progress bool) {
const matching_position_style = "fg=green" const matching_position_style = "fg=green"
func (h *Handler) draw_matching_result(r ResultItem) { func (h *Handler) draw_matching_result(r *ResultItem) {
icon := icon_for(r.abspath, r.dir_entry) icon := icon_for(r.abspath, r.dir_entry)
h.lp.MoveCursorHorizontally(1) h.lp.MoveCursorHorizontally(1)
p, s, _ := strings.Cut(h.lp.SprintStyled(matching_position_style, " "), " ") p, s, _ := strings.Cut(h.lp.SprintStyled(matching_position_style, " "), " ")
@@ -129,7 +129,7 @@ func icon_for(path string, x os.DirEntry) string {
return ans return ans
} }
func (h *Handler) draw_column_of_matches(matches []ResultItem, x, available_width, num_extra_matches int) { func (h *Handler) draw_column_of_matches(matches []*ResultItem, x, available_width, num_extra_matches int) {
for i, m := range matches { for i, m := range matches {
h.lp.QueueWriteString("\r") h.lp.QueueWriteString("\r")
h.lp.MoveCursorHorizontally(x) h.lp.MoveCursorHorizontally(x)
@@ -154,7 +154,7 @@ func (h *Handler) draw_column_of_matches(matches []ResultItem, x, available_widt
} }
} }
func (h *Handler) draw_list_of_results(matches []ResultItem, y, height int) { func (h *Handler) draw_list_of_results(matches []*ResultItem, y, height int) {
if len(matches) == 0 || height < 2 { if len(matches) == 0 || height < 2 {
return return
} }
@@ -180,7 +180,7 @@ func (h *Handler) draw_list_of_results(matches []ResultItem, y, height int) {
} }
} }
func (h *Handler) draw_results(y, bottom_margin int, matches []ResultItem, in_progress bool) (height int) { func (h *Handler) draw_results(y, bottom_margin int, matches []*ResultItem, in_progress bool) (height int) {
height = h.screen_size.height - y - bottom_margin height = h.screen_size.height - y - bottom_margin
h.lp.MoveCursorTo(1, 1+y) h.lp.MoveCursorTo(1, 1+y)
h.draw_frame(h.screen_size.width, height) h.draw_frame(h.screen_size.width, height)

View File

@@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"slices" "slices"
"sort"
"strings" "strings"
"sync" "sync"
@@ -33,7 +34,7 @@ type ScanCache struct {
mutex sync.Mutex mutex sync.Mutex
root_dir, search_text string root_dir, search_text string
in_progress bool in_progress bool
matches []ResultItem 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 []ResultItem, found bool) {
@@ -93,48 +94,72 @@ func (sc *ScanCache) fs_scan(root_dir, current_dir string, max_depth int, exclud
return return
} }
func (sc *ScanCache) scan(root_dir, search_text string, max_depth int, exclude_patterns []*regexp.Regexp) (ans []ResultItem) { func sort_items_without_search_text(items []ResultItem) (ans []*ResultItem) {
seen := make(map[string]bool, 1024) type s struct {
ans = sc.fs_scan(root_dir, root_dir, max_depth, exclude_patterns, seen) ltext string
if search_text == "" { num_of_slashes int
slices.SortFunc(ans, func(a, b ResultItem) int { is_dir bool
switch a.dir_entry.IsDir() { is_hidden bool
case true: r *ResultItem
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 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.Slice(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 {
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 (h *Handler) get_results() (ans []ResultItem, in_progress bool) { func get_modified_score(r *ResultItem, score float64, score_patterns []ScorePattern) float64 {
for _, sp := range score_patterns {
if sp.pat.MatchString(r.abspath) {
score = sp.op(score, sp.val)
}
}
return score
}
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)
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.SortFunc(ss, func(a, b s) int { return cmp.Compare(b.score, a.score) })
return utils.Map(func(s s) *ResultItem { return s.r }, ss)
}
func (h *Handler) get_results() (ans []*ResultItem, in_progress bool) {
sc := &h.scan_cache sc := &h.scan_cache
sc.mutex.Lock() sc.mutex.Lock()
defer sc.mutex.Unlock() defer sc.mutex.Unlock()
@@ -150,8 +175,9 @@ func (h *Handler) get_results() (ans []ResultItem, in_progress bool) {
search_text := h.state.SearchText() search_text := h.state.SearchText()
sc.root_dir = root_dir sc.root_dir = root_dir
sc.search_text = search_text sc.search_text = search_text
md, ep, sp := h.state.MaxDepth(), h.state.ExcludePatterns(), h.state.ScorePatterns()
go func() { go func() {
results := sc.scan(root_dir, search_text, h.state.MaxDepth(), h.state.ExcludePatterns()) results := sc.scan(root_dir, search_text, md, ep, sp)
sc.mutex.Lock() sc.mutex.Lock()
defer sc.mutex.Unlock() defer sc.mutex.Unlock()
if root_dir == sc.root_dir && search_text == sc.search_text { if root_dir == sc.root_dir && search_text == sc.search_text {