More work on choose files kitten

This commit is contained in:
Kovid Goyal
2025-06-19 11:24:21 +05:30
parent bbdee7efdf
commit 9de6bfbbd7
4 changed files with 70 additions and 163 deletions

View File

@@ -201,6 +201,7 @@ def make_bitfields() -> None:
) )
mb('tools/vt', 'CellColor', 'is_idx 1', 'red 8', 'green 8', 'blue 8') mb('tools/vt', 'CellColor', 'is_idx 1', 'red 8', 'green 8', 'blue 8')
mb('tools/vt', 'LineAttrs', 'prompt_kind 2',) mb('tools/vt', 'LineAttrs', 'prompt_kind 2',)
mb('kittens/choose_files', 'CombinedScore', 'score 32', 'index 32')
# }}} # }}}
# Completions {{{ # Completions {{{

View File

@@ -4,9 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"slices" "slices"
"strconv"
"strings" "strings"
"github.com/kovidgoyal/kitty/tools/cli" "github.com/kovidgoyal/kitty/tools/cli"
@@ -22,12 +20,6 @@ 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 Screen int type Screen int
const ( const (
@@ -104,7 +96,6 @@ type State struct {
current_dir string current_dir string
select_dirs bool select_dirs bool
multiselect bool multiselect bool
score_patterns []ScorePattern
search_text string search_text string
mode Mode mode Mode
suggested_save_file_name string suggested_save_file_name string
@@ -140,9 +131,8 @@ func (s *State) SetCurrentDir(val string) {
s.current_dir = val s.current_dir = val
} }
} }
func (s State) ScorePatterns() []ScorePattern { return s.score_patterns } func (s State) CurrentIndex() int { return s.current_idx }
func (s State) CurrentIndex() int { return s.current_idx } func (s *State) SetCurrentIndex(val int) { s.current_idx = max(0, val) }
func (s *State) SetCurrentIndex(val int) { s.current_idx = max(0, val) }
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)
} }
@@ -365,32 +355,8 @@ func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (
return return
} }
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, opts *Options) (err error) { func (h *Handler) set_state_from_config(conf *Config, opts *Options) (err error) {
h.state = State{} h.state = State{}
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)
}
}
switch opts.Mode { switch opts.Mode {
case "file": case "file":
h.state.mode = SELECT_SINGLE_FILE h.state.mode = SELECT_SINGLE_FILE

View File

@@ -17,17 +17,6 @@ opt = definition.add_option
map = definition.add_map map = definition.add_map
mma = definition.add_mouse_map mma = definition.add_mouse_map
agr('scan', 'Scanning the filesystem')
opt('+modify_score', r'(^|/)\.[^/]+(/|$) *= 0.1', 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 and
items in some well known cache folder names. Only applies when some actual search expression is provided.
The syntax is :code:`regular-expression operator value`. Supported operators are: :code:`*=, +=, -=, /=`.
''')
opt('+modify_score', '(^|/)__pycache__(/|$) *= 0.1', add_to_default=True)
egr()
def main(args: list[str]) -> None: def main(args: list[str]) -> None:
raise SystemExit('This must be run as kitten choose-files') raise SystemExit('This must be run as kitten choose-files')

View File

@@ -1,8 +1,10 @@
package choose_files package choose_files
import ( import (
"cmp"
"fmt" "fmt"
"io/fs" "io/fs"
"math"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
@@ -10,8 +12,6 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode"
"unsafe"
"github.com/kovidgoyal/kitty/tools/fzf" "github.com/kovidgoyal/kitty/tools/fzf"
"github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils"
@@ -20,11 +20,10 @@ import (
var _ = fmt.Print var _ = fmt.Print
type ResultItem struct { type ResultItem struct {
text, ltext, abspath string text, abspath string
ftype fs.FileMode ftype fs.FileMode
positions []int // may be nil positions []int // may be nil
score float64 score CombinedScore
positions_sorted bool
} }
func (r ResultItem) String() string { func (r ResultItem) String() string {
@@ -32,34 +31,12 @@ func (r ResultItem) String() string {
} }
func (r *ResultItem) sorted_positions() []int { func (r *ResultItem) sorted_positions() []int {
if !r.positions_sorted { if len(r.positions) > 1 {
r.positions_sorted = true sort.Ints(r.positions)
if len(r.positions) > 1 {
sort.Ints(r.positions)
}
} }
return r.positions return r.positions
} }
func get_modified_score(abspath string, score float64, score_patterns []ScorePattern) float64 {
for _, sp := range score_patterns {
if sp.pat.MatchString(abspath) {
score = sp.op(score, sp.val)
}
}
return score
}
func count_uppercase(s string) int {
count := 0
for _, r := range s {
if unicode.IsUpper(r) {
count++
}
}
return count
}
type ScanRequest struct { type ScanRequest struct {
root_dir string root_dir string
} }
@@ -85,7 +62,6 @@ type ScoreResult struct {
type Settings interface { type Settings interface {
OnlyDirs() bool OnlyDirs() bool
ScorePatterns() []ScorePattern
CurrentDir() string CurrentDir() string
SearchText() string SearchText() string
} }
@@ -139,7 +115,7 @@ func (m *ResultManager) unlock() {
m.mutex.Unlock() m.mutex.Unlock()
} }
func (m *ResultManager) scan(dir, root_dir string, level int) (err error) { func (m *ResultManager) scan(dir, root_dir string, level int, idx *uint32) (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
st, qerr := utils.Format_stacktrace_on_panic(r) st, qerr := utils.Format_stacktrace_on_panic(r)
@@ -153,15 +129,20 @@ func (m *ResultManager) scan(dir, root_dir string, level int) (err error) {
} }
return nil return nil
} }
m.scan_results <- ScanResult{root_dir: root_dir, items: utils.Map(func(x os.DirEntry) ResultItem { ritems := utils.Map(func(x os.DirEntry) ResultItem {
return ResultItem{abspath: filepath.Join(dir, x.Name()), ftype: x.Type()} ans := ResultItem{abspath: filepath.Join(dir, x.Name()), text: strings.ToLower(x.Name()), ftype: x.Type()}
}, items)} ans.score.Set_index(*idx)
for _, x := range items { *idx = *idx + 1
if x.IsDir() { return ans
}, items)
slices.SortFunc(ritems, func(a, b ResultItem) int { return cmp.Compare(a.text, b.text) })
m.scan_results <- ScanResult{root_dir: root_dir, items: ritems}
for _, x := range ritems {
if x.ftype.IsDir() {
if !m.is_root_dir_current(root_dir) { if !m.is_root_dir_current(root_dir) {
return return
} }
if err = m.scan(filepath.Join(dir, x.Name()), root_dir, level+1); err != nil { if err = m.scan(x.abspath, root_dir, level+1, idx); err != nil {
return return
} }
} }
@@ -172,7 +153,8 @@ func (m *ResultManager) scan(dir, root_dir string, level int) (err error) {
func (m *ResultManager) scan_worker() { func (m *ResultManager) scan_worker() {
for r := range m.scan_requests { for r := range m.scan_requests {
st := time.Now() st := time.Now()
if err := m.scan(r.root_dir, r.root_dir, 0); err == nil { var idx uint32
if err := m.scan(r.root_dir, r.root_dir, 0, &idx); err == nil {
m.scan_results <- ScanResult{root_dir: r.root_dir, is_finished: true} m.scan_results <- ScanResult{root_dir: r.root_dir, is_finished: true}
} }
debugprintln(111111111, time.Now().Sub(st), len(m.results_for_current_root_dir)) debugprintln(111111111, time.Now().Sub(st), len(m.results_for_current_root_dir))
@@ -186,7 +168,7 @@ func (m *ResultManager) create_score_query(items []ResultItem, is_finished bool)
if err != nil { if err != nil {
text = r.abspath text = r.abspath
} }
return ResultItem{abspath: r.abspath, text: text, ltext: strings.ToLower(text), ftype: r.ftype} return ResultItem{abspath: r.abspath, text: text, ftype: r.ftype}
}, items), is_last_for_current_root_dir: is_finished} }, items), is_last_for_current_root_dir: is_finished}
} }
@@ -217,69 +199,39 @@ func (m *ResultManager) scan_result_handler() {
} }
} }
func (r *ResultItem) SetScoreResult(x fzf.Result) {
r.positions = x.Positions
r.score.Set_score(uint32(math.MaxUint32 - x.Score))
}
func (m *ResultManager) score(r ScoreRequest) (err error) { func (m *ResultManager) score(r ScoreRequest) (err error) {
items := r.items items := r.items
m.lock()
only_dirs := m.settings.OnlyDirs() only_dirs := m.settings.OnlyDirs()
sp := m.settings.ScorePatterns()
m.unlock()
if only_dirs { if only_dirs {
items = utils.Filter(items, func(r ResultItem) bool { return r.ftype.IsDir() }) items = utils.Filter(items, func(r ResultItem) bool { return r.ftype.IsDir() })
} }
res := ScoreResult{query: r.query, items: items, root_dir: r.root_dir, is_last_for_current_root_dir: r.is_last_for_current_root_dir} res := ScoreResult{
query: r.query, items: items, root_dir: r.root_dir, is_last_for_current_root_dir: r.is_last_for_current_root_dir,
}
if r.query != "" { if r.query != "" {
var r []fzf.Result var scores []fzf.Result
if r, err = m.scorer.ScoreWithCache(utils.Map(func(r ResultItem) string { return r.text }, items), res.query); err != nil { if scores, err = m.scorer.ScoreWithCache(utils.Map(func(r ResultItem) string { return r.text }, items), res.query); err != nil {
return return
} }
for i, x := range r { matched_items := make([]ResultItem, 0, len(items))
items[i].positions = x.Positions for i, x := range scores {
items[i].score = get_modified_score(items[i].abspath, float64(x.Score), sp) if x.Score > 0 {
matched_items = append(matched_items, items[i])
item := &matched_items[len(matched_items)-1]
item.SetScoreResult(x)
}
} }
items = utils.Filter(items, func(r ResultItem) bool { return r.score > 0 }) items = matched_items
} }
m.score_results <- res m.score_results <- res
return return
} }
func int_cmp(a, b int) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
func str_cmp(a, b string) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
func float_cmp(a, b float64) int { // deliberately doesnt handle NaN
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
func bool_as_int(b bool) int {
return *(*int)(unsafe.Pointer(&b))
}
func bool_cmp(a, b bool) int {
return bool_as_int(a) - bool_as_int(b)
}
func (m *ResultManager) score_worker() { func (m *ResultManager) score_worker() {
for r := range m.score_queries { for r := range m.score_queries {
if m.is_query_current(r.query, r.root_dir) { if m.is_query_current(r.query, r.root_dir) {
@@ -290,33 +242,11 @@ func (m *ResultManager) score_worker() {
} }
} }
func cmp_with_score(a, b ResultItem) (ans int) { func merge_sorted_slices(a, b []ResultItem) []ResultItem {
ans = float_cmp(b.score, a.score)
if ans == 0 {
ans = int_cmp(len(a.text), len(b.text))
if ans == 0 {
ans = int_cmp(count_uppercase(a.text), count_uppercase(b.text))
}
}
return
}
func cmp_without_score(a, b ResultItem) (ans int) {
ans = bool_cmp(a.ftype.IsDir(), b.ftype.IsDir())
if ans == 0 {
ans = str_cmp(a.ltext, b.ltext)
if ans == 0 {
ans = int_cmp(count_uppercase(a.text), count_uppercase(b.text))
}
}
return
}
func merge_sorted_slices(a, b []ResultItem, cmp func(a, b ResultItem) int) []ResultItem {
result := make([]ResultItem, 0, len(a)+len(b)) result := make([]ResultItem, 0, len(a)+len(b))
i, j := 0, 0 i, j := 0, 0
for i < len(a) && j < len(b) { for i < len(a) && j < len(b) {
if cmp(a[i], b[j]) <= 0 { if a[i].score <= b[j].score {
result = append(result, a[i]) result = append(result, a[i])
i++ i++
} else { } else {
@@ -328,9 +258,30 @@ func merge_sorted_slices(a, b []ResultItem, cmp func(a, b ResultItem) int) []Res
return append(result, b[j:]...) return append(result, b[j:]...)
} }
type ByRelevance []ResultItem
func (a ByRelevance) Len() int {
return len(a)
}
func (a ByRelevance) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevance) Less(i, j int) bool {
return a[i].score < a[j].score
}
func (m *ResultManager) add_score_results(r ScoreResult) (err error) { func (m *ResultManager) add_score_results(r ScoreResult) (err error) {
cmp := utils.IfElse(r.query == "", cmp_without_score, cmp_with_score) min_score, max_score := CombinedScore(math.MaxUint64), CombinedScore(0)
slices.SortStableFunc(r.items, cmp) if len(r.items) > 0 {
sort.Sort(ByRelevance(r.items))
min_score = r.items[0].score
max_score = r.items[len(r.items)-1].score
}
_, _ = min_score, max_score
// renderable_results := merge_sorted_slices(m.renderable_results, r.items)
renderable_results := append(m.renderable_results, r.items...)
m.lock() m.lock()
defer func() { defer func() {
m.unlock() m.unlock()
@@ -339,7 +290,7 @@ func (m *ResultManager) add_score_results(r ScoreResult) (err error) {
err = fmt.Errorf("%w\n%s", qerr, st) err = fmt.Errorf("%w\n%s", qerr, st)
} }
}() }()
m.renderable_results = merge_sorted_slices(m.renderable_results, r.items, cmp) m.renderable_results = renderable_results
if r.is_last_for_current_root_dir { if r.is_last_for_current_root_dir {
m.current_query_scoring_complete = true m.current_query_scoring_complete = true
} }