diff options
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 @@ -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 @@ -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 @@ -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='#'>×</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="#">×</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", -> | 
