Skip to content

Commit

Permalink
Enable closing of fuzzyfinder from the caller by passing context (#165)
Browse files Browse the repository at this point in the history
* Enable closing of fuzzyfinder from the caller by passing context

* Remove unused quit channel

* Use ctx.Err

* Update fuzzyfinder.go

Co-authored-by: ktr <ktr@syfm.me>

Co-authored-by: ktr <ktr@syfm.me>
  • Loading branch information
ahsan and ktr0731 authored Nov 25, 2022
1 parent 504da0e commit 663fa92
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 37 deletions.
95 changes: 61 additions & 34 deletions fuzzyfinder.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type finder struct {
drawTimer *time.Timer
eventCh chan struct{}
opt *opt

termEventsChan <-chan tcell.Event
}

func newFinder() *finder {
Expand All @@ -94,6 +96,10 @@ func (f *finder) initFinder(items []string, matched []matching.Matched, opt opt)
if err := f.term.Init(); err != nil {
return errors.Wrap(err, "failed to initialize screen")
}

eventsChan := make(chan tcell.Event)
go f.term.ChannelEvents(eventsChan, nil)
f.termEventsChan = eventsChan
}

f.opt = &opt
Expand Down Expand Up @@ -442,9 +448,10 @@ func (f *finder) draw(d time.Duration) {
}

// readKey reads a key input.
// It returns ErrAbort if esc, CTRL-C or CTRL-D keys are inputted.
// Also, it returns errEntered if enter key is inputted.
func (f *finder) readKey() error {
// It returns ErrAbort if esc, CTRL-C or CTRL-D keys are inputted,
// errEntered in case of enter key, and a context error when the passed
// context is cancelled.
func (f *finder) readKey(ctx context.Context) error {
f.stateMu.RLock()
prevInputLen := len(f.state.input)
f.stateMu.RUnlock()
Expand All @@ -457,7 +464,15 @@ func (f *finder) readKey() error {
}
}()

e := f.term.PollEvent()
var e tcell.Event

select {
case ee := <-f.termEventsChan:
e = ee
case <-ctx.Done():
return ctx.Err()
}

f.stateMu.Lock()
defer f.stateMu.Unlock()

Expand Down Expand Up @@ -670,7 +685,14 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt
matched []matching.Matched
)

ctx, cancel := context.WithCancel(context.Background())
var parentContext context.Context
if opt.context != nil {
parentContext = opt.context
} else {
parentContext = context.Background()
}

ctx, cancel := context.WithCancel(parentContext)
defer cancel()

inited := make(chan struct{})
Expand Down Expand Up @@ -727,40 +749,45 @@ func (f *finder) find(slice interface{}, itemFunc func(i int) string, opts []Opt
}()

for {
f.draw(10 * time.Millisecond)

err := f.readKey()
// hack for earning time to filter exec
if isInTesting() {
time.Sleep(50 * time.Millisecond)
}
switch {
case errors.Is(err, ErrAbort):
return nil, ErrAbort
case errors.Is(err, errEntered):
f.stateMu.RLock()
defer f.stateMu.RUnlock()
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
f.draw(10 * time.Millisecond)

if len(f.state.matched) == 0 {
return nil, ErrAbort
err := f.readKey(ctx)
// hack for earning time to filter exec
if isInTesting() {
time.Sleep(50 * time.Millisecond)
}
if f.opt.multi {
if len(f.state.selection) == 0 {
return []int{f.state.matched[f.state.y].Idx}, nil
switch {
case errors.Is(err, ErrAbort):
return nil, ErrAbort
case errors.Is(err, errEntered):
f.stateMu.RLock()
defer f.stateMu.RUnlock()

if len(f.state.matched) == 0 {
return nil, ErrAbort
}
poss, idxs := make([]int, 0, len(f.state.selection)), make([]int, 0, len(f.state.selection))
for idx, pos := range f.state.selection {
idxs = append(idxs, idx)
poss = append(poss, pos)
if f.opt.multi {
if len(f.state.selection) == 0 {
return []int{f.state.matched[f.state.y].Idx}, nil
}
poss, idxs := make([]int, 0, len(f.state.selection)), make([]int, 0, len(f.state.selection))
for idx, pos := range f.state.selection {
idxs = append(idxs, idx)
poss = append(poss, pos)
}
sort.Slice(idxs, func(i, j int) bool {
return poss[i] < poss[j]
})
return idxs, nil
}
sort.Slice(idxs, func(i, j int) bool {
return poss[i] < poss[j]
})
return idxs, nil
return []int{f.state.matched[f.state.y].Idx}, nil
case err != nil:
return nil, errors.Wrap(err, "failed to read a key")
}
return []int{f.state.matched[f.state.y].Idx}, nil
case err != nil:
return nil, errors.Wrap(err, "failed to read a key")
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions fuzzyfinder_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fuzzyfinder_test

import (
"context"
"flag"
"io/ioutil"
"log"
Expand Down Expand Up @@ -445,6 +446,33 @@ func TestFind_WithPreviewWindow(t *testing.T) {
}
}

func TestFind_withContext(t *testing.T) {
t.Parallel()

f, term := fuzzyfinder.NewWithMockedTerminal()
events := append(runes("adrena"), keys(input{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone})...)
term.SetEventsV2(events...)

cancelledCtx, cancelFunc := context.WithCancel(context.Background())
cancelFunc()

assertWithGolden(t, func(t *testing.T) string {
_, err := f.Find(
tracks,
func(i int) string {
return tracks[i].Name
},
fuzzyfinder.WithContext(cancelledCtx),
)
if !errors.Is(err, context.Canceled) {
t.Fatalf("Find must return ErrAbort, but got '%s'", err)
}

res := term.GetResult()
return res
})
}

func TestFind_error(t *testing.T) {
t.Parallel()

Expand Down
8 changes: 8 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package fuzzyfinder

import "github.com/gdamore/tcell/v2"

func New() *finder {
return &finder{}
}

func NewWithMockedTerminal() (*finder, *TerminalMock) {
eventsChan := make(chan tcell.Event, 10)

f := New()
f.termEventsChan = eventsChan

m := f.UseMockedTerminalV2()
go m.ChannelEvents(eventsChan, nil)

w, h := 60, 10 // A normally value.
m.SetSize(w, h)
return f, m
Expand Down
17 changes: 14 additions & 3 deletions option.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package fuzzyfinder

import "sync"
import (
"context"
"sync"
)

type opt struct {
mode mode
Expand All @@ -11,6 +14,7 @@ type opt struct {
promptString string
header string
beginAtTop bool
context context.Context
}

type mode int
Expand All @@ -27,7 +31,7 @@ const (
)

var defaultOption = opt{
promptString: "> ",
promptString: "> ",
hotReloadLock: &sync.Mutex{}, // this won't resolve the race condition but avoid nil panic
}

Expand Down Expand Up @@ -69,7 +73,7 @@ func WithHotReload() Option {
// The caller must pass a pointer of the slice instead of the slice itself.
// The caller must pass a RLock which is used to synchronize access to the slice.
// The caller MUST NOT lock in the itemFunc passed to Find / FindMulti because it will be locked by the fuzzyfinder.
// If used together with WithPreviewWindow, the caller MUST use the RLock only in the previewFunc passed to WithPreviewWindow.
// If used together with WithPreviewWindow, the caller MUST use the RLock only in the previewFunc passed to WithPreviewWindow.
func WithHotReloadLock(lock sync.Locker) Option {
return func(o *opt) {
o.hotReload = true
Expand Down Expand Up @@ -116,3 +120,10 @@ func WithHeader(s string) Option {
o.header = s
}
}

// WithContext enables closing the fuzzy finder from parent.
func WithContext(ctx context.Context) Option {
return func(o *opt) {
o.context = ctx
}
}
11 changes: 11 additions & 0 deletions testdata/fixtures/testfind_withcontext.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@












0 comments on commit 663fa92

Please sign in to comment.