Skip to content

Commit 32e506d

Browse files
committed
Merge pull request philc#715 from smblott-github/history--remove-entries
Remove entries from History/Domain completers when they're removed on chrome.
2 parents a3eb1cb + f034738 commit 32e506d

File tree

2 files changed

+111
-11
lines changed

2 files changed

+111
-11
lines changed

background_scripts/completion.coffee

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,11 @@ class HistoryCompleter
169169
# The domain completer is designed to match a single-word query which looks like it is a domain. This supports
170170
# the user experience where they quickly type a partial domain, hit tab -> enter, and expect to arrive there.
171171
class DomainCompleter
172-
domains: null # A map of domain -> history
172+
# A map of domain -> { entry: <historyEntry>, referenceCount: <count> }
173+
# - `entry` is the most recently accessed page in the History within this domain.
174+
# - `referenceCount` is a count of the number of History entries within this domain.
175+
# If `referenceCount` goes to zero, the domain entry can and should be deleted.
176+
domains: null
173177

174178
filter: (queryTerms, onComplete) ->
175179
return onComplete([]) if queryTerms.length > 1
@@ -190,7 +194,7 @@ class DomainCompleter
190194
sortDomainsByRelevancy: (queryTerms, domainCandidates) ->
191195
results = []
192196
for domain in domainCandidates
193-
recencyScore = RankingUtils.recencyScore(@domains[domain].lastVisitTime || 0)
197+
recencyScore = RankingUtils.recencyScore(@domains[domain].entry.lastVisitTime || 0)
194198
wordRelevancy = RankingUtils.wordRelevancy(queryTerms, domain, null)
195199
score = (wordRelevancy + Math.max(recencyScore, wordRelevancy)) / 2
196200
results.push([domain, score])
@@ -200,18 +204,27 @@ class DomainCompleter
200204
populateDomains: (onComplete) ->
201205
HistoryCache.use (history) =>
202206
@domains = {}
203-
history.forEach (entry) =>
204-
# We want each key in our domains hash to point to the most recent History entry for that domain.
205-
domain = @parseDomain(entry.url)
206-
if domain
207-
previousEntry = @domains[domain]
208-
@domains[domain] = entry if !previousEntry || (previousEntry.lastVisitTime < entry.lastVisitTime)
207+
history.forEach (entry) => @onPageVisited entry
209208
chrome.history.onVisited.addListener(@onPageVisited.bind(this))
209+
chrome.history.onVisitRemoved.addListener(@onVisitRemoved.bind(this))
210210
onComplete()
211211

212212
onPageVisited: (newPage) ->
213213
domain = @parseDomain(newPage.url)
214-
@domains[domain] = newPage if domain
214+
if domain
215+
slot = @domains[domain] ||= { entry: newPage, referenceCount: 0 }
216+
# We want each entry in our domains hash to point to the most recent History entry for that domain.
217+
slot.entry = newPage if slot.entry.lastVisitTime < newPage.lastVisitTime
218+
slot.referenceCount += 1
219+
220+
onVisitRemoved: (toRemove) ->
221+
if toRemove.allHistory
222+
@domains = {}
223+
else
224+
toRemove.urls.forEach (url) =>
225+
domain = @parseDomain(url)
226+
if domain and @domains[domain] and ( @domains[domain].referenceCount -= 1 ) == 0
227+
delete @domains[domain]
215228

216229
parseDomain: (url) -> url.split("/")[2] || ""
217230

@@ -365,6 +378,7 @@ HistoryCache =
365378
history.sort @compareHistoryByUrl
366379
@history = history
367380
chrome.history.onVisited.addListener(@onPageVisited.bind(this))
381+
chrome.history.onVisitRemoved.addListener(@onVisitRemoved.bind(this))
368382
callback(@history) for callback in @callbacks
369383
@callbacks = null
370384

