mirror of https://github.com/chzyer/readline.git
add simple vim mode
This commit is contained in:
parent
390f0ebb6b
commit
79d1bf27b4
14
README.md
14
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 |
|
||||
|
|
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:
|
||||
|
|
|
@ -40,6 +40,7 @@ func main() {
|
|||
Prompt: "\033[31m»\033[0m ",
|
||||
HistoryFile: "/tmp/readline.tmp",
|
||||
AutoComplete: completer,
|
||||
// VimMode: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
27
operation.go
27
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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
38
runebuf.go
38
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()
|
||||
|
|
|
@ -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,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
|
||||
}
|
Loading…
Reference in New Issue