Refactor completions

This commit is contained in:
c-bata 2017-08-09 21:33:47 +09:00
parent b578417898
commit e871f177ec
5 changed files with 125 additions and 110 deletions

View File

@ -1,35 +1,56 @@
package prompt
import (
"log"
"strings"
)
type Suggest struct {
Text string
Description string
}
type CompletionManager struct {
selected int // -1 means nothing one is selected.
tmp []*Suggest
Max uint16
selected int // -1 means nothing one is selected.
tmp []Suggest
Max uint16
completer Completer
}
func (c *CompletionManager) GetSelectedSuggestion() (s Suggest, ok bool) {
if c.selected == -1 {
return Suggest{}, false
} else if c.selected < -1 {
log.Printf("[ERROR] shoud be reached here, selected=%d", c.selected)
return Suggest{}, false
}
return c.tmp[c.selected], true
}
func (c *CompletionManager) GetSuggestions() []Suggest {
return c.tmp
}
func (c *CompletionManager) Reset() {
c.selected = -1
c.Update([]*Suggest{})
c.Update("")
return
}
func (c *CompletionManager) Update(new []*Suggest) {
c.selected = -1
c.tmp = new
func (c *CompletionManager) Update(in string) {
c.tmp = c.completer(in)
return
}
func (c *CompletionManager) Previous() {
c.selected--
c.update()
return
}
func (c *CompletionManager) Next() {
c.selected++
c.update()
return
}
@ -37,10 +58,10 @@ func (c *CompletionManager) Completing() bool {
return c.selected != -1
}
func (c *CompletionManager) update(completions []Suggest) {
func (c *CompletionManager) update() {
max := int(c.Max)
if len(completions) < max {
max = len(completions)
if len(c.tmp) < max {
max = len(c.tmp)
}
if c.selected >= max {
c.Reset()
@ -49,9 +70,66 @@ func (c *CompletionManager) update(completions []Suggest) {
}
}
func NewCompletionManager(max uint16) *CompletionManager {
func formatCompletions(completions []Suggest, max int) (new []Suggest, width int) {
num := len(completions)
new = make([]Suggest, num)
leftWidth := 0
rightWidth := 0
for i := 0; i < num; i++ {
if leftWidth < len([]rune(completions[i].Text)) {
leftWidth = len([]rune(completions[i].Text))
}
if rightWidth < len([]rune(completions[i].Description)) {
rightWidth = len([]rune(completions[i].Description))
}
}
if diff := max - completionMargin - leftWidth - rightWidth; diff < 0 {
if rightWidth > diff {
rightWidth += diff
} else if rightWidth+rightMargin > -diff {
leftWidth += rightWidth + rightMargin + diff
rightWidth = 0
}
}
if rightWidth == 0 {
width = leftWidth + leftMargin
} else {
width = leftWidth + leftMargin + rightWidth + rightMargin
}
for i := 0; i < num; i++ {
var newText string
var newDescription string
if l := len(completions[i].Text); l > leftWidth {
newText = leftPrefix + completions[i].Text[:leftWidth-len("...")] + "..." + leftSuffix
} else if l < width {
spaces := strings.Repeat(" ", leftWidth-len([]rune(completions[i].Text)))
newText = leftPrefix + completions[i].Text + spaces + leftSuffix
} else {
newText = leftPrefix + completions[i].Text + leftSuffix
}
if rightWidth == 0 {
newDescription = ""
} else if l := len(completions[i].Description); l > rightWidth {
newDescription = rightPrefix + completions[i].Description[:rightWidth-len("...")] + "..." + rightSuffix
} else if l < width {
spaces := strings.Repeat(" ", rightWidth-len([]rune(completions[i].Description)))
newDescription = rightPrefix + completions[i].Description + spaces + rightSuffix
} else {
newDescription = rightPrefix + completions[i].Description + rightSuffix
}
new[i] = Suggest{Text: newText, Description: newDescription}
}
return
}
func NewCompletionManager(completer Completer, max uint16) *CompletionManager {
return &CompletionManager{
selected: -1,
Max: max,
selected: -1,
Max: max,
completer: completer,
}
}

View File

@ -166,11 +166,10 @@ func New(executor Executor, completer Completer, opts ...option) *Prompt {
selectedDescriptionTextColor: White,
selectedDescriptionBGColor: Cyan,
},
buf: NewBuffer(),
executor: executor,
completer: completer,
history: NewHistory(),
completion: NewCompletionManager(6),
buf: NewBuffer(),
executor: executor,
history: NewHistory(),
completion: NewCompletionManager(completer, 6),
}
for _, opt := range opts {

View File

@ -22,7 +22,6 @@ type Prompt struct {
buf *Buffer
renderer *Render
executor Executor
completer Completer
history *History
completion *CompletionManager
}
@ -45,7 +44,7 @@ func (p *Prompt) Run() {
log.Println("[INFO] Logging is enabled.")
}
p.renderer.Render(p.buf, p.completer(p.buf.Text()), p.completion.Max, p.completion.selected)
p.renderer.Render(p.buf, p.completion)
bufCh := make(chan []byte, 128)
stopReadBufCh := make(chan struct{})
@ -69,22 +68,19 @@ func (p *Prompt) Run() {
p.in.TearDown()
p.executor(e.input)
completions := p.completer(p.buf.Text())
p.completion.update(completions)
p.renderer.Render(p.buf, completions, p.completion.Max, p.completion.selected)
p.completion.Update(p.buf.Text())
p.renderer.Render(p.buf, p.completion)
// Set raw mode
p.in.Setup()
go p.readBuffer(bufCh, stopReadBufCh)
} else {
completions := p.completer(p.buf.Text())
p.completion.update(completions)
p.renderer.Render(p.buf, completions, p.completion.Max, p.completion.selected)
p.completion.Update(p.buf.Text())
p.renderer.Render(p.buf, p.completion)
}
case w := <-winSizeCh:
p.renderer.UpdateWinSize(w)
completions := p.completer(p.buf.Text())
p.renderer.Render(p.buf, completions, p.completion.Max, p.completion.selected)
p.renderer.Render(p.buf, p.completion)
case code := <-exitCh:
p.tearDown()
os.Exit(code)
@ -99,13 +95,12 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) {
switch key {
case ControlJ, Enter:
if p.completion.Completing() {
c := p.completer(p.buf.Text())[p.completion.selected]
if s, ok := p.completion.GetSelectedSuggestion(); ok {
w := p.buf.Document().GetWordBeforeCursor()
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(c.Text, false, true)
p.buf.InsertText(s.Text, false, true)
}
p.renderer.BreakLine(p.buf)
@ -148,24 +143,22 @@ func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) {
case Right:
p.buf.CursorRight(1)
case Backspace:
if p.completion.Completing() {
c := p.completer(p.buf.Text())[p.completion.selected]
if s, ok := p.completion.GetSelectedSuggestion(); ok {
w := p.buf.Document().GetWordBeforeCursor()
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(c.Text, false, true)
p.buf.InsertText(s.Text, false, true)
p.completion.Reset()
}
p.buf.DeleteBeforeCursor(1)
case NotDefined:
if p.completion.Completing() {
c := p.completer(p.buf.Text())[p.completion.selected]
if s, ok := p.completion.GetSelectedSuggestion(); ok {
w := p.buf.Document().GetWordBeforeCursor()
if w != "" {
p.buf.DeleteBeforeCursor(len([]rune(w)))
}
p.buf.InsertText(c.Text, false, true)
p.buf.InsertText(s.Text, false, true)
}
p.completion.Reset()
p.buf.InsertText(string(b), false, true)

View File

@ -1,6 +1,8 @@
package prompt
import "strings"
import (
"log"
)
const (
leftPrefix = " "
@ -79,21 +81,24 @@ func (r *Render) renderWindowTooSmall() {
return
}
func (r *Render) renderCompletion(buf *Buffer, completions []Suggest, max uint16, selected int) {
func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
max := completions.Max
if max > r.row {
max = r.row
}
if l := len(completions); l == 0 {
suggestions := completions.GetSuggestions()
if l := len(completions.GetSuggestions()); l == 0 {
return
} else if l > int(max) {
completions = completions[:max]
suggestions = suggestions[:max]
}
formatted, width := formatCompletions(
completions,
suggestions,
int(r.col)-len(r.prefix),
)
log.Printf("[INFO] formatted: %#v\n", formatted)
l := len(formatted)
r.prepareArea(l)
@ -107,14 +112,14 @@ func (r *Render) renderCompletion(buf *Buffer, completions []Suggest, max uint16
r.out.SetColor(White, Cyan, false)
for i := 0; i < l; i++ {
r.out.CursorDown(1)
if i == selected {
if i == completions.selected {
r.out.SetColor(r.selectedSuggestionTextColor, r.selectedSuggestionBGColor, true)
} else {
r.out.SetColor(r.suggestionTextColor, r.suggestionBGColor, false)
}
r.out.WriteStr(formatted[i].Text)
if i == selected {
if i == completions.selected {
r.out.SetColor(r.selectedDescriptionTextColor, r.selectedDescriptionBGColor, false)
} else {
r.out.SetColor(r.descriptionTextColor, r.descriptionBGColor, false)
@ -134,14 +139,14 @@ func (r *Render) renderCompletion(buf *Buffer, completions []Suggest, max uint16
return
}
func (r *Render) Render(buffer *Buffer, completions []Suggest, maxCompletions uint16, selected int) {
func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
// Erasing
r.out.CursorBackward(int(r.col) + len(buffer.Text()) + len(r.prefix))
r.out.EraseDown()
// prepare area
line := buffer.Text()
h := ((len(r.prefix) + len(line)) / int(r.col)) + 1 + int(maxCompletions)
h := ((len(r.prefix) + len(line)) / int(r.col)) + 1 + int(completion.Max)
if h > int(r.row) || completionMargin > int(r.col) {
r.renderWindowTooSmall()
return
@ -154,12 +159,11 @@ func (r *Render) Render(buffer *Buffer, completions []Suggest, maxCompletions ui
r.out.WriteStr(line)
r.out.SetColor(DefaultColor, DefaultColor, false)
r.out.CursorBackward(len(line) - buffer.CursorPosition)
r.renderCompletion(buffer, completions, maxCompletions, selected)
if selected != -1 {
c := completions[selected]
r.renderCompletion(buffer, completion)
if suggest, ok := completion.GetSelectedSuggestion(); ok {
r.out.CursorBackward(len([]rune(buffer.Document().GetWordBeforeCursor())))
r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
r.out.WriteStr(c.Text)
r.out.WriteStr(suggest.Text)
r.out.SetColor(DefaultColor, DefaultColor, false)
}
r.out.Flush()
@ -176,59 +180,3 @@ func (r *Render) BreakLine(buffer *Buffer) {
r.out.SetColor(DefaultColor, DefaultColor, false)
r.out.Flush()
}
func formatCompletions(completions []Suggest, max int) (new []Suggest, width int) {
num := len(completions)
new = make([]Suggest, num)
leftWidth := 0
rightWidth := 0
for i := 0; i < num; i++ {
if leftWidth < len([]rune(completions[i].Text)) {
leftWidth = len([]rune(completions[i].Text))
}
if rightWidth < len([]rune(completions[i].Description)) {
rightWidth = len([]rune(completions[i].Description))
}
}
if diff := max - completionMargin - leftWidth - rightWidth; diff < 0 {
if rightWidth > diff {
rightWidth += diff
} else if rightWidth+rightMargin > -diff {
leftWidth += rightWidth + rightMargin + diff
rightWidth = 0
}
}
if rightWidth == 0 {
width = leftWidth + leftMargin
} else {
width = leftWidth + leftMargin + rightWidth + rightMargin
}
for i := 0; i < num; i++ {
var newText string
var newDescription string
if l := len(completions[i].Text); l > leftWidth {
newText = leftPrefix + completions[i].Text[:leftWidth-len("...")] + "..." + leftSuffix
} else if l < width {
spaces := strings.Repeat(" ", leftWidth-len([]rune(completions[i].Text)))
newText = leftPrefix + completions[i].Text + spaces + leftSuffix
} else {
newText = leftPrefix + completions[i].Text + leftSuffix
}
if rightWidth == 0 {
newDescription = ""
} else if l := len(completions[i].Description); l > rightWidth {
newDescription = rightPrefix + completions[i].Description[:rightWidth-len("...")] + "..." + rightSuffix
} else if l < width {
spaces := strings.Repeat(" ", rightWidth-len([]rune(completions[i].Description)))
newDescription = rightPrefix + completions[i].Description + spaces + rightSuffix
} else {
newDescription = rightPrefix + completions[i].Description + rightSuffix
}
new[i] = Suggest{Text: newText, Description: newDescription}
}
return
}

View File

@ -41,12 +41,10 @@ func (t *VT100Parser) TearDown() error {
func (t *VT100Parser) setRawMode() error {
x := t.origTermios.Lflag
if x &^= syscall.ICANON; x != 0 && x == t.origTermios.Lflag {
if x &^= syscall.ICANON; x != 0 && x == t.origTermios.Lflag {
// fd is already raw mode
log.Print("[INFO] already raw mode.")
return nil
}
log.Print("[INFO] set raw mode.")
var n syscall.Termios
if err := termios.Tcgetattr(uintptr(t.fd), &t.origTermios); err != nil {
return err
@ -64,7 +62,6 @@ func (t *VT100Parser) resetRawMode() error {
if t.origTermios.Lflag == 0 {
return nil
}
log.Print("[INFO] Reset raw mode.")
return termios.Tcsetattr(uintptr(t.fd), termios.TCSANOW, &t.origTermios)
}