From 57d7e4b0a5bd2dc43c3afbbcdcc26671e3d02cfa Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 8 Feb 2025 20:39:38 +0900 Subject: [PATCH] Fix 'deny' action for excluding current/selecte items from the result --- src/core.go | 36 ++++++++++++++++++++++++++++++++++-- src/options.go | 2 ++ src/pattern.go | 31 ++++++++++++++++++++++++++++++- src/pattern_test.go | 2 +- src/terminal.go | 33 ++++++++++++++++++++++++++------- 5 files changed, 93 insertions(+), 11 deletions(-) diff --git a/src/core.go b/src/core.go index cad139dd..6e34ed8d 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 99d58634..bde66215 100644 --- a/src/options.go +++ b/src/options.go @@ -1600,6 +1600,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA } case "bell": appendAction(actBell) + case "deny": + appendAction(actDeny) default: t := isExecuteAction(specLower) if t == actIgnore { diff --git a/src/pattern.go b/src/pattern.go index 29149fe7..137dcddb 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 b1b5251d..d80bc92c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -583,6 +583,7 @@ const ( actShowHeader actHideHeader actBell + actDeny ) func (a actionType) Name() string { @@ -620,12 +621,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 { @@ -4741,6 +4744,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() { @@ -4897,6 +4901,21 @@ func (t *Terminal) Loop() error { } case actBell: t.tui.Bell() + case actDeny: + 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: @@ -6006,7 +6025,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