aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTulir Asokan <tulir@maunium.net>2018-04-21 19:41:19 +0300
committerTulir Asokan <tulir@maunium.net>2018-04-21 19:41:19 +0300
commitd147fc7579bf77bf6f3ace669c8ade68be89d1ca (patch)
treee19b9409e15feecd9b9317c86408da37fb18a0e4
parentc3386ba118b1a0f2ae1a31a9787ea5cb8b68396f (diff)
Improve tab completion system
-rw-r--r--interface/ui.go2
-rw-r--r--lib/util/lcp.go38
-rw-r--r--ui/room-view.go120
-rw-r--r--ui/view-main.go26
-rw-r--r--ui/widget/advanced-inputfield.go25
5 files changed, 154 insertions, 57 deletions
diff --git a/interface/ui.go b/interface/ui.go
index cdd1927..83fa7da 100644
--- a/interface/ui.go
+++ b/interface/ui.go
@@ -77,7 +77,7 @@ type RoomView interface {
SaveHistory(dir string) error
LoadHistory(matrix MatrixContainer, dir string) (int, error)
- SetStatus(status string)
+ SetCompletions(completions []string)
SetTyping(users []string)
UpdateUserList()
diff --git a/lib/util/lcp.go b/lib/util/lcp.go
new file mode 100644
index 0000000..2e2e690
--- /dev/null
+++ b/lib/util/lcp.go
@@ -0,0 +1,38 @@
+// Licensed under the GNU Free Documentation License 1.2
+// https://www.gnu.org/licenses/old-licenses/fdl-1.2.en.html
+//
+// Source: https://rosettacode.org/wiki/Longest_common_prefix#Go
+
+package util
+
+func LongestCommonPrefix(list []string) string {
+ // Special cases first
+ switch len(list) {
+ case 0:
+ return ""
+ case 1:
+ return list[0]
+ }
+
+ // LCP of min and max (lexigraphically)
+ // is the LCP of the whole set.
+ min, max := list[0], list[0]
+ for _, s := range list[1:] {
+ switch {
+ case s < min:
+ min = s
+ case s > max:
+ max = s
+ }
+ }
+
+ for i := 0; i < len(min) && i < len(max); i++ {
+ if min[i] != max[i] {
+ return min[:i]
+ }
+ }
+
+ // In the case where lengths are not equal but all bytes
+ // are equal, min is the answer ("foo" < "foobar").
+ return min
+}
diff --git a/ui/room-view.go b/ui/room-view.go
index 5268682..332605b 100644
--- a/ui/room-view.go
+++ b/ui/room-view.go
@@ -19,11 +19,14 @@ package ui
import (
"fmt"
"path/filepath"
+ "sort"
"strconv"
"strings"
"time"
+ "github.com/mattn/go-runewidth"
"maunium.net/go/gomuks/interface"
+ "maunium.net/go/gomuks/lib/util"
"maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/messages"
"maunium.net/go/gomuks/ui/widget"
@@ -41,6 +44,13 @@ type RoomView struct {
ulBorder *widget.Border
input *widget.AdvancedInputField
Room *rooms.Room
+
+ typing []string
+ completions struct {
+ list []string
+ textCache string
+ time time.Time
+ }
}
func NewRoomView(room *rooms.Room) *RoomView {
@@ -58,7 +68,8 @@ func NewRoomView(room *rooms.Room) *RoomView {
view.input.
SetFieldBackgroundColor(tcell.ColorDefault).
SetPlaceholder("Send a message...").
- SetPlaceholderExtColor(tcell.ColorGray)
+ SetPlaceholderExtColor(tcell.ColorGray).
+ SetTabCompleteFunc(view.InputTabComplete)
view.topic.
SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)).
@@ -85,13 +96,6 @@ func (view *RoomView) LoadHistory(matrix ifc.MatrixContainer, dir string) (int,
return view.MessageView().LoadHistory(matrix, view.logPath(dir))
}
-func (view *RoomView) SetTabCompleteFunc(fn func(room *RoomView, text string, cursorOffset int) string) *RoomView {
- view.input.SetTabCompleteFunc(func(text string, cursorOffset int) string {
- return fn(view, text, cursorOffset)
- })
- return view
-}
-
func (view *RoomView) SetInputCapture(fn func(room *RoomView, event *tcell.EventKey) *tcell.EventKey) *RoomView {
view.input.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
return fn(view, event)
@@ -151,6 +155,30 @@ const (
StaticVerticalSpace = TopicBarHeight + StatusBarHeight + InputBarHeight
)
+func (view *RoomView) GetStatus() string {
+ var buf strings.Builder
+
+ if len(view.completions.list) > 0 {
+ if view.completions.textCache != view.input.GetText() || view.completions.time.Add(10 * time.Second).Before(time.Now()) {
+ view.completions.list = []string{}
+ } else {
+ buf.WriteString(strings.Join(view.completions.list, ", "))
+ buf.WriteString(" - ")
+ }
+ }
+
+ if len(view.typing) == 1 {
+ buf.WriteString("Typing: " + view.typing[0])
+ buf.WriteString(" - ")
+ } else if len(view.typing) > 1 {
+ fmt.Fprintf(&buf,
+ "Typing: %s and %s - ",
+ strings.Join(view.typing[:len(view.typing)-1], ", "), view.typing[len(view.typing)-1])
+ }
+
+ return strings.TrimSuffix(buf.String(), " - ")
+}
+
func (view *RoomView) Draw(screen tcell.Screen) {
x, y, width, height := view.GetInnerRect()
if width <= 0 || height <= 0 {
@@ -185,14 +213,17 @@ func (view *RoomView) Draw(screen tcell.Screen) {
view.Box.Draw(screen)
view.topic.Draw(screen)
view.content.Draw(screen)
+ view.status.SetText(view.GetStatus())
view.status.Draw(screen)
view.input.Draw(screen)
view.ulBorder.Draw(screen)
view.userList.Draw(screen)
}
-func (view *RoomView) SetStatus(status string) {
- view.status.SetText(status)
+func (view *RoomView) SetCompletions(completions []string) {
+ view.completions.list = completions
+ view.completions.textCache = view.input.GetText()
+ view.completions.time = time.Now()
}
func (view *RoomView) SetTyping(users []string) {
@@ -202,31 +233,74 @@ func (view *RoomView) SetTyping(users []string) {
users[index] = member.DisplayName
}
}
- if len(users) == 0 {
- view.status.SetText("")
- } else if len(users) < 2 {
- view.status.SetText("Typing: " + strings.Join(users, " and "))
- } else {
- view.status.SetText(fmt.Sprintf(
- "Typing: %s and %s",
- strings.Join(users[:len(users)-1], ", "), users[len(users)-1]))
- }
+ view.typing = users
+}
+
+type completion struct {
+ displayName string
+ id string
}
-func (view *RoomView) AutocompleteUser(existingText string) (completions []*rooms.Member) {
+func (view *RoomView) AutocompleteUser(existingText string) (completions []completion) {
textWithoutPrefix := existingText
if strings.HasPrefix(existingText, "@") {
textWithoutPrefix = existingText[1:]
}
for _, user := range view.Room.GetMembers() {
- if strings.HasPrefix(user.DisplayName, textWithoutPrefix) ||
- strings.HasPrefix(user.UserID, existingText) {
- completions = append(completions, user)
+ if user.DisplayName == textWithoutPrefix || user.UserID == existingText {
+ // Exact match, return that.
+ return []completion{{user.DisplayName, user.UserID}}
+ }
+
+ if strings.HasPrefix(user.DisplayName, textWithoutPrefix) || strings.HasPrefix(user.UserID, existingText) {
+ completions = append(completions, completion{user.DisplayName, user.UserID})
}
}
return
}
+func (view *RoomView) AutocompleteRoom(existingText string) (completions []completion) {
+ // TODO - This was harder than I expected.
+
+ return []completion{}
+}
+
+func (view *RoomView) InputTabComplete(text string, cursorOffset int) {
+ str := runewidth.Truncate(text, cursorOffset, "")
+ word := findWordToTabComplete(str)
+ startIndex := len(str) - len(word)
+
+ var strCompletions []string
+ var strCompletion string
+
+ completions := view.AutocompleteUser(word)
+ completions = append(completions, view.AutocompleteRoom(word)...)
+
+ if len(completions) == 1 {
+ completion := completions[0]
+ strCompletion = fmt.Sprintf("[%s](https://matrix.to/#/%s)", completion.displayName, completion.id)
+ if startIndex == 0 {
+ strCompletion = strCompletion + ": "
+ }
+ } else if len(completions) > 1 {
+ for _, completion := range completions {
+ strCompletions = append(strCompletions, completion.displayName)
+ }
+ }
+
+ if len(strCompletions) > 0 {
+ strCompletion = util.LongestCommonPrefix(strCompletions)
+ sort.Sort(sort.StringSlice(strCompletions))
+ }
+
+ if len(strCompletion) > 0 {
+ text = str[0:startIndex] + strCompletion + text[len(str):]
+ }
+
+ view.input.SetTextAndMoveCursor(text)
+ view.SetCompletions(strCompletions)
+}
+
func (view *RoomView) MessageView() *MessageView {
return view.content
}
diff --git a/ui/view-main.go b/ui/view-main.go
index c3e94a5..3034102 100644
--- a/ui/view-main.go
+++ b/ui/view-main.go
@@ -23,7 +23,6 @@ import (
"time"
"unicode"
- "github.com/mattn/go-runewidth"
"maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug"
@@ -101,28 +100,6 @@ func findWordToTabComplete(text string) string {
return output
}
-func (view *MainView) InputTabComplete(roomView *RoomView, text string, cursorOffset int) string {
- str := runewidth.Truncate(text, cursorOffset, "")
- word := findWordToTabComplete(str)
-
- userCompletions := roomView.AutocompleteUser(word)
- if len(userCompletions) == 1 {
- startIndex := len(str) - len(word)
- member := userCompletions[0]
- completion := fmt.Sprintf("[%s](https://matrix.to/#/%s)", member.DisplayName, member.UserID)
- if startIndex == 0 {
- completion = completion + ": "
- }
- text = str[0:startIndex] + completion + text[len(str):]
- } else if len(userCompletions) > 1 && len(userCompletions) <= 5 {
- // roomView.SetStatus(fmt.Sprintf("Completions: %s", strings.Join(userCompletions, ", ")))
- } else if len(userCompletions) > 5 {
- roomView.SetStatus("Over 5 completion options.")
- }
-
- return text
-}
-
func (view *MainView) InputSubmit(roomView *RoomView, text string) {
if len(text) == 0 {
return
@@ -147,7 +124,7 @@ func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Messag
eventID, err := view.matrix.SendMarkdownMessage(roomView.Room.ID, tempMessage.Type(), text)
if err != nil {
tempMessage.SetState(ifc.MessageStateFailed)
- roomView.SetStatus(fmt.Sprintf("Failed to send message: %s", err))
+ roomView.AddServiceMessage(fmt.Sprintf("Failed to send message: %v", err))
} else {
roomView.MessageView().UpdateMessageID(tempMessage, eventID)
}
@@ -314,7 +291,6 @@ func (view *MainView) addRoom(index int, room string) {
roomView := NewRoomView(roomStore).
SetInputSubmitFunc(view.InputSubmit).
SetInputChangedFunc(view.InputChanged).
- SetTabCompleteFunc(view.InputTabComplete).
SetInputCapture(view.KeyEventHandler).
SetMouseCapture(view.MouseEventHandler)
view.rooms[room] = roomView
diff --git a/ui/widget/advanced-inputfield.go b/ui/widget/advanced-inputfield.go
index cbe9dcc..a084dcf 100644
--- a/ui/widget/advanced-inputfield.go
+++ b/ui/widget/advanced-inputfield.go
@@ -84,7 +84,7 @@ type AdvancedInputField struct {
done func(tcell.Key)
// An optional function which is called when the user presses tab.
- tabComplete func(text string, cursorOffset int) string
+ tabComplete func(text string, cursorOffset int)
}
// NewAdvancedInputField returns a new input field.
@@ -107,6 +107,20 @@ func (field *AdvancedInputField) SetText(text string) *AdvancedInputField {
return field
}
+// SetTextAndMoveCursor sets the current text of the input field and moves the cursor with the width difference.
+func (field *AdvancedInputField) SetTextAndMoveCursor(text string) *AdvancedInputField {
+ oldWidth := runewidth.StringWidth(field.text)
+ field.text = text
+ newWidth := runewidth.StringWidth(field.text)
+ if oldWidth != newWidth {
+ field.cursorOffset += newWidth - oldWidth
+ }
+ if field.changed != nil {
+ field.changed(field.text)
+ }
+ return field
+}
+
// GetText returns the current text of the input field.
func (field *AdvancedInputField) GetText() string {
return field.text
@@ -212,7 +226,7 @@ func (field *AdvancedInputField) SetDoneFunc(handler func(key tcell.Key)) *Advan
return field
}
-func (field *AdvancedInputField) SetTabCompleteFunc(handler func(text string, cursorOffset int) string) *AdvancedInputField {
+func (field *AdvancedInputField) SetTabCompleteFunc(handler func(text string, cursorOffset int)) *AdvancedInputField {
field.tabComplete = handler
return field
}
@@ -443,12 +457,7 @@ func (field *AdvancedInputField) RemovePreviousCharacter() {
func (field *AdvancedInputField) TriggerTabComplete() bool {
if field.tabComplete != nil {
- oldWidth := runewidth.StringWidth(field.text)
- field.text = field.tabComplete(field.text, field.cursorOffset)
- newWidth := runewidth.StringWidth(field.text)
- if oldWidth != newWidth {
- field.cursorOffset += newWidth - oldWidth
- }
+ field.tabComplete(field.text, field.cursorOffset)
return true
}
return false