@@ -383,6 +397,16 @@ HistoryCache =
383397
else
384398
@history.splice(i, 0, newPage)
385399

400+
# When a page is removed from the chrome history, remove it from the vimium history too.
401+
onVisitRemoved: (toRemove) ->
402+
if toRemove.allHistory
403+
@history = []
404+
else
405+
toRemove.urls.forEach (url) =>
406+
i = HistoryCache.binarySearch({url:url}, @history, @compareHistoryByUrl)
407+
if i < @history.length and @history[i].url == url
408+
@history.splice(i, 1)
409+
386410
# Returns the matching index or the closest matching index if the element is not found. That means you
387411
# must check the element at the returned index to know whether the element was actually found.
388412
# This method is used for quickly searching through our history cache.

tests/unit_tests/completion_test.coffee

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ context "HistoryCache",
4141
should "return length - 1 if it should be at the end of the list", ->
4242
assert.equal 0, HistoryCache.binarySearch(3, [3, 5, 8], @compare)
4343

44+
should "return one passed end of array (so: array.length) if greater than last element in array", ->
45+
assert.equal 3, HistoryCache.binarySearch(10, [3, 5, 8], @compare)
46+
4447
should "found return the position if it's between two elements", ->
4548
assert.equal 1, HistoryCache.binarySearch(4, [3, 5, 8], @compare)
4649
assert.equal 2, HistoryCache.binarySearch(7, [3, 5, 8], @compare)
@@ -51,9 +54,11 @@ context "HistoryCache",
5154
@history2 = { url: "a.com", lastVisitTime: 10 }
5255
history = [@history1, @history2]
5356
@onVisitedListener = null
57+
@onVisitRemovedListener = null
5458
global.chrome.history =
5559
search: (options, callback) -> callback(history)
5660
onVisited: { addListener: (@onVisitedListener) => }
61+
onVisitRemoved: { addListener: (@onVisitRemovedListener) => }
5762
HistoryCache.reset()
5863

5964
should "store visits sorted by url ascending", ->
@@ -75,6 +80,30 @@ context "HistoryCache",
7580
HistoryCache.use (@results) =>
7681
assert.arrayEqual [newSite, @history1], @results
7782

83+
should "(not) remove page from the history, when page is not in history (it should be a no-op)", ->
84+
HistoryCache.use (@results) =>
85+
assert.arrayEqual [@history2, @history1], @results
86+
toRemove = { urls: [ "x.com" ], allHistory: false }
87+
@onVisitRemovedListener(toRemove)
88+
HistoryCache.use (@results) =>
89+
assert.arrayEqual [@history2, @history1], @results
90+
91+
should "remove pages from the history", ->
92+
HistoryCache.use (@results) =>
93+
assert.arrayEqual [@history2, @history1], @results
94+
toRemove = { urls: [ "a.com" ], allHistory: false }
95+
@onVisitRemovedListener(toRemove)
96+
HistoryCache.use (@results) =>
97+
assert.arrayEqual [@history1], @results
98+
99+
should "remove all pages from the history", ->
100+
HistoryCache.use (@results) =>
101+
assert.arrayEqual [@history2, @history1], @results
102+
toRemove = { allHistory: true }
103+
@onVisitRemovedListener(toRemove)
104+
HistoryCache.use (@results) =>
105+
assert.arrayEqual [], @results
106+
78107
context "history completer",
79108
setup ->
80109
@history1 = { title: "history1", url: "history1.com", lastVisitTime: hours(1) }
@@ -83,6 +112,7 @@ context "history completer",
83112
global.chrome.history =
84113
search: (options, callback) => callback([@history1, @history2])
85114
onVisited: { addListener: -> }
115+
onVisitRemoved: { addListener: -> }
86116

87117
@completer = new HistoryCompleter()
88118

@@ -102,7 +132,9 @@ context "domain completer",
102132
@history2 = { title: "history2", url: "http://history2.com", lastVisitTime: hours(1) }
103133

