Merge branch 'master' into make-some-pizza

Conflicts:
	ctx.go
	keymap.go
	matchers.go
This commit is contained in:
Daisuke Maki 2014-06-18 14:32:00 +09:00
commit 124a994499
10 changed files with 278 additions and 104 deletions

26
Changes
View file

@ -1,6 +1,32 @@
Changes
=======
v0.1.4 - 17 Jun 2014
Buts/Fixes:
* Check for ev.Ch and ev.Key (should fix input problems)
* Fix crashing issue on empty match
Features:
* In your config, setting the value to "-" will remove the
binding.
* Default ToggleSelect binding has been changed to
ToggleSelectAndSelectNext
v0.1.3 - 17 Jun 2014
Bugs/Fixes:
* When dealing with fast/successive user input on large buffers,
peco was taking too long to execute queries.
* XDG style config directories are now searched, and if all fails,
falls back to the original ~/.peco/config.json
* Some internal cleanup
Features:
* Multiple line selection has been implemented. Ctrl-Space will
toggle the currently selected line, and peco will exit after
printing all the selected lines. Note that on OS X, Spotlight
by default captures these keys. You may need to reconfigure
your settings.
* Custom matchers via external processes have been implemented.
See the README for more details
v0.1.2 - 16 Jun 2014
Bugs/Fixes:
* Multiple queries were not being match fully until the end of line

View file

