aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--background_scripts/commands.coffee6
-rw-r--r--background_scripts/completion_engines.coffee60
-rw-r--r--background_scripts/exclusions.coffee4
-rw-r--r--background_scripts/main.coffee2
-rw-r--r--background_scripts/settings.coffee126
-rw-r--r--background_scripts/sync.coffee74
-rw-r--r--content_scripts/hud.coffee6
-rw-r--r--content_scripts/ui_component.coffee12
-rw-r--r--content_scripts/vimium.css11
-rw-r--r--content_scripts/vimium_frontend.coffee2
-rw-r--r--lib/settings.coffee202
-rw-r--r--lib/utils.coffee7
-rw-r--r--manifest.json3
-rw-r--r--pages/help_dialog.html1
-rw-r--r--pages/options.coffee10
-rw-r--r--pages/options.html1
-rw-r--r--pages/popup.html2
-rw-r--r--tests/unit_tests/commands_test.coffee1
-rw-r--r--tests/unit_tests/exclusion_test.coffee5
-rw-r--r--tests/unit_tests/settings_test.coffee20
-rw-r--r--tests/unit_tests/test_chrome_stubs.coffee3
-rw-r--r--tests/unit_tests/utils_test.coffee5
23 files changed, 312 insertions, 258 deletions
diff --git a/README.md b/README.md
index c391c08e..ad40f3ba 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,8 @@ Additional advanced browsing commands:
gU go up to root of the URL hierarchy
zH scroll all the way left
zL scroll all the way right
+ v enter visual mode; use p/P to paste-and-go, use y to yank
+ V enter visual line mode
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.
@@ -143,8 +145,9 @@ Release Notes
-------------
1.52 (not yet released)
-- Search engine completion for selected search engines (including Google, Youtube, Bing, DuckDuckGo, Wikipedia and Amazon).
-- Much improved custom search engine experience (including completion, where available).
+- Improved custom-search engine experience (including completion for Google,
+ Youtube, Bing, DuckDuckGo, Wikipedia, Amazon and a number of other search
+ engines).
- Bug fixes: bookmarklets accessed from the vomnibar.
1.51 (2015-05-02)
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index fa5354df..bf892c1a 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -343,5 +343,11 @@ commandDescriptions =
Commands.init()
+# Register postUpdateHook for keyMappings setting.
+Settings.postUpdateHooks["keyMappings"] = (value) ->
+ Commands.clearKeyMappingsAndSetDefaults()
+ Commands.parseCustomKeyMappings value
+ refreshCompletionKeysAfterMappingSave()
+
root = exports ? window
root.Commands = Commands
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 189f66f3..f15e6db4 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -21,12 +21,11 @@
# A base class for common regexp-based matching engines.
class RegexpEngine
- constructor: (@regexps) ->
+ constructor: (args...) -> @regexps = args.map (regexp) -> new RegExp regexp
match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl
# Several Google completion engines package XML responses in this way.
class GoogleXMLRegexpEngine extends RegexpEngine
- doNotCache: false # true (disbaled, experimental)
parse: (xhr) ->
for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
continue unless suggestion = suggestion.getAttribute "data"
@@ -34,31 +33,47 @@ class GoogleXMLRegexpEngine extends RegexpEngine
class Google extends GoogleXMLRegexpEngine
# Example search URL: http://www.google.com/search?q=%s
- constructor: ->
- super [
- # We match the major English-speaking TLDs.
- new RegExp "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/"
- new RegExp "localhost/cgi-bin/booky" # Only for smblott.
- ]
+ constructor: (regexps = null) ->
+ super regexps ? "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=%s"
+# A wrapper class for Google completions. This adds prefix terms to the query, and strips those terms from
+# the resulting suggestions. For example, for Google Maps, we add "map of" as a prefix, then strip "map of"
+# from the resulting suggestions.
+class GoogleWithPrefix
+ constructor: (prefix, args...) ->
+ @engine = new Google args...
+ @prefix = "#{prefix.trim()} "
+ @queryTerms = @prefix.split /\s+/
+ match: (args...) -> @engine.match args...
+ getUrl: (queryTerms) -> @engine.getUrl [ @queryTerms..., queryTerms... ]
+ parse: (xhr) ->
+ @engine.parse(xhr)
+ .filter (suggestion) => suggestion.startsWith @prefix
+ .map (suggestion) => suggestion[@prefix.length..].ltrim()
+
+# For Google Maps, we add the prefix "map of" to the query, and send it to Google's general search engine,
+# then strip "map of" from the resulting suggestions.
+class GoogleMaps extends GoogleWithPrefix
+ # Example search URL: https://www.google.com/maps?q=%s
+ constructor: -> super "map of", "https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/maps"
+
class Youtube extends GoogleXMLRegexpEngine
# Example search URL: http://www.youtube.com/results?search_query=%s
constructor: ->
- super [ new RegExp "^https?://[a-z]+\.youtube\.com/results" ]
+ super "^https?://[a-z]+\.youtube\.com/results"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
"http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=%s"
class Wikipedia extends RegexpEngine
- doNotCache: false # true (disbaled, experimental)
# Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s
constructor: ->
- super [ new RegExp "^https?://[a-z]+\.wikipedia\.org/" ]
+ super "^https?://[a-z]+\.wikipedia\.org/"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
@@ -67,28 +82,15 @@ class Wikipedia extends RegexpEngine
parse: (xhr) ->
JSON.parse(xhr.responseText)[1]
-## Does not work...
-## class GoogleMaps extends RegexpEngine
-## # Example search URL: https://www.google.com/maps/search/%s
-## constructor: ->
-## super [ new RegExp "^https?://www\.google\.com/maps/search/" ]
-##
-## getUrl: (queryTerms) ->
-## "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}"
-##
-## parse: (xhr) ->
-## data = JSON.parse xhr.responseText
-## []
-
class Bing extends RegexpEngine
# Example search URL: https://www.bing.com/search?q=%s
- constructor: -> super [ new RegExp "^https?://www\.bing\.com/search" ]
+ constructor: -> super "^https?://www\.bing\.com/search"
getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "http://api.bing.com/osjson.aspx?query=%s"
parse: (xhr) -> JSON.parse(xhr.responseText)[1]
class Amazon extends RegexpEngine
# Example search URL: http://www.amazon.com/s/?field-keywords=%s
- constructor: -> super [ new RegExp "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/" ]
+ constructor: -> super "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
"https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=%s"
@@ -96,15 +98,14 @@ class Amazon extends RegexpEngine
class DuckDuckGo extends RegexpEngine
# Example search URL: https://duckduckgo.com/?q=%s
- constructor: -> super [ new RegExp "^https?://([a-z]+\.)?duckduckgo\.com/" ]
- getUrl: (queryTerms) ->
+ constructor: -> super "^https?://([a-z]+\.)?duckduckgo\.com/"
getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "https://duckduckgo.com/ac/?q=%s"
parse: (xhr) ->
suggestion.phrase for suggestion in JSON.parse xhr.responseText
class Webster extends RegexpEngine
# Example search URL: http://www.merriam-webster.com/dictionary/%s
- constructor: -> super [ new RegExp "^https?://www.merriam-webster.com/dictionary/" ]
+ constructor: -> super "^https?://www.merriam-webster.com/dictionary/"
getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "http://www.merriam-webster.com/autocomplete?query=%s"
parse: (xhr) -> JSON.parse(xhr.responseText).suggestions
@@ -120,6 +121,7 @@ class DummyCompletionEngine
# Note: Order matters here.
CompletionEngines = [
Youtube
+ GoogleMaps
Google
DuckDuckGo
Wikipedia
diff --git a/background_scripts/exclusions.coffee b/background_scripts/exclusions.coffee
index 5ec76e2a..21342d61 100644
--- a/background_scripts/exclusions.coffee
+++ b/background_scripts/exclusions.coffee
@@ -73,3 +73,7 @@ if not Settings.has("exclusionRules") and Settings.has("excludedUrls")
# We'll keep a backup of the "excludedUrls" setting, just in case.
Settings.set("excludedUrlsBackup", Settings.get("excludedUrls")) if not Settings.has("excludedUrlsBackup")
Settings.clear("excludedUrls")
+
+# Register postUpdateHook for exclusionRules setting.
+Settings.postUpdateHooks["exclusionRules"] = (value) ->
+ Exclusions.postUpdateHook value
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index e7a1f82c..99a5672b 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -744,5 +744,5 @@ chrome.windows.getAll { populate: true }, (windows) ->
chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler())
# Start pulling changes from synchronized storage.
-Sync.init()
+Settings.init()
showUpgradeMessage()
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
deleted file mode 100644
index d23649ee..00000000
--- a/background_scripts/settings.coffee
+++ /dev/null
@@ -1,126 +0,0 @@
-#
-# Used by all parts of Vimium to manipulate localStorage.
-#
-
-root = exports ? window
-root.Settings = Settings =
- get: (key) ->
- if (key of localStorage) then JSON.parse(localStorage[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 (value == @defaults[key])
- @clear(key)
- else
- jsonValue = JSON.stringify value
- localStorage[key] = jsonValue
- Sync.set key, jsonValue
-
- clear: (key) ->
- if @has key
- delete localStorage[key]
- Sync.clear key
-
- has: (key) -> key of localStorage
-
- # For settings which require action when their value changes, add hooks here called from
- # options/options.coffee (when the options page is saved), and from background_scripts/sync.coffee (when an
- # update propagates from chrome.storage.sync).
- postUpdateHooks:
- keyMappings: (value) ->
- root.Commands.clearKeyMappingsAndSetDefaults()
- root.Commands.parseCustomKeyMappings value
- root.refreshCompletionKeysAfterMappingSave()
-
- exclusionRules: (value) ->
- root.Exclusions.postUpdateHook value
-
- # postUpdateHooks convenience wrapper
- performPostUpdateHook: (key, value) ->
- @postUpdateHooks[key] value if @postUpdateHooks[key]
-
- # options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans
- # or strings
- defaults:
- scrollStepSize: 60
- smoothScroll: true
- keyMappings: "# Insert your preferred key mappings here."
- linkHintCharacters: "sadfjklewcmpgh"
- linkHintNumbers: "0123456789"
- filterLinkHints: false
- hideHud: false
- userDefinedLinkHintCss:
- """
- div > .vimiumHintMarker {
- /* linkhint boxes */
- background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785),
- color-stop(100%,#FFC542));
- border: 1px solid #E3BE23;
- }
-
- div > .vimiumHintMarker span {
- /* linkhint text */
- color: black;
- font-weight: bold;
- font-size: 12px;
- }
-
- div > .vimiumHintMarker > .matchingCharacter {
- }
- """
- # Default exclusion rules.
- exclusionRules:
- [
- # Disable Vimium on Gmail.
- { pattern: "http*://mail.google.com/*", passKeys: "" }
- ]
-
- # NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in
- # most cases the single bracket link will be "prev/next page" and the double bracket link will be
- # "first/last page", so we put the single bracket first in the pattern string so that it gets searched
- # for first.
-
- # "\bprev\b,\bprevious\b,\bback\b,<,←,«,≪,<<"
- previousPatterns: "prev,previous,back,<,\u2190,\xab,\u226a,<<"
- # "\bnext\b,\bmore\b,>,→,»,≫,>>"
- nextPatterns: "next,more,>,\u2192,\xbb,\u226b,>>"
- # default/fall back search engine
- searchUrl: "https://www.google.com/search?q="
- # put in an example search engine
- searchEngines: [
- "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
- ""
- "# More examples."
- "#"
- "# (Vimium has built-in completion for these.)"
- "#"
- "# g: http://www.google.com/search?q=%s Google"
- "# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
- "# y: http://www.youtube.com/results?search_query=%s Youtube"
- "# b: https://www.bing.com/search?q=%s Bing"
- "# d: https://duckduckgo.com/?q=%s DuckDuckGo"
- "# az: http://www.amazon.com/s/?field-keywords=%s Amazon"
- "#"
- "# Another example (for Vimium does not have completion)."
- "#"
- "# m: https://www.google.com/maps/search/%s Google Maps"
- ].join "\n"
- newTabUrl: "chrome://newtab"
- grabBackFocus: false
-
- settingsVersion: Utils.getCurrentVersion()
-
-
-# We use settingsVersion to coordinate any necessary schema changes.
-if Utils.compareVersions("1.42", Settings.get("settingsVersion")) != -1
- Settings.set("scrollStepSize", parseFloat Settings.get("scrollStepSize"))
-Settings.set("settingsVersion", Utils.getCurrentVersion())
-
-# Migration (after 1.49, 2015/2/1).
-# Legacy setting: findModeRawQuery (a string).
-# New setting: findModeRawQueryList (a list of strings), now stored in chrome.storage.local (not localStorage).
-chrome.storage.local.get "findModeRawQueryList", (items) ->
- unless chrome.runtime.lastError or items.findModeRawQueryList
- rawQuery = Settings.get "findModeRawQuery"
- chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else [])
-
diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee
deleted file mode 100644
index d0d501d3..00000000
--- a/background_scripts/sync.coffee
+++ /dev/null
@@ -1,74 +0,0 @@
-#
-# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync.
-# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those
-# changes to localStorage and into vimium's internal state.
-# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating
-# changes to localStorage and into vimium's internal state.
-#
-# Changes are propagated into vimium's state using the same mechanism
-# (Settings.performPostUpdateHook) that is used when options are changed on
-# the options page.
-#
-# The effect is best-effort synchronization of vimium options/settings between
-# chrome/vimium instances.
-#
-# NOTE:
-# Values handled within this module are ALWAYS already JSON.stringifed, so
-# they're always non-empty strings.
-#
-
-root = exports ? window
-root.Sync = Sync =
-
- storage: chrome.storage.sync
- doNotSync: ["settingsVersion", "previousVersion"]
-
- # This is called in main.coffee.
- init: ->
- chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area
- @fetchAsync()
-
- # Asynchronous fetch from synced storage, called only at startup.
- fetchAsync: ->
- @storage.get null, (items) =>
- unless chrome.runtime.lastError
- for own key, value of items
- @storeAndPropagate key, value
-
- # Asynchronous message from synced storage.
- handleStorageUpdate: (changes, area) ->
- for own key, change of changes
- @storeAndPropagate key, change?.newValue
-
- # Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate).
- storeAndPropagate: (key, value) ->
- return unless key of Settings.defaults
- return if not @shouldSyncKey key
- return if value and key of localStorage and localStorage[key] is value
- defaultValue = Settings.defaults[key]
- defaultValueJSON = JSON.stringify(defaultValue)
-
- if value and value != defaultValueJSON
- # Key/value has been changed to non-default value at remote instance.
- localStorage[key] = value
- Settings.performPostUpdateHook key, JSON.parse(value)
- else
- # Key has been reset to default value at remote instance.
- if key of localStorage
- delete localStorage[key]
- Settings.performPostUpdateHook key, defaultValue
-
- # Only called synchronously from within vimium, never on a callback.
- # No need to propagate updates to the rest of vimium, that's already been done.
- set: (key, value) ->
- if @shouldSyncKey key
- setting = {}; setting[key] = value
- @storage.set setting
-
- # Only called synchronously from within vimium, never on a callback.
- clear: (key) ->
- @storage.remove key if @shouldSyncKey key
-
- # Should we synchronize this key?
- shouldSyncKey: (key) -> key not in @doNotSync
-
diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee
index e07d0713..f38d6b45 100644
--- a/content_scripts/hud.coffee
+++ b/content_scripts/hud.coffee
@@ -57,6 +57,12 @@ class Tween
constructor: (@cssSelector, insertionPoint = document.documentElement) ->
@styleElement = document.createElement "style"
+
+ unless @styleElement.style
+ # We're in an XML document, so we shouldn't inject any elements. See the comment in UIComponent.
+ Tween::fade = Tween::stop = Tween::updateStyle = ->
+ return
+
@styleElement.type = "text/css"
@styleElement.innerHTML = ""
insertionPoint.appendChild @styleElement
diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee
index ba141b23..e4cfc293 100644
--- a/content_scripts/ui_component.coffee
+++ b/content_scripts/ui_component.coffee
@@ -7,6 +7,18 @@ class UIComponent
constructor: (iframeUrl, className, @handleMessage) ->
styleSheet = document.createElement "style"
+
+ unless styleSheet.style
+ # If this is an XML document, nothing we do here works:
+ # * <style> elements show their contents inline,
+ # * <iframe> elements don't load any content,
+ # * document.createElement generates elements that have style == null and ignore CSS.
+ # If this is the case we don't want to pollute the DOM to no or negative effect. So we bail
+ # immediately, and disable all externally-called methods.
+ @postMessage = @activate = @show = @hide = ->
+ console.log "This vimium feature is disabled because it is incompatible with this page."
+ return
+
styleSheet.type = "text/css"
# Default to everything hidden while the stylesheet loads.
styleSheet.innerHTML = "@import url(\"#{chrome.runtime.getURL("content_scripts/vimium.css")}\");"
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index 647c8025..b4bce776 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -208,13 +208,18 @@ div#vimiumHelpDialog a {
text-decoration: underline;
}
-div#vimiumHelpDialog .optionsPage {
+div#vimiumHelpDialog .wikiPage, div#vimiumHelpDialog .optionsPage {
position: absolute;
display: block;
font-size: 11px;
line-height: 130%;
- right: 60px;
- top: 8px;
+ top: 6px;
+}
+div#vimiumHelpDialog .optionsPage {
+ right: 40px;
+}
+div#vimiumHelpDialog .wikiPage {
+ right: 83px;
}
div#vimiumHelpDialog a.closeButton:hover {
color:black;
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 005412e5..c8c83029 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -1121,6 +1121,8 @@ window.showHelpDialog = (html, fid) ->
chrome.runtime.sendMessage({handler: "openOptionsPageInNewTab"})
false)
+ # Simulating a click on the help dialog makes it the active element for scrolling.
+ DomUtils.simulateClick document.getElementById "vimiumHelpDialog"
hideHelpDialog = (clickEvent) ->
isShowingHelpDialog = false
diff --git a/lib/settings.coffee b/lib/settings.coffee
new file mode 100644
index 00000000..dd667dbd
--- /dev/null
+++ b/lib/settings.coffee
@@ -0,0 +1,202 @@
+#
+# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync.
+# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those
+# changes to localStorage and into vimium's internal state.
+# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating
+# changes to localStorage and into vimium's internal state.
+#
+# The effect is best-effort synchronization of vimium options/settings between
+# chrome/vimium instances.
+#
+# NOTE:
+# Values handled within this module are ALWAYS already JSON.stringifed, so
+# they're always non-empty strings.
+#
+
+root = exports ? window
+Sync =
+
+ storage: chrome.storage.sync
+ doNotSync: ["settingsVersion", "previousVersion"]
+
+ # This is called in main.coffee.
+ init: ->
+ chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area
+ @fetchAsync()
+
+ # Asynchronous fetch from synced storage, called only at startup.
+ fetchAsync: ->
+ @storage.get null, (items) =>
+ unless chrome.runtime.lastError
+ for own key, value of items
+ Settings.storeAndPropagate key, value if @shouldSyncKey key
+
+ # Asynchronous message from synced storage.
+ handleStorageUpdate: (changes, area) ->
+ for own key, change of changes
+ Settings.storeAndPropagate key, change?.newValue if @shouldSyncKey key
+
+ # Only called synchronously from within vimium, never on a callback.
+ # No need to propagate updates to the rest of vimium, that's already been done.
+ set: (key, value) ->
+ if @shouldSyncKey key
+ setting = {}; setting[key] = value
+ @storage.set setting
+
+ # Only called synchronously from within vimium, never on a callback.
+ clear: (key) ->
+ @storage.remove key if @shouldSyncKey key
+
+ # Should we synchronize this key?
+ shouldSyncKey: (key) -> key not in @doNotSync
+
+#
+# Used by all parts of Vimium to manipulate localStorage.
+#
+
+# Select the object to use as the cache for settings.
+if Utils.isExtensionPage()
+ if Utils.isBackgroundPage()
+ settingsCache = localStorage
+ else
+ settingsCache = extend {}, localStorage # Make a copy of the cached settings from localStorage
+else
+ settingsCache = {}
+
+root.Settings = Settings =
+ cache: settingsCache
+ init: -> Sync.init()
+ get: (key) ->
+ if (key of @cache) 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 (value == @defaults[key])
+ @clear(key)
+ else
+ jsonValue = JSON.stringify value
+ @cache[key] = jsonValue
+ Sync.set key, jsonValue
+
+ clear: (key) ->
+ if @has key
+ delete @cache[key]
+ Sync.clear key
+
+ has: (key) -> key of @cache
+
+ # For settings which require action when their value changes, add hooks to this object, to be called from
+ # options/options.coffee (when the options page is saved), and by Settings.storeAndPropagate (when an
+ # update propagates from chrome.storage.sync).
+ postUpdateHooks: {}
+
+ # postUpdateHooks convenience wrapper
+ performPostUpdateHook: (key, value) ->
+ @postUpdateHooks[key]? value
+
+ # Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate).
+ storeAndPropagate: (key, value) ->
+ return unless key of @defaults
+ return if value and key of @cache and @cache[key] is value
+ defaultValue = @defaults[key]
+ defaultValueJSON = JSON.stringify(defaultValue)
+
+ if value and value != defaultValueJSON
+ # Key/value has been changed to non-default value at remote instance.
+ @cache[key] = value
+ @performPostUpdateHook key, JSON.parse(value)
+ else
+ # Key has been reset to default value at remote instance.
+ if key of @cache
+ delete @cache[key]
+ @performPostUpdateHook key, defaultValue
+
+ # options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans
+ # or strings
+ defaults:
+ scrollStepSize: 60
+ smoothScroll: true
+ keyMappings: "# Insert your preferred key mappings here."
+ linkHintCharacters: "sadfjklewcmpgh"
+ linkHintNumbers: "0123456789"
+ filterLinkHints: false
+ hideHud: false
+ userDefinedLinkHintCss:
+ """
+ div > .vimiumHintMarker {
+ /* linkhint boxes */
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785),
+ color-stop(100%,#FFC542));
+ border: 1px solid #E3BE23;
+ }
+
+ div > .vimiumHintMarker span {
+ /* linkhint text */
+ color: black;
+ font-weight: bold;
+ font-size: 12px;
+ }
+
+ div > .vimiumHintMarker > .matchingCharacter {
+ }
+ """
+ # Default exclusion rules.
+ exclusionRules:
+ [
+ # Disable Vimium on Gmail.
+ { pattern: "http*://mail.google.com/*", passKeys: "" }
+ ]
+
+ # NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in
+ # most cases the single bracket link will be "prev/next page" and the double bracket link will be
+ # "first/last page", so we put the single bracket first in the pattern string so that it gets searched
+ # for first.
+
+ # "\bprev\b,\bprevious\b,\bback\b,<,←,«,≪,<<"
+ previousPatterns: "prev,previous,back,<,\u2190,\xab,\u226a,<<"
+ # "\bnext\b,\bmore\b,>,→,»,≫,>>"
+ nextPatterns: "next,more,>,\u2192,\xbb,\u226b,>>"
+ # default/fall back search engine
+ searchUrl: "https://www.google.com/search?q="
+ # put in an example search engine
+ searchEngines: [
+ "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
+ ""
+ "# More examples."
+ "#"
+ "# (Vimium has built-in completion for these.)"
+ "#"
+ "# g: http://www.google.com/search?q=%s Google"
+ "# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
+ "# y: http://www.youtube.com/results?search_query=%s Youtube"
+ "# b: https://www.bing.com/search?q=%s Bing"
+ "# d: https://duckduckgo.com/?q=%s DuckDuckGo"
+ "# az: http://www.amazon.com/s/?field-keywords=%s Amazon"
+ "#"
+ "# Another example (for Vimium does not have completion)."
+ "#"
+ "# m: https://www.google.com/maps/search/%s Google Maps"
+ ].join "\n"
+ newTabUrl: "chrome://newtab"
+ grabBackFocus: false
+
+ settingsVersion: Utils.getCurrentVersion()
+
+# Export Sync via Settings for tests.
+root.Settings.Sync = Sync
+
+# Perform migration from old settings versions, if this is the background page.
+if Utils.isBackgroundPage()
+
+ # We use settingsVersion to coordinate any necessary schema changes.
+ if Utils.compareVersions("1.42", Settings.get("settingsVersion")) != -1
+ Settings.set("scrollStepSize", parseFloat Settings.get("scrollStepSize"))
+ Settings.set("settingsVersion", Utils.getCurrentVersion())
+
+ # Migration (after 1.49, 2015/2/1).
+ # Legacy setting: findModeRawQuery (a string).
+ # New setting: findModeRawQueryList (a list of strings), now stored in chrome.storage.local (not localStorage).
+ chrome.storage.local.get "findModeRawQueryList", (items) ->
+ unless chrome.runtime.lastError or items.findModeRawQueryList
+ rawQuery = Settings.get "findModeRawQuery"
+ chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else [])
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 77e2b68d..93045f32 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -2,6 +2,13 @@ Utils =
getCurrentVersion: ->
chrome.runtime.getManifest().version
+ # Returns true whenever the current page is from the extension's origin (and thus can access the
+ # extension's localStorage).
+ isExtensionPage: -> document.location?.origin + "/" == chrome.extension.getURL ""
+
+ # Returns true whenever the current page is the extension's background page.
+ isBackgroundPage: -> @isExtensionPage() and chrome.extension.getBackgroundPage() == window
+
# Takes a dot-notation object string and call the function
# that it points to with the correct value for 'this'.
invokeCommandString: (str, argArray) ->
diff --git a/manifest.json b/manifest.json
index fe5c69ca..f0c51117 100644
--- a/manifest.json
+++ b/manifest.json
@@ -9,10 +9,9 @@
"background": {
"scripts": [
"lib/utils.js",
+ "lib/settings.js",
"background_scripts/commands.js",
"lib/clipboard.js",
- "background_scripts/sync.js",
- "background_scripts/settings.js",
"background_scripts/exclusions.js",
"background_scripts/completion_engines.js",
"background_scripts/completion_search.js",
diff --git a/pages/help_dialog.html b/pages/help_dialog.html
index 77c3e2bf..6c7c78c2 100644
--- a/pages/help_dialog.html
+++ b/pages/help_dialog.html
@@ -7,6 +7,7 @@
page with the up-to-date key bindings when the dialog is shown. -->
<div id="vimiumHelpDialog" class="vimiumReset">
<a class="vimiumReset optionsPage" href="#">Options</a>
+ <a class="vimiumReset wikiPage" href="https://github.com/philc/vimium/wiki" target="_blank">Wiki</a>
<a class="vimiumReset closeButton" href="#">&times;</a>
<div id="vimiumTitle" class="vimiumReset"><span class="vimiumReset" style="color:#2f508e">Vim</span>ium {{title}}</div>
<div class="vimiumReset vimiumColumn">
diff --git a/pages/options.coffee b/pages/options.coffee
index 18ff226d..110f869c 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -1,7 +1,6 @@
$ = (id) -> document.getElementById id
-bgUtils = chrome.extension.getBackgroundPage().Utils
-bgSettings = chrome.extension.getBackgroundPage().Settings
+Settings.init()
bgExclusions = chrome.extension.getBackgroundPage().Exclusions
#
@@ -22,21 +21,20 @@ class Option
# Fetch a setting from localStorage, remember the @previous value and populate the DOM element.
# Return the fetched value.
fetch: ->
- @populateElement @previous = bgSettings.get @field
+ @populateElement @previous = Settings.get @field
@previous
# Write this option's new value back to localStorage, if necessary.
save: ->
value = @readValueFromElement()
if not @areEqual value, @previous
- bgSettings.set @field, @previous = value
- bgSettings.performPostUpdateHook @field, value
+ Settings.set @field, @previous = value
# Compare values; this is overridden by sub-classes.
areEqual: (a,b) -> a == b
restoreToDefault: ->
- bgSettings.clear @field
+ Settings.clear @field
@fetch()
# Static method.
diff --git a/pages/options.html b/pages/options.html
index 0fa5b18d..67e2b16d 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -3,6 +3,7 @@
<title>Vimium Options</title>
<link rel="stylesheet" type="text/css" href="options.css">
<script src="content_script_loader.js"></script>
+ <script type="text/javascript" src="../lib/settings.js"></script>
<script type="text/javascript" src="options.js"></script>
</head>
diff --git a/pages/popup.html b/pages/popup.html
index c7e2fd6f..fdf116e5 100644
--- a/pages/popup.html
+++ b/pages/popup.html
@@ -48,6 +48,8 @@
}
</style>
+ <script src="../lib/utils.js"></script>
+ <script src="../lib/settings.js"></script>
<script src="options.js"></script>
</head>
<body>
diff --git a/tests/unit_tests/commands_test.coffee b/tests/unit_tests/commands_test.coffee
index daaef016..e55dc0f2 100644
--- a/tests/unit_tests/commands_test.coffee
+++ b/tests/unit_tests/commands_test.coffee
@@ -1,5 +1,6 @@
require "./test_helper.js"
extend global, require "./test_chrome_stubs.js"
+global.Settings = {postUpdateHooks: {}}
{Commands} = require "../../background_scripts/commands.js"
context "Key mappings",
diff --git a/tests/unit_tests/exclusion_test.coffee b/tests/unit_tests/exclusion_test.coffee
index b3ed7194..28c17a2f 100644
--- a/tests/unit_tests/exclusion_test.coffee
+++ b/tests/unit_tests/exclusion_test.coffee
@@ -14,9 +14,8 @@ root.Marks =
extend(global, require "../../lib/utils.js")
Utils.getCurrentVersion = -> '1.44'
-extend(global,require "../../background_scripts/sync.js")
-extend(global,require "../../background_scripts/settings.js")
-Sync.init()
+extend(global,require "../../lib/settings.js")
+Settings.init()
extend(global, require "../../background_scripts/exclusions.js")
extend(global, require "../../background_scripts/commands.js")
extend(global, require "../../background_scripts/main.js")
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index 4cd20211..ded7b5f8 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -3,15 +3,18 @@ extend global, require "./test_chrome_stubs.js"
extend(global, require "../../lib/utils.js")
Utils.getCurrentVersion = -> '1.44'
+Utils.isBackgroundPage = -> true
+Utils.isExtensionPage = -> true
global.localStorage = {}
-extend(global,require "../../background_scripts/sync.js")
-extend(global,require "../../background_scripts/settings.js")
-Sync.init()
+extend(global,require "../../lib/settings.js")
context "settings",
setup ->
stub global, 'localStorage', {}
+ Settings.cache = global.localStorage # Point the settings cache to the new localStorage object.
+ Settings.postUpdateHooks = {} # Avoid running update hooks which include calls to outside of settings.
+ Settings.init()
should "save settings in localStorage as JSONified strings", ->
Settings.set 'dummy', ""
@@ -39,24 +42,22 @@ context "settings",
should "propagate non-default value via synced storage listener", ->
Settings.set 'scrollStepSize', 20
assert.equal Settings.get('scrollStepSize'), 20
- Sync.handleStorageUpdate { scrollStepSize: { newValue: "40" } }
+ Settings.Sync.handleStorageUpdate { scrollStepSize: { newValue: "40" } }
assert.equal Settings.get('scrollStepSize'), 40
should "propagate default value via synced storage listener", ->
Settings.set 'scrollStepSize', 20
assert.equal Settings.get('scrollStepSize'), 20
- Sync.handleStorageUpdate { scrollStepSize: { newValue: "60" } }
+ Settings.Sync.handleStorageUpdate { scrollStepSize: { newValue: "60" } }
assert.isFalse Settings.has 'scrollStepSize'
should "propagate non-default values from synced storage", ->
chrome.storage.sync.set { scrollStepSize: JSON.stringify(20) }
- Sync.fetchAsync()
assert.equal Settings.get('scrollStepSize'), 20
should "propagate default values from synced storage", ->
Settings.set 'scrollStepSize', 20
chrome.storage.sync.set { scrollStepSize: JSON.stringify(60) }
- Sync.fetchAsync()
assert.isFalse Settings.has 'scrollStepSize'
should "clear a setting from synced storage", ->
@@ -66,9 +67,10 @@ context "settings",
should "trigger a postUpdateHook", ->
message = "Hello World"
- Settings.postUpdateHooks['scrollStepSize'] = (value) -> Sync.message = value
+ receivedMessage = ""
+ Settings.postUpdateHooks['scrollStepSize'] = (value) -> receivedMessage = value
chrome.storage.sync.set { scrollStepSize: JSON.stringify(message) }
- assert.equal message, Sync.message
+ assert.equal message, receivedMessage
should "sync a key which is not a known setting (without crashing)", ->
chrome.storage.sync.set { notASetting: JSON.stringify("notAUsefullValue") }
diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee
index 60f3a890..16f0e144 100644
--- a/tests/unit_tests/test_chrome_stubs.coffee
+++ b/tests/unit_tests/test_chrome_stubs.coffee
@@ -19,6 +19,9 @@ exports.chrome =
onInstalled:
addListener: ->
+ extension:
+ getURL: (path) -> path
+
tabs:
onSelectionChanged:
addListener: () -> true
diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee
index bfe066c3..f9ed3636 100644
--- a/tests/unit_tests/utils_test.coffee
+++ b/tests/unit_tests/utils_test.coffee
@@ -2,9 +2,8 @@ require "./test_helper.js"
extend global, require "./test_chrome_stubs.js"
extend(global, require "../../lib/utils.js")
Utils.getCurrentVersion = -> '1.43'
-extend(global, require "../../background_scripts/sync.js")
-extend(global, require "../../background_scripts/settings.js")
-Sync.init()
+extend(global, require "../../lib/settings.js")
+Settings.init()
context "isUrl",
should "accept valid URLs", ->