Skip to content

Commit 96436f1

Browse files
committed
Allow modifying the score of matches using a regex
1 parent daea53a commit 96436f1

File tree

4 files changed

+111
-44
lines changed

4 files changed

+111
-44
lines changed

kittens/choose_files/main.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"regexp"
7+
"strconv"
78
"strings"
89

910
"github.com/kovidgoyal/kitty/tools/cli"
@@ -16,13 +17,20 @@ import (
1617
var _ = fmt.Print
1718
var debugprintln = tty.DebugPrintln
1819

20+
type ScorePattern struct {
21+
pat *regexp.Regexp
22+
op func(float64, float64) float64
23+
val float64
24+
}
25+
1926
type State struct {
2027
base_dir string
2128
current_dir string
2229
select_dirs bool
2330
multiselect bool
2431
max_depth int
2532
exclude_patterns []*regexp.Regexp
33+
score_patterns []ScorePattern
2634
search_text string
2735
}
2836

@@ -33,6 +41,7 @@ func (s State) MaxDepth() int { return utils.IfElse(s.max_de
3341
func (s State) String() string { return utils.Repr(s) }
3442
func (s State) SearchText() string { return s.search_text }
3543
func (s State) ExcludePatterns() []*regexp.Regexp { return s.exclude_patterns }
44+
func (s State) ScorePatterns() []ScorePattern { return s.score_patterns }
3645
func (s State) CurrentDir() string {
3746
return utils.IfElse(s.current_dir == "", s.BaseDir(), s.current_dir)
3847
}
@@ -114,8 +123,13 @@ func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (
114123
return h.draw_screen()
115124
}
116125

126+
func mult(a, b float64) float64 { return a * b }
127+
func sub(a, b float64) float64 { return a - b }
128+
func add(a, b float64) float64 { return a + b }
129+
func div(a, b float64) float64 { return a / b }
130+
117131
func (h *Handler) set_state_from_config(conf *Config) (err error) {
118-
h.state.max_depth = int(conf.Max_depth)
132+
h.state = State{max_depth: int(conf.Max_depth)}
119133
h.state.exclude_patterns = make([]*regexp.Regexp, 0, len(conf.Exclude_directory))
120134
seen := map[string]*regexp.Regexp{}
121135
for _, x := range conf.Exclude_directory {
@@ -130,6 +144,25 @@ func (h *Handler) set_state_from_config(conf *Config) (err error) {
130144
}
131145
}
132146
h.state.exclude_patterns = utils.Values(seen)
147+
fmap := map[string]func(float64, float64) float64{
148+
"*=": mult, "+=": add, "-=": sub, "/=": div}
149+
h.state.score_patterns = make([]ScorePattern, len(conf.Modify_score))
150+
for i, x := range conf.Modify_score {
151+
p, rest, _ := strings.Cut(x, " ")
152+
if h.state.score_patterns[i].pat, err = regexp.Compile(p); err == nil {
153+
op, val, _ := strings.Cut(rest, " ")
154+
if h.state.score_patterns[i].val, err = strconv.ParseFloat(val, 64); err != nil {
155+
return fmt.Errorf("The modify score value %#v is invalid: %w", val, err)
156+
}
157+
if h.state.score_patterns[i].op = fmap[op]; h.state.score_patterns[i].op == nil {
158+
return fmt.Errorf("The modify score operator %#v is unknown", op)
159+
}
160+
161+
} else {
162+
return fmt.Errorf("The modify score pattern %#v is invalid: %w", x, err)
163+
}
164+
165+
}
133166
return
134167
}
135168

kittens/choose_files/main.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
add_to_default=True,
2525
long_text='''
2626
Regular expression to exclude directories. Matching directories will not be recursed into, but
27-
you can still or change into them to inspect their contents. Can be specified multiple times. Matches against the absolute path to the directory.
27+
you can still or change into them to inspect their contents. Can be specified multiple times.
28+
Matches against the absolute path to the directory.
2829
If the pattern starts with :code:`!`, the :code:`!` is removed and the remaining pattern is removed from the list of patterns. This
2930
can be used to remove the default excluded directory patterns.
3031
''',
@@ -37,6 +38,13 @@
3738
The maximum depth to which to scan the filesystem for matches. Using large values will slow things down considerably. The better
3839
approach is to use a small value and first change to the directory of interest then actually select the file of interest.
3940
''')
41+
42+
opt('+modify_score', r'(^|/)\.[^/]+(/|$) *= 0.5', add_to_default=True, long_text='''
43+
Modify the score of items matching the specified regular expression (matches against the absolute path).
44+
Can be used to make certain files and directories less or more prominent in the results.
45+
Can be specified multiple times. The default includes rules to reduce the score of hidden items.
46+
The syntax is :code:`regular-expression operator value`. Supported operators are: :code:`*=, +=, -=, /=`.
47+
''')
4048
egr()
4149

4250
def main(args: list[str]) -> None:

kittens/choose_files/results.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (h *Handler) draw_no_matches_message(in_progress bool) {
5656

5757
const matching_position_style = "fg=green"
5858

59-
func (h *Handler) draw_matching_result(r ResultItem) {
59+
func (h *Handler) draw_matching_result(r *ResultItem) {
6060
icon := icon_for(r.abspath, r.dir_entry)
6161
h.lp.MoveCursorHorizontally(1)
6262
p, s, _ := strings.Cut(h.lp.SprintStyled(matching_position_style, " "), " ")
@@ -129,7 +129,7 @@ func icon_for(path string, x os.DirEntry) string {
129129
return ans
130130
}
131131

132-
func (h *Handler) draw_column_of_matches(matches []ResultItem, x, available_width, num_extra_matches int) {
132+
func (h *Handler) draw_column_of_matches(matches []*ResultItem, x, available_width, num_extra_matches int) {
133133
for i, m := range matches {
134134
h.lp.QueueWriteString("\r")
135135
h.lp.MoveCursorHorizontally(x)
@@ -154,7 +154,7 @@ func (h *Handler) draw_column_of_matches(matches []ResultItem, x, available_widt
154154
}
155155
}
156156

157-
func (h *Handler) draw_list_of_results(matches []ResultItem, y, height int) {
157+
func (h *Handler) draw_list_of_results(matches []*ResultItem, y, height int) {
158158
if len(matches) == 0 || height < 2 {
159159
return
160160
}
@@ -180,7 +180,7 @@ func (h *Handler) draw_list_of_results(matches []ResultItem, y, height int) {
180180
}
181181
}
182182

183-
func (h *Handler) draw_results(y, bottom_margin int, matches []ResultItem, in_progress bool) (height int) {
183+
func (h *Handler) draw_results(y, bottom_margin int, matches []*ResultItem, in_progress bool) (height int) {
184184
height = h.screen_size.height - y - bottom_margin
185185
h.lp.MoveCursorTo(1, 1+y)
186186
h.draw_frame(h.screen_size.width, height)

kittens/choose_files/scan.go

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"path/filepath"
88
"regexp"
99
"slices"
10+
"sort"
1011
"strings"
1112
"sync"
1213

@@ -33,7 +34,7 @@ type ScanCache struct {
3334
mutex sync.Mutex
3435
root_dir, search_text string
3536
in_progress bool
36-
matches []ResultItem
37+
matches []*ResultItem
3738
}
3839

3940
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
9394
return
9495
}
9596

96-
func (sc *ScanCache) scan(root_dir, search_text string, max_depth int, exclude_patterns []*regexp.Regexp) (ans []ResultItem) {
97-
seen := make(map[string]bool, 1024)
98-
ans = sc.fs_scan(root_dir, root_dir, max_depth, exclude_patterns, seen)
99-
if search_text == "" {
100-
slices.SortFunc(ans, func(a, b ResultItem) int {
101-
switch a.dir_entry.IsDir() {
102-
case true:
103-
switch b.dir_entry.IsDir() {
104-
case true:
105-
return strings.Compare(strings.ToLower(a.text), strings.ToLower(b.text))
106-
case false:
107-
return -1
108-
}
109-
case false:
110-
switch b.dir_entry.IsDir() {
111-
case true:
112-
return 1
113-
case false:
114-
return strings.Compare(strings.ToLower(a.text), strings.ToLower(b.text))
97+
func sort_items_without_search_text(items []ResultItem) (ans []*ResultItem) {
98+
type s struct {
99+
ltext string
100+
num_of_slashes int
101+
is_dir bool
102+
is_hidden bool
103+
r *ResultItem
104+
}
105+
hidden_pat := regexp.MustCompile(`(^|/)\.[^/]+(/|$)`)
106+
d := utils.Map(func(x ResultItem) s {
107+
return s{strings.ToLower(x.text), strings.Count(x.text, "/"), x.dir_entry.IsDir(), hidden_pat.MatchString(x.abspath), &x}
108+
}, items)
109+
sort.Slice(d, func(i, j int) bool {
110+
a, b := d[i], d[j]
111+
if a.num_of_slashes == b.num_of_slashes {
112+
if a.is_dir == b.is_dir {
113+
if a.is_hidden == b.is_hidden {
114+
return a.ltext < b.ltext
115115
}
116+
return b.is_hidden
116117
}
117-
return 0
118-
})
119-
} else {
120-
pm := make(map[string]ResultItem, len(ans))
121-
for _, x := range ans {
122-
pm[x.text] = x
118+
return a.is_dir
119+
}
120+
return a.num_of_slashes < b.num_of_slashes
121+
})
122+
return utils.Map(func(s s) *ResultItem { return s.r }, d)
123+
}
124+
125+
func get_modified_score(r *ResultItem, score float64, score_patterns []ScorePattern) float64 {
126+
for _, sp := range score_patterns {
127+
if sp.pat.MatchString(r.abspath) {
128+
score = sp.op(score, sp.val)
123129
}
124-
matches := utils.Filter(subseq.ScoreItems(search_text, utils.Keys(pm), subseq.Options{}), func(x *subseq.Match) bool {
125-
return x.Score > 0
126-
})
127-
slices.SortFunc(matches, func(a, b *subseq.Match) int { return cmp.Compare(b.Score, a.Score) })
128-
ans = utils.Map(func(m *subseq.Match) ResultItem {
129-
x := pm[m.Text]
130-
x.positions = m.Positions
131-
return x
132-
}, matches)
133130
}
134-
return ans
131+
return score
132+
}
133+
134+
func (sc *ScanCache) scan(root_dir, search_text string, max_depth int, exclude_patterns []*regexp.Regexp, score_patterns []ScorePattern) (ans []*ResultItem) {
135+
seen := make(map[string]bool, 1024)
136+
matches := sc.fs_scan(root_dir, root_dir, max_depth, exclude_patterns, seen)
137+
if search_text == "" {
138+
ans = sort_items_without_search_text(matches)
139+
return
140+
}
141+
pm := make(map[string]*ResultItem, len(ans))
142+
for _, x := range matches {
143+
nx := x
144+
pm[x.text] = &nx
145+
}
146+
matches2 := utils.Filter(subseq.ScoreItems(search_text, utils.Keys(pm), subseq.Options{}), func(x *subseq.Match) bool {
147+
return x.Score > 0
148+
})
149+
type s struct {
150+
r *ResultItem
151+
score float64
152+
}
153+
ss := utils.Map(func(m *subseq.Match) s {
154+
x := pm[m.Text]
155+
x.positions = m.Positions
156+
return s{x, get_modified_score(x, m.Score, score_patterns)}
157+
}, matches2)
158+
slices.SortFunc(ss, func(a, b s) int { return cmp.Compare(b.score, a.score) })
159+
return utils.Map(func(s s) *ResultItem { return s.r }, ss)
135160
}
136161

137-
func (h *Handler) get_results() (ans []ResultItem, in_progress bool) {
162+
func (h *Handler) get_results() (ans []*ResultItem, in_progress bool) {
138163
sc := &h.scan_cache
139164
sc.mutex.Lock()
140165
defer sc.mutex.Unlock()
@@ -150,8 +175,9 @@ func (h *Handler) get_results() (ans []ResultItem, in_progress bool) {
150175
search_text := h.state.SearchText()
151176
sc.root_dir = root_dir
152177
sc.search_text = search_text
178+
md, ep, sp := h.state.MaxDepth(), h.state.ExcludePatterns(), h.state.ScorePatterns()
153179
go func() {
154-
results := sc.scan(root_dir, search_text, h.state.MaxDepth(), h.state.ExcludePatterns())
180+
results := sc.scan(root_dir, search_text, md, ep, sp)
155181
sc.mutex.Lock()
156182
defer sc.mutex.Unlock()
157183
if root_dir == sc.root_dir && search_text == sc.search_text {

0 commit comments

Comments
 (0)