fix fuzzy matching not working properly (#2321)

* fix fuzzy matching not working properly

* delete continuousRanges
mine
Jayson Wang 2023-12-08 23:10:40 +08:00 committed by GitHub
parent e94f991ad4
commit d0ec55737a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 169 additions and 168 deletions

View File

@ -96,3 +96,17 @@ func serviceAccountMatches(podSA, saName string) bool {
}
return podSA == saName
}
// ContinuousRanges takes a sorted slice of integers and returns a slice of
// sub-slices representing continuous ranges of integers.
func ContinuousRanges(indexes []int) [][]int {
var ranges [][]int
for i, p := 1, 0; i <= len(indexes); i++ {
if i == len(indexes) || indexes[i]-indexes[p] != i-p {
ranges = append(ranges, []int{indexes[p], indexes[i-1] + 1})
p = i
}
}
return ranges
}

View File

@ -60,3 +60,35 @@ func TestServiceAccountMatches(t *testing.T) {
assert.Equal(t, u.expect, serviceAccountMatches(u.podTemplate.ServiceAccountName, u.saName))
}
}
func TestContinuousRanges(t *testing.T) {
tests := []struct {
Indexes []int
Ranges [][]int
}{
{
Indexes: []int{0},
Ranges: [][]int{{0, 1}},
},
{
Indexes: []int{1},
Ranges: [][]int{{1, 2}},
},
{
Indexes: []int{0, 1, 2},
Ranges: [][]int{{0, 3}},
},
{
Indexes: []int{4, 5, 6},
Ranges: [][]int{{4, 7}},
},
{
Indexes: []int{0, 2, 4, 5, 6},
Ranges: [][]int{{0, 1}, {2, 3}, {4, 7}},
},
}
for _, tt := range tests {
assert.Equal(t, tt.Ranges, ContinuousRanges(tt.Indexes))
}
}

View File

@ -7,7 +7,6 @@ import (
"context"
"fmt"
"reflect"
"regexp"
"strings"
"sync/atomic"
"time"
@ -69,28 +68,13 @@ func (d *Describe) filter(q string, lines []string) fuzzy.Matches {
if dao.IsFuzzySelector(q) {
return d.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
}
return d.rxFilter(q, lines)
return rxFilter(q, lines)
}
func (*Describe) fuzzyFilter(q string, lines []string) fuzzy.Matches {
return fuzzy.Find(q, lines)
}
func (*Describe) rxFilter(q string, lines []string) fuzzy.Matches {
rx, err := regexp.Compile(`(?i)` + q)
if err != nil {
return nil
}
matches := make(fuzzy.Matches, 0, len(lines))
for i, l := range lines {
if loc := rx.FindStringIndex(l); len(loc) == 2 {
matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc})
}
}
return matches
}
func (d *Describe) fireResourceChanged(lines []string, matches fuzzy.Matches) {
for _, l := range d.listeners {
l.ResourceChanged(lines, matches)

View File

@ -5,11 +5,13 @@ package model
import (
"context"
"regexp"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
"github.com/mattn/go-runewidth"
"github.com/sahilm/fuzzy"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -37,3 +39,25 @@ func NewExpBackOff(ctx context.Context, start, max time.Duration) backoff.BackOf
bf.InitialInterval, bf.MaxElapsedTime = start, max
return backoff.WithContext(bf, ctx)
}
func rxFilter(q string, lines []string) fuzzy.Matches {
rx, err := regexp.Compile(`(?i)` + q)
if err != nil {
return nil
}
matches := make(fuzzy.Matches, 0, len(lines))
for i, l := range lines {
locs := rx.FindAllStringIndex(l, -1)
for _, loc := range locs {
indexes := make([]int, 0, loc[1]-loc[0])
for v := loc[0]; v < loc[1]; v++ {
indexes = append(indexes, v)
}
matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: indexes})
}
}
return matches
}

View File

