summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2025-02-09 13:22:33 +0900
committerGitHub <noreply@github.com>2025-02-09 13:22:33 +0900
commit67dd7e1923f8084de1064bf54659100626c1e0ef (patch)
treea89ef048133cadf06854e891932c622580590f21 /src
parent2b584586ed1caf15429625da981575ee35d407b8 (diff)
downloadfzf-67dd7e1923f8084de1064bf54659100626c1e0ef.tar.gz
Add 'exclude' action for excluding current/selected items from the result (#4231)
Close #4185
Diffstat (limited to 'src')
-rw-r--r--src/core.go36
-rw-r--r--src/options.go2
-rw-r--r--src/pattern.go31
-rw-r--r--src/pattern_test.go2
-rw-r--r--src/terminal.go33
5 files changed, 93 insertions, 11 deletions
diff --git a/src/core.go b/src/core.go
index 8f4a6d84..08d9e868 100644
--- a/src/core.go
+++ b/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()
diff --git a/src/options.go b/src/options.go
index 2b310612..4a6c3b2b 100644
--- a/src/options.go
+++ b/src/options.go
@@ -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 {
diff --git a/src/pattern.go b/src/pattern.go
index 8919ad87..93640cb6 100644
--- a/src/pattern.go
+++ b/src/pattern.go
@@ -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,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 {
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 {
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)
}
diff --git a/src/pattern_test.go b/src/pattern_test.go
index 24b17744..8e566263 100644
--- a/src/pattern_test.go
+++ b/src/pattern_test.go
@@ -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) {
diff --git a/src/terminal.go b/src/terminal.go
index 9a06ad61..60ddaa71 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -584,6 +584,7 @@ const (
actShowHeader
actHideHeader
actBell
+ actExclude
)
func (a actionType) Name() string {
@@ -621,12 +622,14 @@ type placeholderFlags struct {
}
type searchRequest struct {
- sort bool
- sync bool
- nth *[]Range
- command *commandSpec
- environ []string
- changed bool
+ sort bool
+ sync bool
+ nth *[]Range
+ 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