diff options
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 15 | ||||
| -rw-r--r-- | content_scripts/link_hints.coffee | 50 | ||||
| -rw-r--r-- | lib/settings.coffee | 88 | ||||
| -rw-r--r-- | pages/options.coffee | 3 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.coffee | 27 | ||||
| -rw-r--r-- | tests/unit_tests/settings_test.coffee | 12 | ||||
| -rw-r--r-- | tests/unit_tests/test_chrome_stubs.coffee | 6 |
8 files changed, 134 insertions, 70 deletions
@@ -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: |
