aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md63
-rw-r--r--background_scripts/completion.coffee6
-rw-r--r--background_scripts/completion_search.coffee6
-rw-r--r--background_scripts/main.coffee47
-rw-r--r--background_scripts/marks.coffee131
-rw-r--r--content_scripts/hud.coffee2
-rw-r--r--content_scripts/link_hints.coffee17
-rw-r--r--content_scripts/marks.coffee124
-rw-r--r--content_scripts/mode.coffee14
-rw-r--r--content_scripts/scroller.coffee11
-rw-r--r--content_scripts/vimium_frontend.coffee134
-rw-r--r--content_scripts/vomnibar.coffee2
-rw-r--r--lib/settings.coffee194
-rw-r--r--manifest.json1
-rw-r--r--pages/options.coffee25
-rw-r--r--pages/options.css2
-rw-r--r--pages/options.html1
-rw-r--r--pages/vomnibar.coffee10
-rw-r--r--tests/dom_tests/chrome.coffee8
-rw-r--r--tests/dom_tests/dom_tests.coffee32
-rw-r--r--tests/dom_tests/dom_tests.html1
-rw-r--r--tests/unit_tests/exclusion_test.coffee1
-rw-r--r--tests/unit_tests/settings_test.coffee12
-rw-r--r--tests/unit_tests/test_chrome_stubs.coffee4
-rw-r--r--tests/unit_tests/utils_test.coffee1
25 files changed, 450 insertions, 399 deletions
diff --git a/README.md b/README.md
index 5abe3196..097a0ede 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ Navigating the current page:
F open a link in a new tab
r reload
gs view source
- i enter insert mode -- all commands will be ignored until you hit esc to exit
+ i enter insert mode -- all commands will be ignored until you hit Esc to exit
yy copy the current url to the clipboard
yf copy a link url to the clipboard
gf cycle forward to the next frame
@@ -56,11 +56,13 @@ Navigating to new pages:
Using find:
- / enter find mode -- type your search query and hit enter to search or esc to cancel
- See here for advanced usage (regular expressions): https://github.com/philc/vimium/wiki/Find-Mode
+ / enter find mode
+ -- type your search query and hit enter to search, or Esc to cancel
n cycle forward to the next find match
N cycle backward to the previous find match
+For advanced usage, see [regular expressions](https://github.com/philc/vimium/wiki/Find-Mode) on the wiki.
+
Navigating your history:
H go back in history
@@ -68,21 +70,28 @@ Navigating your history:
Manipulating tabs:
- J, gT go one tab left
- K, gt go one tab right
- g0 go to the first tab
- g$ go to the last tab
- t create tab
- yt duplicate current tab
- x close current tab
- X restore closed tab (i.e. unwind the 'x' command)
- T search through your open tabs
- <a-p> pin/unpin current tab
+ J, gT go one tab left
+ K, gt go one tab right
+ g0 go to the first tab
+ g$ go to the last tab
+ t create tab
+ yt duplicate current tab
+ x close current tab
+ X restore closed tab (i.e. unwind the 'x' command)
+ T search through your open tabs
+ <a-p> pin/unpin current tab
+
+Using marks:
+
+ ma, mA set local mark "a" (global mark "A")
+ `a, `A jump to local mark "a" (global mark "A")
+ `` jump back to the position before the previous jump
+ -- that is, before the previous gg, G, n, N, / or `a
Additional advanced browsing commands:
- ]] Follow the link labeled 'next' or '>'. Helpful for browsing paginated sites.
- [[ Follow the link labeled 'previous' or '<'. Helpful for browsing paginated sites.
+ ]], [[ Follow the link labeled 'next' or '>' ('previous' or '<')
+ - helpful for browsing paginated sites
<a-f> open multiple links in a new tab
gi focus the first (or n-th) text input box on the page
gu go up one level in the URL hierarchy
@@ -92,7 +101,7 @@ Additional advanced browsing commands:
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
+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.
There are some advanced commands which aren't documented here; refer to the help dialog (type `?`) for a full
@@ -101,8 +110,7 @@ list.
Custom Key Mappings
-------------------
-You may remap or unmap any of the default key bindings in the "Key mappings" section under "Advanced Options"
-on the options page.
+You may remap or unmap any of the default key bindings in the "Custom key mappings" on the options page.
Enter one of the following key mapping commands per line:
@@ -119,10 +127,11 @@ Examples:
- `unmap <c-d>` removes any mapping for ctrl+d and restores Chrome's default behavior.
- `unmap r` removes any mapping for the r key.
-Available Vimium commands can be found via the "Show Available Commands" link near the key mapping box. The
-command name appears to the right of the description in parenthesis.
+Available Vimium commands can be found via the "Show available commands" link
+near the key mapping box on the options page. The command name appears to the
+right of the description in parenthesis.
-You can add comments to your key mappings by starting a line with `"` or `#`.
+You can add comments to key mappings by starting a line with `"` or `#`.
The following special keys are available for mapping:
@@ -150,7 +159,13 @@ Release Notes
([here](https://github.com/philc/vimium/wiki/Search-Completion) and
[here](https://github.com/philc/vimium/wiki/Tips-and-Tricks#repeat-recent-queries)).
- A much improved interface for custom search engines.
-- Bug fixes: bookmarklets accessed from the vomnibar.
+- Added <tt>\`\`</tt> to jump back to the previous position after selected
+ jump-like movements (`gg`, `G`, `/`, `n`, `N` and local mark movements).
+- Global marks are now persistent across tab closes and browser sessions, and
+ are synced between browser instances.
+- Bug fixes, including:
+ - Bookmarklets accessed from the vomnibar.
+ - Global marks on non-Windows platforms.
1.51 (2015-05-02)
@@ -373,7 +388,7 @@ does not support command repetition.
- Bug fixes related to entering insert mode when the page first loads, and when focusing Flash embeds.
- Added command listing to the Options page for easy reference.
- `J` & `K` have reversed for tab switching: `J` goes left and `K` goes right.
-- `<c-[>` is now equivalent to ESC, to match the behavior of VIM.
+- `<c-[>` is now equivalent to `Esc`, to match the behavior of VIM.
- `<c-e>` and `<c-y>` are now mapped to scroll down and up respectively.
- The characters used for link hints are now configurable under Advanced Options.
@@ -383,7 +398,7 @@ does not support command repetition.
- Command `yy` to yank (copy) the current tab's url to the clipboard.
- Better Linux support.
- Fix for `Shift+F` link hints.
-- `ESC` now clears the keyQueue. So, for example, hitting `g`, `ESC`, `g` will no longer scroll the page.
+- `Esc` now clears the keyQueue. So, for example, hitting `g`, `Esc`, `g` will no longer scroll the page.
1.1 (2010-01-03)
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index fc6263ee..189929b4 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -166,10 +166,14 @@ class Suggestion
stripPatterns: [
# Google search specific replacements; this replaces query parameters which are known to not be helpful.
# There's some additional information here: http://www.teknoids.net/content/google-search-parameters-2012
- [ "^https?://www\.google\.(com|ca|com\.au|co\.uk|ie)/.*[&?]q="
+ [ "^https?://www\\.google\\.(com|ca|com\\.au|co\\.uk|ie)/.*[&?]q="
"ei gws_rd url ved usg sa usg sig2 bih biw cd aqs ie sourceid es_sm"
.split(/\s+/).map (param) -> new RegExp "\&#{param}=[^&]+" ]
+ # On Google maps, we get a new history entry for every pan and zoom event.
+ [ "^https?://www\\.google\\.(com|ca|com\\.au|co\\.uk|ie)/maps/place/.*/@"
+ [ new RegExp "/@.*" ] ]
+
# General replacements; replaces leading and trailing fluff.
[ '.', [ "^https?://", "\\W+$" ].map (re) -> new RegExp re ]
]
diff --git a/background_scripts/completion_search.coffee b/background_scripts/completion_search.coffee
index 09261ff6..7926b45b 100644
--- a/background_scripts/completion_search.coffee
+++ b/background_scripts/completion_search.coffee
@@ -47,7 +47,7 @@ CompletionSearch =
get: (searchUrl, url, callback) ->
xhr = new XMLHttpRequest()
xhr.open "GET", url, true
- xhr.timeout = 1000
+ xhr.timeout = 2500
xhr.ontimeout = xhr.onerror = -> callback null
xhr.send()
@@ -146,8 +146,8 @@ CompletionSearch =
console.log "GET", url if @debug
catch
suggestions = []
- # We allow failures to be cached too, but remove them after just thirty minutes.
- Utils.setTimeout 30 * 60 * 1000, => @completionCache.set completionCacheKey, null
+ # We allow failures to be cached too, but remove them after just thirty seconds.
+ Utils.setTimeout 30 * 1000, => @completionCache.set completionCacheKey, null
console.log "fail", url if @debug
callback suggestions
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 99a5672b..835b8a9a 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -35,7 +35,8 @@ namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/
# Event handlers
selectionChangedHandlers = []
-tabLoadedHandlers = {} # tabId -> function()
+# Note. tabLoadedHandlers handlers is exported for use also by "marks.coffee".
+root.tabLoadedHandlers = {} # tabId -> function()
# A secret, available only within the current instantiation of Vimium. The secret is big, likely unguessable
# in practice, but less than 2^31.
@@ -149,17 +150,26 @@ helpDialogHtmlForCommandGroup = (group, commandsToKey, availableCommands,
bindings = (commandsToKey[command] || [""]).join(", ")
if (showUnboundCommands || commandsToKey[command])
isAdvanced = Commands.advancedCommands.indexOf(command) >= 0
- html.push(
- "<tr class='vimiumReset #{"advanced" if isAdvanced}'>",
- "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>",
- "<td class='vimiumReset'>:</td><td class='vimiumReset'>", availableCommands[command].description)
-
- if (showCommandNames)
- html.push("<span class='vimiumReset commandName'>(#{command})</span>")
-
- html.push("</td></tr>")
+ description = availableCommands[command].description
+ if bindings.length < 12
+ helpDialogHtmlForCommand html, isAdvanced, bindings, description, showCommandNames, command
+ else
+ # If the length of the bindings is too long, then we display the bindings on a separate row from the
+ # description. This prevents the column alignment from becoming out of whack.
+ helpDialogHtmlForCommand html, isAdvanced, bindings, "", false, ""
+ helpDialogHtmlForCommand html, isAdvanced, "", description, showCommandNames, command
html.join("\n")
+helpDialogHtmlForCommand = (html, isAdvanced, bindings, description, showCommandNames, command) ->
+ html.push "<tr class='vimiumReset #{"advanced" if isAdvanced}'>"
+ if description
+ html.push "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>"
+ html.push "<td class='vimiumReset'>#{if description and bindings then ':' else ''}</td><td class='vimiumReset'>", description
+ html.push("<span class='vimiumReset commandName'>(#{command})</span>") if showCommandNames
+ else
+ html.push "<td class='vimiumReset' colspan='3' style='text-align: left;'>", Utils.escapeHtml(bindings)
+ html.push("</td></tr>")
+
#
# Fetches the contents of a file bundled with this extension.
#
@@ -217,20 +227,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 +646,6 @@ bgLog = (request, sender) ->
# Port handler mapping
portHandlers =
keyDown: handleKeyDown,
- settings: handleSettings,
completions: handleCompletions
sendRequestHandlers =
@@ -743,6 +738,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/background_scripts/marks.coffee b/background_scripts/marks.coffee
index 15d41205..6e5f08ba 100644
--- a/background_scripts/marks.coffee
+++ b/background_scripts/marks.coffee
@@ -1,34 +1,97 @@
-root = window.Marks = {}
-
-marks = {}
-
-root.create = (req, sender) ->
- marks[req.markName] =
- tabId: sender.tab.id
- scrollX: req.scrollX
- scrollY: req.scrollY
-
-chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) ->
- if changeInfo.url?
- removeMarksForTab tabId
-
-chrome.tabs.onRemoved.addListener (tabId, removeInfo) ->
- # XXX(jez): what about restored tabs?
- removeMarksForTab tabId
-
-removeMarksForTab = (id) ->
- for markName, mark of marks
- if mark.tabId is id
- delete marks[markName]
-
-root.goto = (req, sender) ->
- mark = marks[req.markName]
- chrome.tabs.update mark.tabId, selected: true
- chrome.tabs.sendMessage mark.tabId,
- name: "setScrollPosition"
- scrollX: mark.scrollX
- scrollY: mark.scrollY
- chrome.tabs.sendMessage mark.tabId,
- name: "showHUDforDuration",
- text: "Jumped to global mark '#{req.markName}'"
- duration: 1000
+
+Marks =
+ # This returns the key which is used for storing mark locations in chrome.storage.sync.
+ getLocationKey: (markName) -> "vimiumGlobalMark|#{markName}"
+
+ # Get the part of a URL we use for matching here (that is, everything up to the first anchor).
+ getBaseUrl: (url) -> url.split("#")[0]
+
+ # Create a global mark. We record vimiumSecret with the mark so that we can tell later, when the mark is
+ # used, whether this is the original Vimium session or a subsequent session. This affects whether or not
+ # tabId can be considered valid.
+ create: (req, sender) ->
+ chrome.storage.local.get "vimiumSecret", (items) =>
+ markInfo =
+ vimiumSecret: items.vimiumSecret
+ markName: req.markName
+ url: @getBaseUrl sender.tab.url
+ tabId: sender.tab.id
+ scrollX: req.scrollX
+ scrollY: req.scrollY
+
+ if markInfo.scrollX? and markInfo.scrollY?
+ @saveMark markInfo
+ else
+ # The front-end frame hasn't provided the scroll position (because it's not the top frame within its
+ # tab). We need to ask the top frame what its scroll position is. (With the frame Id set to 0, below,
+ # the request will only be handled by the top frame within the tab.)
+ chrome.tabs.sendMessage sender.tab.id, name: "getScrollPosition", frameId: 0, (response) =>
+ @saveMark extend markInfo, scrollX: response.scrollX, scrollY: response.scrollY
+
+ saveMark: (markInfo) ->
+ item = {}
+ item[@getLocationKey markInfo.markName] = markInfo
+ chrome.storage.sync.set item
+
+ # Goto a global mark. We try to find the original tab. If we can't find that, then we try to find another
+ # tab with the original URL, and use that. And if we can't find such an existing tab, then we create a new
+ # one. Whichever of those we do, we then set the scroll position to the original scroll position.
+ goto: (req, sender) ->
+ chrome.storage.local.get "vimiumSecret", (items) =>
+ vimiumSecret = items.vimiumSecret
+ key = @getLocationKey req.markName
+ chrome.storage.sync.get key, (items) =>
+ markInfo = items[key]
+ if not markInfo
+ # The mark is not defined.
+ chrome.tabs.sendMessage sender.tab.id,
+ name: "showHUDforDuration",
+ text: "Global mark not set: '#{req.markName}'."
+ duration: 1000
+ else if markInfo.vimiumSecret != vimiumSecret
+ # This is a different Vimium instantiation, so markInfo.tabId is definitely out of date.
+ @focusOrLaunch markInfo
+ else
+ # Check whether markInfo.tabId still exists. According to here (https://developer.chrome.com/extensions/tabs),
+ # tab Ids are unqiue within a Chrome session. So, if we find a match, we can use it.
+ chrome.tabs.get markInfo.tabId, (tab) =>
+ if not chrome.runtime.lastError and tab?.url and markInfo.url == @getBaseUrl tab.url
+ # The original tab still exists.
+ @gotoPositionInTab markInfo
+ else
+ # The original tab no longer exists.
+ @focusOrLaunch markInfo
+
+ # Focus an existing tab and scroll to the given position within it.
+ gotoPositionInTab: ({ tabId, scrollX, scrollY, markName }) ->
+ chrome.tabs.update tabId, { selected: true }, ->
+ chrome.tabs.sendMessage tabId,
+ { name: "setScrollPosition", scrollX: scrollX, scrollY: scrollY }, ->
+ chrome.tabs.sendMessage tabId,
+ name: "showHUDforDuration",
+ text: "Jumped to global mark '#{markName}'."
+ duration: 1000
+
+ # The tab we're trying to find no longer exists. We either find another tab with a matching URL and use it,
+ # or we create a new tab.
+ focusOrLaunch: (markInfo) ->
+ chrome.tabs.query { url: markInfo.url }, (tabs) =>
+ if 0 < tabs.length
+ # We have a matching tab: use it (prefering, if there are more than one, one in the current window).
+ @pickTabInWindow tabs, (tab) =>
+ @gotoPositionInTab extend markInfo, tabId: tab.id
+ else
+ # There is no existing matching tab, we'll have to create one.
+ chrome.tabs.create { url: @getBaseUrl markInfo.url }, (tab) =>
+ # Note. tabLoadedHandlers is defined in "main.coffee". The handler below will be called when the tab
+ # is loaded, its DOM is ready and it registers with the background page.
+ tabLoadedHandlers[tab.id] = => @gotoPositionInTab extend markInfo, tabId: tab.id
+
+ # Given a list of tabs, pick one in the current window, if possible, otherwise just pick any.
+ pickTabInWindow: (tabs, continuation) ->
+ chrome.windows.getCurrent ({ id }) ->
+ tabsInWindow = tabs.filter (tab) -> tab.windowId == id
+ continuation tabsInWindow[0] ? tabs[0]
+
+root = exports ? window
+root.Marks = Marks
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/marks.coffee b/content_scripts/marks.coffee
index 316ab951..067d05a8 100644
--- a/content_scripts/marks.coffee
+++ b/content_scripts/marks.coffee
@@ -1,45 +1,79 @@
-root = window.Marks = {}
-
-root.activateCreateMode = ->
- handlerStack.push keydown: (e) ->
- keyChar = KeyboardUtils.getKeyChar(event)
- return unless keyChar isnt ""
-
- if /[A-Z]/.test keyChar
- chrome.runtime.sendMessage {
- handler: 'createMark',
- markName: keyChar
- scrollX: window.scrollX,
- scrollY: window.scrollY
- }, -> HUD.showForDuration "Created global mark '#{keyChar}'", 1000
- else if /[a-z]/.test keyChar
- [baseLocation, sep, hash] = window.location.href.split '#'
- localStorage["vimiumMark|#{baseLocation}|#{keyChar}"] = JSON.stringify
- scrollX: window.scrollX,
- scrollY: window.scrollY
- HUD.showForDuration "Created local mark '#{keyChar}'", 1000
-
- @remove()
-
- false
-
-root.activateGotoMode = ->
- handlerStack.push keydown: (e) ->
- keyChar = KeyboardUtils.getKeyChar(event)
- return unless keyChar isnt ""
-
- if /[A-Z]/.test keyChar
- chrome.runtime.sendMessage
- handler: 'gotoMark'
- markName: keyChar
- else if /[a-z]/.test keyChar
- [baseLocation, sep, hash] = window.location.href.split '#'
- markString = localStorage["vimiumMark|#{baseLocation}|#{keyChar}"]
- if markString?
- mark = JSON.parse markString
- window.scrollTo mark.scrollX, mark.scrollY
- HUD.showForDuration "Jumped to local mark '#{keyChar}'", 1000
-
- @remove()
-
- false
+
+Marks =
+ previousPositionRegisters: [ "`", "'" ]
+ localRegisters: {}
+ mode: null
+
+ exit: (continuation = null) ->
+ @mode?.exit()
+ @mode = null
+ continuation?()
+
+ # This returns the key which is used for storing mark locations in localStorage.
+ getLocationKey: (keyChar) ->
+ "vimiumMark|#{window.location.href.split('#')[0]}|#{keyChar}"
+
+ getMarkString: ->
+ JSON.stringify scrollX: window.scrollX, scrollY: window.scrollY
+
+ setPreviousPosition: ->
+ markString = @getMarkString()
+ @localRegisters[reg] = markString for reg in @previousPositionRegisters
+
+ showMessage: (message, keyChar) ->
+ HUD.showForDuration "#{message} \"#{keyChar}\".", 1000
+
+ # If <Shift> is depressed, then it's a global mark, otherwise it's a local mark. This is consistent
+ # vim's [A-Z] for global marks and [a-z] for local marks. However, it also admits other non-Latin
+ # characters. The exceptions are "`" and "'", which are always considered local marks.
+ isGlobalMark: (event, keyChar) ->
+ event.shiftKey and keyChar not in @previousPositionRegisters
+
+ activateCreateMode: ->
+ @mode = new Mode
+ name: "create-mark"
+ indicator: "Create mark..."
+ exitOnEscape: true
+ suppressAllKeyboardEvents: true
+ keypress: (event) =>
+ keyChar = String.fromCharCode event.charCode
+ @exit =>
+ if @isGlobalMark event, keyChar
+ # We record the current scroll position, but only if this is the top frame within the tab.
+ # Otherwise, we'll fetch the scroll position of the top frame from the background page later.
+ [ scrollX, scrollY ] = [ window.scrollX, window.scrollY ] if DomUtils.isTopFrame()
+ chrome.runtime.sendMessage
+ handler: 'createMark'
+ markName: keyChar
+ scrollX: scrollX
+ scrollY: scrollY
+ , => @showMessage "Created global mark", keyChar
+ else
+ localStorage[@getLocationKey keyChar] = @getMarkString()
+ @showMessage "Created local mark", keyChar
+
+ activateGotoMode: (registryEntry) ->
+ @mode = new Mode
+ name: "goto-mark"
+ indicator: "Go to mark..."
+ exitOnEscape: true
+ suppressAllKeyboardEvents: true
+ keypress: (event) =>
+ @exit =>
+ keyChar = String.fromCharCode event.charCode
+ if @isGlobalMark event, keyChar
+ chrome.runtime.sendMessage
+ handler: 'gotoMark'
+ markName: keyChar
+ else
+ markString = @localRegisters[keyChar] ? localStorage[@getLocationKey keyChar]
+ if markString?
+ @setPreviousPosition()
+ position = JSON.parse markString
+ window.scrollTo position.scrollX, position.scrollY
+ @showMessage "Jumped to local mark", keyChar
+ else
+ @showMessage "Local mark not set", keyChar
+
+root = exports ? window
+root.Marks = Marks
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index f631b4cd..ffabc111 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -47,6 +47,13 @@ class Mode
@id = "#{@name}-#{@count}"
@log "activate:", @id
+ # If options.suppressAllKeyboardEvents is truthy, then all keyboard events are suppressed. This avoids
+ # the need for modes which suppress all keyboard events 1) to provide handlers for all of those events,
+ # or 2) to worry about event suppression and event-handler return values.
+ if @options.suppressAllKeyboardEvents
+ for type in [ "keydown", "keypress", "keyup" ]
+ @options[type] = @alwaysSuppressEvent @options[type]
+
@push
keydown: @options.keydown || null
keypress: @options.keypress || null
@@ -171,6 +178,13 @@ class Mode
# case), because they do not need to be concerned with the value they yield.
alwaysContinueBubbling: handlerStack.alwaysContinueBubbling
+ # Shorthand for an event handler which always suppresses event propagation.
+ alwaysSuppressEvent: (handler = null) ->
+ (event) =>
+ handler? event
+ DomUtils.suppressPropagation event
+ @stopBubblingAndFalse
+
# Activate a new instance of this mode, together with all of its original options (except its main
# keybaord-event handlers; these will be recreated).
cloneMode: ->
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 c8c83029..3055ecea 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()
@@ -201,12 +142,12 @@ initializePreDomReady = ->
window.removeEventListener "focus", onFocus
requestHandlers =
- showHUDforDuration: (request) -> HUD.showForDuration request.text, request.duration
+ showHUDforDuration: handleShowHUDforDuration
toggleHelpDialog: (request) -> toggleHelpDialog(request.dialogHtml, request.frameId)
focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame request
refreshCompletionKeys: refreshCompletionKeys
getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY
- setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY
+ setScrollPosition: setScrollPosition
executePageCommand: executePageCommand
currentKeyQueue: (request) ->
keyQueue = request.keyQueue
@@ -220,7 +161,7 @@ initializePreDomReady = ->
# from the former.
return if sender.tab and not sender.tab.url.startsWith 'chrome-extension://'
# These requests are delivered to the options page, but there are no handlers there.
- return if request.handler in [ "registerFrame", "frameFocused", "unregisterFrame" ]
+ return if request.handler in [ "registerFrame", "frameFocused", "unregisterFrame", "setIcon" ]
shouldHandleRequest = isEnabledForUrl
# We always handle the message if it's one of these listed message types.
shouldHandleRequest ||= request.name in [ "executePageCommand", "checkEnabledAfterURLChange" ]
@@ -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
@@ -302,9 +241,10 @@ unregisterFrame = ->
tab_is_closing: DomUtils.isTopFrame()
executePageCommand = (request) ->
+ commandType = request.command.split(".")[0]
# Vomnibar commands are handled in the tab's main/top frame. They are handled even if Vimium is otherwise
# disabled in the frame.
- if request.command.split(".")[0] == "Vomnibar"
+ if commandType == "Vomnibar"
if DomUtils.isTopFrame()
# We pass the frameId from request. That's the frame which originated the request, so that's the frame
# which should receive the focus when the vomnibar closes.
@@ -322,9 +262,18 @@ executePageCommand = (request) ->
refreshCompletionKeys(request)
-setScrollPosition = (scrollX, scrollY) ->
- if (scrollX > 0 || scrollY > 0)
- DomUtils.documentReady(-> window.scrollTo(scrollX, scrollY))
+handleShowHUDforDuration = ({ text, duration }) ->
+ if DomUtils.isTopFrame()
+ DomUtils.documentReady -> HUD.showForDuration text, duration
+
+setScrollPosition = ({ scrollX, scrollY }) ->
+ if DomUtils.isTopFrame()
+ DomUtils.documentReady ->
+ window.focus()
+ document.body.focus()
+ if 0 < scrollX or 0 < scrollY
+ Marks.setPreviousPosition()
+ window.scrollTo scrollX, scrollY
#
# Called from the backend in order to change frame focus.
@@ -360,18 +309,22 @@ window.focusThisFrame = do ->
setTimeout (-> highlightedFrameElement.remove()), 200
extend window,
- scrollToBottom: -> Scroller.scrollTo "y", "max"
- scrollToTop: -> Scroller.scrollTo "y", 0
+ scrollToBottom: ->
+ Marks.setPreviousPosition()
+ Scroller.scrollTo "y", "max"
+ scrollToTop: ->
+ Marks.setPreviousPosition()
+ 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 +670,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
@@ -922,6 +875,7 @@ window.getFindModeQuery = (backwards) ->
findModeQuery.parsedQuery
findAndFocus = (backwards) ->
+ Marks.setPreviousPosition()
query = getFindModeQuery backwards
findModeQueryHasResults =
@@ -1029,12 +983,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)
@@ -1068,6 +1022,7 @@ findModeRestoreSelection = (range = findModeInitialRange) ->
# Enters find mode. Returns the new find-mode instance.
window.enterFindMode = (options = {}) ->
+ Marks.setPreviousPosition()
# Save the selection, so performFindInPlace can restore it.
findModeSaveSelection()
findModeQuery = rawQuery: ""
@@ -1089,7 +1044,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 +1060,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 +1139,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..ca4e77b0 100644
--- a/lib/settings.coffee
+++ b/lib/settings.coffee
@@ -1,118 +1,86 @@
-#
-# * 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.
+ if JSON.stringify(value) == JSON.stringify @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 +112,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 +127,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 +167,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 6c85911f..80aca4c5 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 c8c21850..ea4301a9 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,17 @@ 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
-
- # Compare values; this is overridden by sub-classes.
- areEqual: (a,b) -> a == b
+ if JSON.stringify(value) != JSON.stringify @previous
+ bgSettings.set @field, @previous = value
restoreToDefault: ->
- Settings.clear @field
+ bgSettings.clear @field
@fetch()
# Static method.
@@ -120,12 +122,6 @@ class ExclusionRulesOption extends Option
passKeys: @getPassKeys(element).value.trim()
rules.filter (rule) -> rule.pattern
- areEqual: (a,b) ->
- # Flatten each list of rules to a newline-separated string representation, and then use string equality.
- # This is correct because patterns and passKeys cannot themselves contain newlines.
- flatten = (rule) -> if rule and rule.pattern then rule.pattern + "\n" + rule.passKeys else ""
- a.map(flatten).join("\n") == b.map(flatten).join("\n")
-
# Accessors for the three main sub-elements of an "exclusionRuleTemplateInstance".
getPattern: (element) -> element.querySelector(".pattern")
getPassKeys: (element) -> element.querySelector(".passKeys")
@@ -258,7 +254,6 @@ initOptionsPage = ->
searchEngines: TextOption
searchUrl: NonEmptyTextOption
userDefinedLinkHintCss: TextOption
- omniSearchWeight: NumberOption
# Populate options. The constructor adds each new object to "Option.all".
for name, type of options
diff --git a/pages/options.css b/pages/options.css
index 282a523b..75bbe159 100644
--- a/pages/options.css
+++ b/pages/options.css
@@ -107,7 +107,7 @@ input#linkHintNumbers {
input#linkHintCharacters {
width: 100%;
}
-input#scrollStepSize, input#omniSearchWeight {
+input#scrollStepSize {
width: 50px;
margin-right: 3px;
padding-left: 3px;
diff --git a/pages/options.html b/pages/options.html
index 7b66efde..12a3ad21 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/pages/vomnibar.coffee b/pages/vomnibar.coffee
index d5659fdc..cf9ed7b0 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -169,14 +169,14 @@ class VomnibarUI
completion = @completions[@selection]
@hide -> completion.performAction openInNewTab
else if action == "delete"
- inputIsEmpty = @input.value.length == 0
- if inputIsEmpty and @customSearchMode?
+ if @customSearchMode? and @input.selectionEnd == 0
# Normally, with custom search engines, the keyword (e,g, the "w" of "w query terms") is suppressed.
- # If the input is empty, then reinstate the keyword (the "w").
- @input.value = @customSearchMode
+ # If the cursor is at the start of the input, then reinstate the keyword (the "w").
+ @input.value = @customSearchMode + @input.value.ltrim()
+ @input.selectionStart = @input.selectionEnd = @customSearchMode.length
@customSearchMode = null
@update true
- else if inputIsEmpty and @seenTabToOpenCompletionList
+ else if @seenTabToOpenCompletionList and @input.value.trim().length == 0
@seenTabToOpenCompletionList = false
@update true
else
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'>&gt;&gt;</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", ->