From 79d1bf27b4a097f08e01f85485f11086a78eacf6 Mon Sep 17 00:00:00 2001 From: Cheney Date: Thu, 1 Oct 2015 22:44:43 +0800 Subject: [PATCH] add simple vim mode --- README.md | 14 ++++- char.go | 6 +- complete.go | 2 +- example/main.go | 1 + operation.go | 27 +++++++-- readline.go | 5 ++ runebuf.go | 38 ++++++++++++- terminal.go | 8 +++ utils.go | 4 +- vim.go | 144 ++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 234 insertions(+), 15 deletions(-) create mode 100644 vim.go diff --git a/README.md b/README.md index 545dd28..4f92e2e 100644 --- a/README.md +++ b/README.md @@ -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 @@ -139,7 +139,7 @@ for { # Shortcut `Meta`+`B` means press `Esc` and `n` separately. -Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B` +Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B` Notice: `Meta`+`B` is equals with `Alt`+`B` in windows. * Shortcut in normal mode @@ -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 | diff --git a/char.go b/char.go index 1293e16..7885da3 100644 --- a/char.go +++ b/char.go @@ -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 diff --git a/complete.go b/complete.go index 9101752..122779a 100644 --- a/complete.go +++ b/complete.go @@ -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: diff --git a/example/main.go b/example/main.go index 534bfb8..eda3be9 100644 --- a/example/main.go +++ b/example/main.go @@ -40,6 +40,7 @@ func main() { Prompt: "\033[31m»\033[0m ", HistoryFile: "/tmp/readline.tmp", AutoComplete: completer, + // VimMode: true, }) if err != nil { panic(err) diff --git a/operation.go b/operation.go index b1d77e8..739d2f6 100644 --- a/operation.go +++ b/operation.go @@ -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() { diff --git a/readline.go b/readline.go index 8bdebab..04f339c 100644 --- a/readline.go +++ b/readline.go @@ -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) } diff --git a/runebuf.go b/runebuf.go index d680628..5cfa9f7 100644 --- a/runebuf.go +++ b/runebuf.go @@ -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() diff --git a/terminal.go b/terminal.go index 12ac640..7afb025 100644 --- a/terminal.go +++ b/terminal.go @@ -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 diff --git a/utils.go b/utils.go index f3b3eb6..6e83ea2 100644 --- a/utils.go +++ b/utils.go @@ -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: diff --git a/vim.go b/vim.go new file mode 100644 index 0000000..c64fbaa --- /dev/null +++ b/vim.go @@ -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 +}