From 79d1bf27b4a097f08e01f85485f11086a78eacf6 Mon Sep 17 00:00:00 2001 From: Cheney Date: Thu, 1 Oct 2015 22:44:43 +0800 Subject: [PATCH 1/3] 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 +} From 879224ddc999a58d955e1d5a6aa72c83405f7fab Mon Sep 17 00:00:00 2001 From: Cheney Date: Fri, 2 Oct 2015 10:37:21 +0800 Subject: [PATCH 2/3] refactor --- README.md | 6 ++ example/main.go | 22 ++++++- operation.go | 5 +- readline.go | 4 ++ runebuf.go | 28 +++++++++ vim.go | 153 +++++++++++++++++++++++++----------------------- 6 files changed, 139 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 4f92e2e..0d5dd8d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ You can read the source code in [example/main.go](https://github.com/chzyer/read * More funny examples * Support dumb/eterm-color terminal in emacs +# Features +* Support emacs/vi mode, almost all basic features GNU-Readline support +* 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 * Simplest example diff --git a/example/main.go b/example/main.go index eda3be9..e550e95 100644 --- a/example/main.go +++ b/example/main.go @@ -20,6 +20,10 @@ bye } var completer = readline.NewPrefixCompleter( + readline.PcItem("mode", + readline.PcItem("vi"), + readline.PcItem("emacs"), + ), readline.PcItem("login"), readline.PcItem("say", readline.PcItem("hello"), @@ -40,7 +44,6 @@ func main() { Prompt: "\033[31m»\033[0m ", HistoryFile: "/tmp/readline.tmp", AutoComplete: completer, - // VimMode: true, }) if err != nil { panic(err) @@ -53,8 +56,23 @@ func main() { if err != nil { break } - + line = strings.TrimSpace(line) 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": pswd, err := l.ReadPassword("please enter your password: ") if err != nil { diff --git a/operation.go b/operation.go index 739d2f6..bd2247c 100644 --- a/operation.go +++ b/operation.go @@ -95,9 +95,8 @@ func (o *Operation) ioloop() { } if o.IsEnableVimMode() { - var ok bool - r, ok = o.HandleVim(r, o.t.ReadRune) - if ok { + r = o.HandleVim(r, o.t.ReadRune) + if r == 0 { continue } } diff --git a/readline.go b/readline.go index 04f339c..e91d6ae 100644 --- a/readline.go +++ b/readline.go @@ -64,6 +64,10 @@ 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) { return i.o.Password(prompt) } diff --git a/runebuf.go b/runebuf.go index 5cfa9f7..4b0f4aa 100644 --- a/runebuf.go +++ b/runebuf.go @@ -5,6 +5,11 @@ import ( "io" ) +type runeBufferBck struct { + buf []rune + idx int +} + type RuneBuffer struct { buf []rune idx int @@ -12,6 +17,22 @@ type RuneBuffer struct { w io.Writer 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 { @@ -98,6 +119,13 @@ func (r *RuneBuffer) MoveForward() { }) } +func (r *RuneBuffer) Erase() { + r.Refresh(func() { + r.idx = 0 + r.buf = r.buf[:0] + }) +} + func (r *RuneBuffer) Delete() (success bool) { r.Refresh(func() { if r.idx == len(r.buf) { diff --git a/vim.go b/vim.go index c64fbaa..61f28e0 100644 --- a/vim.go +++ b/vim.go @@ -30,92 +30,97 @@ func (o *opVim) SetVimMode(on bool) { } func (o *opVim) ExitVimMode() { - o.vimMode = VIM_NORMAL + o.vimMode = VIM_INSERT } func (o *opVim) IsEnableVimMode() bool { return o.cfg.VimMode } -func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune, handle bool) { +func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) { + rb := o.op.buf + handled = true switch r { - case CharEnter, CharInterrupt: + 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': + 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: + 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 '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() + 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.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 + 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 r, true + return 0 } func (o *opVim) EnterVimInsertMode() { @@ -126,19 +131,19 @@ func (o *opVim) ExitVimInsertMode() { o.vimMode = VIM_NORMAL } -func (o *opVim) HandleVim(r rune, readNext func() rune) (rune, bool) { +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 r, true + return 0 } switch o.vimMode { case VIM_INSERT: - return r, false + return r case VIM_VISUAL: } - return r, false + return r } From 4d6d6c223f24eb2a5bd3bc61e990116486d2b7ac Mon Sep 17 00:00:00 2001 From: Cheney Date: Fri, 2 Oct 2015 10:58:43 +0800 Subject: [PATCH 3/3] fixes bugs --- README.md | 13 +------------ vim.go | 4 ++-- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0d5dd8d..72154aa 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,11 @@ You can read the source code in [example/main.go](https://github.com/chzyer/read # Todo -* Vim mode (WIP) * More funny examples * Support dumb/eterm-color terminal in emacs # Features -* Support emacs/vi mode, almost all basic features GNU-Readline support +* 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). @@ -205,16 +204,6 @@ 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/vim.go b/vim.go index 61f28e0..f587b6c 100644 --- a/vim.go +++ b/vim.go @@ -53,9 +53,9 @@ func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, h rb.MoveToLineStart() case '$': rb.MoveToLineEnd() - case 'b': + case 'b', 'B': rb.MoveToPrevWord() - case 'w': + case 'w', 'W', 'e', 'E': rb.MoveToNextWord() case 'f', 'F', 't', 'T': next := readNext()