aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--background_scripts/commands.coffee89
-rw-r--r--background_scripts/completion.coffee198
-rw-r--r--background_scripts/completion_engines.coffee60
-rw-r--r--background_scripts/exclusions.coffee4
-rw-r--r--background_scripts/main.coffee54
-rw-r--r--background_scripts/settings.coffee127
-rw-r--r--background_scripts/sync.coffee74
-rw-r--r--content_scripts/hud.coffee6
-rw-r--r--content_scripts/mode_visual_edit.coffee7
-rw-r--r--content_scripts/ui_component.coffee79
-rw-r--r--content_scripts/vimium.css11
-rw-r--r--content_scripts/vimium_frontend.coffee26
-rw-r--r--content_scripts/vomnibar.coffee34
-rw-r--r--lib/dom_utils.coffee20
-rw-r--r--lib/settings.coffee202
-rw-r--r--lib/utils.coffee40
-rw-r--r--manifest.json3
-rw-r--r--pages/help_dialog.html3
-rw-r--r--pages/options.coffee10
-rw-r--r--pages/options.html7
-rw-r--r--pages/popup.html2
-rw-r--r--pages/vomnibar.coffee26
-rw-r--r--pages/vomnibar.css1
-rw-r--r--tests/dom_tests/dom_utils_test.coffee9
-rw-r--r--tests/dom_tests/phantom_runner.coffee29
-rw-r--r--tests/dom_tests/vomnibar_test.coffee1
-rw-r--r--tests/unit_tests/commands_test.coffee1
-rw-r--r--tests/unit_tests/completion_test.coffee8
-rw-r--r--tests/unit_tests/exclusion_test.coffee5
-rw-r--r--tests/unit_tests/settings_test.coffee20
-rw-r--r--tests/unit_tests/test_chrome_stubs.coffee3
-rw-r--r--tests/unit_tests/utils_test.coffee5
33 files changed, 642 insertions, 529 deletions
diff --git a/README.md b/README.md
index 023605f4..d94e5e73 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,8 @@ Additional advanced browsing commands:
gU go up to root of the URL hierarchy
zH scroll all the way left
zL scroll all the way right
+ v enter visual mode; use p/P to paste-and-go, use y to yank
+ V enter visual line mode
Vimium supports command repetition so, for example, hitting `5t` will open 5 tabs in rapid succession. `<ESC>` (or
`<c-[>`) will clear any partial commands in the queue and will also exit insert and find modes.
@@ -144,8 +146,9 @@ Release Notes
-------------
1.52 (not yet released)
-- Search engine completion for selected search engines (including Google, Youtube, Bing, DuckDuckGo, Wikipedia and Amazon).
-- Much improved custom search engine experience (including completion, where available).
+- Improved custom-search engine experience (including completion for Google,
+ Youtube, Bing, DuckDuckGo, Wikipedia, Amazon and a number of other search
+ engines).
- Bug fixes: bookmarklets accessed from the vomnibar.
1.51 (2015-05-02)
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index 708fe5d5..64ec36be 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -24,21 +24,13 @@ Commands =
noRepeat: options.noRepeat
repeatLimit: options.repeatLimit
- mapKeyToCommand: (key, command) ->
+ mapKeyToCommand: ({ key, command, options }) ->
unless @availableCommands[command]
- console.log(command, "doesn't exist!")
+ console.log command, "doesn't exist!"
return
- commandDetails = @availableCommands[command]
-
- @keyToCommandRegistry[key] =
- command: command
- isBackgroundCommand: commandDetails.isBackgroundCommand
- passCountToFunction: commandDetails.passCountToFunction
- noRepeat: commandDetails.noRepeat
- repeatLimit: commandDetails.repeatLimit
-
- unmapKey: (key) -> delete @keyToCommandRegistry[key]
+ options ?= []
+ @keyToCommandRegistry[key] = extend { command, options }, @availableCommands[command]
# Lower-case the appropriate portions of named keys.
#
@@ -55,37 +47,29 @@ Commands =
.replace /<space>/ig, " "
parseCustomKeyMappings: (customKeyMappings) ->
- lines = customKeyMappings.split("\n")
-
- for line in lines
- continue if (line[0] == "\"" || line[0] == "#")
- splitLine = line.replace(/\s+$/, "").split(/\s+/)
-
- lineCommand = splitLine[0]
-
- if (lineCommand == "map")
- continue if (splitLine.length != 3)
- key = @normalizeKey(splitLine[1])
- vimiumCommand = splitLine[2]
-
- continue unless @availableCommands[vimiumCommand]
-
- console.log("Mapping", key, "to", vimiumCommand)
- @mapKeyToCommand(key, vimiumCommand)
- else if (lineCommand == "unmap")
- continue if (splitLine.length != 2)
-
- key = @normalizeKey(splitLine[1])
- console.log("Unmapping", key)
- @unmapKey(key)
- else if (lineCommand == "unmapAll")
- @keyToCommandRegistry = {}
+ for line in customKeyMappings.split "\n"
+ unless line[0] == "\"" or line[0] == "#"
+ tokens = line.replace(/\s+$/, "").split /\s+/
+ switch tokens[0]
+ when "map"
+ [ _, key, command, options... ] = tokens
+ if command? and @availableCommands[command]
+ key = @normalizeKey key
+ console.log "Mapping", key, "to", command
+ @mapKeyToCommand { key, command, options }
+
+ when "unmap"
+ if tokens.length == 2
+ key = @normalizeKey tokens[1]
+ console.log "Unmapping", key
+ delete @keyToCommandRegistry[key]
+
+ when "unmapAll"
+ @keyToCommandRegistry = {}
clearKeyMappingsAndSetDefaults: ->
@keyToCommandRegistry = {}
-
- for key of defaultKeyMappings
- @mapKeyToCommand(key, defaultKeyMappings[key])
+ @mapKeyToCommand { key, command } for key, command of defaultKeyMappings
# An ordered listing of all available commands, grouped by type. This is the order they will
# be shown in the help page.
@@ -122,19 +106,20 @@ Commands =
"LinkHints.activateModeWithQueue",
"LinkHints.activateModeToDownloadLink",
"LinkHints.activateModeToOpenIncognito",
- "Vomnibar.activate",
- "Vomnibar.activateInNewTab",
- "Vomnibar.activateTabSelection",
- "Vomnibar.activateBookmarks",
- "Vomnibar.activateBookmarksInNewTab",
"goPrevious",
"goNext",
"nextFrame",
"mainFrame",
"Marks.activateCreateMode",
- "Vomnibar.activateEditUrl",
- "Vomnibar.activateEditUrlInNewTab",
"Marks.activateGotoMode"]
+ vomnibarCommands:
+ ["Vomnibar.activate",
+ "Vomnibar.activateInNewTab",
+ "Vomnibar.activateTabSelection",
+ "Vomnibar.activateBookmarks",
+ "Vomnibar.activateBookmarksInNewTab",
+ "Vomnibar.activateEditUrl",
+ "Vomnibar.activateEditUrlInNewTab"]
findCommands: ["enterFindMode", "performFind", "performBackwardsFind"]
historyNavigation:
["goBack", "goForward"]
@@ -322,8 +307,8 @@ commandDescriptions =
goToRoot: ["Go to root of current URL hierarchy", { passCountToFunction: true }]
# Manipulating tabs
- nextTab: ["Go one tab right", { background: true }]
- previousTab: ["Go one tab left", { background: true }]
+ nextTab: ["Go one tab right", { background: true, passCountToFunction: true }]
+ previousTab: ["Go one tab left", { background: true, passCountToFunction: true }]
firstTab: ["Go to the first tab", { background: true }]
lastTab: ["Go to the last tab", { background: true }]
@@ -360,5 +345,11 @@ commandDescriptions =
Commands.init()
+# Register postUpdateHook for keyMappings setting.
+Settings.postUpdateHooks["keyMappings"] = (value) ->
+ Commands.clearKeyMappingsAndSetDefaults()
+ Commands.parseCustomKeyMappings value
+ refreshCompletionKeysAfterMappingSave()
+
root = exports ? window
root.Commands = Commands
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index 98a82f88..c83066a6 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -29,6 +29,8 @@ class Suggestion
@highlightTerms = true
# @insertText is text to insert into the vomnibar input when the suggestion is selected.
@insertText = null
+ # @deDuplicate controls whether this suggestion is a candidate for deduplication.
+ @deDuplicate = true
# Other options set by individual completers include:
# - tabId (TabCompleter)
@@ -41,24 +43,33 @@ class Suggestion
# or @relevancyFunction.
@relevancy ?= @relevancyFunction this
- generateHtml: ->
+ generateHtml: (request) ->
return @html if @html
relevancyHtml = if @showRelevancy then "<span class='relevancy'>#{@computeRelevancy()}</span>" else ""
- # NOTE(philc): We're using these vimium-specific class names so we don't collide with the page's CSS.
insertTextClass = if @insertText then "vomnibarInsertText" else "vomnibarNoInsertText"
- insertTextIndicator = "&#xfe62;" # A small plus sign.
- insertTextIndicator = "&#xfe65;" # A small "greater than" sign.
+ insertTextIndicator = "&#8618;" # A right hooked arrow.
+ @title = @insertText if @insertText and request.isCustomSearch
+ # NOTE(philc): We're using these vimium-specific class names so we don't collide with the page's CSS.
@html =
- """
- <div class="vimiumReset vomnibarTopHalf">
- <span class="vimiumReset vomnibarSource #{insertTextClass}">#{insertTextIndicator}</span><span class="vimiumReset vomnibarSource">#{@type}</span>
- <span class="vimiumReset vomnibarTitle">#{@highlightQueryTerms Utils.escapeHtml @title}</span>
- </div>
- <div class="vimiumReset vomnibarBottomHalf">
- <span class="vimiumReset vomnibarSource vomnibarNoInsertText">#{insertTextIndicator}</span><span class="vimiumReset vomnibarUrl">#{@highlightUrlTerms Utils.escapeHtml @shortenUrl()}</span>
- #{relevancyHtml}
- </div>
- """
+ if request.isCustomSearch
+ """
+ <div class="vimiumReset vomnibarTopHalf">
+ <span class="vimiumReset vomnibarSource #{insertTextClass}">#{insertTextIndicator}</span><span class="vimiumReset vomnibarSource">#{@type}</span>
+ <span class="vimiumReset vomnibarTitle">#{@highlightQueryTerms Utils.escapeHtml @title}</span>
+ #{relevancyHtml}
+ </div>
+ """
+ else
+ """
+ <div class="vimiumReset vomnibarTopHalf">
+ <span class="vimiumReset vomnibarSource #{insertTextClass}">#{insertTextIndicator}</span><span class="vimiumReset vomnibarSource">#{@type}</span>
+ <span class="vimiumReset vomnibarTitle">#{@highlightQueryTerms Utils.escapeHtml @title}</span>
+ </div>
+ <div class="vimiumReset vomnibarBottomHalf">
+ <span class="vimiumReset vomnibarSource vomnibarNoInsertText">#{insertTextIndicator}</span><span class="vimiumReset vomnibarUrl">#{@highlightUrlTerms Utils.escapeHtml @shortenUrl()}</span>
+ #{relevancyHtml}
+ </div>
+ """
# Use neat trick to snatch a domain (http://stackoverflow.com/a/8498668).
getUrlRoot: (url) ->
@@ -292,7 +303,7 @@ class DomainCompleter
queryTerms: queryTerms
type: "domain"
url: domains[0]?[0] ? "" # This is the URL or an empty string, but not null.
- relevancy: 1
+ relevancy: 2.0
].filter (s) -> 0 < s.url.length
# Returns a list of domains of the form: [ [domain, relevancy], ... ]
@@ -391,6 +402,7 @@ class TabCompleter
title: tab.title
relevancyFunction: @computeRelevancy
tabId: tab.id
+ deDuplicate: false
onComplete suggestions
computeRelevancy: (suggestion) ->
@@ -401,7 +413,6 @@ class TabCompleter
class SearchEngineCompleter
@debug: false
- searchEngines: null
previousSuggestions: null
cancel: ->
@@ -410,9 +421,9 @@ class SearchEngineCompleter
# This looks up the custom search engine and, if one is found, notes it and removes its keyword from the
# query terms.
preprocessRequest: (request) ->
- @searchEngines.use (engines) =>
+ SearchEngines.use (engines) =>
{ queryTerms, query } = request
- request.searchEngines = engines
+ extend request, searchEngines: engines, keywords: key for own key of engines
keyword = queryTerms[0]
# Note. For a keyword "w", we match "w search terms" and "w ", but not "w" on its own.
if keyword and engines[keyword] and (1 < queryTerms.length or /\S\s/.test query)
@@ -420,29 +431,11 @@ class SearchEngineCompleter
queryTerms: queryTerms[1..]
keyword: keyword
engine: engines[keyword]
+ isCustomSearch: true
refresh: (port) ->
@previousSuggestions = {}
- # Parse the search-engine configuration.
- @searchEngines = new AsyncDataFetcher (callback) ->
- engines = {}
- for line in Settings.get("searchEngines").split "\n"
- line = line.trim()
- continue if /^[#"]/.test line
- tokens = line.split /\s+/
- continue unless 2 <= tokens.length
- keyword = tokens[0].split(":")[0]
- url = tokens[1]
- description = tokens[2..].join(" ") || "search (#{keyword})"
- continue unless Utils.hasFullUrlPrefix url
- engines[keyword] =
- keyword: keyword
- searchUrl: url
- description: description
- searchUrlPrefix: url.split("%s")[0]
-
- callback engines
-
+ SearchEngines.refreshAndUse Settings.get("searchEngines"), (engines) ->
# Let the front-end vomnibar know the search-engine keywords. It needs to know them so that, when the
# query goes from "w" to "w ", the vomnibar can synchronously launch the next filter() request (which
# avoids an ugly delay/flicker).
@@ -452,90 +445,77 @@ class SearchEngineCompleter
filter: (request, onComplete) ->
{ queryTerms, query, engine } = request
+ return onComplete [] unless engine
- { custom, searchUrl, description } =
- if engine
- { keyword, searchUrl, description } = engine
- extend request, { searchUrl, customSearchMode: true }
- custom: true
- searchUrl: searchUrl
- description: description
- else
- custom: false
- searchUrl: Settings.get "searchUrl"
- description: "search"
-
- return onComplete [] unless custom or 0 < queryTerms.length
+ { keyword, searchUrl, description } = engine
+ extend request, searchUrl, customSearchMode: true
- factor = Math.max 0.0, Math.min 1.0, Settings.get "omniSearchWeight"
- haveCompletionEngine = (0.0 < factor or custom) and CompletionSearch.haveCompletionEngine searchUrl
+ factor = 0.5
+ haveCompletionEngine = CompletionSearch.haveCompletionEngine searchUrl
# This filter is applied to all of the suggestions from all of the completers, after they have been
# aggregated by the MultiCompleter.
filter = (suggestions) ->
- if custom and haveCompletionEngine
- # We only accept suggestions:
- # - from this completer, or
- # - from other completers, but then only if their URL matches this search engine and matches this
- # query (that is only if their URL could have been generated by this search engine).
- suggestions.filter (suggestion) ->
- suggestion.type == description or
- # This is a suggestion for the same search engine.
- (suggestion.url.startsWith(engine.searchUrlPrefix) and
- # And the URL suffix (which must contain the query part) matches the current query.
- RankingUtils.matches queryTerms, suggestion.url[engine.searchUrlPrefix.length..])
-
- else if not custom
- # Filter out any suggestion which is just what the user would get if they hit <Enter> anyway. For
- # example, don't offer "https://www.google.com/search?q=vimium" if the query is "vimium".
- defaultUrl = Utils.createSearchUrl queryTerms, searchUrl
- defaultQuery = queryTerms.join " "
- suggestions.filter (suggestion) -> Utils.extractQuery(searchUrl, suggestion.url) != defaultQuery
+ suggestions.filter (suggestion) ->
+ # We only keep suggestions which either *were* generated by this search engine, or *could have
+ # been* generated by this search engine (and match the current query).
+ suggestion.isSearchSuggestion or suggestion.isCustomSearch or
+ (
+ terms = Utils.extractQuery searchUrl, suggestion.url
+ terms and RankingUtils.matches queryTerms, terms
+ )
# If a previous suggestion still matches the query, then we keep it (even if the completion engine may not
# return it for the current query). This allows the user to pick suggestions by typing fragments of their
# text, without regard to whether the completion engine can complete the actual text of the query.
previousSuggestions =
- for url, suggestion of @previousSuggestions
- continue unless RankingUtils.matches queryTerms, suggestion.title
- # Reset various fields, they may not be correct wrt. the current query.
- extend suggestion, relevancy: null, html: null, highlightTerms: true, queryTerms: queryTerms
- suggestion.relevancy = null
- suggestion
+ if queryTerms.length == 0
+ []
+ else
+ for url, suggestion of @previousSuggestions
+ continue unless RankingUtils.matches queryTerms, suggestion.title
+ # Reset various fields, they may not be correct wrt. the current query.
+ extend suggestion, relevancy: null, html: null, queryTerms: queryTerms
+ suggestion.relevancy = null
+ suggestion
primarySuggestion = new Suggestion
queryTerms: queryTerms
type: description
url: Utils.createSearchUrl queryTerms, searchUrl
title: queryTerms.join " "
- relevancy: 1
- autoSelect: custom
- highlightTerms: not haveCompletionEngine
+ relevancy: 2.0
+ autoSelect: true
+ highlightTerms: false
isSearchSuggestion: true
+ isPrimarySuggestion: true
- mkSuggestion = (suggestion) =>
- url = Utils.createSearchUrl suggestion, searchUrl
- @previousSuggestions[url] = new Suggestion
- queryTerms: queryTerms
- type: description
- url: url
- title: suggestion
- insertText: suggestion
- highlightTerms: false
- highlightTermsExcludeUrl: true
- isCustomSearch: custom
- relevancyFunction: @computeRelevancy
- relevancyData: factor
+ return onComplete [ primarySuggestion ], { filter } if queryTerms.length == 0
+
+ mkSuggestion = do =>
+ count = 0
+ (suggestion) =>
+ url = Utils.createSearchUrl suggestion, searchUrl
+ @previousSuggestions[url] = new Suggestion
+ queryTerms: queryTerms
+ type: description
+ url: url
+ title: suggestion
+ insertText: suggestion
+ highlightTerms: false
+ highlightTermsExcludeUrl: true
+ isCustomSearch: true
+ relevancy: if ++count == 1 then 1.0 else null
+ relevancyFunction: @computeRelevancy
cachedSuggestions =
if haveCompletionEngine then CompletionSearch.complete searchUrl, queryTerms else null
suggestions = previousSuggestions
- suggestions.push primarySuggestion if custom
- suggestions.push cachedSuggestions.map(mkSuggestion)... if custom and cachedSuggestions?
+ suggestions.push primarySuggestion
if queryTerms.length == 0 or cachedSuggestions? or not haveCompletionEngine
- # There is no prospect of adding further completions.
+ # There is no prospect of adding further completions, so we're done.
suggestions.push cachedSuggestions.map(mkSuggestion)... if cachedSuggestions?
onComplete suggestions, { filter, continuation: null }
else
@@ -543,20 +523,7 @@ class SearchEngineCompleter
# continuation.
onComplete suggestions,
filter: filter
- continuation: (suggestions, onComplete) =>
-
- # We can skip querying the completion engine if any new suggestions we propose will not score highly
- # enough to make the list anyway. We construct a suggestion which perfectly matches the query, and
- # ask the relevancy function what score it would get. If that score is less than the score of the
- # lowest-ranked suggestion from another completer (and there are already 10 suggestions), then
- # there's no need to query the completion engine.
- perfectRelevancyScore = @computeRelevancy new Suggestion
- queryTerms: queryTerms, title: queryTerms.join(" "), relevancyData: factor
-
- if 10 <= suggestions.length and perfectRelevancyScore < suggestions[suggestions.length-1].relevancy
- console.log "skip (cannot make the grade):", suggestions.length, query if SearchEngineCompleter.debug
- return onComplete []
-
+ continuation: (onComplete) =>
CompletionSearch.complete searchUrl, queryTerms, (suggestions = []) =>
console.log "fetched suggestions:", suggestions.length, query if SearchEngineCompleter.debug
onComplete suggestions.map mkSuggestion
@@ -567,7 +534,7 @@ class SearchEngineCompleter
# scores here, and those provided by other completers.
# - Relevancy depends only on the title (which is the search terms), and not on the URL.
Suggestion.boostRelevancyScore 0.5,
- relevancyData * RankingUtils.wordRelevancy queryTerms, title, title
+ 0.7 * RankingUtils.wordRelevancy queryTerms, title, title
postProcessSuggestions: (request, suggestions) ->
return unless request.searchEngines
@@ -582,9 +549,6 @@ class SearchEngineCompleter
# suggestion, then custom search-engine mode should be activated.
suggestion.customSearchMode = engine.keyword
suggestion.title ||= suggestion.insertText
- # NOTE(smblott) The following is disabled: experimentation with UI.
- # suggestion.highlightTermsExcludeUrl = true
- # suggestion.type = engine.description ? "custom search history"
break
# A completer which calls filter() on many completers, aggregates the results, ranks them, and returns the top
@@ -639,7 +603,7 @@ class MultiCompleter
if shouldRunContinuations
jobs = new JobRunner continuations.map (continuation) ->
(callback) ->
- continuation suggestions, (newSuggestions) ->
+ continuation (newSuggestions) ->
suggestions.push newSuggestions...
callback()
@@ -664,7 +628,7 @@ class MultiCompleter
suggestions =
for suggestion in suggestions
url = suggestion.shortenUrl()
- continue if seenUrls[url]
+ continue if suggestion.deDuplicate and seenUrls[url]
break if count++ == @maxResults
seenUrls[url] = suggestion
@@ -672,7 +636,7 @@ class MultiCompleter
completer.postProcessSuggestions? request, suggestions for completer in @completers
# Generate HTML for the remaining suggestions and return them.
- suggestion.generateHtml() for suggestion in suggestions
+ suggestion.generateHtml request for suggestion in suggestions
suggestions
# Utilities which help us compute a relevancy score for a given item.
diff --git a/background_scripts/completion_engines.coffee b/background_scripts/completion_engines.coffee
index 189f66f3..f15e6db4 100644
--- a/background_scripts/completion_engines.coffee
+++ b/background_scripts/completion_engines.coffee
@@ -21,12 +21,11 @@
# A base class for common regexp-based matching engines.
class RegexpEngine
- constructor: (@regexps) ->
+ constructor: (args...) -> @regexps = args.map (regexp) -> new RegExp regexp
match: (searchUrl) -> Utils.matchesAnyRegexp @regexps, searchUrl
# Several Google completion engines package XML responses in this way.
class GoogleXMLRegexpEngine extends RegexpEngine
- doNotCache: false # true (disbaled, experimental)
parse: (xhr) ->
for suggestion in xhr.responseXML.getElementsByTagName "suggestion"
continue unless suggestion = suggestion.getAttribute "data"
@@ -34,31 +33,47 @@ class GoogleXMLRegexpEngine extends RegexpEngine
class Google extends GoogleXMLRegexpEngine
# Example search URL: http://www.google.com/search?q=%s
- constructor: ->
- super [
- # We match the major English-speaking TLDs.
- new RegExp "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/"
- new RegExp "localhost/cgi-bin/booky" # Only for smblott.
- ]
+ constructor: (regexps = null) ->
+ super regexps ? "^https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=%s"
+# A wrapper class for Google completions. This adds prefix terms to the query, and strips those terms from
+# the resulting suggestions. For example, for Google Maps, we add "map of" as a prefix, then strip "map of"
+# from the resulting suggestions.
+class GoogleWithPrefix
+ constructor: (prefix, args...) ->
+ @engine = new Google args...
+ @prefix = "#{prefix.trim()} "
+ @queryTerms = @prefix.split /\s+/
+ match: (args...) -> @engine.match args...
+ getUrl: (queryTerms) -> @engine.getUrl [ @queryTerms..., queryTerms... ]
+ parse: (xhr) ->
+ @engine.parse(xhr)
+ .filter (suggestion) => suggestion.startsWith @prefix
+ .map (suggestion) => suggestion[@prefix.length..].ltrim()
+
+# For Google Maps, we add the prefix "map of" to the query, and send it to Google's general search engine,
+# then strip "map of" from the resulting suggestions.
+class GoogleMaps extends GoogleWithPrefix
+ # Example search URL: https://www.google.com/maps?q=%s
+ constructor: -> super "map of", "https?://[a-z]+\.google\.(com|ie|co\.uk|ca|com\.au)/maps"
+
class Youtube extends GoogleXMLRegexpEngine
# Example search URL: http://www.youtube.com/results?search_query=%s
constructor: ->
- super [ new RegExp "^https?://[a-z]+\.youtube\.com/results" ]
+ super "^https?://[a-z]+\.youtube\.com/results"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
"http://suggestqueries.google.com/complete/search?client=youtube&ds=yt&xml=t&q=%s"
class Wikipedia extends RegexpEngine
- doNotCache: false # true (disbaled, experimental)
# Example search URL: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s
constructor: ->
- super [ new RegExp "^https?://[a-z]+\.wikipedia\.org/" ]
+ super "^https?://[a-z]+\.wikipedia\.org/"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
@@ -67,28 +82,15 @@ class Wikipedia extends RegexpEngine
parse: (xhr) ->
JSON.parse(xhr.responseText)[1]
-## Does not work...
-## class GoogleMaps extends RegexpEngine
-## # Example search URL: https://www.google.com/maps/search/%s
-## constructor: ->
-## super [ new RegExp "^https?://www\.google\.com/maps/search/" ]
-##
-## getUrl: (queryTerms) ->
-## "https://www.google.com/s?tbm=map&fp=1&gs_ri=maps&source=hp&suggest=p&authuser=0&hl=en&pf=p&tch=1&ech=2&q=#{Utils.createSearchQuery queryTerms}"
-##
-## parse: (xhr) ->
-## data = JSON.parse xhr.responseText
-## []
-
class Bing extends RegexpEngine
# Example search URL: https://www.bing.com/search?q=%s
- constructor: -> super [ new RegExp "^https?://www\.bing\.com/search" ]
+ constructor: -> super "^https?://www\.bing\.com/search"
getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "http://api.bing.com/osjson.aspx?query=%s"
parse: (xhr) -> JSON.parse(xhr.responseText)[1]
class Amazon extends RegexpEngine
# Example search URL: http://www.amazon.com/s/?field-keywords=%s
- constructor: -> super [ new RegExp "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/" ]
+ constructor: -> super "^https?://www\.amazon\.(com|co.uk|ca|com.au)/s/"
getUrl: (queryTerms) ->
Utils.createSearchUrl queryTerms,
"https://completion.amazon.com/search/complete?method=completion&search-alias=aps&client=amazon-search-ui&mkt=1&q=%s"
@@ -96,15 +98,14 @@ class Amazon extends RegexpEngine
class DuckDuckGo extends RegexpEngine
# Example search URL: https://duckduckgo.com/?q=%s
- constructor: -> super [ new RegExp "^https?://([a-z]+\.)?duckduckgo\.com/" ]
- getUrl: (queryTerms) ->
+ constructor: -> super "^https?://([a-z]+\.)?duckduckgo\.com/"
getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "https://duckduckgo.com/ac/?q=%s"
parse: (xhr) ->
suggestion.phrase for suggestion in JSON.parse xhr.responseText
class Webster extends RegexpEngine
# Example search URL: http://www.merriam-webster.com/dictionary/%s
- constructor: -> super [ new RegExp "^https?://www.merriam-webster.com/dictionary/" ]
+ constructor: -> super "^https?://www.merriam-webster.com/dictionary/"
getUrl: (queryTerms) -> Utils.createSearchUrl queryTerms, "http://www.merriam-webster.com/autocomplete?query=%s"
parse: (xhr) -> JSON.parse(xhr.responseText).suggestions
@@ -120,6 +121,7 @@ class DummyCompletionEngine
# Note: Order matters here.
CompletionEngines = [
Youtube
+ GoogleMaps
Google
DuckDuckGo
Wikipedia
diff --git a/background_scripts/exclusions.coffee b/background_scripts/exclusions.coffee
index 5ec76e2a..21342d61 100644
--- a/background_scripts/exclusions.coffee
+++ b/background_scripts/exclusions.coffee
@@ -73,3 +73,7 @@ if not Settings.has("exclusionRules") and Settings.has("excludedUrls")
# We'll keep a backup of the "excludedUrls" setting, just in case.
Settings.set("excludedUrlsBackup", Settings.get("excludedUrls")) if not Settings.has("excludedUrlsBackup")
Settings.clear("excludedUrls")
+
+# Register postUpdateHook for exclusionRules setting.
+Settings.postUpdateHooks["exclusionRules"] = (value) ->
+ Exclusions.postUpdateHook value
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 6ee0e8e7..99a5672b 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -268,10 +268,10 @@ BackgroundCommands =
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")
+ nextTab: (count) -> selectTab "next", count
+ previousTab: (count) -> selectTab "previous", count
+ firstTab: -> selectTab "first"
+ lastTab: -> selectTab "last"
removeTab: (callback) ->
chrome.tabs.getSelected(null, (tab) ->
chrome.tabs.remove(tab.id)
@@ -345,21 +345,23 @@ removeTabsRelative = (direction) ->
# Selects a tab before or after the currently selected tab.
# - direction: "next", "previous", "first" or "last".
-selectTab = (callback, direction) ->
- chrome.tabs.getAllInWindow(null, (tabs) ->
+selectTab = (direction, count = 1) ->
+ chrome.tabs.getAllInWindow null, (tabs) ->
return unless tabs.length > 1
- chrome.tabs.getSelected(null, (currentTab) ->
- switch direction
- when "next"
- toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length]
- when "previous"
- toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length]
- when "first"
- toSelect = tabs[0]
- when "last"
- toSelect = tabs[tabs.length - 1]
- selectionChangedHandlers.push(callback)
- chrome.tabs.update(toSelect.id, { selected: true })))
+ chrome.tabs.getSelected null, (currentTab) ->
+ toSelect =
+ switch direction
+ when "next"
+ currentTab.index + count
+ when "previous"
+ currentTab.index - count
+ when "first"
+ 0
+ when "last"
+ tabs.length - 1
+ # Bring toSelect into the range [0,tabs.length).
+ toSelect = (toSelect + tabs.length * Math.abs count) % tabs.length
+ chrome.tabs.update tabs[toSelect].id, selected: true
updateOpenTabs = (tab, deleteFrames = false) ->
# Chrome might reuse the tab ID of a recently removed tab.
@@ -556,13 +558,13 @@ checkKeyQueue = (keysToCheck, tabId, frameId) ->
if runCommand
if not registryEntry.isBackgroundCommand
- chrome.tabs.sendMessage(tabId,
- name: "executePageCommand",
- command: registryEntry.command,
- frameId: frameId,
- count: count,
- passCountToFunction: registryEntry.passCountToFunction,
- completionKeys: generateCompletionKeys(""))
+ chrome.tabs.sendMessage tabId,
+ name: "executePageCommand"
+ command: registryEntry.command
+ frameId: frameId
+ count: count
+ completionKeys: generateCompletionKeys ""
+ registryEntry: registryEntry
refreshedCompletionKeys = true
else
if registryEntry.passCountToFunction
@@ -742,5 +744,5 @@ chrome.windows.getAll { populate: true }, (windows) ->
chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler())
# Start pulling changes from synchronized storage.
-Sync.init()
+Settings.init()
showUpgradeMessage()
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
deleted file mode 100644
index 269b4a2c..00000000
--- a/background_scripts/settings.coffee
+++ /dev/null
@@ -1,127 +0,0 @@
-#
-# Used by all parts of Vimium to manipulate localStorage.
-#
-
-root = exports ? window
-root.Settings = Settings =
- get: (key) ->
- if (key of localStorage) then JSON.parse(localStorage[key]) else @defaults[key]
-
- set: (key, value) ->
- # Don't store the value if it is equal to the default, so we can change the defaults in the future
- if (value == @defaults[key])
- @clear(key)
- else
- jsonValue = JSON.stringify value
- localStorage[key] = jsonValue
- Sync.set key, jsonValue
-
- clear: (key) ->
- if @has key
- delete localStorage[key]
- Sync.clear key
-
- has: (key) -> key of localStorage
-
- # For settings which require action when their value changes, add hooks here called from
- # options/options.coffee (when the options page is saved), and from background_scripts/sync.coffee (when an
- # update propagates from chrome.storage.sync).
- postUpdateHooks:
- keyMappings: (value) ->
- root.Commands.clearKeyMappingsAndSetDefaults()
- root.Commands.parseCustomKeyMappings value
- root.refreshCompletionKeysAfterMappingSave()
-
- exclusionRules: (value) ->
- root.Exclusions.postUpdateHook value
-
- # postUpdateHooks convenience wrapper
- performPostUpdateHook: (key, value) ->
- @postUpdateHooks[key] value if @postUpdateHooks[key]
-
- # options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans
- # or strings
- defaults:
- scrollStepSize: 60
- omniSearchWeight: 0.4
- smoothScroll: true
- keyMappings: "# Insert your preferred key mappings here."
- linkHintCharacters: "sadfjklewcmpgh"
- linkHintNumbers: "0123456789"
- filterLinkHints: false
- hideHud: false
- userDefinedLinkHintCss:
- """
- div > .vimiumHintMarker {
- /* linkhint boxes */
- background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785),
- color-stop(100%,#FFC542));
- border: 1px solid #E3BE23;
- }
-
- div > .vimiumHintMarker span {
- /* linkhint text */
- color: black;
- font-weight: bold;
- font-size: 12px;
- }
-
- div > .vimiumHintMarker > .matchingCharacter {
- }
- """
- # Default exclusion rules.
- exclusionRules:
- [
- # Disable Vimium on Gmail.
- { pattern: "http*://mail.google.com/*", passKeys: "" }
- ]
-
- # NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in
- # most cases the single bracket link will be "prev/next page" and the double bracket link will be
- # "first/last page", so we put the single bracket first in the pattern string so that it gets searched
- # for first.
-
- # "\bprev\b,\bprevious\b,\bback\b,<,←,«,≪,<<"
- previousPatterns: "prev,previous,back,<,\u2190,\xab,\u226a,<<"
- # "\bnext\b,\bmore\b,>,→,»,≫,>>"
- nextPatterns: "next,more,>,\u2192,\xbb,\u226b,>>"
- # default/fall back search engine
- searchUrl: "https://www.google.com/search?q="
- # put in an example search engine
- searchEngines: [
- "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
- ""
- "# More examples."
- "#"
- "# (Vimium has built-in completion for these.)"
- "#"
- "# g: http://www.google.com/search?q=%s Google"
- "# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
- "# y: http://www.youtube.com/results?search_query=%s Youtube"
- "# b: https://www.bing.com/search?q=%s Bing"
- "# d: https://duckduckgo.com/?q=%s DuckDuckGo"
- "# az: http://www.amazon.com/s/?field-keywords=%s Amazon"
- "#"
- "# Another example (for Vimium does not have completion)."
- "#"
- "# m: https://www.google.com/maps/search/%s Google Maps"
- ].join "\n"
- newTabUrl: "chrome://newtab"
- grabBackFocus: false
-
- settingsVersion: Utils.getCurrentVersion()
-
-
-# We use settingsVersion to coordinate any necessary schema changes.
-if Utils.compareVersions("1.42", Settings.get("settingsVersion")) != -1
- Settings.set("scrollStepSize", parseFloat Settings.get("scrollStepSize"))
-Settings.set("settingsVersion", Utils.getCurrentVersion())
-
-# Migration (after 1.49, 2015/2/1).
-# Legacy setting: findModeRawQuery (a string).
-# New setting: findModeRawQueryList (a list of strings), now stored in chrome.storage.local (not localStorage).
-chrome.storage.local.get "findModeRawQueryList", (items) ->
- unless chrome.runtime.lastError or items.findModeRawQueryList
- rawQuery = Settings.get "findModeRawQuery"
- chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else [])
-
diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee
deleted file mode 100644
index d0d501d3..00000000
--- a/background_scripts/sync.coffee
+++ /dev/null
@@ -1,74 +0,0 @@
-#
-# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync.
-# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those
-# changes to localStorage and into vimium's internal state.
-# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating
-# changes to localStorage and into vimium's internal state.
-#
-# Changes are propagated into vimium's state using the same mechanism
-# (Settings.performPostUpdateHook) that is used when options are changed on
-# the options page.
-#
-# The effect is best-effort synchronization of vimium options/settings between
-# chrome/vimium instances.
-#
-# NOTE:
-# Values handled within this module are ALWAYS already JSON.stringifed, so
-# they're always non-empty strings.
-#
-
-root = exports ? window
-root.Sync = Sync =
-
- storage: chrome.storage.sync
- doNotSync: ["settingsVersion", "previousVersion"]
-
- # This is called in main.coffee.
- init: ->
- chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area
- @fetchAsync()
-
- # Asynchronous fetch from synced storage, called only at startup.
- fetchAsync: ->
- @storage.get null, (items) =>
- unless chrome.runtime.lastError
- for own key, value of items
- @storeAndPropagate key, value
-
- # Asynchronous message from synced storage.
- handleStorageUpdate: (changes, area) ->
- for own key, change of changes
- @storeAndPropagate key, change?.newValue
-
- # Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate).
- storeAndPropagate: (key, value) ->
- return unless key of Settings.defaults
- return if not @shouldSyncKey key
- return if value and key of localStorage and localStorage[key] is value
- defaultValue = Settings.defaults[key]
- defaultValueJSON = JSON.stringify(defaultValue)
-
- if value and value != defaultValueJSON
- # Key/value has been changed to non-default value at remote instance.
- localStorage[key] = value
- Settings.performPostUpdateHook key, JSON.parse(value)
- else
- # Key has been reset to default value at remote instance.
- if key of localStorage
- delete localStorage[key]
- Settings.performPostUpdateHook key, defaultValue
-
- # Only called synchronously from within vimium, never on a callback.
- # No need to propagate updates to the rest of vimium, that's already been done.
- set: (key, value) ->
- if @shouldSyncKey key
- setting = {}; setting[key] = value
- @storage.set setting
-
- # Only called synchronously from within vimium, never on a callback.
- clear: (key) ->
- @storage.remove key if @shouldSyncKey key
-
- # Should we synchronize this key?
- shouldSyncKey: (key) -> key not in @doNotSync
-
diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee
index e07d0713..f38d6b45 100644
--- a/content_scripts/hud.coffee
+++ b/content_scripts/hud.coffee
@@ -57,6 +57,12 @@ class Tween
constructor: (@cssSelector, insertionPoint = document.documentElement) ->
@styleElement = document.createElement "style"
+
+ unless @styleElement.style
+ # We're in an XML document, so we shouldn't inject any elements. See the comment in UIComponent.
+ Tween::fade = Tween::stop = Tween::updateStyle = ->
+ return
+
@styleElement.type = "text/css"
@styleElement.innerHTML = ""
insertionPoint.appendChild @styleElement
diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee
index 326ee8c9..8d1d96cc 100644
--- a/content_scripts/mode_visual_edit.coffee
+++ b/content_scripts/mode_visual_edit.coffee
@@ -143,7 +143,12 @@ class Movement extends CountPrefix
# Test whether the character following the focus is a word character (and leave the selection unchanged).
nextCharacterIsWordCharacter: do ->
- regexp = /[A-Za-z0-9_]/; -> regexp.test @getNextForwardCharacter()
+ regexp = null
+ ->
+ # This regexp matches "word" characters (apparently in any language).
+ # From http://stackoverflow.com/questions/150033/regular-expression-to-match-non-english-characters
+ regexp || = /[_0-9\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]/
+ regexp.test @getNextForwardCharacter()
# Run a movement. This is the core movement method, all movements happen here. For convenience, the
# following three argument forms are supported:
diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee
index e7cd3f82..e4cfc293 100644
--- a/content_scripts/ui_component.coffee
+++ b/content_scripts/ui_component.coffee
@@ -7,6 +7,18 @@ class UIComponent
constructor: (iframeUrl, className, @handleMessage) ->
styleSheet = document.createElement "style"
+
+ unless styleSheet.style
+ # If this is an XML document, nothing we do here works:
+ # * <style> elements show their contents inline,
+ # * <iframe> elements don't load any content,
+ # * document.createElement generates elements that have style == null and ignore CSS.
+ # If this is the case we don't want to pollute the DOM to no or negative effect. So we bail
+ # immediately, and disable all externally-called methods.
+ @postMessage = @activate = @show = @hide = ->
+ console.log "This vimium feature is disabled because it is incompatible with this page."
+ return
+
styleSheet.type = "text/css"
# Default to everything hidden while the stylesheet loads.
styleSheet.innerHTML = "@import url(\"#{chrome.runtime.getURL("content_scripts/vimium.css")}\");"
@@ -15,19 +27,33 @@ class UIComponent
extend @iframeElement,
className: className
seamless: "seamless"
- src: chrome.runtime.getURL iframeUrl
- @iframeElement.addEventListener "load", => @openPort()
shadowWrapper = document.createElement "div"
# PhantomJS doesn't support createShadowRoot, so guard against its non-existance.
@shadowDOM = shadowWrapper.createShadowRoot?() ? shadowWrapper
@shadowDOM.appendChild styleSheet
@shadowDOM.appendChild @iframeElement
- document.documentElement.appendChild shadowWrapper
@showing = true # The iframe is visible now.
# Hide the iframe, but don't interfere with the focus.
@hide false
+ # Open a port and pass it to the iframe via window.postMessage. We use an AsyncDataFetcher to handle
+ # requests which arrive before the iframe (and its message handlers) have completed initialization. See
+ # #1679.
+ @iframePort = new AsyncDataFetcher (setIframePort) =>
+ # We set the iframe source and append the new element here (as opposed to above) to avoid a potential
+ # race condition vis-a-vis the "load" event (because this callback runs on "nextTick").
+ @iframeElement.src = chrome.runtime.getURL iframeUrl
+ document.documentElement.appendChild shadowWrapper
+
+ @iframeElement.addEventListener "load", =>
+ # Get vimiumSecret so the iframe can determine that our message isn't the page impersonating us.
+ chrome.storage.local.get "vimiumSecret", ({ vimiumSecret }) =>
+ { port1, port2 } = new MessageChannel
+ port1.onmessage = (event) => @handleMessage event
+ @iframeElement.contentWindow.postMessage vimiumSecret, chrome.runtime.getURL(""), [ port2 ]
+ setIframePort port1
+
# If any other frame in the current tab receives the focus, then we hide the UI component.
# NOTE(smblott) This is correct for the vomnibar, but might be incorrect (and need to be revisited) for
# other UI components.
@@ -35,40 +61,31 @@ class UIComponent
@postMessage "hide" if @showing and request.name == "frameFocused" and request.focusFrameId != frameId
false # Free up the sendResponse handler.
- # Open a port and pass it to the iframe via window.postMessage.
- openPort: ->
- messageChannel = new MessageChannel()
- @iframePort = messageChannel.port1
- @iframePort.onmessage = (event) => @handleMessage event
-
- # Get vimiumSecret so the iframe can determine that our message isn't the page impersonating us.
- chrome.storage.local.get "vimiumSecret", ({vimiumSecret: secret}) =>
- @iframeElement.contentWindow.postMessage secret, chrome.runtime.getURL(""), [messageChannel.port2]
-
- # Posts a message; returns true if the message was sent, false otherwise.
- postMessage: (message) ->
- # We use "?" here because the iframe port is initialized asynchronously, and may not yet be ready.
- @iframePort?.postMessage message
- @iframePort?
+ # Posts a message (if one is provided), then calls continuation (if provided). The continuation is only
+ # ever called *after* the message has been posted.
+ postMessage: (message = null, continuation = null) ->
+ @iframePort.use (port) =>
+ port.postMessage message if message?
+ continuation?()
activate: (@options) ->
- if @postMessage @options
+ @postMessage @options, =>
@show() unless @showing
@iframeElement.focus()
show: (message) ->
- @postMessage message if message?
- @iframeElement.classList.remove "vimiumUIComponentHidden"
- @iframeElement.classList.add "vimiumUIComponentVisible"
- # The window may not have the focus. We focus it now, to prevent the "focus" listener below from firing
- # immediately.
- window.focus()
- window.addEventListener "focus", @onFocus = (event) =>
- if event.target == window
- window.removeEventListener "focus", @onFocus
- @onFocus = null
- @postMessage "hide"
- @showing = true
+ @postMessage message, =>
+ @iframeElement.classList.remove "vimiumUIComponentHidden"
+ @iframeElement.classList.add "vimiumUIComponentVisible"
+ # The window may not have the focus. We focus it now, to prevent the "focus" listener below from firing
+ # immediately.
+ window.focus()
+ window.addEventListener "focus", @onFocus = (event) =>
+ if event.target == window
+ window.removeEventListener "focus", @onFocus
+ @onFocus = null
+ @postMessage "hide"
+ @showing = true
hide: (focusWindow = true)->
@refocusSourceFrame @options?.sourceFrameId if focusWindow
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index 647c8025..b4bce776 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -208,13 +208,18 @@ div#vimiumHelpDialog a {
text-decoration: underline;
}
-div#vimiumHelpDialog .optionsPage {
+div#vimiumHelpDialog .wikiPage, div#vimiumHelpDialog .optionsPage {
position: absolute;
display: block;
font-size: 11px;
line-height: 130%;
- right: 60px;
- top: 8px;
+ top: 6px;
+}
+div#vimiumHelpDialog .optionsPage {
+ right: 40px;
+}
+div#vimiumHelpDialog .wikiPage {
+ right: 83px;
}
div#vimiumHelpDialog a.closeButton:hover {
color:black;
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 7e0416aa..c8c83029 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -62,6 +62,7 @@ settings =
helpDialog_showAdvancedCommands: null
smoothScroll: null
grabBackFocus: null
+ searchEngines: null
init: ->
@port = chrome.runtime.connect name: "settings"
@@ -307,14 +308,14 @@ executePageCommand = (request) ->
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.
- Utils.invokeCommandString request.command, [ request.frameId ]
+ Utils.invokeCommandString request.command, [ request.frameId, request.registryEntry ]
refreshCompletionKeys request
return
# All other commands are handled in their frame (but only if Vimium is enabled).
return unless frameId == request.frameId and isEnabledForUrl
- if (request.passCountToFunction)
+ if request.registryEntry.passCountToFunction
Utils.invokeCommandString(request.command, [request.count])
else
Utils.invokeCommandString(request.command) for i in [0...request.count]
@@ -718,21 +719,16 @@ updateFindModeQuery = ->
# character. here we grep for the relevant escape sequences.
findModeQuery.isRegex = settings.get 'regexFindMode'
hasNoIgnoreCaseFlag = false
- findModeQuery.parsedQuery = findModeQuery.rawQuery.replace /\\./g, (match) ->
- switch (match)
- when "\\r"
+ findModeQuery.parsedQuery = findModeQuery.rawQuery.replace /(\\{1,2})([rRI]?)/g, (match, slashes, flag) ->
+ return match if flag == "" or slashes.length != 1
+ switch (flag)
+ when "r"
findModeQuery.isRegex = true
- return ""
- when "\\R"
+ when "R"
findModeQuery.isRegex = false
- return ""
- when "\\I"
+ when "I"
hasNoIgnoreCaseFlag = true
- return ""
- when "\\\\"
- return "\\"
- else
- return match
+ ""
# default to 'smartcase' mode, unless noIgnoreCase is explicitly specified
findModeQuery.ignoreCase = !hasNoIgnoreCaseFlag && !Utils.hasUpperCase(findModeQuery.parsedQuery)
@@ -1125,6 +1121,8 @@ window.showHelpDialog = (html, fid) ->
chrome.runtime.sendMessage({handler: "openOptionsPageInNewTab"})
false)
+ # Simulating a click on the help dialog makes it the active element for scrolling.
+ DomUtils.simulateClick document.getElementById "vimiumHelpDialog"
hideHelpDialog = (clickEvent) ->
isShowingHelpDialog = false
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index 2529c077..4bd8e8fd 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -4,14 +4,36 @@
Vomnibar =
vomnibarUI: null
+ # Parse any additional options from the command's registry entry. Currently, this only includes a flag of
+ # the form "keyword=X", for direct activation of a custom search engine.
+ parseRegistryEntry: (registryEntry = { options: [] }, callback = null) ->
+ options = {}
+ searchEngines = settings.get("searchEngines") ? ""
+ SearchEngines.refreshAndUse searchEngines, (engines) ->
+ for option in registryEntry.options
+ [ key, value ] = option.split "="
+ switch key
+ when "keyword"
+ if value? and engines[value]?
+ options.keyword = value
+ else
+ console.log "Vimium configuration error: no such custom search engine: #{option}."
+ else
+ console.log "Vimium configuration error: unused flag: #{option}."
+
+ callback? options
+
# sourceFrameId here (and below) is the ID of the frame from which this request originates, which may be different
# from the current frame.
- activate: (sourceFrameId) -> @open sourceFrameId, {completer:"omni"}
- activateInNewTab: (sourceFrameId) -> @open sourceFrameId, {
- completer: "omni"
- selectFirst: false
- newTab: true
- }
+
+ activate: (sourceFrameId, registryEntry) ->
+ @parseRegistryEntry registryEntry, (options) =>
+ @open sourceFrameId, extend options, completer:"omni"
+
+ activateInNewTab: (sourceFrameId, registryEntry) ->
+ @parseRegistryEntry registryEntry, (options) =>
+ @open sourceFrameId, extend options, completer:"omni", newTab: true
+
activateTabSelection: (sourceFrameId) -> @open sourceFrameId, {
completer: "tabs"
selectFirst: true
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 30b9f68c..7c47179c 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -62,17 +62,29 @@ DomUtils =
# Note: this call will be expensive if we modify the DOM in between calls.
clientRects = (Rect.copy clientRect for clientRect in element.getClientRects())
+ # Inline elements with font-size: 0px; will declare a height of zero, even if a child with non-zero
+ # font-size contains text.
+ isInlineZeroHeight = ->
+ elementComputedStyle = window.getComputedStyle element, null
+ isInlineZeroFontSize = (0 == elementComputedStyle.getPropertyValue("display").indexOf "inline") and
+ (elementComputedStyle.getPropertyValue("font-size") == "0px")
+ # Override the function to return this value for the rest of this context.
+ isInlineZeroHeight = -> isInlineZeroFontSize
+ isInlineZeroFontSize
+
for clientRect in clientRects
# If the link has zero dimensions, it may be wrapping visible but floated elements. Check for this.
if (clientRect.width == 0 or clientRect.height == 0) and testChildren
for child in element.children
computedStyle = window.getComputedStyle(child, null)
- # Ignore child elements which are not floated and not absolutely positioned for parent elements with
- # zero width/height
+ # Ignore child elements which are not floated and not absolutely positioned for parent elements
+ # with zero width/height, as long as the case described at isInlineZeroHeight does not apply.
# NOTE(mrmr1993): This ignores floated/absolutely positioned descendants nested within inline
# children.
- continue if (computedStyle.getPropertyValue('float') == 'none' &&
- computedStyle.getPropertyValue('position') != 'absolute')
+ continue if (computedStyle.getPropertyValue("float") == "none" and
+ computedStyle.getPropertyValue("position") != "absolute" and
+ not (clientRect.height == 0 and isInlineZeroHeight() and
+ 0 == computedStyle.getPropertyValue("display").indexOf "inline"))
childClientRect = @getVisibleClientRect child, true
continue if childClientRect == null or childClientRect.width < 3 or childClientRect.height < 3
return childClientRect
diff --git a/lib/settings.coffee b/lib/settings.coffee
new file mode 100644
index 00000000..dd667dbd
--- /dev/null
+++ b/lib/settings.coffee
@@ -0,0 +1,202 @@
+#
+# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync.
+# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those
+# changes to localStorage and into vimium's internal state.
+# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating
+# changes to localStorage and into vimium's internal state.
+#
+# The effect is best-effort synchronization of vimium options/settings between
+# chrome/vimium instances.
+#
+# NOTE:
+# Values handled within this module are ALWAYS already JSON.stringifed, so
+# they're always non-empty strings.
+#
+
+root = exports ? window
+Sync =
+
+ storage: chrome.storage.sync
+ doNotSync: ["settingsVersion", "previousVersion"]
+
+ # This is called in main.coffee.
+ init: ->
+ chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area
+ @fetchAsync()
+
+ # Asynchronous fetch from synced storage, called only at startup.
+ fetchAsync: ->
+ @storage.get null, (items) =>
+ unless chrome.runtime.lastError
+ for own key, value of items
+ Settings.storeAndPropagate key, value if @shouldSyncKey key
+
+ # Asynchronous message from synced storage.
+ handleStorageUpdate: (changes, area) ->
+ for own key, change of changes
+ Settings.storeAndPropagate key, change?.newValue if @shouldSyncKey key
+
+ # Only called synchronously from within vimium, never on a callback.
+ # No need to propagate updates to the rest of vimium, that's already been done.
+ set: (key, value) ->
+ if @shouldSyncKey key
+ setting = {}; setting[key] = value
+ @storage.set setting
+
+ # Only called synchronously from within vimium, never on a callback.
+ clear: (key) ->
+ @storage.remove key if @shouldSyncKey key
+
+ # Should we synchronize this key?
+ shouldSyncKey: (key) -> key not in @doNotSync
+
+#
+# Used by all parts of Vimium to manipulate localStorage.
+#
+
+# Select the object to use as the cache for settings.
+if Utils.isExtensionPage()
+ if Utils.isBackgroundPage()
+ settingsCache = localStorage
+ else
+ settingsCache = extend {}, localStorage # Make a copy of the cached settings from localStorage
+else
+ settingsCache = {}
+
+root.Settings = Settings =
+ cache: settingsCache
+ init: -> Sync.init()
+ get: (key) ->
+ if (key of @cache) then JSON.parse(@cache[key]) else @defaults[key]
+
+ set: (key, value) ->
+ # Don't store the value if it is equal to the default, so we can change the defaults in the future
+ if (value == @defaults[key])
+ @clear(key)
+ else
+ jsonValue = JSON.stringify value
+ @cache[key] = jsonValue
+ Sync.set key, jsonValue
+
+ clear: (key) ->
+ if @has key
+ delete @cache[key]
+ Sync.clear key
+
+ has: (key) -> key of @cache
+
+ # For settings which require action when their value changes, add hooks to this object, to be called from
+ # options/options.coffee (when the options page is saved), and by Settings.storeAndPropagate (when an
+ # update propagates from chrome.storage.sync).
+ postUpdateHooks: {}
+
+ # postUpdateHooks convenience wrapper
+ performPostUpdateHook: (key, value) ->
+ @postUpdateHooks[key]? value
+
+ # Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate).
+ storeAndPropagate: (key, value) ->
+ return unless key of @defaults
+ return if value and key of @cache and @cache[key] is value
+ defaultValue = @defaults[key]
+ defaultValueJSON = JSON.stringify(defaultValue)
+
+ if value and value != defaultValueJSON
+ # Key/value has been changed to non-default value at remote instance.
+ @cache[key] = value
+ @performPostUpdateHook key, JSON.parse(value)
+ else
+ # Key has been reset to default value at remote instance.
+ if key of @cache
+ delete @cache[key]
+ @performPostUpdateHook key, defaultValue
+
+ # options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans
+ # or strings
+ defaults:
+ scrollStepSize: 60
+ smoothScroll: true
+ keyMappings: "# Insert your preferred key mappings here."
+ linkHintCharacters: "sadfjklewcmpgh"
+ linkHintNumbers: "0123456789"
+ filterLinkHints: false
+ hideHud: false
+ userDefinedLinkHintCss:
+ """
+ div > .vimiumHintMarker {
+ /* linkhint boxes */
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785),
+ color-stop(100%,#FFC542));
+ border: 1px solid #E3BE23;
+ }
+
+ div > .vimiumHintMarker span {
+ /* linkhint text */
+ color: black;
+ font-weight: bold;
+ font-size: 12px;
+ }
+
+ div > .vimiumHintMarker > .matchingCharacter {
+ }
+ """
+ # Default exclusion rules.
+ exclusionRules:
+ [
+ # Disable Vimium on Gmail.
+ { pattern: "http*://mail.google.com/*", passKeys: "" }
+ ]
+
+ # NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in
+ # most cases the single bracket link will be "prev/next page" and the double bracket link will be
+ # "first/last page", so we put the single bracket first in the pattern string so that it gets searched
+ # for first.
+
+ # "\bprev\b,\bprevious\b,\bback\b,<,←,«,≪,<<"
+ previousPatterns: "prev,previous,back,<,\u2190,\xab,\u226a,<<"
+ # "\bnext\b,\bmore\b,>,→,»,≫,>>"
+ nextPatterns: "next,more,>,\u2192,\xbb,\u226b,>>"
+ # default/fall back search engine
+ searchUrl: "https://www.google.com/search?q="
+ # put in an example search engine
+ searchEngines: [
+ "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
+ ""
+ "# More examples."
+ "#"
+ "# (Vimium has built-in completion for these.)"
+ "#"
+ "# g: http://www.google.com/search?q=%s Google"
+ "# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
+ "# y: http://www.youtube.com/results?search_query=%s Youtube"
+ "# b: https://www.bing.com/search?q=%s Bing"
+ "# d: https://duckduckgo.com/?q=%s DuckDuckGo"
+ "# az: http://www.amazon.com/s/?field-keywords=%s Amazon"
+ "#"
+ "# Another example (for Vimium does not have completion)."
+ "#"
+ "# m: https://www.google.com/maps/search/%s Google Maps"
+ ].join "\n"
+ newTabUrl: "chrome://newtab"
+ grabBackFocus: false
+
+ settingsVersion: Utils.getCurrentVersion()
+
+# Export Sync via Settings for tests.
+root.Settings.Sync = Sync
+
+# Perform migration from old settings versions, if this is the background page.
+if Utils.isBackgroundPage()
+
+ # We use settingsVersion to coordinate any necessary schema changes.
+ if Utils.compareVersions("1.42", Settings.get("settingsVersion")) != -1
+ Settings.set("scrollStepSize", parseFloat Settings.get("scrollStepSize"))
+ Settings.set("settingsVersion", Utils.getCurrentVersion())
+
+ # Migration (after 1.49, 2015/2/1).
+ # Legacy setting: findModeRawQuery (a string).
+ # New setting: findModeRawQueryList (a list of strings), now stored in chrome.storage.local (not localStorage).
+ chrome.storage.local.get "findModeRawQueryList", (items) ->
+ unless chrome.runtime.lastError or items.findModeRawQueryList
+ rawQuery = Settings.get "findModeRawQuery"
+ chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else [])
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 65e26b7a..93045f32 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -2,6 +2,13 @@ Utils =
getCurrentVersion: ->
chrome.runtime.getManifest().version
+ # Returns true whenever the current page is from the extension's origin (and thus can access the
+ # extension's localStorage).
+ isExtensionPage: -> document.location?.origin + "/" == chrome.extension.getURL ""
+
+ # Returns true whenever the current page is the extension's background page.
+ isBackgroundPage: -> @isExtensionPage() and chrome.extension.getBackgroundPage() == window
+
# Takes a dot-notation object string and call the function
# that it points to with the correct value for 'this'.
invokeCommandString: (str, argArray) ->
@@ -228,6 +235,38 @@ Utils =
# Like Nodejs's nextTick.
nextTick: (func) -> @setTimeout 0, func
+# Utility for parsing and using the custom search-engine configuration. We re-use the previous parse if the
+# search-engine configuration is unchanged.
+SearchEngines =
+ previousSearchEngines: null
+ searchEngines: null
+
+ refresh: (searchEngines) ->
+ unless @previousSearchEngines? and searchEngines == @previousSearchEngines
+ @previousSearchEngines = searchEngines
+ @searchEngines = new AsyncDataFetcher (callback) ->
+ engines = {}
+ for line in searchEngines.split "\n"
+ line = line.trim()
+ continue if /^[#"]/.test line
+ tokens = line.split /\s+/
+ continue unless 2 <= tokens.length
+ keyword = tokens[0].split(":")[0]
+ searchUrl = tokens[1]
+ description = tokens[2..].join(" ") || "search (#{keyword})"
+ continue unless Utils.hasFullUrlPrefix searchUrl
+ engines[keyword] = { keyword, searchUrl, description }
+
+ callback engines
+
+ # Use the parsed search-engine configuration, possibly asynchronously.
+ use: (callback) ->
+ @searchEngines.use callback
+
+ # Both set (refresh) the search-engine configuration and use it at the same time.
+ refreshAndUse: (searchEngines, callback) ->
+ @refresh searchEngines
+ @use callback
# This creates a new function out of an existing function, where the new function takes fewer arguments. This
# allows us to pass around functions instead of functions + a partial list of arguments.
@@ -325,6 +364,7 @@ class JobRunner
root = exports ? window
root.Utils = Utils
+root.SearchEngines = SearchEngines
root.SimpleCache = SimpleCache
root.AsyncDataFetcher = AsyncDataFetcher
root.JobRunner = JobRunner
diff --git a/manifest.json b/manifest.json
index fe5c69ca..f0c51117 100644
--- a/manifest.json
+++ b/manifest.json
@@ -9,10 +9,9 @@
"background": {
"scripts": [
"lib/utils.js",
+ "lib/settings.js",
"background_scripts/commands.js",
"lib/clipboard.js",
- "background_scripts/sync.js",
- "background_scripts/settings.js",
"background_scripts/exclusions.js",
"background_scripts/completion_engines.js",
"background_scripts/completion_search.js",
diff --git a/pages/help_dialog.html b/pages/help_dialog.html
index 77c3e2bf..5c09c0ab 100644
--- a/pages/help_dialog.html
+++ b/pages/help_dialog.html
@@ -7,6 +7,7 @@
page with the up-to-date key bindings when the dialog is shown. -->
<div id="vimiumHelpDialog" class="vimiumReset">
<a class="vimiumReset optionsPage" href="#">Options</a>
+ <a class="vimiumReset wikiPage" href="https://github.com/philc/vimium/wiki" target="_blank">Wiki</a>
<a class="vimiumReset closeButton" href="#">&times;</a>
<div id="vimiumTitle" class="vimiumReset"><span class="vimiumReset" style="color:#2f508e">Vim</span>ium {{title}}</div>
<div class="vimiumReset vimiumColumn">
@@ -20,6 +21,8 @@
<div class="vimiumReset vimiumColumn">
<table class="vimiumReset" >
<tbody class="vimiumReset">
+ <tr class="vimiumReset" ><td class="vimiumReset" ></td><td class="vimiumReset" ></td><td class="vimiumReset vimiumHelpSectionTitle">Using the vomnibar</td></tr>
+ {{vomnibarCommands}}
<tr class="vimiumReset" ><td class="vimiumReset" ></td><td class="vimiumReset" ></td><td class="vimiumReset vimiumHelpSectionTitle">Using find</td></tr>
{{findCommands}}
<tr class="vimiumReset" ><td class="vimiumReset" ></td><td class="vimiumReset" ></td><td class="vimiumReset vimiumHelpSectionTitle">Navigating history</td></tr>
diff --git a/pages/options.coffee b/pages/options.coffee
index 18ff226d..110f869c 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -1,7 +1,6 @@
$ = (id) -> document.getElementById id
-bgUtils = chrome.extension.getBackgroundPage().Utils
-bgSettings = chrome.extension.getBackgroundPage().Settings
+Settings.init()
bgExclusions = chrome.extension.getBackgroundPage().Exclusions
#
@@ -22,21 +21,20 @@ class Option
# Fetch a setting from localStorage, remember the @previous value and populate the DOM element.
# Return the fetched value.
fetch: ->
- @populateElement @previous = bgSettings.get @field
+ @populateElement @previous = Settings.get @field
@previous
# Write this option's new value back to localStorage, if necessary.
save: ->
value = @readValueFromElement()
if not @areEqual value, @previous
- bgSettings.set @field, @previous = value
- bgSettings.performPostUpdateHook @field, value
+ Settings.set @field, @previous = value
# Compare values; this is overridden by sub-classes.
areEqual: (a,b) -> a == b
restoreToDefault: ->
- bgSettings.clear @field
+ Settings.clear @field
@fetch()
# Static method.
diff --git a/pages/options.html b/pages/options.html
index 75089d75..67e2b16d 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -3,6 +3,7 @@
<title>Vimium Options</title>
<link rel="stylesheet" type="text/css" href="options.css">
<script src="content_script_loader.js"></script>
+ <script type="text/javascript" src="../lib/settings.js"></script>
<script type="text/javascript" src="options.js"></script>
</head>
@@ -71,6 +72,9 @@ b: http://b.com/?q=%s description
</tr>
<tbody id='advancedOptions'>
<tr>
+ <td colspan="2"><header>Advanced Options</header></td>
+ </tr>
+ <tr>
<td class="caption">Scroll step size</td>
<td>
<div class="help">
@@ -235,6 +239,8 @@ b: http://b.com/?q=%s description
</tr>
<!-- Vimium Labs -->
+ <!--
+ Disabled. But we leave this code here as a template for the next time we need to introduce "Vimium Labs".
<tr>
<td colspan="2"><header>Vimium Labs</header></td>
</tr>
@@ -260,6 +266,7 @@ b: http://b.com/?q=%s description
<input id="omniSearchWeight" type="number" min="0.0" max="1.0" step="0.05" />(0 to 1)
</td>
</tr>
+ -->
</tbody>
</table>
</div>
diff --git a/pages/popup.html b/pages/popup.html
index c7e2fd6f..fdf116e5 100644
--- a/pages/popup.html
+++ b/pages/popup.html
@@ -48,6 +48,8 @@
}
</style>
+ <script src="../lib/utils.js"></script>
+ <script src="../lib/settings.js"></script>
<script src="options.js"></script>
</head>
<body>
diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee
index fd7fd3cc..d5659fdc 100644
--- a/pages/vomnibar.coffee
+++ b/pages/vomnibar.coffee
@@ -17,6 +17,7 @@ Vomnibar =
query: ""
newTab: false
selectFirst: false
+ keyword: null
extend options, userOptions
extend options, refreshInterval: if options.completer == "omni" then 150 else 0
@@ -28,6 +29,7 @@ Vomnibar =
@vomnibarUI.setRefreshInterval options.refreshInterval
@vomnibarUI.setForceNewTab options.newTab
@vomnibarUI.setQuery options.query
+ @vomnibarUI.setKeyword options.keyword
@vomnibarUI.update true
hide: -> @vomnibarUI?.hide()
@@ -40,6 +42,7 @@ class VomnibarUI
@initDom()
setQuery: (query) -> @input.value = query
+ setKeyword: (keyword) -> @customSearchMode = keyword
setInitialSelectionValue: (@initialSelectionValue) ->
setRefreshInterval: (@refreshInterval) ->
setForceNewTab: (@forceNewTab) ->
@@ -76,7 +79,7 @@ class VomnibarUI
updateSelection: ->
# For custom search engines, we suppress the leading term (e.g. the "w" of "w query terms") within the
# vomnibar input.
- if @lastReponse.customSearchMode and not @customSearchMode?
+ if @lastReponse.isCustomSearch and not @customSearchMode?
queryTerms = @input.value.trim().split /\s+/
@customSearchMode = queryTerms[0]
@input.value = queryTerms[1..].join " "
@@ -126,9 +129,12 @@ class VomnibarUI
if (action == "dismiss")
@hide()
else if action in [ "tab", "down" ]
- if @input.value.trim().length == 0 and action == "tab" and not @seenTabToOpenCompletionList
- @seenTabToOpenCompletionList = true
- @update true
+ if action == "tab" and
+ @completer.name == "omni" and
+ not @seenTabToOpenCompletionList and
+ @input.value.trim().length == 0
+ @seenTabToOpenCompletionList = true
+ @update true
else
@selection += 1
@selection = @initialSelectionValue if @selection == @completions.length
@@ -138,15 +144,23 @@ class VomnibarUI
@selection = @completions.length - 1 if @selection < @initialSelectionValue
@updateSelection()
else if (action == "enter")
- if @selection == -1
+ isCustomSearchPrimarySuggestion = @completions[@selection]?.isPrimarySuggestion and @lastReponse.engine?.searchUrl?
+ if @selection == -1 or isCustomSearchPrimarySuggestion
query = @input.value.trim()
# <Enter> on an empty query is a no-op.
return unless 0 < query.length
+ # First case (@selection == -1).
# If the user types something and hits enter without selecting a completion from the list, then:
# - If a search URL has been provided, then use it. This is custom search engine request.
# - Otherwise, send the query to the background page, which will open it as a URL or create a
# default search, as appropriate.
- query = Utils.createSearchUrl query, @lastReponse.searchUrl if @lastReponse.searchUrl?
+ #
+ # Second case (isCustomSearchPrimarySuggestion).
+ # Alternatively, the selected completion could be the primary selection for a custom search engine.
+ # Because the the suggestions are updated asynchronously in omni mode, the user may have typed more
+ # text than that which is included in the URL associated with the primary suggestion. Therefore, to
+ # avoid a race condition, we construct the query from the actual contents of the input (query).
+ query = Utils.createSearchUrl query, @lastReponse.engine.searchUrl if isCustomSearchPrimarySuggestion
@hide ->
chrome.runtime.sendMessage
handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
diff --git a/pages/vomnibar.css b/pages/vomnibar.css
index b1ed0252..1b19daad 100644
--- a/pages/vomnibar.css
+++ b/pages/vomnibar.css
@@ -126,7 +126,6 @@
#vomnibar li em { font-style: italic; }
#vomnibar li em .vomnibarMatch, #vomnibar li .vomnibarTitle .vomnibarMatch {
color: #333;
- text-decoration: underline;
}
#vomnibar li.vomnibarSelected {
diff --git a/tests/dom_tests/dom_utils_test.coffee b/tests/dom_tests/dom_utils_test.coffee
index e98dc958..ce8fa370 100644
--- a/tests/dom_tests/dom_utils_test.coffee
+++ b/tests/dom_tests/dom_utils_test.coffee
@@ -73,6 +73,15 @@ context "Check visibility",
"""
assert.equal null, (DomUtils.getVisibleClientRect (document.getElementById 'foo'), true)
+ should "detect font-size: 0; and display: inline; links when their children are display: inline", ->
+ # This test represents the minimal test case covering issue #1554.
+ document.getElementById("test-div").innerHTML = """
+ <a id='foo' style='display: inline; font-size: 0px;'>
+ <div style='display: inline; font-size: 16px;'>test</div>
+ </a>
+ """
+ assert.isTrue (DomUtils.getVisibleClientRect (document.getElementById 'foo'), true) != null
+
should "detect links inside opacity:0 elements as visible", ->
# XXX This is an expected failure. See issue #16.
document.getElementById("test-div").innerHTML = """
diff --git a/tests/dom_tests/phantom_runner.coffee b/tests/dom_tests/phantom_runner.coffee
index 93218724..e0382a35 100644
--- a/tests/dom_tests/phantom_runner.coffee
+++ b/tests/dom_tests/phantom_runner.coffee
@@ -37,15 +37,20 @@ page.open testfile, (status) ->
console.log 'Unable to load tests.'
phantom.exit 1
- testsFailed = page.evaluate ->
- Tests.run()
- return Tests.testsFailed
-
- if system.args[1] == '--coverage'
- data = page.evaluate -> JSON.stringify _$jscoverage
- fs.write dirname + 'dom_tests_coverage.json', data, 'w'
-
- if testsFailed > 0
- phantom.exit 1
- else
- phantom.exit 0
+ runTests = ->
+ testsFailed = page.evaluate ->
+ Tests.run()
+ return Tests.testsFailed
+
+ if system.args[1] == '--coverage'
+ data = page.evaluate -> JSON.stringify _$jscoverage
+ fs.write dirname + 'dom_tests_coverage.json', data, 'w'
+
+ if testsFailed > 0
+ phantom.exit 1
+ else
+ phantom.exit 0
+
+ # We add a short delay to allow asynchronous initialization (that is, initialization which happens on
+ # "nextTick") to complete.
+ setTimeout runTests, 10
diff --git a/tests/dom_tests/vomnibar_test.coffee b/tests/dom_tests/vomnibar_test.coffee
index 380175f3..3eda6234 100644
--- a/tests/dom_tests/vomnibar_test.coffee
+++ b/tests/dom_tests/vomnibar_test.coffee
@@ -1,4 +1,5 @@
vomnibarFrame = null
+SearchEngines.refresh ""
context "Keep selection within bounds",
diff --git a/tests/unit_tests/commands_test.coffee b/tests/unit_tests/commands_test.coffee
index daaef016..e55dc0f2 100644
--- a/tests/unit_tests/commands_test.coffee
+++ b/tests/unit_tests/commands_test.coffee
@@ -1,5 +1,6 @@
require "./test_helper.js"
extend global, require "./test_chrome_stubs.js"
+global.Settings = {postUpdateHooks: {}}
{Commands} = require "../../background_scripts/commands.js"
context "Key mappings",
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index 88df0a43..4a0cf746 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -244,7 +244,7 @@ context "suggestions",
url: "url"
title: "title <span>"
relevancyFunction: returns 1
- assert.isTrue suggestion.generateHtml().indexOf("title &lt;span&gt;") >= 0
+ assert.isTrue suggestion.generateHtml({}).indexOf("title &lt;span&gt;") >= 0
should "highlight query words", ->
suggestion = new Suggestion
@@ -254,7 +254,7 @@ context "suggestions",
title: "ninjawords"
relevancyFunction: returns 1
expected = "<span class='vomnibarMatch'>ninj</span>a<span class='vomnibarMatch'>words</span>"
- assert.isTrue suggestion.generateHtml().indexOf(expected) >= 0
+ assert.isTrue suggestion.generateHtml({}).indexOf(expected) >= 0
should "highlight query words correctly when whey they overlap", ->
suggestion = new Suggestion
@@ -264,7 +264,7 @@ context "suggestions",
title: "ninjawords"
relevancyFunction: returns 1
expected = "<span class='vomnibarMatch'>ninjaword</span>s"
- assert.isTrue suggestion.generateHtml().indexOf(expected) >= 0
+ assert.isTrue suggestion.generateHtml({}).indexOf(expected) >= 0
should "shorten urls", ->
suggestion = new Suggestion
@@ -273,7 +273,7 @@ context "suggestions",
url: "http://ninjawords.com"
title: "ninjawords"
relevancyFunction: returns 1
- assert.equal -1, suggestion.generateHtml().indexOf("http://ninjawords.com")
+ assert.equal -1, suggestion.generateHtml({}).indexOf("http://ninjawords.com")
context "RankingUtils.wordRelevancy",
should "score higher in shorter URLs", ->
diff --git a/tests/unit_tests/exclusion_test.coffee b/tests/unit_tests/exclusion_test.coffee
index b3ed7194..28c17a2f 100644
--- a/tests/unit_tests/exclusion_test.coffee
+++ b/tests/unit_tests/exclusion_test.coffee
@@ -14,9 +14,8 @@ root.Marks =
extend(global, require "../../lib/utils.js")
Utils.getCurrentVersion = -> '1.44'
-extend(global,require "../../background_scripts/sync.js")
-extend(global,require "../../background_scripts/settings.js")
-Sync.init()
+extend(global,require "../../lib/settings.js")
+Settings.init()
extend(global, require "../../background_scripts/exclusions.js")
extend(global, require "../../background_scripts/commands.js")
extend(global, require "../../background_scripts/main.js")
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index 4cd20211..ded7b5f8 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -3,15 +3,18 @@ extend global, require "./test_chrome_stubs.js"
extend(global, require "../../lib/utils.js")
Utils.getCurrentVersion = -> '1.44'
+Utils.isBackgroundPage = -> true
+Utils.isExtensionPage = -> true
global.localStorage = {}
-extend(global,require "../../background_scripts/sync.js")
-extend(global,require "../../background_scripts/settings.js")
-Sync.init()
+extend(global,require "../../lib/settings.js")
context "settings",
setup ->
stub global, 'localStorage', {}
+ Settings.cache = global.localStorage # Point the settings cache to the new localStorage object.
+ Settings.postUpdateHooks = {} # Avoid running update hooks which include calls to outside of settings.
+ Settings.init()
should "save settings in localStorage as JSONified strings", ->
Settings.set 'dummy', ""
@@ -39,24 +42,22 @@ context "settings",
should "propagate non-default value via synced storage listener", ->
Settings.set 'scrollStepSize', 20
assert.equal Settings.get('scrollStepSize'), 20
- Sync.handleStorageUpdate { scrollStepSize: { newValue: "40" } }
+ Settings.Sync.handleStorageUpdate { scrollStepSize: { newValue: "40" } }
assert.equal Settings.get('scrollStepSize'), 40
should "propagate default value via synced storage listener", ->
Settings.set 'scrollStepSize', 20
assert.equal Settings.get('scrollStepSize'), 20
- Sync.handleStorageUpdate { scrollStepSize: { newValue: "60" } }
+ Settings.Sync.handleStorageUpdate { scrollStepSize: { newValue: "60" } }
assert.isFalse Settings.has 'scrollStepSize'
should "propagate non-default values from synced storage", ->
chrome.storage.sync.set { scrollStepSize: JSON.stringify(20) }
- Sync.fetchAsync()
assert.equal Settings.get('scrollStepSize'), 20
should "propagate default values from synced storage", ->
Settings.set 'scrollStepSize', 20
chrome.storage.sync.set { scrollStepSize: JSON.stringify(60) }
- Sync.fetchAsync()
assert.isFalse Settings.has 'scrollStepSize'
should "clear a setting from synced storage", ->
@@ -66,9 +67,10 @@ context "settings",
should "trigger a postUpdateHook", ->
message = "Hello World"
- Settings.postUpdateHooks['scrollStepSize'] = (value) -> Sync.message = value
+ receivedMessage = ""
+ Settings.postUpdateHooks['scrollStepSize'] = (value) -> receivedMessage = value
chrome.storage.sync.set { scrollStepSize: JSON.stringify(message) }
- assert.equal message, Sync.message
+ assert.equal message, receivedMessage
should "sync a key which is not a known setting (without crashing)", ->
chrome.storage.sync.set { notASetting: JSON.stringify("notAUsefullValue") }
diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee
index 60f3a890..16f0e144 100644
--- a/tests/unit_tests/test_chrome_stubs.coffee
+++ b/tests/unit_tests/test_chrome_stubs.coffee
@@ -19,6 +19,9 @@ exports.chrome =
onInstalled:
addListener: ->
+ extension:
+ getURL: (path) -> path
+
tabs:
onSelectionChanged:
addListener: () -> true
diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee
index bfe066c3..f9ed3636 100644
--- a/tests/unit_tests/utils_test.coffee
+++ b/tests/unit_tests/utils_test.coffee
@@ -2,9 +2,8 @@ require "./test_helper.js"
extend global, require "./test_chrome_stubs.js"
extend(global, require "../../lib/utils.js")
Utils.getCurrentVersion = -> '1.43'
-extend(global, require "../../background_scripts/sync.js")
-extend(global, require "../../background_scripts/settings.js")
-Sync.init()
+extend(global, require "../../lib/settings.js")
+Settings.init()
context "isUrl",
should "accept valid URLs", ->