diff options
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | background_scripts/commands.coffee | 6 | ||||
| -rw-r--r-- | background_scripts/completion_engines.coffee | 60 | ||||
| -rw-r--r-- | background_scripts/exclusions.coffee | 4 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 2 | ||||
| -rw-r--r-- | background_scripts/settings.coffee | 126 | ||||
| -rw-r--r-- | background_scripts/sync.coffee | 74 | ||||
| -rw-r--r-- | content_scripts/hud.coffee | 6 | ||||
| -rw-r--r-- | content_scripts/ui_component.coffee | 12 | ||||
| -rw-r--r-- | content_scripts/vimium.css | 11 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 2 | ||||
| -rw-r--r-- | lib/settings.coffee | 202 | ||||
| -rw-r--r-- | lib/utils.coffee | 7 | ||||
| -rw-r--r-- | manifest.json | 3 | ||||
| -rw-r--r-- | pages/help_dialog.html | 1 | ||||
| -rw-r--r-- | pages/options.coffee | 10 | ||||
| -rw-r--r-- | pages/options.html | 1 | ||||
| -rw-r--r-- | pages/popup.html | 2 | ||||
| -rw-r--r-- | tests/unit_tests/commands_test.coffee | 1 | ||||
| -rw-r--r-- | tests/unit_tests/exclusion_test.coffee | 5 | ||||
| -rw-r--r-- | tests/unit_tests/settings_test.coffee | 20 | ||||
| -rw-r--r-- | tests/unit_tests/test_chrome_stubs.coffee | 3 | ||||
| -rw-r--r-- | tests/unit_tests/utils_test.coffee | 5 |
23 files changed, 312 insertions, 258 deletions
@@ -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="#">×</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", -> |
