Improve tab completion system

This commit is contained in:
Tulir Asokan 2018-04-21 19:41:19 +03:00
parent c3386ba118
commit d147fc7579
5 changed files with 154 additions and 57 deletions

View File

@ -77,7 +77,7 @@ type RoomView interface {
SaveHistory(dir string) error SaveHistory(dir string) error
LoadHistory(matrix MatrixContainer, dir string) (int, error) LoadHistory(matrix MatrixContainer, dir string) (int, error)
SetStatus(status string) SetCompletions(completions []string)
SetTyping(users []string) SetTyping(users []string)
UpdateUserList() UpdateUserList()

38
lib/util/lcp.go Normal file
View File

@ -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
}

View File

@ -19,11 +19,14 @@ package ui
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/mattn/go-runewidth"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/lib/util"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/messages"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
@ -41,6 +44,13 @@ type RoomView struct {
ulBorder *widget.Border ulBorder *widget.Border
input *widget.AdvancedInputField input *widget.AdvancedInputField
Room *rooms.Room Room *rooms.Room
typing []string
completions struct {
list []string
textCache string
time time.Time
}
} }
func NewRoomView(room *rooms.Room) *RoomView { func NewRoomView(room *rooms.Room) *RoomView {
@ -58,7 +68,8 @@ func NewRoomView(room *rooms.Room) *RoomView {
view.input. view.input.
SetFieldBackgroundColor(tcell.ColorDefault). SetFieldBackgroundColor(tcell.ColorDefault).
SetPlaceholder("Send a message..."). SetPlaceholder("Send a message...").
SetPlaceholderExtColor(tcell.ColorGray) SetPlaceholderExtColor(tcell.ColorGray).
SetTabCompleteFunc(view.InputTabComplete)
view.topic. view.topic.
SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)). 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)) 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 { func (view *RoomView) SetInputCapture(fn func(room *RoomView, event *tcell.EventKey) *tcell.EventKey) *RoomView {
view.input.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { view.input.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
return fn(view, event) return fn(view, event)
@ -151,6 +155,30 @@ const (
StaticVerticalSpace = TopicBarHeight + StatusBarHeight + InputBarHeight 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) { func (view *RoomView) Draw(screen tcell.Screen) {
x, y, width, height := view.GetInnerRect() x, y, width, height := view.GetInnerRect()
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
@ -185,14 +213,17 @@ func (view *RoomView) Draw(screen tcell.Screen) {
view.Box.Draw(screen) view.Box.Draw(screen)
view.topic.Draw(screen) view.topic.Draw(screen)
view.content.Draw(screen) view.content.Draw(screen)
view.status.SetText(view.GetStatus())
view.status.Draw(screen) view.status.Draw(screen)
view.input.Draw(screen) view.input.Draw(screen)
view.ulBorder.Draw(screen) view.ulBorder.Draw(screen)
view.userList.Draw(screen) view.userList.Draw(screen)
} }
func (view *RoomView) SetStatus(status string) { func (view *RoomView) SetCompletions(completions []string) {
view.status.SetText(status) view.completions.list = completions
view.completions.textCache = view.input.GetText()
view.completions.time = time.Now()
} }
func (view *RoomView) SetTyping(users []string) { func (view *RoomView) SetTyping(users []string) {
@ -202,31 +233,74 @@ func (view *RoomView) SetTyping(users []string) {
users[index] = member.DisplayName users[index] = member.DisplayName
} }
} }
if len(users) == 0 { view.typing = users
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]))
}
} }
func (view *RoomView) AutocompleteUser(existingText string) (completions []*rooms.Member) { type completion struct {
displayName string
id string
}
func (view *RoomView) AutocompleteUser(existingText string) (completions []completion) {
textWithoutPrefix := existingText textWithoutPrefix := existingText
if strings.HasPrefix(existingText, "@") { if strings.HasPrefix(existingText, "@") {
textWithoutPrefix = existingText[1:] textWithoutPrefix = existingText[1:]
} }
for _, user := range view.Room.GetMembers() { for _, user := range view.Room.GetMembers() {
if strings.HasPrefix(user.DisplayName, textWithoutPrefix) || if user.DisplayName == textWithoutPrefix || user.UserID == existingText {
strings.HasPrefix(user.UserID, existingText) { // Exact match, return that.
completions = append(completions, user) 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 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 { func (view *RoomView) MessageView() *MessageView {
return view.content return view.content
} }

View File

@ -23,7 +23,6 @@ import (
"time" "time"
"unicode" "unicode"
"github.com/mattn/go-runewidth"
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"maunium.net/go/gomuks/config" "maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
@ -101,28 +100,6 @@ func findWordToTabComplete(text string) string {
return output 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) { func (view *MainView) InputSubmit(roomView *RoomView, text string) {
if len(text) == 0 { if len(text) == 0 {
return 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) eventID, err := view.matrix.SendMarkdownMessage(roomView.Room.ID, tempMessage.Type(), text)
if err != nil { if err != nil {
tempMessage.SetState(ifc.MessageStateFailed) 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 { } else {
roomView.MessageView().UpdateMessageID(tempMessage, eventID) roomView.MessageView().UpdateMessageID(tempMessage, eventID)
} }
@ -314,7 +291,6 @@ func (view *MainView) addRoom(index int, room string) {
roomView := NewRoomView(roomStore). roomView := NewRoomView(roomStore).
SetInputSubmitFunc(view.InputSubmit). SetInputSubmitFunc(view.InputSubmit).
SetInputChangedFunc(view.InputChanged). SetInputChangedFunc(view.InputChanged).
SetTabCompleteFunc(view.InputTabComplete).
SetInputCapture(view.KeyEventHandler). SetInputCapture(view.KeyEventHandler).
SetMouseCapture(view.MouseEventHandler) SetMouseCapture(view.MouseEventHandler)
view.rooms[room] = roomView view.rooms[room] = roomView

View File

@ -84,7 +84,7 @@ type AdvancedInputField struct {
done func(tcell.Key) done func(tcell.Key)
// An optional function which is called when the user presses tab. // 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. // NewAdvancedInputField returns a new input field.
@ -107,6 +107,20 @@ func (field *AdvancedInputField) SetText(text string) *AdvancedInputField {
return field 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. // GetText returns the current text of the input field.
func (field *AdvancedInputField) GetText() string { func (field *AdvancedInputField) GetText() string {
return field.text return field.text
@ -212,7 +226,7 @@ func (field *AdvancedInputField) SetDoneFunc(handler func(key tcell.Key)) *Advan
return field 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 field.tabComplete = handler
return field return field
} }
@ -443,12 +457,7 @@ func (field *AdvancedInputField) RemovePreviousCharacter() {
func (field *AdvancedInputField) TriggerTabComplete() bool { func (field *AdvancedInputField) TriggerTabComplete() bool {
if field.tabComplete != nil { if field.tabComplete != nil {
oldWidth := runewidth.StringWidth(field.text) field.tabComplete(field.text, field.cursorOffset)
field.text = field.tabComplete(field.text, field.cursorOffset)
newWidth := runewidth.StringWidth(field.text)
if oldWidth != newWidth {
field.cursorOffset += newWidth - oldWidth
}
return true return true
} }
return false return false