aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md50
-rw-r--r--CREDITS1
-rw-r--r--Cakefile2
-rw-r--r--README.md85
-rw-r--r--background_scripts/commands.coffee42
-rw-r--r--background_scripts/completion.coffee142
-rw-r--r--background_scripts/main.coffee55
-rw-r--r--background_scripts/settings.coffee53
-rw-r--r--background_scripts/sync.coffee102
-rw-r--r--content_scripts/file_urls.css6
-rw-r--r--content_scripts/link_hints.coffee52
-rw-r--r--content_scripts/scroller.coffee26
-rw-r--r--content_scripts/vimium.css8
-rw-r--r--content_scripts/vimium_frontend.coffee29
-rw-r--r--content_scripts/vomnibar.coffee14
-rw-r--r--lib/dom_utils.coffee20
-rw-r--r--lib/keyboard_utils.coffee3
-rw-r--r--lib/utils.coffee2
-rw-r--r--manifest.json9
-rw-r--r--pages/help_dialog.html2
-rw-r--r--pages/options.coffee12
-rw-r--r--pages/options.html24
-rw-r--r--pages/popup.html2
-rw-r--r--tests/dom_tests/dom_tests.coffee29
-rw-r--r--tests/dom_tests/dom_utils_test.coffee8
-rw-r--r--tests/unit_tests/completion_test.coffee78
-rw-r--r--tests/unit_tests/settings_test.coffee57
-rw-r--r--tests/unit_tests/test_chrome_stubs.coffee61
-rw-r--r--tests/unit_tests/utils_test.coffee4
29 files changed, 802 insertions, 176 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 57133d8f..03ac26e9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,10 +1,11 @@
# Contributing to Vimium
You'd like to fix a bug or implement a feature? Great! Check out the bugs on our issues tracker, or implement
-one of the suggestions there that have been tagged 'todo'. If you have a suggestion of your own, start a
-discussion on the issues tracker or on the [mailing list](http://groups.google.com/group/vimium-dev?hl=en). If
-it mirrors a similar feature in another browser or in Vim itself, let us know! Once you've picked something to
-work on, add a comment to the respective issue so others don't duplicate your effort.
+one of the suggestions there that have been tagged "help wanted". If you have a suggestion of your own, start
+a discussion on the issues tracker or on the
+[mailing list](http://groups.google.com/group/vimium-dev?hl=en). If it mirrors a similar feature in another
+browser or in Vim itself, let us know. Once you've picked something to work on, add a comment to the
+respective issue so others don't duplicate your effort.
## Reporting Issues
@@ -25,7 +26,12 @@ install Vimium from source:
1. Click on "Load Unpacked Extension..."
1. Select the Vimium directory.
-## Tests
+## Development tips
+
+ 1. Run `cake autobuild` to watch for changes to coffee files, and have the .js files automatically
+ regenerated
+
+## Running the tests
Our tests use [shoulda.js](https://github.com/philc/shoulda.js) and [PhantomJS](http://phantomjs.org/). To run the tests:
@@ -37,8 +43,8 @@ Our tests use [shoulda.js](https://github.com/philc/shoulda.js) and [PhantomJS](
## Code Coverage
-Bugs and features are not the only way to contribute -- more tests are always welcome. You can find out which
-portions of code need them by looking at our coverage reports. To generate these reports:
+You can find out which portions of code need them by looking at our coverage reports. To generate these
+reports:
1. Download [JSCoverage](http://siliconforks.com/jscoverage/download.html) or `brew install jscoverage`
1. `npm install temp`
@@ -46,11 +52,6 @@ portions of code need them by looking at our coverage reports. To generate these
then be viewed using [jscoverage-report](https://github.com/int3/jscoverage-report). See
jscoverage-report's [README](https://github.com/int3/jscoverage-report#jscoverage-report) for more details.
-## Pull Requests
-
-When you're done with your changes, send us a pull request on Github. Feel free to include a change to the
-CREDITS file with your patch.
-
## Coding Style
* We follow the recommendations from
@@ -58,3 +59,28 @@ CREDITS file with your patch.
* We follow two major differences from this style guide:
* Wrap lines at 110 characters instead of 80.
* Use double-quoted strings by default.
+
+## Pull Requests
+
+When you're done with your changes, send us a pull request on Github. Feel free to include a change to the
+CREDITS file with your patch.
+
+## How to release Vimium to the Chrome Store
+
+This process is currently only done by Phil or Ilya.
+
+1. Increment the version number in manifest.json
+2. Update the Changelog in README.md
+
+ You can see a summary of commits since the last version: `git log --oneline v1.45..`
+
+3. Push your commits
+4. Create a git tag for this newly released version
+
+ git tag -a v1.45 -m "v1.45 release"
+
+5. Run `cake package`
+6. Take the distributable found in `dist` and upload it
+ [here](https://chrome.google.com/webstore/developer/dashboard)
+7. Update the description in the Chrome store to include the latest version's release notes
+8. Celebrate
diff --git a/CREDITS b/CREDITS
index 605860fe..d8aee04f 100644
--- a/CREDITS
+++ b/CREDITS
@@ -28,6 +28,7 @@ Contributors:
markstos
Matthew Cline <matt@nightrealms.com>
Matt Garriott (github: mgarriott)
+ Michael Hauser-Raspe (github: mijoharas)
Murph (github: pandeiro)
Niklas Baumstark <niklas.baumstark@gmail.com> (github: niklasb)
rodimius
diff --git a/Cakefile b/Cakefile
index 85e9ff24..0fa75e24 100644
--- a/Cakefile
+++ b/Cakefile
@@ -2,7 +2,6 @@ util = require "util"
fs = require "fs"
path = require "path"
child_process = require "child_process"
-{Utils} = require './lib/utils'
spawn = (procName, optArray, silent=false) ->
if process.platform is "win32"
@@ -118,6 +117,7 @@ task "test", "run all tests", (options) ->
process.exit 0
task "coverage", "generate coverage report", ->
+ {Utils} = require './lib/utils'
temp = require 'temp'
tmpDir = temp.mkdirSync null
jscoverage = spawn "jscoverage", [".", tmpDir].concat optArrayFromDict
diff --git a/README.md b/README.md
index c79a0045..fc38c2b8 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Keyboard Bindings
-----------------
Modifier keys are specified as `<c-x>`, `<m-x>`, and `<a-x>` for ctrl+x, meta+x, and alt+x
-respectively. See the next section for instructions on modifying these bindings.
+respectively. See the next section for instructions on customizing these bindings.
Navigating the current page:
@@ -74,6 +74,7 @@ Manipulating tabs:
x close current tab
X restore closed tab (i.e. unwind the 'x' command)
T search through your open tabs
+ <a-p> pin/unpin current tab
Additional advanced browsing commands:
@@ -89,6 +90,8 @@ Additional advanced browsing commands:
Vimium supports command repetition so, for example, hitting '5t' will open 5 tabs in rapid succession. `<ESC>` (or
`<c-[>`) will clear any partial commands in the queue and will also exit insert and find modes.
+There are some advanced commands which aren't documented here. Refer to the help dialog (type `?`) for a full
+list.
Custom Key Mappings
-------------------
@@ -125,13 +128,31 @@ The following special keys are available for mapping:
Shifts are automatically detected so, for example, `<c-&>` corresponds to ctrl+shift+7 on an English keyboard.
+More documentation
+------------------
+Many of the more advanced or involved features are documented on [Vimium's github wiki](https://github.com/philc/vimium/wiki).
+
Contributing
------------
Please see [CONTRIBUTING.md](https://github.com/philc/vimium/blob/master/CONTRIBUTING.md) for details.
Release Notes
-------------
-1.43 (05/18/2013)
+1.45 (2014-07-20)
+
+- Vimium's settings are now synced across computers.
+- New commands: "open link in new tab and focus", "move tab left", "move tab right", "pin/unpin tab".
+- Vomnibar can now use [search engine shortcuts](https://github.com/philc/vimium/wiki/Search-Engines), similar to Chrome's Omnibar.
+- Due to significant ranking improvements, Vomnibar's search results are now even more helpful.
+- When reopening a closed tab, its history is now preserved.
+- Bugfixes.
+
+1.44 (2013-11-06)
+
+- Add support for recent versions of Chromium.
+- Bugfixes.
+
+1.43 (2013-05-18)
- Relevancy improvements to the Vomnibar's domain & history search.
- Added gU, which goes to the root of the current URL.
@@ -143,25 +164,25 @@ Release Notes
- Update our Chrome APIs so Vimium works on Chrome 28+.
- Bugfixes.
-1.42 (11/03/2012)
+1.42 (2012-11-03)
- Bugfixes.
-1.41 (10/27/2012)
+1.41 (2012-10-27)
- Bugfixes.
-1.40 (10/27/2012)
+1.40 (2012-10-27)
- Bugfixes.
- Added options for search engines and regex find.
- Pressing unmapped keys in hints mode now deactivates the mode.
-1.39 (09/09/2012)
+1.39 (2012-09-09)
- Bugfixes.
-1.38 (09/08/2012)
+1.38 (2012-09-08)
- `O` now opens Vomnibar results in a new tab. `B` does the same for bookmarks only.
- Add a browser icon to quickly add sites to Vimium's exclude list.
@@ -169,33 +190,33 @@ Release Notes
- `gi` now launches a new mode that allows the user to tab through the input elements on the page.
- Bugfixes.
-1.37 (07/07/2012)
+1.37 (2012-07-07)
- Select the first result by default in Vomnibar tab and bookmark modes.
-1.36 (07/07/2012)
+1.36 (2012-07-07)
- 'b' brings up a bookmark-only Vomnibar.
- Better support for some bookmarklets.
-1.35 (07/05/2012)
+1.35 (2012-07-05)
- Bugfixes.
-1.34 (07/03/2012)
+1.34 (2012-07-03)
- A bugfix for bookmarklets in Vomnibar.
-1.33 (07/02/2012)
+1.33 (2012-07-02)
- A Vomnibar, which allows you to open sites from history, bookmarks, and tabs using Vimium's UI. Type "o" to try it.
-1.32 (03/05/2012)
+1.32 (2012-03-05)
- More tweaks to the next / previous link-detection algorithm.
- Minor bug fixes.
-1.31 (02/28/2012)
+1.31 (2012-02-28)
- Improve style of link hints, and use fewer characters for hints.
- Add an option to hide the heads up display (HUD). Notably, the HUD obscures Facebook Chat's textbox.
@@ -205,7 +226,7 @@ Release Notes
- A new find mode which optionally supports case sensitivity and regular expressions.
- Bug fixes.
-1.30 (12/04/2011)
+1.30 (2011-12-04)
- Support for image maps in link hints.
- Counts now work with forward & backward navigation.
@@ -213,29 +234,29 @@ Release Notes
- An alternate link hints mode: type the title of a link to select it. You can enable it in Vimium's Advanced Preferences.
- Bug fixes.
-1.29 (07/30/2011)
+1.29 (2012-07-30)
- `yf` to copy a link hint url to the clipboard.
- Scatter link hints to prevent clustering on dense sites.
- Don't show insert mode notification unless you specifically hit `i`.
- Remove zooming functionality now that Chrome does it all natively.
-1.28 (06/29/2011)
+1.28 (2011-06-29)
- Support for opening bookmarks (`b` and `B`).
- Support for contenteditable text boxes.
- Speed improvements and bugfixes.
-1.27 (03/24/2011)
+1.27 (2011-03-24)
- Improvements and bugfixes.
-1.26 (02/17/2011)
+1.26 (2011-02-17)
- `<c-d>`, `<c-f>` and related are no longer bound by default. You can rebind them on the options page.
- Faster link hinting.
-1.22, 1.23, 1.24, 1.25 (02/10/2011)
+1.22, 1.23, 1.24, 1.25 (2011-02-10)
- Some sites are now excluded by default.
- View source (`gs`) now opens in a new tab.
@@ -245,11 +266,11 @@ Release Notes
- Improvements to link hinting.
- Bugfixes.
-1.21 (10/24/2010)
+1.21 (2010-10-24)
- Critical bugfix for an excluded URLs regression due to frame support.
-1.20 (10/24/2010)
+1.20 (2010-10-24)
- In link hints mode, holding down the shift key will now toggle between opening in the current tab and
opening in a new tab.
@@ -260,12 +281,12 @@ Release Notes
- More robust support for non-US keyboard layouts.
- Numerous bug fixes.
-1.19 (06/29/2010)
+1.19 (2010-06-29)
- A critical bug fix for development channel Chromium.
- Vimium icons for the Chrome extensions panel and other places.
-1.18 (06/22/2010)
+1.18 (2010-06-22)
- Vimium now runs on pages with file:/// and ftp:///
- The Options page is now linked from the Help dialog.
@@ -276,7 +297,7 @@ Release Notes
does not support command repetition.
- Bug fixes and optimizations.
-1.17 (04/18/2010)
+1.17 (2010-04-18)
- 'u' now restores tabs that were closed by the mouse or with native shortcuts. Tabs are also restored in
their prior position.
@@ -284,24 +305,24 @@ does not support command repetition.
- Link hints are now faster and more reliable.
- Bug fixes.
-1.16 (03/09/2010)
+1.16 (2010-03-09)
- Add support for configurable key mappings under Advanced Options.
- A help dialog which shows all currently bound keyboard shortcuts. Type "?" to see it.
- Bug fixes related to key stroke handling.
-1.15 (01/31/2010)
+1.15 (2010-01-31)
- Make the CSS used by the link hints configurable. It's under Advanced Options.
- Add a notification linking to the changelog when Vimium is updated in the background.
- Link-hinting performance improvements and bugfixes.
- Ctrl+D and Ctrl+U now scroll by 1/2 page instead of a fixed amount, to mirror Vim's behavior.
-1.14 (01/21/2010)
+1.14 (2010-01-21)
- Fixed a bug introduced in 1.13 that prevented excluded URLs from being saved.
-1.13 (01/21/2010)
+1.13 (2010-01-21)
- `<c-f>` and `<c-b>` are now mapped to scroll a full page up or down respectively.
- Bugfixes related to entering insert mode when the page first loads, and when focusing Flash embeds.
@@ -311,7 +332,7 @@ does not support command repetition.
- `<c-e>` and `<c-y>` are now mapped to scroll down and up respectively.
- The characters used for link hints are now configurable under Advanced Options.
-1.11, 1.12 (01/08/2010)
+1.11, 1.12 (2010-01-08)
- Commands 'gt' & 'gT' to move to the next & previous tab.
- Command 'yy' to yank (copy) the current tab's url to the clipboard.
@@ -319,7 +340,7 @@ does not support command repetition.
- Fix for Shift+F link hints.
- ESC now clears the keyQueue. So, for example, hitting 'g', 'ESC', 'g' will no longer scroll the page.
-1.1 (01/03/2010)
+1.1 (2010-01-03)
- A nicer looking settings page.
- An exclusion list that allows you to define URL patterns for which Vimium will be disabled (e.g.
@@ -330,4 +351,4 @@ does not support command repetition.
License
-------
-Copyright (c) 2010 Phil Crosby, Ilya Sukhar. See MIT-LICENSE.txt for details.
+Copyright (c) Phil Crosby, Ilya Sukhar. See MIT-LICENSE.txt for details.
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index ce159c71..35c94bb9 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -86,21 +86,21 @@ 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", "Vomnibar.activate",
+ "Vomnibar.activateInNewTab", "Vomnibar.activateTabSelection", "Vomnibar.activateBookmarks",
+ "Vomnibar.activateBookmarksInNewTab", "goPrevious", "goNext", "nextFrame", "Marks.activateCreateMode",
+ "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", "moveTabLeft", "moveTabRight"]
misc:
["showHelp"]
@@ -111,7 +111,7 @@ Commands =
"scrollToLeft", "scrollToRight", "moveTabToNewWindow",
"goUp", "goToRoot", "focusInput", "LinkHints.activateModeWithQueue",
"LinkHints.activateModeToOpenIncognito", "goNext", "goPrevious", "Marks.activateCreateMode",
- "Marks.activateGotoMode"]
+ "Marks.activateGotoMode", "moveTabLeft", "moveTabRight"]
defaultKeyMappings =
"?": "showHelp"
@@ -161,6 +161,8 @@ defaultKeyMappings =
"J": "previousTab"
"gt": "nextTab"
"gT": "previousTab"
+ "<<": "moveTabLeft"
+ ">>": "moveTabRight"
"g0": "firstTab"
"g$": "lastTab"
@@ -170,6 +172,8 @@ defaultKeyMappings =
"x": "removeTab"
"X": "restoreTab"
+ "<a-p>": "togglePinTab"
+
"o": "Vomnibar.activate"
"O": "Vomnibar.activateInNewTab"
@@ -212,11 +216,13 @@ commandDescriptions =
enterInsertMode: ["Enter insert mode"]
- 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"]
+ "LinkHints.activateModeToOpenInNewTab": ["Open a link in a new tab"]
+ "LinkHints.activateModeToOpenInNewForegroundTab": ["Open a link in a new tab & switch to it"]
+ "LinkHints.activateModeWithQueue": ["Open multiple links in a new tab"]
"LinkHints.activateModeToOpenIncognito": ["Open a link in incognito window"]
@@ -245,6 +251,10 @@ commandDescriptions =
removeTab: ["Close current tab", { background: true, noRepeat: true }]
restoreTab: ["Restore closed tab", { background: true }]
moveTabToNewWindow: ["Move tab to new window", { background: true }]
+ togglePinTab: ["Pin/unpin current tab", { background: 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"]
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee
index fd41cdc8..b0ab4b88 100644
--- a/background_scripts/completion.coffee
+++ b/background_scripts/completion.coffee
@@ -35,12 +35,12 @@ class Suggestion
<span class="vimiumReset vomnibarTitle">#{@highlightTerms(Utils.escapeHtml(@title))}</span>
</div>
<div class="vimiumReset vomnibarBottomHalf">
- <span class="vimiumReset vomnibarUrl">#{@shortenUrl(@highlightTerms(@url))}</span>
+ <span class="vimiumReset vomnibarUrl">#{@shortenUrl(@highlightTerms(Utils.escapeHtml(@url)))}</span>
#{relevancyHtml}
</div>
"""
- 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] == "/"
@@ -71,7 +71,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
@@ -101,6 +102,7 @@ class Suggestion
class BookmarkCompleter
+ folderSeparator: "/"
currentSearch: null
# These bookmarks are loaded asynchronously when refresh() is called.
bookmarks: null
@@ -112,14 +114,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)
@@ -130,16 +137,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)
@@ -247,6 +267,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
@@ -295,24 +336,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.
@@ -325,6 +421,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)
@@ -435,6 +534,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/main.coffee b/background_scripts/main.coffee
index f564f477..3b7670d4 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])
@@ -217,6 +219,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
@@ -238,29 +246,39 @@ BackgroundCommands =
chrome.tabs.getSelected(null, (tab) ->
chrome.tabs.remove(tab.id))
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.sessionRestore
+ chrome.sessionRestore.getRecentlyClosed((closed) ->
+ chrome.sessionRestore.restore(closed[0]))
+ 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
@@ -596,3 +614,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..175f3262 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,18 +8,54 @@ 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
+
+ # 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
linkHintCharacters: "sadfjklewcmpgh"
@@ -49,7 +85,7 @@ root.Settings = Settings =
"""
http*://mail.google.com/*
"""
- # NOTE : If a page contains both a single angle-bracket link and a double angle-bracket link, then in
+ # 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 +96,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
diff --git a/content_scripts/file_urls.css b/content_scripts/file_urls.css
new file mode 100644
index 00000000..fd63c224
--- /dev/null
+++ b/content_scripts/file_urls.css
@@ -0,0 +1,6 @@
+/* Chrome file:// URLs set draggable=true for links to files (CSS selector .icon.file). This automatically
+ * sets -webkit-user-select: none, which disables selecting the file names and so prevents Vimium's search
+ * from working as expected. Here, we reset the value back to default. */
+.icon.file {
+ -webkit-user-select: auto !important;
+}
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index f2479d0f..24314b26 100644
--- a/content_scripts/link_hints.coffee
+++ b/content_scripts/link_hints.coffee
@@ -9,7 +9,8 @@
# typing the text of the link itself.
#
OPEN_IN_CURRENT_TAB = {}
-OPEN_IN_NEW_TAB = {}
+OPEN_IN_NEW_BG_TAB = {}
+OPEN_IN_NEW_FG_TAB = {}
OPEN_WITH_QUEUE = {}
COPY_LINK_URL = {}
OPEN_INCOGNITO = {}
@@ -24,7 +25,8 @@ LinkHints =
delayMode: false
# Handle the link hinting marker generation and matching. Must be initialized after settings have been
# loaded, so that we can retrieve the option setting.
- markerMatcher: undefined
+ getMarkerMatcher: ->
+ if settings.get("filterLinkHints") then filterHints else alphabetHints
# lock to ensure only one instance runs at a time
isActive: false
@@ -32,7 +34,6 @@ LinkHints =
# To be called after linkHints has been generated from linkHintsBase.
#
init: ->
- @markerMatcher = if settings.get("filterLinkHints") then filterHints else alphabetHints
#
# Generate an XPath describing what a clickable element is.
@@ -46,7 +47,8 @@ LinkHints =
"@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"])
# We need this as a top-level function because our command system doesn't yet support arguments.
- activateModeToOpenInNewTab: -> @activateMode(OPEN_IN_NEW_TAB)
+ activateModeToOpenInNewTab: -> @activateMode(OPEN_IN_NEW_BG_TAB)
+ activateModeToOpenInNewForegroundTab: -> @activateMode(OPEN_IN_NEW_FG_TAB)
activateModeToCopyLinkUrl: -> @activateMode(COPY_LINK_URL)
activateModeWithQueue: -> @activateMode(OPEN_WITH_QUEUE)
activateModeToOpenIncognito: -> @activateMode(OPEN_INCOGNITO)
@@ -60,7 +62,8 @@ LinkHints =
@isActive = true
@setOpenLinkMode(mode)
- hintMarkers = @markerMatcher.fillInMarkers(@createMarkerFor(el) for el in @getVisibleClickableElements())
+ hintMarkers = (@createMarkerFor(el) for el in @getVisibleClickableElements())
+ @getMarkerMatcher().fillInMarkers(hintMarkers)
# Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
# because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat
@@ -77,15 +80,18 @@ LinkHints =
})
setOpenLinkMode: (@mode) ->
- if @mode is OPEN_IN_NEW_TAB or @mode is OPEN_WITH_QUEUE
- if @mode is OPEN_IN_NEW_TAB
+ if @mode is OPEN_IN_NEW_BG_TAB or @mode is OPEN_IN_NEW_FG_TAB or @mode is OPEN_WITH_QUEUE
+ if @mode is OPEN_IN_NEW_BG_TAB
HUD.show("Open link in new tab")
+ else if @mode is OPEN_IN_NEW_FG_TAB
+ HUD.show("Open link in new tab and switch to it")
else
HUD.show("Open multiple links in a new tab")
@linkActivator = (link) ->
# When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on
# windows) to open it in a new tab if necessary.
DomUtils.simulateClick(link, {
+ shiftKey: @mode is OPEN_IN_NEW_FG_TAB,
metaKey: KeyboardUtils.platform == "Mac",
ctrlKey: KeyboardUtils.platform != "Mac" })
else if @mode is COPY_LINK_URL
@@ -101,9 +107,7 @@ LinkHints =
url: link.href)
else # OPEN_IN_CURRENT_TAB
HUD.show("Open link in current tab")
- # When we're opening the link in the current tab, don't navigate to the selected link immediately
- # we want to give the user some time to notice which link has received focus.
- @linkActivator = (link) -> setTimeout(DomUtils.simulateClick.bind(DomUtils, link), 400)
+ @linkActivator = (link) -> DomUtils.simulateClick.bind(DomUtils, link)()
#
# Creates a link marker for the given link.
@@ -161,29 +165,29 @@ LinkHints =
visibleElements
#
- # Handles shift and esc keys. The other keys are passed to markerMatcher.matchHintsByKey.
+ # Handles shift and esc keys. The other keys are passed to getMarkerMatcher().matchHintsByKey.
#
onKeyDownInMode: (hintMarkers, event) ->
return if @delayMode
- if (event.keyCode == keyCodes.shiftKey && @mode != COPY_LINK_URL)
+ if ((event.keyCode == keyCodes.shiftKey or event.keyCode == keyCodes.ctrlKey) and
+ (@mode == OPEN_IN_CURRENT_TAB or
+ @mode == OPEN_IN_NEW_BG_TAB or
+ @mode == OPEN_IN_NEW_FG_TAB))
# Toggle whether to open link in a new or current tab.
prev_mode = @mode
- @setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_TAB else OPEN_IN_CURRENT_TAB)
+ if event.keyCode == keyCodes.shiftKey
+ @setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_CURRENT_TAB)
- handlerStack.push({
- keyup: (event) =>
- return if (event.keyCode != keyCodes.shiftKey)
- @setOpenLinkMode(prev_mode) if @isActive
- @remove()
- })
+ else # event.keyCode == keyCodes.ctrlKey
+ @setOpenLinkMode(if @mode is OPEN_IN_NEW_FG_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_NEW_FG_TAB)
# TODO(philc): Ignore keys that have modifiers.
if (KeyboardUtils.isEscape(event))
@deactivateMode()
- else if (event.keyCode != keyCodes.shiftKey)
- keyResult = @markerMatcher.matchHintsByKey(hintMarkers, event)
+ else
+ keyResult = @getMarkerMatcher().matchHintsByKey(hintMarkers, event)
linksMatched = keyResult.linksMatched
delay = keyResult.delay ? 0
if (linksMatched.length == 0)
@@ -194,7 +198,7 @@ LinkHints =
for marker in hintMarkers
@hideMarker(marker)
for matched in linksMatched
- @showMarker(matched, @markerMatcher.hintKeystrokeQueue.length)
+ @showMarker(matched, @getMarkerMatcher().hintKeystrokeQueue.length)
false # We've handled this key, so prevent propagation.
#
@@ -239,8 +243,8 @@ LinkHints =
#
deactivateMode: (delay, callback) ->
deactivate = =>
- if (LinkHints.markerMatcher.deactivate)
- LinkHints.markerMatcher.deactivate()
+ if (LinkHints.getMarkerMatcher().deactivate)
+ LinkHints.getMarkerMatcher().deactivate()
if (LinkHints.hintMarkerContainingDiv)
DomUtils.removeElement LinkHints.hintMarkerContainingDiv
LinkHints.hintMarkerContainingDiv = null
diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee
index 08ab14a1..f3c632b3 100644
--- a/content_scripts/scroller.coffee
+++ b/content_scripts/scroller.coffee
@@ -39,17 +39,17 @@ ensureScrollChange = (direction, changeFn) ->
loop
oldScrollValue = element[axisName]
changeFn(element, axisName)
+ break unless (element[axisName] == oldScrollValue && element != document.body)
lastElement = element
# we may have an orphaned element. if so, just scroll the body element.
element = element.parentElement || document.body
- break unless (lastElement[axisName] == oldScrollValue && lastElement != document.body)
# if the activated element has been scrolled completely offscreen, subsequent changes in its scroll
# position will not provide any more visual feedback to the user. therefore we deactivate it so that
# subsequent scrolls only move the parent element.
rect = activatedElement.getBoundingClientRect()
if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth)
- activatedElement = lastElement
+ activatedElement = element
# scroll the active element in :direction by :amount * :factor.
# :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions.
@@ -65,12 +65,13 @@ root.scrollBy = (direction, amount, factor = 1) ->
if (!activatedElement || !isRendered(activatedElement))
activatedElement = document.body
- amount = getDimension activatedElement, direction, amount if Utils.isString amount
-
- amount *= factor
-
- if (amount != 0)
- ensureScrollChange direction, (element, axisName) -> element[axisName] += amount
+ ensureScrollChange direction, (element, axisName) ->
+ if Utils.isString amount
+ elementAmount = getDimension element, direction, amount
+ else
+ elementAmount = amount
+ elementAmount *= factor
+ element[axisName] += elementAmount
root.scrollTo = (direction, pos) ->
return unless document.body
@@ -78,9 +79,12 @@ root.scrollTo = (direction, pos) ->
if (!activatedElement || !isRendered(activatedElement))
activatedElement = document.body
- pos = getDimension activatedElement, direction, pos if Utils.isString pos
-
- ensureScrollChange direction, (element, axisName) -> element[axisName] = pos
+ ensureScrollChange direction, (element, axisName) ->
+ if Utils.isString pos
+ elementPos = getDimension element, direction, pos
+ else
+ elementPos = pos
+ element[axisName] = elementPos
# TODO refactor and put this together with the code in getVisibleClientRect
isRendered = (element) ->
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index ccbcb339..a63fc3a5 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -175,14 +175,14 @@ div#vimiumHelpDialog div.advanced { display: none; }
div#vimiumHelpDialog div.advanced td:nth-of-type(3) { color: #555; }
div#vimiumHelpDialog a.closeButton {
position:absolute;
- right:10px;
+ right:7px;
top:5px;
font-family:"courier new";
font-weight:bold;
color:#555;
text-decoration:none;
padding-left:10px;
- font-size:16px;
+ font-size:20px;
}
div#vimiumHelpDialog a {
text-decoration: underline;
@@ -296,6 +296,8 @@ body.vimiumFindMode ::selection {
}
#vomnibar input {
+ color: #000;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 20px;
height: 34px;
margin-bottom: 0;
@@ -332,6 +334,8 @@ body.vimiumFindMode ::selection {
font-size: 16px;
color: black;
position: relative;
+ display: list-item;
+ margin: auto;
}
#vomnibar li:last-of-type {
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index a2139df6..896253a6 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -8,7 +8,7 @@ window.handlerStack = new HandlerStack
insertModeLock = null
findMode = false
-findModeQuery = { rawQuery: "" }
+findModeQuery = { rawQuery: "", matchCount: 0 }
findModeQueryHasResults = false
findModeAnchorNode = null
isShowingHelpDialog = false
@@ -554,6 +554,18 @@ updateFindModeQuery = ->
text = document.body.innerText
findModeQuery.regexMatches = text.match(pattern)
findModeQuery.activeRegexIndex = 0
+ findModeQuery.matchCount = findModeQuery.regexMatches?.length
+ # if we are doing a basic plain string match, we still want to grep for matches of the string, so we can
+ # show a the number of results. We can grep on document.body.innerText, as it should be indistinguishable
+ # from the internal representation used by window.find.
+ else
+ # escape all special characters, so RegExp just parses the string 'as is'.
+ # Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
+ escapeRegExp = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
+ parsedNonRegexQuery = findModeQuery.parsedQuery.replace(escapeRegExp, (char) -> "\\" + char)
+ pattern = new RegExp(parsedNonRegexQuery, "g" + (if findModeQuery.ignoreCase then "i" else ""))
+ text = document.body.innerText
+ findModeQuery.matchCount = text.match(pattern)?.length
handleKeyCharForFindMode = (keyChar) ->
findModeQuery.rawQuery += keyChar
@@ -799,7 +811,7 @@ findAndFollowRel = (value) ->
for tag in relTags
elements = document.getElementsByTagName(tag)
for element in elements
- if (element.hasAttribute("rel") && element.rel == value)
+ if (element.hasAttribute("rel") && element.rel.toLowerCase() == value)
followLink(element)
return true
@@ -815,7 +827,7 @@ window.goNext = ->
showFindModeHUDForQuery = ->
if (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0)
- HUD.show("/" + findModeQuery.rawQuery)
+ HUD.show("/" + findModeQuery.rawQuery + " (" + findModeQuery.matchCount + " Matches)")
else
HUD.show("/" + findModeQuery.rawQuery + " (No Matches)")
@@ -839,7 +851,7 @@ window.showHelpDialog = (html, fid) ->
container.innerHTML = html
container.getElementsByClassName("closeButton")[0].addEventListener("click", hideHelpDialog, false)
-
+
VimiumHelpDialog =
# This setting is pulled out of local storage. It's false by default.
getShowAdvancedCommands: -> settings.get("helpDialog_showAdvancedCommands")
@@ -869,8 +881,9 @@ window.showHelpDialog = (html, fid) ->
VimiumHelpDialog.init()
- container.getElementsByClassName("optionsPage")[0].addEventListener("click",
- -> chrome.runtime.sendMessage({ handler: "openOptionsPageInNewTab" })
+ container.getElementsByClassName("optionsPage")[0].addEventListener("click", (clickEvent) ->
+ clickEvent.preventDefault()
+ chrome.runtime.sendMessage({handler: "openOptionsPageInNewTab"})
false)
@@ -908,7 +921,7 @@ HUD =
show: (text) ->
return unless HUD.enabled()
clearTimeout(HUD._showForDurationTimerId)
- HUD.displayElement().innerHTML = text
+ HUD.displayElement().innerText = text
clearInterval(HUD._tweenId)
HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150)
HUD.displayElement().style.display = ""
@@ -917,7 +930,7 @@ HUD =
HUD.upgradeNotificationElement().innerHTML = "Vimium has been updated to
<a class='vimiumReset'
href='https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb'>
- #{version}</a>.<a class='vimiumReset close-button' href='#'>x</a>"
+ #{version}</a>.<a class='vimiumReset close-button' href='#'>&times;</a>"
links = HUD.upgradeNotificationElement().getElementsByTagName("a")
links[0].addEventListener("click", HUD.onUpdateLinkClicked, false)
links[1].addEventListener "click", (event) ->
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index 6782ab28..6997d387 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -68,8 +68,18 @@ class VomnibarUI
@update(true)
updateSelection: ->
+ # We have taken the option to add some global state here (previousCompletionType) to tell if a search
+ # item has just appeared or disappeared, if that happens we either set the initialSelectionValue to 0 or 1
+ # I feel that this approach is cleaner than bubbling the state up from the suggestion level
+ # so we just inspect it afterwards
+ if @completions[0]
+ if @previousCompletionType != "search" && @completions[0].type == "search"
+ @selection = 0
+ else if @previousCompletionType == "search" && @completions[0].type != "search"
+ @selection = -1
for i in [0...@completionList.children.length]
@completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")
+ @previousCompletionType = @completions[0].type if @completions[0]
#
# Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress.
@@ -192,8 +202,8 @@ class BackgroundCompleter
filter: (query, callback) ->
id = Utils.createUniqueId()
- @filterPort.onMessage.addListener (msg) ->
- return if (msg.id != id)
+ @filterPort.onMessage.addListener (msg) =>
+ @filterPort.onMessage.removeListener(arguments.callee)
# The result objects coming from the background page will be of the form:
# { html: "", type: "", url: "" }
# type will be one of [tab, bookmark, history, domain].
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 38b23202..dcdd5518 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -46,11 +46,21 @@ DomUtils =
#
getVisibleClientRect: (element) ->
# Note: this call will be expensive if we modify the DOM in between calls.
- clientRects = element.getClientRects()
+ clientRects = ({
+ top: clientRect.top, right: clientRect.right, bottom: clientRect.bottom, left: clientRect.left,
+ width: clientRect.width, height: clientRect.height
+ } for clientRect in element.getClientRects())
for clientRect in clientRects
- if (clientRect.top < -2 || clientRect.top >= window.innerHeight - 4 ||
- clientRect.left < -2 || clientRect.left >= window.innerWidth - 4)
+ if (clientRect.top < 0)
+ clientRect.height += clientRect.top
+ clientRect.top = 0
+
+ if (clientRect.left < 0)
+ clientRect.width += clientRect.left
+ clientRect.left = 0
+
+ if (clientRect.top >= window.innerHeight - 4 || clientRect.left >= window.innerWidth - 4)
continue
if (clientRect.width < 3 || clientRect.height < 3)
@@ -99,8 +109,8 @@ DomUtils =
eventSequence = ["mouseover", "mousedown", "mouseup", "click"]
for event in eventSequence
mouseEvent = document.createEvent("MouseEvents")
- mouseEvent.initMouseEvent(event, true, true, window, 1, 0, 0, 0, 0, modifiers.ctrlKey, false, false,
- modifiers.metaKey, 0, null)
+ mouseEvent.initMouseEvent(event, true, true, window, 1, 0, 0, 0, 0, modifiers.ctrlKey, modifiers.altKey,
+ modifiers.shiftKey, modifiers.metaKey, 0, null)
# Debugging note: Firefox will not execute the element's default action if we dispatch this click event,
# but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately
element.dispatchEvent(mouseEvent)
diff --git a/lib/keyboard_utils.coffee b/lib/keyboard_utils.coffee
index df5bbbad..d2a843f9 100644
--- a/lib/keyboard_utils.coffee
+++ b/lib/keyboard_utils.coffee
@@ -1,6 +1,7 @@
KeyboardUtils =
keyCodes:
- { ESC: 27, backspace: 8, deleteKey: 46, enter: 13, space: 32, shiftKey: 16, f1: 112, f12: 123, tab: 9 }
+ { ESC: 27, backspace: 8, deleteKey: 46, enter: 13, space: 32, shiftKey: 16, ctrlKey: 17, f1: 112,
+ f12: 123, tab: 9 }
keyNames:
{ 37: "left", 38: "up", 39: "right", 40: "down" }
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 8d588a43..a93831d7 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -26,7 +26,7 @@ Utils =
-> id += 1
hasChromePrefix: (url) ->
- chromePrefixes = [ 'about', 'view-source', "chrome-extension" ]
+ chromePrefixes = [ "about", "view-source", "chrome-extension", "data" ]
for prefix in chromePrefixes
return true if url.startsWith prefix
false
diff --git a/manifest.json b/manifest.json
index 96fa9ba7..b134ceaa 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Vimium",
- "version": "1.43",
+ "version": "1.45",
"description": "The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.",
"icons": { "16": "icons/icon16.png",
"48": "icons/icon48.png",
@@ -11,6 +11,7 @@
"lib/utils.js",
"background_scripts/commands.js",
"lib/clipboard.js",
+ "background_scripts/sync.js",
"background_scripts/settings.js",
"background_scripts/completion.js",
"background_scripts/marks.js",
@@ -23,6 +24,7 @@
"bookmarks",
"history",
"clipboardRead",
+ "storage",
"<all_urls>"
],
"content_scripts": [
@@ -42,6 +44,11 @@
"css": ["content_scripts/vimium.css"],
"run_at": "document_start",
"all_frames": true
+ },
+ {
+ "matches": ["file://*"],
+ "css": ["content_scripts/file_urls.css"],
+ "run_at": "document_start"
}
],
"browser_action": {
diff --git a/pages/help_dialog.html b/pages/help_dialog.html
index 2a69ea03..0884f2cd 100644
--- a/pages/help_dialog.html
+++ b/pages/help_dialog.html
@@ -7,7 +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 closeButton" href="#">x</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">
<table class="vimiumReset">
diff --git a/pages/options.coffee b/pages/options.coffee
index 117ce4a6..d73d8f15 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -4,15 +4,9 @@ bgSettings = chrome.extension.getBackgroundPage().Settings
editableFields = [ "scrollStepSize", "excludedUrls", "linkHintCharacters", "linkHintNumbers",
"userDefinedLinkHintCss", "keyMappings", "filterLinkHints", "previousPatterns",
- "nextPatterns", "hideHud", "regexFindMode", "searchUrl"]
+ "nextPatterns", "hideHud", "regexFindMode", "searchUrl", "searchEngines"]
-canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"]
-
-postSaveHooks = keyMappings: (value) ->
- commands = chrome.extension.getBackgroundPage().Commands
- commands.clearKeyMappingsAndSetDefaults()
- commands.parseCustomKeyMappings value
- chrome.extension.getBackgroundPage().refreshCompletionKeysAfterMappingSave()
+canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss", "searchEngines"]
document.addEventListener "DOMContentLoaded", ->
populateOptions()
@@ -73,7 +67,7 @@ saveOptions = ->
bgSettings.set fieldName, fieldValue
$(fieldName).value = fieldValue
$(fieldName).setAttribute "savedValue", fieldValue
- postSaveHooks[fieldName] fieldValue if postSaveHooks[fieldName]
+ bgSettings.performPostUpdateHook fieldName, fieldValue
$("saveOptions").disabled = true
diff --git a/pages/options.html b/pages/options.html
index 8c7c007a..b71625e8 100644
--- a/pages/options.html
+++ b/pages/options.html
@@ -122,6 +122,10 @@
width: 100%;
min-height: 135px;
}
+ textarea#searchEngines {
+ width: 100%;
+ min-height: 135px;
+ }
input#previousPatterns, input#nextPatterns {
width: 100%;
}
@@ -326,16 +330,32 @@ unmapAll
</td>
</tr>
<tr>
- <td class="caption">Search</td>
+ <td class="caption">Default Search<br/>Engine</td>
<td verticalAlign="top">
<div class="help">
<div class="example">
- Set which search engine is used when searching from the Vomnibar (examples: "http://duckduckgo.com/?q=", "http://www.google.com/search?q=").
+ The search engine which is used when searching from the Vomnibar
+ (e.g.: "http://duckduckgo.com/?q=").
</div>
</div>
<input id="searchUrl" type="text" />
</td>
</tr>
+ <tr>
+ <td class="caption">Custom Search<br/>Engines</td>
+ <td verticalAlign="top">
+ <div class="help">
+ <div class="example">
+ Use this to add shortcuts for your common search engines when using the Vomnibar.<br/><br/>
+ The format is:<br/>
+ <pre>your-keyword: http://the-site.com/?q=%s</pre>
+ %s will be replaced with your search term.<br/>
+ Lines which start with "#" are comments.
+ </div>
+ </div>
+ <textarea id="searchEngines"></textarea>
+ </td>
+ </tr>
</tbody>
</table>
diff --git a/pages/popup.html b/pages/popup.html
index c3cd3832..8ccf7126 100644
--- a/pages/popup.html
+++ b/pages/popup.html
@@ -54,7 +54,7 @@
<div id="excludeControls">
<input id="popupInput" type="text" />
<input id="popupButton" type="button" value="Exclude URL" />
- <span id="excludeConfirm"></span>
+ <span id="excludeConfirm">Saved exclude pattern.</span>
</div>
<div id="popupMenu">
diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index 04c81068..ac3f9ebe 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -184,6 +184,12 @@ context "Input focus",
assert.equal "third", document.activeElement.id
handlerStack.bubbleEvent 'keydown', mockKeyboardEvent("A")
+# TODO: these find prev/next link tests could be refactored into unit tests which invoke a function which has
+# a tighter contract than goNext(), since they test minor aspects of goNext()'s link matching behavior, and we
+# don't need to construct external state many times over just to test that.
+# i.e. these tests should look something like:
+# assert.equal(findLink(html("<a href=...">))[0].href, "first")
+# These could then move outside of the dom_tests file.
context "Find prev / next links",
setup ->
@@ -215,6 +221,29 @@ context "Find prev / next links",
goNext()
assert.equal '#second', window.location.hash
+ should "find link relation in header", ->
+ document.getElementById("test-div").innerHTML = """
+ <link rel='next' href='#first'>
+ """
+ goNext()
+ assert.equal '#first', window.location.hash
+
+ should "favor link relation to text matching", ->
+ document.getElementById("test-div").innerHTML = """
+ <link rel='next' href='#first'>
+ <a href='#second'>next</a>
+ """
+ goNext()
+ assert.equal '#first', window.location.hash
+
+ should "match mixed case link relation", ->
+ document.getElementById("test-div").innerHTML = """
+ <link rel='Next' href='#first'>
+ """
+ goNext()
+ assert.equal '#first', window.location.hash
+
+
createLinks = (n) ->
for i in [0...n] by 1
link = document.createElement("a")
diff --git a/tests/dom_tests/dom_utils_test.coffee b/tests/dom_tests/dom_utils_test.coffee
index d0f881ba..130a3014 100644
--- a/tests/dom_tests/dom_utils_test.coffee
+++ b/tests/dom_tests/dom_utils_test.coffee
@@ -42,6 +42,14 @@ context "Check visibility",
assert.equal null, DomUtils.getVisibleClientRect document.getElementById 'foo'
assert.equal null, DomUtils.getVisibleClientRect document.getElementById 'bar'
+ should "detect links only partially outside viewport as visible", ->
+ document.getElementById("test-div").innerHTML = """
+ <a id='foo' style='position:absolute;top:-10px'>test</a>
+ <a id='bar' style='position:absolute;left:-10px'>test</a>
+ """
+ assert.isTrue (DomUtils.getVisibleClientRect document.getElementById 'foo') != null
+ assert.isTrue (DomUtils.getVisibleClientRect document.getElementById 'bar') != null
+
should "detect opacity:0 links as hidden", ->
document.getElementById("test-div").innerHTML = """
<a id='foo' style='opacity:0'>test</a>
diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee
index fb267f63..02a741d5 100644
--- a/tests/unit_tests/completion_test.coffee
+++ b/tests/unit_tests/completion_test.coffee
@@ -23,6 +23,26 @@ context "bookmark completer",
results = filterCompleter(@completer, ["mark2"])
assert.arrayEqual [@bookmark2.url], results.map (suggestion) -> suggestion.url
+ should "return *no* matching bookmarks when there is no match", ->
+ @completer.refresh()
+ results = filterCompleter(@completer, ["does-not-match"])
+ assert.arrayEqual [], results.map (suggestion) -> suggestion.url
+
+ should "construct bookmark paths correctly", ->
+ @completer.refresh()
+ results = filterCompleter(@completer, ["mark2"])
+ assert.equal "/bookmark1/bookmark2", @bookmark2.pathAndTitle
+
+ should "return matching bookmark *titles* when searching *without* the folder separator character", ->
+ @completer.refresh()
+ results = filterCompleter(@completer, ["mark2"])
+ assert.arrayEqual ["bookmark2"], results.map (suggestion) -> suggestion.title
+
+ should "return matching bookmark *paths* when searching with the folder separator character", ->
+ @completer.refresh()
+ results = filterCompleter(@completer, ["/bookmark1", "mark2"])
+ assert.arrayEqual ["/bookmark1/bookmark2"], results.map (suggestion) -> suggestion.title
+
context "HistoryCache",
context "binary search",
setup ->
@@ -209,6 +229,20 @@ context "tab completer",
assert.arrayEqual ["tab2.com"], results.map (tab) -> tab.url
assert.arrayEqual [2], results.map (tab) -> tab.tabId
+context "search engines",
+ setup ->
+ searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s"
+ Settings.set 'searchEngines', searchEngines
+ @completer = new SearchEngineCompleter()
+ # note, I couldn't just call @completer.refresh() here as I couldn't set root.Settings without errors
+ # workaround is below, would be good for someone that understands the testing system better than me to improve
+ @completer.searchEngines = Settings.getSearchEngines()
+
+ should "return search engine suggestion", ->
+ results = filterCompleter(@completer, ["foo", "hello"])
+ assert.arrayEqual ["bar?q=hello"], results.map (result) -> result.url
+ assert.arrayEqual ["foo: hello"], results.map (result) -> result.title
+
context "suggestions",
should "escape html in page titles", ->
suggestion = new Suggestion(["queryterm"], "tab", "url", "title <span>", returns(1))
@@ -228,6 +262,50 @@ context "suggestions",
suggestion = new Suggestion(["queryterm"], "tab", "http://ninjawords.com", "ninjawords", returns(1))
assert.equal -1, suggestion.generateHtml().indexOf("http://ninjawords.com")
+context "RankingUtils.wordRelevancy",
+ should "score higher in shorter URLs", ->
+ highScore = RankingUtils.wordRelevancy(["stack"], "http://stackoverflow.com/short", "a-title")
+ lowScore = RankingUtils.wordRelevancy(["stack"], "http://stackoverflow.com/longer", "a-title")
+ assert.isTrue highScore > lowScore
+
+ should "score higher in shorter titles", ->
+ highScore = RankingUtils.wordRelevancy(["coffee"], "a-url", "Coffeescript")
+ lowScore = RankingUtils.wordRelevancy(["coffee"], "a-url", "Coffeescript rocks")
+ assert.isTrue highScore > lowScore
+
+ should "score higher for matching the start of a word (in a URL)", ->
+ lowScore = RankingUtils.wordRelevancy(["stack"], "http://Xstackoverflow.com/same", "a-title")
+ highScore = RankingUtils.wordRelevancy(["stack"], "http://stackoverflowX.com/same", "a-title")
+ assert.isTrue highScore > lowScore
+
+ should "score higher for matching the start of a word (in a title)", ->
+ lowScore = RankingUtils.wordRelevancy(["te"], "a-url", "Dist racted")
+ highScore = RankingUtils.wordRelevancy(["te"], "a-url", "Distrac ted")
+ assert.isTrue highScore > lowScore
+
+ should "score higher for matching a whole word (in a URL)", ->
+ lowScore = RankingUtils.wordRelevancy(["com"], "http://stackoverflow.comX/same", "a-title")
+ highScore = RankingUtils.wordRelevancy(["com"], "http://stackoverflowX.com/same", "a-title")
+ assert.isTrue highScore > lowScore
+
+ should "score higher for matching a whole word (in a title)", ->
+ lowScore = RankingUtils.wordRelevancy(["com"], "a-url", "abc comX")
+ highScore = RankingUtils.wordRelevancy(["com"], "a-url", "abcX com")
+ assert.isTrue highScore > lowScore
+
+ # # TODO: (smblott)
+ # # Word relevancy should take into account the number of matches (it doesn't currently).
+ # should "score higher for multiple matches (in a URL)", ->
+ # lowScore = RankingUtils.wordRelevancy(["stack"], "http://stackoverflow.com/Xxxxxx", "a-title")
+ # highScore = RankingUtils.wordRelevancy(["stack"], "http://stackoverflow.com/Xstack", "a-title")
+ # assert.isTrue highScore > lowScore
+
+ # should "score higher for multiple matches (in a title)", ->
+ # lowScore = RankingUtils.wordRelevancy(["bbc"], "http://stackoverflow.com/same", "BBC Radio 4 (XBCr4)")
+ # highScore = RankingUtils.wordRelevancy(["bbc"], "http://stackoverflow.com/same", "BBC Radio 4 (BBCr4)")
+ # assert.isTrue highScore > lowScore
+
+context "Suggestion.pushMatchingRanges",
should "extract ranges matching term (simple case, two matches)", ->
ranges = []
[ one, two, three ] = [ "one", "two", "three" ]
diff --git a/tests/unit_tests/settings_test.coffee b/tests/unit_tests/settings_test.coffee
index cfc0cfb6..1283497c 100644
--- a/tests/unit_tests/settings_test.coffee
+++ b/tests/unit_tests/settings_test.coffee
@@ -1,15 +1,22 @@
require "./test_helper.js"
+require "./test_chrome_stubs.js"
extend(global, require "../../lib/utils.js")
-Utils.getCurrentVersion = -> '1.43'
+Utils.getCurrentVersion = -> '1.44'
global.localStorage = {}
-{Settings} = require "../../background_scripts/settings.js"
+extend(global,require "../../background_scripts/sync.js")
+extend(global,require "../../background_scripts/settings.js")
+Sync.init()
context "settings",
setup ->
stub global, 'localStorage', {}
+ should "save settings in localStorage as JSONified strings", ->
+ Settings.set 'dummy', ""
+ assert.equal localStorage.dummy, '""'
+
should "obtain defaults if no key is stored", ->
assert.isFalse Settings.has 'scrollStepSize'
assert.equal Settings.get('scrollStepSize'), 60
@@ -28,3 +35,49 @@ context "settings",
Settings.set 'scrollStepSize', 20
Settings.clear 'scrollStepSize'
assert.equal Settings.get('scrollStepSize'), 60
+
+ should "propagate non-default value via synced storage listener", ->
+ Settings.set 'scrollStepSize', 20
+ assert.equal Settings.get('scrollStepSize'), 20
+ 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" } }
+ 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", ->
+ Settings.set 'scrollStepSize', 20
+ chrome.storage.sync.remove 'scrollStepSize'
+ assert.isFalse Settings.has 'scrollStepSize'
+
+ should "trigger a postUpdateHook", ->
+ message = "Hello World"
+ Settings.postUpdateHooks['scrollStepSize'] = (value) -> Sync.message = value
+ chrome.storage.sync.set { scrollStepSize: JSON.stringify(message) }
+ assert.equal message, Sync.message
+
+ should "set search engines, retrieve them correctly and check that it has been parsed correctly", ->
+ searchEngines = "foo: bar?q=%s\n# comment\nbaz: qux?q=%s"
+ parsedSearchEngines = {"foo": "bar?q=%s", "baz": "qux?q=%s"}
+ Settings.set 'searchEngines', searchEngines
+ assert.equal(searchEngines, Settings.get('searchEngines'))
+ result = Settings.getSearchEngines()
+ assert.isTrue(parsedSearchEngines["foo"] == result["foo"] &&
+ parsedSearchEngines["baz"] == result["baz"] && Object.keys(result).length == 2)
+
+ 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
new file mode 100644
index 00000000..e9c48f31
--- /dev/null
+++ b/tests/unit_tests/test_chrome_stubs.coffee
@@ -0,0 +1,61 @@
+
+#
+# This is a stub for chrome.strorage.sync for testing.
+# It does what chrome.storage.sync should do (roughly), but does so synchronously.
+#
+
+global.chrome =
+ runtime: {}
+
+ storage:
+
+ # chrome.storage.onChanged
+ onChanged:
+ addListener: (func) -> @func = func
+
+ # Fake a callback from chrome.storage.sync.
+ call: (key, value) ->
+ chrome.runtime = { lastError: undefined }
+ key_value = {}
+ key_value[key] = { newValue: value }
+ @func(key_value,'synced storage stub') if @func
+
+ callEmpty: (key) ->
+ chrome.runtime = { lastError: undefined }
+ if @func
+ items = {}
+ items[key] = {}
+ @func(items,'synced storage stub')
+
+ # chrome.storage.sync
+ sync:
+ store: {}
+
+ set: (items, callback) ->
+ chrome.runtime = { lastError: undefined }
+ for own key, value of items
+ @store[key] = value
+ callback() if callback
+ # Now, generate (supposedly asynchronous) notifications for listeners.
+ for own key, value of items
+ global.chrome.storage.onChanged.call(key,value)
+
+ get: (keys, callback) ->
+ chrome.runtime = { lastError: undefined }
+ if keys == null
+ keys = []
+ for own key, value of @store
+ keys.push key
+ items = {}
+ for key in keys
+ items[key] = @store[key]
+ # Now, generate (supposedly asynchronous) callback
+ callback items if callback
+
+ remove: (key, callback) ->
+ chrome. runtime = { lastError: undefined }
+ if key of @store
+ delete @store[key]
+ callback() if callback
+ # Now, generate (supposedly asynchronous) notification for listeners.
+ global.chrome.storage.onChanged.callEmpty(key)
diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee
index e1aa32c7..c4139dbb 100644
--- a/tests/unit_tests/utils_test.coffee
+++ b/tests/unit_tests/utils_test.coffee
@@ -1,6 +1,10 @@
require "./test_helper.js"
+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()
context "isUrl",
should "accept valid URLs", ->