Mention result ordering
The mentioning now uses a fuzzy matching algorithm. This also fixed
MODIFY
shortcuts/shortcuts.go
shortcuts/shortcuts.go
@@ -58,7 +58,7 @@ var (
DeleteRight = addShortcut("delete_right", "Delete right",
multilineTextInput, tcell.NewEventKey(tcell.KeyDelete, 0, tcell.ModNone))
InputNewLine = addShortcut("add_new_line_character", "Add new line character",
- multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModAlt))
+ multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModShift))
CopySelection = addShortcut("copy_selection", "Copy selected text",
multilineTextInput, tcell.NewEventKey(tcell.KeyRune, 'C', tcell.ModAlt))

MODIFY
ui/window.go
ui/window.go
@@ -27,6 +27,8 @@ import (
"github.com/Bios-Marcel/tview"
"github.com/gdamore/tcell"
"github.com/gen2brain/beeep"
+
+ "github.com/Bios-Marcel/cordless/util/fuzzy"
)
const (
@@ -976,43 +978,90 @@ func (window *Window) ShowMentionWindowChildren(mentionWindow *tview.TreeView, m
}
func (window *Window) PopulateMentionWindow(mentionWindow *tview.TreeView, namePart string) {
- if window.selectedChannel != nil {
+ if window.selectedChannel == nil {
+ return
+ }
+
if window.selectedChannel.GuildID != "" {
+ window.PopulateMentionWindowFromCurrentGuild(mentionWindow, namePart)
+ } else {
+ window.PopulateMentionWindowFromCurrentChannel(mentionWindow, namePart)
+ }
+}
+
+func (window *Window) PopulateMentionWindowFromCurrentGuild(mentionWindow *tview.TreeView, namePart string) {
guild, discordError := window.session.State.Guild(window.selectedChannel.GuildID)
- if discordError == nil {
+ if discordError != nil {
+ return
+ }
+
+ nameMap := make(map[string]string)
+ var memberNames []string
for _, user := range guild.Members {
- if strings.Contains(strings.ToUpper(user.Nick), strings.ToUpper(namePart)) || strings.Contains(strings.ToUpper(user.User.Username)+"#"+user.User.Discriminator, strings.ToUpper(namePart)) {
userName := user.User.Username + "#" + user.User.Discriminator
- userNodeText := "\t" + userName
if len(user.Nick) > 0 {
- userNodeText += " | " + user.Nick
- }
- userNode := tview.NewTreeNode(userNodeText)
- userNode.SetReference(userName)
- mentionWindow.GetRoot().AddChild(userNode)
+ combined := "\t" + userName + " | " + user.Nick
+ nameMap[userName] = combined
+ nameMap[user.Nick] = combined
+ nameMap[combined] = userName
+ memberNames = append(memberNames, userName, user.Nick)
+ } else {
+ memberNames = append(memberNames, userName)
}
}
for _, role := range guild.Roles {
- if strings.Contains(strings.ToUpper(role.Name), strings.ToUpper(namePart)) {
- roleNode := tview.NewTreeNode(role.Name)
- roleNode.SetReference(role)
- mentionWindow.GetRoot().AddChild(roleNode)
+ role := role.Name
+ memberNames = append(memberNames, role)
+ }
+
+ searchResults := fuzzy.ScoreSearch(namePart, memberNames)
+ sortedResults := fuzzy.SortSearchResults(searchResults)
+
+ userWithNickSet := make(map[string]struct{})
+ for _, result := range sortedResults {
+ userOrNickName := result.Key
+ userAndNickName := nameMap[userOrNickName]
+ var userNodeText string
+ var userName string
+ if len(userAndNickName) > 0 {
+ _, containsStr := userWithNickSet[userAndNickName]
+ // If the combined string has been added, skip this entry.
+ if containsStr {
+ continue
}
+ userWithNickSet[userAndNickName] = struct{}{}
+ userNodeText = userAndNickName
+ userName = nameMap[userAndNickName]
+ } else {
+ userNodeText = userOrNickName
+ userName = userOrNickName
}
+ userNode := tview.NewTreeNode(userNodeText)
+ userNode.SetReference(userName)
+ mentionWindow.GetRoot().AddChild(userNode)
}
- } else {
+
+}
+
+func (window *Window) PopulateMentionWindowFromCurrentChannel(mentionWindow *tview.TreeView, namePart string) {
+ var memberNames []string
for _, user := range window.selectedChannel.Recipients {
- if strings.Contains(strings.ToUpper(user.Username)+"#"+user.Discriminator, strings.ToUpper(namePart)) {
userName := user.Username + "#" + user.Discriminator
+ memberNames = append(memberNames, userName)
+ }
+
+ searchResults := fuzzy.ScoreSearch(namePart, memberNames)
+ sortedResults := fuzzy.SortSearchResults(searchResults)
+
+ for _, result := range sortedResults {
+ userName := result.Key
userNodeText := "\t" + userName
userNode := tview.NewTreeNode(userNodeText)
userNode.SetReference(userName)
mentionWindow.GetRoot().AddChild(userNode)
}
- }
- }
- }
+
}
func (window *Window) updateServerReadStatus(guildID string, guildNode *tview.TreeNode, isSelected bool) {

ADD
util/fuzzy/fuzzy.go
+package fuzzy
+
+import (
+ "sort"
+ "strings"
+)
+
+type SearchResult struct {
+ Key string
+ Value float64
+}
+
+func ScoreSearch(searchTerm string, searchItems []string) map[string]float64 {
+ mymap := make(map[string]float64)
+ // Ignore case if there are no capital letters in the search term.
+ caseInsensitive := searchTerm == strings.ToLower(searchTerm)
+ if caseInsensitive {
+ searchTerm = strings.ToLower(searchTerm)
+ }
+ for _, str := range searchItems {
+ if caseInsensitive {
+ mymap[str] = Score(searchTerm, strings.ToLower(str))
+ } else {
+ mymap[str] = Score(searchTerm, str)
+ }
+ }
+ return mymap
+}
+
+func SortSearchResults(results map[string]float64) []SearchResult {
+ // Convert the results into an array.
+ var arr []SearchResult
+ for key, value := range results {
+ if value >= 0.0 {
+ arr = append(arr, SearchResult{key, value})
+ }
+ }
+ // Sort results based on score. Equal scores are sorted alphbetically.
+ sort.Slice(arr, func(i, j int) bool {
+ if arr[i].Value == arr[j].Value {
+ return strings.Compare(strings.ToLower(arr[i].Key), strings.ToLower(arr[j].Key)) < 0
+ }
+ return arr[i].Value > arr[j].Value
+ })
+ return arr
+}
+
+// Returns:
+// -1 if the needle contains letters the haystack does not contain,
+// or if the needle length exceeds the haystack length.
+//
+// 0 if no similarities were found
+//
+// > 0 based on similarities between the needle and haystack (increasing)
+func Score(needle, haystack string) float64 {
+ needleLength := len(needle)
+ haystackLength := len(haystack)
+ if needleLength > haystackLength {
+ return -1
+ }
+
+ if needleLength == 0 {
+ return 0
+ }
+
+ score := 0.0
+ needleIndex := 0
+ for haystackIndex := 0; needleIndex < needleLength; haystackIndex++ {
+
+ letterIndex := findLetterIndex(needle[needleIndex], haystack, haystackIndex)
+ if letterIndex < 0 {
+ return -1
+ }
+
+ if letterIndex == haystackIndex {
+ // Letter was consecutive
+ score += 8
+ } else {
+ score += 1 - (0.1 * float64(letterIndex))
+ // Move haystackIndex up to the next found letter.
+ haystackIndex = letterIndex
+ }
+
+ needleIndex++
+ }
+ return score
+}
+
+// Scores a letter from inside a string based on its distance from the start of the string.
+// The index at which the letter was found will be returned.
+// The score and index will be -1 if the character is not found.
+func findLetterIndex(c byte, haystack string, startIndex int) int {
+ for i := startIndex; i < len(haystack); i++ {
+ if c == haystack[i] {
+ return i
+ }
+ }
+ // Letter not found.
+ return -1
+}

ADD
util/fuzzy/fuzzy_test.go
+package fuzzy
+
+import (
+ "testing"
+)
+
+func TestFuzzyScore(t *testing.T) {
+ scoreA := Score("Marc", "Marcel#3414")
+ scoreB := Score("Marc", "Maurice")
+ if scoreA <= scoreB {
+ t.Errorf("Incorrect score rating")
+ }
+
+ scoreC := Score("Marc", "Marchesi#9331")
+ if scoreC <= scoreB {
+ t.Errorf("Incorrect score rating")
+ }
+
+ arr := []string{"tests", "test", "testosterone", "atesta", "bob"}
+ sorted := SortSearchResults(ScoreSearch("te", arr))
+ expected := [4]string{"test", "testosterone", "tests", "atesta"}
+
+ if len(sorted) != len(expected) {
+ t.Errorf("Expected length of %d, but received %d.\n", len(expected), len(sorted))
+ }
+
+ var results [4]string
+ for i, result := range sorted {
+ results[i] = result.Key
+ }
+
+ if results != expected {
+ t.Errorf("Expected\n%v\nbut received\n%v\n", expected, results)
+ }
+
+ if Score("S", "cvcx") >= 0 {
+ t.Errorf("Expected score to be < 0\n")
+ }
+
+ if Score("S", "scheme god#2313") >= 0 {
+ t.Errorf("Expected score to be < 0\n")
+ }
+
+ if Score("K", "KC") <= 0 {
+ t.Errorf("Expected score to be < 0\n")
+ }
+
+ if Score("s", "space") <= 0 {
+ t.Errorf("Expected score to be < 0\n")
+ }
+
+}

Mention result ordering (#128)

The mentioning now uses a fuzzy matching algorithm. This also fixed #122