Improve tab completion system
This commit is contained in:
parent
c3386ba118
commit
d147fc7579
@ -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()
|
||||
|
||||
|
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 (
|
||||
"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
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user