@ -4,13 +4,12 @@
package model
import (
"testing"
"github.com/sahilm/fuzzy"
"github.com/stretchr/testify/assert"
"testing"
)
func TestYAML_rxFilter(t *testing.T) {
func Test_rxFilter(t *testing.T) {
uu := map[string]struct {
q string
lines []string
@ -32,7 +31,7 @@ func TestYAML_rxFilter(t *testing.T) {
{
Str: "foo",
Index: 0,
MatchedIndexes: []int{0, 3},
MatchedIndexes: []int{0, 1, 2},
},
},
},
@ -43,26 +42,25 @@ func TestYAML_rxFilter(t *testing.T) {
{
Str: "foo",
Index: 0,
MatchedIndexes: []int{0, 3},
MatchedIndexes: []int{0, 1, 2},
},
{
Str: "foo",
Index: 2,
MatchedIndexes: []int{0, 3},
MatchedIndexes: []int{0, 1, 2},
},
{
Str: "foo",
Index: 2,
MatchedIndexes: []int{8, 11},
MatchedIndexes: []int{8, 9, 10},
},
},
},
}
var y YAML
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, y.rxFilter(u.q, u.lines))
assert.Equal(t, u.e, rxFilter(u.q, u.lines))
})
}
}

View File

@ -4,7 +4,6 @@
package model
import (
"regexp"
"strings"
"github.com/derailed/k9s/internal/dao"
@ -115,24 +114,9 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches {
if dao.IsFuzzySelector(q) {
return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
}
return t.rxFilter(q, lines)
return rxFilter(q, lines)
}
func (*Text) fuzzyFilter(q string, lines []string) fuzzy.Matches {
return fuzzy.Find(q, lines)
}
func (*Text) rxFilter(q string, lines []string) fuzzy.Matches {
rx, err := regexp.Compile(`(?i)` + q)
if err != nil {
return nil
}
matches := make(fuzzy.Matches, 0, len(lines))
for i, l := range lines {
if loc := rx.FindStringIndex(l); len(loc) == 2 {
matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc})
}
}
return matches
}

View File

