Allow modifying the score of matches using a regex
This commit is contained in:
parent
daea53ac6d
commit
96436f10f7
4 changed files with 114 additions and 47 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/cli"
|
||||
|
|
@ -16,6 +17,12 @@ import (
|
|||
var _ = fmt.Print
|
||||
var debugprintln = tty.DebugPrintln
|
||||
|
||||
type ScorePattern struct {
|
||||
pat *regexp.Regexp
|
||||
op func(float64, float64) float64
|
||||
val float64
|
||||
}
|
||||
|
||||
type State struct {
|
||||
base_dir string
|
||||
current_dir string
|
||||
|
|
@ -23,6 +30,7 @@ type State struct {
|
|||
multiselect bool
|
||||
max_depth int
|
||||
exclude_patterns []*regexp.Regexp
|
||||
score_patterns []ScorePattern
|
||||
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) SearchText() string { return s.search_text }
|
||||
func (s State) ExcludePatterns() []*regexp.Regexp { return s.exclude_patterns }
|
||||
func (s State) ScorePatterns() []ScorePattern { return s.score_patterns }
|
||||
func (s State) CurrentDir() string {
|
||||
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()
|
||||
}
|
||||
|
||||
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) {
|
||||
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))
|
||||
seen := map[string]*regexp.Regexp{}
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
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.
|
||||
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.
|
||||
''',
|
||||
|
|
@ -37,6 +38,13 @@
|
|||
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.
|
||||
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()
|
||||
|
||||
def main(args: list[str]) -> None:
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ func (h *Handler) draw_no_matches_message(in_progress bool) {
|
|||
|
||||
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)
|
||||
h.lp.MoveCursorHorizontally(1)
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
h.lp.QueueWriteString("\r")
|
||||
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 {
|
||||
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
|
||||
h.lp.MoveCursorTo(1, 1+y)
|
||||
h.draw_frame(h.screen_size.width, height)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ type ScanCache struct {
|
|||
mutex sync.Mutex
|
||||
root_dir, search_text string
|
||||
in_progress bool
|
||||
matches []ResultItem
|
||||
matches []*ResultItem
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
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.mutex.Lock()
|
||||
defer sc.mutex.Unlock()
|
||||
|
|
@ -150,8 +175,9 @@ func (h *Handler) get_results() (ans []ResultItem, in_progress bool) {
|
|||
search_text := h.state.SearchText()
|
||||
sc.root_dir = root_dir
|
||||
sc.search_text = search_text
|
||||
md, ep, sp := h.state.MaxDepth(), h.state.ExcludePatterns(), h.state.ScorePatterns()
|
||||
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()
|
||||
defer sc.mutex.Unlock()
|
||||
if root_dir == sc.root_dir && search_text == sc.search_text {
|
||||
|
|
|
|||
Loading…
Reference in a new issue