1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
// 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 (his request,
// 2026-09-17).
//
// 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
}
|