aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md48
-rw-r--r--README.md12
-rw-r--r--background_scripts/completion.coffee45
-rw-r--r--background_scripts/main.coffee57
-rw-r--r--content_scripts/vimium.css7
-rw-r--r--content_scripts/vimium_frontend.coffee37
-rw-r--r--lib/dom_utils.coffee4
-rw-r--r--pages/popup.coffee16
-rw-r--r--tests/unit_tests/completion_test.coffee34
9 files changed, 156 insertions, 104 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9382a020..a417caf5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -74,6 +74,34 @@ reports:
When you're done with your changes, send us a pull request on Github. Feel free to include a change to the
CREDITS file with your patch.
+Vimium design goals
+-------------------
+
+When improving Vimium it's helpful to know what design goals we're optimizing for.
+
+The core goal is to make it easy to navigate the web using just the keyboard. When people first start using
+Vimium, it provides an incredibly powerful workflow improvement and it makes them feel awesome. And it turns
+out that Vimium is applicable to a huge, broad population of people, not just users of Vim, which is great.
+
+A secondary goal is to make Vimium approachable, or in other words, to minimize the barriers which will
+prevent a new user from feeling awesome. Many of Vimium's users haven't used Vim before (about 1 in 5 app
+store reviews say this), and most people have strong web browsing habits forged from years of browsing that
+they rely on. Given that, it's a great experience when Vimium feels like a natural addition to Chrome which
+augments but doesn't break their current browsing habits.
+
+In some ways, making software approachable is even harder than just enabling the core use case. But in this
+area, Vimium really shines. It's approachable today because:
+
+1. It's simple to understand (even if you're not very familiar with Vim). The Vimium video shows you all you
+ need to know to start using Vimium and feel awesome.
+2. The core feature set works in almost all cases on all sites, so Vimium feels reliable.
+3. Requires no configuration or doc-reading before it's useful. Just watch the video or hit `?`.
+4. Doesn't drastically change the way Chrome looks or behaves. You can transition into using Vimium piecemeal;
+ you don't need to jump in whole-hog from the start.
+5. The core feature set isn't overwhelming. This is easy to degrade as we evolve Vimium, so it requires active
+ effort to maintain this feel.
+6. Developers find the code is relatively simple and easy to jump into, so we have an active dev community.
+
## What makes for a good feature request/contribution to Vimium?
Good features:
@@ -96,23 +124,3 @@ We use these guidelines, in addition to the code complexity, when deciding wheth
If you're worried that a feature you plan to build won't be a good fit for core Vimium, just open a github
issue for discussion or send an email to the Vimium mailing list.
-
-## How to release Vimium to the Chrome Store
-
-This process is currently only done by Phil or Ilya.
-
-1. Increment the version number in manifest.json
-2. Update the Changelog in README.md
-
- You can see a summary of commits since the last version: `git log --oneline v1.45..`
-
-3. Push your commits
-4. Create a git tag for this newly released version
-
- git tag -a v1.45 -m "v1.45 release"
-
-5. Run `cake package`
-6. Take the distributable found in `dist` and upload it
- [here](https://chrome.google.com/webstore/developer/dashboard)
-7. Update the description in the Chrome store to include the latest version's release notes
-8. Celebrate
diff --git a/README.md b/README.md
index fc38c2b8..20331785 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@ Vimium - The Hacker's Browser
[![Build Status](https://secure.travis-ci.org/philc/vimium.png?branch=master)](https://travis-ci.org/philc/vimium)
-Vimium is a Chrome extension that provides keyboard-based navigation and control in the spirit of the Vim
-editor.
+Vimium is a Chrome extension that provides keyboard-based navigation and control of the web in the spirit of
+the Vim editor.
__Installation instructions:__
@@ -15,14 +15,16 @@ Please see
[CONTRIBUTING.md](https://github.com/philc/vimium/blob/master/CONTRIBUTING.md#installing-from-source)
for instructions on how you can install Vimium from source.
-The Options page can be reached via a link on the help dialog (hit `?`) or via the button next to Vimium on
+The Options page can be reached via a link on the help dialog (type `?`) or via the button next to Vimium on
the Chrome Extensions page (`chrome://extensions`).
Keyboard Bindings
-----------------
Modifier keys are specified as `<c-x>`, `<m-x>`, and `<a-x>` for ctrl+x, meta+x, and alt+x
-respectively. See the next section for instructions on customizing these bindings.
+respectively. See the next section for how to customize these bindings.
+
+Once you have Vimium installed, you can see this list of key bindings at any time by typing `?`.
Navigating the current page:
@@ -90,7 +92,7 @@ Additional advanced browsing commands:
Vimium supports command repetition so, for example, hitting '5t' will open 5 tabs in rapid succession. `<ESC>` (or
`<c-[>`) will clear any partial commands in the queue and will also exit insert and find modes.
-There are some advanced commands which aren't documented here. Refer to the help dialog (type `?`) for a full
+There are some advanced commands which aren't documented here; refer to the help dialog (type `?`) for a full
list.
Custom Key Mappings
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index b6a52a15..dc5519d5 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -26,7 +26,6 @@ class Suggestion
generateHtml: ->
return @html if @html
- favIconUrl = @tabFavIconUrl or "#{@getUrlRoot(@url)}/favicon.ico"
relevancyHtml = if @showRelevancy then "<span class='relevancy'>#{@computeRelevancy()}</span>" else ""
# NOTE(philc): We're using these vimium-specific class names so we don't collide with the page's CSS.
@html =
@@ -35,8 +34,7 @@ class Suggestion
<span class="vimiumReset vomnibarSource">#{@type}</span>
<span class="vimiumReset vomnibarTitle">#{@highlightTerms(Utils.escapeHtml(@title))}</span>
</div>
- <div class="vimiumReset vomnibarBottomHalf vomnibarIcon"
- style="background-image: url(#{favIconUrl});">
+ <div class="vimiumReset vomnibarBottomHalf">
<span class="vimiumReset vomnibarUrl">#{@shortenUrl(@highlightTerms(Utils.escapeHtml(@url)))}</span>
#{relevancyHtml}
</div>
@@ -261,26 +259,44 @@ class DomainCompleter
# Suggestions from the Domain completer have the maximum relevancy. They should be shown first in the list.
computeRelevancy: -> 1
-# TabRecency associates a logical timestamp with each tab id.
+# TabRecency associates a logical timestamp with each tab id. These are used to provide an initial
+# recency-based ordering in the tabs vomnibar (which allows jumping quickly between recently-visited tabs).
class TabRecency
- constructor: ->
- @timestamp = 1
- @cache = {}
+ timestamp: 1
+ current: -1
+ cache: {}
+ lastVisited: null
+ lastVisitedTime: null
+ timeDelta: 500 # Milliseconds.
- chrome.tabs.onActivated.addListener (activeInfo) => @add activeInfo.tabId
- chrome.tabs.onRemoved.addListener (tabId) => @remove tabId
+ constructor: ->
+ chrome.tabs.onActivated.addListener (activeInfo) => @register activeInfo.tabId
+ chrome.tabs.onRemoved.addListener (tabId) => @deregister tabId
chrome.tabs.onReplaced.addListener (addedTabId, removedTabId) =>
- @remove removedTabId
- @add addedTabId
+ @deregister removedTabId
+ @register addedTabId
+
+ register: (tabId) ->
+ currentTime = new Date()
+ # Register tabId if it has been visited for at least @timeDelta ms. Tabs which are visited only for a
+ # very-short time (e.g. those passed through with `5J`) aren't registered as visited at all.
+ if @lastVisitedTime? and @timeDelta <= currentTime - @lastVisitedTime
+ @cache[@lastVisited] = ++@timestamp
+
+ @current = @lastVisited = tabId
+ @lastVisitedTime = currentTime
- add: (tabId) -> @cache[tabId] = ++@timestamp
- remove: (tabId) -> delete @cache[tabId]
+ deregister: (tabId) ->
+ if tabId == @lastVisited
+ # Ensure we don't register this tab, since it's going away.
+ @lastVisited = @lastVisitedTime = null
+ delete @cache[tabId]
# Recently-visited tabs get a higher score (except the current tab, which gets a low score).
recencyScore: (tabId) ->
@cache[tabId] ||= 1
- if @cache[tabId] == @timestamp then 0.0 else @cache[tabId] / @timestamp
+ if tabId == @current then 0.0 else @cache[tabId] / @timestamp
tabRecency = new TabRecency()
@@ -294,7 +310,6 @@ class TabCompleter
suggestions = results.map (tab) =>
suggestion = new Suggestion(queryTerms, "tab", tab.url, tab.title, @computeRelevancy)
suggestion.tabId = tab.id
- suggestion.tabFavIconUrl = tab.favIconUrl
suggestion
onComplete(suggestions)
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index b40907fb..3ec618c9 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -8,7 +8,7 @@ keyQueue = "" # Queue of keys typed
validFirstKeys = {}
singleKeyCommands = []
focusedFrame = null
-framesForTab = {}
+frameIdsForTab = {}
# Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12>
# This regular expression captures two groups: the first is a named key, the second is the remainder of
@@ -282,16 +282,14 @@ BackgroundCommands =
{ name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId }))
moveTabLeft: (count) -> moveTab(null, -count)
moveTabRight: (count) -> moveTab(null, count)
- nextFrame: (count) ->
+ nextFrame: (count,frameId) ->
chrome.tabs.getSelected(null, (tab) ->
- frames = framesForTab[tab.id].frames
- currIndex = getCurrFrameIndex(frames)
-
- # TODO: Skip the "top" frame (which doesn't actually have a <frame> tag),
- # since it exists only to contain the other frames.
- newIndex = (currIndex + count) % frames.length
-
- chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[newIndex].id, highlight: true }))
+ frames = frameIdsForTab[tab.id]
+ # We can't always track which frame chrome has focussed, but here we learn that it's frameId; so add an
+ # additional offset such that we do indeed start from frameId.
+ count = (count + Math.max 0, frameIdsForTab[tab.id].indexOf frameId) % frames.length
+ frames = frameIdsForTab[tab.id] = [frames[count..]..., frames[0...count]...]
+ chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[0], highlight: true }))
closeTabsOnLeft: -> removeTabsRelative "before"
closeTabsOnRight: -> removeTabsRelative "after"
@@ -347,7 +345,7 @@ updateOpenTabs = (tab) ->
scrollY: null
deletor: null
# Frames are recreated on refresh
- delete framesForTab[tab.id]
+ delete frameIdsForTab[tab.id]
setBrowserActionIcon = (tabId,path) ->
chrome.browserAction.setIcon({ tabId: tabId, path: path })
@@ -394,7 +392,7 @@ chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) ->
code: Settings.get("userDefinedLinkHintCss")
runAt: "document_start"
chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError
- updateOpenTabs(tab)
+ updateOpenTabs(tab) if changeInfo.url?
updateActiveState(tabId)
chrome.tabs.onAttached.addListener (tabId, attachedInfo) ->
@@ -429,7 +427,7 @@ chrome.tabs.onRemoved.addListener (tabId) ->
# scroll position)
tabInfoMap.deletor = -> delete tabInfoMap[tabId]
setTimeout tabInfoMap.deletor, 1000
- delete framesForTab[tabId]
+ delete frameIdsForTab[tabId]
chrome.tabs.onActiveChanged.addListener (tabId, selectInfo) -> updateActiveState(tabId)
@@ -554,9 +552,9 @@ checkKeyQueue = (keysToCheck, tabId, frameId) ->
refreshedCompletionKeys = true
else
if registryEntry.passCountToFunction
- BackgroundCommands[registryEntry.command](count)
+ BackgroundCommands[registryEntry.command](count, frameId)
else if registryEntry.noRepeat
- BackgroundCommands[registryEntry.command]()
+ BackgroundCommands[registryEntry.command](frameId)
else
repeatFunction(BackgroundCommands[registryEntry.command], count, 0, frameId)
@@ -603,21 +601,21 @@ openOptionsPageInNewTab = ->
chrome.tabs.create({ url: chrome.runtime.getURL("pages/options.html"), index: tab.index + 1 }))
registerFrame = (request, sender) ->
- unless framesForTab[sender.tab.id]
- framesForTab[sender.tab.id] = { frames: [] }
-
- if (request.is_top)
- focusedFrame = request.frameId
- framesForTab[sender.tab.id].total = request.total
+ (frameIdsForTab[sender.tab.id] ?= []).push request.frameId
- framesForTab[sender.tab.id].frames.push({ id: request.frameId })
-
-handleFrameFocused = (request, sender) -> focusedFrame = request.frameId
+unregisterFrame = (request, sender) ->
+ tabId = sender.tab.id
+ if frameIdsForTab[tabId]?
+ if request.tab_is_closing
+ updateOpenTabs sender.tab
+ else
+ frameIdsForTab[tabId] = frameIdsForTab[tabId].filter (id) -> id != request.frameId
-getCurrFrameIndex = (frames) ->
- for i in [0...frames.length]
- return i if frames[i].id == focusedFrame
- frames.length + 1
+handleFrameFocused = (request, sender) ->
+ tabId = sender.tab.id
+ if frameIdsForTab[tabId]?
+ frameIdsForTab[tabId] =
+ [request.frameId, (frameIdsForTab[tabId].filter (id) -> id != request.frameId)...]
# Port handler mapping
portHandlers =
@@ -633,6 +631,7 @@ sendRequestHandlers =
openUrlInCurrentTab: openUrlInCurrentTab,
openOptionsPageInNewTab: openOptionsPageInNewTab,
registerFrame: registerFrame,
+ unregisterFrame: unregisterFrame,
frameFocused: handleFrameFocused,
upgradeNotificationClosed: upgradeNotificationClosed,
updateScrollPosition: handleUpdateScrollPosition,
@@ -640,7 +639,7 @@ sendRequestHandlers =
isEnabledForUrl: isEnabledForUrl,
saveHelpDialogSettings: saveHelpDialogSettings,
selectSpecificTab: selectSpecificTab,
- refreshCompleter: refreshCompleter
+ refreshCompleter: refreshCompleter,
createMark: Marks.create.bind(Marks),
gotoMark: Marks.goto.bind(Marks)
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index 24f229f3..7998fe5c 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -352,13 +352,6 @@ body.vimiumFindMode ::selection {
padding: 2px 0;
}
-#vomnibar li .vomnibarIcon {
- background-position-y: center;
- background-size: 16px;
- background-repeat: no-repeat;
- padding-left: 20px;
-}
-
#vomnibar li .vomnibarSource {
color: #777;
margin-right: 4px;
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 9c59396f..469afe71 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -179,19 +179,24 @@ window.addEventListener "focus", ->
# Initialization tasks that must wait for the document to be ready.
#
initializeOnDomReady = ->
- registerFrame(window.top == window.self)
-
enterInsertModeIfElementIsFocused() if isEnabledForUrl
# Tell the background page we're in the dom ready state.
chrome.runtime.connect({ name: "domReady" })
-registerFrame = (is_top) ->
- chrome.runtime.sendMessage(
- handler: "registerFrame"
+registerFrame = ->
+ # Don't register frameset containers; focusing them is no use.
+ if document.body.tagName != "FRAMESET"
+ chrome.runtime.sendMessage
+ handler: "registerFrame"
+ frameId: frameId
+
+# Unregister the frame if we're going to exit.
+unregisterFrame = ->
+ chrome.runtime.sendMessage
+ handler: "unregisterFrame"
frameId: frameId
- is_top: is_top
- total: frames.length + 1)
+ tab_is_closing: window.top == window.self
#
# Enters insert mode if the currently focused element in the DOM is focusable.
@@ -367,7 +372,7 @@ onKeypress = (event) ->
else if (!isInsertMode() && !findMode)
if (isPassKey keyChar)
return undefined
- if (currentCompletionKeys.indexOf(keyChar) != -1)
+ if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar))
DomUtils.suppressEvent(event)
keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
@@ -440,7 +445,7 @@ onKeydown = (event) ->
else if (!isInsertMode() && !findMode)
if (keyChar)
- if (currentCompletionKeys.indexOf(keyChar) != -1)
+ if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar))
DomUtils.suppressEvent event
handledKeydownEvents.push event
@@ -502,7 +507,7 @@ refreshCompletionKeys = (response) ->
chrome.runtime.sendMessage({ handler: "getCompletionKeys" }, refreshCompletionKeys)
isValidFirstKey = (keyChar) ->
- validFirstKeys[keyChar] || /[1-9]/.test(keyChar)
+ validFirstKeys[keyChar] || /^[1-9]/.test(keyChar)
onFocusCapturePhase = (event) ->
if (isFocusable(event.target) && !findMode)
@@ -529,6 +534,7 @@ isEmbed = (element) -> ["embed", "object"].indexOf(element.nodeName.toLowerCase(
# any element which makes it a rich text editor, like the notes on jjot.com.
#
isEditable = (target) ->
+ # Note: document.activeElement.isContentEditable is also rechecked in isInsertMode() dynamically.
return true if target.isContentEditable
nodeName = target.nodeName.toLowerCase()
# use a blacklist instead of a whitelist because new form controls are still being implemented for html5
@@ -552,6 +558,7 @@ window.enterInsertMode = (target) ->
# when the last editable element that came into focus -- which insertModeLock points to -- has been blurred.
# If insert mode is entered manually (via pressing 'i'), then we set insertModeLock to 'undefined', and only
# leave insert mode when the user presses <ESC>.
+# Note. This returns the truthiness of target, which is required by isInsertMode.
#
enterInsertModeWithoutShowingIndicator = (target) -> insertModeLock = target
@@ -560,7 +567,13 @@ exitInsertMode = (target) ->
insertModeLock = null
HUD.hide()
-isInsertMode = -> insertModeLock != null
+isInsertMode = ->
+ return true if insertModeLock != null
+ # Some sites (e.g. inbox.google.com) change the contentEditable attribute on the fly (see #1245); and
+ # unfortunately, isEditable() is called *before* the change is made. Therefore, we need to re-check whether
+ # the active element is contentEditable.
+ document.activeElement and document.activeElement.isContentEditable and
+ enterInsertModeWithoutShowingIndicator document.activeElement
# should be called whenever rawQuery is modified.
updateFindModeQuery = ->
@@ -1055,6 +1068,8 @@ Tween =
state.onUpdate(value)
initializePreDomReady()
+window.addEventListener("DOMContentLoaded", registerFrame)
+window.addEventListener("unload", unregisterFrame)
window.addEventListener("DOMContentLoaded", initializeOnDomReady)
window.onbeforeunload = ->
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 62e655e7..21018049 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -32,8 +32,8 @@ DomUtils =
#
makeXPath: (elementArray) ->
xpath = []
- for i of elementArray
- xpath.push("//" + elementArray[i], "//xhtml:" + elementArray[i])
+ for element in elementArray
+ xpath.push("//" + element, "//xhtml:" + element)
xpath.join(" | ")
evaluateXPath: (xpath, resultType) ->
diff --git a/pages/popup.coffee b/pages/popup.coffee
index 2ab97bef..99a4eb87 100644
--- a/pages/popup.coffee
+++ b/pages/popup.coffee
@@ -3,6 +3,17 @@ originalRule = undefined
originalPattern = undefined
originalPassKeys = undefined
+generateDefaultPattern = (url) ->
+ if /^https?:\/\/./.test url
+ # The common use case is to disable Vimium at the domain level.
+ # Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html".
+ "https?:/" + url.split("/",3)[1..].join("/") + "/*"
+ else if /^[a-z]{3,}:\/\/./.test url
+ # Anything else which seems to be a URL.
+ url.split("/",3).join("/") + "/*"
+ else
+ url + "*"
+
reset = (initialize=false) ->
document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html")
chrome.tabs.getSelected null, (tab) ->
@@ -13,11 +24,8 @@ reset = (initialize=false) ->
originalPattern = originalRule.pattern
originalPassKeys = originalRule.passKeys
else
- # The common use case is to disable Vimium at the domain level.
- # This regexp will match "http://www.example.com/" from "http://www.example.com/path/to/page.html".
- domain = (tab.url.match(/[^\/]*\/\/[^\/]*\//) or tab.url) + "*"
originalRule = null
- originalPattern = domain
+ originalPattern = generateDefaultPattern tab.url
originalPassKeys = ""
patternElement = document.getElementById("popupPattern")
passKeysElement = document.getElementById("popupPassKeys")
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index 88f59b7e..e4966016 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -399,21 +399,32 @@ context "RegexpCache",
should "search for a string with a prefix/suffix (negative case)", ->
assert.isTrue "hound dog".search(RegexpCache.get("do", "\\b", "\\b")) == -1
+fakeTimeDeltaElapsing = ->
+
context "TabRecency",
setup ->
@tabRecency = new TabRecency()
- @tabRecency.add 3
- @tabRecency.add 2
- @tabRecency.add 9
- @tabRecency.add 1
- @tabRecency.remove 9
- @tabRecency.add 4
-
- should "have entries for active tabs", ->
+
+ fakeTimeDeltaElapsing = =>
+ if @tabRecency.lastVisitedTime?
+ @tabRecency.lastVisitedTime = new Date(@tabRecency.lastVisitedTime - @tabRecency.timeDelta)
+
+ @tabRecency.register 3
+ fakeTimeDeltaElapsing()
+ @tabRecency.register 2
+ fakeTimeDeltaElapsing()
+ @tabRecency.register 9
+ fakeTimeDeltaElapsing()
+ @tabRecency.register 1
+ @tabRecency.deregister 9
+ fakeTimeDeltaElapsing()
+ @tabRecency.register 4
+ fakeTimeDeltaElapsing()
+
+ should "have entries for recently active tabs", ->
assert.isTrue @tabRecency.cache[1]
assert.isTrue @tabRecency.cache[2]
assert.isTrue @tabRecency.cache[3]
- assert.isTrue @tabRecency.cache[4]
should "not have entries for removed tabs", ->
assert.isFalse @tabRecency.cache[9]
@@ -431,8 +442,9 @@ context "TabRecency",
should "rank tabs by recency", ->
assert.isTrue @tabRecency.recencyScore(3) < @tabRecency.recencyScore 2
assert.isTrue @tabRecency.recencyScore(2) < @tabRecency.recencyScore 1
- @tabRecency.add 3
- @tabRecency.add 4 # Making 3 the most recent tab which isn't the current tab.
+ @tabRecency.register 3
+ fakeTimeDeltaElapsing()
+ @tabRecency.register 4 # Making 3 the most recent tab which isn't the current tab.
assert.isTrue @tabRecency.recencyScore(1) < @tabRecency.recencyScore 3
assert.isTrue @tabRecency.recencyScore(2) < @tabRecency.recencyScore 3
assert.isTrue @tabRecency.recencyScore(4) < @tabRecency.recencyScore 3