aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--background_scripts/main.coffee15
-rw-r--r--content_scripts/link_hints.coffee50
-rw-r--r--lib/settings.coffee88
-rw-r--r--pages/options.coffee3
-rw-r--r--tests/dom_tests/dom_tests.coffee27
-rw-r--r--tests/unit_tests/settings_test.coffee12
-rw-r--r--tests/unit_tests/test_chrome_stubs.coffee6
8 files changed, 134 insertions, 70 deletions
diff --git a/README.md b/README.md
index a3bf84bb..b2b865f1 100644
--- a/README.md
+++ b/README.md
@@ -163,7 +163,8 @@ Release Notes
- Added <tt>\`\`</tt> to jump back to the previous position after selected jump-like movements: <br/>
(`gg`, `G`, `n`, `N`, `/` and local mark movements).
- Global marks are now persistent (across tab closes and browser sessions) and synced.
-- For filtered link hints (not the default), you can now use `Tab` to select hints.
+- For filtered link hints (not the default), you can now use `Tab` and `Enter`
+ to select hints and hints are ordered by best match.
- Bug fixes, including:
- Bookmarklets accessed from the Vomnibar.
- Global marks on non-Windows platforms.
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 835b8a9a..d4b14f3c 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -237,11 +237,12 @@ repeatFunction = (func, totalCount, currentCount, frameId) ->
-> repeatFunction(func, totalCount, currentCount + 1, frameId),
frameId)
-moveTab = (callback, direction) ->
- chrome.tabs.getSelected(null, (tab) ->
- # Use Math.max to prevent -1 as the new index, otherwise the tab of index n will wrap to the far RHS when
- # moved left by exactly (n+1) places.
- chrome.tabs.move(tab.id, {index: Math.max(0, tab.index + direction) }, callback))
+moveTab = (count) ->
+ chrome.tabs.getAllInWindow null, (tabs) ->
+ pinnedCount = (tabs.filter (tab) -> tab.pinned).length
+ chrome.tabs.getSelected null, (tab) ->
+ chrome.tabs.move tab.id,
+ index: Math.max pinnedCount, Math.min tabs.length - 1, tab.index + count
# Start action functions
@@ -304,8 +305,8 @@ BackgroundCommands =
chrome.tabs.getSelected(null, (tab) ->
chrome.tabs.sendMessage(tab.id,
{ name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId }))
- moveTabLeft: (count) -> moveTab(null, -count)
- moveTabRight: (count) -> moveTab(null, count)
+ moveTabLeft: (count) -> moveTab -count
+ moveTabRight: (count) -> moveTab count
nextFrame: (count,frameId) ->
chrome.tabs.getSelected null, (tab) ->
frameIdsForTab[tab.id] = cycleToFrame frameIdsForTab[tab.id], frameId, count
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index 107a292e..15af15c5 100644
--- a/content_scripts/link_hints.coffee
+++ b/content_scripts/link_hints.coffee
@@ -478,7 +478,7 @@ class FilterHints
@labelMap[forElement] = labelText
generateHintString: (linkHintNumber) ->
- numberToHintString linkHintNumber + 1, @linkHintNumbers.toUpperCase()
+ numberToHintString linkHintNumber, @linkHintNumbers.toUpperCase()
generateLinkText: (element) ->
linkText = ""
@@ -512,8 +512,7 @@ class FilterHints
fillInMarkers: (hintMarkers) ->
@generateLabelMap()
DomUtils.textContent.reset()
- for marker, idx in hintMarkers
- marker.hintString = @generateHintString(idx)
+ for marker in hintMarkers
linkTextObject = @generateLinkText(marker.clickableItem)
marker.linkText = linkTextObject.text
marker.showLinkText = linkTextObject.show
@@ -522,7 +521,9 @@ class FilterHints
@activeHintMarker = hintMarkers[0]
@activeHintMarker?.classList.add "vimiumActiveHintMarker"
- hintMarkers
+ # We use @filterLinkHints() here (although we know that all of the hints will match) to fill in the hint
+ # strings. This ensures that we always get hint strings in the same order.
+ @filterLinkHints hintMarkers
getMatchingHints: (hintMarkers, tabCount = 0) ->
delay = 0
@@ -563,15 +564,46 @@ class FilterHints
# Filter link hints by search string, renumbering the hints as necessary.
filterLinkHints: (hintMarkers) ->
- idx = 0
- linkSearchString = @linkTextKeystrokeQueue.join("").toLowerCase()
-
+ linkSearchString = @linkTextKeystrokeQueue.join("").trim().toLowerCase()
+ do (scoreFunction = @scoreLinkHint linkSearchString) ->
+ linkMarker.score = scoreFunction linkMarker for linkMarker in hintMarkers
+ # The Javascript sort() method is known not to be stable. Nevertheless, we require (and assume, here)
+ # that it is deterministic. So, if the user is typing hint characters, then hints will always end up in
+ # the same order and hence with the same hint strings (because hint-string filtering happens after the
+ # filtering here).
+ hintMarkers = hintMarkers[..].sort (a,b) -> b.score - a.score
+
+ linkHintNumber = 1
for linkMarker in hintMarkers
- continue unless 0 <= linkMarker.linkText.toLowerCase().indexOf linkSearchString
- linkMarker.hintString = @generateHintString idx++
+ continue unless 0 < linkMarker.score
+ linkMarker.hintString = @generateHintString linkHintNumber++
@renderMarker linkMarker
linkMarker
+ # Assign a score to a filter match (higher is better). We assign a higher score for matches at the start of
+ # a word, and a considerably higher score still for matches which are whole words.
+ scoreLinkHint: (linkSearchString) ->
+ searchWords = linkSearchString.trim().split /\s+/
+ (linkMarker) ->
+ linkWords = linkMarker.linkWords ?= linkMarker.linkText.trim().toLowerCase().split /\s+/
+
+ searchWordScores =
+ for searchWord in searchWords
+ linkWordScores =
+ for linkWord, idx in linkWords
+ if linkWord == searchWord
+ if idx == 0 then 8 else 6
+ else if linkWord.startsWith searchWord
+ if idx == 0 then 4 else 2
+ else if 0 <= linkWord.indexOf searchWord
+ 1
+ else
+ 0
+ Math.max linkWordScores...
+
+ addFunc = (a,b) -> a + b
+ if 0 in searchWordScores then 0 else searchWordScores.reduce addFunc, 0
+
#
# Make each hint character a span, so that we can highlight the typed characters as you type them.
#
diff --git a/lib/settings.coffee b/lib/settings.coffee
index ca4e77b0..437e4d45 100644
--- a/lib/settings.coffee
+++ b/lib/settings.coffee
@@ -1,5 +1,17 @@
+# A "setting" is a stored key/value pair. An "option" is a setting which has a default value and whose value
+# can be changed on the options page.
+#
+# Option values which have never been changed by the user are in Settings.defaults.
+#
+# Settings whose values have been changed are:
+# 1. stored either in chrome.storage.sync or in chrome.storage.local (but never both), and
+# 2. cached in Settings.cache; on extension pages, Settings.cache uses localStorage (so it persists).
+#
+# In all cases except Settings.defaults, values are stored as jsonified strings.
+
Settings =
+ debug: false
storage: chrome.storage.sync
cache: {}
isLoaded: false
@@ -11,18 +23,21 @@ Settings =
@cache = if Utils.isBackgroundPage() then localStorage else extend {}, localStorage
@onLoaded()
- @storage.get null, (items) =>
- unless chrome.runtime.lastError
- @handleUpdateFromChromeStorage key, value for own key, value of items
+ chrome.storage.local.get null, (localItems) =>
+ localItems = {} if chrome.runtime.lastError
+ @storage.get null, (syncedItems) =>
+ unless chrome.runtime.lastError
+ @handleUpdateFromChromeStorage key, value for own key, value of extend localItems, syncedItems
- chrome.storage.onChanged.addListener (changes, area) =>
- @propagateChangesFromChromeStorage changes if area == "sync"
+ chrome.storage.onChanged.addListener (changes, area) =>
+ @propagateChangesFromChromeStorage changes if area == "sync"
- @onLoaded()
+ @onLoaded()
# Called after @cache has been initialized. On extension pages, this will be called twice, but that does
# not matter because it's idempotent.
onLoaded: ->
+ @log "onLoaded: #{@onLoadedCallbacks.length} callback(s)"
@isLoaded = true
callback() while callback = @onLoadedCallbacks.pop()
@@ -33,46 +48,40 @@ Settings =
@handleUpdateFromChromeStorage key, change?.newValue for own key, change of changes
handleUpdateFromChromeStorage: (key, value) ->
+ @log "handleUpdateFromChromeStorage: #{key}"
# Note: value here is either null or a JSONified string. Therefore, even falsy settings values (like
# false, 0 or "") are truthy here. Only null is falsy.
if @shouldSyncKey key
unless value and key of @cache and @cache[key] == value
- defaultValue = @defaults[key]
- defaultValueJSON = JSON.stringify defaultValue
-
- if value and value != defaultValueJSON
- # Key/value has been changed to a non-default value.
- @cache[key] = value
- @performPostUpdateHook key, JSON.parse value
- else
- # The key has been reset to its default value.
- delete @cache[key] if key of @cache
- @performPostUpdateHook key, defaultValue
+ value ?= JSON.stringify @defaults[key]
+ @set key, JSON.parse(value), false
get: (key) ->
console.log "WARNING: Settings have not loaded yet; using the default value for #{key}." unless @isLoaded
if key of @cache and @cache[key]? then JSON.parse @cache[key] else @defaults[key]
- set: (key, value) ->
- # Don't store the value if it is equal to the default, so we can change the defaults in the future.
- if JSON.stringify(value) == JSON.stringify @defaults[key]
- @clear key
- else
- jsonValue = JSON.stringify value
- @cache[key] = jsonValue
- if @shouldSyncKey key
- setting = {}; setting[key] = jsonValue
+ set: (key, value, shouldSetInSyncedStorage = true) ->
+ @cache[key] = JSON.stringify value
+ @log "set: #{key} (length=#{@cache[key].length}, shouldSetInSyncedStorage=#{shouldSetInSyncedStorage})"
+ if @shouldSyncKey key
+ if shouldSetInSyncedStorage
+ setting = {}; setting[key] = @cache[key]
+ @log " chrome.storage.sync.set(#{key})"
@storage.set setting
- @performPostUpdateHook key, value
+ if Utils.isBackgroundPage()
+ # Remove options installed by the "copyNonDefaultsToChromeStorage-20150717" migration; see below.
+ @log " chrome.storage.local.remove(#{key})"
+ chrome.storage.local.remove key
+ @performPostUpdateHook key, value
clear: (key) ->
- delete @cache[key] if @has key
- @storage.remove key if @shouldSyncKey key
- @performPostUpdateHook key, @get key
+ @log "clear: #{key}"
+ @set key, @defaults[key]
has: (key) -> key of @cache
use: (key, callback) ->
+ @log "use: #{key} (isLoaded=#{@isLoaded})"
invokeCallback = => callback @get key
if @isLoaded then invokeCallback() else @onLoadedCallbacks.push invokeCallback
@@ -80,6 +89,10 @@ Settings =
postUpdateHooks: {}
performPostUpdateHook: (key, value) -> @postUpdateHooks[key]? value
+ # For development only.
+ log: (args...) ->
+ console.log "settings:", args... if @debug
+
# Default values for all settings.
defaults:
scrollStepSize: 60
@@ -146,6 +159,7 @@ Settings =
"""
newTabUrl: "chrome://newtab"
grabBackFocus: false
+ regexFindMode: false
settingsVersion: Utils.getCurrentVersion()
helpDialog_showAdvancedCommands: false
@@ -168,5 +182,19 @@ if Utils.isBackgroundPage()
rawQuery = Settings.get "findModeRawQuery"
chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else [])
+ # Migration (after 1.51, 2015/6/17).
+ # Copy options with non-default values (and which are not in synced storage) to chrome.storage.local;
+ # thereby making these settings accessible within content scripts.
+ do (migrationKey = "copyNonDefaultsToChromeStorage-20150717") ->
+ unless localStorage[migrationKey]
+ chrome.storage.sync.get null, (items) ->
+ unless chrome.runtime.lastError
+ updates = {}
+ for own key of localStorage
+ if Settings.shouldSyncKey(key) and not items[key]
+ updates[key] = localStorage[key]
+ chrome.storage.local.set updates, ->
+ localStorage[migrationKey] = not chrome.runtime.lastError
+
root = exports ? window
root.Settings = Settings
diff --git a/pages/options.coffee b/pages/options.coffee
index ea4301a9..21e81c8f 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -229,7 +229,6 @@ initOptionsPage = ->
element.className = element.className + " example info"
element.innerHTML = "Leave empty to reset this option."
- maintainLinkHintsView()
window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled
document.addEventListener "keyup", (event) ->
@@ -259,6 +258,8 @@ initOptionsPage = ->
for name, type of options
new type(name,onUpdated)
+ maintainLinkHintsView()
+
initPopupPage = ->
chrome.tabs.getSelected null, (tab) ->
exclusions = null
diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index dd2f5a5d..a79735ae 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -212,11 +212,14 @@ context "Filtered link hints",
@linkHints.deactivateMode()
should "label the images", ->
- hintMarkers = getHintMarkers()
- assert.equal "1: alt text", hintMarkers[0].textContent.toLowerCase()
- assert.equal "2: some title", hintMarkers[1].textContent.toLowerCase()
- assert.equal "3: alt text", hintMarkers[2].textContent.toLowerCase()
- assert.equal "4", hintMarkers[3].textContent.toLowerCase()
+ hintMarkers = getHintMarkers().map (marker) -> marker.textContent.toLowerCase()
+ # We don't know the actual hint numbers which will be assigned, so we replace them with "N".
+ hintMarkers = hintMarkers.map (str) -> str.replace /^[1-4]/, "N"
+ assert.equal 4, hintMarkers.length
+ assert.isTrue "N: alt text" in hintMarkers
+ assert.isTrue "N: some title" in hintMarkers
+ assert.isTrue "N: alt text" in hintMarkers
+ assert.isTrue "N" in hintMarkers
context "Input hints",
@@ -235,11 +238,15 @@ context "Filtered link hints",
should "label the input elements", ->
hintMarkers = getHintMarkers()
- assert.equal "1", hintMarkers[0].textContent.toLowerCase()
- assert.equal "2", hintMarkers[1].textContent.toLowerCase()
- assert.equal "3: a label", hintMarkers[2].textContent.toLowerCase()
- assert.equal "4: a label", hintMarkers[3].textContent.toLowerCase()
- assert.equal "5", hintMarkers[4].textContent.toLowerCase()
+ hintMarkers = getHintMarkers().map (marker) -> marker.textContent.toLowerCase()
+ # We don't know the actual hint numbers which will be assigned, so we replace them with "N".
+ hintMarkers = hintMarkers.map (str) -> str.replace /^[1-5]/, "N"
+ assert.equal 5, hintMarkers.length
+ assert.isTrue "N" in hintMarkers
+ assert.isTrue "N" in hintMarkers
+ assert.isTrue "N: a label" in hintMarkers
+ assert.isTrue "N: a label" in hintMarkers
+ assert.isTrue "N" in hintMarkers
context "Input focus",
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index 08145190..6270ae3e 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -27,12 +27,6 @@ context "settings",
Settings.set 'scrollStepSize', 20
assert.equal Settings.get('scrollStepSize'), 20
- should "not store values equal to the default", ->
- Settings.set 'scrollStepSize', 20
- assert.isTrue Settings.has 'scrollStepSize'
- Settings.set 'scrollStepSize', 60
- assert.isFalse Settings.has 'scrollStepSize'
-
should "revert to defaults if no key is stored", ->
Settings.set 'scrollStepSize', 20
Settings.clear 'scrollStepSize'
@@ -55,7 +49,7 @@ context "synced settings",
Settings.set 'scrollStepSize', 20
assert.equal Settings.get('scrollStepSize'), 20
Settings.propagateChangesFromChromeStorage { scrollStepSize: { newValue: "60" } }
- assert.isFalse Settings.has 'scrollStepSize'
+ assert.equal Settings.get('scrollStepSize'), 60
should "propagate non-default values from synced storage", ->
chrome.storage.sync.set { scrollStepSize: JSON.stringify(20) }
@@ -64,12 +58,12 @@ context "synced settings",
should "propagate default values from synced storage", ->
Settings.set 'scrollStepSize', 20
chrome.storage.sync.set { scrollStepSize: JSON.stringify(60) }
- assert.isFalse Settings.has 'scrollStepSize'
+ assert.equal Settings.get('scrollStepSize'), 60
should "clear a setting from synced storage", ->
Settings.set 'scrollStepSize', 20
chrome.storage.sync.remove 'scrollStepSize'
- assert.isFalse Settings.has 'scrollStepSize'
+ assert.equal Settings.get('scrollStepSize'), 60
should "trigger a postUpdateHook", ->
message = "Hello World"
diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee
index fe2fc298..c6a56521 100644
--- a/tests/unit_tests/test_chrome_stubs.coffee
+++ b/tests/unit_tests/test_chrome_stubs.coffee
@@ -57,9 +57,9 @@ exports.chrome =
storage:
# chrome.storage.local
local:
- get: ->
- set: ->
- remove: ->
+ get: (_, callback) -> callback?()
+ set: (_, callback) -> callback?()
+ remove: (_, callback) -> callback?()
# chrome.storage.onChanged
onChanged: