diff options
| author | Stephen Blott | 2015-06-01 14:56:08 +0100 | 
|---|---|---|
| committer | Stephen Blott | 2015-06-01 14:56:08 +0100 | 
| commit | 35c52143d82c8b2bc3e07832e8f6cdb089453baf (patch) | |
| tree | 9e38267f3cc8e0d82331a52373ad8f212f0ce3e0 | |
| parent | f44ca0ff0b7f17a280e30348fdb68daa606b1b9f (diff) | |
| parent | 34f0f90debf0050ece9bd847993f281c1e64be59 (diff) | |
| download | vimium-35c52143d82c8b2bc3e07832e8f6cdb089453baf.tar.bz2 | |
Merge branch 'unified-settings-implementation'
| -rw-r--r-- | background_scripts/main.coffee | 17 | ||||
| -rw-r--r-- | content_scripts/hud.coffee | 2 | ||||
| -rw-r--r-- | content_scripts/link_hints.coffee | 17 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 11 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 100 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 2 | ||||
| -rw-r--r-- | lib/settings.coffee | 196 | ||||
| -rw-r--r-- | manifest.json | 1 | ||||
| -rw-r--r-- | pages/options.coffee | 13 | ||||
| -rw-r--r-- | pages/options.html | 1 | ||||
| -rw-r--r-- | tests/dom_tests/chrome.coffee | 8 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.coffee | 32 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.html | 1 | ||||
| -rw-r--r-- | tests/unit_tests/exclusion_test.coffee | 1 | ||||
| -rw-r--r-- | tests/unit_tests/settings_test.coffee | 12 | ||||
| -rw-r--r-- | tests/unit_tests/test_chrome_stubs.coffee | 4 | ||||
| -rw-r--r-- | tests/unit_tests/utils_test.coffee | 1 | 
17 files changed, 163 insertions, 256 deletions
| diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 99a5672b..980f8e18 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -217,20 +217,6 @@ selectSpecificTab = (request) ->      chrome.windows.update(tab.windowId, { focused: true })      chrome.tabs.update(request.id, { selected: true })) -# -# Used by the content scripts to get settings from the local storage. -# -handleSettings = (request, port) -> -  switch request.operation -    when "get" # Get a single settings value. -      port.postMessage key: request.key, value: Settings.get request.key -    when "set" # Set a single settings value. -      Settings.set request.key, request.value -    when "fetch" # Fetch multiple settings values. -      values = request.values -      values[key] = Settings.get key for own key of values -      port.postMessage { values } -  chrome.tabs.onSelectionChanged.addListener (tabId, selectionInfo) ->    if (selectionChangedHandlers.length > 0)      selectionChangedHandlers.pop().call() @@ -650,7 +636,6 @@ bgLog = (request, sender) ->  # Port handler mapping  portHandlers =    keyDown: handleKeyDown, -  settings: handleSettings,    completions: handleCompletions  sendRequestHandlers = @@ -743,6 +728,4 @@ chrome.windows.getAll { populate: true }, (windows) ->          (response) -> updateScrollPosition(tab, response.scrollX, response.scrollY) if response?        chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler()) -# Start pulling changes from synchronized storage. -Settings.init()  showUpgradeMessage() diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee index f38d6b45..84b8abeb 100644 --- a/content_scripts/hud.coffee +++ b/content_scripts/hud.coffee @@ -48,7 +48,7 @@ HUD =      -> ready and document.body != null    # A preference which can be toggled in the Options page. */ -  enabled: -> !settings.get("hideHud") +  enabled: -> !Settings.get("hideHud")  class Tween    opacity: 0 diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 3cebac4c..2bcc7508 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -26,20 +26,15 @@ LinkHints =    linkActivator: undefined    # While in delayMode, all keypresses have no effect.    delayMode: false -  # Handle the link hinting marker generation and matching. Must be initialized after settings have been +  # Handle the link hinting marker generation and matching. Must be initialized after Settings have been    # loaded, so that we can retrieve the option setting.    getMarkerMatcher: -> -    if settings.get("filterLinkHints") then filterHints else alphabetHints +    if Settings.get("filterLinkHints") then filterHints else alphabetHints    # lock to ensure only one instance runs at a time    isActive: false    # Call this function on exit (if defined).    onExit: null -  # -  # To be called after linkHints has been generated from linkHintsBase. -  # -  init: -> -    # We need this as a top-level function because our command system doesn't yet support arguments.    activateModeToOpenInNewTab: -> @activateMode(OPEN_IN_NEW_BG_TAB)    activateModeToOpenInNewForegroundTab: -> @activateMode(OPEN_IN_NEW_FG_TAB) @@ -60,7 +55,7 @@ LinkHints =      # For these modes, we filter out those elements which don't have an HREF (since there's nothing we can do      # with them).      elements = (el for el in elements when el.element.href?) if mode in [ COPY_LINK_URL, OPEN_INCOGNITO ] -    if settings.get "filterLinkHints" +    if Settings.get "filterLinkHints"        # When using text filtering, we sort the elements such that we visit descendants before their ancestors.        # This allows us to exclude the text used for matching descendants from that used for matching their        # ancestors. @@ -389,7 +384,7 @@ alphabetHints =    # may be of different lengths.    #    hintStrings: (linkCount) -> -    linkHintCharacters = settings.get("linkHintCharacters") +    linkHintCharacters = Settings.get("linkHintCharacters")      # Determine how many digits the link hints will require in the worst case. Usually we do not need      # all of these digits for every link single hint, so we can show shorter hints for a few of the links.      digitsNeeded = Math.ceil(@logXOfBase(linkCount, linkHintCharacters.length)) @@ -460,7 +455,7 @@ filterHints =          @labelMap[forElement] = labelText    generateHintString: (linkHintNumber) -> -    (numberToHintString linkHintNumber + 1, settings.get "linkHintNumbers").toUpperCase() +    (numberToHintString linkHintNumber + 1, Settings.get "linkHintNumbers").toUpperCase()    generateLinkText: (element) ->      linkText = "" @@ -519,7 +514,7 @@ filterHints =        if (!@hintKeystrokeQueue.pop() && !@linkTextKeystrokeQueue.pop())          return { linksMatched: [] }      else if (keyChar) -      if (settings.get("linkHintNumbers").indexOf(keyChar) >= 0) +      if (Settings.get("linkHintNumbers").indexOf(keyChar) >= 0)          @hintKeystrokeQueue.push(keyChar)        else          # since we might renumber the hints, the current hintKeyStrokeQueue diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 27fc9cdc..81c71fcd 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -117,8 +117,7 @@ checkVisibility = (element) ->  # CoreScroller contains the core function (scroll) and logic for relative scrolls.  All scrolls are ultimately  # translated to relative scrolls.  CoreScroller is not exported.  CoreScroller = -  init: (frontendSettings) -> -    @settings = frontendSettings +  init: ->      @time = 0      @lastEvent = null      @keyIsDown = false @@ -139,7 +138,7 @@ CoreScroller =            @time += 1    # Return true if CoreScroller would not initiate a new scroll right now. -  wouldNotInitiateScroll: -> @lastEvent?.repeat and @settings.get "smoothScroll" +  wouldNotInitiateScroll: -> @lastEvent?.repeat and Settings.get "smoothScroll"    # Calibration fudge factors for continuous scrolling.  The calibration value starts at 1.0.  We then    # increase it (until it exceeds @maxCalibration) if we guess that the scroll is too slow, or decrease it @@ -153,7 +152,7 @@ CoreScroller =    scroll: (element, direction, amount, continuous = true) ->      return unless amount -    unless @settings.get "smoothScroll" +    unless Settings.get "smoothScroll"        # Jump scrolling.        performScroll element, direction, amount        checkVisibility element @@ -215,11 +214,11 @@ CoreScroller =  # Scroller contains the two main scroll functions which are used by clients.  Scroller = -  init: (frontendSettings) -> +  init: ->      handlerStack.push        _name: 'scroller/active-element'        DOMActivate: (event) -> handlerStack.alwaysContinueBubbling -> activatedElement = event.target -    CoreScroller.init frontendSettings +    CoreScroller.init()    # scroll the active element in :direction by :amount * :factor.    # :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions. diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index ea1f5930..7ad75514 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -42,62 +42,6 @@ textInputXPath = (->  )()  # -# settings provides a browser-global localStorage-backed dict. get() and set() are synchronous, but load() -# must be called beforehand to ensure get() will return up-to-date values. -# -settings = -  isLoaded: false -  port: null -  eventListeners: {} -  values: -    scrollStepSize: null -    linkHintCharacters: null -    linkHintNumbers: null -    filterLinkHints: null -    hideHud: null -    previousPatterns: null -    nextPatterns: null -    regexFindMode: null -    userDefinedLinkHintCss: null -    helpDialog_showAdvancedCommands: null -    smoothScroll: null -    grabBackFocus: null -    searchEngines: null - -  init: -> -    @port = chrome.runtime.connect name: "settings" -    @port.onMessage.addListener (response) => @receiveMessage response - -    # If the port is closed, the background page has gone away (since we never close it ourselves). Stub the -    # settings object so we don't keep trying to connect to the extension even though it's gone away. -    @port.onDisconnect.addListener => -      @port = null -      for own property, value of this -        # @get doesn't depend on @port, so we can continue to support it to try and reduce errors. -        @[property] = (->) if "function" == typeof value and property != "get" - -  get: (key) -> @values[key] - -  set: (key, value) -> -    @init() unless @port - -    @values[key] = value -    @port.postMessage operation: "set", key: key, value: value - -  load: -> -    @init() unless @port -    @port.postMessage operation: "fetch", values: @values - -  receiveMessage: (response) -> -    @values = response.values if response.values? -    @values[response.key] = response.value if response.key? and response.value? -    @isLoaded = true -    listener() while listener = @eventListeners.load?.pop() - -  addEventListener: (eventName, callback) -> -    (@eventListeners[eventName] ||= []).push callback - -#  # Give this frame a unique (non-zero) id.  #  frameId = 1 + Math.floor(Math.random()*999999999) @@ -119,15 +63,15 @@ class GrabBackFocus extends Mode        _name: "grab-back-focus-mousedown"        mousedown: => @alwaysContinueBubbling => @exit() -    activate = => -      return @exit() unless settings.get "grabBackFocus" -      @push -        _name: "grab-back-focus-focus" -        focus: (event) => @grabBackFocus event.target -      # An input may already be focused. If so, grab back the focus. -      @grabBackFocus document.activeElement if document.activeElement - -    if settings.isLoaded then activate() else settings.addEventListener "load", activate +    Settings.use "grabBackFocus", (grabBackFocus) => +      if grabBackFocus +        @push +          _name: "grab-back-focus-focus" +          focus: (event) => @grabBackFocus event.target +        # An input may already be focused. If so, grab back the focus. +        @grabBackFocus document.activeElement if document.activeElement +      else +        @exit()    grabBackFocus: (element) ->      return @continueBubbling unless DomUtils.isEditable element @@ -176,15 +120,12 @@ window.initializeModes = ->    new NormalMode    new PassKeysMode    new InsertMode permanent: true -  Scroller.init settings +  Scroller.init()  #  # Complete initialization work that sould be done prior to DOMReady.  #  initializePreDomReady = -> -  settings.addEventListener("load", LinkHints.init.bind(LinkHints)) -  settings.load() -    initializeModes()    checkIfEnabledForUrl()    refreshCompletionKeys() @@ -261,13 +202,11 @@ window.installListeners = ->  #  # Whenever we get the focus: -# - Reload settings (they may have changed).  # - Tell the background page this frame's URL.  # - Check if we should be enabled.  #  onFocus = (event) ->    if event.target == window -    settings.load()      chrome.runtime.sendMessage handler: "frameFocused", frameId: frameId      checkIfEnabledForUrl true @@ -364,14 +303,14 @@ extend window,    scrollToTop: -> Scroller.scrollTo "y", 0    scrollToLeft: -> Scroller.scrollTo "x", 0    scrollToRight: -> Scroller.scrollTo "x", "max" -  scrollUp: -> Scroller.scrollBy "y", -1 * settings.get("scrollStepSize") -  scrollDown: -> Scroller.scrollBy "y", settings.get("scrollStepSize") +  scrollUp: -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") +  scrollDown: -> Scroller.scrollBy "y", Settings.get("scrollStepSize")    scrollPageUp: -> Scroller.scrollBy "y", "viewSize", -1/2    scrollPageDown: -> Scroller.scrollBy "y", "viewSize", 1/2    scrollFullPageUp: -> Scroller.scrollBy "y", "viewSize", -1    scrollFullPageDown: -> Scroller.scrollBy "y", "viewSize" -  scrollLeft: -> Scroller.scrollBy "x", -1 * settings.get("scrollStepSize") -  scrollRight: -> Scroller.scrollBy "x", settings.get("scrollStepSize") +  scrollLeft: -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") +  scrollRight: -> Scroller.scrollBy "x", Settings.get("scrollStepSize")  extend window,    reload: -> window.location.reload() @@ -717,7 +656,7 @@ updateFindModeQuery = ->    # the query can be treated differently (e.g. as a plain string versus regex depending on the presence of    # escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal    # character. here we grep for the relevant escape sequences. -  findModeQuery.isRegex = settings.get 'regexFindMode' +  findModeQuery.isRegex = Settings.get 'regexFindMode'    hasNoIgnoreCaseFlag = false    findModeQuery.parsedQuery = findModeQuery.rawQuery.replace /(\\{1,2})([rRI]?)/g, (match, slashes, flag) ->      return match if flag == "" or slashes.length != 1 @@ -1029,12 +968,12 @@ findAndFollowRel = (value) ->          return true  window.goPrevious = -> -  previousPatterns = settings.get("previousPatterns") || "" +  previousPatterns = Settings.get("previousPatterns") || ""    previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length )    findAndFollowRel("prev") || findAndFollowLink(previousStrings)  window.goNext = -> -  nextPatterns = settings.get("nextPatterns") || "" +  nextPatterns = Settings.get("nextPatterns") || ""    nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length )    findAndFollowRel("next") || findAndFollowLink(nextStrings) @@ -1089,7 +1028,7 @@ window.showHelpDialog = (html, fid) ->    VimiumHelpDialog =      # This setting is pulled out of local storage. It's false by default. -    getShowAdvancedCommands: -> settings.get("helpDialog_showAdvancedCommands") +    getShowAdvancedCommands: -> Settings.get("helpDialog_showAdvancedCommands")      init: () ->        this.dialogElement = document.getElementById("vimiumHelpDialog") @@ -1105,7 +1044,7 @@ window.showHelpDialog = (html, fid) ->        event.preventDefault()        showAdvanced = VimiumHelpDialog.getShowAdvancedCommands()        VimiumHelpDialog.showAdvancedCommands(!showAdvanced) -      settings.set("helpDialog_showAdvancedCommands", !showAdvanced) +      Settings.set("helpDialog_showAdvancedCommands", !showAdvanced)      showAdvancedCommands: (visible) ->        VimiumHelpDialog.dialogElement.getElementsByClassName("toggleAdvancedCommands")[0].innerHTML = @@ -1184,7 +1123,6 @@ window.onbeforeunload = ->      scrollY: window.scrollY)  root = exports ? window -root.settings = settings  root.handlerStack = handlerStack  root.frameId = frameId  root.windowIsFocused = windowIsFocused diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 4bd8e8fd..6c08ce92 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -8,7 +8,7 @@ Vomnibar =    # the form "keyword=X", for direct activation of a custom search engine.    parseRegistryEntry: (registryEntry = { options: [] }, callback = null) ->      options = {} -    searchEngines = settings.get("searchEngines") ? "" +    searchEngines = Settings.get("searchEngines") ? ""      SearchEngines.refreshAndUse searchEngines, (engines) ->        for option in registryEntry.options          [ key, value ] = option.split "=" diff --git a/lib/settings.coffee b/lib/settings.coffee index dd667dbd..4fafa7d3 100644 --- a/lib/settings.coffee +++ b/lib/settings.coffee @@ -1,118 +1,88 @@ -# -# * 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 = +Settings =    storage: chrome.storage.sync -  doNotSync: ["settingsVersion", "previousVersion"] +  cache: {} +  isLoaded: false +  onLoadedCallbacks: [] -  # This is called in main.coffee.    init: -> -    chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area -    @fetchAsync() +    if Utils.isExtensionPage() +      # On extension pages, we use localStorage (or a copy of it) as the cache. +      @cache = if Utils.isBackgroundPage() then localStorage else extend {}, localStorage +      @onLoaded() -  # 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 +        @handleUpdateFromChromeStorage key, value for own key, value of items -  # Asynchronous message from synced storage. -  handleStorageUpdate: (changes, area) -> -    for own key, change of changes -      Settings.storeAndPropagate key, change?.newValue if @shouldSyncKey key +      chrome.storage.onChanged.addListener (changes, area) => +        @propagateChangesFromChromeStorage changes if area == "sync" -  # 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 +      @onLoaded() -  # Only called synchronously from within vimium, never on a callback. -  clear: (key) -> -    @storage.remove key if @shouldSyncKey key +  # Called after @cache has been initialized.  On extension pages, this will be called twice, but that does +  # not matter because it's idempotent. +  onLoaded: -> +    @isLoaded = true +    callback() while callback = @onLoadedCallbacks.pop() + +  shouldSyncKey: (key) -> +    (key of @defaults) and key not in [ "settingsVersion", "previousVersion" ] + +  propagateChangesFromChromeStorage: (changes) -> +    @handleUpdateFromChromeStorage key, change?.newValue for own key, change of changes + +  handleUpdateFromChromeStorage: (key, value) -> +    # 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 -  # 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] +    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 (value == @defaults[key]) -      @clear(key) +    # Don't store the value if it is equal to the default, so we can change the defaults in the future. +    # FIXME(smblott).  This test is broken for exclusionRules (for which it is never true).  In this case, we +    # need some kind of structural equality (or perhaps comparison of JSONified strings). +    if value == @defaults[key] +      @clear key      else        jsonValue = JSON.stringify value        @cache[key] = jsonValue -      Sync.set key, jsonValue +      if @shouldSyncKey key +        setting = {}; setting[key] = jsonValue +        @storage.set setting +      @performPostUpdateHook key, value    clear: (key) -> -    if @has key -      delete @cache[key] -    Sync.clear key +    delete @cache[key] if @has key +    @storage.remove key if @shouldSyncKey key +    @performPostUpdateHook key, @get 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: {} +  use: (key, callback) -> +    invokeCallback = => callback @get key +    if @isLoaded then invokeCallback() else @onLoadedCallbacks.push invokeCallback -  # 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 +  # For settings which require action when their value changes, add hooks to this object. +  postUpdateHooks: {} +  performPostUpdateHook: (key, value) -> @postUpdateHooks[key]? value -  # options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans -  # or strings +  # Default values for all settings.    defaults:      scrollStepSize: 60      smoothScroll: true @@ -144,7 +114,7 @@ root.Settings = Settings =      exclusionRules:        [          # Disable Vimium on Gmail. -        { pattern: "http*://mail.google.com/*", passKeys: "" } +        { pattern: "https?://mail.google.com/*", passKeys: "" }        ]      # NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in @@ -159,31 +129,30 @@ root.Settings = Settings =      # 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" +    searchEngines: +      """ +      w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia + +      # More examples. +      # +      # (Vimium supports search completion Wikipedia, as +      # above, and 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 +      # gm: https://www.google.com/maps?q=%s Google maps +      # 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 +      """      newTabUrl: "chrome://newtab"      grabBackFocus: false      settingsVersion: Utils.getCurrentVersion() +    helpDialog_showAdvancedCommands: false -# Export Sync via Settings for tests. -root.Settings.Sync = Sync +Settings.init()  # Perform migration from old settings versions, if this is the background page.  if Utils.isBackgroundPage() @@ -200,3 +169,6 @@ if Utils.isBackgroundPage()      unless chrome.runtime.lastError or items.findModeRawQueryList        rawQuery = Settings.get "findModeRawQuery"        chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else []) + +root = exports ? window +root.Settings = Settings diff --git a/manifest.json b/manifest.json index f0c51117..80eef6c1 100644 --- a/manifest.json +++ b/manifest.json @@ -41,6 +41,7 @@               "lib/rect.js",               "lib/handler_stack.js",               "lib/clipboard.js", +             "lib/settings.js",               "content_scripts/ui_component.js",               "content_scripts/link_hints.js",               "content_scripts/vomnibar.js", diff --git a/pages/options.coffee b/pages/options.coffee index 5521cc55..93b0be11 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -1,8 +1,13 @@  $ = (id) -> document.getElementById id -Settings.init()  bgExclusions = chrome.extension.getBackgroundPage().Exclusions +# We have to use Settings from the background page here (not Settings, directly) to avoid a race condition for +# the page popup.  Specifically, we must ensure that the settings have been updated on the background page +# *before* the popup closes.  This ensures that any exclusion-rule changes are in place before the page +# regains the focus. +bgSettings = chrome.extension.getBackgroundPage().Settings +  #  # Class hierarchy for various types of option.  class Option @@ -21,20 +26,20 @@ class Option    # Fetch a setting from localStorage, remember the @previous value and populate the DOM element.    # Return the fetched value.    fetch: -> -    @populateElement @previous = Settings.get @field +    @populateElement @previous = bgSettings.get @field      @previous    # Write this option's new value back to localStorage, if necessary.    save: ->      value = @readValueFromElement()      if not @areEqual value, @previous -      Settings.set @field, @previous = value +      bgSettings.set @field, @previous = value    # Compare values; this is overridden by sub-classes.    areEqual: (a,b) -> a == b    restoreToDefault: -> -    Settings.clear @field +    bgSettings.clear @field      @fetch()    # Static method. diff --git a/pages/options.html b/pages/options.html index b14c454f..441bd9da 100644 --- a/pages/options.html +++ b/pages/options.html @@ -3,7 +3,6 @@      <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/tests/dom_tests/chrome.coffee b/tests/dom_tests/chrome.coffee index 4c9bfa52..d4e6930d 100644 --- a/tests/dom_tests/chrome.coffee +++ b/tests/dom_tests/chrome.coffee @@ -7,6 +7,9 @@ root.chromeMessages = []  document.hasFocus = -> true +fakeManifest = +  version: "1.51" +  root.chrome =    runtime:      connect: -> @@ -18,16 +21,17 @@ root.chrome =      onMessage:        addListener: ->      sendMessage: (message) -> chromeMessages.unshift message -    getManifest: -> +    getManifest: -> fakeManifest      getURL: (url) -> "../../#{url}"    storage:      local:        get: ->        set: ->      sync: -      get: -> +      get: (_, callback) -> callback? {}        set: ->      onChanged:        addListener: ->    extension:      inIncognitoContext: false +    getURL: (url) -> chrome.runtime.getURL url diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee index 8c2b73c3..8f293075 100644 --- a/tests/dom_tests/dom_tests.coffee +++ b/tests/dom_tests/dom_tests.coffee @@ -34,12 +34,20 @@ initializeModeState = ->    handlerStack.bubbleEvent "registerKeyQueue",      keyQueue: "" +# Tell Settings that it's been loaded. +Settings.isLoaded = true + +# Shoulda.js doesn't support async code, so we try not to use any. +Utils.nextTick = (func) -> func() +  #  # Retrieve the hint markers as an array object.  #  getHintMarkers = ->    Array::slice.call document.getElementsByClassName("vimiumHintMarker"), 0 +stubSettings = (key, value) -> stub Settings.cache, key, JSON.stringify value +  #  # Generate tests that are common to both default and filtered  # link hinting modes. @@ -52,8 +60,8 @@ createGeneralHintTests = (isFilteredMode) ->        initializeModeState()        testContent = "<a>test</a>" + "<a>tress</a>"        document.getElementById("test-div").innerHTML = testContent -      stub settings.values, "filterLinkHints", false -      stub settings.values, "linkHintCharacters", "ab" +      stubSettings "filterLinkHints", false +      stubSettings "linkHintCharacters", "ab"      tearDown ->        document.getElementById("test-div").innerHTML = "" @@ -92,8 +100,8 @@ context "Test link hints for focusing input elements correctly",      testDiv = document.getElementById("test-div")      testDiv.innerHTML = "" -    stub settings.values, "filterLinkHints", false -    stub settings.values, "linkHintCharacters", "ab" +    stubSettings "filterLinkHints", false +    stubSettings "linkHintCharacters", "ab"      # Every HTML5 input type except for hidden. We should be able to activate all of them with link hints.      inputTypes = ["button", "checkbox", "color", "date", "datetime", "datetime-local", "email", "file", @@ -129,12 +137,11 @@ context "Alphabetical link hints",    setup ->      initializeModeState() -    stub settings.values, "filterLinkHints", false -    stub settings.values, "linkHintCharacters", "ab" +    stubSettings "filterLinkHints", false +    stubSettings "linkHintCharacters", "ab"      # Three hints will trigger double hint chars.      createLinks 3 -    LinkHints.init()      LinkHints.activateMode()    tearDown -> @@ -161,8 +168,8 @@ context "Filtered link hints",    # elements.    setup -> -    stub settings.values, "filterLinkHints", true -    stub settings.values, "linkHintNumbers", "0123456789" +    stubSettings "filterLinkHints", true +    stubSettings "linkHintNumbers", "0123456789"    context "Text hints", @@ -170,7 +177,6 @@ context "Filtered link hints",        initializeModeState()        testContent = "<a>test</a>" + "<a>tress</a>" + "<a>trait</a>" + "<a>track<img alt='alt text'/></a>"        document.getElementById("test-div").innerHTML = testContent -      LinkHints.init()        LinkHints.activateMode()      tearDown -> @@ -289,7 +295,7 @@ context "Find prev / next links",      <a href='#first'>nextcorrupted</a>      <a href='#second'>next page</a>      """ -    stub settings.values, "nextPatterns", "next" +    stubSettings "nextPatterns", "next"      goNext()      assert.equal '#second', window.location.hash @@ -297,7 +303,7 @@ context "Find prev / next links",      document.getElementById("test-div").innerHTML = """      <a href='#first'>>></a>      """ -    stub settings.values, "nextPatterns", ">>" +    stubSettings "nextPatterns", ">>"      goNext()      assert.equal '#first', window.location.hash @@ -306,7 +312,7 @@ context "Find prev / next links",      <a href='#first'>lorem ipsum next</a>      <a href='#second'>next!</a>      """ -    stub settings.values, "nextPatterns", "next" +    stubSettings "nextPatterns", "next"      goNext()      assert.equal '#second', window.location.hash diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index 5ccd39e7..f7cc430d 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -35,6 +35,7 @@      <script type="text/javascript" src="../../lib/rect.js"></script>      <script type="text/javascript" src="../../lib/handler_stack.js"></script>      <script type="text/javascript" src="../../lib/clipboard.js"></script> +    <script type="text/javascript" src="../../lib/settings.js"></script>      <script type="text/javascript" src="../../content_scripts/ui_component.js"></script>      <script type="text/javascript" src="../../content_scripts/link_hints.js"></script>      <script type="text/javascript" src="../../content_scripts/vomnibar.js"></script> diff --git a/tests/unit_tests/exclusion_test.coffee b/tests/unit_tests/exclusion_test.coffee index 28c17a2f..0e4b87bc 100644 --- a/tests/unit_tests/exclusion_test.coffee +++ b/tests/unit_tests/exclusion_test.coffee @@ -15,7 +15,6 @@ root.Marks =  extend(global, require "../../lib/utils.js")  Utils.getCurrentVersion = -> '1.44'  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 ded7b5f8..08145190 100644 --- a/tests/unit_tests/settings_test.coffee +++ b/tests/unit_tests/settings_test.coffee @@ -14,7 +14,6 @@ context "settings",      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,16 +38,23 @@ context "settings",      Settings.clear 'scrollStepSize'      assert.equal Settings.get('scrollStepSize'), 60 +context "synced 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. +    should "propagate non-default value via synced storage listener", ->      Settings.set 'scrollStepSize', 20      assert.equal Settings.get('scrollStepSize'), 20 -    Settings.Sync.handleStorageUpdate { scrollStepSize: { newValue: "40" } } +    Settings.propagateChangesFromChromeStorage { 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 -    Settings.Sync.handleStorageUpdate { scrollStepSize: { newValue: "60" } } +    Settings.propagateChangesFromChromeStorage { scrollStepSize: { newValue: "60" } }      assert.isFalse Settings.has 'scrollStepSize'    should "propagate non-default values from synced storage", -> diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee index 16f0e144..fe2fc298 100644 --- a/tests/unit_tests/test_chrome_stubs.coffee +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -70,14 +70,14 @@ exports.chrome =          chrome.runtime.lastError = undefined          key_value = {}          key_value[key] = { newValue: value } -        @func(key_value,'synced storage stub') if @func +        @func(key_value,'sync') if @func        callEmpty: (key) ->          chrome.runtime.lastError = undefined          if @func            items = {}            items[key] = {} -          @func(items,'synced storage stub') +          @func(items,'sync')      session:        MAX_SESSION_RESULTS: 25 diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee index f9ed3636..67c3b333 100644 --- a/tests/unit_tests/utils_test.coffee +++ b/tests/unit_tests/utils_test.coffee @@ -3,7 +3,6 @@ extend global, require "./test_chrome_stubs.js"  extend(global, require "../../lib/utils.js")  Utils.getCurrentVersion = -> '1.43'  extend(global, require "../../lib/settings.js") -Settings.init()  context "isUrl",    should "accept valid URLs", -> | 
