// SPDX-License-Identifier: GPL-3.0-or-later package model import ( "strings" "krino/internal/norm" ) // FuzzyMatch reports whether every character of pattern appears in text in // order - the way fzf matches - and how good the match is. Capitals and // accents are ignored, using krino's own folding, so "zazolc" finds // "zażółć" exactly as a rule with (fold yes) would. // // The score rewards characters that follow one another and those at the // start of a word, so "lec" ranks "lectio-2026.pdf" above // "old-latex-certificate.pdf". func FuzzyMatch(pattern, text string) (score int, ok bool) { p := []rune(norm.Fold(strings.ToLower(pattern))) t := []rune(norm.Fold(strings.ToLower(text))) if len(p) == 0 { return 0, true } if len(p) > len(t) { return 0, false } pi := 0 run := 0 for ti := 0; ti < len(t) && pi < len(p); ti++ { if t[ti] != p[pi] { run = 0 continue } score += 1 + run // a run of matching characters is worth more if ti == 0 || isWordBreak(t[ti-1]) { score += 4 // the start of a word counts for more still } run++ pi++ } if pi < len(p) { return 0, false } // A short name matching is a better match than a long one. if len(t) > 0 { score += 20 * len(p) / len(t) } return score, true } // isWordBreak reports whether a character separates words in a file name. func isWordBreak(r rune) bool { switch r { case ' ', '.', '-', '_', '/', ',', '(', ')', '[', ']': return true } return false } // Matching is the rows a filter leaves, in the plan's own order - which is // what a list of files is read in - not in score order. An empty pattern // matches everything. func (t *PlanTab) Matching(pattern string) []int { var out []int for i, r := range t.Rows { if _, ok := FuzzyMatch(pattern, r.Rel); ok { out = append(out, i) continue } // The rule a file took is worth searching too: "to-sort" finds // everything that rule claimed. if _, ok := FuzzyMatch(pattern, r.Rule); ok { out = append(out, i) } } return out } // SelectOnly checks exactly the rows given - a filtered "Select all" - and // clears the rest, so what is applied is what was on screen. func (t *PlanTab) SelectOnly(rows []int) { want := map[int]bool{} for _, i := range rows { want[i] = true } for i := range t.Rows { t.Rows[i].Selected = want[i] && t.Rows[i].Actable } } // HiddenSelected is how many checked files a filter is hiding, so the // window can say so rather than apply something out of sight. func (t *PlanTab) HiddenSelected(shown []int) int { visible := map[int]bool{} for _, i := range shown { visible[i] = true } n := 0 for i, r := range t.Rows { if r.Selected && !visible[i] { n++ } } return n }