aboutsummaryrefslogtreecommitdiffstats
path: root/background_scripts
diff options
context:
space:
mode:
Diffstat (limited to 'background_scripts')
-rw-r--r--background_scripts/commands.coffee185
-rw-r--r--background_scripts/completion.coffee146
-rw-r--r--background_scripts/exclusions.coffee70
-rw-r--r--background_scripts/main.coffee266
-rw-r--r--background_scripts/settings.coffee68
-rw-r--r--background_scripts/sync.coffee102
6 files changed, 661 insertions, 176 deletions
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index ce159c71..0ade35ec 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -22,17 +22,21 @@ Commands =
isBackgroundCommand: options.background
passCountToFunction: options.passCountToFunction
noRepeat: options.noRepeat
+ repeatLimit: options.repeatLimit
mapKeyToCommand: (key, command) ->
unless @availableCommands[command]
console.log(command, "doesn't exist!")
return
+ commandDetails = @availableCommands[command]
+
@keyToCommandRegistry[key] =
command: command
- isBackgroundCommand: @availableCommands[command].isBackgroundCommand
- passCountToFunction: @availableCommands[command].passCountToFunction
- noRepeat: @availableCommands[command].noRepeat
+ isBackgroundCommand: commandDetails.isBackgroundCommand
+ passCountToFunction: commandDetails.passCountToFunction
+ noRepeat: commandDetails.noRepeat
+ repeatLimit: commandDetails.repeatLimit
unmapKey: (key) -> delete @keyToCommandRegistry[key]
@@ -86,21 +90,63 @@ Commands =
# be shown in the help page.
commandGroups:
pageNavigation:
- ["scrollDown", "scrollUp", "scrollLeft", "scrollRight",
- "scrollToTop", "scrollToBottom", "scrollToLeft", "scrollToRight", "scrollPageDown",
- "scrollPageUp", "scrollFullPageUp", "scrollFullPageDown",
- "reload", "toggleViewSource", "copyCurrentUrl", "LinkHints.activateModeToCopyLinkUrl",
- "openCopiedUrlInCurrentTab", "openCopiedUrlInNewTab", "goUp", "goToRoot",
- "enterInsertMode", "focusInput",
- "LinkHints.activateMode", "LinkHints.activateModeToOpenInNewTab", "LinkHints.activateModeWithQueue",
- "Vomnibar.activate", "Vomnibar.activateInNewTab", "Vomnibar.activateTabSelection",
- "Vomnibar.activateBookmarks", "Vomnibar.activateBookmarksInNewTab",
- "goPrevious", "goNext", "nextFrame", "Marks.activateCreateMode", "Marks.activateGotoMode"]
+ ["scrollDown",
+ "scrollUp",
+ "scrollLeft",
+ "scrollRight",
+ "scrollToTop",
+ "scrollToBottom",
+ "scrollToLeft",
+ "scrollToRight",
+ "scrollPageDown",
+ "scrollPageUp",
+ "scrollFullPageUp",
+ "scrollFullPageDown",
+ "reload",
+ "toggleViewSource",
+ "copyCurrentUrl",
+ "LinkHints.activateModeToCopyLinkUrl",
+ "openCopiedUrlInCurrentTab",
+ "openCopiedUrlInNewTab",
+ "goUp",
+ "goToRoot",
+ "enterInsertMode",
+ "focusInput",
+ "LinkHints.activateMode",
+ "LinkHints.activateModeToOpenInNewTab",
+ "LinkHints.activateModeToOpenInNewForegroundTab",
+ "LinkHints.activateModeWithQueue",
+ "LinkHints.activateModeToDownloadLink",
+ "Vomnibar.activate",
+ "Vomnibar.activateInNewTab",
+ "Vomnibar.activateTabSelection",
+ "Vomnibar.activateBookmarks",
+ "Vomnibar.activateBookmarksInNewTab",
+ "goPrevious",
+ "goNext",
+ "nextFrame",
+ "Marks.activateCreateMode",
+ "Vomnibar.activateEditUrl",
+ "Vomnibar.activateEditUrlInNewTab",
+ "Marks.activateGotoMode"]
findCommands: ["enterFindMode", "performFind", "performBackwardsFind"]
historyNavigation:
["goBack", "goForward"]
tabManipulation:
- ["nextTab", "previousTab", "firstTab", "lastTab", "createTab", "duplicateTab", "removeTab", "restoreTab", "moveTabToNewWindow"]
+ ["nextTab",
+ "previousTab",
+ "firstTab",
+ "lastTab",
+ "createTab",
+ "duplicateTab",
+ "removeTab",
+ "restoreTab",
+ "moveTabToNewWindow",
+ "togglePinTab",
+ "closeTabsOnLeft","closeTabsOnRight",
+ "closeOtherTabs",
+ "moveTabLeft",
+ "moveTabRight"]
misc:
["showHelp"]
@@ -108,10 +154,26 @@ Commands =
# a focused, high-signal set of commands to the new and casual user. Only those truly hungry for more power
# from Vimium will uncover these gems.
advancedCommands: [
- "scrollToLeft", "scrollToRight", "moveTabToNewWindow",
- "goUp", "goToRoot", "focusInput", "LinkHints.activateModeWithQueue",
- "LinkHints.activateModeToOpenIncognito", "goNext", "goPrevious", "Marks.activateCreateMode",
- "Marks.activateGotoMode"]
+ "scrollToLeft",
+ "scrollToRight",
+ "moveTabToNewWindow",
+ "goUp",
+ "goToRoot",
+ "focusInput",
+ "LinkHints.activateModeWithQueue",
+ "LinkHints.activateModeToDownloadLink",
+ "Vomnibar.activateEditUrl",
+ "Vomnibar.activateEditUrlInNewTab",
+ "LinkHints.activateModeToOpenIncognito",
+ "goNext",
+ "goPrevious",
+ "Marks.activateCreateMode",
+ "Marks.activateGotoMode",
+ "moveTabLeft",
+ "moveTabRight",
+ "closeTabsOnLeft",
+ "closeTabsOnRight",
+ "closeOtherTabs"]
defaultKeyMappings =
"?": "showHelp"
@@ -144,6 +206,8 @@ defaultKeyMappings =
"F": "LinkHints.activateModeToOpenInNewTab"
"<a-f>": "LinkHints.activateModeWithQueue"
+ "af": "LinkHints.activateModeToDownloadLink"
+
"/": "enterFindMode"
"n": "performFind"
"N": "performBackwardsFind"
@@ -161,6 +225,8 @@ defaultKeyMappings =
"J": "previousTab"
"gt": "nextTab"
"gT": "previousTab"
+ "<<": "moveTabLeft"
+ ">>": "moveTabRight"
"g0": "firstTab"
"g$": "lastTab"
@@ -170,6 +236,8 @@ defaultKeyMappings =
"x": "removeTab"
"X": "restoreTab"
+ "<a-p>": "togglePinTab"
+
"o": "Vomnibar.activate"
"O": "Vomnibar.activateInNewTab"
@@ -178,6 +246,9 @@ defaultKeyMappings =
"b": "Vomnibar.activateBookmarks"
"B": "Vomnibar.activateBookmarksInNewTab"
+ "ge": "Vomnibar.activateEditUrl"
+ "gE": "Vomnibar.activateEditUrlInNewTab"
+
"gf": "nextFrame"
"m": "Marks.activateCreateMode"
@@ -185,6 +256,7 @@ defaultKeyMappings =
# This is a mapping of: commandIdentifier => [description, options].
+# If the noRepeat and repeatLimit options are both specified, then noRepeat takes precedence.
commandDescriptions =
# Navigating the current page
showHelp: ["Show help", { background: true }]
@@ -192,40 +264,43 @@ commandDescriptions =
scrollUp: ["Scroll up"]
scrollLeft: ["Scroll left"]
scrollRight: ["Scroll right"]
- scrollToTop: ["Scroll to the top of the page"]
- scrollToBottom: ["Scroll to the bottom of the page"]
- scrollToLeft: ["Scroll all the way to the left"]
- scrollToRight: ["Scroll all the way to the right"]
+ scrollToTop: ["Scroll to the top of the page", { noRepeat: true }]
+ scrollToBottom: ["Scroll to the bottom of the page", { noRepeat: true }]
+ scrollToLeft: ["Scroll all the way to the left", { noRepeat: true }]
+ scrollToRight: ["Scroll all the way to the right", { noRepeat: true }]
+
scrollPageDown: ["Scroll a page down"]
scrollPageUp: ["Scroll a page up"]
scrollFullPageDown: ["Scroll a full page down"]
scrollFullPageUp: ["Scroll a full page up"]
- reload: ["Reload the page"]
- toggleViewSource: ["View page source"]
+ reload: ["Reload the page", { noRepeat: true }]
+ toggleViewSource: ["View page source", { noRepeat: true }]
- copyCurrentUrl: ["Copy the current URL to the clipboard"]
- 'LinkHints.activateModeToCopyLinkUrl': ["Copy a link URL to the clipboard"]
+ copyCurrentUrl: ["Copy the current URL to the clipboard", { noRepeat: true }]
+ "LinkHints.activateModeToCopyLinkUrl": ["Copy a link URL to the clipboard", { noRepeat: true }]
openCopiedUrlInCurrentTab: ["Open the clipboard's URL in the current tab", { background: true }]
- openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true }]
+ openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true, repeatLimit: 20 }]
- enterInsertMode: ["Enter insert mode"]
+ enterInsertMode: ["Enter insert mode", { noRepeat: true }]
- focusInput: ["Focus the first (or n-th) text box on the page", { passCountToFunction: true }]
+ focusInput: ["Focus the first text box on the page. Cycle between them using tab",
+ { passCountToFunction: true }]
- 'LinkHints.activateMode': ["Open a link in the current tab"]
- 'LinkHints.activateModeToOpenInNewTab': ["Open a link in a new tab"]
- 'LinkHints.activateModeWithQueue': ["Open multiple links in a new tab"]
+ "LinkHints.activateMode": ["Open a link in the current tab", { noRepeat: true }]
+ "LinkHints.activateModeToOpenInNewTab": ["Open a link in a new tab", { noRepeat: true }]
+ "LinkHints.activateModeToOpenInNewForegroundTab": ["Open a link in a new tab & switch to it", { noRepeat: true }]
+ "LinkHints.activateModeWithQueue": ["Open multiple links in a new tab", { noRepeat: true }]
+ "LinkHints.activateModeToOpenIncognito": ["Open a link in incognito window", { noRepeat: true }]
+ "LinkHints.activateModeToDownloadLink": ["Download link url", { noRepeat: true }]
- "LinkHints.activateModeToOpenIncognito": ["Open a link in incognito window"]
-
- enterFindMode: ["Enter find mode"]
+ enterFindMode: ["Enter find mode", { noRepeat: true }]
performFind: ["Cycle forward to the next find match"]
performBackwardsFind: ["Cycle backward to the previous find match"]
- goPrevious: ["Follow the link labeled previous or <"]
- goNext: ["Follow the link labeled next or >"]
+ goPrevious: ["Follow the link labeled previous or <", { noRepeat: true }]
+ goNext: ["Follow the link labeled next or >", { noRepeat: true }]
# Navigating your history
goBack: ["Go back in history", { passCountToFunction: true }]
@@ -240,22 +315,36 @@ commandDescriptions =
previousTab: ["Go one tab left", { background: true }]
firstTab: ["Go to the first tab", { background: true }]
lastTab: ["Go to the last tab", { background: true }]
- createTab: ["Create new tab", { background: true }]
- duplicateTab: ["Duplicate current tab", { background: true }]
- removeTab: ["Close current tab", { background: true, noRepeat: true }]
- restoreTab: ["Restore closed tab", { background: true }]
+
+ createTab: ["Create new tab", { background: true, repeatLimit: 20 }]
+ duplicateTab: ["Duplicate current tab", { background: true, repeatLimit: 20 }]
+ removeTab: ["Close current tab", { background: true, repeatLimit:
+ # Require confirmation to remove more tabs than we can restore.
+ (if chrome.session then chrome.session.MAX_SESSION_RESULTS else 25) }]
+ restoreTab: ["Restore closed tab", { background: true, repeatLimit: 20 }]
+
moveTabToNewWindow: ["Move tab to new window", { background: true }]
+ togglePinTab: ["Pin/unpin current tab", { background: true }]
+
+ closeTabsOnLeft: ["Close tabs on the left", {background: true, noRepeat: true}]
+ closeTabsOnRight: ["Close tabs on the right", {background: true, noRepeat: true}]
+ closeOtherTabs: ["Close all other tabs", {background: true, noRepeat: true}]
+
+ moveTabLeft: ["Move tab to the left", { background: true, passCountToFunction: true }]
+ moveTabRight: ["Move tab to the right", { background: true, passCountToFunction: true }]
- "Vomnibar.activate": ["Open URL, bookmark, or history entry"]
- "Vomnibar.activateInNewTab": ["Open URL, bookmark, history entry, in a new tab"]
- "Vomnibar.activateTabSelection": ["Search through your open tabs"]
- "Vomnibar.activateBookmarks": ["Open a bookmark"]
- "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab"]
+ "Vomnibar.activate": ["Open URL, bookmark, or history entry", { noRepeat: true }]
+ "Vomnibar.activateInNewTab": ["Open URL, bookmark, history entry, in a new tab", { noRepeat: true }]
+ "Vomnibar.activateTabSelection": ["Search through your open tabs", { noRepeat: true }]
+ "Vomnibar.activateBookmarks": ["Open a bookmark", { noRepeat: true }]
+ "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab", { noRepeat: true }]
+ "Vomnibar.activateEditUrl": ["Edit the current URL", { noRepeat: true }]
+ "Vomnibar.activateEditUrlInNewTab": ["Edit the current URL and open in a new tab", { noRepeat: true }]
nextFrame: ["Cycle forward to the next frame on the page", { background: true, passCountToFunction: true }]
- "Marks.activateCreateMode": ["Create a new mark"]
- "Marks.activateGotoMode": ["Go to a mark"]
+ "Marks.activateCreateMode": ["Create a new mark", { noRepeat: true }]
+ "Marks.activateGotoMode": ["Go to a mark", { noRepeat: true }]
Commands.init()
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index a03a3006..b52d9eb8 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -37,18 +37,20 @@ class Suggestion
</div>
<div class="vimiumReset vomnibarBottomHalf vomnibarIcon"
style="background-image: url(#{favIconUrl});">
- <span class="vimiumReset vomnibarUrl">#{@shortenUrl(@highlightTerms(@url))}</span>
+ <span class="vimiumReset vomnibarUrl">#{@shortenUrl(@highlightTerms(Utils.escapeHtml(@url)))}</span>
#{relevancyHtml}
</div>
"""
- # use neat trick to snatch a domain (http://stackoverflow.com/a/8498668)
+ # Use neat trick to snatch a domain (http://stackoverflow.com/a/8498668).
+ # TODO(smblott) Is this really faster than using parseUri? That's probably what's happening behind the
+ # scenes anyway.
getUrlRoot: (url) ->
a = document.createElement 'a'
a.href = url
a.protocol + "//" + a.hostname
- shortenUrl: (url) -> @stripTrailingSlash(url).replace(/^http:\/\//, "")
+ shortenUrl: (url) -> @stripTrailingSlash(url).replace(/^https?:\/\//, "")
stripTrailingSlash: (url) ->
url = url.substring(url, url.length - 1) if url[url.length - 1] == "/"
@@ -79,7 +81,8 @@ class Suggestion
# Wraps each occurence of the query terms in the given string in a <span>.
highlightTerms: (string) ->
ranges = []
- for term in @queryTerms
+ escapedTerms = @queryTerms.map (term) -> Utils.escapeHtml(term)
+ for term in escapedTerms
@pushMatchingRanges string, term, ranges
return string if ranges.length == 0
@@ -109,6 +112,7 @@ class Suggestion
class BookmarkCompleter
+ folderSeparator: "/"
currentSearch: null
# These bookmarks are loaded asynchronously when refresh() is called.
bookmarks: null
@@ -120,14 +124,19 @@ class BookmarkCompleter
onBookmarksLoaded: -> @performSearch() if @currentSearch
performSearch: ->
+ # If the folder separator character the first character in any query term, then we'll use the bookmark's full path as its title.
+ # Otherwise, we'll just use the its regular title.
+ usePathAndTitle = @currentSearch.queryTerms.reduce ((prev,term) => prev || term.indexOf(@folderSeparator) == 0), false
results =
if @currentSearch.queryTerms.length > 0
@bookmarks.filter (bookmark) =>
- RankingUtils.matches(@currentSearch.queryTerms, bookmark.url, bookmark.title)
+ suggestionTitle = if usePathAndTitle then bookmark.pathAndTitle else bookmark.title
+ RankingUtils.matches(@currentSearch.queryTerms, bookmark.url, suggestionTitle)
else
[]
suggestions = results.map (bookmark) =>
- new Suggestion(@currentSearch.queryTerms, "bookmark", bookmark.url, bookmark.title, @computeRelevancy)
+ suggestionTitle = if usePathAndTitle then bookmark.pathAndTitle else bookmark.title
+ new Suggestion(@currentSearch.queryTerms, "bookmark", bookmark.url, suggestionTitle, @computeRelevancy)
onComplete = @currentSearch.onComplete
@currentSearch = null
onComplete(suggestions)
@@ -138,16 +147,29 @@ class BookmarkCompleter
@bookmarks = @traverseBookmarks(bookmarks).filter((bookmark) -> bookmark.url?)
@onBookmarksLoaded()
- # Traverses the bookmark hierarchy, and retuns a flattened list of all bookmarks in the tree.
+ # If these names occur as top-level bookmark names, then they are not included in the names of bookmark folders.
+ ignoreTopLevel:
+ 'Other Bookmarks': true
+ 'Mobile Bookmarks': true
+ 'Bookmarks Bar': true
+
+ # Traverses the bookmark hierarchy, and returns a flattened list of all bookmarks.
traverseBookmarks: (bookmarks) ->
results = []
- toVisit = bookmarks.reverse()
- while toVisit.length > 0
- bookmark = toVisit.pop()
- results.push(bookmark)
- toVisit.push.apply(toVisit, bookmark.children.reverse()) if (bookmark.children)
+ bookmarks.forEach (folder) =>
+ @traverseBookmarksRecursive folder, results
results
+ # Recursive helper for `traverseBookmarks`.
+ traverseBookmarksRecursive: (bookmark, results, parent={pathAndTitle:""}) ->
+ bookmark.pathAndTitle =
+ if bookmark.title and not (parent.pathAndTitle == "" and @ignoreTopLevel[bookmark.title])
+ parent.pathAndTitle + @folderSeparator + bookmark.title
+ else
+ parent.pathAndTitle
+ results.push bookmark
+ bookmark.children.forEach((child) => @traverseBookmarksRecursive child, results, bookmark) if bookmark.children
+
computeRelevancy: (suggestion) ->
RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title)
@@ -256,6 +278,27 @@ class TabCompleter
computeRelevancy: (suggestion) ->
RankingUtils.wordRelevancy(suggestion.queryTerms, suggestion.url, suggestion.title)
+# A completer which will return your search engines
+class SearchEngineCompleter
+ searchEngines: {}
+
+ filter: (queryTerms, onComplete) ->
+ searchEngineMatch = this.getSearchEngineMatches(queryTerms[0])
+ suggestions = []
+ if searchEngineMatch
+ searchEngineMatch = searchEngineMatch.replace(/%s/g, queryTerms[1..].join(" "))
+ suggestion = new Suggestion(queryTerms, "search", searchEngineMatch, queryTerms[0] + ": " + queryTerms[1..].join(" "), @computeRelevancy)
+ suggestions.push(suggestion)
+ onComplete(suggestions)
+
+ computeRelevancy: -> 1
+
+ refresh: ->
+ this.searchEngines = root.Settings.getSearchEngines()
+
+ getSearchEngineMatches: (queryTerm) ->
+ this.searchEngines[queryTerm]
+
# A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top
# 10. Queries from the vomnibar frontend script come through a multi completer.
class MultiCompleter
@@ -304,24 +347,79 @@ RankingUtils =
return false unless matchedTerm
true
+ # Weights used for scoring matches.
+ matchWeights:
+ matchAnywhere: 1
+ matchStartOfWord: 1
+ matchWholeWord: 1
+ # The following must be the sum of the three weights above; it is used for normalization.
+ maximumScore: 3
+ #
+ # Calibration factor for balancing word relevancy and recency.
+ recencyCalibrator: 2.0/3.0
+ # The current value of 2.0/3.0 has the effect of:
+ # - favoring the contribution of recency when matches are not on word boundaries ( because 2.0/3.0 > (1)/3 )
+ # - favoring the contribution of word relevance when matches are on whole words ( because 2.0/3.0 < (1+1+1)/3 )
+
+ # Calculate a score for matching term against string.
+ # The score is in the range [0, matchWeights.maximumScore], see above.
+ # Returns: [ score, count ], where count is the number of matched characters in string.
+ scoreTerm: (term, string) ->
+ score = 0
+ count = 0
+ nonMatching = string.split(RegexpCache.get term)
+ if nonMatching.length > 1
+ # Have match.
+ score = RankingUtils.matchWeights.matchAnywhere
+ count = nonMatching.reduce(((p,c) -> p - c.length), string.length)
+ if RegexpCache.get(term, "\\b").test string
+ # Have match at start of word.
+ score += RankingUtils.matchWeights.matchStartOfWord
+ if RegexpCache.get(term, "\\b", "\\b").test string
+ # Have match of whole word.
+ score += RankingUtils.matchWeights.matchWholeWord
+ [ score, if count < string.length then count else string.length ]
+
# Returns a number between [0, 1] indicating how often the query terms appear in the url and title.
wordRelevancy: (queryTerms, url, title) ->
- queryLength = 0
- urlScore = 0.0
- titleScore = 0.0
+ urlScore = titleScore = 0.0
+ urlCount = titleCount = 0
+ # Calculate initial scores.
for term in queryTerms
- queryLength += term.length
- urlScore += 1 if url && RankingUtils.matches [term], url
- titleScore += 1 if title && RankingUtils.matches [term], title
- urlScore = urlScore / queryTerms.length
- urlScore = urlScore * RankingUtils.normalizeDifference(queryLength, url.length)
+ [ s, c ] = RankingUtils.scoreTerm term, url
+ urlScore += s
+ urlCount += c
+ if title
+ [ s, c ] = RankingUtils.scoreTerm term, title
+ titleScore += s
+ titleCount += c
+
+ maximumPossibleScore = RankingUtils.matchWeights.maximumScore * queryTerms.length
+
+ # Normalize scores.
+ urlScore /= maximumPossibleScore
+ urlScore *= RankingUtils.normalizeDifference urlCount, url.length
+
if title
- titleScore = titleScore / queryTerms.length
- titleScore = titleScore * RankingUtils.normalizeDifference(queryLength, title.length)
+ titleScore /= maximumPossibleScore
+ titleScore *= RankingUtils.normalizeDifference titleCount, title.length
else
titleScore = urlScore
+
+ # Prefer matches in the title over matches in the URL.
+ # In other words, don't let a poor urlScore pull down the titleScore.
+ # For example, urlScore can be unreasonably poor if the URL is very long.
+ urlScore = titleScore if urlScore < titleScore
+
+ # Return the average.
(urlScore + titleScore) / 2
+ # Untested alternative to the above:
+ # - Don't let a poor urlScore pull down a good titleScore, and don't let a poor titleScore pull down a
+ # good urlScore.
+ #
+ # return Math.max(urlScore, titleScore)
+
# Returns a score between [0, 1] which indicates how recent the given timestamp is. Items which are over
# a month old are counted as 0. This range is quadratic, so an item from one day ago has a much stronger
# score than an item from two days ago.
@@ -334,6 +432,9 @@ RankingUtils =
# incresingly discount older history entries.
recencyScore = recencyDifference * recencyDifference * recencyDifference
+ # Calibrate recencyScore vis-a-vis word-relevancy scores.
+ recencyScore *= RankingUtils.matchWeights.recencyCalibrator
+
# Takes the difference of two numbers and returns a number between [0, 1] (the percentage difference).
normalizeDifference: (a, b) ->
max = Math.max(a, b)
@@ -444,6 +545,7 @@ root.MultiCompleter = MultiCompleter
root.HistoryCompleter = HistoryCompleter
root.DomainCompleter = DomainCompleter
root.TabCompleter = TabCompleter
+root.SearchEngineCompleter = SearchEngineCompleter
root.HistoryCache = HistoryCache
root.RankingUtils = RankingUtils
root.RegexpCache = RegexpCache
diff --git a/background_scripts/exclusions.coffee b/background_scripts/exclusions.coffee
new file mode 100644
index 00000000..3a8ef1e7
--- /dev/null
+++ b/background_scripts/exclusions.coffee
@@ -0,0 +1,70 @@
+root = exports ? window
+
+RegexpCache =
+ cache: {}
+ get: (pattern) ->
+ if regexp = @cache[pattern]
+ regexp
+ else
+ @cache[pattern] = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$")
+
+# The Exclusions class manages the exclusion rule setting.
+# An exclusion is an object with two attributes: pattern and passKeys.
+# The exclusions are an array of such objects (because the order matters).
+
+root.Exclusions = Exclusions =
+
+ rules: Settings.get("exclusionRules")
+
+ # Return the first exclusion rule matching the URL, or null.
+ getRule: (url) ->
+ for rule in @rules
+ return rule if url.match(RegexpCache.get(rule.pattern))
+ return null
+
+ setRules: (rules) ->
+ # Callers map a rule to null to have it deleted, and rules without a pattern are useless.
+ @rules = rules.filter (rule) -> rule and rule.pattern
+ Settings.set("exclusionRules", @rules)
+
+ postUpdateHook: (rules) ->
+ @rules = rules
+
+ # Update an existing rule or add a new rule.
+ updateOrAdd: (newRule) ->
+ seen = false
+ @rules.push(newRule)
+ @setRules @rules.map (rule) ->
+ if rule.pattern == newRule.pattern
+ if seen then null else seen = newRule
+ else
+ rule
+
+ remove: (pattern) ->
+ @setRules(@rules.filter((rule) -> rule and rule.pattern != pattern))
+
+# Development and debug only.
+# Enable this (temporarily) to restore legacy exclusion rules from backup.
+if false and Settings.has("excludedUrlsBackup")
+ Settings.clear("exclusionRules")
+ Settings.set("excludedUrls", Settings.get("excludedUrlsBackup"))
+
+if not Settings.has("exclusionRules") and Settings.has("excludedUrls")
+ # Migration from the legacy representation of exclusion rules.
+ #
+ # In Vimium 1.45 and in github/master on 27 August, 2014, exclusion rules are represented by the setting:
+ # excludedUrls: "http*://www.google.com/reader/*\nhttp*://mail.google.com/* jk"
+ #
+ # The new (equivalent) settings is:
+ # exclusionRules: [ { pattern: "http*://www.google.com/reader/*", passKeys: "" }, { pattern: "http*://mail.google.com/*", passKeys: "jk" } ]
+
+ parseLegacyRules = (lines) ->
+ for line in lines.trim().split("\n").map((line) -> line.trim())
+ if line.length and line.indexOf("#") != 0 and line.indexOf('"') != 0
+ parse = line.split(/\s+/)
+ { pattern: parse[0], passKeys: parse[1..].join("") }
+
+ Exclusions.setRules(parseLegacyRules(Settings.get("excludedUrls")))
+ # We'll keep a backup of the "excludedUrls" setting, just in case.
+ Settings.set("excludedUrlsBackup", Settings.get("excludedUrls")) if not Settings.has("excludedUrlsBackup")
+ Settings.clear("excludedUrls")
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index f564f477..898f46f1 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -24,9 +24,11 @@ completionSources =
history: new HistoryCompleter()
domains: new DomainCompleter()
tabs: new TabCompleter()
+ seachEngines: new SearchEngineCompleter()
completers =
omni: new MultiCompleter([
+ completionSources.seachEngines,
completionSources.bookmarks,
completionSources.history,
completionSources.domains])
@@ -67,30 +69,31 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) ->
getCurrentTabUrl = (request, sender) -> sender.tab.url
#
-# Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL.
+# Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL, and
+# whether any keys should be passed through to the underlying page.
#
-isEnabledForUrl = (request) ->
- # excludedUrls are stored as a series of URL expressions separated by newlines.
- excludedUrls = Settings.get("excludedUrls").split("\n")
- isEnabled = true
- for url in excludedUrls
- # The user can add "*" to the URL which means ".*"
- regexp = new RegExp("^" + url.replace(/\*/g, ".*") + "$")
- isEnabled = false if request.url.match(regexp)
- { isEnabledForUrl: isEnabled }
-
-# Called by the popup UI. Strips leading/trailing whitespace and ignores empty strings.
-root.addExcludedUrl = (url) ->
- return unless url = url.trim()
-
- excludedUrls = Settings.get("excludedUrls")
- return if excludedUrls.indexOf(url) >= 0
-
- excludedUrls += "\n" + url
- Settings.set("excludedUrls", excludedUrls)
-
- chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true },
- (tabs) -> updateActiveState(tabs[0].id))
+root.isEnabledForUrl = isEnabledForUrl = (request) ->
+ rule = Exclusions.getRule(request.url)
+ {
+ rule: rule
+ isEnabledForUrl: not rule or rule.passKeys
+ passKeys: rule?.passKeys or ""
+ }
+
+# Called by the popup UI.
+# If the URL pattern matches an existing rule, then the existing rule is updated. Otherwise, a new rule is created.
+root.addExclusionRule = (pattern,passKeys) ->
+ if pattern = pattern.trim()
+ Exclusions.updateOrAdd({ pattern: pattern, passKeys: passKeys })
+ chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true },
+ (tabs) -> updateActiveState(tabs[0].id))
+
+# Called by the popup UI. Remove all existing exclusion rules with this pattern.
+root.removeExclusionRule = (pattern) ->
+ if pattern = pattern.trim()
+ Exclusions.remove(pattern)
+ chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true },
+ (tabs) -> updateActiveState(tabs[0].id))
saveHelpDialogSettings = (request) ->
Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands)
@@ -217,6 +220,12 @@ repeatFunction = (func, totalCount, currentCount, frameId) ->
-> repeatFunction(func, totalCount, currentCount + 1, frameId),
frameId)
+moveTab = (callback, direction) ->
+ chrome.tabs.getSelected(null, (tab) ->
+ # Use Math.max to prevent -1 as the new index, otherwise the tab of index n will wrap to the far RHS when
+ # moved left by exactly (n+1) places.
+ chrome.tabs.move(tab.id, {index: Math.max(0, tab.index + direction) }, callback))
+
# Start action functions
# These are commands which are bound to keystroke which must be handled by the background page. They are
@@ -228,39 +237,51 @@ BackgroundCommands =
chrome.tabs.duplicate(tab.id)
selectionChangedHandlers.push(callback))
moveTabToNewWindow: (callback) ->
- chrome.tabs.getSelected(null, (tab) ->
- chrome.windows.create({tabId: tab.id}))
+ chrome.tabs.query {active: true, currentWindow: true}, (tabs) ->
+ tab = tabs[0]
+ chrome.windows.create {tabId: tab.id, incognito: tab.incognito}
nextTab: (callback) -> selectTab(callback, "next")
previousTab: (callback) -> selectTab(callback, "previous")
firstTab: (callback) -> selectTab(callback, "first")
lastTab: (callback) -> selectTab(callback, "last")
- removeTab: ->
+ removeTab: (callback) ->
chrome.tabs.getSelected(null, (tab) ->
- chrome.tabs.remove(tab.id))
+ chrome.tabs.remove(tab.id)
+ selectionChangedHandlers.push(callback))
restoreTab: (callback) ->
- # TODO(ilya): Should this be getLastFocused instead?
- chrome.windows.getCurrent((window) ->
- return unless (tabQueue[window.id] && tabQueue[window.id].length > 0)
- tabQueueEntry = tabQueue[window.id].pop()
- # Clean out the tabQueue so we don't have unused windows laying about.
- delete tabQueue[window.id] if (tabQueue[window.id].length == 0)
-
- # We have to chain a few callbacks to set the appropriate scroll position. We can't just wait until the
- # tab is created because the content script is not available during the "loading" state. We need to
- # wait until that's over before we can call setScrollPosition.
- chrome.tabs.create({ url: tabQueueEntry.url, index: tabQueueEntry.positionIndex }, (tab) ->
- tabLoadedHandlers[tab.id] = ->
- chrome.tabs.sendMessage(tab.id,
- name: "setScrollPosition",
- scrollX: tabQueueEntry.scrollX,
- scrollY: tabQueueEntry.scrollY)
- callback()))
+ # TODO: remove if-else -block when adopted into stable
+ if chrome.sessions
+ chrome.sessions.restore(null, (restoredSession) ->
+ callback() unless chrome.runtime.lastError)
+ else
+ # TODO(ilya): Should this be getLastFocused instead?
+ chrome.windows.getCurrent((window) ->
+ return unless (tabQueue[window.id] && tabQueue[window.id].length > 0)
+ tabQueueEntry = tabQueue[window.id].pop()
+ # Clean out the tabQueue so we don't have unused windows laying about.
+ delete tabQueue[window.id] if (tabQueue[window.id].length == 0)
+
+ # We have to chain a few callbacks to set the appropriate scroll position. We can't just wait until the
+ # tab is created because the content script is not available during the "loading" state. We need to
+ # wait until that's over before we can call setScrollPosition.
+ chrome.tabs.create({ url: tabQueueEntry.url, index: tabQueueEntry.positionIndex }, (tab) ->
+ tabLoadedHandlers[tab.id] = ->
+ chrome.tabs.sendRequest(tab.id,
+ name: "setScrollPosition",
+ scrollX: tabQueueEntry.scrollX,
+ scrollY: tabQueueEntry.scrollY)
+ callback()))
openCopiedUrlInCurrentTab: (request) -> openUrlInCurrentTab({ url: Clipboard.paste() })
openCopiedUrlInNewTab: (request) -> openUrlInNewTab({ url: Clipboard.paste() })
+ togglePinTab: (request) ->
+ chrome.tabs.getSelected(null, (tab) ->
+ chrome.tabs.update(tab.id, { pinned: !tab.pinned }))
showHelp: (callback, frameId) ->
chrome.tabs.getSelected(null, (tab) ->
chrome.tabs.sendMessage(tab.id,
{ name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId }))
+ moveTabLeft: (count) -> moveTab(null, -count)
+ moveTabRight: (count) -> moveTab(null, count)
nextFrame: (count) ->
chrome.tabs.getSelected(null, (tab) ->
frames = framesForTab[tab.id].frames
@@ -272,6 +293,30 @@ BackgroundCommands =
chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[newIndex].id, highlight: true }))
+ closeTabsOnLeft: -> removeTabsRelative "before"
+ closeTabsOnRight: -> removeTabsRelative "after"
+ closeOtherTabs: -> removeTabsRelative "both"
+
+# Remove tabs before, after, or either side of the currently active tab
+removeTabsRelative = (direction) ->
+ chrome.tabs.query {currentWindow: true}, (tabs) ->
+ chrome.tabs.query {currentWindow: true, active: true}, (activeTabs) ->
+ activeTabIndex = activeTabs[0].index
+
+ shouldDelete = switch direction
+ when "before"
+ (index) -> index < activeTabIndex
+ when "after"
+ (index) -> index > activeTabIndex
+ when "both"
+ (index) -> index != activeTabIndex
+
+ toRemove = []
+ for tab in tabs
+ if not tab.pinned and shouldDelete tab.index
+ toRemove.push tab.id
+ chrome.tabs.remove toRemove
+
# Selects a tab before or after the currently selected tab.
# - direction: "next", "previous", "first" or "last".
selectTab = (callback, direction) ->
@@ -304,32 +349,36 @@ updateOpenTabs = (tab) ->
# Frames are recreated on refresh
delete framesForTab[tab.id]
-# Updates the browserAction icon to indicated whether Vimium is enabled or disabled on the current page.
-# Also disables Vimium if it is currently enabled but should be disabled according to the url blacklist.
+setBrowserActionIcon = (tabId,path) ->
+ chrome.browserAction.setIcon({ tabId: tabId, path: path })
+
+# Updates the browserAction icon to indicate whether Vimium is enabled or disabled on the current page.
+# Also propagates new enabled/disabled/passkeys state to active window, if necessary.
# This lets you disable Vimium on a page without needing to reload.
-#
-# Three situations are considered:
-# 1. Active tab is disabled -> disable icon
-# 2. Active tab is enabled and should be enabled -> enable icon
-# 3. Active tab is enabled but should be disabled -> disable icon and disable vimium
updateActiveState = (tabId) ->
enabledIcon = "icons/browser_action_enabled.png"
disabledIcon = "icons/browser_action_disabled.png"
- chrome.tabs.get(tabId, (tab) ->
- # Default to disabled state in case we can't connect to Vimium, primarily for the "New Tab" page.
- chrome.browserAction.setIcon({ path: disabledIcon })
- chrome.tabs.sendMessage(tabId, { name: "getActiveState" }, (response) ->
- isCurrentlyEnabled = (response? && response.enabled)
- shouldBeEnabled = isEnabledForUrl({url: tab.url}).isEnabledForUrl
-
- if (isCurrentlyEnabled)
- if (shouldBeEnabled)
- chrome.browserAction.setIcon({ path: enabledIcon })
+ partialIcon = "icons/browser_action_partial.png"
+ chrome.tabs.get tabId, (tab) ->
+ chrome.tabs.sendMessage tabId, { name: "getActiveState" }, (response) ->
+ if response
+ isCurrentlyEnabled = response.enabled
+ currentPasskeys = response.passKeys
+ config = isEnabledForUrl({url: tab.url})
+ enabled = config.isEnabledForUrl
+ passKeys = config.passKeys
+ if (enabled and passKeys)
+ setBrowserActionIcon(tabId,partialIcon)
+ else if (enabled)
+ setBrowserActionIcon(tabId,enabledIcon)
else
- chrome.browserAction.setIcon({ path: disabledIcon })
- chrome.tabs.sendMessage(tabId, { name: "disableVimium" })
+ setBrowserActionIcon(tabId,disabledIcon)
+ # Propagate the new state only if it has changed.
+ if (isCurrentlyEnabled != enabled || currentPasskeys != passKeys)
+ chrome.tabs.sendMessage(tabId, { name: "setState", enabled: enabled, passKeys: passKeys })
else
- chrome.browserAction.setIcon({ path: disabledIcon })))
+ # We didn't get a response from the front end, so Vimium isn't running.
+ setBrowserActionIcon(tabId,disabledIcon)
handleUpdateScrollPosition = (request, sender) ->
updateScrollPosition(sender.tab, request.scrollX, request.scrollY)
@@ -340,12 +389,14 @@ updateScrollPosition = (tab, scrollX, scrollY) ->
chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) ->
return unless changeInfo.status == "loading" # only do this once per URL change
- chrome.tabs.insertCSS tabId,
+ cssConf =
allFrames: true
code: Settings.get("userDefinedLinkHintCss")
runAt: "document_start"
- updateOpenTabs(tab)
- updateActiveState(tabId)
+ chrome.tabs.insertCSS tabId, cssConf, ->
+ if not chrome.runtime.lastError
+ updateOpenTabs(tab)
+ updateActiveState(tabId)
chrome.tabs.onAttached.addListener (tabId, attachedInfo) ->
# We should update all the tabs in the old window and the new window.
@@ -363,16 +414,17 @@ chrome.tabs.onRemoved.addListener (tabId) ->
# If we restore pages that content scripts can't run on, they'll ignore Vimium keystrokes when they
# reappear. Pretend they never existed and adjust tab indices accordingly. Could possibly expand this into
# a blacklist in the future.
- if (/^(chrome|view-source:)[^:]*:\/\/.*/.test(openTabInfo.url))
- for i of tabQueue[openTabInfo.windowId]
- if (tabQueue[openTabInfo.windowId][i].positionIndex > openTabInfo.positionIndex)
- tabQueue[openTabInfo.windowId][i].positionIndex--
- return
-
- if (tabQueue[openTabInfo.windowId])
- tabQueue[openTabInfo.windowId].push(openTabInfo)
- else
- tabQueue[openTabInfo.windowId] = [openTabInfo]
+ unless chrome.sessions
+ if (/^(chrome|view-source:)[^:]*:\/\/.*/.test(openTabInfo.url))
+ for i of tabQueue[openTabInfo.windowId]
+ if (tabQueue[openTabInfo.windowId][i].positionIndex > openTabInfo.positionIndex)
+ tabQueue[openTabInfo.windowId][i].positionIndex--
+ return
+
+ if (tabQueue[openTabInfo.windowId])
+ tabQueue[openTabInfo.windowId].push(openTabInfo)
+ else
+ tabQueue[openTabInfo.windowId] = [openTabInfo]
# keep the reference around for a while to wait for the last messages from the closed tab (e.g. for updating
# scroll position)
@@ -382,7 +434,8 @@ chrome.tabs.onRemoved.addListener (tabId) ->
chrome.tabs.onActiveChanged.addListener (tabId, selectInfo) -> updateActiveState(tabId)
-chrome.windows.onRemoved.addListener (windowId) -> delete tabQueue[windowId]
+unless chrome.sessions
+ chrome.windows.onRemoved.addListener (windowId) -> delete tabQueue[windowId]
# End action functions
@@ -458,6 +511,14 @@ handleKeyDown = (request, port) ->
console.log("checking keyQueue: [", keyQueue + key, "]")
keyQueue = checkKeyQueue(keyQueue + key, port.sender.tab.id, request.frameId)
console.log("new KeyQueue: " + keyQueue)
+ # Tell the content script whether there are keys in the queue.
+ # FIXME: There is a race condition here. The behaviour in the content script depends upon whether this message gets
+ # back there before or after the next keystroke.
+ # That being said, I suspect there are other similar race conditions here, for example in checkKeyQueue().
+ # Steve (23 Aug, 14).
+ chrome.tabs.sendMessage(port.sender.tab.id,
+ name: "currentKeyQueue",
+ keyQueue: keyQueue)
checkKeyQueue = (keysToCheck, tabId, frameId) ->
refreshedCompletionKeys = false
@@ -470,23 +531,35 @@ checkKeyQueue = (keysToCheck, tabId, frameId) ->
if (Commands.keyToCommandRegistry[command])
registryEntry = Commands.keyToCommandRegistry[command]
-
- if !registryEntry.isBackgroundCommand
- chrome.tabs.sendMessage(tabId,
- name: "executePageCommand",
- command: registryEntry.command,
- frameId: frameId,
- count: count,
- passCountToFunction: registryEntry.passCountToFunction,
- completionKeys: generateCompletionKeys(""))
- refreshedCompletionKeys = true
- else
- if registryEntry.passCountToFunction
- BackgroundCommands[registryEntry.command](count)
- else if registryEntry.noRepeat
- BackgroundCommands[registryEntry.command]()
+ runCommand = true
+
+ if registryEntry.noRepeat
+ count = 1
+ else if registryEntry.repeatLimit and count > registryEntry.repeatLimit
+ runCommand = confirm """
+ You have asked Vimium to perform #{count} repeats of the command:
+ #{Commands.availableCommands[registryEntry.command].description}
+
+ Are you sure you want to continue?
+ """
+
+ if runCommand
+ if not registryEntry.isBackgroundCommand
+ chrome.tabs.sendMessage(tabId,
+ name: "executePageCommand",
+ command: registryEntry.command,
+ frameId: frameId,
+ count: count,
+ passCountToFunction: registryEntry.passCountToFunction,
+ completionKeys: generateCompletionKeys(""))
+ refreshedCompletionKeys = true
else
- repeatFunction(BackgroundCommands[registryEntry.command], count, 0, frameId)
+ if registryEntry.passCountToFunction
+ BackgroundCommands[registryEntry.command](count)
+ else if registryEntry.noRepeat
+ BackgroundCommands[registryEntry.command]()
+ else
+ repeatFunction(BackgroundCommands[registryEntry.command], count, 0, frameId)
newKeyQueue = ""
else if (getActualKeyStrokeLength(command) > 1)
@@ -538,7 +611,7 @@ registerFrame = (request, sender) ->
focusedFrame = request.frameId
framesForTab[sender.tab.id].total = request.total
- framesForTab[sender.tab.id].frames.push({ id: request.frameId, area: request.area })
+ framesForTab[sender.tab.id].frames.push({ id: request.frameId })
handleFrameFocused = (request, sender) -> focusedFrame = request.frameId
@@ -596,3 +669,6 @@ chrome.windows.getAll { populate: true }, (windows) ->
createScrollPositionHandler = ->
(response) -> updateScrollPosition(tab, response.scrollX, response.scrollY) if response?
chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler())
+
+# Start pulling changes from synchronized storage.
+Sync.init()
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 0fe1e1bb..7150fcba 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -1,5 +1,5 @@
#
-# Used by everyone to manipulate localStorage.
+# Used by all parts of Vimium to manipulate localStorage.
#
root = exports ? window
@@ -8,20 +8,60 @@ root.Settings = Settings =
if (key of localStorage) then JSON.parse(localStorage[key]) else @defaults[key]
set: (key, value) ->
- # don't store the value if it is equal to the default, so we can change the defaults in the future
+ # Don't store the value if it is equal to the default, so we can change the defaults in the future
if (value == @defaults[key])
@clear(key)
else
- localStorage[key] = JSON.stringify(value)
+ jsonValue = JSON.stringify value
+ localStorage[key] = jsonValue
+ Sync.set key, jsonValue
- clear: (key) -> delete localStorage[key]
+ clear: (key) ->
+ if @has key
+ delete localStorage[key]
+ Sync.clear key
has: (key) -> key of localStorage
- # options/options.(coffee|html) only handle booleans and strings; therefore
- # all defaults must be booleans or strings
+ # For settings which require action when their value changes, add hooks here called from
+ # options/options.coffee (when the options page is saved), and from background_scripts/sync.coffee (when an
+ # update propagates from chrome.storage.sync).
+ postUpdateHooks:
+ keyMappings: (value) ->
+ root.Commands.clearKeyMappingsAndSetDefaults()
+ root.Commands.parseCustomKeyMappings value
+ root.refreshCompletionKeysAfterMappingSave()
+
+ searchEngines: (value) ->
+ root.Settings.parseSearchEngines value
+
+ exclusionRules: (value) ->
+ root.Exclusions.postUpdateHook value
+
+ # postUpdateHooks convenience wrapper
+ performPostUpdateHook: (key, value) ->
+ @postUpdateHooks[key] value if @postUpdateHooks[key]
+
+ # Here we have our functions that parse the search engines
+ # this is a map that we use to store our search engines for use.
+ searchEnginesMap: {}
+
+ # this parses the search engines settings and clears the old searchEngines and sets the new one
+ parseSearchEngines: (searchEnginesText) ->
+ @searchEnginesMap = {}
+ # find the split pairs by first splitting by line then splitting on the first `: `
+ split_pairs = ( pair.split( /: (.+)/, 2) for pair in searchEnginesText.split( /\n/ ) when pair[0] != "#" )
+ @searchEnginesMap[a[0]] = a[1] for a in split_pairs
+ @searchEnginesMap
+ getSearchEngines: ->
+ this.parseSearchEngines(@get("searchEngines") || "") if Object.keys(@searchEnginesMap).length == 0
+ @searchEnginesMap
+
+ # options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans
+ # or strings
defaults:
scrollStepSize: 60
+ keyMappings: "# Insert your prefered key mappings here."
linkHintCharacters: "sadfjklewcmpgh"
linkHintNumbers: "0123456789"
filterLinkHints: false
@@ -45,11 +85,14 @@ root.Settings = Settings =
div > .vimiumHintMarker > .matchingCharacter {
}
"""
- excludedUrls:
- """
- http*://mail.google.com/*
- """
- # NOTE : If a page contains both a single angle-bracket link and a double angle-bracket link, then in
+ # Default exclusion rules.
+ exclusionRules:
+ [
+ # Disable Vimium on Gmail.
+ { pattern: "http*://mail.google.com/*", passKeys: "" }
+ ]
+
+ # NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in
# most cases the single bracket link will be "prev/next page" and the double bracket link will be
# "first/last page", so we put the single bracket first in the pattern string so that it gets searched
# for first.
@@ -60,9 +103,12 @@ root.Settings = Settings =
nextPatterns: "next,more,>,\u2192,\xbb,\u226b,>>"
# default/fall back search engine
searchUrl: "http://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"
settingsVersion: Utils.getCurrentVersion()
+
# We use settingsVersion to coordinate any necessary schema changes.
if Utils.compareVersions("1.42", Settings.get("settingsVersion")) != -1
Settings.set("scrollStepSize", parseFloat Settings.get("scrollStepSize"))
diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee
new file mode 100644
index 00000000..93430856
--- /dev/null
+++ b/background_scripts/sync.coffee
@@ -0,0 +1,102 @@
+#
+# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync.
+# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those
+# changes to localStorage and into vimium's internal state.
+# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating
+# changes to localStorage and into vimium's internal state.
+#
+# Changes are propagated into vimium's state using the same mechanism
+# (Settings.performPostUpdateHook) that is used when options are changed on
+# the options page.
+#
+# The effect is best-effort synchronization of vimium options/settings between
+# chrome/vimium instances.
+#
+# NOTE:
+# Values handled within this module are ALWAYS already JSON.stringifed, so
+# they're always non-empty strings.
+#
+
+root = exports ? window
+root.Sync = Sync =
+
+ # April 19 2014: Leave logging statements in, but disable debugging. We may need to come back to this, so
+ # removing logging now would be premature. However, if users report problems, they are unlikely to notice
+ # and make sense of console logs on background pages. So disable it, by default. For genuine errors, we
+ # call console.log directly.
+ debug: false
+ storage: chrome.storage.sync
+ doNotSync: ["settingsVersion", "previousVersion"]
+
+ # This is called in main.coffee.
+ init: ->
+ chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area
+ @fetchAsync()
+
+ # Asynchronous fetch from synced storage, called only at startup.
+ fetchAsync: ->
+ @storage.get null, (items) =>
+ # Chrome sets chrome.runtime.lastError if there is an error.
+ if chrome.runtime.lastError is undefined
+ for own key, value of items
+ @log "fetchAsync: #{key} <- #{value}"
+ @storeAndPropagate key, value
+ else
+ console.log "callback for Sync.fetchAsync() indicates error"
+ console.log chrome.runtime.lastError
+
+ # Asynchronous message from synced storage.
+ handleStorageUpdate: (changes, area) ->
+ for own key, change of changes
+ @log "handleStorageUpdate: #{key} <- #{change.newValue}"
+ @storeAndPropagate key, change?.newValue
+
+ # Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate).
+ storeAndPropagate: (key, value) ->
+ return if not key of Settings.defaults
+ return if not @shouldSyncKey key
+ return if value and key of localStorage and localStorage[key] is value
+ defaultValue = Settings.defaults[key]
+ defaultValueJSON = JSON.stringify(defaultValue)
+
+ if value and value != defaultValueJSON
+ # Key/value has been changed to non-default value at remote instance.
+ @log "storeAndPropagate update: #{key}=#{value}"
+ localStorage[key] = value
+ Settings.performPostUpdateHook key, JSON.parse(value)
+ else
+ # Key has been reset to default value at remote instance.
+ @log "storeAndPropagate clear: #{key}"
+ if key of localStorage
+ delete localStorage[key]
+ Settings.performPostUpdateHook key, defaultValue
+
+ # Only called synchronously from within vimium, never on a callback.
+ # No need to propagate updates to the rest of vimium, that's already been done.
+ set: (key, value) ->
+ if @shouldSyncKey key
+ @log "set scheduled: #{key}=#{value}"
+ key_value = {}
+ key_value[key] = value
+ @storage.set key_value, =>
+ # Chrome sets chrome.runtime.lastError if there is an error.
+ if chrome.runtime.lastError
+ console.log "callback for Sync.set() indicates error: #{key} <- #{value}"
+ console.log chrome.runtime.lastError
+
+ # Only called synchronously from within vimium, never on a callback.
+ clear: (key) ->
+ if @shouldSyncKey key
+ @log "clear scheduled: #{key}"
+ @storage.remove key, =>
+ # Chrome sets chrome.runtime.lastError if there is an error.
+ if chrome.runtime.lastError
+ console.log "for Sync.clear() indicates error: #{key}"
+ console.log chrome.runtime.lastError
+
+ # Should we synchronize this key?
+ shouldSyncKey: (key) ->
+ key not in @doNotSync
+
+ log: (msg) ->
+ console.log "Sync: #{msg}" if @debug