You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cointop/cointop/search.go

176 lines
3.8 KiB
Go

package cointop
import (
"regexp"
"strings"
"github.com/cointop-sh/cointop/pkg/levenshtein"
"github.com/cointop-sh/cointop/pkg/ui"
log "github.com/sirupsen/logrus"
)
// SearchFieldView is structure for search field view
type SearchFieldView = ui.View
// NewSearchFieldView returns a new search field view
func NewSearchFieldView() *SearchFieldView {
return ui.NewView("searchfield")
}
// InputView is structure for help view
type InputView = ui.View
// NewInputView returns a new help view
func NewInputView() *InputView {
return ui.NewView("input")
}
// OpenSearch opens the search field
func (ct *Cointop) OpenSearch() error {
log.Debug("OpenSearch()")
if ct.ui.ActiveViewName() != ct.Views.Table.Name() {
return nil
}
ct.State.searchFieldVisible = true
ct.ui.SetCursor(true)
ct.SetActiveView(ct.Views.SearchField.Name())
return nil
}
// CancelSearch closes the search field
func (ct *Cointop) CancelSearch() error {
log.Debug("CancelSearch()")
ct.State.searchFieldVisible = false
ct.ui.SetCursor(false)
ct.SetActiveView(ct.Views.Table.Name())
return nil
}
// DoSearch triggers the search and sets views
func (ct *Cointop) DoSearch() error {
log.Debug("DoSearch()")
ct.Views.SearchField.Rewind()
b := make([]byte, 100)
n, err := ct.Views.SearchField.Read(b)
if err != nil {
return err
}
// TODO: do this a better way (SoC)
ct.SetSelectedView(CoinsView)
defer ct.SetActiveView(ct.Views.Table.Name())
if err != nil {
return nil
}
if n == 0 {
return nil
}
q := strings.TrimSpace(string(b[:n]))
// remove slash
regex := regexp.MustCompile(`/(.*)`)
matches := regex.FindStringSubmatch(q)
if len(matches) > 0 {
q = matches[1]
}
return ct.Search(q)
}
// Search performs the search and filtering
func (ct *Cointop) Search(q string) error {
log.Debugf("Search(%s)", q)
// If there are no coins, return no result
if len(ct.State.coins) == 0 {
return nil
}
// If search term is empty, use the previous search term.
q = strings.TrimSpace(strings.ToLower(q))
if q == "" {
q = ct.State.lastSearchQuery
} else {
ct.State.lastSearchQuery = q
}
canSearchSymbol := true
canSearchName := true
if strings.HasPrefix(q, "s:") {
canSearchSymbol = true
canSearchName = false
q = q[2:]
log.Debug("Search, by keyword")
}
if strings.HasPrefix(q, "n:") {
canSearchSymbol = false
canSearchName = true
q = q[2:]
log.Debug("Search, by name")
}
idx := -1
min := -1
var hasprefixidx []int
var hasprefixdist []int
// Start the search from the current position (+1), looking names that start with the search term, or symbols that match completely
currentIndex := ct.GetGlobalCoinIndex(ct.HighlightedRowCoin()) + 1
if ct.IsLastPage() && ct.IsLastRow() {
currentIndex = 0
}
for i := currentIndex; i < len(ct.State.allCoins); i++ {
coin := ct.State.allCoins[i]
name := strings.ToLower(coin.Name)
symbol := strings.ToLower(coin.Symbol)
// if query matches symbol, return immediately
if canSearchSymbol && symbol == q {
ct.GoToGlobalIndex(i)
return nil
}
if !canSearchName {
continue
}
// if query matches name, return immediately
if name == q {
ct.GoToGlobalIndex(i)
return nil
}
// store index with the smallest levenshtein
dist := levenshtein.DamerauLevenshteinDistance(name, q)
if min == -1 || dist <= min {
idx = i
min = dist
}
// store index where query is substring to name
if strings.HasPrefix(name, q) {
if len(hasprefixdist) == 0 || dist < hasprefixdist[0] {
hasprefixidx = append(hasprefixidx, i)
hasprefixdist = append(hasprefixdist, dist)
}
}
}
if !canSearchName {
return nil
}
// go to row if prefix match
if len(hasprefixidx) > 0 && hasprefixidx[0] != -1 && min > 0 {
ct.GoToGlobalIndex(hasprefixidx[0])
return nil
}
// go to row if levenshtein distance is small enough
if idx > -1 && min <= 6 {
ct.GoToGlobalIndex(idx)
return nil
}
return nil
}