m/fzf
1
0
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:
Junegunn Choi
2025-02-09 13:22:33 +09:00
committed by GitHub
parent 2b584586ed
commit 67dd7e1923
7 changed files with 118 additions and 11 deletions

View File

@@ -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 \fBdown\fR \fIctrl\-j ctrl\-n down\fR
\fBenable\-search\fR (enable search functionality) \fBenable\-search\fR (enable search functionality)
\fBend\-of\-line\fR \fIctrl\-e end\fR \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(...)\fR (see below for the details)
\fBexecute\-silent(...)\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) \fBfirst\fR (move to the first match; same as \fBpos(1)\fR)

View File

@@ -198,10 +198,26 @@ func Run(opts *Options) (int, error) {
inputRevision := revision{} inputRevision := revision{}
snapshotRevision := revision{} snapshotRevision := revision{}
patternCache := make(map[string]*Pattern) 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 { 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, return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, 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) matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
@@ -301,6 +317,9 @@ func Run(opts *Options) (int, error) {
var snapshot []*Chunk var snapshot []*Chunk
var count int var count int
restart := func(command commandSpec, environ []string) { restart := func(command commandSpec, environ []string) {
if !useSnapshot {
clearDenylist()
}
reading = true reading = true
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
@@ -347,7 +366,8 @@ func Run(opts *Options) (int, error) {
} else { } else {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
} }
if useSnapshot && evt == EvtReadFin { if useSnapshot && evt == EvtReadFin { // reload-sync
clearDenylist()
useSnapshot = false useSnapshot = false
} }
if !useSnapshot { if !useSnapshot {
@@ -378,9 +398,21 @@ func Run(opts *Options) (int, error) {
command = val.command command = val.command
environ = val.environ environ = val.environ
changed = val.changed 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 { if val.nth != nil {
// Change nth and clear caches // Change nth and clear caches
nth = *val.nth nth = *val.nth
bump = true
}
if bump {
patternCache = make(map[string]*Pattern) patternCache = make(map[string]*Pattern)
cache.Clear() cache.Clear()
inputRevision.bumpMinor() inputRevision.bumpMinor()

View File

@@ -1603,6 +1603,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
} }
case "bell": case "bell":
appendAction(actBell) appendAction(actBell)
case "exclude":
appendAction(actExclude)
default: default:
t := isExecuteAction(specLower) t := isExecuteAction(specLower)
if t == actIgnore { if t == actIgnore {

View File

@@ -63,6 +63,7 @@ type Pattern struct {
revision revision revision revision
procFun map[termType]algo.Algo procFun map[termType]algo.Algo
cache *ChunkCache cache *ChunkCache
denylist map[int32]struct{}
} }
var _splitRegex *regexp.Regexp var _splitRegex *regexp.Regexp
@@ -73,7 +74,7 @@ func init() {
// BuildPattern builds Pattern object from the given arguments // 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, 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 var asString string
if extended { if extended {
@@ -144,6 +145,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
revision: revision, revision: revision,
delimiter: delimiter, delimiter: delimiter,
cache: cache, cache: cache,
denylist: denylist,
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey() 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 // IsEmpty returns true if the pattern is effectively empty
func (p *Pattern) IsEmpty() bool { func (p *Pattern) IsEmpty() bool {
if len(p.denylist) > 0 {
return false
}
if !p.extended { if !p.extended {
return len(p.text) == 0 return len(p.text) == 0
} }
@@ -296,14 +301,38 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result { func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
matches := []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 {
matches = append(matches, *match)
}
}
} else {
for _, result := range space {
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match)
}
}
}
return matches
}
if space == nil { if space == nil {
for idx := 0; idx < chunk.count; idx++ { 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 { if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
matches = append(matches, *match) matches = append(matches, *match)
} }
} }
} else { } else {
for _, result := range space { for _, result := range space {
if _, prs := p.denylist[result.item.Index()]; prs {
continue
}
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil { if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
matches = append(matches, *match) matches = append(matches, *match)
} }

View File

@@ -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 { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern), return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward, fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, revision{}, runes) withPos, cacheable, nth, delimiter, revision{}, runes, nil)
} }
func TestExact(t *testing.T) { func TestExact(t *testing.T) {

View File

@@ -584,6 +584,7 @@ const (
actShowHeader actShowHeader
actHideHeader actHideHeader
actBell actBell
actExclude
) )
func (a actionType) Name() string { func (a actionType) Name() string {
@@ -621,12 +622,14 @@ type placeholderFlags struct {
} }
type searchRequest struct { type searchRequest struct {
sort bool sort bool
sync bool sync bool
nth *[]Range nth *[]Range
command *commandSpec command *commandSpec
environ []string environ []string
changed bool changed bool
denylist []int32
revision revision
} }
type previewRequest struct { type previewRequest struct {
@@ -4751,6 +4754,7 @@ func (t *Terminal) Loop() error {
changed := false changed := false
beof := false beof := false
queryChanged := false queryChanged := false
denylist := []int32{}
// Special handling of --sync. Activate the interface on the second tick. // Special handling of --sync. Activate the interface on the second tick.
if loopIndex == 1 && t.deferActivation() { if loopIndex == 1 && t.deferActivation() {
@@ -4907,6 +4911,21 @@ func (t *Terminal) Loop() error {
} }
case actBell: case actBell:
t.tui.Bell() 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: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "") t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "")
case actExecuteMulti: case actExecuteMulti:
@@ -6016,7 +6035,7 @@ func (t *Terminal) Loop() error {
reload := changed || newCommand != nil reload := changed || newCommand != nil
var reloadRequest *searchRequest var reloadRequest *searchRequest
if reload { 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 t.mutex.Unlock() // Must be unlocked before touching reqBox

View File

@@ -1666,6 +1666,30 @@ class TestCore < TestInteractive
end end
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 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 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 wait do