Merge pull request #7 from chzyer/feature/add_vim_mode

add simple vim mode
This commit is contained in:
Chzyer 2015-10-02 10:59:48 +08:00
commit 38e5213cc8
10 changed files with 284 additions and 16 deletions

View File

@ -24,10 +24,15 @@ You can read the source code in [example/main.go](https://github.com/chzyer/read
# Todo # Todo
* Vim mode
* More funny examples * More funny examples
* Support dumb/eterm-color terminal in emacs * Support dumb/eterm-color terminal in emacs
# Features
* Support emacs/vi mode, almost all basic features that GNU-Readline is supported
* zsh-style backward/forward history search
* zsh-style completion
* Readline auto refresh when others write to Stdout while editing(it needs specify the Stdout/Stderr provided by *readline.Instance to others).
# Usage # Usage
* Simplest example * Simplest example

View File

@ -7,7 +7,7 @@ const (
CharDelete = 4 CharDelete = 4
CharLineEnd = 5 CharLineEnd = 5
CharForward = 6 CharForward = 6
CharCancel = 7 CharBell = 7
CharCtrlH = 8 CharCtrlH = 8
CharTab = 9 CharTab = 9
CharCtrlJ = 10 CharCtrlJ = 10
@ -26,8 +26,8 @@ const (
) )
const ( const (
MetaPrev rune = -iota - 1 MetaBackward rune = -iota - 1
MetaNext MetaForward
MetaDelete MetaDelete
MetaBackspace MetaBackspace
MetaTranspose MetaTranspose

View File

@ -127,7 +127,7 @@ func (o *opCompleter) HandleCompleteSelect(r rune) bool {
next = false next = false
case CharTab, CharForward: case CharTab, CharForward:
o.doSelect() o.doSelect()
case CharCancel, CharInterrupt: case CharBell, CharInterrupt:
o.ExitCompleteMode(true) o.ExitCompleteMode(true)
next = false next = false
case CharNext: case CharNext:

View File

@ -20,6 +20,10 @@ bye
} }
var completer = readline.NewPrefixCompleter( var completer = readline.NewPrefixCompleter(
readline.PcItem("mode",
readline.PcItem("vi"),
readline.PcItem("emacs"),
),
readline.PcItem("login"), readline.PcItem("login"),
readline.PcItem("say", readline.PcItem("say",
readline.PcItem("hello"), readline.PcItem("hello"),
@ -52,8 +56,23 @@ func main() {
if err != nil { if err != nil {
break break
} }
line = strings.TrimSpace(line)
switch { switch {
case strings.HasPrefix(line, "mode "):
switch line[5:] {
case "vi":
l.SetVimMode(true)
case "emacs":
l.SetVimMode(false)
default:
println("invalid mode:", line[5:])
}
case line == "mode":
if l.IsVimMode() {
println("current mode: vim")
} else {
println("current mode: emacs")
}
case line == "login": case line == "login":
pswd, err := l.ReadPassword("please enter your password: ") pswd, err := l.ReadPassword("please enter your password: ")
if err != nil { if err != nil {

View File

@ -18,6 +18,7 @@ type Operation struct {
*opHistory *opHistory
*opSearch *opSearch
*opCompleter *opCompleter
*opVim
} }
type wrapWriter struct { type wrapWriter struct {
@ -56,6 +57,7 @@ func NewOperation(t *Terminal, cfg *Config) *Operation {
outchan: make(chan []rune), outchan: make(chan []rune),
opHistory: newOpHistory(cfg.HistoryFile), opHistory: newOpHistory(cfg.HistoryFile),
} }
op.opVim = newVimMode(op)
op.w = op.buf.w op.w = op.buf.w
op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory) op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory)
op.opCompleter = newOpCompleter(op.buf.w, op) op.opCompleter = newOpCompleter(op.buf.w, op)
@ -87,13 +89,20 @@ func (o *Operation) ioloop() {
case CharInterrupt: case CharInterrupt:
o.t.KickRead() o.t.KickRead()
fallthrough fallthrough
case CharCancel: case CharBell:
continue
}
}
if o.IsEnableVimMode() {
r = o.HandleVim(r, o.t.ReadRune)
if r == 0 {
continue continue
} }
} }
switch r { switch r {
case CharCancel: case CharBell:
if o.IsSearchMode() { if o.IsSearchMode() {
o.ExitSearchMode(true) o.ExitSearchMode(true)
o.buf.Refresh(nil) o.buf.Refresh(nil)
@ -119,11 +128,11 @@ func (o *Operation) ioloop() {
case CharKill: case CharKill:
o.buf.Kill() o.buf.Kill()
keepInCompleteMode = true keepInCompleteMode = true
case MetaNext: case MetaForward:
o.buf.MoveToNextWord() o.buf.MoveToNextWord()
case CharTranspose: case CharTranspose:
o.buf.Transpose() o.buf.Transpose()
case MetaPrev: case MetaBackward:
o.buf.MoveToPrevWord() o.buf.MoveToPrevWord()
case MetaDelete: case MetaDelete:
o.buf.DeleteWord() o.buf.DeleteWord()
@ -132,7 +141,9 @@ func (o *Operation) ioloop() {
case CharLineEnd: case CharLineEnd:
o.buf.MoveToLineEnd() o.buf.MoveToLineEnd()
case CharDelete: case CharDelete:
o.buf.Delete() if !o.buf.Delete() {
o.t.Bell()
}
case CharBackspace, CharCtrlH: case CharBackspace, CharCtrlH:
if o.IsSearchMode() { if o.IsSearchMode() {
o.SearchBackspace() o.SearchBackspace()
@ -141,6 +152,7 @@ func (o *Operation) ioloop() {
} }
if o.buf.Len() == 0 { if o.buf.Len() == 0 {
o.t.Bell()
break break
} }
o.buf.Backspace() o.buf.Backspace()
@ -167,11 +179,15 @@ func (o *Operation) ioloop() {
buf := o.PrevHistory() buf := o.PrevHistory()
if buf != nil { if buf != nil {
o.buf.Set(buf) o.buf.Set(buf)
} else {
o.t.Bell()
} }
case CharNext: case CharNext:
buf, ok := o.NextHistory() buf, ok := o.NextHistory()
if ok { if ok {
o.buf.Set(buf) o.buf.Set(buf)
} else {
o.t.Bell()
} }
case CharInterrupt: case CharInterrupt:
if o.IsSearchMode() { if o.IsSearchMode() {

View File

@ -11,6 +11,7 @@ type Config struct {
Prompt string Prompt string
HistoryFile string HistoryFile string
AutoComplete AutoCompleter AutoComplete AutoCompleter
VimMode bool
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
@ -59,6 +60,14 @@ func (i *Instance) Stderr() io.Writer {
return i.o.Stderr() return i.o.Stderr()
} }
func (i *Instance) SetVimMode(on bool) {
i.o.SetVimMode(on)
}
func (i *Instance) IsVimMode() bool {
return i.o.IsEnableVimMode()
}
func (i *Instance) ReadPassword(prompt string) ([]byte, error) { func (i *Instance) ReadPassword(prompt string) ([]byte, error) {
return i.o.Password(prompt) return i.o.Password(prompt)
} }

View File

@ -5,6 +5,11 @@ import (
"io" "io"
) )
type runeBufferBck struct {
buf []rune
idx int
}
type RuneBuffer struct { type RuneBuffer struct {
buf []rune buf []rune
idx int idx int
@ -12,6 +17,22 @@ type RuneBuffer struct {
w io.Writer w io.Writer
cleanInScreen bool cleanInScreen bool
bck *runeBufferBck
}
func (r *RuneBuffer) Backup() {
r.bck = &runeBufferBck{r.buf, r.idx}
}
func (r *RuneBuffer) Restore() {
r.Refresh(func() {
if r.bck == nil {
return
}
r.buf = r.bck.buf
r.idx = r.bck.idx
})
} }
func NewRuneBuffer(w io.Writer, prompt string) *RuneBuffer { func NewRuneBuffer(w io.Writer, prompt string) *RuneBuffer {
@ -98,13 +119,22 @@ func (r *RuneBuffer) MoveForward() {
}) })
} }
func (r *RuneBuffer) Delete() { func (r *RuneBuffer) Erase() {
r.Refresh(func() {
r.idx = 0
r.buf = r.buf[:0]
})
}
func (r *RuneBuffer) Delete() (success bool) {
r.Refresh(func() { r.Refresh(func() {
if r.idx == len(r.buf) { if r.idx == len(r.buf) {
return return
} }
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
success = true
}) })
return
} }
func (r *RuneBuffer) DeleteWord() { func (r *RuneBuffer) DeleteWord() {
@ -126,7 +156,7 @@ func (r *RuneBuffer) DeleteWord() {
r.Kill() r.Kill()
} }
func (r *RuneBuffer) MoveToPrevWord() { func (r *RuneBuffer) MoveToPrevWord() (success bool) {
r.Refresh(func() { r.Refresh(func() {
if r.idx == 0 { if r.idx == 0 {
return return
@ -135,11 +165,14 @@ func (r *RuneBuffer) MoveToPrevWord() {
for i := r.idx - 1; i > 0; i-- { for i := r.idx - 1; i > 0; i-- {
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
r.idx = i r.idx = i
success = true
return return
} }
} }
r.idx = 0 r.idx = 0
success = true
}) })
return
} }
func (r *RuneBuffer) KillFront() { func (r *RuneBuffer) KillFront() {
@ -237,6 +270,35 @@ func (r *RuneBuffer) LineCount() int {
return LineCount(RunesWidth(r.buf) + r.PromptLen()) return LineCount(RunesWidth(r.buf) + r.PromptLen())
} }
func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) {
r.Refresh(func() {
if reverse {
for i := r.idx - 1; i >= 0; i-- {
if r.buf[i] == ch {
r.idx = i
if prevChar {
r.idx++
}
success = true
return
}
}
return
}
for i := r.idx + 1; i < len(r.buf); i++ {
if r.buf[i] == ch {
r.idx = i
if prevChar {
r.idx--
}
success = true
return
}
}
})
return
}
func (r *RuneBuffer) IdxLine() int { func (r *RuneBuffer) IdxLine() int {
totalWidth := RunesWidth(r.buf[:r.idx]) + r.PromptLen() totalWidth := RunesWidth(r.buf[:r.idx]) + r.PromptLen()
w := getWidth() w := getWidth()

View File

@ -113,6 +113,10 @@ func (t *Terminal) ioloop() {
expectNextChar = true expectNextChar = true
switch r { switch r {
case CharEsc: case CharEsc:
if t.cfg.VimMode {
t.outchan <- r
break
}
isEscape = true isEscape = true
case CharInterrupt, CharEnter, CharCtrlJ: case CharInterrupt, CharEnter, CharCtrlJ:
expectNextChar = false expectNextChar = false
@ -123,6 +127,10 @@ func (t *Terminal) ioloop() {
} }
} }
func (t *Terminal) Bell() {
fmt.Fprintf(t, "%c", CharBell)
}
func (t *Terminal) Close() error { func (t *Terminal) Close() error {
if atomic.SwapInt64(&t.closed, 1) != 0 { if atomic.SwapInt64(&t.closed, 1) != 0 {
return nil return nil

View File

@ -57,9 +57,9 @@ func escapeExKey(r rune) rune {
func escapeKey(r rune) rune { func escapeKey(r rune) rune {
switch r { switch r {
case 'b': case 'b':
r = MetaPrev r = MetaBackward
case 'f': case 'f':
r = MetaNext r = MetaForward
case 'd': case 'd':
r = MetaDelete r = MetaDelete
case CharTranspose: case CharTranspose:

149
vim.go Normal file
View File

@ -0,0 +1,149 @@
package readline
const (
VIM_NORMAL = iota
VIM_INSERT
VIM_VISUAL
)
type opVim struct {
cfg *Config
op *Operation
vimMode int
}
func newVimMode(op *Operation) *opVim {
ov := &opVim{
cfg: op.cfg,
op: op,
}
ov.SetVimMode(ov.cfg.VimMode)
return ov
}
func (o *opVim) SetVimMode(on bool) {
if o.cfg.VimMode && !on { // turn off
o.ExitVimMode()
}
o.cfg.VimMode = on
o.vimMode = VIM_INSERT
}
func (o *opVim) ExitVimMode() {
o.vimMode = VIM_INSERT
}
func (o *opVim) IsEnableVimMode() bool {
return o.cfg.VimMode
}
func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) {
rb := o.op.buf
handled = true
switch r {
case 'h':
t = CharBackward
case 'j':
t = CharNext
case 'k':
t = CharPrev
case 'l':
t = CharForward
case '0', '^':
rb.MoveToLineStart()
case '$':
rb.MoveToLineEnd()
case 'b', 'B':
rb.MoveToPrevWord()
case 'w', 'W', 'e', 'E':
rb.MoveToNextWord()
case 'f', 'F', 't', 'T':
next := readNext()
prevChar := r == 't' || r == 'T'
reverse := r == 'F' || r == 'T'
switch next {
case CharEsc:
default:
rb.MoveTo(next, prevChar, reverse)
}
default:
return r, false
}
return t, true
}
func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) {
rb := o.op.buf
handled = true
switch r {
case 'i':
case 'I':
rb.MoveToLineStart()
case 'a':
rb.MoveForward()
case 'A':
rb.MoveToLineEnd()
case 's':
rb.Delete()
case 'S':
rb.Erase()
case 'c':
next := readNext()
switch next {
case 'c':
rb.Erase()
case 'w':
rb.DeleteWord()
}
default:
return r, false
}
o.EnterVimInsertMode()
return
}
func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) {
switch r {
case CharEnter, CharInterrupt:
o.ExitVimMode()
return r
}
if r, handled := o.handleVimNormalMovement(r, readNext); handled {
return r
}
if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled {
return r
}
// invalid operation
o.op.t.Bell()
return 0
}
func (o *opVim) EnterVimInsertMode() {
o.vimMode = VIM_INSERT
}
func (o *opVim) ExitVimInsertMode() {
o.vimMode = VIM_NORMAL
}
func (o *opVim) HandleVim(r rune, readNext func() rune) rune {
if o.vimMode == VIM_NORMAL {
return o.HandleVimNormal(r, readNext)
}
if r == CharEsc {
o.ExitVimInsertMode()
return 0
}
switch o.vimMode {
case VIM_INSERT:
return r
case VIM_VISUAL:
}
return r
}