mirror of https://github.com/chzyer/readline.git
Merge pull request #7 from chzyer/feature/add_vim_mode
add simple vim mode
This commit is contained in:
commit
38e5213cc8
|
@ -24,10 +24,15 @@ You can read the source code in [example/main.go](https://github.com/chzyer/read
|
|||
|
||||
# Todo
|
||||
|
||||
* Vim mode
|
||||
* More funny examples
|
||||
* 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
|
||||
|
||||
* Simplest example
|
||||
|
@ -139,7 +144,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
|
||||
|
|
6
char.go
6
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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"),
|
||||
|
@ -52,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 {
|
||||
|
|
26
operation.go
26
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,20 @@ func (o *Operation) ioloop() {
|
|||
case CharInterrupt:
|
||||
o.t.KickRead()
|
||||
fallthrough
|
||||
case CharCancel:
|
||||
case CharBell:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if o.IsEnableVimMode() {
|
||||
r = o.HandleVim(r, o.t.ReadRune)
|
||||
if r == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch r {
|
||||
case CharCancel:
|
||||
case CharBell:
|
||||
if o.IsSearchMode() {
|
||||
o.ExitSearchMode(true)
|
||||
o.buf.Refresh(nil)
|
||||
|
@ -119,11 +128,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 +141,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 +152,7 @@ func (o *Operation) ioloop() {
|
|||
}
|
||||
|
||||
if o.buf.Len() == 0 {
|
||||
o.t.Bell()
|
||||
break
|
||||
}
|
||||
o.buf.Backspace()
|
||||
|
@ -167,11 +179,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() {
|
||||
|
|
|
@ -11,6 +11,7 @@ type Config struct {
|
|||
Prompt string
|
||||
HistoryFile string
|
||||
AutoComplete AutoCompleter
|
||||
VimMode bool
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
|
@ -59,6 +60,14 @@ func (i *Instance) Stderr() io.Writer {
|
|||
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) {
|
||||
return i.o.Password(prompt)
|
||||
}
|
||||
|
|
66
runebuf.go
66
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,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() {
|
||||
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 +156,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 +165,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 +270,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()
|
||||
|
|
|
@ -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
|
||||
|
|
4
utils.go
4
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:
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue