From 3b1cf6b8fb36e6c0209fa33ec74bf30206214c94 Mon Sep 17 00:00:00 2001 From: Cheney Date: Tue, 22 Sep 2015 23:01:15 +0800 Subject: [PATCH] add fwd/bck search --- history.go | 22 +++++++++ operation.go | 52 ++++++++++++++++++---- runebuf.go | 27 ++++++++++- search.go | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++ terminal.go | 2 + utils.go | 40 +++++++++++++++++ 6 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 search.go diff --git a/history.go b/history.go index 9a0a331..63b8f5f 100644 --- a/history.go +++ b/history.go @@ -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 { diff --git a/operation.go b/operation.go index 0a17a71..b0f6a20 100644 --- a/operation.go +++ b/operation.go @@ -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) } diff --git a/runebuf.go b/runebuf.go index d3d4cbd..06d0d39 100644 --- a/runebuf.go +++ b/runebuf.go @@ -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) +} diff --git a/search.go b/search.go new file mode 100644 index 0000000..1ae82a5 --- /dev/null +++ b/search.go @@ -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()) +} diff --git a/terminal.go b/terminal.go index 1204be5..85c0433 100644 --- a/terminal.go +++ b/terminal.go @@ -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: diff --git a/utils.go b/utils.go index e159682..be98f28 100644 --- a/utils.go +++ b/utils.go @@ -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 +}