@ -226,7 +226,7 @@ func (t *Tree) reconcile(ctx context.Context) error {
root.Sort()
if t.query != "" {
t.root = root.Filter(t.query, rxFilter)
t.root = root.Filter(t.query, rxMatch)
}
if t.root == nil || t.root.Diff(root) {
t.root = root
@ -277,7 +277,7 @@ func (t *Tree) getMeta(ctx context.Context, gvr string) (ResourceMeta, error) {
// ----------------------------------------------------------------------------
// Helpers...
func rxFilter(q, path string) bool {
func rxMatch(q, path string) bool {
rx := regexp.MustCompile(`(?i)` + q)
tokens := strings.Split(path, "::")

View File

@ -5,7 +5,6 @@ package model
import (
"context"
"regexp"
"strings"
"sync/atomic"
"time"
@ -93,28 +92,13 @@ func (v *Values) filter(q string, lines []string) fuzzy.Matches {
if dao.IsFuzzySelector(q) {
return v.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
}
return v.rxFilter(q, lines)
return rxFilter(q, lines)
}
func (*Values) fuzzyFilter(q string, lines []string) fuzzy.Matches {
return fuzzy.Find(q, lines)
}
func (*Values) rxFilter(q string, lines []string) fuzzy.Matches {
rx, err := regexp.Compile(`(?i)` + q)
if err != nil {
return nil
}
matches := make(fuzzy.Matches, 0, len(lines))
for i, l := range lines {
if loc := rx.FindStringIndex(l); len(loc) == 2 {
matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc})
}
}
return matches
}
func (v *Values) fireResourceChanged(lines []string, matches fuzzy.Matches) {
for _, l := range v.listeners {
l.ResourceChanged(lines, matches)

View File

@ -7,7 +7,6 @@ import (
"context"
"fmt"
"reflect"
"regexp"
"strings"
"sync/atomic"
"time"
@ -78,28 +77,13 @@ func (y *YAML) filter(q string, lines []string) fuzzy.Matches {
if dao.IsFuzzySelector(q) {
return y.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
}
return y.rxFilter(q, lines)
return rxFilter(q, lines)
}
func (*YAML) fuzzyFilter(q string, lines []string) fuzzy.Matches {
return fuzzy.Find(q, lines)
}
func (*YAML) rxFilter(q string, lines []string) fuzzy.Matches {
rx, err := regexp.Compile(`(?i)` + q)
if err != nil {
return nil
}
matches := make(fuzzy.Matches, 0, len(lines))
for i, l := range lines {
locs := rx.FindAllStringIndex(l, -1)
for _, loc := range locs {
matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc})
}
}
return matches
}
func (y *YAML) fireResourceChanged(lines []string, matches fuzzy.Matches) {
for _, l := range y.listeners {
l.ResourceChanged(lines, matches)

View File

@ -102,19 +102,12 @@ func (d *Details) TextChanged(lines []string) {
// TextFiltered notifies when the filter changed.
func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) {
d.currentRegion, d.maxRegions = 0, 0
ll := make([]string, len(lines))
copy(ll, lines)
for _, m := range matches {
loc, line := m.MatchedIndexes, ll[m.Index]
ll[m.Index] = line[:loc[0]] + fmt.Sprintf(`<<<"search_%d">>>`, d.maxRegions) + line[loc[0]:loc[1]] + `<<<"">>>` + line[loc[1]:]
d.maxRegions++
}
d.currentRegion, d.maxRegions = 0, len(matches)
ll := linesWithRegions(lines, matches)
d.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(ll, "\n")))
d.text.Highlight()
if d.maxRegions > 0 {
if len(matches) > 0 {
d.text.Highlight("search_0")
d.text.ScrollToHighlight()
}

View File

@ -14,12 +14,14 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy"
)
func clipboardWrite(text string) error {
@ -240,3 +242,22 @@ func decorateCpuMemHeaderRows(app *App, data *render.TableData) {
}
}
}
func matchTag(i int, s string) string {
return `<<<"search_` + strconv.Itoa(i) + `">>>` + s + `<<<"">>>`
}
func linesWithRegions(lines []string, matches fuzzy.Matches) []string {
ll := make([]string, len(lines))
copy(ll, lines)
offsetForLine := make(map[int]int)
for i, m := range matches {
for _, loc := range dao.ContinuousRanges(m.MatchedIndexes) {
start, end := loc[0]+offsetForLine[m.Index], loc[1]+offsetForLine[m.Index]
regionStr := matchTag(i, ll[m.Index][start:end])
ll[m.Index] = ll[m.Index][:start] + regionStr + ll[m.Index][end:]
offsetForLine[m.Index] += len(regionStr) - (end - start)
}
}
return ll
}

View File

@ -14,6 +14,7 @@ import (
"github.com/derailed/k9s/internal/render"
"github.com/derailed/tcell/v2"
"github.com/rs/zerolog"
"github.com/sahilm/fuzzy"
"github.com/stretchr/testify/assert"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
@ -266,3 +267,61 @@ func TestContainerID(t *testing.T) {
})
}
}
func Test_linesWithRegions(t *testing.T) {
uu := map[string]struct {
lines []string
matches fuzzy.Matches
e []string
}{
"empty-lines": {
e: []string{},
},
"no-match": {
lines: []string{"bar"},
e: []string{"bar"},
},
"single-match": {
lines: []string{"foo", "bar", "baz"},
matches: fuzzy.Matches{
{Index: 1, MatchedIndexes: []int{0, 1, 2}},
},
e: []string{"foo", matchTag(0, "bar"), "baz"},
},
"single-character": {
lines: []string{"foo", "bar", "baz"},
matches: fuzzy.Matches{
{Index: 1, MatchedIndexes: []int{1}},
},
e: []string{"foo", "b" + matchTag(0, "a") + "r", "baz"},
},
"multiple-matches": {
lines: []string{"foo", "bar", "baz"},
matches: fuzzy.Matches{
{Index: 1, MatchedIndexes: []int{0, 1, 2}},
{Index: 2, MatchedIndexes: []int{0, 1, 2}},
},
e: []string{"foo", matchTag(0, "bar"), matchTag(1, "baz")},
},
"multiple-matches-same-line": {
lines: []string{"foosfoo baz", "dfbarfoos bar"},
matches: fuzzy.Matches{
{Index: 0, MatchedIndexes: []int{0, 1, 2}},
{Index: 0, MatchedIndexes: []int{4, 5, 6}},
{Index: 1, MatchedIndexes: []int{5, 6, 7}},
},
e: []string{
matchTag(0, "foo") + "s" + matchTag(1, "foo") + " baz",
"dfbar" + matchTag(2, "foo") + "s bar",
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
t.Parallel()
assert.Equal(t, u.e, linesWithRegions(u.lines, u.matches))
})
}
}

View File

@ -100,36 +100,17 @@ func (v *LiveView) ResourceFailed(err error) {
v.text.SetText(cowTalk(err.Error(), x+w))
}
func (*LiveView) linesWithRegions(lines []string, matches fuzzy.Matches) []string {
ll := make([]string, len(lines))
copy(ll, lines)
offsetForLine := make(map[int]int)
for i, m := range matches {
loc, line := m.MatchedIndexes, ll[m.Index]
if len(loc) < 2 {
continue
}
offset := offsetForLine[m.Index]
loc[0], loc[1] = loc[0]+offset, loc[1]+offset
regionStr := `<<<"search_` + strconv.Itoa(i) + `">>>` + line[loc[0]:loc[1]] + `<<<"">>>`
ll[m.Index] = line[:loc[0]] + regionStr + line[loc[1]:]
offsetForLine[m.Index] += len(regionStr) - (loc[1] - loc[0])
}
return ll
}
// ResourceChanged notifies when the filter changes.
func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) {
v.app.QueueUpdateDraw(func() {
v.text.SetTextAlign(tview.AlignLeft)
v.maxRegions = len(matches)
v.currentRegion, v.maxRegions = 0, len(matches)
if v.text.GetText(true) == "" {
v.text.ScrollToBeginning()
}
lines = v.linesWithRegions(lines, matches)
lines = linesWithRegions(lines, matches)
v.text.SetText(colorizeYAML(v.app.Styles.Views().Yaml, strings.Join(lines, "\n")))
v.text.Highlight()
if v.currentRegion < v.maxRegions {

View File

@ -5,18 +5,12 @@ package view
import (
"context"
"strconv"
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/sahilm/fuzzy"
"github.com/stretchr/testify/assert"
)
func matchTag(i int, s string) string {
return `<<<"search_` + strconv.Itoa(i) + `">>>` + s + `<<<"">>>`
}
func TestLiveViewSetText(t *testing.T) {
s := `
apiVersion: v1
@ -30,54 +24,3 @@ apiVersion: v1
assert.Equal(t, s, sanitizeEsc(v.text.GetText(true)))
}
func TestLiveView_linesWithRegions(t *testing.T) {
uu := map[string]struct {
lines []string
matches fuzzy.Matches
e []string
}{
"empty-lines": {
e: []string{},
},
"no-match": {
lines: []string{"bar"},
e: []string{"bar"},
},
"single-match": {
lines: []string{"foo", "bar", "baz"},
matches: fuzzy.Matches{
{Index: 1, MatchedIndexes: []int{0, 3}},
},
e: []string{"foo", matchTag(0, "bar"), "baz"},
},
"multiple-matches": {
lines: []string{"foo", "bar", "baz"},
matches: fuzzy.Matches{
{Index: 1, MatchedIndexes: []int{0, 3}},
{Index: 2, MatchedIndexes: []int{0, 3}},
},
e: []string{"foo", matchTag(0, "bar"), matchTag(1, "baz")},
},
"multiple-matches-same-line": {
lines: []string{"foosfoo baz", "dfbarfoos bar"},
matches: fuzzy.Matches{
{Index: 0, MatchedIndexes: []int{0, 3}},
{Index: 0, MatchedIndexes: []int{4, 7}},
{Index: 1, MatchedIndexes: []int{5, 8}},
},
e: []string{
matchTag(0, "foo") + "s" + matchTag(1, "foo") + " baz",
"dfbar" + matchTag(2, "foo") + "s bar",
},
},
}
var v LiveView
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
t.Parallel()
assert.Equal(t, u.e, v.linesWithRegions(u.lines, u.matches))
})
}
}