diff --git a/go.mod b/go.mod index a06560a..69e0485 100644 --- a/go.mod +++ b/go.mod @@ -16,12 +16,11 @@ require ( github.com/rhysd/go-github-selfupdate v1.2.3 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.3.8 - golang.org/x/crypto v0.21.0 - golang.org/x/net v0.23.0 - golang.org/x/sys v0.18.0 - golang.org/x/term v0.18.0 + golang.org/x/crypto v0.36.0 + golang.org/x/net v0.38.0 + golang.org/x/sys v0.31.0 + golang.org/x/term v0.30.0 google.golang.org/api v0.114.0 - google.golang.org/grpc v1.56.3 gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 ) @@ -52,12 +51,13 @@ require ( github.com/ulikunitz/xz v0.5.9 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.1.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/grpc v1.56.3 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3558b50..6558531 100644 --- a/go.sum +++ b/go.sum @@ -164,8 +164,8 @@ go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -178,8 +178,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= @@ -187,8 +187,8 @@ golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -202,20 +202,20 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go index cf9319a..44e067b 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ const ( var ( // Version is the released version of passline - version string = "1.15.1" + version string = "1.15.2" // BuildTime is the time the binary was built date string ) diff --git a/pkg/cli/selection/selection.go b/pkg/cli/selection/selection.go index 6afb231..a25bfa4 100644 --- a/pkg/cli/selection/selection.go +++ b/pkg/cli/selection/selection.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "passline/pkg/cli/terminal" "passline/pkg/util" @@ -16,6 +17,11 @@ type SelectItem struct { Label string } +type SelectItemWithDistance struct { + item SelectItem + score int +} + func ArgOrSelect(ctx context.Context, args ucli.Args, index int, message string, items []SelectItem) (string, error) { userInput := args.Get(index) @@ -59,12 +65,23 @@ func arrayContains(l []SelectItem, i string) bool { } func filterArray(l []SelectItem, filter string) []SelectItem { - filteredNames := make([]SelectItem, 0) + selectItemsWithDistance := make([]SelectItemWithDistance, 0) + for _, i := range l { _, distance := util.LevenshteinDistanceSubstring(i.Label, filter) - if distance <= 2 { - filteredNames = append(filteredNames, i) + if distance <= max(0, min(len(filter)-2, 2)) { + selectItemsWithDistance = append(selectItemsWithDistance, SelectItemWithDistance{item: i, score: distance}) } } - return filteredNames + + slices.SortFunc(selectItemsWithDistance, func(a, b SelectItemWithDistance) int { + return a.score - b.score + }) + + filteredItems := make([]SelectItem, 0) + for _, i := range selectItemsWithDistance { + filteredItems = append(filteredItems, i.item) + } + + return filteredItems } diff --git a/pkg/cli/selection/selection_test.go b/pkg/cli/selection/selection_test.go new file mode 100644 index 0000000..641447a --- /dev/null +++ b/pkg/cli/selection/selection_test.go @@ -0,0 +1,98 @@ +package selection + +import ( + "testing" +) + +func TestFilterArray1(t *testing.T) { + filter := "abc" + items := []SelectItem{ + { + Value: "ab", + Label: "ab", + }, + { + Value: "abc", + Label: "abc", + }, + { + Value: "abcd", + Label: "abcd", + }, + } + + result := filterArray(items, filter) + + if len(result) != 3 { + t.Errorf("filterArray() = %d; wanted length %v", len(result), 3) + } +} + +func TestFilterArray2(t *testing.T) { + filter := "haus" + items := []SelectItem{ + { + Value: "baum", + Label: "baum", + }, + { + Value: "raus", + Label: "raus", + }, + { + Value: "maus", + Label: "maus", + }, + } + + result := filterArray(items, filter) + + if len(result) != 3 { + t.Errorf("filterArray() = %d; wanted length %v", len(result), 3) + } + if result[0].Value != "raus" { + t.Errorf("filterArray()[0] = %s; wanted %s", result[0].Value, "raus") + } + if result[1].Value != "maus" { + t.Errorf("filterArray()[0] = %s; wanted %s", result[0].Value, "raus") + } +} + +func TestFilterArray3(t *testing.T) { + filter := "abc" + items := []SelectItem{ + { + Value: "acd", + Label: "acd", + }, + } + + result := filterArray(items, filter) + + if len(result) != 0 { + t.Errorf("filterArray() = %d; wanted length %v", len(result), 1) + } +} + +func TestFilterArraySorting(t *testing.T) { + filter := "comd" + items := []SelectItem{ + { + Value: "test.com", + Label: "test.com", + }, + { + Value: "comdirect.de", + Label: "comdirect.de", + }, + } + + result := filterArray(items, filter) + + if len(result) != 2 { + t.Errorf("filterArray() = %d; wanted length %v", len(result), 1) + } + if result[0].Value != "comdirect.de" { + t.Errorf("filterArray()[0] = %s; wanted %s", result[0].Value, "comdirect") + } +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 0d01db0..3b30a19 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -35,15 +35,26 @@ func StringToArray(s string) []string { func LevenshteinDistanceSubstring(target, pattern string) (string, int) { minDistance := len(pattern) bestMatch := "" - patternLength := len(pattern) - for i := 0; i <= len(target)-patternLength; i++ { - substring := target[i : i+patternLength] - distance := LevenshteinDistance(substring, pattern) + if len(target) >= len(pattern) { + for i := 0; i <= len(target)-len(pattern); i++ { + substring := target[i : i+len(pattern)] + distance := LevenshteinDistance(substring, pattern) - if distance < minDistance { - minDistance = distance - bestMatch = substring + if distance < minDistance { + minDistance = distance + bestMatch = substring + } + } + } else { + for i := 0; i <= len(pattern)-len(target); i++ { + substring := pattern[i : i+len(target)] + distance := LevenshteinDistance(target, substring) + + if distance < minDistance { + minDistance = distance + bestMatch = substring + } } } diff --git a/pkg/util/utils_test.go b/pkg/util/utils_test.go index ef950a9..30e575c 100644 --- a/pkg/util/utils_test.go +++ b/pkg/util/utils_test.go @@ -22,6 +22,30 @@ func TestLevenshteinDistanceDifferenceLength(t *testing.T) { } } +func TestLevenshteinDistanceSubstringDifference1(t *testing.T) { + bestMatch, distance := LevenshteinDistanceSubstring("ab", "abc") + + if distance != 0 { + t.Errorf("LevenshteinDistanceSubscring() = _,%d; wanted length %v", distance, 1) + } + + if bestMatch != "ab" { + t.Errorf("LevenshteinDistance() = %s,_; wanted length %s", bestMatch, "ab") + } +} + +func TestLevenshteinDistanceSubstringDifference2(t *testing.T) { + bestMatch, distance := LevenshteinDistanceSubstring("abd", "abcd") + + if distance != 1 { + t.Errorf("LevenshteinDistanceSubscring() = _,%d; wanted length %v", distance, 1) + } + + if bestMatch != "abc" { + t.Errorf("LevenshteinDistance() = %s,_; wanted length %s", bestMatch, "abc") + } +} + func TestFindClosestSubstringMass(t *testing.T) { items := []string{ "google.com", @@ -1029,7 +1053,7 @@ func TestFindClosestSubstringMass(t *testing.T) { elapsed := time.Since(start) fmt.Printf("Time elapsed: %s\n", elapsed) - maxTime := 100000 + maxTime := 5_000_000 //5 milliseconds if elapsed > time.Duration(maxTime) { t.Errorf("FindClosestSubstring duration = %d; wanted < %d", elapsed, maxTime)