mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-06 01:05:48 +02:00
More work on choose files kitten
This commit is contained in:
@@ -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 {{{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user