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
|
\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)
|
||||||
|
|||||||
36
src/core.go
36
src/core.go
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user