Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable closing of fuzzyfinder from the caller by passing context #165

Merged
merged 4 commits into from
Nov 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@