Skip to content

Commit

Permalink
implement command line completion of command names
Browse files Browse the repository at this point in the history
  • Loading branch information
itchyny committed Oct 19, 2024
1 parent a61637c commit af8b033
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 109 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ This binary editor is influenced by the Vim editor.
- `:quit`, `ZQ`, `:qall`, `:write`,
`:wq`, `ZZ`, `:xit`, `:xall`, `:cquit`
- Window operations
- `:wincmd [nolhkjtbpKJHL]`, `<C-w>[nolhkjtbpKJHL]`
- `:wincmd [nohjkltbpHJKL]`, `<C-w>[nohjkltbpHJKL]`
- Cursor motions
- `h`, `j`, `k`, `l`, `w`, `b`, `^`, `0`, `$`,
`<C-[fb]>`, `<C-[du]>`, `<C-[ey]>`, `<C-[np]>`,
Expand Down
2 changes: 1 addition & 1 deletion cmdline/cmdline.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func (c *Cmdline) execute() (finish bool) {
defer c.saveHistory()
switch c.typ {
case ':':
cmd, r, bang, _, arg, err := parse(string(c.cmdline))
cmd, r, bang, _, _, arg, err := parse(string(c.cmdline))
if err != nil {
c.eventCh <- event.Event{Type: event.Error, Error: err}
} else if cmd.name != "" {
Expand Down
71 changes: 46 additions & 25 deletions cmdline/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,54 @@ import "github.com/itchyny/bed/event"

type command struct {
name string
fullname string
eventType event.Type
rangeType rangeType
}

type rangeType int

const (
rangeEmpty rangeType = 1 << iota
rangeCount
rangeBoth
)

func (rt rangeType) allows(r *event.Range) bool {
switch {
case r == nil:
return rt&rangeEmpty != 0
case r.To == nil:
return rt&rangeCount != 0
default:
return rt&rangeBoth != 0
}
}

var commands = []command{
{"e[dit]", event.Edit},
{"ene[w]", event.Enew},
{"new", event.New},
{"vne[w]", event.Vnew},
{"on[ly]", event.Only},
{"winc[md]", event.Wincmd},

{"go[to]", event.CursorGoto},
{"%", event.CursorGoto},

{"u[ndo]", event.Undo},
{"red[o]", event.Redo},

{"pw[d]", event.Pwd},
{"cd", event.Chdir},
{"chd[ir]", event.Chdir},
{"exi[t]", event.Quit},
{"q[uit]", event.Quit},
{"qa[ll]", event.QuitAll},
{"quita[ll]", event.QuitAll},
{"cq[uit]", event.QuitErr},
{"w[rite]", event.Write},
{"wq", event.WriteQuit},
{"x[it]", event.WriteQuit},
{"xa[ll]", event.WriteQuit},
{"e[dit]", "edit", event.Edit, rangeEmpty},
{"ene[w]", "enew", event.Enew, rangeEmpty},
{"new", "new", event.New, rangeEmpty},
{"vne[w]", "vnew", event.Vnew, rangeEmpty},
{"on[ly]", "only", event.Only, rangeEmpty},
{"winc[md]", "wincmd", event.Wincmd, rangeEmpty},

{"go[to]", "goto", event.CursorGoto, rangeCount},
{"%", "%", event.CursorGoto, rangeCount},

{"u[ndo]", "undo", event.Undo, rangeEmpty},
{"red[o]", "redo", event.Redo, rangeEmpty},

{"pw[d]", "pwd", event.Pwd, rangeEmpty},
{"cd", "cd", event.Chdir, rangeEmpty},
{"chd[ir]", "chdir", event.Chdir, rangeEmpty},
{"exi[t]", "exit", event.Quit, rangeEmpty},
{"q[uit]", "quit", event.Quit, rangeEmpty},
{"qa[ll]", "qall", event.QuitAll, rangeEmpty},
{"quita[ll]", "quitall", event.QuitAll, rangeEmpty},
{"cq[uit]", "cquit", event.QuitErr, rangeEmpty},
{"w[rite]", "write", event.Write, rangeEmpty | rangeBoth},
{"wq", "wq", event.WriteQuit, rangeEmpty | rangeBoth},
{"x[it]", "xit", event.WriteQuit, rangeEmpty | rangeBoth},
{"xa[ll]", "xall", event.WriteQuit, rangeEmpty | rangeBoth},
}
105 changes: 63 additions & 42 deletions cmdline/completor.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (
"path/filepath"
"slices"
"strings"
"unicode"
"unicode/utf8"

"github.com/itchyny/bed/event"
)

type completor struct {
fs fs
env env
command bool
target string
arg string
results []string
Expand All @@ -22,24 +25,24 @@ func newCompletor(fs fs, env env) *completor {
return &completor{fs: fs, env: env}
}

func (c *completor) clear() {
c.target = ""
c.arg = ""
c.results = nil
c.index = 0
}

func (c *completor) complete(cmdline string, forward bool) string {
cmd, _, _, prefix, arg, _ := parse(cmdline)
cmd, r, _, name, prefix, arg, _ := parse(cmdline)
if name == "" || c.command ||
!hasSuffixFunc(prefix, unicode.IsSpace) && cmd.fullname != name {
cmdline = c.completeCommand(cmdline, name, prefix, r, forward)
if c.results != nil {
return cmdline
}
prefix = cmdline
}
switch cmd.eventType {
case event.Edit, event.New, event.Vnew, event.Write:
return c.completeFilepaths(cmdline, prefix, arg, forward, false)
return c.completeFilepath(cmdline, prefix, arg, forward, false)
case event.Chdir:
return c.completeFilepaths(cmdline, prefix, arg, forward, true)
return c.completeFilepath(cmdline, prefix, arg, forward, true)
case event.Wincmd:
return c.completeWincmd(cmdline, prefix, arg, forward)
default:
c.clear()
return cmdline
}
}
Expand All @@ -53,35 +56,45 @@ func (c *completor) completeNext(prefix string, forward bool) string {
if c.index < 0 {
return c.target
}
if len(c.results) == 1 {
defer c.clear()
}
return prefix + c.arg + c.results[c.index]
}

func (c *completor) completeFilepaths(
cmdline, prefix, arg string, forward, dirOnly bool,
func (c *completor) completeCommand(
cmdline, name, prefix string, r *event.Range, forward bool,
) string {
if !strings.HasSuffix(prefix, " ") {
prefix += " "
prefix = prefix[:len(prefix)-len(name)]
if c.results == nil {
c.command, c.target, c.index = true, cmdline, -1
c.arg, c.results = "", listCommandNames(name, r)
}
if len(c.results) > 0 {
return c.completeNext(prefix, forward)
return c.completeNext(prefix, forward)
}

func listCommandNames(name string, r *event.Range) []string {
var targets []string
for _, cmd := range commands {
if strings.HasPrefix(cmd.fullname, name) && cmd.rangeType.allows(r) {
targets = append(targets, cmd.fullname)
}
}
c.target = cmdline
c.index = 0
c.arg, c.results = c.listFileNames(arg, dirOnly)
if len(c.results) == 1 {
cmdline := prefix + c.arg + c.results[0]
c.results = nil
return cmdline
slices.Sort(targets)
return targets
}

func (c *completor) completeFilepath(
cmdline, prefix, arg string, forward, dirOnly bool,
) string {
if !hasSuffixFunc(prefix, unicode.IsSpace) {
prefix += " "
}
if len(c.results) > 1 {
if forward {
c.index = 0
return prefix + c.arg + c.results[0]
}
c.index = len(c.results) - 1
return prefix + c.arg + c.results[len(c.results)-1]
if c.results == nil {
c.command, c.target, c.index = false, cmdline, -1
c.arg, c.results = c.listFileNames(arg, dirOnly)
}
return cmdline
return c.completeNext(prefix, forward)
}

const separator = string(filepath.Separator)
Expand Down Expand Up @@ -224,17 +237,25 @@ func (c *completor) expandPath(path string) (string, func(string) string) {
}

func (c *completor) completeWincmd(cmdline, prefix, arg string, forward bool) string {
if !strings.HasSuffix(prefix, " ") {
if !hasSuffixFunc(prefix, unicode.IsSpace) {
prefix += " "
}
if len(c.results) > 0 {
return c.completeNext(prefix, forward)
}
if len(arg) > 0 {
return cmdline
if c.results == nil {
if arg != "" {
return cmdline
}
c.command, c.target, c.arg, c.index = false, cmdline, "", -1
c.results = strings.Split("nohjkltbpHJKL", "")
}
c.target = cmdline
c.results = []string{"n", "h", "l", "k", "j", "H", "L", "K", "J", "t", "b", "p"}
c.index = -1
return cmdline
return c.completeNext(prefix, forward)
}

func (c *completor) clear() {
c.command, c.target, c.arg = false, "", ""
c.results, c.index = nil, 0
}

func hasSuffixFunc(s string, f func(rune) bool) bool {
r, size := utf8.DecodeLastRuneInString(s)
return size > 0 && f(r)
}
Loading

0 comments on commit af8b033

Please sign in to comment.