Autocompleting has been completly rewritten.
It now works for:
* Emojis
* Custom Emojis
* Roles
* Users
It can be disabled by setting `Autocomplete` to `false`.
MODIFY
config/config.go
config/config.go
@@ -36,6 +36,7 @@ const (
var (
Current = &Config{
+ Autocomplete: true,
Times: HourMinuteAndSeconds,
UseRandomUserColors: false,
ShowUserContainer: true,
@@ -62,6 +63,10 @@ type Config struct {
//Token is the authorization token for accessing the discord API.
Token string
+ //Autocomplete decides whether the chat automatically offers autocomplete
+ //values for the currently given text.
+ Autocomplete bool
+
//Times decides on the time format (none, short and long).
Times int
//UseRandomUserColors decides whether the users get assigned a random color

MODIFY
go.mod
go.mod
@@ -3,7 +3,7 @@ module github.com/Bios-Marcel/cordless
go 1.12
require (
- github.com/Bios-Marcel/discordemojimap v1.0.0
+ github.com/Bios-Marcel/discordemojimap v1.0.1
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103003547-0699c2def4b0
github.com/Bios-Marcel/femto v0.0.0-20191123155547-99f32baf0a56
github.com/Bios-Marcel/goclipimg v0.0.0-20191117180634-d0f7b06fbe82

MODIFY
go.sum
go.sum
github.com/Bios-Marcel/discordemojimap v1.0.0 h1:PgFm9D3gWJXyTlIxIG2Xl2IVZFGqVqNrrG/g+RSrc7Q=
github.com/Bios-Marcel/discordemojimap v1.0.0/go.mod h1:AoHIpUwf3EVCAAUmk+keXjb9khyZcFnW84/rhJd4IkU=
+github.com/Bios-Marcel/discordemojimap v1.0.1 h1:b3UYPO7+h1+ciStkwU/KQCerOmpUNPHsBf4a7EjMdys=
+github.com/Bios-Marcel/discordemojimap v1.0.1/go.mod h1:AoHIpUwf3EVCAAUmk+keXjb9khyZcFnW84/rhJd4IkU=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103003547-0699c2def4b0 h1:wnLeeQhagkXoTJQ681D25HBxVSefFO7KMB0txBJMqTE=
github.com/Bios-Marcel/discordgo v0.20.4-0.20191103003547-0699c2def4b0/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/femto v0.0.0-20191123155547-99f32baf0a56 h1:dMOxrFsqjuF0KHuMhQWffDrL7+LUfoJSqSus5+ckpkI=

MODIFY
ui/editor.go
ui/editor.go
package ui
import (
- "github.com/Bios-Marcel/cordless/shortcuts"
- "github.com/Bios-Marcel/cordless/ui/tviewutil"
+ "unicode"
+
"github.com/Bios-Marcel/femto"
"github.com/Bios-Marcel/tview"
"github.com/atotto/clipboard"
"github.com/gdamore/tcell"
+
+ "github.com/Bios-Marcel/cordless/config"
+ "github.com/Bios-Marcel/cordless/shortcuts"
+ "github.com/Bios-Marcel/cordless/ui/tviewutil"
)
// Editor is a simple component that wraps tview.TextView in order to gove the
@@ -17,15 +21,22 @@ type Editor struct {
tempBuffer *femto.Buffer
inputCapture func(event *tcell.EventKey) *tcell.EventKey
- mentionShowHandler func(namePart string)
- mentionHideHandler func()
+ autocompleteValuesUpdateHandler func(values []*AutocompleteValue)
+ autocompleters []*Autocomplete
+
heightRequestHandler func(requestHeight int)
requestedHeight int
- currentMentionBeginIndex int
- currentMentionEndIndex int
+ autocompleteFrom *femto.Loc
}
func (editor *Editor) applyBuffer() {
+ editor.applyBufferWithoutAutocompletionCheck()
+ if config.Current.Autocomplete {
+ editor.checkForAutocompletion()
+ }
+}
+
+func (editor *Editor) applyBufferWithoutAutocompletionCheck() {
selectionStart := editor.buffer.Cursor.CurSelection[0]
selectionEnd := editor.buffer.Cursor.CurSelection[1]
@@ -53,6 +64,51 @@ func (editor *Editor) applyBuffer() {
editor.internalTextView.SetText(editor.tempBuffer.String())
}
+func (editor *Editor) checkForAutocompletion() {
+ if editor.autocompleteValuesUpdateHandler != nil {
+ cursorLoc := editor.buffer.Cursor.Loc
+ var spaceFound bool
+ for {
+ cursorLoc.X = cursorLoc.X - 1
+ if cursorLoc.X < 0 {
+ break
+ }
+
+ runeAtCursor := editor.buffer.RuneAt(cursorLoc)
+ if runeAtCursor == ' ' {
+ spaceFound = true
+ }
+
+ for _, value := range editor.autocompleters {
+ if value.firstRune == runeAtCursor {
+ if spaceFound && !value.allowSpaces {
+ break
+ }
+
+ cursorLocCopy := cursorLoc
+ cursorLocCopy.X--
+
+ if cursorLocCopy.X >= 0 && !unicode.IsSpace(editor.buffer.RuneAt(cursorLocCopy)) {
+ break
+ }
+
+ editor.autocompleteFrom = &cursorLoc
+ //We don't want the autocomplete character to be part of the search value
+ cursorLocCopy = cursorLoc
+ cursorLocCopy.X++
+ editor.autocompleteValuesUpdateHandler(value.valueSupplier(
+ editor.buffer.Substr(cursorLocCopy, editor.buffer.Cursor.Loc)))
+ return
+ }
+
+ }
+ }
+
+ editor.autocompleteFrom = nil
+ editor.autocompleteValuesUpdateHandler(nil)
+ }
+}
+
func (editor *Editor) MoveCursorLeft() {
if editor.buffer.Cursor.HasSelection() {
editor.buffer.Cursor.GotoLoc(editor.buffer.Cursor.CurSelection[0])
@@ -374,7 +430,6 @@ func NewEditor() *Editor {
return event
}
- editor.UpdateMentionHandler()
editor.triggerHeightRequestIfNecessary()
editor.internalTextView.ScrollToHighlight()
return nil
@@ -392,33 +447,6 @@ func (editor *Editor) GetTextLeftOfSelection() string {
return editor.buffer.Substr(editor.buffer.Start(), to)
}
-func (editor *Editor) UpdateMentionHandler() {
- atSymbolIndex := editor.FindAtSymbolIndexInCurrentWord()
- if atSymbolIndex == -1 {
- editor.HideAndResetMentionHandler()
- } else {
- editor.ShowMentionHandler(atSymbolIndex)
- }
-}
-
-func (editor *Editor) ShowMentionHandler(atSymbolIndex int) {
- text := editor.GetTextLeftOfSelection()
- lookupKeyword := text[atSymbolIndex+1:]
- editor.currentMentionBeginIndex = atSymbolIndex + 1
- editor.currentMentionEndIndex = len(lookupKeyword) + atSymbolIndex
- if editor.mentionShowHandler != nil {
- editor.mentionShowHandler(lookupKeyword)
- }
-}
-
-func (editor *Editor) HideAndResetMentionHandler() {
- editor.currentMentionBeginIndex = 0
- editor.currentMentionEndIndex = 0
- if editor.mentionHideHandler != nil {
- editor.mentionHideHandler()
- }
-}
-
func (editor *Editor) FindAtSymbolIndexInCurrentWord() int {
newLeft := editor.GetTextLeftOfSelection()
for i := len(newLeft) - 1; i >= 0; i-- {
@@ -448,6 +476,35 @@ func (editor *Editor) triggerHeightRequestIfNecessary() {
}
}
+type AutocompleteValue struct {
+ RenderValue string
+ InsertValue string
+}
+
+type Autocomplete struct {
+ firstRune rune
+ allowSpaces bool
+ valueSupplier func(string) []*AutocompleteValue
+}
+
+func (editor *Editor) RegisterAutocomplete(firstRune rune, allowSpaces bool, valueSupplier func(string) []*AutocompleteValue) {
+ editor.autocompleters = append(editor.autocompleters, &Autocomplete{
+ firstRune: firstRune,
+ allowSpaces: allowSpaces,
+ valueSupplier: valueSupplier,
+ })
+}
+
+func (editor *Editor) Autocomplete(value string) {
+ if editor.autocompleteFrom != nil {
+ editor.buffer.Replace(*editor.autocompleteFrom, editor.buffer.Cursor.Loc, value)
+ editor.autocompleteFrom = nil
+ //Not necessary, since you probably don't want to autocomplete any
+ //further after you've chosen a value.
+ editor.applyBufferWithoutAutocompletionCheck()
+ }
+}
+
// GetRequestedHeight returns the currently requested size.
func (editor *Editor) GetRequestedHeight() int {
return editor.requestedHeight
@@ -496,20 +553,8 @@ func (editor *Editor) SetInputCapture(captureFunc func(event *tcell.EventKey) *t
editor.inputCapture = captureFunc
}
-// SetMentionShowHandler sets the handler for when a mention is being requested
-func (editor *Editor) SetMentionShowHandler(handlerFunc func(namePart string)) {
- editor.mentionShowHandler = handlerFunc
-}
-
-// SetMentionHideHandler sets the handler for when a mention is no longer being requested
-func (editor *Editor) SetMentionHideHandler(handlerFunc func()) {
- editor.mentionHideHandler = handlerFunc
-}
-
-// GetCurrentMentionIndices gets the starting and ending indices of the input box text
-// which are to be replaced
-func (editor *Editor) GetCurrentMentionIndices() (int, int) {
- return editor.currentMentionBeginIndex, editor.currentMentionEndIndex
+func (editor *Editor) SetAutocompleteValuesUpdateHandler(handlerFunc func(values []*AutocompleteValue)) {
+ editor.autocompleteValuesUpdateHandler = handlerFunc
}
// GetText returns the text without color tags, region tags and so on.

MODIFY
ui/window.go
ui/window.go
@@ -11,6 +11,7 @@ import (
"github.com/mattn/go-runewidth"
"github.com/mdp/qrterminal/v3"
+ "github.com/Bios-Marcel/cordless/util/fuzzy"
"github.com/Bios-Marcel/cordless/util/text"
"github.com/Bios-Marcel/discordemojimap"
@@ -31,7 +32,6 @@ import (
"github.com/Bios-Marcel/cordless/scripting/js"
"github.com/Bios-Marcel/cordless/shortcuts"
"github.com/Bios-Marcel/cordless/ui/tviewutil"
- "github.com/Bios-Marcel/cordless/util/fuzzy"
"github.com/Bios-Marcel/cordless/util/maths"
)
@@ -129,13 +129,13 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
guilds := readyEvent.Guilds
mentionWindowRootNode := tview.NewTreeNode("")
- mentionWindow := tview.NewTreeView().
+ autocompleteView := tview.NewTreeView().
SetVimBindingsEnabled(false).
SetRoot(mentionWindowRootNode).
SetTopLevel(1).
SetCycleSelection(true)
- mentionWindow.SetBorder(true)
- mentionWindow.SetBorderSides(false, true, false, true)
+ autocompleteView.SetBorder(true)
+ autocompleteView.SetBorderSides(false, true, false, true)
window.leftArea = tview.NewPages()
@@ -369,16 +369,190 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
window.chatArea.ResizeItem(window.messageInput.GetPrimitive(), newHeight, 0)
})
- window.messageInput.SetMentionShowHandler(func(namePart string) {
- mentionWindow.GetRoot().ClearChildren()
- window.PopulateMentionWindow(mentionWindow, namePart)
- if !window.ShowMentionWindowChildren(mentionWindow, 10) {
- window.HideMentionWindow(mentionWindow)
+ window.messageInput.SetAutocompleteValuesUpdateHandler(func(values []*AutocompleteValue) {
+ autocompleteView.GetRoot().ClearChildren()
+ if len(values) == 0 {
+ autocompleteView.SetVisible(false)
+ window.app.SetFocus(window.messageInput.GetPrimitive())
+ } else {
+ rootNode := autocompleteView.GetRoot()
+ for _, value := range values {
+ newNode := tview.NewTreeNode(value.RenderValue)
+ newNode.SetReference(value)
+ rootNode.AddChild(newNode)
+ }
+ autocompleteView.SetCurrentNode(rootNode)
+ autocompleteView.SetVisible(true)
+ window.app.SetFocus(autocompleteView)
+ window.chatArea.ResizeItem(autocompleteView, maths.Min(10, len(values)), 0)
}
})
- window.messageInput.SetMentionHideHandler(func() {
- window.HideMentionWindow(mentionWindow)
+ autocompleteView.SetSelectedFunc(func(node *tview.TreeNode) {
+ value := node.GetReference().(*AutocompleteValue)
+ window.messageInput.Autocomplete(value.InsertValue)
+ window.app.SetFocus(window.messageInput.GetPrimitive())
+ autocompleteView.SetVisible(false)
+ })
+
+ window.messageInput.RegisterAutocomplete('#', false, func(value string) []*AutocompleteValue {
+ if window.selectedChannel != nil && window.selectedChannel.GuildID != "" {
+ guild, stateError := session.State.Guild(window.selectedChannel.GuildID)
+ if stateError != nil {
+ return nil
+ }
+
+ filtered := fuzzy.ScoreAndSortChannels(value, guild.Channels)
+ var autocompleteValues []*AutocompleteValue
+ for _, channel := range filtered {
+ if channel.Type != discordgo.ChannelTypeGuildText {
+ continue
+ }
+
+ autocompleteValues = append(autocompleteValues, &AutocompleteValue{
+ RenderValue: channel.Name,
+ InsertValue: "<#" + channel.ID + ">",
+ })
+ }
+
+ return autocompleteValues
+ }
+
+ return nil
+ })
+
+ window.messageInput.RegisterAutocomplete('@', true, func(value string) []*AutocompleteValue {
+ if window.selectedChannel != nil {
+ guildID := window.selectedChannel.GuildID
+ var autocompleteValues []*AutocompleteValue
+ if guildID != "" {
+ guild, stateError := session.State.Guild(guildID)
+ if stateError == nil {
+ filteredRoles := fuzzy.ScoreAndSortRoles(value, guild.Roles)
+ for _, role := range filteredRoles {
+ autocompleteValues = append(autocompleteValues, &AutocompleteValue{
+ RenderValue: role.Name,
+ //FIXME Inconsistent, we should change this.
+ InsertValue: "<@&" + role.ID + ">",
+ })
+ }
+
+ filteredMembers := fuzzy.ScoreAndSortMembers(value, guild.Members)
+ for _, member := range filteredMembers {
+ insertValue := member.User.Username + "#" + member.User.Discriminator
+ var renderValue string
+ if member.Nick != "" {
+ renderValue = insertValue + " | " + member.Nick
+ } else {
+ renderValue = insertValue
+ }
+ autocompleteValues = append(autocompleteValues, &AutocompleteValue{
+ RenderValue: renderValue,
+ InsertValue: "@" + insertValue,
+ })
+ }
+
+ return autocompleteValues
+ }
+ }
+
+ filtered := fuzzy.ScoreAndSortUsers(value, window.selectedChannel.Recipients)
+ for _, user := range filtered {
+ insertValue := user.Username + "#" + user.Discriminator
+ autocompleteValues = append(autocompleteValues, &AutocompleteValue{
+ RenderValue: insertValue,
+ InsertValue: "@" + insertValue,
+ })
+ }
+
+ return autocompleteValues
+ }
+
+ return nil
+ })
+
+ emojisAsArray := make([]string, 0, len(discordemojimap.EmojiMap))
+ for emoji, _ := range discordemojimap.EmojiMap {
+ emojisAsArray = append(emojisAsArray, emoji)
+ }
+
+ var globallyUsableCustomEmoji []*discordgo.Emoji
+ if window.session.State.User.PremiumType == discordgo.UserPremiumTypeNone {
+ for _, guild := range window.session.State.Guilds {
+ for _, emoji := range guild.Emojis {
+ if emoji.Animated {
+ continue
+ }
+
+ if !strings.HasPrefix(emoji.Name, "GW") {
+ continue
+ }
+
+ globallyUsableCustomEmoji = append(globallyUsableCustomEmoji, emoji)
+ }
+ }
+ } else {
+ for _, guild := range window.session.State.Guilds {
+ for _, emoji := range guild.Emojis {
+ globallyUsableCustomEmoji = append(globallyUsableCustomEmoji, emoji)
+ }
+ }
+ }
+
+ emojisByGuild := make(map[string][]*discordgo.Emoji)
+
+ window.messageInput.RegisterAutocomplete(':', false, func(value string) []*AutocompleteValue {
+ var autocompleteValues []*AutocompleteValue
+
+ var filteredCustomEmoji []*discordgo.Emoji
+ if window.session.State.User.PremiumType == discordgo.UserPremiumTypeNone {
+ if window.selectedChannel != nil && window.selectedChannel.GuildID != "" {
+ guildID := window.selectedChannel.GuildID
+ var cached bool
+ filteredCustomEmoji, cached = emojisByGuild[guildID]
+ if cached {
+ goto EVALUATE_EMOJIS
+ }
+
+ guild, stateError := window.session.State.Guild(guildID)
+ if stateError == nil {
+ for _, emoji := range guild.Emojis {
+ if emoji.Animated {
+ continue
+ }
+
+ filteredCustomEmoji = append(filteredCustomEmoji, emoji)
+ }
+
+ filteredCustomEmoji = append(filteredCustomEmoji, globallyUsableCustomEmoji...)
+ emojisByGuild[window.selectedChannel.GuildID] = filteredCustomEmoji
+
+ goto EVALUATE_EMOJIS
+ }
+ }
+ }
+
+ EVALUATE_EMOJIS:
+ filteredEmoji := fuzzy.ScoreAndSortEmoji(value, emojisAsArray, filteredCustomEmoji)
+ for _, emoji := range filteredEmoji {
+ unicodeSymbol := discordemojimap.GetEmoji(emoji)
+ var renderValue string
+ var insertValue string
+ if unicodeSymbol != "" {
+ renderValue = unicodeSymbol + " | " + emoji
+ insertValue = unicodeSymbol
+ } else {
+ trimmed := emoji[1:]
+ renderValue = "? | " + trimmed + " (custom emoji)"
+ insertValue = ":!" + trimmed + ":"
+ }
+ autocompleteValues = append(autocompleteValues, &AutocompleteValue{
+ RenderValue: renderValue,
+ InsertValue: insertValue,
+ })
+ }
+
+ return autocompleteValues
})
window.messageInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
@@ -874,8 +1048,8 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
window.middleContainer.AddItem(window.userList.internalTreeView, 0, 6, false)
}
- mentionWindow.SetVisible(false)
- mentionWindow.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ autocompleteView.SetVisible(false)
+ autocompleteView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch key := event.Key(); key {
case tcell.KeyRune, tcell.KeyDelete, tcell.KeyBackspace, tcell.KeyBackspace2, tcell.KeyLeft, tcell.KeyRight, tcell.KeyCtrlA, tcell.KeyCtrlV:
window.messageInput.internalTextView.GetInputCapture()(event)
@@ -884,30 +1058,8 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
return event
})
- // Invoked when enter is pressed
- mentionWindow.SetSelectedFunc(func(node *tview.TreeNode) {
- beginIndex, endIndex := window.messageInput.GetCurrentMentionIndices()
- var mentionInsertion string
- oldText := window.messageInput.GetText()
- reference := node.GetReference()
- switch reference.(type) {
- case string:
- data := reference.(string)
- mentionInsertion = strings.TrimSpace(data)
- case *discordgo.Role:
- role := reference.(*discordgo.Role)
- mentionInsertion = "<@&" + strings.TrimSpace(role.ID) + ">"
- default:
- panic(fmt.Sprintf("Invalid data type in mention handler: %t:%v", reference, reference))
- }
-
- newMessageText := oldText[:beginIndex] + mentionInsertion + " " + oldText[endIndex+1:]
- window.messageInput.SetText(newMessageText)
- window.messageInput.mentionHideHandler()
- })
-
window.chatArea.AddItem(window.messageContainer, 0, 1, false)
- window.chatArea.AddItem(mentionWindow, 2, 2, true)
+ window.chatArea.AddItem(autocompleteView, 2, 2, true)
window.chatArea.AddItem(window.messageInput.GetPrimitive(), window.messageInput.GetRequestedHeight(), 0, false)
window.commandView.commandOutput.SetVisible(false)
@@ -1100,117 +1252,6 @@ func (window *Window) sendMessage(targetChannelID, message string) {
}
}
-func (window *Window) HideMentionWindow(mentionWindow *tview.TreeView) {
- mentionWindow.SetVisible(false)
- window.app.SetFocus(window.messageInput.internalTextView)
-}
-
-func (window *Window) ShowMentionWindowChildren(mentionWindow *tview.TreeView, maxChildren int) bool {
- children := mentionWindow.GetRoot().GetChildren()
- if children == nil {
- return false
- }
- numChildren := maths.Min(len(children), 10)
- window.chatArea.ResizeItem(mentionWindow, numChildren, 0)
- if numChildren > 0 {
- mentionWindow.SetCurrentNode(children[0])
- }
- mentionWindow.SetVisible(true)
- window.app.SetFocus(mentionWindow)
- return true
-}
-
-func (window *Window) PopulateMentionWindow(mentionWindow *tview.TreeView, namePart string) {
- 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 {
- return
- }
-
- nameMap := make(map[string]string)
- var memberNames []string
- for _, user := range guild.Members {
- userName := user.User.Username + "#" + user.User.Discriminator
- if len(user.Nick) > 0 {
- 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)
- }
- }
-
- roleMap := make(map[string]*discordgo.Role)
- for _, role := range guild.Roles {
- roleMap[role.Name] = role
- memberNames = append(memberNames, role.Name)
- }
-
- searchResults := fuzzy.ScoreSearch(namePart, memberNames)
- sortedResults := fuzzy.SortSearchResults(searchResults)
-
- userWithNickSet := make(map[string]struct{})
- for _, result := range sortedResults {
- // Check if result was a role.
- if role, ok := roleMap[result.Key]; ok {
- window.addNodeToMentionWindow(mentionWindow, role.Name, role)
- continue
- }
-
- var displayName string = result.Key
- var userMentionReference string = result.Key
- if combinedUserAndNickName, ok := nameMap[result.Key]; ok {
- // If the combined string has been added, skip this entry.
- if _, containsStr := userWithNickSet[combinedUserAndNickName]; containsStr {
- continue
- }
- userWithNickSet[combinedUserAndNickName] = struct{}{}
- displayName = combinedUserAndNickName
- userMentionReference = nameMap[combinedUserAndNickName]
- }
- window.addNodeToMentionWindow(mentionWindow, displayName, userMentionReference)
- }
-}
-
-func (window *Window) addNodeToMentionWindow(mentionWindow *tview.TreeView, name string, reference interface{}) {
- userNode := tview.NewTreeNode(name)
- userNode.SetReference(reference)
- mentionWindow.GetRoot().AddChild(userNode)
-}
-
-func (window *Window) PopulateMentionWindowFromCurrentChannel(mentionWindow *tview.TreeView, namePart string) {
- memberNames := make([]string, 0, len(window.selectedChannel.Recipients))
- for _, user := range window.selectedChannel.Recipients {
- 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) {
if isSelected {
guildNode.SetColor(tview.Styles.ContrastBackgroundColor)

MODIFY
util/fuzzy/fuzzy.go
util/fuzzy/fuzzy.go
@@ -4,6 +4,8 @@ import (
"sort"
"strings"
"unicode"
+
+ "github.com/Bios-Marcel/discordgo"
)
type SearchResult struct {
@@ -11,6 +13,188 @@ type SearchResult struct {
Value float64
}
+func ScoreAndSortRoles(searchTerm string, searchables []*discordgo.Role) []*discordgo.Role {
+ if len(searchTerm) == 0 {
+ return searchables
+ }
+
+ searchableNameCache := make(map[*discordgo.Role]string)
+ scoredItems := make(map[*discordgo.Role]float64, len(searchables))
+ for _, searchable := range searchables {
+ searchableValue := searchable.Name
+ score := Score(searchTerm, searchableValue)
+ if score > 0 {
+ searchableNameCache[searchable] = searchableValue
+ scoredItems[searchable] = score
+ }
+ }
+
+ // Convert the results into an array.
+ sortedItems := make([]*discordgo.Role, 0, len(scoredItems))
+ for key, _ := range scoredItems {
+ sortedItems = append(sortedItems, key)
+ }
+
+ // Sort results based on score. Equal scores are sorted alphabetically.
+ sort.Slice(sortedItems, func(a, b int) bool {
+ aItem := sortedItems[a]
+ bItem := sortedItems[b]
+ scoreA := scoredItems[aItem]
+ scoreB := scoredItems[bItem]
+ if scoreA == scoreB {
+ return strings.Compare(strings.ToLower(searchableNameCache[aItem]), strings.ToLower(searchableNameCache[bItem])) < 0
+ }
+ return scoreA > scoreB
+ })
+ return sortedItems
+}
+
+func ScoreAndSortMembers(searchTerm string, searchables []*discordgo.Member) []*discordgo.Member {
+ if len(searchTerm) == 0 {
+ return searchables
+ }
+
+ searchableNameCache := make(map[*discordgo.Member]string)
+ scoredItems := make(map[*discordgo.Member]float64, len(searchables))
+ for _, searchable := range searchables {
+ searchableValue := searchable.User.Username + "#" + searchable.User.Discriminator + " | " + searchable.Nick
+ score := Score(searchTerm, searchableValue)
+ if score > 0 {
+ searchableNameCache[searchable] = searchableValue
+ scoredItems[searchable] = score
+ }
+ }
+
+ // Convert the results into an array.
+ sortedItems := make([]*discordgo.Member, 0, len(scoredItems))
+ for key, _ := range scoredItems {
+ sortedItems = append(sortedItems, key)
+ }
+
+ // Sort results based on score. Equal scores are sorted alphabetically.
+ sort.Slice(sortedItems, func(a, b int) bool {
+ aItem := sortedItems[a]
+ bItem := sortedItems[b]
+ scoreA := scoredItems[aItem]
+ scoreB := scoredItems[bItem]
+ if scoreA == scoreB {
+ return strings.Compare(strings.ToLower(searchableNameCache[aItem]), strings.ToLower(searchableNameCache[bItem])) < 0
+ }
+ return scoreA > scoreB
+ })
+ return sortedItems
+}
+
+func ScoreAndSortUsers(searchTerm string, searchables []*discordgo.User) []*discordgo.User {
+ if len(searchTerm) == 0 {
+ return searchables
+ }
+
+ searchableNameCache := make(map[*discordgo.User]string)
+ scoredItems := make(map[*discordgo.User]float64, len(searchables))
+ for _, searchable := range searchables {
+ searchableValue := searchable.Username + "#" + searchable.Discriminator
+ score := Score(searchTerm, searchableValue)
+ if score > 0 {
+ searchableNameCache[searchable] = searchableValue
+ scoredItems[searchable] = score
+ }
+ }
+
+ // Convert the results into an array.
+ sortedItems := make([]*discordgo.User, 0, len(scoredItems))
+ for key, _ := range scoredItems {
+ sortedItems = append(sortedItems, key)
+ }
+
+ // Sort results based on score. Equal scores are sorted alphabetically.
+ sort.Slice(sortedItems, func(a, b int) bool {
+ aItem := sortedItems[a]
+ bItem := sortedItems[b]
+ scoreA := scoredItems[aItem]
+ scoreB := scoredItems[bItem]
+ if scoreA == scoreB {
+ return strings.Compare(strings.ToLower(searchableNameCache[aItem]), strings.ToLower(searchableNameCache[bItem])) < 0
+ }
+ return scoreA > scoreB
+ })
+ return sortedItems
+}
+
+func ScoreAndSortEmoji(searchTerm string, unicodeEmoji []string, customEmoji []*discordgo.Emoji) []string {
+ if len(searchTerm) == 0 {
+ //For now, we just returned the normal ones in case no keyword was given
+ return unicodeEmoji
+ }
+
+ scoredItems := make(map[string]float64, len(unicodeEmoji)+len(customEmoji))
+ for _, searchable := range unicodeEmoji {
+ score := Score(searchTerm, searchable)
+ if score > 0 {
+ scoredItems[searchable] = score
+ }
+ }
+ for _, searchable := range customEmoji {
+ score := Score(searchTerm, searchable.Name)
+ if score > 0 {
+ //Little hack to allow duplicate names
+ scoredItems[" "+searchable.Name] = score
+ }
+ }
+
+ // Convert the results into an array.
+ sortedItems := make([]string, 0, len(scoredItems))
+ for key, _ := range scoredItems {
+ sortedItems = append(sortedItems, key)
+ }
+
+ // Sort results based on score. Equal scores are sorted alphabetically.
+ sort.Slice(sortedItems, func(a, b int) bool {
+ aItem := sortedItems[a]
+ bItem := sortedItems[b]
+ scoreA := scoredItems[aItem]
+ scoreB := scoredItems[bItem]
+ if scoreA == scoreB {
+ return strings.Compare(strings.ToLower(aItem), strings.ToLower(bItem)) < 0
+ }
+ return scoreA > scoreB
+ })
+ return sortedItems
+}
+
+func ScoreAndSortChannels(searchTerm string, searchables []*discordgo.Channel) []*discordgo.Channel {
+ if len(searchTerm) == 0 {
+ return searchables
+ }
+
+ scoredItems := make(map[*discordgo.Channel]float64, len(searchables))
+ for _, searchable := range searchables {
+ score := Score(searchTerm, searchable.Name)
+ if score > 0 {
+ scoredItems[searchable] = score
+ }
+ }
+
+ // Convert the results into an array.
+ sortedItems := make([]*discordgo.Channel, 0, len(scoredItems))
+ for key, _ := range scoredItems {
+ sortedItems = append(sortedItems, key)
+ }
+
+ // Sort results based on score. Equal scores are sorted alphabetically.
+ sort.Slice(sortedItems, func(a, b int) bool {
+ aItem := sortedItems[a]
+ bItem := sortedItems[b]
+ scoreA := scoredItems[aItem]
+ scoreB := scoredItems[bItem]
+ if scoreA == scoreB {
+ return strings.Compare(strings.ToLower(aItem.Name), strings.ToLower(bItem.Name)) < 0
+ }
+ return scoreA > scoreB
+ })
+ return sortedItems
+}
+
func ScoreSearch(searchTerm string, searchItems []string) map[string]float64 {
mymap := make(map[string]float64)
for _, str := range searchItems {
@@ -27,7 +211,7 @@ func SortSearchResults(results map[string]float64) []SearchResult {
arr = append(arr, SearchResult{key, value})
}
}
- // Sort results based on score. Equal scores are sorted alphbetically.
+ // Sort results based on score. Equal scores are sorted alphabetically.
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

Autocompleting has been completly rewritten.

It now works for:
* Emojis
* Custom Emojis
* Roles
* Users

It can be disabled by setting `Autocomplete` to `false`.