Skip to content

Commit 02ca408

Browse files
KristofferCKristofferC
andcommitted
only show unique entries in history search when filtering (#60066)
Co-authored-by: KristofferC <[email protected]> (cherry picked from commit 6058082)
1 parent 16198e5 commit 02ca408

File tree

3 files changed

+92
-21
lines changed

3 files changed

+92
-21
lines changed

stdlib/REPL/src/History/resumablefiltering.jl

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,21 +253,25 @@ end
253253

254254

255255
"""
256-
filterchunkrev!(out, candidates, spec; idx, maxtime, maxresults) -> Int
256+
filterchunkrev!(out, candidates, spec, seen, idx; maxtime, maxresults) -> Int
257257
258258
Incrementally filter `candidates[1:idx]` in reverse order.
259259
260260
Pushes matches onto `out` until either `maxtime` is exceeded or `maxresults`
261-
collected, then returns the new resume index.
261+
collected, then returns the new resume index. Only unique entries (by mode and content)
262+
are added to avoid showing duplicate history items.
262263
"""
263264
function filterchunkrev!(out::Vector{HistEntry}, candidates::DenseVector{HistEntry},
264-
spec::FilterSpec, idx::Int = length(candidates);
265+
spec::FilterSpec, seen::Set{Tuple{Symbol,String}}, idx::Int = length(candidates);
265266
maxtime::Float64 = Inf, maxresults::Int = length(candidates))
266267
batchsize = clamp(length(candidates) ÷ 512, 10, 1000)
267268
for batch in Iterators.partition(idx:-1:1, batchsize)
268269
time() > maxtime && break
269270
for outer idx in batch
270271
entry = candidates[idx]
272+
if (entry.mode, entry.content) seen
273+
continue
274+
end
271275
if !isempty(spec.modes)
272276
entry.mode spec.modes || continue
273277
end
@@ -293,6 +297,7 @@ function filterchunkrev!(out::Vector{HistEntry}, candidates::DenseVector{HistEnt
293297
end
294298
end
295299
matchfail && continue
300+
push!(seen, (entry.mode, entry.content))
296301
pushfirst!(out, entry)
297302
length(out) == maxresults && break
298303
end

stdlib/REPL/src/History/search.jl

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function run_display!((; term, pstate), events::Channel{Symbol}, hist::Vector{Hi
6262
cands_temp = HistEntry[]
6363
# Filter state
6464
filter_idx = 0
65+
filter_seen = Set{Tuple{Symbol,String}}()
6566
# Event loop
6667
while true
6768
event = @lock events if !isempty(events) take!(events) end
@@ -153,10 +154,21 @@ function run_display!((; term, pstate), events::Channel{Symbol}, hist::Vector{Hi
153154
end
154155
end
155156
# Start filtering candidates
156-
filter_idx = filterchunkrev!(
157-
state, cands_current;
158-
maxtime = time() + 0.01,
159-
maxresults = outsize[1])
157+
# Only deduplicate when user has entered a search query. When browsing
158+
# with no filter (empty query), show all history including duplicates.
159+
if isempty(filter_spec.exacts) && isempty(filter_spec.negatives) &&
160+
isempty(filter_spec.regexps) && isempty(filter_spec.modes)
161+
# No filtering needed, just copy all candidates
162+
append!(state.candidates, cands_current)
163+
filter_idx = 0
164+
else
165+
# Filtering needed, deduplicate results
166+
empty!(filter_seen)
167+
filter_idx = filterchunkrev!(
168+
state, cands_current, filter_seen;
169+
maxtime = time() + 0.01,
170+
maxresults = outsize[1])
171+
end
160172
if filter_idx == 0
161173
cands_cachestate = addcache!(
162174
cands_cache, cands_cachestate, cands_cond => state.candidates)
@@ -186,7 +198,7 @@ function run_display!((; term, pstate), events::Channel{Symbol}, hist::Vector{Hi
186198
state.area, state.query, state.filter, cands_temp,
187199
state.scroll, state.selection, state.hover)
188200
filter_idx = filterchunkrev!(
189-
state, cands_current, filter_idx;
201+
state, cands_current, filter_seen, filter_idx;
190202
maxtime = time() + 0.01)
191203
if filter_idx == 0
192204
cands_cachestate = addcache!(
@@ -203,10 +215,11 @@ function run_display!((; term, pstate), events::Channel{Symbol}, hist::Vector{Hi
203215
end
204216
end
205217

206-
function filterchunkrev!(state::SelectorState, candidates::DenseVector{HistEntry}, idx::Int = length(candidates);
218+
function filterchunkrev!(state::SelectorState, candidates::DenseVector{HistEntry},
219+
seen::Set{Tuple{Symbol,String}}, idx::Int = length(candidates);
207220
maxtime::Float64 = Inf, maxresults::Int = length(candidates))
208221
oldlen = length(state.candidates)
209-
idx = filterchunkrev!(state.candidates, candidates, state.filter, idx;
222+
idx = filterchunkrev!(state.candidates, candidates, state.filter, seen, idx;
210223
maxtime = maxtime, maxresults = maxresults)
211224
newlen = length(state.candidates)
212225
newcands = view(state.candidates, (oldlen + 1):newlen)

stdlib/REPL/test/history.jl

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -279,71 +279,124 @@ end
279279
empty!(results)
280280
cset = ConditionSet("hello")
281281
spec = FilterSpec(cset)
282-
@test filterchunkrev!(results, entries, spec) == 0
282+
seen = Set{Tuple{Symbol,String}}()
283+
@test filterchunkrev!(results, entries, spec, seen) == 0
283284
@test results == [entries[1], entries[7]]
284285
empty!(results)
285286
cset2 = ConditionSet("world")
286287
spec2 = FilterSpec(cset2)
287-
@test filterchunkrev!(results, entries, spec2) == 0
288+
empty!(seen)
289+
@test filterchunkrev!(results, entries, spec2, seen) == 0
288290
@test results == [entries[1], entries[7]]
289291
empty!(results)
290292
cset3 = ConditionSet("World")
291293
spec3 = FilterSpec(cset3)
292-
@test filterchunkrev!(results, entries, spec3) == 0
294+
empty!(seen)
295+
@test filterchunkrev!(results, entries, spec3, seen) == 0
293296
@test results == [entries[7]]
294297
end
295298
@testset "Exact" begin
296299
empty!(results)
297300
cset = ConditionSet("=test")
298301
spec = FilterSpec(cset)
299-
@test filterchunkrev!(results, entries, spec, maxresults = 2) == 5
302+
seen = Set{Tuple{Symbol,String}}()
303+
@test filterchunkrev!(results, entries, spec, seen; maxresults = 2) == 5
300304
@test results == [entries[6], entries[9]]
301305
empty!(results)
302306
cset2 = ConditionSet("=test case")
303307
spec2 = FilterSpec(cset2)
304-
@test filterchunkrev!(results, entries, spec2) == 0
308+
empty!(seen)
309+
@test filterchunkrev!(results, entries, spec2, seen) == 0
305310
@test results == [entries[3]]
306311
end
307312
@testset "Negative" begin
308313
empty!(results)
309314
cset = ConditionSet("!hello ; !test;! cos")
310315
spec = FilterSpec(cset)
311-
@test filterchunkrev!(results, entries, spec) == 0
316+
seen = Set{Tuple{Symbol,String}}()
317+
@test filterchunkrev!(results, entries, spec, seen) == 0
312318
@test results == [entries[2], entries[7], entries[8]]
313319
end
314320
@testset "Initialism" begin
315321
empty!(results)
316322
cset = ConditionSet("`tc")
317323
spec = FilterSpec(cset)
318-
@test filterchunkrev!(results, entries, spec) == 0
324+
seen = Set{Tuple{Symbol,String}}()
325+
@test filterchunkrev!(results, entries, spec, seen) == 0
319326
@test results == [entries[3]]
320327
empty!(results)
321328
cset2 = ConditionSet("`fb")
322329
spec2 = FilterSpec(cset2)
323-
@test filterchunkrev!(results, entries, spec2) == 0
330+
empty!(seen)
331+
@test filterchunkrev!(results, entries, spec2, seen) == 0
324332
@test results == [entries[8]]
325333
end
326334
@testset "Regexp" begin
327335
empty!(results)
328336
cset = ConditionSet("/^c.s\\b")
329337
spec = FilterSpec(cset)
330-
@test filterchunkrev!(results, entries, spec) == 0
338+
seen = Set{Tuple{Symbol,String}}()
339+
@test filterchunkrev!(results, entries, spec, seen) == 0
331340
@test results == [entries[4], entries[5]]
332341
end
333342
@testset "Mode" begin
334343
empty!(results)
335344
cset = ConditionSet(">shell")
336345
spec = FilterSpec(cset)
337-
@test filterchunkrev!(results, entries, spec) == 0
346+
seen = Set{Tuple{Symbol,String}}()
347+
@test filterchunkrev!(results, entries, spec, seen) == 0
338348
@test results == [entries[7]]
339349
end
340350
@testset "Fuzzy" begin
341351
empty!(results)
342352
cset = ConditionSet("~cs")
343353
spec = FilterSpec(cset)
344-
@test filterchunkrev!(results, entries, spec) == 0
354+
seen = Set{Tuple{Symbol,String}}()
355+
@test filterchunkrev!(results, entries, spec, seen) == 0
345356
@test results == entries[3:6]
346357
end
358+
@testset "Uniqueness" begin
359+
empty!(results)
360+
# Create entries with duplicate content in the same mode
361+
dup_entries = [
362+
HistEntry(:julia, now(UTC), "println(\"hello\")", 1),
363+
HistEntry(:julia, now(UTC), "cos(2π)", 2),
364+
HistEntry(:julia, now(UTC), "println(\"hello\")", 3), # duplicate
365+
HistEntry(:julia, now(UTC), "sin(π)", 4),
366+
HistEntry(:julia, now(UTC), "cos(2π)", 5), # duplicate
367+
HistEntry(:julia, now(UTC), "println(\"hello\")", 6), # duplicate
368+
HistEntry(:julia, now(UTC), "tan(π/4)", 7),
369+
]
370+
# When filtering with seen Set, duplicates are removed
371+
cset = ConditionSet("cos")
372+
spec = FilterSpec(cset)
373+
seen = Set{Tuple{Symbol,String}}()
374+
@test filterchunkrev!(results, dup_entries, spec, seen) == 0
375+
# Should only get unique entries matching the filter
376+
# Since we iterate in reverse (7->1), we keep the most recent occurrence of each unique content
377+
@test length(results) == 1
378+
@test results[1] == dup_entries[5] # cos(2π) - most recent
379+
# When browsing without filtering, duplicates are kept
380+
empty!(results)
381+
append!(results, dup_entries)
382+
@test length(results) == 7 # All entries, including duplicates
383+
@test results == dup_entries
384+
# Test that same content in different modes is NOT deduplicated
385+
empty!(results)
386+
mode_entries = [
387+
HistEntry(:julia, now(UTC), "ls", 1),
388+
HistEntry(:shell, now(UTC), "ls", 2),
389+
HistEntry(:julia, now(UTC), "ls", 3), # duplicate in :julia mode
390+
HistEntry(:shell, now(UTC), "pwd", 4),
391+
]
392+
empty!(seen)
393+
cset3 = ConditionSet("ls")
394+
spec3 = FilterSpec(cset3)
395+
@test filterchunkrev!(results, mode_entries, spec3, seen) == 0
396+
@test length(results) == 2 # "ls" from :julia and "ls" from :shell
397+
@test results[1] == mode_entries[2] # :shell ls
398+
@test results[2] == mode_entries[3] # :julia ls (most recent)
399+
end
347400
end
348401
@testset "Strictness comparison" begin
349402
c1 = ConditionSet("hello world")

0 commit comments

Comments
 (0)