@ -28,18 +28,24 @@ When you combine tools like zsh, peco, and [ghq](https://github.com/motemen/ghq)
Features
========
## Incremental search
## Incremental Search
Search results are filtered as you type. This is great to drill down to the
line you are looking for
Multiple terms turn the query into an "AND" query:
![optimized](https://cloud.githubusercontent.com/assets/554281/3241419/d5fccc5c-f13c-11e3-898b-280a246b083c.gif)
![optimized](http://lestrrat.github.io/peco/peco-demo-multiple-queries.gif)
When you find that line that you want, press enter, and the resulting line
is printed to stdout, which allows you to pipe it to other tools
## Select Multiple Lines
You can select multiple lines!
![optimized](http://lestrrat.github.io/peco/peco-demo-multiple-selection.gif)
## Select Matchers
Different types of matchers are available. Default is case-insensitive matcher, so lines with any case will match. You can toggle between IgnoreCase, CaseSensitive, and RegExp matchers. The RegExp matcher allows you to use any valid regular expression to match lines
@ -197,6 +203,8 @@ Example:
| peco.SelectNextPage | Jumps to next page|
| peco.SelectPrevious | Selects previous line |
| peco.SelectNext | Selects next line |
| peco.ToggleSelection | Selects the current line, and saves it |
| peco.ToggleSelectionAndSelectNext | Selects the current line, saves it, and proceeds to the next line |
| peco.RotateMatcher | Rotate between matchers (by default, ignore-case/no-ignore-case)|
| peco.Finish | Exits from peco, with success status |
| peco.Cancel | Exits from peco, with failure status |
@ -247,9 +255,9 @@ For now, styles of following 3 items can be customized in `config.json`.
This is an experimental feature. Please note that some details of this specificaiton may change
By default `peco` comes with `IgnoreCase`, `CaseSensitive`, and `Regexp` matchers, but it is possible to create your own custom matcher.
By default `peco` comes with `IgnoreCase`, `CaseSensitive`, and `Regexp` matchers, but since v0.1.3, it is possible to create your own custom matcher.
The matcher will be executed via `Command.Run()` as an external process, and it will be passed the query values in the command line, and the original unaltered buffer is passed via `os.Stdin`. Your matcher must perform the matching, and print out to `os.Stdout` matched lines. Note that currently there is no way to specify where in the line the match occurred.
The matcher will be executed via `Command.Run()` as an external process, and it will be passed the query values in the command line, and the original unaltered buffer is passed via `os.Stdin`. Your matcher must perform the matching, and print out to `os.Stdout` matched lines. Note that currently there is no way to specify where in the line the match occurred. Note that the matcher does not need to be a go program. It can be a perl/ruby/python/bash script, or anything else that is executable.
Once you have a matcher, you must specify how the matcher is spawned:
@ -267,7 +275,8 @@ You may specify as many matchers as you like.
## Examples
* [C/Migemo](https://github.com/lestrrat/peco/wiki/CustoMatcher-CMigemo)
* [An example of a simple perl regexp matcher](https://gist.github.com/mattn/24712964da6e3112251c)
* [C/Migemo](https://github.com/mattn/peco-cmigemo/)
Hacking
=======

View file

@ -11,7 +11,7 @@ import (
"github.com/nsf/termbox-go"
)
var version = "v0.1.2"
var version = "v0.1.4"
func showHelp() {
const v = `

View file

@ -12,6 +12,7 @@ import (
)
var currentUser = user.Current
type Config struct {
Keymap Keymap `json:"Keymap"`
Matcher string `json:"Matcher"`
@ -131,6 +132,7 @@ func stringsToStyle(raw []string) *Style {
}
var _locateRcfileIn = locateRcfileIn
func locateRcfileIn(dir string) (string, error) {
const basename = "config.json"
file := filepath.Join(dir, basename)

4
ctx.go
View file

@ -60,6 +60,7 @@ func (s Selection) Less(i, j int) bool {
// data in this struct from anwyehre, only do so via channels
type Ctx struct {
enableSep bool
statusMessage string
result []Match
loopCh chan struct{}
queryCh chan string
@ -83,6 +84,7 @@ type Ctx struct {
func NewCtx(enableSep bool) *Ctx {
return &Ctx{
enableSep,
"",
[]Match{},
make(chan struct{}), // loopCh. You never send messages to this. no point in buffering
make(chan string, 5), // queryCh.
@ -188,7 +190,7 @@ func (c *Ctx) NewView() *View {
}
func (c *Ctx) NewFilter() *Filter {
return &Filter{c}
return &Filter{c, make(chan string)}
}
func (c *Ctx) NewInput() *Input {

View file

@ -2,19 +2,41 @@ package peco
type Filter struct {
*Ctx
jobs chan string
}
func (f *Filter) Work(cancel chan struct{}, q string) {
if q == "" {
f.DrawMatches(nil)
return
}
results := f.Matcher().Match(cancel, q, f.Buffer())
f.statusMessage = ""
f.selection.Clear()
f.DrawMatches(results)
}
func (f *Filter) Loop() {
defer f.ReleaseWaitGroup()
// previous holds a channel that can cancel the previous
// query. This is used when multiple queries come in succession
// and the previous query is discarded anyway
var previous chan struct{}
for {
select {
case <-f.LoopCh():
return
case q := <-f.QueryCh():
results := f.Matcher().Match(q, f.Buffer())
f.selection.Clear()
f.DrawMatches(results)
if previous != nil {
// Tell the previous query to stop
previous <- struct{}{}
}
previous = make(chan struct{}, 1)
f.statusMessage = "Running query..."
f.DrawMatches(nil)
go f.Work(previous, q)
}
}
}

View file

@ -43,7 +43,7 @@ func (i *Input) Loop() {
}
func (i *Input) handleKeyEvent(ev termbox.Event) {
if h := i.config.Keymap.Handler(ev.Key); h != nil {
if h := i.config.Keymap.Handler(ev); h != nil {
h(i, ev)
return
}

View file

@ -131,7 +131,9 @@ func handleFinish(i *Input, _ termbox.Event) {
i.result = []Match{}
for _, lineno := range i.selection {
i.result = append(i.result, i.current[lineno-1])
if lineno <= len(i.current) {
i.result = append(i.result, i.current[lineno-1])
}
}
i.Finish()
}
@ -144,6 +146,11 @@ func handleToggleSelection(i *Input, _ termbox.Event) {
i.selection.Add(i.currentLine)
}
func handleToggleSelectionAndSelectNext(i *Input, ev termbox.Event) {
handleToggleSelection(i, ev)
handleSelectNext(i, ev)
}
// peco.Cancel -> end program, exit with failure
func handleCancel(i *Input, ev termbox.Event) {
i.ExitStatus = 1
@ -430,27 +437,28 @@ func (ksk KeymapStringKey) ToKey() (k termbox.Key, err error) {
}
var handlers = map[string]KeymapHandler{
"peco.KillEndOfLine": handleKillEndOfLine,
"peco.DeleteAll": handleDeleteAll,
"peco.BeginningOfLine": handleBeginningOfLine,
"peco.EndOfLine": handleEndOfLine,
"peco.EndOfFile": handleEndOfFile,
"peco.ForwardChar": handleForwardChar,
"peco.BackwardChar": handleBackwardChar,
"peco.ForwardWord": handleForwardWord,
"peco.BackwardWord": handleBackwardWord,
"peco.DeleteForwardChar": handleDeleteForwardChar,
"peco.DeleteBackwardChar": handleDeleteBackwardChar,
"peco.DeleteForwardWord": handleDeleteForwardWord,
"peco.DeleteBackwardWord": handleDeleteBackwardWord,
"peco.SelectPreviousPage": handleSelectPreviousPage,
"peco.SelectNextPage": handleSelectNextPage,
"peco.SelectPrevious": handleSelectPrevious,
"peco.SelectNext": handleSelectNext,
"peco.ToggleSelection": handleToggleSelection,
"peco.RotateMatcher": handleRotateMatcher,
"peco.Finish": handleFinish,
"peco.Cancel": handleCancel,
"peco.KillEndOfLine": handleKillEndOfLine,
"peco.DeleteAll": handleDeleteAll,
"peco.BeginningOfLine": handleBeginningOfLine,
"peco.EndOfLine": handleEndOfLine,
"peco.EndOfFile": handleEndOfFile,
"peco.ForwardChar": handleForwardChar,
"peco.BackwardChar": handleBackwardChar,
"peco.ForwardWord": handleForwardWord,
"peco.BackwardWord": handleBackwardWord,
"peco.DeleteForwardChar": handleDeleteForwardChar,
"peco.DeleteBackwardChar": handleDeleteBackwardChar,
"peco.DeleteForwardWord": handleDeleteForwardWord,
"peco.DeleteBackwardWord": handleDeleteBackwardWord,
"peco.SelectPreviousPage": handleSelectPreviousPage,
"peco.SelectNextPage": handleSelectNextPage,
"peco.SelectPrevious": handleSelectPrevious,
"peco.SelectNext": handleSelectNext,
"peco.ToggleSelection": handleToggleSelection,
"peco.ToggleSelectionAndSelectNext": handleToggleSelectionAndSelectNext,
"peco.RotateMatcher": handleRotateMatcher,
"peco.Finish": handleFinish,
"peco.Cancel": handleCancel,
}
func NewKeymap() Keymap {
@ -474,13 +482,16 @@ func NewKeymap() Keymap {
termbox.KeyCtrlK: handleKillEndOfLine,
termbox.KeyCtrlU: handleKillBeginOfLine,
termbox.KeyCtrlR: handleRotateMatcher,
termbox.KeyCtrlSpace: handleToggleSelectionAndSelectNext,
}
}
func (km Keymap) Handler(k termbox.Key) KeymapHandler {
h, ok := km[k]
if ok {
return h
func (km Keymap) Handler(ev termbox.Event) KeymapHandler {
if ev.Ch == 0 {
h, ok := km[ev.Key]
if ok {
return h
}
}
return handleAcceptChar
}
@ -498,6 +509,11 @@ func (km Keymap) UnmarshalJSON(buf []byte) error {
continue
}
if vs == "-" {
delete(km, k)
continue
}
v, ok := handlers[vs]
if !ok {
fmt.Fprintf(os.Stderr, "Unknown handler %s", vs)

View file

@ -12,13 +12,13 @@ import (
// we have a DidMatch and NoMatch types instead of using []Match and []string.
type Match interface {
Buffer() string // Raw buffer, may contain null
Line() string // Line to be displayed
Line() string // Line to be displayed
Output() string // Output string to be displayed after peco is done
Indices() [][]int
}
type MatchString struct {
buf string
buf string
sepLoc int
}
@ -74,7 +74,7 @@ func (m NoMatch) Indices() [][]int {
return nil
}
// DidMatch contains the actual match, and the indices to the matches
// DidMatch contains the actual match, and the indices to the matches
// in the line
type DidMatch struct {
*MatchString
@ -92,7 +92,15 @@ func (d DidMatch) Indices() [][]int {
// Matcher interface defines the API for things that want to
// match against the buffer
type Matcher interface {
Match(string, []Match) []Match
// Match takes in three parameters.
//
// The first chan is the channel where cancel requests are sent.
// If you receive a request here, you should stop running your query.
//
// The second is the query. Do what you want with it
//
// The third is the buffer in which to match the query against.
Match(chan struct{}, string, []Match) []Match
String() string
}
@ -118,8 +126,8 @@ type IgnoreCaseMatcher struct {
type CustomMatcher struct {
enableSep bool
name string
args []string
name string
args []string
}
func NewCaseSensitiveMatcher(enableSep bool) *CaseSensitiveMatcher {
@ -210,19 +218,54 @@ func (m byStart) Less(i, j int) bool {
return m[i][0] < m[j][0]
}
func (m *RegexpMatcher) Match(q string, buffer []Match) []Match {
func (m *RegexpMatcher) Match(quit chan struct{}, q string, buffer []Match) []Match {
results := []Match{}
regexps, err := m.QueryToRegexps(q)
if err != nil {
return results
}
for _, line := range buffer {
ms := m.MatchAllRegexps(regexps, line.Line())
if ms == nil {
continue
// The actual matching is done in a separate goroutine
iter := make(chan Match, len(buffer))
go func() {
// This protects us from panics, caused when we cancel the
// query and forcefully close the channel (and thereby
// causing a "close of a closed channel"
defer func() { recover() }()
// This must be here to make sure the channel is properly
// closed in normal cases
defer close(iter)
// Iterate through the lines, and do the match.
// Upon success, send it through the channel
for _, match := range buffer {
ms := m.MatchAllRegexps(regexps, match.Line())
if ms == nil {
continue
}
iter <- NewDidMatch(match.Buffer(), m.enableSep, ms)
}
iter <- nil
}()
MATCH:
for {
select {
case <-quit:
// If we recieved a cancel request, we immediately bail out.
// It's a little dirty, but we focefully terminate the other
// goroutine by closing the channel, and invoking a panic
close(iter)
break MATCH
case match := <-iter:
// Receive elements from the goroutine performing the match
if match == nil {
break MATCH
}
results = append(results, match)
}
results = append(results, NewDidMatch(line.Buffer(), m.enableSep, ms))
}
return results
}
@ -263,40 +306,68 @@ Match:
return matches
}
func (m *CustomMatcher) Match(q string, buffer []Match) []Match {
func (m *CustomMatcher) Match(quit chan struct{}, q string, buffer []Match) []Match {
if len(m.args) < 1 {
return []Match{}
}
results := []Match{}
if q != "" {
lines := []Match{}
matcherInput := ""
if q == "" {
for _, match := range buffer {
matcherInput += match.Line() + "\n"
lines = append(lines, match)
results = append(results, NewDidMatch(match.Buffer(), m.enableSep, nil))
}
args := []string{}
for _, arg := range m.args {
if arg == "$QUERY" {
arg = q
return results
}
// Receive elements from the goroutine performing the match
lines := []Match{}
matcherInput := ""
for _, match := range buffer {
matcherInput += match.Line() + "\n"
lines = append(lines, match)
}
args := []string{}
for _, arg := range m.args {
if arg == "$QUERY" {
arg = q
}
args = append(args, arg)
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = strings.NewReader(matcherInput)
// See RegexpMatcher.Match() for explanation of constructs
iter := make(chan Match, len(buffer))
go func() {
defer func() { recover() }()
defer func() {
close(iter)
if p := cmd.Process; p != nil {
p.Kill()
}
args = append(args, arg)
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = strings.NewReader(matcherInput)
}()
b, err := cmd.Output()
if err != nil {
return []Match{}
iter <- nil
}
for _, line := range strings.Split(string(b), "\n") {
if len(line) > 0 {
results = append(results, NewDidMatch(line, m.enableSep, nil))
iter <- NewDidMatch(line, m.enableSep, nil)
}
}
} else {
for _, match := range buffer {
results = append(results, NewDidMatch(match.Buffer(), m.enableSep, nil))
iter <- nil
}()
MATCH:
for {
select {
case <-quit:
close(iter)
break MATCH
case match := <-iter:
if match == nil {
break MATCH
}
results = append(results, match)
}
}

98
view.go
View file

@ -8,33 +8,57 @@ import (
"github.com/nsf/termbox-go"
)
// View handles the drawing/updating the screen
type View struct {
*Ctx
}
// PagingRequest can be sent to move the selection cursor
type PagingRequest int
const (
// ToNextLine moves the selection to the next line
ToNextLine PagingRequest = iota
// ToNextPage moves the selection to the next page
ToNextPage
// ToPrevLine moves the selection to the previous line
ToPrevLine
// ToPrevPage moves the selection to the previous page
ToPrevPage
)
func (u *View) Loop() {
defer u.ReleaseWaitGroup()
// Loop receives requests to update the screen
func (v *View) Loop() {
defer v.ReleaseWaitGroup()
for {
select {
case <-u.LoopCh():
case <-v.LoopCh():
return
case r := <-u.PagingCh():
u.movePage(r)
case lines := <-u.DrawCh():
u.drawScreen(lines)
case r := <-v.PagingCh():
v.movePage(r)
case lines := <-v.DrawCh():
v.drawScreen(lines)
}
}
}
func (v *View) printStatus() {
w, h := termbox.Size()
msg := v.statusMessage
width := runewidth.StringWidth(msg)
pad := make([]byte, w - width)
for i := 0; i < w - width; i++ {
pad[i] = ' '
}
printTB(0, h - 2, termbox.ColorDefault, termbox.ColorDefault, string(pad))
if width > 0 {
printTB(w - width, h - 2, termbox.AttrReverse|termbox.ColorDefault|termbox.AttrBold, termbox.AttrReverse|termbox.ColorDefault, msg)
}
}
func printTB(x, y int, fg, bg termbox.Attribute, msg string) {
for len(msg) > 0 {
c, w := utf8.DecodeRuneInString(msg)
@ -83,17 +107,17 @@ func (v *View) movePage(p PagingRequest) {
v.drawScreen(nil)
}
func (u *View) drawScreen(targets []Match) {
u.mutex.Lock()
defer u.mutex.Unlock()
func (v *View) drawScreen(targets []Match) {
v.mutex.Lock()
defer v.mutex.Unlock()
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
if targets == nil {
if current := u.Ctx.current; current != nil {
targets = u.Ctx.current
if current := v.Ctx.current; current != nil {
targets = v.Ctx.current
} else {
targets = u.Ctx.lines
targets = v.Ctx.lines
}
}
@ -101,7 +125,7 @@ func (u *View) drawScreen(targets []Match) {
perPage := height - 4
CALCULATE_PAGE:
currentPage := ((u.Ctx.currentLine - 1) / perPage) + 1
currentPage := ((v.Ctx.currentLine - 1) / perPage) + 1
if currentPage <= 0 {
currentPage = 1
}
@ -114,7 +138,7 @@ CALCULATE_PAGE:
}
if maxPage < currentPage {
u.Ctx.currentLine = offset
v.Ctx.currentLine = offset
goto CALCULATE_PAGE
}
@ -122,24 +146,24 @@ CALCULATE_PAGE:
promptLen := runewidth.StringWidth(prompt)
printTB(0, 0, termbox.ColorDefault, termbox.ColorDefault, prompt)
if u.caretPos <= 0 {
u.caretPos = 0 // sanity
if v.caretPos <= 0 {
v.caretPos = 0 // sanity
}
if u.caretPos > len(u.query) {
u.caretPos = len(u.query)
if v.caretPos > len(v.query) {
v.caretPos = len(v.query)
}
if u.caretPos == len(u.query) {
if v.caretPos == len(v.query) {
// the entire string + the caret after the string
printTB(promptLen+1, 0, termbox.ColorDefault, termbox.ColorDefault, string(u.query))
termbox.SetCell(promptLen+1+runewidth.StringWidth(string(u.query)), 0, ' ', termbox.ColorDefault|termbox.AttrReverse, termbox.ColorDefault|termbox.AttrReverse)
printTB(promptLen+1, 0, termbox.ColorDefault, termbox.ColorDefault, string(v.query))
termbox.SetCell(promptLen+1+runewidth.StringWidth(string(v.query)), 0, ' ', termbox.ColorDefault|termbox.AttrReverse, termbox.ColorDefault|termbox.AttrReverse)
} else {
// the caret is in the middle of the string
prev := 0
for i, r := range u.query {
for i, r := range v.query {
fg := termbox.ColorDefault
bg := termbox.ColorDefault
if i == u.caretPos {
if i == v.caretPos {
fg |= termbox.AttrReverse
bg |= termbox.AttrReverse
}
@ -148,19 +172,19 @@ CALCULATE_PAGE:
}
}
pmsg := fmt.Sprintf("%s [%d/%d]", u.Ctx.Matcher().String(), currentPage, maxPage)
pmsg := fmt.Sprintf("%s [%d/%d]", v.Ctx.Matcher().String(), currentPage, maxPage)
printTB(width-runewidth.StringWidth(pmsg), 0, termbox.ColorDefault, termbox.ColorDefault, pmsg)
for n := 1; n <= perPage; n++ {
fgAttr := u.config.Style.Basic.fg
bgAttr := u.config.Style.Basic.bg
if n+offset == u.currentLine {
fgAttr = u.config.Style.Selected.fg
bgAttr = u.config.Style.Selected.bg
} else if u.selection.Has(n+offset) {
fgAttr = u.config.Style.SavedSelection.fg
bgAttr = u.config.Style.SavedSelection.bg
fgAttr := v.config.Style.Basic.fg
bgAttr := v.config.Style.Basic.bg
if n+offset == v.currentLine {
fgAttr = v.config.Style.Selected.fg
bgAttr = v.config.Style.Selected.bg
} else if v.selection.Has(n+offset) {
fgAttr = v.config.Style.SavedSelection.fg
bgAttr = v.config.Style.SavedSelection.bg
}
targetIdx := offset + n - 1
@ -184,21 +208,23 @@ CALCULATE_PAGE:
index += len(c)
}
c := line[m[0]:m[1]]
printTB(prev, n, u.config.Style.Query.fg, bgAttr|u.config.Style.Query.bg, c)
printTB(prev, n, v.config.Style.Query.fg, bgAttr|v.config.Style.Query.bg, c)
prev += runewidth.StringWidth(c)
index += len(c)
}
m := matches[len(matches)-1]
if m[0] > prev {
printTB(prev, n, u.config.Style.Query.fg, bgAttr|u.config.Style.Query.bg, line[m[0]:m[1]])
printTB(prev, n, v.config.Style.Query.fg, bgAttr|v.config.Style.Query.bg, line[m[0]:m[1]])
} else if len(line) > m[1] {
printTB(prev, n, fgAttr, bgAttr, line[m[1]:len(line)])
}
}
}
v.printStatus()
termbox.Flush()
// FIXME
u.current = targets
v.current = targets
}