add fwd/bck search

This commit is contained in:
Cheney 2015-09-22 23:01:15 +08:00
parent 3f23122fec
commit 3b1cf6b8fb
6 changed files with 256 additions and 10 deletions

View File

@ -58,6 +58,28 @@ func (o *opHistory) Close() {
}
}
func (o *opHistory) FindHistoryBck(rs []rune) (int, *list.Element) {
for elem := o.current; elem != nil; elem = elem.Prev() {
idx := RunesIndex(o.showItem(elem.Value), rs)
if idx < 0 {
continue
}
return idx, elem
}
return -1, nil
}
func (o *opHistory) FindHistoryFwd(rs []rune) (int, *list.Element) {
for elem := o.current; elem != nil; elem = elem.Next() {
idx := RunesIndex(o.showItem(elem.Value), rs)
if idx < 0 {
continue
}
return idx, elem
}
return -1, nil
}
func (o *opHistory) showItem(obj interface{}) []rune {
item := obj.(*HisItem)
if item.Version == o.historyVer {

View File

@ -11,19 +11,23 @@ type Operation struct {
buf *RuneBuffer
outchan chan []rune
*opHistory
*opSearch
}
const (
CharLineStart = 0x1
CharLineEnd = 0x5
CharLineStart = 1
CharLineEnd = 5
CharKill = 11
CharNext = 0xe
CharPrev = 0x10
CharBackward = 0x2
CharForward = 0x6
CharNext = 14
CharPrev = 16
CharBackward = 2
CharForward = 6
CharBackspace = 0x7f
CharEnter = 0xd
CharEnter2 = 0xa
CharBckSearch = 18
CharFwdSearch = 19
CharCannel = 7
)
type wrapWriter struct {
@ -47,14 +51,27 @@ func NewOperation(t *Terminal, cfg *Config) *Operation {
outchan: make(chan []rune),
opHistory: newOpHistory(cfg.HistoryFile),
}
op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory)
go op.ioloop()
return op
}
func (l *Operation) ioloop() {
for {
keepInSearchMode := false
r := l.t.ReadRune()
switch r {
case CharCannel:
if l.IsSearchMode() {
l.ExitSearchMode(true)
l.buf.Refresh()
}
case CharBckSearch:
l.SearchMode(S_DIR_BCK)
keepInSearchMode = true
case CharFwdSearch:
l.SearchMode(S_DIR_FWD)
keepInSearchMode = true
case CharKill:
l.buf.Kill()
case MetaNext:
@ -70,7 +87,12 @@ func (l *Operation) ioloop() {
case KeyDelete:
l.buf.Delete()
case CharBackspace:
l.buf.Backspace()
if l.IsSearchMode() {
l.SearchBackspace()
keepInSearchMode = true
} else {
l.buf.Backspace()
}
case MetaBackspace:
l.buf.BackEscapeWord()
case CharEnter, CharEnter2:
@ -95,10 +117,24 @@ func (l *Operation) ioloop() {
l.buf.Set(buf)
}
case KeyInterrupt:
if l.IsSearchMode() {
l.ExitSearchMode(false)
}
l.buf.MoveToLineEnd()
l.buf.Refresh()
l.buf.WriteString("^C\n")
l.outchan <- nil
default:
l.buf.WriteRune(r)
if l.IsSearchMode() {
l.SearchChar(r)
keepInSearchMode = true
} else {
l.buf.WriteRune(r)
}
}
if !keepInSearchMode && l.IsSearchMode() {
l.ExitSearchMode(false)
l.buf.Refresh()
}
l.UpdateHistory(l.buf.Runes(), false)
}

View File

@ -170,6 +170,25 @@ func (r *RuneBuffer) MoveToLineEnd() {
r.Refresh()
}
func (r *RuneBuffer) LineCount() int {
return LineCount(RunesWidth(r.buf) + len(r.prompt))
}
func (r *RuneBuffer) IdxLine() int {
totalWidth := RunesWidth(r.buf[:r.idx]) + len(r.prompt)
w := getWidth()
line := 0
for totalWidth >= w {
totalWidth -= w
line++
}
return line
}
func (r *RuneBuffer) CursorLineCount() int {
return r.LineCount() - r.IdxLine()
}
func (r *RuneBuffer) Refresh() {
r.w.Write(r.Output())
}
@ -205,8 +224,12 @@ func (r *RuneBuffer) Reset() []rune {
return ret
}
func (r *RuneBuffer) Set(buf []rune) {
func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) {
r.buf = buf
r.idx = len(r.buf)
r.idx = idx
r.Refresh()
}
func (r *RuneBuffer) Set(buf []rune) {
r.SetWithIdx(len(buf), buf)
}

123
search.go Normal file
View File

@ -0,0 +1,123 @@
package readline
import (
"bytes"
"container/list"
"fmt"
"io"
)
const (
S_STATE_FOUND = iota
S_STATE_FAILING
)
const (
S_DIR_BCK = iota
S_DIR_FWD
)
type opSearch struct {
inMode bool
state int
dir int
source *list.Element
w io.Writer
buf *RuneBuffer
data []rune
history *opHistory
}
func newOpSearch(w io.Writer, buf *RuneBuffer, history *opHistory) *opSearch {
return &opSearch{
w: w,
buf: buf,
history: history,
}
}
func (o *opSearch) IsSearchMode() bool {
return o.inMode
}
func (o *opSearch) SearchBackspace() {
if len(o.data) > 0 {
o.data = o.data[:len(o.data)-1]
o.search()
}
}
func (o *opSearch) findHistoryBy() (int, *list.Element) {
if o.dir == S_DIR_BCK {
return o.history.FindHistoryBck(o.data)
}
return o.history.FindHistoryFwd(o.data)
}
func (o *opSearch) search() bool {
idx, elem := o.findHistoryBy()
if elem == nil {
o.SearchRefresh(-2)
return false
}
o.history.current = elem
o.buf.SetWithIdx(idx, o.history.showItem(o.history.current.Value))
o.SearchRefresh(idx)
return true
}
func (o *opSearch) SearchChar(r rune) {
o.data = append(o.data, r)
o.search()
}
func (o *opSearch) SearchMode(dir int) {
o.inMode = true
o.dir = dir
o.source = o.history.current
o.SearchRefresh(-1)
}
func (o *opSearch) ExitSearchMode(revert bool) {
if revert {
o.history.current = o.source
o.buf.Set(o.history.showItem(o.history.current.Value))
}
o.inMode = false
o.source = nil
o.data = nil
}
func (o *opSearch) SearchRefresh(x int) {
if x == -2 {
o.state = S_STATE_FAILING
} else if x >= 0 {
o.state = S_STATE_FOUND
}
if x < 0 {
x = o.buf.idx
}
x += len(o.buf.prompt)
x = x % getWidth()
lineCnt := o.buf.CursorLineCount()
buf := bytes.NewBuffer(nil)
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
buf.WriteString("\033[J")
if o.state == S_STATE_FAILING {
buf.WriteString("failing ")
}
if o.dir == S_DIR_BCK {
buf.WriteString("bck")
} else if o.dir == S_DIR_FWD {
buf.WriteString("fwd")
}
buf.WriteString("-i-search: ")
buf.WriteString(string(o.data)) // keyword
buf.WriteString("\033[4m \033[0m") // _
fmt.Fprintf(buf, "\r\033[%dA", lineCnt) // move prev
if x > 0 {
fmt.Fprintf(buf, "\033[%dC", x) // move forward
}
o.w.Write(buf.Bytes())
}

View File

@ -102,6 +102,8 @@ func (t *Terminal) ioloop() {
isEscape = true
case CharEnter, CharEnter2, KeyPrevChar, KeyNextChar, KeyDelete:
fallthrough
case CharFwdSearch, CharBckSearch, CharCannel:
fallthrough
case CharLineEnd, CharLineStart, CharNext, CharPrev, CharKill:
t.outchan <- r
default:

View File

@ -6,6 +6,7 @@ import (
"os"
"syscall"
"time"
"unicode/utf8"
"unsafe"
"golang.org/x/crypto/ssh/terminal"
@ -109,3 +110,42 @@ func sleep(n int) {
Debug(n)
time.Sleep(2000 * time.Millisecond)
}
func LineCount(w int) int {
screenWidth := getWidth()
r := w / screenWidth
if w%screenWidth != 0 {
r++
}
return r
}
func RunesWidth(r []rune) (length int) {
for i := 0; i < len(r); i++ {
if utf8.RuneLen(r[i]) > 1 {
length += 2
} else {
length += 1
}
}
return
}
func RunesIndex(r, sub []rune) int {
for i := 0; i < len(r); i++ {
found := true
if len(r[i:]) < len(sub) {
return -1
}
for j := 0; j < len(sub); j++ {
if r[i+j] != sub[j] {
found = false
break
}
}
if found {
return i
}
}
return -1
}