mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-16 15:23:48 -05:00
Add 'exclude' action for excluding current/selected items from the result (#4231)
Close #4185
This commit is contained in:
@@ -1601,6 +1601,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
|
||||
\fBenable\-search\fR (enable search functionality)
|
||||
\fBend\-of\-line\fR \fIctrl\-e end\fR
|
||||
\fBexclude\fR (exclude the current item or the selected items from the result)
|
||||
\fBexecute(...)\fR (see below for the details)
|
||||
\fBexecute\-silent(...)\fR (see below for the details)
|
||||
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
|
||||
|
||||
36
src/core.go
36
src/core.go
@@ -198,10 +198,26 @@ func Run(opts *Options) (int, error) {
|
||||
inputRevision := revision{}
|
||||
snapshotRevision := revision{}
|
||||
patternCache := make(map[string]*Pattern)
|
||||
denyMutex := sync.Mutex{}
|
||||
denylist := make(map[int32]struct{})
|
||||
clearDenylist := func() {
|
||||
denyMutex.Lock()
|
||||
if len(denylist) > 0 {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
}
|
||||
denylist = make(map[int32]struct{})
|
||||
denyMutex.Unlock()
|
||||
}
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
denyMutex.Lock()
|
||||
denylistCopy := make(map[int32]struct{})
|
||||
for k, v := range denylist {
|
||||
denylistCopy[k] = v
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes)
|
||||
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
|
||||
}
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
|
||||
@@ -301,6 +317,9 @@ func Run(opts *Options) (int, error) {
|
||||
var snapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command commandSpec, environ []string) {
|
||||
if !useSnapshot {
|
||||
clearDenylist()
|
||||
}
|
||||
reading = true
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
@@ -347,7 +366,8 @@ func Run(opts *Options) (int, error) {
|
||||
} else {
|
||||
reading = reading && evt == EvtReadNew
|
||||
}
|
||||
if useSnapshot && evt == EvtReadFin {
|
||||
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||
clearDenylist()
|
||||
useSnapshot = false
|
||||
}
|
||||
if !useSnapshot {
|
||||
@@ -378,9 +398,21 @@ func Run(opts *Options) (int, error) {
|
||||
command = val.command
|
||||
environ = val.environ
|
||||
changed = val.changed
|
||||
bump := false
|
||||
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
|
||||
denyMutex.Lock()
|
||||
for _, itemIndex := range val.denylist {
|
||||
denylist[itemIndex] = struct{}{}
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
bump = true
|
||||
}
|
||||
if val.nth != nil {
|
||||
// Change nth and clear caches
|
||||
nth = *val.nth
|
||||
bump = true
|
||||
}
|
||||
if bump {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
cache.Clear()
|
||||
inputRevision.bumpMinor()
|
||||
|
||||
@@ -1603,6 +1603,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
}
|
||||
case "bell":
|
||||
appendAction(actBell)
|
||||
case "exclude":
|
||||
appendAction(actExclude)
|
||||
default:
|
||||
t := isExecuteAction(specLower)
|
||||
if t == actIgnore {
|
||||
|
||||
@@ -63,6 +63,7 @@ type Pattern struct {
|
||||
revision revision
|
||||
procFun map[termType]algo.Algo
|
||||
cache *ChunkCache
|
||||
denylist map[int32]struct{}
|
||||
}
|
||||
|
||||
var _splitRegex *regexp.Regexp
|
||||
@@ -73,7 +74,7 @@ func init() {
|
||||
|
||||
// BuildPattern builds Pattern object from the given arguments
|
||||
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune) *Pattern {
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
|
||||
|
||||
var asString string
|
||||
if extended {
|
||||
@@ -144,6 +145,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
revision: revision,
|
||||
delimiter: delimiter,
|
||||
cache: cache,
|
||||
denylist: denylist,
|
||||
procFun: make(map[termType]algo.Algo)}
|
||||
|
||||
ptr.cacheKey = ptr.buildCacheKey()
|
||||
@@ -243,6 +245,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
|
||||
// IsEmpty returns true if the pattern is effectively empty
|
||||
func (p *Pattern) IsEmpty() bool {
|
||||
if len(p.denylist) > 0 {
|
||||
return false
|
||||
}
|
||||
if !p.extended {
|
||||
return len(p.text) == 0
|
||||
}
|
||||
@@ -296,6 +301,8 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
|
||||
if len(p.denylist) == 0 {
|
||||
// Huge code duplication for minimizing unnecessary map lookups
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
@@ -312,6 +319,28 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
|
||||
return matches
|
||||
}
|
||||
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if _, prs := p.denylist[result.item.Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
// MatchItem returns true if the Item is a match
|
||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
||||
if p.extended {
|
||||
|
||||
@@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||
withPos, cacheable, nth, delimiter, revision{}, runes)
|
||||
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
||||
}
|
||||
|
||||
func TestExact(t *testing.T) {
|
||||
|
||||
@@ -584,6 +584,7 @@ const (
|
||||
actShowHeader
|
||||
actHideHeader
|
||||
actBell
|
||||
actExclude
|
||||
)
|
||||
|
||||
func (a actionType) Name() string {
|
||||
@@ -627,6 +628,8 @@ type searchRequest struct {
|
||||
command *commandSpec
|
||||
environ []string
|
||||
changed bool
|
||||
denylist []int32
|
||||
revision revision
|
||||
}
|
||||
|
||||
type previewRequest struct {
|
||||
@@ -4751,6 +4754,7 @@ func (t *Terminal) Loop() error {
|
||||
changed := false
|
||||
beof := false
|
||||
queryChanged := false
|
||||
denylist := []int32{}
|
||||
|
||||
// Special handling of --sync. Activate the interface on the second tick.
|
||||
if loopIndex == 1 && t.deferActivation() {
|
||||
@@ -4907,6 +4911,21 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
case actBell:
|
||||
t.tui.Bell()
|
||||
case actExclude:
|
||||
if len(t.selected) > 0 {
|
||||
for _, item := range t.sortSelected() {
|
||||
denylist = append(denylist, item.item.Index())
|
||||
}
|
||||
// Clear selected items
|
||||
t.selected = make(map[int32]selectedItem)
|
||||
t.version++
|
||||
} else {
|
||||
item := t.currentItem()
|
||||
if item != nil {
|
||||
denylist = append(denylist, item.Index())
|
||||
}
|
||||
}
|
||||
changed = true
|
||||
case actExecute, actExecuteSilent:
|
||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "")
|
||||
case actExecuteMulti:
|
||||
@@ -6016,7 +6035,7 @@ func (t *Terminal) Loop() error {
|
||||
reload := changed || newCommand != nil
|
||||
var reloadRequest *searchRequest
|
||||
if reload {
|
||||
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed}
|
||||
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.merger.Revision()}
|
||||
}
|
||||
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
||||
|
||||
|
||||
@@ -1666,6 +1666,30 @@ class TestCore < TestInteractive
|
||||
end
|
||||
end
|
||||
|
||||
def test_exclude
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --multi --bind 'a:exclude,b:reload(seq 1000),c:reload-sync(seq 1000)'), :Enter
|
||||
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||
tmux.send_keys :a
|
||||
tmux.until { |lines| assert_includes lines, '> 2' }
|
||||
tmux.until { |lines| assert_equal 999, lines.match_count }
|
||||
tmux.send_keys :Up, :BTab, :BTab, :BTab, :a
|
||||
tmux.until { |lines| assert_equal 996, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines, '> 9' }
|
||||
tmux.send_keys :b
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines, '> 5' }
|
||||
tmux.send_keys :Tab, :Tab, :Tab, :a
|
||||
tmux.until { |lines| assert_equal 997, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines, '> 2' }
|
||||
tmux.send_keys :c
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines, '> 2' }
|
||||
|
||||
# TODO: We should also check the behavior of 'exclude' during reloads
|
||||
end
|
||||
|
||||
def test_accept_nth
|
||||
tmux.send_keys %((echo "foo bar baz"; echo "bar baz foo") | #{FZF} --multi --accept-nth 2,2 --sync --bind start:select-all+accept > #{tempname}), :Enter
|
||||
wait do
|
||||
|
||||
Reference in New Issue
Block a user