show completion menu for multiple match

This commit is contained in:
Gokcehan 2016-08-21 18:41:03 +03:00
parent 66ce56eb6e
commit 19561f6531
4 changed files with 110 additions and 80 deletions

2
app.go
View File

@ -51,7 +51,7 @@ func (app *App) handleInp() {
return return
} }
e := app.ui.getExpr() e := app.ui.getExpr(app.nav)
if e == nil { if e == nil {
continue continue
} }

108
comp.go
View File

@ -36,29 +36,26 @@ func matchLongest(s1, s2 string) string {
return s1[:i] return s1[:i]
} }
func matchWord(s string, words []string) string { func matchWord(s string, words []string) (matches []string, longest string) {
var match string
for _, w := range words { for _, w := range words {
if strings.HasPrefix(w, s) { if strings.HasPrefix(w, s) {
if match != "" { matches = append(matches, w)
match = matchLongest(match, w) if longest != "" {
longest = matchLongest(longest, w)
} else { } else {
match = w + " " longest = w + " "
} }
} }
} }
if match != "" { if longest == "" {
return match longest = s
} }
return s return
} }
func matchExec(s string) string { func matchExec(s string) (matches []string, longest string) {
var match string
paths := strings.Split(envPath, ":") paths := strings.Split(envPath, ":")
for _, p := range paths { for _, p := range paths {
@ -81,25 +78,25 @@ func matchExec(s string) string {
if !f.Mode().IsRegular() || f.Mode()&0111 == 0 { if !f.Mode().IsRegular() || f.Mode()&0111 == 0 {
continue continue
} }
if match != "" {
match = matchLongest(match, f.Name()) matches = append(matches, f.Name())
if longest != "" {
longest = matchLongest(longest, f.Name())
} else { } else {
match = f.Name() + " " longest = f.Name() + " "
} }
} }
} }
} }
if match != "" { if longest == "" {
return match longest = s
} }
return s return
} }
func matchFile(s string) string { func matchFile(s string) (matches []string, longest string) {
var match string
wd, err := os.Getwd() wd, err := os.Getwd()
if err != nil { if err != nil {
log.Printf("getting current directory: %s", err) log.Printf("getting current directory: %s", err)
@ -112,81 +109,78 @@ func matchFile(s string) string {
for _, f := range fi { for _, f := range fi {
if strings.HasPrefix(f.Name(), s) { if strings.HasPrefix(f.Name(), s) {
if match != "" { matches = append(matches, f.Name())
match = matchLongest(match, f.Name()) if longest != "" {
longest = matchLongest(longest, f.Name())
} else { } else {
match = f.Name() + " " longest = f.Name() + " "
} }
} }
} }
if match != "" { if longest == "" {
return match longest = s
} }
return s return
} }
func compCmd(acc []rune) []rune { func compCmd(acc []rune) (matches []string, longestAcc []rune) {
if len(acc) == 0 || acc[len(acc)-1] == ' ' { if len(acc) == 0 || acc[len(acc)-1] == ' ' {
return acc return matches, acc
} }
s := string(acc) s := string(acc)
f := strings.Fields(s) f := strings.Fields(s)
var longest string
switch len(f) { switch len(f) {
case 0: // do nothing case 0:
longestAcc = acc
case 1: case 1:
words := gCmdWords words := gCmdWords
for c, _ := range gOpts.cmds { for c, _ := range gOpts.cmds {
words = append(words, c) words = append(words, c)
} }
return []rune(matchWord(s, words)) matches, longest = matchWord(s, words)
longestAcc = []rune(longest)
default: default:
switch f[0] { switch f[0] {
case "set": case "set":
opt := matchWord(f[1], gOptWords) matches, longest = matchWord(f[1], gOptWords)
ret := []rune(f[0]) longestAcc = append(acc[:len(acc)-len(f[len(f)-1])], []rune(longest)...)
ret = append(ret, ' ') case "map", "cmd":
ret = append(ret, []rune(opt)...) longestAcc = acc
return ret
case "map", "cmd": // do nothing
default: default:
ret := []rune(f[0]) matches, longest = matchFile(f[len(f)-1])
ret = append(ret, ' ') longestAcc = append(acc[:len(acc)-len(f[len(f)-1])], []rune(longest)...)
for i := 1; i < len(f); i++ {
name := matchFile(f[i])
ret = append(ret, []rune(name)...)
}
return ret
} }
} }
return acc return
} }
func compShell(acc []rune) []rune { func compShell(acc []rune) (matches []string, longestAcc []rune) {
if len(acc) == 0 || acc[len(acc)-1] == ' ' { if len(acc) == 0 || acc[len(acc)-1] == ' ' {
return acc return matches, acc
} }
s := string(acc) s := string(acc)
f := strings.Fields(s) f := strings.Fields(s)
var longest string
switch len(f) { switch len(f) {
case 0: // do nothing case 0:
longestAcc = acc
case 1: case 1:
return []rune(matchExec(s)) matches, longest = matchExec(s)
longestAcc = []rune(longest)
default: default:
ret := []rune(f[0]) matches, longest = matchFile(f[len(f)-1])
ret = append(ret, ' ') longestAcc = append(acc[:len(acc)-len(f[len(f)-1])], []rune(longest)...)
for i := 1; i < len(f); i++ {
name := matchFile(f[i])
ret = append(ret, []rune(name)...)
}
return ret
} }
return acc return
} }

12
eval.go
View File

@ -206,7 +206,7 @@ func (e *CallExpr) eval(app *App, args []string) {
} }
app.ui.echoFileInfo(app.nav) app.ui.echoFileInfo(app.nav)
case "read": case "read":
s := app.ui.prompt(":") s := app.ui.prompt(app.nav, ":")
if len(s) == 0 { if len(s) == 0 {
app.ui.echoFileInfo(app.nav) app.ui.echoFileInfo(app.nav)
return return
@ -221,24 +221,24 @@ func (e *CallExpr) eval(app *App, args []string) {
log.Print(p.err) log.Print(p.err)
} }
case "read-shell": case "read-shell":
s := app.ui.prompt("$") s := app.ui.prompt(app.nav, "$")
log.Printf("shell: %s", s) log.Printf("shell: %s", s)
app.runShell(s, nil, false, false) app.runShell(s, nil, false, false)
case "read-shell-wait": case "read-shell-wait":
s := app.ui.prompt("!") s := app.ui.prompt(app.nav, "!")
log.Printf("shell-wait: %s", s) log.Printf("shell-wait: %s", s)
app.runShell(s, nil, true, false) app.runShell(s, nil, true, false)
case "read-shell-async": case "read-shell-async":
s := app.ui.prompt("&") s := app.ui.prompt(app.nav, "&")
log.Printf("shell-async: %s", s) log.Printf("shell-async: %s", s)
app.runShell(s, nil, false, true) app.runShell(s, nil, false, true)
case "search": case "search":
s := app.ui.prompt("/") s := app.ui.prompt(app.nav, "/")
log.Printf("search: %s", s) log.Printf("search: %s", s)
app.ui.message = "sorry, search is not implemented yet!" app.ui.message = "sorry, search is not implemented yet!"
// TODO: implement // TODO: implement
case "search-back": case "search-back":
s := app.ui.prompt("?") s := app.ui.prompt(app.nav, "?")
log.Printf("search-back: %s", s) log.Printf("search-back: %s", s)
app.ui.message = "sorry, search-back is not implemented yet!" app.ui.message = "sorry, search-back is not implemented yet!"
// TODO: implement // TODO: implement

68
ui.go
View File

@ -346,7 +346,7 @@ func findBinds(keys map[string]Expr, prefix string) (binds map[string]Expr, ok b
return return
} }
func (ui *UI) getExpr() Expr { func (ui *UI) getExpr(nav *Nav) Expr {
r := &CallExpr{"redraw", nil} r := &CallExpr{"redraw", nil}
var acc []rune var acc []rune
@ -400,12 +400,14 @@ func (ui *UI) getExpr() Expr {
if ok { if ok {
return gOpts.keys[string(acc)] return gOpts.keys[string(acc)]
} }
ui.draw(nav)
ui.listBinds(binds) ui.listBinds(binds)
default: default:
if ok { if ok {
// TODO: use a delay // TODO: use a delay
return gOpts.keys[string(acc)] return gOpts.keys[string(acc)]
} }
ui.draw(nav)
ui.listBinds(binds) ui.listBinds(binds)
} }
case termbox.EventResize: case termbox.EventResize:
@ -416,7 +418,7 @@ func (ui *UI) getExpr() Expr {
} }
} }
func (ui *UI) prompt(pref string) string { func (ui *UI) prompt(nav *Nav, pref string) string {
fg, bg := termbox.ColorDefault, termbox.ColorDefault fg, bg := termbox.ColorDefault, termbox.ColorDefault
win := ui.msgwin win := ui.msgwin
@ -448,10 +450,15 @@ func (ui *UI) prompt(pref string) string {
termbox.Flush() termbox.Flush()
return string(acc) return string(acc)
case termbox.KeyTab: case termbox.KeyTab:
var matches []string
if pref == ":" { if pref == ":" {
acc = compCmd(acc) matches, acc = compCmd(acc)
} else { } else {
acc = compShell(acc) matches, acc = compShell(acc)
}
ui.draw(nav)
if len(matches) > 1 {
ui.listMatches(matches)
} }
case termbox.KeyEsc: case termbox.KeyEsc:
return "" return ""
@ -492,17 +499,7 @@ func (ui *UI) sync() {
} }
} }
func (ui *UI) listBinds(binds map[string]Expr) { func (ui *UI) showMenu(b *bytes.Buffer) {
t := new(tabwriter.Writer)
b := new(bytes.Buffer)
t.Init(b, 0, 8, 0, '\t', 0)
fmt.Fprintln(t, "keys\tcommand")
for key, expr := range binds {
fmt.Fprintf(t, "%s\t%v\n", key, expr)
}
t.Flush()
lines := strings.Split(b.String(), "\n") lines := strings.Split(b.String(), "\n")
lines = lines[:len(lines)-1] lines = lines[:len(lines)-1]
@ -512,8 +509,47 @@ func (ui *UI) listBinds(binds map[string]Expr) {
ui.menuwin.printl(0, 0, termbox.AttrBold, termbox.AttrBold, lines[0]) ui.menuwin.printl(0, 0, termbox.AttrBold, termbox.AttrBold, lines[0])
for i, line := range lines[1:] { for i, line := range lines[1:] {
ui.menuwin.printl(0, i+1, termbox.ColorDefault, termbox.ColorDefault, line) ui.menuwin.printl(0, i+1, termbox.ColorDefault, termbox.ColorDefault, "")
ui.menuwin.print(0, i+1, termbox.ColorDefault, termbox.ColorDefault, line)
} }
termbox.Flush() termbox.Flush()
} }
func (ui *UI) listBinds(binds map[string]Expr) {
t := new(tabwriter.Writer)
b := new(bytes.Buffer)
t.Init(b, 0, gOpts.tabstop, 2, '\t', 0)
fmt.Fprintln(t, "keys\tcommand")
for key, expr := range binds {
fmt.Fprintf(t, "%s\t%v\n", key, expr)
}
t.Flush()
ui.showMenu(b)
}
func (ui *UI) listMatches(matches []string) {
b := new(bytes.Buffer)
wtot, _ := termbox.Size()
wcol := 0
for _, m := range matches {
wcol = max(wcol, len(m))
}
wcol += gOpts.tabstop - wcol%gOpts.tabstop
ncol := wtot / wcol
b.WriteString("possible matches\n")
for i := 0; i < len(matches); i++ {
for j := 0; j < ncol && i < len(matches); i, j = i+1, j+1 {
b.WriteString(fmt.Sprintf("%s%*s", matches[i], wcol-len(matches[i]), ""))
}
b.WriteByte('\n')
}
ui.showMenu(b)
}