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', 'LineAttrs', 'prompt_kind 2',)
mb('kittens/choose_files', 'CombinedScore', 'score 32', 'index 32')
# }}}
# Completions {{{

View File

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

View File

@@ -17,17 +17,6 @@ opt = definition.add_option
map = definition.add_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:
raise SystemExit('This must be run as kitten choose-files')

View File

@@ -1,8 +1,10 @@
package choose_files
import (
"cmp"
"fmt"
"io/fs"
"math"
"os"
"path/filepath"
"slices"
@@ -10,8 +12,6 @@ import (
"strings"
"sync"
"time"
"unicode"
"unsafe"
"github.com/kovidgoyal/kitty/tools/fzf"
"github.com/kovidgoyal/kitty/tools/utils"
@@ -20,11 +20,10 @@ import (
var _ = fmt.Print
type ResultItem struct {
text, ltext, abspath string
ftype fs.FileMode
positions []int // may be nil
score float64
positions_sorted bool
text, abspath string
ftype fs.FileMode
positions []int // may be nil
score CombinedScore
}
func (r ResultItem) String() string {
@@ -32,34 +31,12 @@ func (r ResultItem) String() string {
}
func (r *ResultItem) sorted_positions() []int {
if !r.positions_sorted {
r.positions_sorted = true
if len(r.positions) > 1 {
sort.Ints(r.positions)
}
if len(r.positions) > 1 {
sort.Ints(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 {
root_dir string
}
@@ -85,7 +62,6 @@ type ScoreResult struct {
type Settings interface {
OnlyDirs() bool
ScorePatterns() []ScorePattern
CurrentDir() string
SearchText() string
}
@@ -139,7 +115,7 @@ func (m *ResultManager) 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() {
if r := recover(); r != nil {
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
}
m.scan_results <- ScanResult{root_dir: root_dir, items: utils.Map(func(x os.DirEntry) ResultItem {
return ResultItem{abspath: filepath.Join(dir, x.Name()), ftype: x.Type()}
}, items)}
for _, x := range items {
if x.IsDir() {
ritems := utils.Map(func(x os.DirEntry) ResultItem {
ans := ResultItem{abspath: filepath.Join(dir, x.Name()), text: strings.ToLower(x.Name()), ftype: x.Type()}
ans.score.Set_index(*idx)
*idx = *idx + 1
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) {
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
}
}
@@ -172,7 +153,8 @@ func (m *ResultManager) scan(dir, root_dir string, level int) (err error) {
func (m *ResultManager) scan_worker() {
for r := range m.scan_requests {
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}
}
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 {
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}
}
@@ -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) {
items := r.items
m.lock()
only_dirs := m.settings.OnlyDirs()
sp := m.settings.ScorePatterns()
m.unlock()
if only_dirs {
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 != "" {
var r []fzf.Result
if r, err = m.scorer.ScoreWithCache(utils.Map(func(r ResultItem) string { return r.text }, items), res.query); err != nil {
var scores []fzf.Result
if scores, err = m.scorer.ScoreWithCache(utils.Map(func(r ResultItem) string { return r.text }, items), res.query); err != nil {
return
}
for i, x := range r {
items[i].positions = x.Positions
items[i].score = get_modified_score(items[i].abspath, float64(x.Score), sp)
matched_items := make([]ResultItem, 0, len(items))
for i, x := range scores {
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
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() {
for r := range m.score_queries {
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) {
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 {
func merge_sorted_slices(a, b []ResultItem) []ResultItem {
result := make([]ResultItem, 0, len(a)+len(b))
i, j := 0, 0
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])
i++
} else {
@@ -328,9 +258,30 @@ func merge_sorted_slices(a, b []ResultItem, cmp func(a, b ResultItem) int) []Res
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) {
cmp := utils.IfElse(r.query == "", cmp_without_score, cmp_with_score)
slices.SortStableFunc(r.items, cmp)
min_score, max_score := CombinedScore(math.MaxUint64), CombinedScore(0)
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()
defer func() {
m.unlock()
@@ -339,7 +290,7 @@ func (m *ResultManager) add_score_results(r ScoreResult) (err error) {
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 {
m.current_query_scoring_complete = true
}