aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/completion.coffee40
-rw-r--r--background_scripts/main.coffee56
-rw-r--r--content_scripts/vimium_frontend.coffee27
-rw-r--r--lib/dom_utils.coffee4
-rw-r--r--lib/utils.coffee7
-rw-r--r--tests/unit_tests/completion_test.coffee34
6 files changed, 105 insertions, 63 deletions
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index b411bcba..dc5519d5 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -259,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()
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index b40907fb..10e6121f 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,13 @@ 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 }))
+ # 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.
+ offset = Math.max 0, frameIdsForTab[tab.id].indexOf frameId
+ frames = frameIdsForTab[tab.id] = frameIdsForTab[tab.id].rotate(count+offset)
+ chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[0], highlight: true }))
closeTabsOnLeft: -> removeTabsRelative "before"
closeTabsOnRight: -> removeTabsRelative "after"
@@ -347,7 +344,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 +391,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 +426,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 +551,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 +600,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 +630,7 @@ sendRequestHandlers =
openUrlInCurrentTab: openUrlInCurrentTab,
openOptionsPageInNewTab: openOptionsPageInNewTab,
registerFrame: registerFrame,
+ unregisterFrame: unregisterFrame,
frameFocused: handleFrameFocused,
upgradeNotificationClosed: upgradeNotificationClosed,
updateScrollPosition: handleUpdateScrollPosition,
@@ -640,7 +638,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_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 34fd56d5..d5586bd8 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)
@@ -1063,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/lib/utils.coffee b/lib/utils.coffee
index b7f8731a..c8a02328 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -145,6 +145,13 @@ Function::curry = ->
Array.copy = (array) -> Array.prototype.slice.call(array, 0)
+Array::rotate = (count) ->
+ if @length
+ count = count % @length
+ count += @length while count < 0
+ Array::push.apply(this, @splice(0, count))
+ this
+
String::startsWith = (str) -> @indexOf(str) == 0
globalRoot = window ? global
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