104134
stub(HistoryCache, "use", (onComplete) => onComplete([@history1, @history2]))
105-
global.chrome.history = { onVisited: { addListener: -> } }
135+
global.chrome.history =
136+
onVisited: { addListener: -> }
137+
onVisitRemoved: { addListener: -> }
106138
stub(Date, "now", returns(hours(24)))
107139

108140
@completer = new DomainCompleter()
@@ -112,14 +144,58 @@ context "domain completer",
112144
assert.arrayEqual ["history1.com"], results.map (result) -> result.url
113145

114146
should "pick domains which are more recent", ->
115-
# This domains are the same except for their last visited time.
147+
# These domains are the same except for their last visited time.
116148
assert.equal "history1.com", filterCompleter(@completer, ["story"])[0].url
117149
@history2.lastVisitTime = hours(3)
118150
assert.equal "history2.com", filterCompleter(@completer, ["story"])[0].url
119151

120152
should "returns no results when there's more than one query term, because clearly it's not a domain", ->
121153
assert.arrayEqual [], filterCompleter(@completer, ["his", "tory"])
122154

155+
context "domain completer (removing entries)",
156+
setup ->
157+
@history1 = { title: "history1", url: "http://history1.com", lastVisitTime: hours(2) }
158+
@history2 = { title: "history2", url: "http://history2.com", lastVisitTime: hours(1) }
159+
@history3 = { title: "history2something", url: "http://history2.com/something", lastVisitTime: hours(0) }
160+
161+
stub(HistoryCache, "use", (onComplete) => onComplete([@history1, @history2, @history3]))
162+
@onVisitedListener = null
163+
@onVisitRemovedListener = null
164+
global.chrome.history =
165+
onVisited: { addListener: (@onVisitedListener) => }
166+
onVisitRemoved: { addListener: (@onVisitRemovedListener) => }
167+
stub(Date, "now", returns(hours(24)))
168+
169+
@completer = new DomainCompleter()
170+
# Force installation of listeners.
171+
filterCompleter(@completer, ["story"])
172+
173+
should "remove 1 entry for domain with reference count of 1", ->
174+
@onVisitRemovedListener { allHistory: false, urls: [@history1.url] }
175+
assert.equal "history2.com", filterCompleter(@completer, ["story"])[0].url
176+
assert.equal 0, filterCompleter(@completer, ["story1"]).length
177+
178+
should "remove 2 entries for domain with reference count of 2", ->
179+
@onVisitRemovedListener { allHistory: false, urls: [@history2.url] }
180+
assert.equal "history2.com", filterCompleter(@completer, ["story2"])[0].url
181+
@onVisitRemovedListener { allHistory: false, urls: [@history3.url] }
182+
assert.equal 0, filterCompleter(@completer, ["story2"]).length
183+
assert.equal "history1.com", filterCompleter(@completer, ["story"])[0].url
184+
185+
should "remove 3 (all) matching domain entries", ->
186+
@onVisitRemovedListener { allHistory: false, urls: [@history2.url] }
187+
@onVisitRemovedListener { allHistory: false, urls: [@history1.url] }
188+
@onVisitRemovedListener { allHistory: false, urls: [@history3.url] }
189+
assert.equal 0, filterCompleter(@completer, ["story"]).length
190+
191+
should "remove 3 (all) matching domain entries, and do it all at once", ->
192+
@onVisitRemovedListener { allHistory: false, urls: [ @history2.url, @history1.url, @history3.url ] }
193+
assert.equal 0, filterCompleter(@completer, ["story"]).length
194+
195+
should "remove *all* domain entries", ->
196+
@onVisitRemovedListener { allHistory: true }
197+
assert.equal 0, filterCompleter(@completer, ["story"]).length
198+
123199
context "tab completer",
124200
setup ->
125201
@tabs = [

0 commit comments

Comments
 (0)