Improve tab completion system
This commit is contained in:
parent
c3386ba118
commit
d147fc7579
@ -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
38
lib/util/lcp.go
Normal 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
|
||||||
|
}
|
120
ui/room-view.go
120
ui/room-view.go
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user