add simple vim mode

This commit is contained in:
Cheney 2015-10-01 22:44:43 +08:00
parent 390f0ebb6b
commit 79d1bf27b4
10 changed files with 234 additions and 15 deletions

View File

@ -24,7 +24,7 @@ You can read the source code in [example/main.go](https://github.com/chzyer/read
# Todo
* Vim mode
* Vim mode (WIP)
* More funny examples
* Support dumb/eterm-color terminal in emacs
@ -199,6 +199,16 @@ Notice: `Meta`+`B` is equals with `Alt`+`B` in windows.
| `Ctrl`+`C` / `Ctrl`+`G` | Exit Complete Select Mode |
| Other | Exit Complete Select Mode |
* Vim Mode (set Config.VimMode to true)
| Mode | Shortcut | Comment |
|--------+----------+---------------------------------------------|
| Normal | `j` | Next line (in history) |
| | `k` | Prev line (in history) |
| | `h` | Move Backward |
| | `l` | Move Forward |
# Tested with
| Environment | $TERM |

View File

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

View File

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

View File

@ -40,6 +40,7 @@ func main() {
Prompt: "\033[31m»\033[0m ",
HistoryFile: "/tmp/readline.tmp",
AutoComplete: completer,
// VimMode: true,
})
if err != nil {
panic(err)

View File

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

View File

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

View File

@ -98,13 +98,15 @@ func (r *RuneBuffer) MoveForward() {
})
}
func (r *RuneBuffer) Delete() {
func (r *RuneBuffer) Delete() (success bool) {
r.Refresh(func() {
if r.idx == len(r.buf) {
return
}
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
success = true
})
return
}
func (r *RuneBuffer) DeleteWord() {
@ -126,7 +128,7 @@ func (r *RuneBuffer) DeleteWord() {
r.Kill()
}
func (r *RuneBuffer) MoveToPrevWord() {
func (r *RuneBuffer) MoveToPrevWord() (success bool) {
r.Refresh(func() {
if r.idx == 0 {
return
@ -135,11 +137,14 @@ func (r *RuneBuffer) MoveToPrevWord() {
for i := r.idx - 1; i > 0; i-- {
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
r.idx = i
success = true
return
}
}
r.idx = 0
success = true
})
return
}
func (r *RuneBuffer) KillFront() {
@ -237,6 +242,35 @@ func (r *RuneBuffer) LineCount() int {
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 {
totalWidth := RunesWidth(r.buf[:r.idx]) + r.PromptLen()
w := getWidth()

View File

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

View File

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

144
vim.go Normal file
View File

@ -0,0 +1,144 @@
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_NORMAL
}
func (o *opVim) IsEnableVimMode() bool {
return o.cfg.VimMode
}
func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune, handle bool) {
switch r {
case CharEnter, CharInterrupt:
return r, false
}
rb := o.op.buf
handled := true
{
switch r {
case 'h':
t = CharBackward
case 'j':
t = CharNext
case 'k':
t = CharPrev
case 'l':
t = CharForward
default:
handled = false
}
if handled {
return t, false
}
}
{ // to insert
handled = true
switch r {
case 'i':
case 'I':
rb.MoveToLineStart()
case 'a':
rb.MoveForward()
case 'A':
rb.MoveToLineEnd()
case 's':
rb.Delete()
default:
handled = false
}
if handled {
o.EnterVimInsertMode()
return r, true
}
}
{ // movement
handled = true
switch r {
case '0', '^':
rb.MoveToLineStart()
case '$':
rb.MoveToLineEnd()
case 'b':
rb.MoveToPrevWord()
case 'w':
rb.MoveToNextWord()
case 'f', 'F', 't', 'T':
next := readNext()
prevChar := r == 't' || r == 'T'
reverse := r == 'F' || r == 'T'
switch next {
case CharEsc:
default:
if rb.MoveTo(next, prevChar, reverse) {
return r, true
}
}
default:
handled = false
}
if handled {
return r, true
}
}
// invalid operation
o.op.t.Bell()
return r, true
}
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, bool) {
if o.vimMode == VIM_NORMAL {
return o.HandleVimNormal(r, readNext)
}
if r == CharEsc {
o.ExitVimInsertMode()
return r, true
}
switch o.vimMode {
case VIM_INSERT:
return r, false
case VIM_VISUAL:
}
return r, false
}