Vimium Patches

Check-in [24ef3f8064]
Login

Check-in [24ef3f8064]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:provisional port of patches to newest versions
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 24ef3f8064a53dc4f42063fdd6482ad759f82460
User & Date: beyert 2016-04-27 05:34:35
Context
2016-04-27
05:35
add missing files check-in: 55c6f5555e user: beyert tags: trunk
05:34
provisional port of patches to newest versions check-in: 24ef3f8064 user: beyert tags: trunk
04:23
add more missing files from git clone check-in: a53e9765e8 user: beyert tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to Makefile.

1
2
3
4
5
6
7
8
9
PROJECT=	vimium
PATCHES=	blank.html.diff commands.coffee.diff main.coffee.diff options.coffee.diff options.html.diff utils.coffee.diff vomnibar.coffee.diff

PATCH_PATH=	patches
PATCH_CMD=	patch -p0 <

build:
	cd ${PROJECT} && cake build


|







1
2
3
4
5
6
7
8
9
PROJECT=	vimium
PATCHES=	blank.html.diff commands.coffee.diff main.coffee.diff options.coffee.diff options.css.diff options.html.diff utils.coffee.diff vomnibar.coffee.diff

PATCH_PATH=	patches
PATCH_CMD=	patch -p0 <

build:
	cd ${PROJECT} && cake build

Changes to patches/commands.coffee.diff.

1
2
3
4
5

6

7
8
9
10
11
12

13
14
15
16
17
18
19

20
21







22

23
24
25
26
27
28

29


30
31
32
33
34
35
36
37
--- vimium/background_scripts/commands.coffee.orig	2014-02-16 15:17:40.000000000 -0800
+++ vimium/background_scripts/commands.coffee	2014-02-16 15:23:37.000000000 -0800
@@ -93,14 +93,14 @@
        "openCopiedUrlInCurrentTab", "openCopiedUrlInNewTab", "goUp", "goToRoot",
        "enterInsertMode", "focusInput",

        "LinkHints.activateMode", "LinkHints.activateModeToOpenInNewTab", "LinkHints.activateModeWithQueue",

-       "Vomnibar.activate", "Vomnibar.activateInNewTab", "Vomnibar.activateTabSelection",
+       "Vomnibar.activate", "Vomnibar.activateInitial", "Vomnibar.activateInNewTab", "Vomnibar.activateInNewTabInitial", "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", "createTabHome", "duplicateTab", "backRemoveTab", "removeTab", "restoreTab", "moveTabToNewWindow"]
     misc:
       ["showHelp"]
 

@@ -241,13 +241,17 @@
   firstTab: ["Go to the first tab", { background: true }]







   lastTab: ["Go to the last tab", { background: true }]

   createTab: ["Create new tab", { background: true }]
+  createTabHome: ["Open new tab with home page", { background: true }]
   duplicateTab: ["Duplicate current tab", { background: true }]
   removeTab: ["Close current tab", { background: true, noRepeat: true }]
+  backRemoveTab: ["Close current tab and move back", { background: true, noRepeat: true }]
   restoreTab: ["Restore closed tab", { background: true }]

   moveTabToNewWindow: ["Move tab to new window", { background: true }]


 
   "Vomnibar.activate": ["Open URL, bookmark, or history entry"]
+  "Vomnibar.activateInitial": ["Open URL, bookmark, or history entry, with initial content"]
   "Vomnibar.activateInNewTab": ["Open URL, bookmark, history entry, in a new tab"]
+  "Vomnibar.activateInNewTabInitial": ["Open URL, bookmark, history entry, in a new tab, with initial content"]
   "Vomnibar.activateTabSelection": ["Search through your open tabs"]
   "Vomnibar.activateBookmarks": ["Open a bookmark"]
   "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab"]
|
|
|
<
|
>
|
>
|
|
|
|
<
|
>


|
|
|
<
|
>
|
|
>
>
>
>
>
>
>

>
|
|
|
|

|
>

>
>

|
|
|
|
|
|
|
1
2
3

4
5
6
7
8
9
10
11

12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
--- vimium/background_scripts/commands.coffee.orig	2016-04-26 21:40:19.225524000 -0700
+++ vimium/background_scripts/commands.coffee	2016-04-26 21:55:07.675579000 -0700
@@ -154,7 +154,9 @@

       "Marks.activateGotoMode"]
     vomnibarCommands:
       ["Vomnibar.activate",
+      "Vomnibar.activateInitial",
       "Vomnibar.activateInNewTab",
+      "Vomnibar.activateInNewTabInitial",
       "Vomnibar.activateBookmarks",
       "Vomnibar.activateBookmarksInNewTab",

       "Vomnibar.activateTabSelection",
@@ -165,6 +167,7 @@
       ["goBack", "goForward"]
     tabManipulation:
       ["createTab",
+      "createTabHome",
       "previousTab",

       "nextTab",
       "visitPreviousTab",
@@ -172,6 +175,7 @@
       "lastTab",
       "duplicateTab",
       "togglePinTab",
+      "backRemoveTab",
       "removeTab",
       "restoreTab",
       "moveTabToNewWindow",
@@ -358,8 +362,10 @@
   lastTab: ["Go to the last tab", { background: true }]
 
   createTab: ["Create new tab", { background: true, repeatLimit: 20 }]
+  createTabHome: ["Open new tab with home page", { background: true, repeatLimit: 20 }]
   duplicateTab: ["Duplicate current tab", { background: true, repeatLimit: 20 }]
   removeTab: ["Close current tab", { background: true, repeatLimit: chrome.session?.MAX_SESSION_RESULTS ? 25 }]
+  backRemoveTab: ["Close current tab and move back", { background: true, noRepeat: true }]
   restoreTab: ["Restore closed tab", { background: true, repeatLimit: 20 }]
 
   moveTabToNewWindow: ["Move tab to new window", { background: true }]
@@ -373,7 +379,9 @@
   moveTabRight: ["Move tab to the right", { background: true }]
 
   "Vomnibar.activate": ["Open URL, bookmark or history entry", { topFrame: true }]
+  "Vomnibar.activateInitial": ["Open URL, bookmark, or history entry, with initial content", { topFrame: true }]
   "Vomnibar.activateInNewTab": ["Open URL, bookmark or history entry in a new tab", { topFrame: true }]
+  "Vomnibar.activateInNewTabInitial": ["Open URL, bookmark, history entry, in a new tab, with initial content", { topFrame: true }]
   "Vomnibar.activateTabSelection": ["Search through your open tabs", { topFrame: true }]
   "Vomnibar.activateBookmarks": ["Open a bookmark", { topFrame: true }]
   "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab", { topFrame: true }]

Changes to patches/main.coffee.diff.

1
2
3
4
5
6
7
8
9
10
11

12
13
14
15
16
17
18
19
20
21
22
--- vimium/background_scripts/main.coffee.orig	2013-06-17 14:36:56.000000000 -0700
+++ vimium/background_scripts/main.coffee	2013-06-17 21:56:00.000000000 -0700
@@ -223,6 +223,8 @@
 # mapped in commands.coffee.
 BackgroundCommands =
   createTab: (callback) -> chrome.tabs.create({ url: "chrome://newtab" }, (tab) -> callback())
+##  createTabHome: (request) -> openUrlInNewTab({ url: chrome.extension.getURL("pages/blank.html") })
+  createTabHome: (request) -> openUrlInNewTab({ url: Settings.get("homeUrl") })
   duplicateTab: (callback) ->
     chrome.tabs.getSelected(null, (tab) ->
       chrome.tabs.duplicate(tab.id)

@@ -237,6 +239,10 @@
   removeTab: ->
     chrome.tabs.getSelected(null, (tab) ->
       chrome.tabs.remove(tab.id))
+  backRemoveTab: (callback) ->
+    chrome.tabs.getSelected(null, (tab) ->
+      chrome.tabs.remove(tab.id))
+    selectTab(callback, "previous")
   restoreTab: (callback) ->
     # TODO(ilya): Should this be getLastFocused instead?
     chrome.windows.getCurrent((window) ->
|
|
|
|
|
|


|
<
|
>
|
|
|
|




|
|
|
1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
17
18
19
20
21
22
--- vimium/background_scripts/main.coffee.orig	2016-04-26 21:18:31.110990000 -0700
+++ vimium/background_scripts/main.coffee	2016-04-26 21:57:32.012134000 -0700
@@ -188,6 +188,8 @@
       else
         url
     TabOperations.openUrlInNewTab request, (tab) -> callback extend request, {tab, tabId: tab.id}
+##  createTabHome: (request) -> openUrlInNewTab({ url: chrome.extension.getURL("pages/blank.html") })
+  createTabHome: (request) -> openUrlInNewTab({ url: Settings.get("homeUrl") })
   duplicateTab: mkRepeatCommand (request, callback) ->

     chrome.tabs.duplicate request.tabId, (tab) -> callback extend request, {tab, tabId: tab.id}
   moveTabToNewWindow: ({count, tab}) ->
@@ -206,6 +208,10 @@
       activeTabIndex = tab.index
       startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count
       chrome.tabs.remove (tab.id for tab in tabs[startTabIndex...startTabIndex + count])
+  backRemoveTab: (callback) ->
+    chrome.tabs.getSelected(null, (tab) ->
+      chrome.tabs.remove(tab.id))
+    selectTab(callback, "previous")
   restoreTab: mkRepeatCommand (request, callback) -> chrome.sessions.restore null, callback request
   openCopiedUrlInCurrentTab: (request) -> TabOperations.openUrlInCurrentTab extend request, url: Clipboard.paste()
   openCopiedUrlInNewTab: (request) -> @createTab extend request, url: Clipboard.paste()

Changes to patches/options.coffee.diff.

1
2
3
4
5
6
7
8
9
10
11

--- vimium/pages/options.coffee.orig	2013-06-17 14:36:56.000000000 -0700
+++ vimium/pages/options.coffee	2013-06-17 21:07:24.000000000 -0700
@@ -4,7 +4,7 @@
 
 editableFields = [ "scrollStepSize", "excludedUrls", "linkHintCharacters", "linkHintNumbers",
   "userDefinedLinkHintCss", "keyMappings", "filterLinkHints", "previousPatterns",
-  "nextPatterns", "hideHud", "regexFindMode", "searchUrl"]
+  "nextPatterns", "hideHud", "regexFindMode", "searchUrl", "homeUrl"]
 
 canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"]
 

|
|
|
|
<
<
|
|
|
|

>
1
2
3
4


5
6
7
8
9
10
--- vimium/pages/options.coffee.orig	2016-04-26 21:18:31.138009000 -0700
+++ vimium/pages/options.coffee	2016-04-26 22:12:15.234927000 -0700
@@ -198,6 +198,7 @@
   grabBackFocus: CheckBoxOption


   searchEngines: TextOption
   searchUrl: NonEmptyTextOption
+  homeUrl: NonEmptyTextOption
   userDefinedLinkHintCss: TextOption
 
 initOptionsPage = ->

Changes to patches/options.html.diff.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
--- vimium/pages/options.html.orig	2013-06-17 14:36:56.000000000 -0700
+++ vimium/pages/options.html	2013-06-18 00:31:26.000000000 -0700
@@ -125,6 +125,9 @@
       input#previousPatterns, input#nextPatterns {
         width: 100%;
       }
+      input#homeUrl {
+        width: 100%;
+      }
       input#searchUrl {
         width: 100%;
       }
@@ -326,6 +329,17 @@
             </td>
           </tr>
           <tr>
+            <td class="caption">Home</td>
+            <td verticalAlign="top">
+                <div class="help">
+                  <div class="example">
+                    Set which URL is used for all new tabs.
+                  </div>
+                </div>
+                <input id="homeUrl" type="text" />
+            </td>
+          </tr>
+          <tr>
             <td class="caption">Search</td>
             <td verticalAlign="top">
                 <div class="help">
|
|
|
<
<
<
<
<
<
<
<
<
<














|


1
2
3










4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
--- vimium/pages/options.html.orig	2016-04-26 21:18:31.138184000 -0700
+++ vimium/pages/options.html	2016-04-26 22:30:24.713717000 -0700
@@ -224,6 +224,17 @@










             </td>
           </tr>
           <tr>
+            <td class="caption">Home</td>
+            <td verticalAlign="top">
+                <div class="help">
+                  <div class="example">
+                    Set which URL is used for all new tabs.
+                  </div>
+                </div>
+                <input id="homeUrl" type="text" />
+            </td>
+          </tr>
+          <tr>
             <td class="caption">Default search<br/>engine</td>
             <td verticalAlign="top">
                 <div class="help">

Changes to patches/utils.coffee.diff.

1
2
3
4
5
6
7
8
9
10
11
12

13
14
--- vimium/lib/utils.coffee.orig	2013-07-07 22:01:18.000000000 -0700
+++ vimium/lib/utils.coffee	2014-02-16 15:52:29.000000000 -0800
@@ -85,6 +85,11 @@
     # Fallback: no URL
     return false
 
+  getHome: (cur) ->
+   if Settings.get("homeUrl") == cur
+     ""
+   else cur
+
   # Creates a search URL from the given :query.

   createSearchUrl: (query) ->
     # it would be better to pull the default search engine from chrome itself,
|
|
|








|
>
|
<
1
2
3
4
5
6
7
8
9
10
11
12
13
14

--- vimium/lib/utils.coffee.orig	2016-04-26 21:18:31.137331000 -0700
+++ vimium/lib/utils.coffee	2016-04-26 22:19:43.003107000 -0700
@@ -106,6 +106,11 @@
     # Fallback: no URL
     return false
 
+  getHome: (cur) ->
+   if Settings.get("homeUrl") == cur
+     ""
+   else cur
+
   # Map a search query to its URL encoded form. The query may be either a string or an array of strings.
   # E.g. "BBC Sport" -> "BBC+Sport".
   createSearchQuery: (query) ->

Changes to patches/vomnibar.coffee.diff.

1
2
3

4
5
6
7
8
9
10
11
12




13
14
15
16
17
18
19

20
21
22
--- vimium/content_scripts/vomnibar.coffee.orig	2013-06-17 22:09:33.000000000 -0700
+++ vimium/content_scripts/vomnibar.coffee	2013-06-18 01:23:20.000000000 -0700
@@ -24,7 +24,19 @@

       @vomnibarUI.update()
 
   activate: -> @activateWithCompleter("omni", 100)
+  activateInitial: ->
+    cur = window.location.href
+    ##initial = (if (Settings.get("homeUrl") == cur) then "" else cur)
+    initial = (if ("file:///home/beyert/blank.htm" == cur) then "" else cur)
+    ##@activateWithCompleter("omni", 100, Utils.getHome(cur))
+    @activateWithCompleter("omni", 100, initial)




   activateInNewTab: -> @activateWithCompleter("omni", 100, null, false, true)
+  activateInNewTabInitial: ->
+    cur = window.location.href
+    initial = (if ("file:///home/beyert/blank.htm" == cur) then "" else cur)
+    #initial = (if (Settings.get("homeUrl") == cur) then "" else cur)
+    ##@activateWithCompleter("omni", 100, Utils.getHome(cur), false, true)
+    @activateWithCompleter("omni", 100, initial, false, true)

   activateTabSelection: -> @activateWithCompleter("tabs", 0, null, true)
   activateBookmarks: -> @activateWithCompleter("bookmarks", 0, null, true)
   activateBookmarksInNewTab: -> @activateWithCompleter("bookmarks", 0, null, true, true)
|
|
|
>
|

<






>
>
>
>
|






>
|
|
|
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
--- vimium/content_scripts/vomnibar.coffee.orig	2016-04-26 21:18:31.112901000 -0700
+++ vimium/content_scripts/vomnibar.coffee	2016-04-26 22:23:33.417484000 -0700
@@ -18,10 +18,24 @@
     @parseRegistryEntry registryEntry, (options) =>
       @open sourceFrameId, extend options, completer:"omni"
 

+  activateInitial: ->
+    cur = window.location.href
+    ##initial = (if (Settings.get("homeUrl") == cur) then "" else cur)
+    initial = (if ("file:///home/beyert/blank.htm" == cur) then "" else cur)
+    ##@activateWithCompleter("omni", 100, Utils.getHome(cur))
+    @activateWithCompleter("omni", 100, initial)
+
   activateInNewTab: (sourceFrameId, registryEntry) ->
     @parseRegistryEntry registryEntry, (options) =>
       @open sourceFrameId, extend options, completer:"omni", newTab: true
 
+  activateInNewTabInitial: ->
+    cur = window.location.href
+    initial = (if ("file:///home/beyert/blank.htm" == cur) then "" else cur)
+    #initial = (if (Settings.get("homeUrl") == cur) then "" else cur)
+    ##@activateWithCompleter("omni", 100, Utils.getHome(cur), false, true)
+    @activateWithCompleter("omni", 100, initial, false, true)
+
   activateTabSelection: (sourceFrameId) -> @open sourceFrameId, {
     completer: "tabs"
     selectFirst: true

Changes to vimium/background_scripts/commands.coffee.

150
151
152
153
154
155
156

157

158
159
160
161
162
163
164
165
166
167

168
169
170
171
172
173
174

175
176
177
178
179
180
181
      "goNext",
      "nextFrame",
      "mainFrame",
      "Marks.activateCreateMode",
      "Marks.activateGotoMode"]
    vomnibarCommands:
      ["Vomnibar.activate",

      "Vomnibar.activateInNewTab",

      "Vomnibar.activateBookmarks",
      "Vomnibar.activateBookmarksInNewTab",
      "Vomnibar.activateTabSelection",
      "Vomnibar.activateEditUrl",
      "Vomnibar.activateEditUrlInNewTab"]
    findCommands: ["enterFindMode", "performFind", "performBackwardsFind"]
    historyNavigation:
      ["goBack", "goForward"]
    tabManipulation:
      ["createTab",

      "previousTab",
      "nextTab",
      "visitPreviousTab",
      "firstTab",
      "lastTab",
      "duplicateTab",
      "togglePinTab",

      "removeTab",
      "restoreTab",
      "moveTabToNewWindow",
      "closeTabsOnLeft","closeTabsOnRight",
      "closeOtherTabs",
      "moveTabLeft",
      "moveTabRight"]







>

>










>







>







150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
      "goNext",
      "nextFrame",
      "mainFrame",
      "Marks.activateCreateMode",
      "Marks.activateGotoMode"]
    vomnibarCommands:
      ["Vomnibar.activate",
      "Vomnibar.activateInitial",
      "Vomnibar.activateInNewTab",
      "Vomnibar.activateInNewTabInitial",
      "Vomnibar.activateBookmarks",
      "Vomnibar.activateBookmarksInNewTab",
      "Vomnibar.activateTabSelection",
      "Vomnibar.activateEditUrl",
      "Vomnibar.activateEditUrlInNewTab"]
    findCommands: ["enterFindMode", "performFind", "performBackwardsFind"]
    historyNavigation:
      ["goBack", "goForward"]
    tabManipulation:
      ["createTab",
      "createTabHome",
      "previousTab",
      "nextTab",
      "visitPreviousTab",
      "firstTab",
      "lastTab",
      "duplicateTab",
      "togglePinTab",
      "backRemoveTab",
      "removeTab",
      "restoreTab",
      "moveTabToNewWindow",
      "closeTabsOnLeft","closeTabsOnRight",
      "closeOtherTabs",
      "moveTabLeft",
      "moveTabRight"]
354
355
356
357
358
359
360

361
362

363
364
365
366
367
368
369
370
371
372
373
374
375

376

377
378
379
380
381
382
383
  nextTab: ["Go one tab right", { background: true }]
  previousTab: ["Go one tab left", { background: true }]
  visitPreviousTab: ["Go to previously-visited tab", { background: true }]
  firstTab: ["Go to the first tab", { background: true }]
  lastTab: ["Go to the last tab", { background: true }]

  createTab: ["Create new tab", { background: true, repeatLimit: 20 }]

  duplicateTab: ["Duplicate current tab", { background: true, repeatLimit: 20 }]
  removeTab: ["Close current tab", { background: true, repeatLimit: chrome.session?.MAX_SESSION_RESULTS ? 25 }]

  restoreTab: ["Restore closed tab", { background: true, repeatLimit: 20 }]

  moveTabToNewWindow: ["Move tab to new window", { background: true }]
  togglePinTab: ["Pin/unpin current tab", { background: true, noRepeat: true }]

  closeTabsOnLeft: ["Close tabs on the left", {background: true, noRepeat: true}]
  closeTabsOnRight: ["Close tabs on the right", {background: true, noRepeat: true}]
  closeOtherTabs: ["Close all other tabs", {background: true, noRepeat: true}]

  moveTabLeft: ["Move tab to the left", { background: true }]
  moveTabRight: ["Move tab to the right", { background: true }]

  "Vomnibar.activate": ["Open URL, bookmark or history entry", { topFrame: true }]

  "Vomnibar.activateInNewTab": ["Open URL, bookmark or history entry in a new tab", { topFrame: true }]

  "Vomnibar.activateTabSelection": ["Search through your open tabs", { topFrame: true }]
  "Vomnibar.activateBookmarks": ["Open a bookmark", { topFrame: true }]
  "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab", { topFrame: true }]
  "Vomnibar.activateEditUrl": ["Edit the current URL", { topFrame: true }]
  "Vomnibar.activateEditUrlInNewTab": ["Edit the current URL and open in a new tab", { topFrame: true }]

  nextFrame: ["Select the next frame on the page", { background: true }]







>


>













>

>







358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
  nextTab: ["Go one tab right", { background: true }]
  previousTab: ["Go one tab left", { background: true }]
  visitPreviousTab: ["Go to previously-visited tab", { background: true }]
  firstTab: ["Go to the first tab", { background: true }]
  lastTab: ["Go to the last tab", { background: true }]

  createTab: ["Create new tab", { background: true, repeatLimit: 20 }]
  createTabHome: ["Open new tab with home page", { background: true, repeatLimit: 20 }]
  duplicateTab: ["Duplicate current tab", { background: true, repeatLimit: 20 }]
  removeTab: ["Close current tab", { background: true, repeatLimit: chrome.session?.MAX_SESSION_RESULTS ? 25 }]
  backRemoveTab: ["Close current tab and move back", { background: true, noRepeat: true }]
  restoreTab: ["Restore closed tab", { background: true, repeatLimit: 20 }]

  moveTabToNewWindow: ["Move tab to new window", { background: true }]
  togglePinTab: ["Pin/unpin current tab", { background: true, noRepeat: true }]

  closeTabsOnLeft: ["Close tabs on the left", {background: true, noRepeat: true}]
  closeTabsOnRight: ["Close tabs on the right", {background: true, noRepeat: true}]
  closeOtherTabs: ["Close all other tabs", {background: true, noRepeat: true}]

  moveTabLeft: ["Move tab to the left", { background: true }]
  moveTabRight: ["Move tab to the right", { background: true }]

  "Vomnibar.activate": ["Open URL, bookmark or history entry", { topFrame: true }]
  "Vomnibar.activateInitial": ["Open URL, bookmark, or history entry, with initial content", { topFrame: true }]
  "Vomnibar.activateInNewTab": ["Open URL, bookmark or history entry in a new tab", { topFrame: true }]
  "Vomnibar.activateInNewTabInitial": ["Open URL, bookmark, history entry, in a new tab, with initial content", { topFrame: true }]
  "Vomnibar.activateTabSelection": ["Search through your open tabs", { topFrame: true }]
  "Vomnibar.activateBookmarks": ["Open a bookmark", { topFrame: true }]
  "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab", { topFrame: true }]
  "Vomnibar.activateEditUrl": ["Edit the current URL", { topFrame: true }]
  "Vomnibar.activateEditUrlInNewTab": ["Edit the current URL and open in a new tab", { topFrame: true }]

  nextFrame: ["Select the next frame on the page", { background: true }]

Changes to vimium/background_scripts/commands.coffee.orig.

1
2
3
4







5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

51
52
53
54
55
56



57
58
59
60
61
62
63

64
65

66
67






68
69


70



71
72
73
74

75

76
77

78
79
80

81






82
83










84
85
86
87
88
89

90


91







92
93



94




95














96
97

98


99
100
101
102










103




104
105

106
107
108
109
110
111




112



113




114








115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134


135
136
137
138
139
140
141
142
143
144
145

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163



164
165
166
167
168
169
170
171


172
173
174
175
176
177
178
179
180



181

182
183
184
185
186
187

188
189
190
191
192
193
194

195
196
197

198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213



214
215
216
217
218
219
220
221


222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240

241
242

243
244
245
246

247

248







249
250
251
252
253


254
255

256
257
258
259
260
261
262
263
Commands =
  init: ->
    for command, description of commandDescriptions
      @addCommand(command, description[0], description[1])








  availableCommands: {}
  keyToCommandRegistry: {}

  # Registers a command, making it available to be optionally bound to a key.
  # options:
  #  - background: whether this command needs to be run against the background page.
  #  - passCountToFunction: true if this command should have any digits which were typed prior to the
  #    command passed to it. This is used to implement e.g. "closing of 3 tabs".
  addCommand: (command, description, options) ->
    if command of @availableCommands
      console.log(command, "is already defined! Check commands.coffee for duplicates.")
      return

    options ||= {}
    @availableCommands[command] =
      description: description
      isBackgroundCommand: options.background
      passCountToFunction: options.passCountToFunction
      noRepeat: options.noRepeat

  mapKeyToCommand: (key, command) ->
    unless @availableCommands[command]
      console.log(command, "doesn't exist!")
      return


    @keyToCommandRegistry[key] =
      command: command
      isBackgroundCommand: @availableCommands[command].isBackgroundCommand
      passCountToFunction: @availableCommands[command].passCountToFunction
      noRepeat: @availableCommands[command].noRepeat

  unmapKey: (key) -> delete @keyToCommandRegistry[key]

  # Lower-case the appropriate portions of named keys.
  #
  # A key name is one of three forms exemplified by <c-a> <left> or <c-f12>
  # (prefixed normal key, named key, or prefixed named key). Internally, for
  # simplicity, we would like prefixes and key names to be lowercase, though
  # humans may prefer other forms <Left> or <C-a>.
  # On the other hand, <c-a> and <c-A> are different named keys - for one of
  # them you have to press "shift" as well.
  normalizeKey: (key) ->
    key.replace(/<[acm]-/ig, (match) -> match.toLowerCase())
       .replace(/<([acm]-)?([a-zA-Z0-9]{2,5})>/g, (match, optionalPrefix, keyName) ->
          "<" + (if optionalPrefix then optionalPrefix else "") + keyName.toLowerCase() + ">")


  parseCustomKeyMappings: (customKeyMappings) ->
    lines = customKeyMappings.split("\n")

    for line in lines
      continue if (line[0] == "\"" || line[0] == "#")



      splitLine = line.split(/\s+/)

      lineCommand = splitLine[0]

      if (lineCommand == "map")
        continue if (splitLine.length != 3)
        key = @normalizeKey(splitLine[1])

        vimiumCommand = splitLine[2]


        continue unless @availableCommands[vimiumCommand]







        console.log("Mapping", key, "to", vimiumCommand)
        @mapKeyToCommand(key, vimiumCommand)


      else if (lineCommand == "unmap")



        continue if (splitLine.length != 2)

        key = @normalizeKey(splitLine[1])
        console.log("Unmapping", key)

        @unmapKey(key)

      else if (lineCommand == "unmapAll")
        @keyToCommandRegistry = {}


  clearKeyMappingsAndSetDefaults: ->
    @keyToCommandRegistry = {}








    for key of defaultKeyMappings
      @mapKeyToCommand(key, defaultKeyMappings[key])











  # An ordered listing of all available commands, grouped by type. This is the order they will
  # 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"]


    findCommands: ["enterFindMode", "performFind", "performBackwardsFind"]
    historyNavigation:
      ["goBack", "goForward"]
    tabManipulation:










      ["nextTab", "previousTab", "firstTab", "lastTab", "createTab", "duplicateTab", "removeTab", "restoreTab", "moveTabToNewWindow"]




    misc:
      ["showHelp"]


  # Rarely used commands are not shown by default in the help dialog or in the README. The goal is to present
  # a focused, high-signal set of commands to the new and casual user. Only those truly hungry for more power
  # from Vimium will uncover these gems.
  advancedCommands: [
    "scrollToLeft", "scrollToRight", "moveTabToNewWindow",




    "goUp", "goToRoot", "focusInput", "LinkHints.activateModeWithQueue",



    "LinkHints.activateModeToOpenIncognito", "goNext", "goPrevious", "Marks.activateCreateMode",




    "Marks.activateGotoMode"]









defaultKeyMappings =
  "?": "showHelp"
  "j": "scrollDown"
  "k": "scrollUp"
  "h": "scrollLeft"
  "l": "scrollRight"
  "gg": "scrollToTop"
  "G": "scrollToBottom"
  "zH": "scrollToLeft"
  "zL": "scrollToRight"
  "<c-e>": "scrollDown"
  "<c-y>": "scrollUp"

  "d": "scrollPageDown"
  "u": "scrollPageUp"
  "r": "reload"
  "gs": "toggleViewSource"

  "i": "enterInsertMode"



  "H": "goBack"
  "L": "goForward"
  "gu": "goUp"
  "gU": "goToRoot"

  "gi": "focusInput"

  "f":     "LinkHints.activateMode"
  "F":     "LinkHints.activateModeToOpenInNewTab"
  "<a-f>": "LinkHints.activateModeWithQueue"


  "/": "enterFindMode"
  "n": "performFind"
  "N": "performBackwardsFind"

  "[[": "goPrevious"
  "]]": "goNext"

  "yy": "copyCurrentUrl"
  "yf": "LinkHints.activateModeToCopyLinkUrl"

  "p": "openCopiedUrlInCurrentTab"
  "P": "openCopiedUrlInNewTab"

  "K": "nextTab"
  "J": "previousTab"
  "gt": "nextTab"
  "gT": "previousTab"



  "g0": "firstTab"
  "g$": "lastTab"

  "W": "moveTabToNewWindow"
  "t": "createTab"
  "yt": "duplicateTab"
  "x": "removeTab"
  "X": "restoreTab"



  "o": "Vomnibar.activate"
  "O": "Vomnibar.activateInNewTab"

  "T": "Vomnibar.activateTabSelection"

  "b": "Vomnibar.activateBookmarks"
  "B": "Vomnibar.activateBookmarksInNewTab"




  "gf": "nextFrame"


  "m": "Marks.activateCreateMode"
  "`": "Marks.activateGotoMode"


# This is a mapping of: commandIdentifier => [description, options].

commandDescriptions =
  # Navigating the current page
  showHelp: ["Show help", { background: true }]
  scrollDown: ["Scroll down"]
  scrollUp: ["Scroll up"]
  scrollLeft: ["Scroll left"]
  scrollRight: ["Scroll right"]

  scrollToTop: ["Scroll to the top of the page"]
  scrollToBottom: ["Scroll to the bottom of the page"]
  scrollToLeft: ["Scroll all the way to the left"]


  scrollToRight: ["Scroll all the way to the right"]
  scrollPageDown: ["Scroll a page down"]
  scrollPageUp: ["Scroll a page up"]
  scrollFullPageDown: ["Scroll a full page down"]
  scrollFullPageUp: ["Scroll a full page up"]

  reload: ["Reload the page"]
  toggleViewSource: ["View page source"]

  copyCurrentUrl: ["Copy the current URL to the clipboard"]
  'LinkHints.activateModeToCopyLinkUrl': ["Copy a link URL to the clipboard"]
  openCopiedUrlInCurrentTab: ["Open the clipboard's URL in the current tab", { background: true }]
  openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true }]

  enterInsertMode: ["Enter insert mode"]




  focusInput: ["Focus the first (or n-th) text box on the page", { 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.activateModeToOpenIncognito": ["Open a link in incognito window"]



  enterFindMode: ["Enter find mode"]
  performFind: ["Cycle forward to the next find match"]
  performBackwardsFind: ["Cycle backward to the previous find match"]

  goPrevious: ["Follow the link labeled previous or <"]
  goNext: ["Follow the link labeled next or >"]

  # Navigating your history
  goBack: ["Go back in history", { passCountToFunction: true }]
  goForward: ["Go forward in history", { passCountToFunction: true }]

  # Navigating the URL hierarchy
  goUp: ["Go up the URL hierarchy", { passCountToFunction: true }]
  goToRoot: ["Go to root of current URL hierarchy", { passCountToFunction: true }]

  # Manipulating tabs
  nextTab: ["Go one tab right", { background: true }]
  previousTab: ["Go one tab left", { background: true }]

  firstTab: ["Go to the first tab", { background: true }]
  lastTab: ["Go to the last tab", { background: true }]

  createTab: ["Create new tab", { background: true }]
  duplicateTab: ["Duplicate current tab", { background: true }]
  removeTab: ["Close current tab", { background: true, noRepeat: true }]
  restoreTab: ["Restore closed tab", { background: true }]

  moveTabToNewWindow: ["Move tab to new window", { background: true }]









  "Vomnibar.activate": ["Open URL, bookmark, or history entry"]
  "Vomnibar.activateInNewTab": ["Open URL, bookmark, history entry, in a new tab"]
  "Vomnibar.activateTabSelection": ["Search through your open tabs"]
  "Vomnibar.activateBookmarks": ["Open a bookmark"]
  "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab"]



  nextFrame: ["Cycle forward to the next frame on the page", { background: true, passCountToFunction: true }]


  "Marks.activateCreateMode": ["Create a new mark"]
  "Marks.activateGotoMode": ["Go to a mark"]

Commands.init()

root = exports ? window
root.Commands = Commands


|
|
>
>
>
>
>
>
>







<
<
|

|


<
|
<
<
<
<

|

|


>
|
<
<
<
<
<
<













>


|
|
|
|
>
>
>
|
|
|

|
|
|
>
|

>
|

>
>
>
>
>
>
|
|
>
>
|
>
>
>
|

<
<
>
|
>
|
|
>



>

>
>
>
>
>
>
|
|
>
>
>
>
>
>
>
>
>
>





|
>
|
>
>
|
>
>
>
>
>
>
>
|
|
>
>
>
|
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
>
|
>
>




>
>
>
>
>
>
>
>
>
>
|
>
>
>
>

|
>





|
>
>
>
>
|
>
>
>
|
>
>
>
>
|
>
>
>
>
>
>
>
>




















>
>








|
|

>









<








>
>
>








>
>









>
>
>

>






>


|




>

|
|
>

<
|
|



|
|

|
<
|
|

|
>
>
>

|

|
|
|
|

>
>

|



|
|


|
|


|
|




>


>
|
|
|
|
>

>

>
>
>
>
>
>
>
|
|
|
|
|
>
>

|
>

|
|





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18


19
20
21
22
23

24




25
26
27
28
29
30
31
32






33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84


85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255

256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310

311
312
313
314
315
316
317
318
319

320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
Commands =
  init: ->
    for own command, descriptor of commandDescriptions
      @addCommand(command, descriptor[0], descriptor[1])
    @loadKeyMappings Settings.get "keyMappings"
    Settings.postUpdateHooks["keyMappings"] = @loadKeyMappings.bind this

  loadKeyMappings: (customKeyMappings) ->
    @clearKeyMappingsAndSetDefaults()
    @parseCustomKeyMappings customKeyMappings
    @generateKeyStateMapping()

  availableCommands: {}
  keyToCommandRegistry: {}

  # Registers a command, making it available to be optionally bound to a key.
  # options:
  #  - background: whether this command needs to be run against the background page.


  addCommand: (command, description, options = {}) ->
    if command of @availableCommands
      BgUtils.log "#{command} is already defined! Check commands.coffee for duplicates."
      return


    @availableCommands[command] = extend options, description: description





  mapKeyToCommand: ({ key, command, options }) ->
    unless @availableCommands[command]
      BgUtils.log "#{command} doesn't exist!"
      return

    options ?= {}
    @keyToCommandRegistry[key] = extend { command, options }, @availableCommands[command]







  # Lower-case the appropriate portions of named keys.
  #
  # A key name is one of three forms exemplified by <c-a> <left> or <c-f12>
  # (prefixed normal key, named key, or prefixed named key). Internally, for
  # simplicity, we would like prefixes and key names to be lowercase, though
  # humans may prefer other forms <Left> or <C-a>.
  # On the other hand, <c-a> and <c-A> are different named keys - for one of
  # them you have to press "shift" as well.
  normalizeKey: (key) ->
    key.replace(/<[acm]-/ig, (match) -> match.toLowerCase())
       .replace(/<([acm]-)?([a-zA-Z0-9]{2,5})>/g, (match, optionalPrefix, keyName) ->
          "<" + (if optionalPrefix then optionalPrefix else "") + keyName.toLowerCase() + ">")
       .replace /<space>/ig, " "

  parseCustomKeyMappings: (customKeyMappings) ->
    for line in customKeyMappings.split "\n"
      unless  line[0] == "\"" or line[0] == "#"
        tokens = line.replace(/\s+$/, "").split /\s+/
        switch tokens[0]
          when "map"
            [ _, key, command, optionList... ] = tokens
            if command? and @availableCommands[command]
              key = @normalizeKey key
              BgUtils.log "Mapping #{key} to #{command}"
              @mapKeyToCommand { key, command, options: @parseCommandOptions command, optionList }

          when "unmap"
            if tokens.length == 2
              key = @normalizeKey tokens[1]
              BgUtils.log "Unmapping #{key}"
              delete @keyToCommandRegistry[key]

          when "unmapAll"
            @keyToCommandRegistry = {}

    # Push the key mapping for passNextKey into Settings so that it's available in the front end for insert
    # mode.  We exclude single-key mappings (that is, printable keys) because when users press printable keys
    # in insert mode they expect the character to be input, not to be droppped into some special Vimium
    # mode.
    Settings.set "passNextKeyKeys",
      (key for own key of @keyToCommandRegistry when @keyToCommandRegistry[key].command == "passNextKey" and 1 < key.length)

  # Command options follow command mappings, and are of one of two forms:
  #   key=value     - a value
  #   key           - a flag
  parseCommandOptions: (command, optionList) ->
    options = {}
    for option in optionList
      parse = option.split "=", 2
      options[parse[0]] = if parse.length == 1 then true else parse[1]



    # We parse any `count` option immediately (to avoid having to parse it repeatedly later).
    if "count" of options
      options.count = parseInt options.count
      delete options.count if isNaN(options.count) or @availableCommands[command].noRepeat

    options

  clearKeyMappingsAndSetDefaults: ->
    @keyToCommandRegistry = {}
    @mapKeyToCommand { key, command } for own key, command of defaultKeyMappings

  # This generates a nested key-to-command mapping structure. There is an example in mode_key_handler.coffee.
  generateKeyStateMapping: ->
    # Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12>
    # This regular expression captures two groups: the first is a named key, the second is the remainder of
    # the string.
    namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/
    keyStateMapping = {}
    for own keys, registryEntry of @keyToCommandRegistry
      currentMapping = keyStateMapping
      while 0 < keys.length
        [key, keys] = if 0 == keys.search namedKeyRegex then [RegExp.$1, RegExp.$2] else [keys[0], keys[1..]]
        if currentMapping[key]?.command
          break # Do not overwrite existing command bindings, they take priority.
        else if 0 < keys.length
          currentMapping = currentMapping[key] ?= {}
        else
          currentMapping[key] = registryEntry
    chrome.storage.local.set normalModeKeyStateMapping: keyStateMapping

  # An ordered listing of all available commands, grouped by type. This is the order they will
  # be shown in the help page.
  commandGroups:
    pageNavigation:
      ["scrollDown",
      "scrollUp",
      "scrollToTop",
      "scrollToBottom",
      "scrollPageDown",
      "scrollPageUp",
      "scrollFullPageDown",
      "scrollFullPageUp",
      "scrollLeft",
      "scrollRight",
      "scrollToLeft",
      "scrollToRight",
      "reload",
      "copyCurrentUrl",
      "openCopiedUrlInCurrentTab",
      "openCopiedUrlInNewTab",
      "goUp",
      "goToRoot",
      "enterInsertMode",
      "enterVisualMode",
      "enterVisualLineMode",
      "passNextKey",
      "focusInput",
      "LinkHints.activateMode",
      "LinkHints.activateModeToOpenInNewTab",
      "LinkHints.activateModeToOpenInNewForegroundTab",
      "LinkHints.activateModeWithQueue",
      "LinkHints.activateModeToDownloadLink",
      "LinkHints.activateModeToOpenIncognito",
      "LinkHints.activateModeToCopyLinkUrl",
      "goPrevious",
      "goNext",
      "nextFrame",
      "mainFrame",
      "Marks.activateCreateMode",
      "Marks.activateGotoMode"]
    vomnibarCommands:
      ["Vomnibar.activate",
      "Vomnibar.activateInNewTab",
      "Vomnibar.activateBookmarks",
      "Vomnibar.activateBookmarksInNewTab",
      "Vomnibar.activateTabSelection",
      "Vomnibar.activateEditUrl",
      "Vomnibar.activateEditUrlInNewTab"]
    findCommands: ["enterFindMode", "performFind", "performBackwardsFind"]
    historyNavigation:
      ["goBack", "goForward"]
    tabManipulation:
      ["createTab",
      "previousTab",
      "nextTab",
      "visitPreviousTab",
      "firstTab",
      "lastTab",
      "duplicateTab",
      "togglePinTab",
      "removeTab",
      "restoreTab",
      "moveTabToNewWindow",
      "closeTabsOnLeft","closeTabsOnRight",
      "closeOtherTabs",
      "moveTabLeft",
      "moveTabRight"]
    misc:
      ["showHelp",
      "toggleViewSource"]

  # Rarely used commands are not shown by default in the help dialog or in the README. The goal is to present
  # a focused, high-signal set of commands to the new and casual user. Only those truly hungry for more power
  # from Vimium will uncover these gems.
  advancedCommands: [
    "scrollToLeft",
    "scrollToRight",
    "moveTabToNewWindow",
    "goUp",
    "goToRoot",
    "LinkHints.activateModeWithQueue",
    "LinkHints.activateModeToDownloadLink",
    "Vomnibar.activateEditUrl",
    "Vomnibar.activateEditUrlInNewTab",
    "LinkHints.activateModeToOpenIncognito",
    "LinkHints.activateModeToCopyLinkUrl",
    "goNext",
    "goPrevious",
    "Marks.activateCreateMode",
    "Marks.activateGotoMode",
    "moveTabLeft",
    "moveTabRight",
    "closeTabsOnLeft",
    "closeTabsOnRight",
    "closeOtherTabs",
    "enterVisualLineMode",
    "toggleViewSource",
    "passNextKey"]

defaultKeyMappings =
  "?": "showHelp"
  "j": "scrollDown"
  "k": "scrollUp"
  "h": "scrollLeft"
  "l": "scrollRight"
  "gg": "scrollToTop"
  "G": "scrollToBottom"
  "zH": "scrollToLeft"
  "zL": "scrollToRight"
  "<c-e>": "scrollDown"
  "<c-y>": "scrollUp"

  "d": "scrollPageDown"
  "u": "scrollPageUp"
  "r": "reload"
  "gs": "toggleViewSource"

  "i": "enterInsertMode"
  "v": "enterVisualMode"
  "V": "enterVisualLineMode"

  "H": "goBack"
  "L": "goForward"
  "gu": "goUp"
  "gU": "goToRoot"

  "gi": "focusInput"

  "f": "LinkHints.activateMode"
  "F": "LinkHints.activateModeToOpenInNewTab"
  "<a-f>": "LinkHints.activateModeWithQueue"
  "yf": "LinkHints.activateModeToCopyLinkUrl"

  "/": "enterFindMode"
  "n": "performFind"
  "N": "performBackwardsFind"

  "[[": "goPrevious"
  "]]": "goNext"

  "yy": "copyCurrentUrl"


  "p": "openCopiedUrlInCurrentTab"
  "P": "openCopiedUrlInNewTab"

  "K": "nextTab"
  "J": "previousTab"
  "gt": "nextTab"
  "gT": "previousTab"
  "^": "visitPreviousTab"
  "<<": "moveTabLeft"
  ">>": "moveTabRight"
  "g0": "firstTab"
  "g$": "lastTab"

  "W": "moveTabToNewWindow"
  "t": "createTab"
  "yt": "duplicateTab"
  "x": "removeTab"
  "X": "restoreTab"

  "<a-p>": "togglePinTab"

  "o": "Vomnibar.activate"
  "O": "Vomnibar.activateInNewTab"

  "T": "Vomnibar.activateTabSelection"

  "b": "Vomnibar.activateBookmarks"
  "B": "Vomnibar.activateBookmarksInNewTab"

  "ge": "Vomnibar.activateEditUrl"
  "gE": "Vomnibar.activateEditUrlInNewTab"

  "gf": "nextFrame"
  "gF": "mainFrame"

  "m": "Marks.activateCreateMode"
  "`": "Marks.activateGotoMode"


# This is a mapping of: commandIdentifier => [description, options].
# If the noRepeat and repeatLimit options are both specified, then noRepeat takes precedence.
commandDescriptions =
  # Navigating the current page
  showHelp: ["Show help", { topFrame: true, noRepeat: true }]
  scrollDown: ["Scroll down"]
  scrollUp: ["Scroll up"]
  scrollLeft: ["Scroll left"]
  scrollRight: ["Scroll right"]

  scrollToTop: ["Scroll to the top of the page"]
  scrollToBottom: ["Scroll to the bottom of the page", { noRepeat: true }]
  scrollToLeft: ["Scroll all the way to the left", { noRepeat: true }]
  scrollToRight: ["Scroll all the way to the right", { noRepeat: true }]


  scrollPageDown: ["Scroll a half page down"]
  scrollPageUp: ["Scroll a half page up"]
  scrollFullPageDown: ["Scroll a full page down"]
  scrollFullPageUp: ["Scroll a full page up"]

  reload: ["Reload the page", { noRepeat: true }]
  toggleViewSource: ["View page source", { noRepeat: true }]

  copyCurrentUrl: ["Copy the current URL to the clipboard", { noRepeat: true }]

  openCopiedUrlInCurrentTab: ["Open the clipboard's URL in the current tab", { background: true, noRepeat: true }]
  openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true, repeatLimit: 20 }]

  enterInsertMode: ["Enter insert mode", { noRepeat: true }]
  passNextKey: ["Pass the next key to Chrome"]
  enterVisualMode: ["Enter visual mode", { noRepeat: true }]
  enterVisualLineMode: ["Enter visual line mode", { noRepeat: true }]

  focusInput: ["Focus the first text input on the page"]

  "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", { noRepeat: true }]
  "LinkHints.activateModeToOpenIncognito": ["Open a link in incognito window"]
  "LinkHints.activateModeToDownloadLink": ["Download link url"]
  "LinkHints.activateModeToCopyLinkUrl": ["Copy a link URL to the clipboard"]

  enterFindMode: ["Enter find mode", { noRepeat: true }]
  performFind: ["Cycle forward to the next find match"]
  performBackwardsFind: ["Cycle backward to the previous find match"]

  goPrevious: ["Follow the link labeled previous or <", { noRepeat: true }]
  goNext: ["Follow the link labeled next or >", { noRepeat: true }]

  # Navigating your history
  goBack: ["Go back in history"]
  goForward: ["Go forward in history"]

  # Navigating the URL hierarchy
  goUp: ["Go up the URL hierarchy"]
  goToRoot: ["Go to root of current URL hierarchy"]

  # Manipulating tabs
  nextTab: ["Go one tab right", { background: true }]
  previousTab: ["Go one tab left", { background: true }]
  visitPreviousTab: ["Go to previously-visited tab", { background: true }]
  firstTab: ["Go to the first tab", { background: true }]
  lastTab: ["Go to the last tab", { background: true }]

  createTab: ["Create new tab", { background: true, repeatLimit: 20 }]
  duplicateTab: ["Duplicate current tab", { background: true, repeatLimit: 20 }]
  removeTab: ["Close current tab", { background: true, repeatLimit: chrome.session?.MAX_SESSION_RESULTS ? 25 }]
  restoreTab: ["Restore closed tab", { background: true, repeatLimit: 20 }]

  moveTabToNewWindow: ["Move tab to new window", { background: true }]
  togglePinTab: ["Pin/unpin current tab", { background: true, noRepeat: true }]

  closeTabsOnLeft: ["Close tabs on the left", {background: true, noRepeat: true}]
  closeTabsOnRight: ["Close tabs on the right", {background: true, noRepeat: true}]
  closeOtherTabs: ["Close all other tabs", {background: true, noRepeat: true}]

  moveTabLeft: ["Move tab to the left", { background: true }]
  moveTabRight: ["Move tab to the right", { background: true }]

  "Vomnibar.activate": ["Open URL, bookmark or history entry", { topFrame: true }]
  "Vomnibar.activateInNewTab": ["Open URL, bookmark or history entry in a new tab", { topFrame: true }]
  "Vomnibar.activateTabSelection": ["Search through your open tabs", { topFrame: true }]
  "Vomnibar.activateBookmarks": ["Open a bookmark", { topFrame: true }]
  "Vomnibar.activateBookmarksInNewTab": ["Open a bookmark in a new tab", { topFrame: true }]
  "Vomnibar.activateEditUrl": ["Edit the current URL", { topFrame: true }]
  "Vomnibar.activateEditUrlInNewTab": ["Edit the current URL and open in a new tab", { topFrame: true }]

  nextFrame: ["Select the next frame on the page", { background: true }]
  mainFrame: ["Select the page's main/top frame", { topFrame: true, noRepeat: true }]

  "Marks.activateCreateMode": ["Create a new mark", { noRepeat: true }]
  "Marks.activateGotoMode": ["Go to a mark", { noRepeat: true }]

Commands.init()

root = exports ? window
root.Commands = Commands

Changes to vimium/background_scripts/main.coffee.

184
185
186
187
188
189
190


191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208




209
210
211
212
213
214
215
      url = Settings.get "newTabUrl"
      if url == "pages/blank.html"
        # "pages/blank.html" does not work in incognito mode, so fall back to "chrome://newtab" instead.
        if request.tab.incognito then "chrome://newtab" else chrome.runtime.getURL newTabUrl
      else
        url
    TabOperations.openUrlInNewTab request, (tab) -> callback extend request, {tab, tabId: tab.id}


  duplicateTab: mkRepeatCommand (request, callback) ->
    chrome.tabs.duplicate request.tabId, (tab) -> callback extend request, {tab, tabId: tab.id}
  moveTabToNewWindow: ({count, tab}) ->
    chrome.tabs.query {currentWindow: true}, (tabs) ->
      activeTabIndex = tab.index
      startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count
      [ tab, tabs... ] = tabs[startTabIndex...startTabIndex + count]
      chrome.windows.create {tabId: tab.id, incognito: tab.incognito}, (window) ->
        chrome.tabs.move (tab.id for tab in tabs), {windowId: window.id, index: -1}
  nextTab: (request) -> selectTab "next", request
  previousTab: (request) -> selectTab "previous", request
  firstTab: (request) -> selectTab "first", request
  lastTab: (request) -> selectTab "last", request
  removeTab: ({count, tab}) ->
    chrome.tabs.query {currentWindow: true}, (tabs) ->
      activeTabIndex = tab.index
      startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count
      chrome.tabs.remove (tab.id for tab in tabs[startTabIndex...startTabIndex + count])




  restoreTab: mkRepeatCommand (request, callback) -> chrome.sessions.restore null, callback request
  openCopiedUrlInCurrentTab: (request) -> TabOperations.openUrlInCurrentTab extend request, url: Clipboard.paste()
  openCopiedUrlInNewTab: (request) -> @createTab extend request, url: Clipboard.paste()
  togglePinTab: ({tab}) -> chrome.tabs.update tab.id, {pinned: !tab.pinned}
  moveTabLeft: moveTab
  moveTabRight: moveTab
  nextFrame: ({count, frameId, tabId}) ->







>
>


















>
>
>
>







184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
      url = Settings.get "newTabUrl"
      if url == "pages/blank.html"
        # "pages/blank.html" does not work in incognito mode, so fall back to "chrome://newtab" instead.
        if request.tab.incognito then "chrome://newtab" else chrome.runtime.getURL newTabUrl
      else
        url
    TabOperations.openUrlInNewTab request, (tab) -> callback extend request, {tab, tabId: tab.id}
##  createTabHome: (request) -> openUrlInNewTab({ url: chrome.extension.getURL("pages/blank.html") })
  createTabHome: (request) -> openUrlInNewTab({ url: Settings.get("homeUrl") })
  duplicateTab: mkRepeatCommand (request, callback) ->
    chrome.tabs.duplicate request.tabId, (tab) -> callback extend request, {tab, tabId: tab.id}
  moveTabToNewWindow: ({count, tab}) ->
    chrome.tabs.query {currentWindow: true}, (tabs) ->
      activeTabIndex = tab.index
      startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count
      [ tab, tabs... ] = tabs[startTabIndex...startTabIndex + count]
      chrome.windows.create {tabId: tab.id, incognito: tab.incognito}, (window) ->
        chrome.tabs.move (tab.id for tab in tabs), {windowId: window.id, index: -1}
  nextTab: (request) -> selectTab "next", request
  previousTab: (request) -> selectTab "previous", request
  firstTab: (request) -> selectTab "first", request
  lastTab: (request) -> selectTab "last", request
  removeTab: ({count, tab}) ->
    chrome.tabs.query {currentWindow: true}, (tabs) ->
      activeTabIndex = tab.index
      startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count
      chrome.tabs.remove (tab.id for tab in tabs[startTabIndex...startTabIndex + count])
  backRemoveTab: (callback) ->
    chrome.tabs.getSelected(null, (tab) ->
      chrome.tabs.remove(tab.id))
    selectTab(callback, "previous")
  restoreTab: mkRepeatCommand (request, callback) -> chrome.sessions.restore null, callback request
  openCopiedUrlInCurrentTab: (request) -> TabOperations.openUrlInCurrentTab extend request, url: Clipboard.paste()
  openCopiedUrlInNewTab: (request) -> @createTab extend request, url: Clipboard.paste()
  togglePinTab: ({tab}) -> chrome.tabs.update tab.id, {pinned: !tab.pinned}
  moveTabLeft: moveTab
  moveTabRight: moveTab
  nextFrame: ({count, frameId, tabId}) ->

Changes to vimium/background_scripts/main.coffee.orig.

1
2


















3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21





22
23
24
25
26

27
28
29
30
31

32

33
34
35
36
37
38
39
40
41
42
43
44
45




46

47

48
49
50
51
52

53
54
55
56
57

58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105




106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

123
124








125


126

127
128
129
130
131
132

133
134
135
136
137
138

139
140
141



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212

213
214
215
216
217
218
219
220
221
222
223
224





225



226
227
228
229
230
231



232

233
234
235
236
237
238


239
240
241
242
243
244
245
246
247
248
249
250
251
252



253
254
255
256
257
258
259

260
261
262
263
264
265
266
267
268






269


270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343

344
345
346
347
348
349

350
351
352
353
354
355


356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407


408
409
410


411
412
413
414
415
416
417
418


419
420
421
422
423


424
425
426
427
428
429

430
431
432
433
434
435

436
437
438
439

440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484


485
486
487
488
489
490
491
492
493


494
495
496
497


498
499
500
501
502

503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522




523
524
525
526
527
528
529
530
531
532
533

534
535
536
537
538
539
540
541
542

543




544

545
546
547
548

549
550
551
552
553
554
555
556

557


558
559
560
561
562
563

564
565
566

567
568
569
570
571
572
573




















574
575
576
577
578
579
580
581
582
583
584
585
586
587
588




589

















590
591
592
593
594
595

596
597
598

root = exports ? window



















currentVersion = Utils.getCurrentVersion()

tabQueue = {} # windowId -> Array
tabInfoMap = {} # tabId -> object with various tab properties
keyQueue = "" # Queue of keys typed
validFirstKeys = {}
singleKeyCommands = []
focusedFrame = null
framesForTab = {}

# Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12>
# This regular expression captures two groups: the first is a named key, the second is the remainder of
# the string.
namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/

# Event handlers
selectionChangedHandlers = []
tabLoadedHandlers = {} # tabId -> function()






completionSources =
  bookmarks: new BookmarkCompleter()
  history: new HistoryCompleter()
  domains: new DomainCompleter()
  tabs: new TabCompleter()


completers =
  omni: new MultiCompleter([
    completionSources.bookmarks,
    completionSources.history,

    completionSources.domains])

  bookmarks: new MultiCompleter([completionSources.bookmarks])
  tabs: new MultiCompleter([completionSources.tabs])

chrome.runtime.onConnect.addListener((port, name) ->
  senderTabId = if port.sender.tab then port.sender.tab.id else null
  # If this is a tab we've been waiting to open, execute any "tab loaded" handlers, e.g. to restore
  # the tab's scroll position. Wait until domReady before doing this; otherwise operations like restoring
  # the scroll position will not be possible.
  if (port.name == "domReady" && senderTabId != null)
    if (tabLoadedHandlers[senderTabId])
      toCall = tabLoadedHandlers[senderTabId]
      # Delete first to be sure there's no circular events.
      delete tabLoadedHandlers[senderTabId]




      toCall.call()



    # domReady is the appropriate time to show the "vimium has been upgraded" message.
    # TODO: This might be broken on pages with frames.
    if (shouldShowUpgradeMessage())
      chrome.tabs.sendMessage(senderTabId, { name: "showUpgradeNotification", version: currentVersion })


  if (portHandlers[port.name])
    port.onMessage.addListener(portHandlers[port.name])
)

chrome.runtime.onMessage.addListener((request, sender, sendResponse) ->

  if (sendRequestHandlers[request.handler])
    sendResponse(sendRequestHandlers[request.handler](request, sender))
  # Ensure the sendResponse callback is freed.
  return false)

#
# Used by the content scripts to get their full URL. This is needed for URLs like "view-source:http:# .."
# because window.location doesn't know anything about the Chrome-specific "view-source:".
#
getCurrentTabUrl = (request, sender) -> sender.tab.url

#
# Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL.
#
isEnabledForUrl = (request) ->
  # excludedUrls are stored as a series of URL expressions separated by newlines.
  excludedUrls = Settings.get("excludedUrls").split("\n")
  isEnabled = true
  for url in excludedUrls
    # The user can add "*" to the URL which means ".*"
    regexp = new RegExp("^" + url.replace(/\*/g, ".*") + "$")
    isEnabled = false if request.url.match(regexp)
  { isEnabledForUrl: isEnabled }

# Called by the popup UI. Strips leading/trailing whitespace and ignores empty strings.
root.addExcludedUrl = (url) ->
  return unless url = url.trim()

  excludedUrls = Settings.get("excludedUrls")
  return if excludedUrls.indexOf(url) >= 0

  excludedUrls += "\n" + url
  Settings.set("excludedUrls", excludedUrls)

  chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true },
    (tabs) -> updateActiveState(tabs[0].id))

saveHelpDialogSettings = (request) ->
  Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands)

# Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings.
# This is called by options.coffee.
root.helpDialogHtml = (showUnboundCommands, showCommandNames, customTitle) ->
  commandsToKey = {}
  for key of Commands.keyToCommandRegistry
    command = Commands.keyToCommandRegistry[key].command
    commandsToKey[command] = (commandsToKey[command] || []).concat(key)





  dialogHtml = fetchFileContents("pages/help_dialog.html")
  for group of Commands.commandGroups
    dialogHtml = dialogHtml.replace("{{#{group}}}",
        helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands,
                                      showUnboundCommands, showCommandNames))
  dialogHtml = dialogHtml.replace("{{version}}", currentVersion)
  dialogHtml = dialogHtml.replace("{{title}}", customTitle || "Help")
  dialogHtml

#
# Generates HTML for a given set of commands. commandGroups are defined in commands.js
#
helpDialogHtmlForCommandGroup = (group, commandsToKey, availableCommands,
    showUnboundCommands, showCommandNames) ->
  html = []
  for command in Commands.commandGroups[group]
    bindings = (commandsToKey[command] || [""]).join(", ")

    if (showUnboundCommands || commandsToKey[command])
      isAdvanced = Commands.advancedCommands.indexOf(command) >= 0








      html.push(


        "<tr class='vimiumReset #{"advanced" if isAdvanced}'>",

        "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>",
        "<td class='vimiumReset'>:</td><td class='vimiumReset'>", availableCommands[command].description)

      if (showCommandNames)
        html.push("<span class='vimiumReset commandName'>(#{command})</span>")


      html.push("</td></tr>")
  html.join("\n")

#
# Fetches the contents of a file bundled with this extension.
#

fetchFileContents = (extensionFileName) ->
  req = new XMLHttpRequest()
  req.open("GET", chrome.runtime.getURL(extensionFileName), false) # false => synchronous



  req.send()
  req.responseText

#
# Returns the keys that can complete a valid command given the current key queue.
#
getCompletionKeysRequest = (request, keysToCheck = "") ->
  name: "refreshCompletionKeys"
  completionKeys: generateCompletionKeys(keysToCheck)
  validFirstKeys: validFirstKeys

#
# Opens the url in the current tab.
#
openUrlInCurrentTab = (request) ->
  chrome.tabs.getSelected(null,
    (tab) -> chrome.tabs.update(tab.id, { url: Utils.convertToUrl(request.url) }))

#
# Opens request.url in new tab and switches to it if request.selected is true.
#
openUrlInNewTab = (request) ->
  chrome.tabs.getSelected(null, (tab) ->
    chrome.tabs.create({ url: Utils.convertToUrl(request.url), index: tab.index + 1, selected: true }))

openUrlInIncognito = (request) ->
  chrome.windows.create({ url: Utils.convertToUrl(request.url), incognito: true})

#
# Called when the user has clicked the close icon on the "Vimium has been updated" message.
# We should now dismiss that message in all tabs.
#
upgradeNotificationClosed = (request) ->
  Settings.set("previousVersion", currentVersion)
  sendRequestToAllTabs({ name: "hideUpgradeNotification" })

#
# Copies some data (request.data) to the clipboard.
#
copyToClipboard = (request) -> Clipboard.copy(request.data)

#
# Selects the tab with the ID specified in request.id
#
selectSpecificTab = (request) ->
  chrome.tabs.get(request.id, (tab) ->
    chrome.windows.update(tab.windowId, { focused: true })
    chrome.tabs.update(request.id, { selected: true }))

#
# Used by the content scripts to get settings from the local storage.
#
handleSettings = (args, port) ->
  if (args.operation == "get")
    value = Settings.get(args.key)
    port.postMessage({ key: args.key, value: value })
  else # operation == "set"
    Settings.set(args.key, args.value)

refreshCompleter = (request) -> completers[request.name].refresh()

whitespaceRegexp = /\s+/
filterCompleter = (args, port) ->
  queryTerms = if (args.query == "") then [] else args.query.split(whitespaceRegexp)
  completers[args.name].filter(queryTerms, (results) -> port.postMessage({ id: args.id, results: results }))

getCurrentTimeInSeconds = -> Math.floor((new Date()).getTime() / 1000)

chrome.tabs.onSelectionChanged.addListener (tabId, selectionInfo) ->
  if (selectionChangedHandlers.length > 0)
    selectionChangedHandlers.pop().call()


repeatFunction = (func, totalCount, currentCount, frameId) ->
  if (currentCount < totalCount)
    func(
      -> repeatFunction(func, totalCount, currentCount + 1, frameId),
      frameId)

# Start action functions

# These are commands which are bound to keystroke which must be handled by the background page. They are
# mapped in commands.coffee.
BackgroundCommands =





  createTab: (callback) -> chrome.tabs.create({ url: "chrome://newtab" }, (tab) -> callback())



  duplicateTab: (callback) ->
    chrome.tabs.getSelected(null, (tab) ->
      chrome.tabs.duplicate(tab.id)
      selectionChangedHandlers.push(callback))
  moveTabToNewWindow: (callback) ->
    chrome.tabs.getSelected(null, (tab) ->



      chrome.windows.create({tabId: tab.id}))

  nextTab: (callback) -> selectTab(callback, "next")
  previousTab: (callback) -> selectTab(callback, "previous")
  firstTab: (callback) -> selectTab(callback, "first")
  lastTab: (callback) -> selectTab(callback, "last")
  removeTab: ->
    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()))
  openCopiedUrlInCurrentTab: (request) -> openUrlInCurrentTab({ url: Clipboard.paste() })
  openCopiedUrlInNewTab: (request) -> openUrlInNewTab({ url: Clipboard.paste() })

  showHelp: (callback, frameId) ->
    chrome.tabs.getSelected(null, (tab) ->
      chrome.tabs.sendMessage(tab.id,
        { name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId }))
  nextFrame: (count) ->
    chrome.tabs.getSelected(null, (tab) ->
      frames = framesForTab[tab.id].frames
      currIndex = getCurrFrameIndex(frames)







      # TODO: Skip the "top" frame (which doesn't actually have a <frame> tag),


      # since it exists only to contain the other frames.
      newIndex = (currIndex + count) % frames.length

      chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[newIndex].id, highlight: true }))

# Selects a tab before or after the currently selected tab.
# - direction: "next", "previous", "first" or "last".
selectTab = (callback, direction) ->
  chrome.tabs.getAllInWindow(null, (tabs) ->
    return unless tabs.length > 1
    chrome.tabs.getSelected(null, (currentTab) ->
      switch direction
        when "next"
          toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length]
        when "previous"
          toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length]
        when "first"
          toSelect = tabs[0]
        when "last"
          toSelect = tabs[tabs.length - 1]
      selectionChangedHandlers.push(callback)
      chrome.tabs.update(toSelect.id, { selected: true })))

updateOpenTabs = (tab) ->
  # Chrome might reuse the tab ID of a recently removed tab.
  if tabInfoMap[tab.id]?.deletor
    clearTimeout tabInfoMap[tab.id].deletor
  tabInfoMap[tab.id] =
    url: tab.url
    positionIndex: tab.index
    windowId: tab.windowId
    scrollX: null
    scrollY: null
    deletor: null
  # Frames are recreated on refresh
  delete framesForTab[tab.id]

# Updates the browserAction icon to indicated whether Vimium is enabled or disabled on the current page.
# Also disables Vimium if it is currently enabled but should be disabled according to the url blacklist.
# This lets you disable Vimium on a page without needing to reload.
#
# Three situations are considered:
# 1. Active tab is disabled -> disable icon
# 2. Active tab is enabled and should be enabled -> enable icon
# 3. Active tab is enabled but should be disabled -> disable icon and disable vimium
updateActiveState = (tabId) ->
  enabledIcon = "icons/browser_action_enabled.png"
  disabledIcon = "icons/browser_action_disabled.png"
  chrome.tabs.get(tabId, (tab) ->
    # Default to disabled state in case we can't connect to Vimium, primarily for the "New Tab" page.
    chrome.browserAction.setIcon({ path: disabledIcon })
    chrome.tabs.sendMessage(tabId, { name: "getActiveState" }, (response) ->
      isCurrentlyEnabled = (response? && response.enabled)
      shouldBeEnabled = isEnabledForUrl({url: tab.url}).isEnabledForUrl

      if (isCurrentlyEnabled)
        if (shouldBeEnabled)
          chrome.browserAction.setIcon({ path: enabledIcon })
        else
          chrome.browserAction.setIcon({ path: disabledIcon })
          chrome.tabs.sendMessage(tabId, { name: "disableVimium" })
      else
        chrome.browserAction.setIcon({ path: disabledIcon })))

handleUpdateScrollPosition = (request, sender) ->
  updateScrollPosition(sender.tab, request.scrollX, request.scrollY)

updateScrollPosition = (tab, scrollX, scrollY) ->
  tabInfoMap[tab.id].scrollX = scrollX
  tabInfoMap[tab.id].scrollY = scrollY

chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) ->
  return unless changeInfo.status == "loading" # only do this once per URL change
  chrome.tabs.insertCSS tabId,

    allFrames: true
    code: Settings.get("userDefinedLinkHintCss")
    runAt: "document_start"
  updateOpenTabs(tab)
  updateActiveState(tabId)


chrome.tabs.onAttached.addListener (tabId, attachedInfo) ->
  # We should update all the tabs in the old window and the new window.
  if tabInfoMap[tabId]
    updatePositionsAndWindowsForAllTabsInWindow(tabInfoMap[tabId].windowId)
  updatePositionsAndWindowsForAllTabsInWindow(attachedInfo.newWindowId)



chrome.tabs.onMoved.addListener (tabId, moveInfo) ->
  updatePositionsAndWindowsForAllTabsInWindow(moveInfo.windowId)

chrome.tabs.onRemoved.addListener (tabId) ->
  openTabInfo = tabInfoMap[tabId]
  updatePositionsAndWindowsForAllTabsInWindow(openTabInfo.windowId)

  # If we restore pages that content scripts can't run on, they'll ignore Vimium keystrokes when they
  # reappear. Pretend they never existed and adjust tab indices accordingly. Could possibly expand this into
  # a blacklist in the future.
  if (/^(chrome|view-source:)[^:]*:\/\/.*/.test(openTabInfo.url))
    for i of tabQueue[openTabInfo.windowId]
      if (tabQueue[openTabInfo.windowId][i].positionIndex > openTabInfo.positionIndex)
        tabQueue[openTabInfo.windowId][i].positionIndex--
    return

  if (tabQueue[openTabInfo.windowId])
    tabQueue[openTabInfo.windowId].push(openTabInfo)
  else
    tabQueue[openTabInfo.windowId] = [openTabInfo]

  # keep the reference around for a while to wait for the last messages from the closed tab (e.g. for updating
  # scroll position)
  tabInfoMap.deletor = -> delete tabInfoMap[tabId]
  setTimeout tabInfoMap.deletor, 1000
  delete framesForTab[tabId]

chrome.tabs.onActiveChanged.addListener (tabId, selectInfo) -> updateActiveState(tabId)

chrome.windows.onRemoved.addListener (windowId) -> delete tabQueue[windowId]

# End action functions

updatePositionsAndWindowsForAllTabsInWindow = (windowId) ->
  chrome.tabs.getAllInWindow(windowId, (tabs) ->
    for tab in tabs
      openTabInfo = tabInfoMap[tab.id]
      if (openTabInfo)
        openTabInfo.positionIndex = tab.index
        openTabInfo.windowId = tab.windowId)

splitKeyIntoFirstAndSecond = (key) ->
  if (key.search(namedKeyRegex) == 0)
    { first: RegExp.$1, second: RegExp.$2 }
  else
    { first: key[0], second: key.slice(1) }

getActualKeyStrokeLength = (key) ->
  if (key.search(namedKeyRegex) == 0)
    1 + getActualKeyStrokeLength(RegExp.$2)
  else
    key.length



populateValidFirstKeys = ->
  for key of Commands.keyToCommandRegistry


    if (getActualKeyStrokeLength(key) == 2)
      validFirstKeys[splitKeyIntoFirstAndSecond(key).first] = true

populateSingleKeyCommands = ->
  for key of Commands.keyToCommandRegistry
    if (getActualKeyStrokeLength(key) == 1)
      singleKeyCommands.push(key)



# Invoked by options.coffee.
root.refreshCompletionKeysAfterMappingSave = ->
  validFirstKeys = {}
  singleKeyCommands = []



  populateValidFirstKeys()
  populateSingleKeyCommands()

  sendRequestToAllTabs(getCompletionKeysRequest())

# Generates a list of keys that can complete a valid command given the current key queue or the one passed in

generateCompletionKeys = (keysToCheck) ->
  splitHash = splitKeyQueue(keysToCheck || keyQueue)
  command = splitHash.command
  count = splitHash.count

  completionKeys = singleKeyCommands.slice(0)


  if (getActualKeyStrokeLength(command) == 1)
    for key of Commands.keyToCommandRegistry
      splitKey = splitKeyIntoFirstAndSecond(key)

      if (splitKey.first == command)
        completionKeys.push(splitKey.second)

  completionKeys

splitKeyQueue = (queue) ->
  match = /([1-9][0-9]*)?(.*)/.exec(queue)
  count = parseInt(match[1], 10)
  command = match[2]

  { count: count, command: command }

handleKeyDown = (request, port) ->
  key = request.keyChar
  if (key == "<ESC>")
    console.log("clearing keyQueue")
    keyQueue = ""
  else
    console.log("checking keyQueue: [", keyQueue + key, "]")
    keyQueue = checkKeyQueue(keyQueue + key, port.sender.tab.id, request.frameId)
    console.log("new KeyQueue: " + keyQueue)

checkKeyQueue = (keysToCheck, tabId, frameId) ->
  refreshedCompletionKeys = false
  splitHash = splitKeyQueue(keysToCheck)
  command = splitHash.command
  count = splitHash.count

  return keysToCheck if command.length == 0
  count = 1 if isNaN(count)

  if (Commands.keyToCommandRegistry[command])
    registryEntry = Commands.keyToCommandRegistry[command]

    if !registryEntry.isBackgroundCommand
      chrome.tabs.sendMessage(tabId,
        name: "executePageCommand",
        command: registryEntry.command,
        frameId: frameId,
        count: count,
        passCountToFunction: registryEntry.passCountToFunction,
        completionKeys: generateCompletionKeys(""))
      refreshedCompletionKeys = true
    else
      if registryEntry.passCountToFunction


        BackgroundCommands[registryEntry.command](count)
      else if registryEntry.noRepeat
        BackgroundCommands[registryEntry.command]()
      else
        repeatFunction(BackgroundCommands[registryEntry.command], count, 0, frameId)

    newKeyQueue = ""
  else if (getActualKeyStrokeLength(command) > 1)
    splitKey = splitKeyIntoFirstAndSecond(command)



    # The second key might be a valid command by its self.
    if (Commands.keyToCommandRegistry[splitKey.second])
      newKeyQueue = checkKeyQueue(splitKey.second, tabId, frameId)


    else
      newKeyQueue = (if validFirstKeys[splitKey.second] then splitKey.second else "")
  else
    newKeyQueue = (if validFirstKeys[command] then count.toString() + command else "")


  # If we haven't sent the completion keys piggybacked on executePageCommand,
  # send them by themselves.
  unless refreshedCompletionKeys
    chrome.tabs.sendMessage(tabId, getCompletionKeysRequest(null, newKeyQueue), null)

  newKeyQueue

#
# Message all tabs. Args should be the arguments hash used by the Chrome sendRequest API.
#
sendRequestToAllTabs = (args) ->
  chrome.windows.getAll({ populate: true }, (windows) ->
    for window in windows
      for tab in window.tabs
        chrome.tabs.sendMessage(tab.id, args, null))

#
# Returns true if the current extension version is greater than the previously recorded version in
# localStorage, and false otherwise.
#




shouldShowUpgradeMessage = ->
  # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new
  # installs.
  Settings.set("previousVersion", currentVersion) unless Settings.get("previousVersion")
  Utils.compareVersions(currentVersion, Settings.get("previousVersion")) == 1

openOptionsPageInNewTab = ->
  chrome.tabs.getSelected(null, (tab) ->
    chrome.tabs.create({ url: chrome.runtime.getURL("pages/options.html"), index: tab.index + 1 }))

registerFrame = (request, sender) ->

  unless framesForTab[sender.tab.id]
    framesForTab[sender.tab.id] = { frames: [] }

  if (request.is_top)
    focusedFrame = request.frameId
    framesForTab[sender.tab.id].total = request.total

  framesForTab[sender.tab.id].frames.push({ id: request.frameId, area: request.area })


handleFrameFocused = (request, sender) -> focusedFrame = request.frameId






getCurrFrameIndex = (frames) ->
  for i in [0...frames.length]
    return i if frames[i].id == focusedFrame
  frames.length + 1


# Port handler mapping
portHandlers =
  keyDown: handleKeyDown,
  settings: handleSettings,
  filterCompleter: filterCompleter

sendRequestHandlers =

  getCompletionKeys: getCompletionKeysRequest,


  getCurrentTabUrl: getCurrentTabUrl,
  openUrlInNewTab: openUrlInNewTab,
  openUrlInIncognito: openUrlInIncognito,
  openUrlInCurrentTab: openUrlInCurrentTab,
  openOptionsPageInNewTab: openOptionsPageInNewTab,
  registerFrame: registerFrame,

  frameFocused: handleFrameFocused,
  upgradeNotificationClosed: upgradeNotificationClosed,
  updateScrollPosition: handleUpdateScrollPosition,

  copyToClipboard: copyToClipboard,
  isEnabledForUrl: isEnabledForUrl,
  saveHelpDialogSettings: saveHelpDialogSettings,
  selectSpecificTab: selectSpecificTab,
  refreshCompleter: refreshCompleter
  createMark: Marks.create.bind(Marks),
  gotoMark: Marks.goto.bind(Marks)





















# Convenience function for development use.
window.runTests = -> open(chrome.runtime.getURL('tests/dom_tests/dom_tests.html'))

#
# Begin initialization.
#
Commands.clearKeyMappingsAndSetDefaults()

if Settings.has("keyMappings")
  Commands.parseCustomKeyMappings(Settings.get("keyMappings"))

populateValidFirstKeys()
populateSingleKeyCommands()
if shouldShowUpgradeMessage()




  sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion })


















# Ensure that tabInfoMap is populated when Vimium is installed.
chrome.windows.getAll { populate: true }, (windows) ->
  for window in windows
    for tab in window.tabs
      updateOpenTabs(tab)

      createScrollPositionHandler = ->
        (response) -> updateScrollPosition(tab, response.scrollX, response.scrollY) if response?
      chrome.tabs.sendMessage(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler())



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|
<
<
<
|
<
<
|

<
<
<
<
|
<
<
|

>
>
>
>
>

|
|
|
|
>


|
|
|
>
|
>
|
|

<
<
<
<
<
<
|
<
<
|
>
>
>
>
|
>

>
|
|
|
|

>

|
|
<

>





<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<

<
<
|
<
<
|
<
<
|
<
<


<
|

|



>
>
>
>
|
|
|

|
|
|
<








|
>


>
>
>
>
>
>
>
>
|
>
>
|
>
|
<
|
<
|
|
>
|
<

<
<
<
>
|

|
>
>
>

<

<
<
<
<
<
<
<
|
<
|
<
|
<
<
|
|
|
<
|
|
|
|
<
|
|
<
<
<
<
|
<
<
|
<
<
<
<









<
<
<
|
<
<
<
<
<
|
<
|
<
|
<
<
|
<
|
|
<
<
>

|
|
<
<
<
|
<

|


>
>
>
>
>
|
>
>
>
|
<
|
<
|
|
>
>
>
|
>
|
|
|
|
|
|
>
>
|
|
<
<
<
<
<
<
|
|
<
<
|
|
>
>
>
|
<
|
<
<
|
|
>
|
<
|
<
|
<
<
<

>
>
>
>
>
>
|
>
>
|
|

|



|
|
|
|
|
|
|
|
|
|
|
|
|
<
|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

|
<
>



|
<

>
|
|
|
<
<

>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<
<
<
<

<
<
<
<
|
|
|
|
<

<
|
<
<
|
<
<
<
<
|
<
<
<
<
<

|
<
<
<
<
>
>

<
|
>
>
|
<
|
<
|
<
<

>
>
|
<
<
<

>
>
|
|
|
<
|
<
>
|
|
|
<

<
>

<
<
<
>
|
<
|
<
|
<
<
<
<

<
|
<
|
<
<
<
<
<
<
<

|
<
<
<
<
|
<
<
|
<
<
|
<
|
|
<
|
|
<
<
<
<
<
>
>
|
<
<
<
|

<
<
<
>
>

<
<
|
>
>

|
|
<

>
|
|
|
<
|
<
|
|
|
<
|
<
|
<
<
|
|
<
<
<
>
>
>
>
<
<
<
<
<

<
<
<
|
<
>
|
|
|
|
|
|
|
<
|
>
|
>
>
>
>

>
|
|
<
|
>



|
|
<


>
|
>
>
|
|
|
|
|
<
>
|
<
<
>
|
<
|
|
<
|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







<

<
<
|
<
<
|
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|
|
<
<
|
>
|
<
<
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22



23


24
25




26


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50






51


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

70
71
72
73
74
75
76





77












78



79


80


81


82


83
84

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

128

129
130
131
132

133



134
135
136
137
138
139
140
141

142







143

144

145


146
147
148

149
150
151
152

153
154




155


156




157
158
159
160
161
162
163
164
165



166





167

168

169


170

171
172


173
174
175
176



177

178
179
180
181
182
183
184
185
186
187
188
189
190
191

192

193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209






210
211


212
213
214
215
216
217

218


219
220
221
222

223

224



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254

255
256
















































257
258

259
260
261
262
263

264
265
266
267
268


269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287




288




289
290
291
292

293

294


295




296





297
298




299
300
301

302
303
304
305

306

307


308
309
310
311



312
313
314
315
316
317

318

319
320
321
322

323

324
325



326
327

328

329




330

331

332







333
334




335


336


337

338
339

340
341





342
343
344



345
346



347
348
349


350
351
352
353
354
355

356
357
358
359
360

361

362
363
364

365

366


367
368



369
370
371
372





373



374

375
376
377
378
379
380
381
382

383
384
385
386
387
388
389
390
391
392
393

394
395
396
397
398
399
400

401
402
403
404
405
406
407
408
409
410
411

412
413


414
415

416
417

418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446

447


448


449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474


475
476
477


478
root = exports ? window

# The browser may have tabs already open. We inject the content scripts immediately so that they work straight
# away.
chrome.runtime.onInstalled.addListener ({ reason }) ->
  # See https://developer.chrome.com/extensions/runtime#event-onInstalled
  return if reason in [ "chrome_update", "shared_module_update" ]
  manifest = chrome.runtime.getManifest()
  # Content scripts loaded on every page should be in the same group. We assume it is the first.
  contentScripts = manifest.content_scripts[0]
  jobs = [ [ chrome.tabs.executeScript, contentScripts.js ], [ chrome.tabs.insertCSS, contentScripts.css ] ]
  # Chrome complains if we don't evaluate chrome.runtime.lastError on errors (and we get errors for tabs on
  # which Vimium cannot run).
  checkLastRuntimeError = -> chrome.runtime.lastError
  chrome.tabs.query { status: "complete" }, (tabs) ->
    for tab in tabs
      for [ func, files ] in jobs
        for file in files
          func tab.id, { file: file, allFrames: contentScripts.all_frames }, checkLastRuntimeError

currentVersion = Utils.getCurrentVersion()
frameIdsForTab = {}



portsForTab = {}


root.urlForTab = {}





# This is exported for use by "marks.coffee".


root.tabLoadedHandlers = {} # tabId -> function()

# A secret, available only within the current instantiation of Vimium.  The secret is big, likely unguessable
# in practice, but less than 2^31.
chrome.storage.local.set
  vimiumSecret: Math.floor Math.random() * 2000000000

completionSources =
  bookmarks: new BookmarkCompleter
  history: new HistoryCompleter
  domains: new DomainCompleter
  tabs: new TabCompleter
  searchEngines: new SearchEngineCompleter

completers =
  omni: new MultiCompleter [
    completionSources.bookmarks
    completionSources.history
    completionSources.domains
    completionSources.searchEngines
    ]
  bookmarks: new MultiCompleter [completionSources.bookmarks]
  tabs: new MultiCompleter [completionSources.tabs]







completionHandlers =


  filter: (completer, request, port) ->
    completer.filter request, (response) ->
      # We use try here because this may fail if the sender has already navigated away from the original page.
      # This can happen, for example, when posting completion suggestions from the SearchEngineCompleter
      # (which is done asynchronously).
      try
        port.postMessage extend request, extend response, handler: "completions"

  refresh: (completer, _, port) -> completer.refresh port
  cancel: (completer, _, port) -> completer.cancel port

handleCompletions = (sender) -> (request, port) ->
  completionHandlers[request.handler] completers[request.name], request, port

chrome.runtime.onConnect.addListener (port) ->
  if (portHandlers[port.name])
    port.onMessage.addListener portHandlers[port.name] port.sender, port


chrome.runtime.onMessage.addListener((request, sender, sendResponse) ->
  request = extend {count: 1, frameId: sender.frameId}, extend request, tab: sender.tab, tabId: sender.tab.id
  if (sendRequestHandlers[request.handler])
    sendResponse(sendRequestHandlers[request.handler](request, sender))
  # Ensure the sendResponse callback is freed.
  return false)






onURLChange = (details) ->












  chrome.tabs.sendMessage details.tabId, name: "checkEnabledAfterURLChange"






# Re-check whether Vimium is enabled for a frame when the url changes without a reload.


chrome.webNavigation.onHistoryStateUpdated.addListener onURLChange # history.pushState.


chrome.webNavigation.onReferenceFragmentUpdated.addListener onURLChange # Hash changed.



# Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings.

getHelpDialogHtml = ({showUnboundCommands, showCommandNames, customTitle}) ->
  commandsToKey = {}
  for own key of Commands.keyToCommandRegistry
    command = Commands.keyToCommandRegistry[key].command
    commandsToKey[command] = (commandsToKey[command] || []).concat(key)

  replacementStrings =
    version: currentVersion
    title: customTitle || "Help"
    tip: if showCommandNames then "Tip: click command names to yank them to the clipboard." else "&nbsp;"

  for own group of Commands.commandGroups
    replacementStrings[group] =
        helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands,
                                      showUnboundCommands, showCommandNames)

  replacementStrings


#
# Generates HTML for a given set of commands. commandGroups are defined in commands.js
#
helpDialogHtmlForCommandGroup = (group, commandsToKey, availableCommands,
    showUnboundCommands, showCommandNames) ->
  html = []
  for command in Commands.commandGroups[group]
    keys = commandsToKey[command] || []
    bindings = ("<span class='vimiumHelpDialogKey'>#{Utils.escapeHtml key}</span>" for key in keys).join ", "
    if (showUnboundCommands || commandsToKey[command])
      isAdvanced = Commands.advancedCommands.indexOf(command) >= 0
      description = availableCommands[command].description
      if keys.join(", ").length < 12
        helpDialogHtmlForCommand html, isAdvanced, bindings, description, showCommandNames, command
      else
        # If the length of the bindings is too long, then we display the bindings on a separate row from the
        # description.  This prevents the column alignment from becoming out of whack.
        helpDialogHtmlForCommand html, isAdvanced, bindings, "", false, ""
        helpDialogHtmlForCommand html, isAdvanced, "", description, showCommandNames, command
  html.join("\n")

helpDialogHtmlForCommand = (html, isAdvanced, bindings, description, showCommandNames, command) ->
  html.push "<tr class='vimiumReset #{"advanced" if isAdvanced}'>"
  if description
    html.push "<td class='vimiumReset'>#{bindings}</td>"

    html.push "<td class='vimiumReset'></td><td class='vimiumReset vimiumHelpDescription'>", description

    html.push("(<span class='vimiumReset commandName'>#{command}</span>)") if showCommandNames
  else
    html.push "<td class='vimiumReset' colspan='3' style='text-align: left;'>", bindings
  html.push("</td></tr>")





# Cache "content_scripts/vimium.css" in chrome.storage.local for UI components.
do ->
  req = new XMLHttpRequest()
  req.open "GET", chrome.runtime.getURL("content_scripts/vimium.css"), true # true -> asynchronous.
  req.onload = ->
    {status, responseText} = req
    chrome.storage.local.set vimiumCSSInChromeStorage: responseText if status == 200
  req.send()









TabOperations =

  # Opens the url in the current tab.

  openUrlInCurrentTab: (request) ->


    chrome.tabs.update request.tabId, url: Utils.convertToUrl request.url

  # Opens request.url in new tab and switches to it.

  openUrlInNewTab: (request, callback = (->)) ->
    tabConfig =
      url: Utils.convertToUrl request.url
      index: request.tab.index + 1

      selected: true
      windowId: request.tab.windowId




      openerTabId: request.tab.id


    chrome.tabs.create tabConfig, callback





#
# Selects the tab with the ID specified in request.id
#
selectSpecificTab = (request) ->
  chrome.tabs.get(request.id, (tab) ->
    chrome.windows.update(tab.windowId, { focused: true })
    chrome.tabs.update(request.id, { selected: true }))




moveTab = ({count, tab, registryEntry}) ->





  count = -count if registryEntry.command == "moveTabLeft"

  chrome.tabs.getAllInWindow null, (tabs) ->

    pinnedCount = (tabs.filter (tab) -> tab.pinned).length


    minIndex = if tab.pinned then 0 else pinnedCount

    maxIndex = (if tab.pinned then pinnedCount else tabs.length) - 1
    chrome.tabs.move tab.id,


      index: Math.max minIndex, Math.min maxIndex, tab.index + count

mkRepeatCommand = (command) -> (request) ->
  if 0 < request.count--



    command request, (request) -> (mkRepeatCommand command) request


# These are commands which are bound to keystrokes which must be handled by the background page. They are
# mapped in commands.coffee.
BackgroundCommands =
  createTab: mkRepeatCommand (request, callback) ->
    request.url ?= do ->
      url = Settings.get "newTabUrl"
      if url == "pages/blank.html"
        # "pages/blank.html" does not work in incognito mode, so fall back to "chrome://newtab" instead.
        if request.tab.incognito then "chrome://newtab" else chrome.runtime.getURL newTabUrl
      else
        url
    TabOperations.openUrlInNewTab request, (tab) -> callback extend request, {tab, tabId: tab.id}
  duplicateTab: mkRepeatCommand (request, callback) ->

    chrome.tabs.duplicate request.tabId, (tab) -> callback extend request, {tab, tabId: tab.id}

  moveTabToNewWindow: ({count, tab}) ->
    chrome.tabs.query {currentWindow: true}, (tabs) ->
      activeTabIndex = tab.index
      startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count
      [ tab, tabs... ] = tabs[startTabIndex...startTabIndex + count]
      chrome.windows.create {tabId: tab.id, incognito: tab.incognito}, (window) ->
        chrome.tabs.move (tab.id for tab in tabs), {windowId: window.id, index: -1}
  nextTab: (request) -> selectTab "next", request
  previousTab: (request) -> selectTab "previous", request
  firstTab: (request) -> selectTab "first", request
  lastTab: (request) -> selectTab "last", request
  removeTab: ({count, tab}) ->
    chrome.tabs.query {currentWindow: true}, (tabs) ->
      activeTabIndex = tab.index
      startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count
      chrome.tabs.remove (tab.id for tab in tabs[startTabIndex...startTabIndex + count])
  restoreTab: mkRepeatCommand (request, callback) -> chrome.sessions.restore null, callback request






  openCopiedUrlInCurrentTab: (request) -> TabOperations.openUrlInCurrentTab extend request, url: Clipboard.paste()
  openCopiedUrlInNewTab: (request) -> @createTab extend request, url: Clipboard.paste()


  togglePinTab: ({tab}) -> chrome.tabs.update tab.id, {pinned: !tab.pinned}
  moveTabLeft: moveTab
  moveTabRight: moveTab
  nextFrame: ({count, frameId, tabId}) ->
    frameIdsForTab[tabId] = cycleToFrame frameIdsForTab[tabId], frameId, count
    chrome.tabs.sendMessage tabId, name: "focusFrame", frameId: frameIdsForTab[tabId][0], highlight: true

  closeTabsOnLeft: (request) -> removeTabsRelative "before", request


  closeTabsOnRight: (request) -> removeTabsRelative "after", request
  closeOtherTabs: (request) -> removeTabsRelative "both", request
  visitPreviousTab: ({count, tab}) ->
    tabIds = BgUtils.tabRecency.getTabsByRecency().filter (tabId) -> tabId != tab.id

    if 0 < tabIds.length

      selectSpecificTab id: tabIds[(count-1) % tabIds.length]




# Remove tabs before, after, or either side of the currently active tab
removeTabsRelative = (direction, {tab: activeTab}) ->
  chrome.tabs.query {currentWindow: true}, (tabs) ->
    shouldDelete =
      switch direction
        when "before"
          (index) -> index < activeTab.index
        when "after"
          (index) -> index > activeTab.index
        when "both"
          (index) -> index != activeTab.index

    chrome.tabs.remove (tab.id for tab in tabs when not tab.pinned and shouldDelete tab.index)

# Selects a tab before or after the currently selected tab.
# - direction: "next", "previous", "first" or "last".
selectTab = (direction, {count, tab}) ->
  chrome.tabs.getAllInWindow null, (tabs) ->
    if 1 < tabs.length
      toSelect =
        switch direction
          when "next"
            (tab.index + count) % tabs.length
          when "previous"
            (tab.index - count + count * tabs.length) % tabs.length
          when "first"
            Math.min tabs.length - 1, count - 1
          when "last"
            Math.max 0, tabs.length - count

      chrome.tabs.update tabs[toSelect].id, selected: true

















































chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) ->
  return unless changeInfo.status == "loading" # Only do this once per URL change.

  cssConf =
    allFrames: true
    code: Settings.get("userDefinedLinkHintCss")
    runAt: "document_start"
  chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError


# Symbolic names for the three browser-action icons.
ENABLED_ICON = "icons/browser_action_enabled.png"
DISABLED_ICON = "icons/browser_action_disabled.png"
PARTIAL_ICON = "icons/browser_action_partial.png"



# Convert the three icon PNGs to image data.
iconImageData = {}
for icon in [ENABLED_ICON, DISABLED_ICON, PARTIAL_ICON]
  iconImageData[icon] = {}
  for scale in [19, 38]
    do (icon, scale) ->
      canvas = document.createElement "canvas"
      canvas.width = canvas.height = scale
      # We cannot do the rest of this in the tests.
      unless chrome.areRunningVimiumTests? and chrome.areRunningVimiumTests
        context = canvas.getContext "2d"
        image = new Image
        image.src = icon
        image.onload = ->
          context.drawImage image, 0, 0, scale, scale
          iconImageData[icon][scale] = context.getImageData 0, 0, scale, scale
          document.body.removeChild canvas
        document.body.appendChild canvas









Frames =
  onConnect: (sender, port) ->
    [tabId, frameId] = [sender.tab.id, sender.frameId]
    port.postMessage handler: "registerFrameId", chromeFrameId: frameId



    # Return our onMessage handler for this port.


    (request, port) =>




      this[request.handler] {request, tabId, frameId, port}






  registerFrame: ({tabId, frameId, port}) ->




    frameIdsForTab[tabId].push frameId unless frameId in frameIdsForTab[tabId] ?= []
    (portsForTab[tabId] ?= {})[frameId] = port


  unregisterFrame: ({tabId, frameId}) ->
    if tabId of frameIdsForTab
      frameIdsForTab[tabId] = (fId for fId in frameIdsForTab[tabId] when fId != frameId)
    if tabId of portsForTab

      delete portsForTab[tabId][frameId]

    HintCoordinator.unregisterFrame tabId, frameId



  isEnabledForUrl: ({request, tabId, port}) ->
    urlForTab[tabId] = request.url if request.frameIsFocused
    enabledState = Exclusions.isEnabledForUrl request.url




    if request.frameIsFocused
      chrome.browserAction.setIcon tabId: tabId, imageData: do ->
        enabledStateIcon =
          if not enabledState.isEnabledForUrl
            DISABLED_ICON

          else if 0 < enabledState.passKeys.length

            PARTIAL_ICON
          else
            ENABLED_ICON
        iconImageData[enabledStateIcon]



    port.postMessage extend request, enabledState




  domReady: ({tabId, frameId}) ->
    if frameId == 0

      tabLoadedHandlers[tabId]?()

      delete tabLoadedHandlers[tabId]






  linkHintsMessage: ({request, tabId, frameId}) ->

    HintCoordinator.onMessage tabId, frameId, request








handleFrameFocused = ({tabId, frameId}) ->




  frameIdsForTab[tabId] ?= []


  frameIdsForTab[tabId] = cycleToFrame frameIdsForTab[tabId], frameId


  # Inform all frames that a frame has received the focus.

  chrome.tabs.sendMessage tabId, name: "frameFocused", focusFrameId: frameId


# Rotate through frames to the frame count places after frameId.
cycleToFrame = (frames, frameId, count = 0) ->





  # We can't always track which frame chrome has focussed, but here we learn that it's frameId; so add an
  # additional offset such that we do indeed start from frameId.
  count = (count + Math.max 0, frames.indexOf frameId) % frames.length



  [frames[count..]..., frames[0...count]...]




HintCoordinator =
  tabState: {}



  onMessage: (tabId, frameId, request) ->
    if request.messageType of this
      this[request.messageType] tabId, frameId, request
    else
      # If there's no handler here, then the message is forwarded to all frames in the sender's tab.
      @sendMessage request.messageType, tabId, request


  # Post a link-hints message to a particular frame's port. We catch errors in case the frame has gone away.
  postMessage: (tabId, frameId, messageType, port, request = {}) ->
    try
      port.postMessage extend request, {handler: "linkHintsMessage", messageType}

    catch

      @unregisterFrame tabId, frameId

  # Post a link-hints message to all participating frames.

  sendMessage: (messageType, tabId, request = {}) ->

    for own frameId, port of @tabState[tabId].ports


      @postMessage tabId, parseInt(frameId), messageType, port, request




  prepareToActivateMode: (tabId, originatingFrameId, {modeIndex, isVimiumHelpDialog}) ->
    @tabState[tabId] = {frameIds: frameIdsForTab[tabId][..], hintDescriptors: {}, originatingFrameId, modeIndex}
    @tabState[tabId].ports = extend {}, portsForTab[tabId]
    @sendMessage "getHintDescriptors", tabId, {modeIndex, isVimiumHelpDialog}









  # Receive hint descriptors from all frames and activate link-hints mode when we have them all.

  postHintDescriptors: (tabId, frameId, {hintDescriptors}) ->
    if frameId in @tabState[tabId].frameIds
      @tabState[tabId].hintDescriptors[frameId] = hintDescriptors
      @tabState[tabId].frameIds = @tabState[tabId].frameIds.filter (fId) -> fId != frameId
      if @tabState[tabId].frameIds.length == 0
        for own frameId, port of @tabState[tabId].ports
          if frameId of @tabState[tabId].hintDescriptors
            hintDescriptors = extend {}, @tabState[tabId].hintDescriptors

            # We do not send back the frame's own hint descriptors.  This is faster (approx. speedup 3/2) for
            # link-busy sites like reddit.
            delete hintDescriptors[frameId]
            @postMessage tabId, parseInt(frameId), "activateMode", port,
              originatingFrameId: @tabState[tabId].originatingFrameId
              hintDescriptors: hintDescriptors
              modeIndex: @tabState[tabId].modeIndex

  # If an unregistering frame is participating in link-hints mode, then we need to tidy up after it.
  unregisterFrame: (tabId, frameId) ->
    delete @tabState[tabId]?.ports?[frameId]

    # We fake "postHintDescriptors" for an unregistering frame.
    @postHintDescriptors tabId, frameId, hintDescriptors: [] if @tabState[tabId]?.frameIds

# Port handler mapping
portHandlers =
  completions: handleCompletions
  frames: Frames.onConnect.bind Frames


sendRequestHandlers =
  runBackgroundCommand: (request) -> BackgroundCommands[request.registryEntry.command] request
  getHelpDialogHtml: getHelpDialogHtml
  # getCurrentTabUrl is used by the content scripts to get their full URL, because window.location cannot help
  # with Chrome-specific URLs like "view-source:http:..".
  getCurrentTabUrl: ({tab}) -> tab.url
  openUrlInNewTab: (request) -> TabOperations.openUrlInNewTab request
  openUrlInIncognito: (request) -> chrome.windows.create incognito: true, url: Utils.convertToUrl request.url
  openUrlInCurrentTab: TabOperations.openUrlInCurrentTab
  openOptionsPageInNewTab: (request) ->

    chrome.tabs.create url: chrome.runtime.getURL("pages/options.html"), index: request.tab.index + 1
  frameFocused: handleFrameFocused


  nextFrame: BackgroundCommands.nextFrame
  copyToClipboard: Clipboard.copy.bind Clipboard

  pasteFromClipboard: Clipboard.paste.bind Clipboard
  selectSpecificTab: selectSpecificTab

  createMark: Marks.create.bind(Marks)
  gotoMark: Marks.goto.bind(Marks)
  # Send a message to all frames in the current tab.
  sendMessageToFrames: (request, sender) -> chrome.tabs.sendMessage sender.tab.id, request.message
  # For debugging only. This allows content scripts to log messages to the extension's logging page.
  log: ({frameId, message}, sender) -> BgUtils.log "#{frameId} #{message}", sender

# We always remove chrome.storage.local/findModeRawQueryListIncognito on startup.
chrome.storage.local.remove "findModeRawQueryListIncognito"

# Tidy up tab caches when tabs are removed.  Also remove chrome.storage.local/findModeRawQueryListIncognito if
# there are no remaining incognito-mode windows.  Since the common case is that there are none to begin with,
# we first check whether the key is set at all.
chrome.tabs.onRemoved.addListener (tabId) ->
  delete cache[tabId] for cache in [frameIdsForTab, urlForTab, portsForTab, HintCoordinator.tabState]
  chrome.storage.local.get "findModeRawQueryListIncognito", (items) ->
    if items.findModeRawQueryListIncognito
      chrome.windows.getAll null, (windows) ->
        for window in windows
          return if window.incognito
        # There are no remaining incognito-mode tabs, and findModeRawQueryListIncognito is set.
        chrome.storage.local.remove "findModeRawQueryListIncognito"

# Convenience function for development use.
window.runTests = -> open(chrome.runtime.getURL('tests/dom_tests/dom_tests.html'))

#
# Begin initialization.
#




# Show notification on upgrade.


do showUpgradeMessage = ->
  # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new
  # installs.
  Settings.set "previousVersion", currentVersion  unless Settings.get "previousVersion"
  if Utils.compareVersions(currentVersion, Settings.get "previousVersion" ) == 1
    notificationId = "VimiumUpgradeNotification"
    notification =
      type: "basic"
      iconUrl: chrome.runtime.getURL "icons/vimium.png"
      title: "Vimium Upgrade"
      message: "Vimium has been upgraded to version #{currentVersion}. Click here for more information."
      isClickable: true
    if chrome.notifications?.create?
      chrome.notifications.create notificationId, notification, ->
        unless chrome.runtime.lastError
          Settings.set "previousVersion", currentVersion
          chrome.notifications.onClicked.addListener (id) ->
            if id == notificationId
              chrome.tabs.getSelected null, (tab) ->
                TabOperations.openUrlInNewTab {tab, tabId: tab.id, url: "https://github.com/philc/vimium#release-notes"}
    else
      # We need to wait for the user to accept the "notifications" permission.
      chrome.permissions.onAdded.addListener showUpgradeMessage

# The install date is shown on the logging page.
chrome.runtime.onInstalled.addListener ({reason}) ->


  unless reason in ["chrome_update", "shared_module_update"]
    chrome.storage.local.set installDate: new Date().toString()



extend root, {TabOperations, Frames}

Changes to vimium/content_scripts/vomnibar.coffee.

14
15
16
17
18
19
20







21
22
23
24







25
26
27
28
29
30
31
  # sourceFrameId here (and below) is the ID of the frame from which this request originates, which may be different
  # from the current frame.

  activate: (sourceFrameId, registryEntry) ->
    @parseRegistryEntry registryEntry, (options) =>
      @open sourceFrameId, extend options, completer:"omni"








  activateInNewTab: (sourceFrameId, registryEntry) ->
    @parseRegistryEntry registryEntry, (options) =>
      @open sourceFrameId, extend options, completer:"omni", newTab: true








  activateTabSelection: (sourceFrameId) -> @open sourceFrameId, {
    completer: "tabs"
    selectFirst: true
  }
  activateBookmarks: (sourceFrameId) -> @open sourceFrameId, {
    completer: "bookmarks"
    selectFirst: true







>
>
>
>
>
>
>




>
>
>
>
>
>
>







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  # sourceFrameId here (and below) is the ID of the frame from which this request originates, which may be different
  # from the current frame.

  activate: (sourceFrameId, registryEntry) ->
    @parseRegistryEntry registryEntry, (options) =>
      @open sourceFrameId, extend options, completer:"omni"

  activateInitial: ->
    cur = window.location.href
    ##initial = (if (Settings.get("homeUrl") == cur) then "" else cur)
    initial = (if ("file:///home/beyert/blank.htm" == cur) then "" else cur)
    ##@activateWithCompleter("omni", 100, Utils.getHome(cur))
    @activateWithCompleter("omni", 100, initial)

  activateInNewTab: (sourceFrameId, registryEntry) ->
    @parseRegistryEntry registryEntry, (options) =>
      @open sourceFrameId, extend options, completer:"omni", newTab: true

  activateInNewTabInitial: ->
    cur = window.location.href
    initial = (if ("file:///home/beyert/blank.htm" == cur) then "" else cur)
    #initial = (if (Settings.get("homeUrl") == cur) then "" else cur)
    ##@activateWithCompleter("omni", 100, Utils.getHome(cur), false, true)
    @activateWithCompleter("omni", 100, initial, false, true)

  activateTabSelection: (sourceFrameId) -> @open sourceFrameId, {
    completer: "tabs"
    selectFirst: true
  }
  activateBookmarks: (sourceFrameId) -> @open sourceFrameId, {
    completer: "bookmarks"
    selectFirst: true

Changes to vimium/content_scripts/vomnibar.coffee.orig.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

115
116
117
118
119
120
121
122
123
124
125
126
127
128
129


130
131
132
133
134
135
136
137
138
139
140
141
142

143
144
145

146
147
148
149
150
151


152
153
154
155
156
157
158
159
160
161
162

163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181

182
183
184

185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
Vomnibar =
  vomnibarUI: null # the dialog instance for this window
  completers: {}

  getCompleter: (name) ->
    if (!(name of @completers))
      @completers[name] = new BackgroundCompleter(name)
    @completers[name]

  #
  # Activate the Vomnibox.
  #
  activateWithCompleter: (completerName, refreshInterval, initialQueryValue, selectFirstResult, forceNewTab) ->
    completer = @getCompleter(completerName)
    @vomnibarUI = new VomnibarUI() unless @vomnibarUI
    completer.refresh()
    @vomnibarUI.setInitialSelectionValue(if selectFirstResult then 0 else -1)
    @vomnibarUI.setCompleter(completer)
    @vomnibarUI.setRefreshInterval(refreshInterval)
    @vomnibarUI.setForceNewTab(forceNewTab)
    @vomnibarUI.show()
    if (initialQueryValue)
      @vomnibarUI.setQuery(initialQueryValue)
      @vomnibarUI.update()

  activate: -> @activateWithCompleter("omni", 100)
  activateInNewTab: -> @activateWithCompleter("omni", 100, null, false, true)
  activateTabSelection: -> @activateWithCompleter("tabs", 0, null, true)
  activateBookmarks: -> @activateWithCompleter("bookmarks", 0, null, true)
  activateBookmarksInNewTab: -> @activateWithCompleter("bookmarks", 0, null, true, true)
  getUI: -> @vomnibarUI


class VomnibarUI
  constructor: ->
    @refreshInterval = 0
    @initDom()

  setQuery: (query) -> @input.value = query

  setInitialSelectionValue: (initialSelectionValue) ->
    @initialSelectionValue = initialSelectionValue

  setCompleter: (completer) ->
    @completer = completer
    @reset()

  setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval

  setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab

  show: ->
    @box.style.display = "block"
    @input.focus()
    @handlerId = handlerStack.push keydown: @onKeydown.bind @

  hide: ->
    @box.style.display = "none"
    @completionList.style.display = "none"
    @input.blur()
    handlerStack.remove @handlerId

  reset: ->
    @input.value = ""
    @updateTimer = null
    @completions = []
    @selection = @initialSelectionValue
    @update(true)

  updateSelection: ->
    for i in [0...@completionList.children.length]
      @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "")

  #
  # Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress.
  # We support the arrow keys and other shortcuts for moving, so this method hides that complexity.
  #
  actionFromKeyEvent: (event) ->
    key = KeyboardUtils.getKeyChar(event)
    if (KeyboardUtils.isEscape(event))
      return "dismiss"
    else if (key == "up" ||
        (event.shiftKey && event.keyCode == keyCodes.tab) ||
        (event.ctrlKey && (key == "k" || key == "p")))
      return "up"
    else if (key == "down" ||
        (event.keyCode == keyCodes.tab && !event.shiftKey) ||
        (event.ctrlKey && (key == "j" || key == "n")))
      return "down"
    else if (event.keyCode == keyCodes.enter)
      return "enter"

  onKeydown: (event) ->
    action = @actionFromKeyEvent(event)
    return true unless action # pass through


    openInNewTab = @forceNewTab ||
      (event.shiftKey || event.ctrlKey || KeyboardUtils.isPrimaryModifierKey(event))
    if (action == "dismiss")
      @hide()
    else if (action == "up")
      @selection -= 1
      @selection = @completions.length - 1 if @selection < @initialSelectionValue
      @updateSelection()
    else if (action == "down")
      @selection += 1
      @selection = @initialSelectionValue if @selection == @completions.length
      @updateSelection()
    else if (action == "enter")
      # If they type something and hit enter without selecting a completion from our list of suggestions,
      # try to open their query as a URL directly. If it doesn't look like a URL, we will search using
      # google.
      if (@selection == -1)
        query = @input.value.trim()

        # <Enter> on an empty vomnibar is a no-op.
        return unless 0 < query.length
        @hide()
        chrome.runtime.sendMessage({
          handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
          url: query })
      else
        @update true, =>
          # Shift+Enter will open the result in a new tab instead of the current tab.
          @completions[@selection].performAction(openInNewTab)
          @hide()

    # It seems like we have to manually suppress the event here and still return true.
    event.stopPropagation()
    event.preventDefault()


    true

  updateCompletions: (callback) ->
    query = @input.value.trim()

    @completer.filter query, (completions) =>
      @completions = completions
      @populateUiWithCompletions(completions)
      callback() if callback

  populateUiWithCompletions: (completions) ->
    # update completion list with the new data
    @completionList.innerHTML = completions.map((completion) -> "<li>#{completion.html}</li>").join("")

    @completionList.style.display = if completions.length > 0 then "block" else "none"
    @selection = Math.min(Math.max(@initialSelectionValue, @selection), @completions.length - 1)
    @updateSelection()


  update: (updateSynchronously, callback) ->
    if (updateSynchronously)
      # cancel scheduled update
      if (@updateTimer != null)
        window.clearTimeout(@updateTimer)


      @updateCompletions(callback)
    else if (@updateTimer != null)
      # an update is already scheduled, don't do anything
      return
    else
      # always update asynchronously for better user experience and to take some load off the CPU
      # (not every keystroke will cause a dedicated update)
      @updateTimer = setTimeout(=>
        @updateCompletions(callback)
        @updateTimer = null
      @refreshInterval)


  initDom: ->
    @box = Utils.createElementFromHtml(
      """
      <div id="vomnibar" class="vimiumReset">
        <div class="vimiumReset vomnibarSearchArea">
          <input type="text" class="vimiumReset">
        </div>
        <ul class="vimiumReset"></ul>
      </div>
      """)
    @box.style.display = "none"
    document.body.appendChild(@box)

    @input = document.querySelector("#vomnibar input")
    @input.addEventListener "input", => @update()
    @completionList = document.querySelector("#vomnibar ul")
    @completionList.style.display = "none"


#
# Sends filter and refresh requests to a Vomnibox completer on the background page.
#

class BackgroundCompleter
  # - name: The background page completer that you want to interface with. Either "omni", "tabs", or
  # "bookmarks". */
  constructor: (@name) ->
    @filterPort = chrome.runtime.connect({ name: "filterCompleter" })

  refresh: -> chrome.runtime.sendMessage({ handler: "refreshCompleter", name: @name })

  filter: (query, callback) ->
    id = Utils.createUniqueId()
    @filterPort.onMessage.addListener (msg) ->
      return if (msg.id != id)
      # 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].
      results = msg.results.map (result) ->
        functionToCall = if (result.type == "tab")
          BackgroundCompleter.completionActions.switchToTab.curry(result.tabId)
        else
          BackgroundCompleter.completionActions.navigateToUrl.curry(result.url)
        result.performAction = functionToCall
        result
      callback(results)

    @filterPort.postMessage({ id: id, name: @name, query: query })

extend BackgroundCompleter,
  #
  # These are the actions we can perform when the user selects a result in the Vomnibox.
  #
  completionActions:
    navigateToUrl: (url, openInNewTab) ->
      # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab.
      if url.startsWith "javascript:"
        script = document.createElement 'script'
        script.textContent = decodeURIComponent(url["javascript:".length..])
        (document.head || document.documentElement).appendChild script
      else
        chrome.runtime.sendMessage(
          handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab"
          url: url,
          selected: openInNewTab)

    switchToTab: (tabId) -> chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: tabId })

root = exports ? window
root.Vomnibar = Vomnibar
<
<
<
|
<
<
<
<
|
|
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<

<
|
<
<
|
<
<
<
|
<
|
<
|
<
<
<
<
|
<
<
<
<
<

<
<
<
<
<
<
|
<
<
<
|
|
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
>

|
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
>
<
<
<
<
<
<
<
<
<
<
<

<
<
<
>
>
|
|
<
<
|
|
|
<
<
|
<
<
<
>
|
<
|
>
|
|
|
|
<
|
>
>
|
<
<
<
|
<
<
<
|
<
<
>
|
<
<
<
<
<
<
<
<
<
<
<
<

<
<
<
<
|
>
|
<
<
>
|
<
<
<
<
|
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
|
<
|
<
<
|
<
<
<
<
<
<
<
<
|
<
<
<
<
|
<






1




2
3










4












5



6

7


8



9

10

11




12





13






14



15
16



17













18



19
20
21






22










23











24



25
26
27
28


29
30
31


32



33
34

35
36
37
38
39
40

41
42
43
44



45



46


47
48












49




50
51
52


53
54




55

56













57

58

59


60








61




62

63
64
65



#




# This wraps the vomnibar iframe, which we inject into the page to provide the vomnibar.
#










Vomnibar =












  vomnibarUI: null





  # Parse any additional options from the command's registry entry.  Currently, this only includes a flag of


  # the form "keyword=X", for direct activation of a custom search engine.



  parseRegistryEntry: (registryEntry = { options: [] }, callback = null) ->

    searchEngines = Settings.get("searchEngines") ? ""

    SearchEngines.refreshAndUse searchEngines, (engines) ->




      callback? registryEntry.options












  # sourceFrameId here (and below) is the ID of the frame from which this request originates, which may be different



  # from the current frame.




  activate: (sourceFrameId, registryEntry) ->













    @parseRegistryEntry registryEntry, (options) =>



      @open sourceFrameId, extend options, completer:"omni"

  activateInNewTab: (sourceFrameId, registryEntry) ->






    @parseRegistryEntry registryEntry, (options) =>










      @open sourceFrameId, extend options, completer:"omni", newTab: true















  activateTabSelection: (sourceFrameId) -> @open sourceFrameId, {
    completer: "tabs"
    selectFirst: true
  }


  activateBookmarks: (sourceFrameId) -> @open sourceFrameId, {
    completer: "bookmarks"
    selectFirst: true


  }



  activateBookmarksInNewTab: (sourceFrameId) -> @open sourceFrameId, {
    completer: "bookmarks"

    selectFirst: true
    newTab: true
  }
  activateEditUrl: (sourceFrameId) -> @open sourceFrameId, {
    completer: "omni"
    selectFirst: false

    query: window.location.href
  }
  activateEditUrlInNewTab: (sourceFrameId) -> @open sourceFrameId, {
    completer: "omni"



    selectFirst: false



    query: window.location.href


    newTab: true
  }

















  init: ->
    @vomnibarUI ?= new UIComponent "pages/vomnibar.html", "vomnibarFrame", ->



  # This function opens the vomnibar. It accepts options, a map with the values:
  #   completer   - The completer to fetch results from.




  #   query       - Optional. Text to prefill the Vomnibar with.

  #   selectFirst - Optional, boolean. Whether to select the first entry.













  #   newTab      - Optional, boolean. Whether to open the result in a new tab.

  open: (sourceFrameId, options) ->

    @init()


    # The Vomnibar cannot coexist with the help dialog (it causes focus issues).








    HelpDialog.abort()




    @vomnibarUI.activate extend options, { name: "activate", sourceFrameId, focus: true }


root = exports ? window
root.Vomnibar = Vomnibar

Changes to vimium/lib/utils.coffee.

101
102
103
104
105
106
107





108
109
110
111
112
113
114
      return true if 2 <= lastPart.length <= 3 or lastPart in longTlds

    # Allow IPv4 addresses
    return true if /^(\d{1,3}\.){3}\d{1,3}$/.test hostName

    # Fallback: no URL
    return false






  # Map a search query to its URL encoded form. The query may be either a string or an array of strings.
  # E.g. "BBC Sport" -> "BBC+Sport".
  createSearchQuery: (query) ->
    query = query.split(/\s+/) if typeof(query) == "string"
    query.map(encodeURIComponent).join "+"








>
>
>
>
>







101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
      return true if 2 <= lastPart.length <= 3 or lastPart in longTlds

    # Allow IPv4 addresses
    return true if /^(\d{1,3}\.){3}\d{1,3}$/.test hostName

    # Fallback: no URL
    return false

  getHome: (cur) ->
   if Settings.get("homeUrl") == cur
     ""
   else cur

  # Map a search query to its URL encoded form. The query may be either a string or an array of strings.
  # E.g. "BBC Sport" -> "BBC+Sport".
  createSearchQuery: (query) ->
    query = query.split(/\s+/) if typeof(query) == "string"
    query.map(encodeURIComponent).join "+"

Changes to vimium/lib/utils.coffee.orig.

1
2
3
4







5
6
7
8
9
10
11
12
13
14
15




16
17
18
19
20
21
22
23
24
25
26
27
28
29

30
31
32
33


















34
35
36
37
38
39
40
41
42
43
44
45
46
47
48



49
50
51
52
53
54
55
Utils =
  getCurrentVersion: ->
    chrome.runtime.getManifest().version








  # Takes a dot-notation object string and call the function
  # that it points to with the correct value for 'this'.
  invokeCommandString: (str, argArray) ->
    components = str.split('.')
    obj = window
    for component in components[0...-1]
      obj = obj[component]
    func = obj[components.pop()]
    func.apply(obj, argArray)

  # Creates a single DOM element from :html




  createElementFromHtml: (html) ->
    tmp = document.createElement("div")
    tmp.innerHTML = html
    tmp.firstChild

  escapeHtml: (string) -> string.replace(/</g, "&lt;").replace(/>/g, "&gt;")

  # Generates a unique ID
  createUniqueId: do ->
    id = 0
    -> id += 1

  hasChromePrefix: (url) ->
    chromePrefixes = [ 'about', 'view-source', "chrome-extension" ]

    for prefix in chromePrefixes
      return true if url.startsWith prefix
    false



















  # Completes a partial URL (without scheme)
  createFullUrl: (partialUrl) ->
    unless /^[a-z]{3,}:\/\//.test partialUrl
      "http://" + partialUrl
    else
      partialUrl

  # Tries to detect if :str is a valid URL.
  isUrl: (str) ->
    # Starts with a scheme: URL
    return true if /^[a-z]{3,}:\/\//.test str

    # Must not contain spaces
    return false if ' ' in str




    # More or less RFC compliant URL host part parsing. This should be sufficient for our needs
    urlRegex = new RegExp(
      '^(?:([^:]+)(?::([^:]+))?@)?' + # user:password (optional) => \1, \2
      '([^:]+|\\[[^\\]]+\\])'       + # host name (IPv6 addresses in square brackets allowed) => \3
      '(?::(\\d+))?$'                 # port number (optional) => \4
      )





>
>
>
>
>
>
>
|
|
|
|

<
|
<
|

<
>
>
>
>
|
<
<
<








|
|
>
|
|
|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


<
|
<
<



<
<
<



>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

17

18
19

20
21
22
23
24



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

60


61
62
63



64
65
66
67
68
69
70
71
72
73
74
75
76
Utils =
  getCurrentVersion: ->
    chrome.runtime.getManifest().version

  # Returns true whenever the current page (or the page supplied as an argument) is from the extension's
  # origin (and thus can access the extension's localStorage).
  isExtensionPage: (win = window) -> try win.document.location?.origin + "/" == chrome.extension.getURL ""

  # Returns true whenever the current page is the extension's background page.
  isBackgroundPage: -> @isExtensionPage() and chrome.extension.getBackgroundPage?() == window

  # Takes a dot-notation object string and calls the function that it points to with the correct value for
  # 'this'.
  invokeCommandString: (str, args...) ->
    [names..., name] = str.split '.'
    obj = window

    obj = obj[component] for component in names

    obj[name].apply obj, args


  # Escape all special characters, so RegExp will parse the string 'as is'.
  # Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
  escapeRegexSpecialCharacters: do ->
    escapeRegex = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
    (str) -> str.replace escapeRegex, "\\$&"




  escapeHtml: (string) -> string.replace(/</g, "&lt;").replace(/>/g, "&gt;")

  # Generates a unique ID
  createUniqueId: do ->
    id = 0
    -> id += 1

  hasChromePrefix: do ->
    chromePrefixes = [ "about:", "view-source:", "extension:", "chrome-extension:", "data:" ]
    (url) ->
      for prefix in chromePrefixes
        return true if url.startsWith prefix
      false

  hasJavascriptPrefix: (url) ->
    url.startsWith "javascript:"

  hasFullUrlPrefix: do ->
    urlPrefix = new RegExp "^[a-z]{3,}://."
    (url) -> urlPrefix.test url

  # Decode valid escape sequences in a URI.  This is intended to mimic the best-effort decoding
  # Chrome itself seems to apply when a Javascript URI is enetered into the omnibox (or clicked).
  # See https://code.google.com/p/chromium/issues/detail?id=483000, #1611 and #1636.
  decodeURIByParts: (uri) ->
    uri.split(/(?=%)/).map((uriComponent) ->
      try
        decodeURIComponent uriComponent
      catch
        uriComponent
    ).join ""

  # Completes a partial URL (without scheme)
  createFullUrl: (partialUrl) ->

    if @hasFullUrlPrefix(partialUrl) then partialUrl else ("http://" + partialUrl)



  # Tries to detect if :str is a valid URL.
  isUrl: (str) ->



    # Must not contain spaces
    return false if ' ' in str

    # Starts with a scheme: URL
    return true if @hasFullUrlPrefix str

    # More or less RFC compliant URL host part parsing. This should be sufficient for our needs
    urlRegex = new RegExp(
      '^(?:([^:]+)(?::([^:]+))?@)?' + # user:password (optional) => \1, \2
      '([^:]+|\\[[^\\]]+\\])'       + # host name (IPv6 addresses in square brackets allowed) => \3
      '(?::(\\d+))?$'                 # port number (optional) => \4
      )

81
82
83
84
85
86
87
88

89




90




91





92













93
94
95
96
97
98
99
100
101



102
103
104
105
106
107
108
109




110
111
112
113
114
115
116
117
118
119
120
121
122
123
124





125
126
127
128
129
130
131
132
133
134

















































135
136
137
138
139
140
141
142
143
144
145



146
147
148
149
150
151
152












































































153
154





    # Allow IPv4 addresses
    return true if /^(\d{1,3}\.){3}\d{1,3}$/.test hostName

    # Fallback: no URL
    return false

  # Creates a search URL from the given :query.

  createSearchUrl: (query) ->




    # it would be better to pull the default search engine from chrome itself,




    # but it is not clear if/how that is possible





    Settings.get("searchUrl") + encodeURIComponent(query)














  # Converts :string into a Google search if it's not already a URL. We don't bother with escaping characters
  # as Chrome will do that for us.
  convertToUrl: (string) ->
    string = string.trim()

    # Special-case about:[url] and view-source:[url]
    if Utils.hasChromePrefix string
      string



    else if Utils.isUrl string
      Utils.createFullUrl string
    else
      Utils.createSearchUrl string

  # detects both literals and dynamically created strings
  isString: (obj) -> typeof obj == 'string' or obj instanceof String






  # Compares two version strings (e.g. "1.1" and "1.5") and returns
  # -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB.
  compareVersions: (versionA, versionB) ->
    versionA = versionA.split(".")
    versionB = versionB.split(".")
    for i in [0...(Math.max(versionA.length, versionB.length))]
      a = parseInt(versionA[i] || 0, 10)
      b = parseInt(versionB[i] || 0, 10)
      if (a < b)
        return -1
      else if (a > b)
        return 1
    0






  # Zip two (or more) arrays:
  #   - Utils.zip([ [a,b], [1,2] ]) returns [ [a,1], [b,2] ]
  #   - Length of result is `arrays[0].length`.
  #   - Adapted from: http://stackoverflow.com/questions/4856717/javascript-equivalent-of-pythons-zip-function
  zip: (arrays) ->
    arrays[0].map (_,i) ->
      arrays.map( (array) -> array[i] )

  # locale-sensitive uppercase detection
  hasUpperCase: (s) -> s.toLowerCase() != s


















































# This creates a new function out of an existing function, where the new function takes fewer arguments. This
# allows us to pass around functions instead of functions + a partial list of arguments.
Function::curry = ->
  fixedArguments = Array.copy(arguments)
  fn = this
  -> fn.apply(this, fixedArguments.concat(Array.copy(arguments)))

Array.copy = (array) -> Array.prototype.slice.call(array, 0)

String::startsWith = (str) -> @indexOf(str) == 0




globalRoot = window ? global
globalRoot.extend = (hash1, hash2) ->
  for key of hash2
    hash1[key] = hash2[key]
  hash1













































































root = exports ? window
root.Utils = Utils











|
>
|
>
>
>
>
|
>
>
>
>
|
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>






|


>
>
>








>
>
>
>















>
>
>
>
>










>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>











>
>
>



|



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


>
>
>
>
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346

    # Allow IPv4 addresses
    return true if /^(\d{1,3}\.){3}\d{1,3}$/.test hostName

    # Fallback: no URL
    return false

  # Map a search query to its URL encoded form. The query may be either a string or an array of strings.
  # E.g. "BBC Sport" -> "BBC+Sport".
  createSearchQuery: (query) ->
    query = query.split(/\s+/) if typeof(query) == "string"
    query.map(encodeURIComponent).join "+"

  # Create a search URL from the given :query (using either the provided search URL, or the default one).
  # It would be better to pull the default search engine from chrome itself.  However, chrome does not provide
  # an API for doing so.
  createSearchUrl: (query, searchUrl = Settings.get("searchUrl")) ->
    searchUrl += "%s" unless 0 <= searchUrl.indexOf "%s"
    searchUrl.replace /%s/g, @createSearchQuery query

  # Extract a query from url if it appears to be a URL created from the given search URL.
  # For example, map "https://www.google.ie/search?q=star+wars&foo&bar" to "star wars".
  extractQuery: do =>
    queryTerminator = new RegExp "[?&#/]"
    httpProtocolRegexp = new RegExp "^https?://"
    (searchUrl, url) ->
      url = url.replace httpProtocolRegexp
      searchUrl = searchUrl.replace httpProtocolRegexp
      [ searchUrl, suffixTerms... ] = searchUrl.split "%s"
      # We require the URL to start with the search URL.
      return null unless url.startsWith searchUrl
      # We require any remaining terms in the search URL to also be present in the URL.
      for suffix in suffixTerms
        return null unless 0 <= url.indexOf suffix
      # We use try/catch because decodeURIComponent can throw an exception.
      try
          url[searchUrl.length..].split(queryTerminator)[0].split("+").map(decodeURIComponent).join " "
      catch
        null

  # Converts :string into a Google search if it's not already a URL. We don't bother with escaping characters
  # as Chrome will do that for us.
  convertToUrl: (string) ->
    string = string.trim()

    # Special-case about:[url], view-source:[url] and the like
    if Utils.hasChromePrefix string
      string
    else if Utils.hasJavascriptPrefix string
      # In Chrome versions older than 46.0.2467.2, encoded javascript URIs weren't handled correctly.
      if Utils.haveChromeVersion "46.0.2467.2" then string else Utils.decodeURIByParts string
    else if Utils.isUrl string
      Utils.createFullUrl string
    else
      Utils.createSearchUrl string

  # detects both literals and dynamically created strings
  isString: (obj) -> typeof obj == 'string' or obj instanceof String

  # Transform "zjkjkabz" into "abjkz".
  distinctCharacters: (str) ->
    chars = str.split("").sort()
    (ch for ch, index in chars when index == 0 or ch != chars[index-1]).join ""

  # Compares two version strings (e.g. "1.1" and "1.5") and returns
  # -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB.
  compareVersions: (versionA, versionB) ->
    versionA = versionA.split(".")
    versionB = versionB.split(".")
    for i in [0...(Math.max(versionA.length, versionB.length))]
      a = parseInt(versionA[i] || 0, 10)
      b = parseInt(versionB[i] || 0, 10)
      if (a < b)
        return -1
      else if (a > b)
        return 1
    0

  # True if the current Chrome version is at least the required version.
  haveChromeVersion: (required) ->
    chromeVersion = navigator.appVersion.match(/Chrom(e|ium)\/(.*?) /)?[2]
    chromeVersion and 0 <= Utils.compareVersions chromeVersion, required

  # Zip two (or more) arrays:
  #   - Utils.zip([ [a,b], [1,2] ]) returns [ [a,1], [b,2] ]
  #   - Length of result is `arrays[0].length`.
  #   - Adapted from: http://stackoverflow.com/questions/4856717/javascript-equivalent-of-pythons-zip-function
  zip: (arrays) ->
    arrays[0].map (_,i) ->
      arrays.map( (array) -> array[i] )

  # locale-sensitive uppercase detection
  hasUpperCase: (s) -> s.toLowerCase() != s

  # Does string match any of these regexps?
  matchesAnyRegexp: (regexps, string) ->
    for re in regexps
      return true if re.test string
    false

  # Convenience wrapper for setTimeout (with the arguments around the other way).
  setTimeout: (ms, func) -> setTimeout func, ms

  # Like Nodejs's nextTick.
  nextTick: (func) -> @setTimeout 0, func

  # Make an idempotent function.
  makeIdempotent: (func) ->
    (args...) -> ([previousFunc, func] = [func, null])[0]? args...

# Utility for parsing and using the custom search-engine configuration.  We re-use the previous parse if the
# search-engine configuration is unchanged.
SearchEngines =
  previousSearchEngines: null
  searchEngines: null

  refresh: (searchEngines) ->
    unless @previousSearchEngines? and searchEngines == @previousSearchEngines
      @previousSearchEngines = searchEngines
      @searchEngines = new AsyncDataFetcher (callback) ->
        engines = {}
        for line in searchEngines.split "\n"
          line = line.trim()
          continue if /^[#"]/.test line
          tokens = line.split /\s+/
          continue unless 2 <= tokens.length
          keyword = tokens[0].split(":")[0]
          searchUrl = tokens[1]
          description = tokens[2..].join(" ") || "search (#{keyword})"
          continue unless Utils.hasFullUrlPrefix searchUrl
          engines[keyword] = { keyword, searchUrl, description }

        callback engines

  # Use the parsed search-engine configuration, possibly asynchronously.
  use: (callback) ->
    @searchEngines.use callback

  # Both set (refresh) the search-engine configuration and use it at the same time.
  refreshAndUse: (searchEngines, callback) ->
    @refresh searchEngines
    @use callback

# This creates a new function out of an existing function, where the new function takes fewer arguments. This
# allows us to pass around functions instead of functions + a partial list of arguments.
Function::curry = ->
  fixedArguments = Array.copy(arguments)
  fn = this
  -> fn.apply(this, fixedArguments.concat(Array.copy(arguments)))

Array.copy = (array) -> Array.prototype.slice.call(array, 0)

String::startsWith = (str) -> @indexOf(str) == 0
String::ltrim = -> @replace /^\s+/, ""
String::rtrim = -> @replace /\s+$/, ""
String::reverse = -> @split("").reverse().join ""

globalRoot = window ? global
globalRoot.extend = (hash1, hash2) ->
  for own key of hash2
    hash1[key] = hash2[key]
  hash1

# A simple cache. Entries used within two expiry periods are retained, otherwise they are discarded.
# At most 2 * @entries entries are retained.
class SimpleCache
  # expiry: expiry time in milliseconds (default, one hour)
  # entries: maximum number of entries in @cache (there may be up to this many entries in @previous, too)
  constructor: (@expiry = 60 * 60 * 1000, @entries = 1000) ->
    @cache = {}
    @previous = {}
    @lastRotation = new Date()

  has: (key) ->
    @rotate()
    (key of @cache) or key of @previous

  # Set value, and return that value.  If value is null, then delete key.
  set: (key, value = null) ->
    @rotate()
    delete @previous[key]
    if value?
      @cache[key] = value
    else
      delete @cache[key]
      null

  get: (key) ->
    @rotate()
    if key of @cache
      @cache[key]
    else if key of @previous
      @cache[key] = @previous[key]
      delete @previous[key]
      @cache[key]
    else
      null

  rotate: (force = false) ->
    Utils.nextTick =>
      if force or @entries < Object.keys(@cache).length or @expiry < new Date() - @lastRotation
        @lastRotation = new Date()
        @previous = @cache
        @cache = {}

  clear: ->
    @rotate true
    @rotate true

# This is a simple class for the common case where we want to use some data value which may be immediately
# available, or for which we may have to wait.  It implements a use-immediately-or-wait queue, and calls the
# fetch function to fetch the data asynchronously.
class AsyncDataFetcher
  constructor: (fetch) ->
    @data = null
    @queue = []
    Utils.nextTick =>
      fetch (@data) =>
        callback @data for callback in @queue
        @queue = null

  use: (callback) ->
    if @data? then callback @data else @queue.push callback

# This takes a list of jobs (functions) and runs them, asynchronously.  Functions queued with @onReady() are
# run once all of the jobs have completed.
class JobRunner
  constructor: (@jobs) ->
    @fetcher = new AsyncDataFetcher (callback) =>
      for job in @jobs
        do (job) =>
          Utils.nextTick =>
            job =>
              @jobs = @jobs.filter (j) -> j != job
              callback true if @jobs.length == 0

  onReady: (callback) ->
    @fetcher.use callback

root = exports ? window
root.Utils = Utils
root.SearchEngines = SearchEngines
root.SimpleCache = SimpleCache
root.AsyncDataFetcher = AsyncDataFetcher
root.JobRunner = JobRunner

Changes to vimium/pages/options.coffee.

194
195
196
197
198
199
200

201
202
203
204
205
206
207
  previousPatterns: NonEmptyTextOption
  regexFindMode: CheckBoxOption
  scrollStepSize: NumberOption
  smoothScroll: CheckBoxOption
  grabBackFocus: CheckBoxOption
  searchEngines: TextOption
  searchUrl: NonEmptyTextOption

  userDefinedLinkHintCss: TextOption

initOptionsPage = ->
  onUpdated = ->
    $("saveOptions").removeAttribute "disabled"
    $("saveOptions").innerHTML = "Save Changes"








>







194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
  previousPatterns: NonEmptyTextOption
  regexFindMode: CheckBoxOption
  scrollStepSize: NumberOption
  smoothScroll: CheckBoxOption
  grabBackFocus: CheckBoxOption
  searchEngines: TextOption
  searchUrl: NonEmptyTextOption
  homeUrl: NonEmptyTextOption
  userDefinedLinkHintCss: TextOption

initOptionsPage = ->
  onUpdated = ->
    $("saveOptions").removeAttribute "disabled"
    $("saveOptions").innerHTML = "Save Changes"

Changes to vimium/pages/options.coffee.orig.


1

2




3
4
5

6



7


8





9





10



11

12
13
14

15



16



17
18



19



20
21
22


23


24



25
26



27



28



29

30










31



32




33





34




35


36






37




38
39
40
41
42

43
44







45
46

47


48
49



50




51
52







53

54

55
56
57
58
59
60
61
62
63


64

65


















66

















67
68
69
70

71


72



73





74
75
76




77


78






79



80





81
82

83

84
85
86
87



88



89

90




91

92






93
94



95

96




97


98


99
100
101




102


103

104
105
106


107
108
109

$ = (id) -> document.getElementById id






bgSettings = chrome.extension.getBackgroundPage().Settings

editableFields = [ "scrollStepSize", "excludedUrls", "linkHintCharacters", "linkHintNumbers",

  "userDefinedLinkHintCss", "keyMappings", "filterLinkHints", "previousPatterns",



  "nextPatterns", "hideHud", "regexFindMode", "searchUrl"]








canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"]









postSaveHooks = keyMappings: (value) ->

  commands = chrome.extension.getBackgroundPage().Commands
  commands.clearKeyMappingsAndSetDefaults()
  commands.parseCustomKeyMappings value

  chrome.extension.getBackgroundPage().refreshCompletionKeysAfterMappingSave()







document.addEventListener "DOMContentLoaded", ->
  populateOptions()







  for field in editableFields
    $(field).addEventListener "keyup", onOptionKeyup, false
    $(field).addEventListener "change", enableSaveButton, false


    $(field).addEventListener "change", onDataLoaded, false






  $("advancedOptionsLink").addEventListener "click", toggleAdvancedOptions, false
  $("showCommands").addEventListener "click", (->



    showHelpDialog chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId



  ), false



  document.getElementById("restoreSettings").addEventListener "click", restoreToDefaults

  document.getElementById("saveOptions").addEventListener "click", saveOptions














window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled










onOptionKeyup = (event) ->




  if (event.target.getAttribute("type") isnt "checkbox" and


      event.target.getAttribute("savedValue") isnt event.target.value)






    enableSaveButton()





onDataLoaded = ->
  hide = (el) -> el.parentNode.parentNode.style.display = "none"
  show = (el) -> el.parentNode.parentNode.style.display = "table-row"
  if $("filterLinkHints").checked

    hide $("linkHintCharacters")
    show $("linkHintNumbers")







  else
    show $("linkHintCharacters")

    hide $("linkHintNumbers")



enableSaveButton = ->



  $("saveOptions").removeAttribute "disabled"





# Saves options to localStorage.







saveOptions = ->



  # If the value is unchanged from the default, delete the preference from localStorage; this gives us
  # the freedom to change the defaults in the future.
  for fieldName in editableFields
    field = $(fieldName)
    switch field.getAttribute("type")
      when "checkbox"
        fieldValue = field.checked
      when "number"
        fieldValue = parseFloat field.value


      else

        fieldValue = field.value.trim()


















        field.value = fieldValue


















    # If it's empty and not a field that we allow to be empty, restore to the default value
    if not fieldValue and canBeEmptyFields.indexOf(fieldName) is -1
      bgSettings.clear fieldName

      fieldValue = bgSettings.get(fieldName)


    else



      bgSettings.set fieldName, fieldValue





    $(fieldName).value = fieldValue
    $(fieldName).setAttribute "savedValue", fieldValue
    postSaveHooks[fieldName] fieldValue if postSaveHooks[fieldName]







  $("saveOptions").disabled = true










# Restores select box state to saved value from localStorage.





populateOptions = ->
  for field in editableFields

    val = bgSettings.get(field) or ""

    setFieldValue $(field), val
  onDataLoaded()

restoreToDefaults = ->



  for field in editableFields



    val = bgSettings.defaults[field] or ""

    setFieldValue $(field), val




  onDataLoaded()

  enableSaveButton()







setFieldValue = (field, value) ->



  unless field.getAttribute("type") is "checkbox"

    field.value = value




    field.setAttribute "savedValue", value


  else


    field.checked = value

toggleAdvancedOptions = do (advancedMode=false) -> (event) ->




  if advancedMode


    $("advancedOptions").style.display = "none"

    $("advancedOptionsLink").innerHTML = "Show advanced options&hellip;"
  else
    $("advancedOptions").style.display = "table-row-group"


    $("advancedOptionsLink").innerHTML = "Hide advanced options"
  advancedMode = !advancedMode
  event.preventDefault()
>

>

>
>
>
>


<
>
|
>
>
>
|
>
>

>
>
>
>
>
|
>
>
>
>
>

>
>
>
|
>
|
|
|
>
|
>
>
>

>
>
>
|
<
>
>
>

>
>
>
|
|
<
>
>
|
>
>

>
>
>
|
|
>
>
>
|
>
>
>
|
>
>
>
|
>
|
>
>
>
>
>
>
>
>
>
>

>
>
>
|
>
>
>
>

>
>
>
>
>
|
>
>
>
>
|
>
>
|
>
>
>
>
>
>
|
>
>
>
>

|
|
|
|
>
|
|
>
>
>
>
>
>
>
|
|
>
|
>
>

|
>
>
>
|
>
>
>
>

<
>
>
>
>
>
>
>
|
>

>
|
|
<
<
|
|
|
|
|
>
>
|
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

<
<
<
>
|
>
>

>
>
>
|
>
>
>
>
>
|
<
<
>
>
>
>

>
>
|
>
>
>
>
>
>

>
>
>
|
>
>
>
>
>
|
|
>
|
>
|
|

|
>
>
>
|
>
>
>
|
>
|
>
>
>
>
|
>
|
>
>
>
>
>
>

|
>
>
>
|
>
|
>
>
>
>
|
>
>
|
>
>
|
|
|
>
>
>
>
|
>
>
|
>
|
|
|
>
>
|
<
<
1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57
58

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160

161
162
163
164
165
166
167
168
169
170
171
172
173


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235


236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327



$ = (id) -> document.getElementById id
bgExclusions = chrome.extension.getBackgroundPage().Exclusions

# We have to use Settings from the background page here (not Settings, directly) to avoid a race condition for
# the page popup.  Specifically, we must ensure that the settings have been updated on the background page
# *before* the popup closes.  This ensures that any exclusion-rule changes are in place before the page
# regains the focus.
bgSettings = chrome.extension.getBackgroundPage().Settings


#
# Class hierarchy for various types of option.
class Option
  # Base class for all option classes.
  # Abstract. Option does not define @populateElement or @readValueFromElement.

  # Static. Array of all options.
  @all = []

  constructor: (@field,@onUpdated) ->
    @element = $(@field)
    @element.addEventListener "change", @onUpdated
    @fetch()
    Option.all.push this

  # Fetch a setting from localStorage, remember the @previous value and populate the DOM element.
  # Return the fetched value.
  fetch: ->
    @populateElement @previous = bgSettings.get @field
    @previous

  # Write this option's new value back to localStorage, if necessary.
  save: ->
    value = @readValueFromElement()
    if JSON.stringify(value) != JSON.stringify @previous
      bgSettings.set @field, @previous = value

  restoreToDefault: ->
    bgSettings.clear @field
    @fetch()

  # Static method.
  @saveOptions: ->
    Option.all.map (option) -> option.save()

  # Abstract method; only implemented in sub-classes.
  # Populate the option's DOM element (@element) with the setting's current value.
  # populateElement: (value) -> DO_SOMETHING


  # Abstract method; only implemented in sub-classes.
  # Extract the setting's new value from the option's DOM element (@element).
  # readValueFromElement: -> RETURN_SOMETHING

class NumberOption extends Option
  populateElement: (value) -> @element.value = value
  readValueFromElement: -> parseFloat @element.value

class TextOption extends Option

  constructor: (args...) ->
    super(args...)
    @element.addEventListener "input", @onUpdated
  populateElement: (value) -> @element.value = value
  readValueFromElement: -> @element.value.trim()

class NonEmptyTextOption extends Option
  constructor: (args...) ->
    super(args...)
    @element.addEventListener "input", @onUpdated

  populateElement: (value) -> @element.value = value
  # If the new value is not empty, then return it. Otherwise, restore the default value.
  readValueFromElement: -> if value = @element.value.trim() then value else @restoreToDefault()

class CheckBoxOption extends Option
  populateElement: (value) -> @element.checked = value
  readValueFromElement: -> @element.checked

class ExclusionRulesOption extends Option
  constructor: (args...) ->
    super(args...)
    $("exclusionAddButton").addEventListener "click", (event) =>
      @addRule()

  # Add a new rule, focus its pattern, scroll it into view, and return the newly-added element.  On the
  # options page, there is no current URL, so there is no initial pattern.  This is the default.  On the popup
  # page (see ExclusionRulesOnPopupOption), the pattern is pre-populated based on the current tab's URL.
  addRule: (pattern="") ->
      element = @appendRule { pattern: pattern, passKeys: "" }
      @getPattern(element).focus()
      exclusionScrollBox = $("exclusionScrollBox")
      exclusionScrollBox.scrollTop = exclusionScrollBox.scrollHeight
      @onUpdated()
      element

  populateElement: (rules) ->
    for rule in rules
      @appendRule rule

  # Append a row for a new rule.  Return the newly-added element.
  appendRule: (rule) ->
    content = document.querySelector('#exclusionRuleTemplate').content
    row = document.importNode content, true

    for field in ["pattern", "passKeys"]
      element = row.querySelector ".#{field}"
      element.value = rule[field]
      for event in [ "input", "change" ]
        element.addEventListener event, @onUpdated

    @getRemoveButton(row).addEventListener "click", (event) =>
      rule = event.target.parentNode.parentNode
      rule.parentNode.removeChild rule
      @onUpdated()

    @element.appendChild row
    @element.children[@element.children.length-1]

  readValueFromElement: ->
    rules =
      for element in @element.getElementsByClassName "exclusionRuleTemplateInstance"
        pattern: @getPattern(element).value.trim()
        passKeys: @getPassKeys(element).value.trim()
    rules.filter (rule) -> rule.pattern

  # Accessors for the three main sub-elements of an "exclusionRuleTemplateInstance".
  getPattern: (element) -> element.querySelector(".pattern")
  getPassKeys: (element) -> element.querySelector(".passKeys")
  getRemoveButton: (element) -> element.querySelector(".exclusionRemoveButton")

# ExclusionRulesOnPopupOption is ExclusionRulesOption, extended with some UI tweeks suitable for use in the
# page popup.  This also differs from ExclusionRulesOption in that, on the page popup, there is always a URL
# (@url) associated with the current tab.
class ExclusionRulesOnPopupOption extends ExclusionRulesOption
  constructor: (@url, args...) ->
    super(args...)

  addRule: ->
    element = super @generateDefaultPattern()
    @activatePatternWatcher element
    # ExclusionRulesOption.addRule()/super() has focused the pattern.  Here, focus the passKeys instead;
    # because, in the popup, we already have a pattern, so the user is more likely to edit the passKeys.
    @getPassKeys(element).focus()
    # Return element (for consistency with ExclusionRulesOption.addRule()).
    element

  populateElement: (rules) ->
    super(rules)
    elements = @element.getElementsByClassName "exclusionRuleTemplateInstance"
    @activatePatternWatcher element for element in elements

    haveMatch = false
    for element in elements
      pattern = @getPattern(element).value.trim()
      if 0 <= @url.search bgExclusions.RegexpCache.get pattern
        haveMatch = true
        @getPassKeys(element).focus()
      else
        element.style.display = 'none'
    @addRule() unless haveMatch


  # Provide visual feedback (make it red) when a pattern does not match the current tab's URL.
  activatePatternWatcher: (element) ->
    patternElement = element.children[0].firstChild
    patternElement.addEventListener "keyup", =>
      if @url.match bgExclusions.RegexpCache.get patternElement.value
        patternElement.title = patternElement.style.color = ""
      else
        patternElement.style.color = "red"
        patternElement.title = "Red text means that the pattern does not\nmatch the current URL."

  # Generate a default exclusion-rule pattern from a URL.  This is then used to pre-populate the pattern on
  # the page popup.
  generateDefaultPattern: ->


    if /^https?:\/\/./.test @url
      # The common use case is to disable Vimium at the domain level.
      # Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html".
      "https?:/" + @url.split("/",3)[1..].join("/") + "/*"
    else if /^[a-z]{3,}:\/\/./.test @url
      # Anything else which seems to be a URL.
      @url.split("/",3).join("/") + "/*"
    else
      @url + "*"

Options =
  exclusionRules: ExclusionRulesOption
  filterLinkHints: CheckBoxOption
  waitForEnterForFilteredHints: CheckBoxOption
  hideHud: CheckBoxOption
  keyMappings: TextOption
  linkHintCharacters: NonEmptyTextOption
  linkHintNumbers: NonEmptyTextOption
  newTabUrl: NonEmptyTextOption
  nextPatterns: NonEmptyTextOption
  previousPatterns: NonEmptyTextOption
  regexFindMode: CheckBoxOption
  scrollStepSize: NumberOption
  smoothScroll: CheckBoxOption
  grabBackFocus: CheckBoxOption
  searchEngines: TextOption
  searchUrl: NonEmptyTextOption
  userDefinedLinkHintCss: TextOption

initOptionsPage = ->
  onUpdated = ->
    $("saveOptions").removeAttribute "disabled"
    $("saveOptions").innerHTML = "Save Changes"

  # Display either "linkHintNumbers" or "linkHintCharacters", depending upon "filterLinkHints".
  maintainLinkHintsView = ->
    hide = (el) -> el.style.display = "none"
    show = (el) -> el.style.display = "table-row"
    if $("filterLinkHints").checked
      hide $("linkHintCharactersContainer")
      show $("linkHintNumbersContainer")
      show $("waitForEnterForFilteredHintsContainer")
    else
      show $("linkHintCharactersContainer")
      hide $("linkHintNumbersContainer")
      hide $("waitForEnterForFilteredHintsContainer")




  maintainAdvancedOptions = ->
    if bgSettings.get "optionsPage_showAdvancedOptions"
      $("advancedOptions").style.display = "table-row-group"
      $("advancedOptionsButton").innerHTML = "Hide Advanced Options"
    else
      $("advancedOptions").style.display = "none"
      $("advancedOptionsButton").innerHTML = "Show Advanced Options"
  maintainAdvancedOptions()

  toggleAdvancedOptions = (event) ->
    bgSettings.set "optionsPage_showAdvancedOptions", not bgSettings.get "optionsPage_showAdvancedOptions"
    maintainAdvancedOptions()
    $("advancedOptionsButton").blur()
    event.preventDefault()



  activateHelpDialog = ->
    request = showUnboundCommands: true, showCommandNames: true, customTitle: "Command Listing"
    chrome.runtime.sendMessage extend(request, handler: "getHelpDialogHtml"), (response) ->
      HelpDialog.toggle {html: response}

  saveOptions = ->
    Option.saveOptions()
    $("saveOptions").disabled = true
    $("saveOptions").innerHTML = "No Changes"

  $("saveOptions").addEventListener "click", saveOptions
  $("advancedOptionsButton").addEventListener "click", toggleAdvancedOptions
  $("showCommands").addEventListener "click", activateHelpDialog
  $("filterLinkHints").addEventListener "click", maintainLinkHintsView

  for element in document.getElementsByClassName "nonEmptyTextOption"
    element.className = element.className + " example info"
    element.innerHTML = "Leave empty to reset this option."

  window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled

  document.addEventListener "keyup", (event) ->
    if event.ctrlKey and event.keyCode == 13
      document.activeElement.blur() if document?.activeElement?.blur
      saveOptions()

  # Populate options. The constructor adds each new object to "Option.all".
  for own name, type of Options
    new type(name,onUpdated)

  maintainLinkHintsView()

initPopupPage = ->
  chrome.tabs.getSelected null, (tab) ->
    exclusions = null
    document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html")

    # As the active URL, we choose the most recently registered URL from a frame in the tab, or the tab's own
    # URL.
    url = chrome.extension.getBackgroundPage().urlForTab[tab.id] || tab.url

    updateState = ->
      rule = bgExclusions.getRule url, exclusions.readValueFromElement()
      $("state").innerHTML = "Vimium will " +
        if rule and rule.passKeys
          "exclude <span class='code'>#{rule.passKeys}</span>"
        else if rule
          "be disabled"
        else
          "be enabled"

    onUpdated = ->
      $("helpText").innerHTML = "Type <strong>Ctrl-Enter</strong> to save and close."
      $("saveOptions").removeAttribute "disabled"
      $("saveOptions").innerHTML = "Save Changes"
      updateState() if exclusions

    saveOptions = ->
      Option.saveOptions()
      $("saveOptions").innerHTML = "Saved"
      $("saveOptions").disabled = true

    $("saveOptions").addEventListener "click", saveOptions

    document.addEventListener "keyup", (event) ->
      if event.ctrlKey and event.keyCode == 13
        saveOptions()
        window.close()

    # Populate options. Just one, here.
    exclusions = new ExclusionRulesOnPopupOption url, "exclusionRules", onUpdated

    updateState()
    document.addEventListener "keyup", updateState

#
# Initialization.
document.addEventListener "DOMContentLoaded", ->
  xhr = new XMLHttpRequest()
  xhr.open 'GET', chrome.extension.getURL('pages/exclusions.html'), true
  xhr.onreadystatechange = ->
    if xhr.readyState == 4
      $("exclusionScrollBox").innerHTML = xhr.responseText
      switch location.pathname
        when "/pages/options.html" then initOptionsPage()
        when "/pages/popup.html" then initPopupPage()

  xhr.send()

# Exported for tests.
root = exports ? window
extend root, {Options, isVimiumOptionsPage: true}


Changes to vimium/pages/options.css.

118
119
120
121
122
123
124



125
126
127
128
129
130
131
  white-space: nowrap;
}
input#previousPatterns, input#nextPatterns {
  width: 100%;
}
input#newTabUrl {
  width: 100%;



}
input#searchUrl {
  width: 100%;
}
#status {
  margin-left: 10px;
  font-size: 80%;







>
>
>







118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
  white-space: nowrap;
}
input#previousPatterns, input#nextPatterns {
  width: 100%;
}
input#newTabUrl {
  width: 100%;
}
input#homeUrl {
  width: 100%;
}
input#searchUrl {
  width: 100%;
}
#status {
  margin-left: 10px;
  font-size: 80%;

Changes to vimium/pages/options.html.

219
220
221
222
223
224
225











226
227
228
229
230
231
232
                      Set this to "<tt>pages/blank.html</tt>" for a blank page (except incognito mode).<br />
                  </div>
                </div>
                <input id="newTabUrl" type="text" />
                <div class="nonEmptyTextOption">
            </td>
          </tr>











          <tr>
            <td class="caption">Default search<br/>engine</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                     The search engine to use in the Vomnibar <br> (e.g.: "http://duckduckgo.com/?q=").
                  </div>







>
>
>
>
>
>
>
>
>
>
>







219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
                      Set this to "<tt>pages/blank.html</tt>" for a blank page (except incognito mode).<br />
                  </div>
                </div>
                <input id="newTabUrl" type="text" />
                <div class="nonEmptyTextOption">
            </td>
          </tr>
          <tr>
            <td class="caption">Home</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                    Set which URL is used for all new tabs.
                  </div>
                </div>
                <input id="homeUrl" type="text" />
            </td>
          </tr>
          <tr>
            <td class="caption">Default search<br/>engine</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                     The search engine to use in the Vomnibar <br> (e.g.: "http://duckduckgo.com/?q=").
                  </div>

Changes to vimium/pages/options.html.orig.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200

201


202
203

204




205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234





235
236
237
238
239

240














241
242
243
244
245
246
247
248

249
250
251
252
253
254
255
256
257
258
259







































260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314

315
316
317
318
319
320
321
322
323
324
325














326
327
328
329
330
331
332
333
334
335
336

337
338












































339
340

341

342
343
344











345
346
347
348
349
350
351
352
353
354
355
356
357
<html>
  <head>
    <title>Vimium Options</title>
    <script src="../lib/utils.js"></script>
    <script src="../lib/keyboard_utils.js"></script>
    <script src="../lib/dom_utils.js"></script>
    <script src="../lib/handler_stack.js"></script>
    <script src="../lib/clipboard.js"></script>
    <script src="../content_scripts/link_hints.js"></script>
    <script src="../content_scripts/vomnibar.js"></script>
    <script src="../content_scripts/scroller.js"></script>
    <script src="../content_scripts/vimium_frontend.js"></script>
    <style type="text/css" media="screen">
      body {
        font: 14px "DejaVu Sans", "Arial", sans-serif;
        color: #303942;
        width: 680px;
        margin: 0 auto;
      }
      a, a:visited { color: #15c; }
      a:active { color: #052577; }
      div#wrapper { width: 500px; }
      header {
        font-size: 18px;
        font-weight: normal;
        border-bottom: 1px solid #eee;
        padding: 20px 0 15px 0;
        width: 100%;
      }
      button {
        -webkit-user-select: none;
        -webkit-appearance: none;
        background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
        border: 1px solid rgba(0, 0, 0, 0.25);
        border-radius: 2px;
        box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75);
        color: #444;
        font: inherit;
        text-shadow: 0 1px 0 #f0f0f0;
        height: 24px;
        font-size: 12px;
        padding: 0 10px;
      }
      button:hover {
        background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
        border-color: rgba(0, 0, 0, 0.3);
        box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95);
        color: black;
      }
      button:active {
        background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
        box-shadow: none;
        text-shadow: none;
      }
      button[disabled], button[disabled]:hover, button[disabled]:active {
        background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
        border: 1px solid rgba(0, 0, 0, 0.25);
        box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75);
        text-shadow: 0 1px 0 #f0f0f0;
        color: #888;
      }
      input[type="checkbox"] {
        -webkit-user-select: none;
      }
      label:hover {
        color: black;
      }
      pre, code, .code {
        font-family: Consolas, "Liberation Mono", Courier, monospace;
      }
      pre {
        margin: 5px;
        border-left: 1px solid #eee;
        padding-left: 5px;

      }
      input, textarea {
        box-sizing: border-box;
      }
      textarea {
        /* Horizontal resizing is pretty screwy-looking. */
        resize: vertical;
      }
      table#options{
        width: 100%;
        font-size: 14px;
        position: relative;
        border-spacing: 0 23px;
      }
      .example {
        font-size: 12px;
        line-height: 16px;
        color: #979ca0;
        margin-left: 20px;
      }
      .caption {
        margin-right: 10px;
        min-width: 130px;
      }
      td { padding: 0; }
      div#exampleKeyMapping {
        margin-left: 10px;
        margin-top: 5px;
      }
      input#linkHintCharacters {
        width: 160px;
      }
      input#scrollStepSize {
        width: 40px;
        margin-right: 3px;
      }
      textarea#excludedUrls {
        margin-top: 5px;
        width: 100%;
        min-height: 100px;
      }
      textarea#userDefinedLinkHintCss {
        width: 100%;;
        min-height: 100px;
      }
      textarea#keyMappings {
        width: 100%;
        min-height: 135px;
      }
      input#previousPatterns, input#nextPatterns {
        width: 100%;
      }
      input#searchUrl {
        width: 100%;
      }
      #status {
        margin-left: 10px;
        font-size: 80%;
      }
      /* Make the caption in the settings table as small as possible, to pull the other fields to the right. */
      td:first-child {
        width: 1px;
        white-space: nowrap;
      }
      #buttonsPanel { width: 100%; }
      #advancedOptions { display: none; }
      #advancedOptionsLink { line-height: 24px; }
      #buttonContainer { float: right; }
      #buttonContainer button:last-child {
        margin-right: 0;
      }
      #showHelpDialogMessage { width: 100%; }
      .help {
        position: absolute;
        right: -320px;
        width: 320px;
      }
      input:read-only {
        background-color: #eee;
        color: #666;
        pointer-events: none;
        -webkit-user-select: none;
      }
      input[type="text"], textarea {
        border: 1px solid #bfbfbf;
        border-radius: 2px;
        color: #444;
        font: inherit;
        padding: 3px;
      }
      button:focus, input[type="text"]:focus, textarea:focus {
        -webkit-transition: border-color 200ms;
        border-color: #4d90fe;
        outline: none;
      }
      /* Boolean options have a tighter form representation than text options. */
      td.booleanOption { font-size: 12px; }
      footer {
        padding: 15px 0;
        border-top: 1px solid #eee;
      }
    </style>
  <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />

  <script type="text/javascript" src="options.js"></script>

  </head>

  <body>
    <div id="wrapper">
      <header>Vimium options</header>
      <table id="options">
        <tr>
          <td class="caption">Scroll step size</td>
          <td>
            <input id="scrollStepSize" type="number" />px
          </td>
        </tr>
        <tr>
          <td colspan="3">
              Excluded URLs<br/>
              <div class="help">
                <div class="example">
                  e.g. http*://mail.google.com/*<br/>
                  This will disable Vimium on Gmail.<br/><br/>

                  Enter one URL per line.<br/>


                </div>
              </div>

              <textarea id="excludedUrls"></textarea>




          </td>
        </tr>
        <tbody id='advancedOptions'>
          <tr>
            <td class="caption">Custom key<br/>mappings</td>
            <td id="mappingsHelp" verticalAlign="top">
              <div class="help">
                <div class="example">
                  <!-- TODO(ilya/philc): Expand this and style it better. -->
                  Enter commands to remap your keys. Available commands:<br/>
                  <pre id="exampleKeyMapping">
map j scrollDown
unmap j
unmapAll
" this is a comment
# this is also a comment</pre>
                  <a href="#" id="showCommands">Show available commands.</a>
                </div>
              </div>
              <textarea id="keyMappings" type="text"></textarea>
            </td>
          </tr>
          <tr>
            <td class="caption">CSS for link hints</td>
            <td verticalAlign="top">
              <div class="help">
                <div class="example">
                  The CSS used to style the characters next to each link hint.<br/><br/>
                  Note: these styles are used in addition to and take precedence over Vimium's
                  default styles.





                </div>
              </div>
              <textarea id="userDefinedLinkHintCss" class="code" type="text"></textarea>
            </td>
          </tr>

          <tr>














            <td class="caption">Characters used<br/> for link hints</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                    The characters placed next to each link after typing "F" to enter link hinting mode.
                  </div>
                </div>
                <input id="linkHintCharacters" type="text" />

            </td>
          </tr>
          <tr>
            <td class="caption">Numbers used<br/> for filtered link hints</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                    The numbers placed next to each link after typing "F" to enter link hinting mode.
                  </div>
                </div>
                <input id="linkHintNumbers" type="text" />







































            </td>
          </tr>
          <tr>
            <td class="caption"></td>
            <td verticalAlign="top" class="booleanOption">
              <div class="help">
                <div class="example">
                  After typing "F" to enter link hinting mode, this option lets you type the text of a link
                  to select it.
                </div>
              </div>
              <label>
                <input id="filterLinkHints" type="checkbox"/>
                Use the link's name and numbers for link hint filtering
              </label>
            </td>
          </tr>
          <tr>
            <td class="caption"></td>
            <td verticalAlign="top" class="booleanOption">
              <div class="help">
                <div class="example">
                  The Heads-Up Display appears when typing into text boxes.
                </div>
              </div>
              <label>
                <input id="hideHud" type="checkbox"/>
                Hide the Heads Up Display (HUD)
              </label>
            </td>
          </tr>
          <tr>
            <td class="caption"></td>
            <td verticalAlign="top" class="booleanOption">
              <div class="help">
                <div class="example">
                  Switch back to plain find mode by using the <code>\R</code> escape sequence.
                </div>
              </div>
              <label>
                <input id="regexFindMode" type="checkbox"/>
                Treat find queries as regular expressions.
              </label>
            </td>
          </tr>
          <tr>
            <td class="caption">Previous Patterns</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                    Vimium will match against these patterns when using the "navigate to the previous page"
                    command.
                  </div>
                </div>
                <input id="previousPatterns" type="text" />

            </td>
          </tr>
          <tr>
            <td class="caption">Next Patterns</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                    Vimium will match against these patterns when using the "navigate to the next page" command.
                  </div>
                </div>
                <input id="nextPatterns" type="text" />














            </td>
          </tr>
          <tr>
            <td class="caption">Search</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=").
                  </div>
                </div>
                <input id="searchUrl" type="text" />

            </td>
          </tr>












































        </tbody>
      </table>



      <div id="buttonsPanel">
        <a href="#" id="advancedOptionsLink">Show advanced options&hellip;</a>
        <div id="buttonContainer">











          <button id="restoreSettings">Restore to Defaults</button>
          <button id="saveOptions" disabled="true">Save Options</button>
        </div>
      </div>

      <br/>

      <footer id="showHelpDialogMessage">
        To view all available shortcuts, type <strong>?</strong> to show the Vimium help dialog.
      </footer>
    </div>
  </body>
</html>



<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|
<




|


|

<
<
<
<
<
<
|
|
<
|
>
|
>
>
|
|
>
|
>
>
>
>


<
|
|
|
|
|
|
|
|





|
|
|
|
|
|
|
|
|


|
|
|
>
>
>
>
>


|
|
|
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>




|



>


|
|



|



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







|
<



|
|








|




|













|




|



|
<



>



|



|



>
>
>
>
>
>
>
>
>
>
>
>
>
>



|



|



>


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


>

>
|
|
|
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
<
|
<
<
<



1
2
3














































































































































































4
5
6

7
8
9
10
11
12
13
14
15






16
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152

153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194

195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305

306



307
308
309
<html>
  <head>
    <title>Vimium Options</title>














































































































































































    <link rel="stylesheet" type="text/css" href="options.css">
    <script src="content_script_loader.js"></script>
    <script type="text/javascript" src="options.js"></script>

  </head>

  <body>
    <div id="wrapper">
      <header>Vimium Options</header>
      <table id="options">
        <tr>
          <td class="caption">Excluded URLs<br/>and keys</td>
          <td>






            <div class="help">
              <div class="example">

                Wholly or partially disable Vimium.  "Patterns" are URL regular expressions;
                additionally, "*" matches any zero or more characters.
                <br/><br/>
                If "Keys" is left empty, then Vimium is wholly disabled.
                Otherwise, just the listed keys are disabled (they are passed through).
              </div>
            </div>
            <div>
               <div id="exclusionScrollBox">
                  <!-- Populated from exclusions.html by options.coffee. -->
               </div>
               <button id="exclusionAddButton">Add Rule</button>
            </div>
          </td>
        </tr>

        <tr>
          <td class="caption">Custom key<br/>mappings</td>
          <td id="mappingsHelp" verticalAlign="top">
            <div class="help">
              <div class="example">
                <!-- TODO(ilya/philc): Expand this and style it better. -->
                Enter commands to remap your keys. Available commands:<br/>
                <pre id="exampleKeyMapping">
map j scrollDown
unmap j
unmapAll
" this is a comment
# this is also a comment</pre>
                <a href="#" id="showCommands">Show available commands</a>.
              </div>
            </div>
            <textarea id="keyMappings" type="text"></textarea>
          </td>
        </tr>
        <tr>
          <td class="caption">Custom search<br/>engines</td>
          <td verticalAlign="top">
              <div class="help">
                <div class="example">
                  Add search-engine shortcuts to the Vomnibar. Format:<br/>
                  <pre>
a: http://a.com/?q=%s
b: http://b.com/?q=%s description
" this is a comment
# this is also a comment</pre>
                  %s is replaced with the search terms. <br/>
                  For search completion, see <a href="completion_engines.html" target="_blank">here</a>.
                </div>
              </div>
              <textarea id="searchEngines"></textarea>
          </td>
        </tr>
        <tbody id='advancedOptions'>
          <tr>
            <td colspan="2"><header>Advanced Options</header></td>
          </tr>
          <tr>
            <td class="caption">Scroll step size</td>
            <td>
                <div class="help">
                  <div class="example">
                    The size for basic movements (usually j/k/h/l).
                  </div>
                </div>
              <input id="scrollStepSize" type="number" />px
            </td>
          </tr>
          <tr id="linkHintCharactersContainer">
            <td class="caption">Characters used<br/> for link hints</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                    The characters placed next to each link after typing "f" to enter link-hint mode.
                  </div>
                </div>
                <input id="linkHintCharacters" type="text" />
                <div class="nonEmptyTextOption">
            </td>
          </tr>
          <tr id="linkHintNumbersContainer">
            <td class="caption">Numbers used<br/> for link hints</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                    The numbers placed next to each link after typing "f" to enter link-hint mode.
                  </div>
                </div>
                <input id="linkHintNumbers" type="text" />
                <div class="nonEmptyTextOption">
            </td>
          </tr>
          <tr>
            <td class="caption" verticalAlign="top">Miscellaneous<br/>options</td>
            <td verticalAlign="top" class="booleanOption">
              <label>
                <input id="smoothScroll" type="checkbox"/>
                Use smooth scrolling
              </label>
            </td>
          </tr>
          <tr>
            <td class="caption"></td>
            <td verticalAlign="top" class="booleanOption">
              <div class="help">
                <div class="example">
                  In link-hint mode, this option lets you select a link by typing its text.
                </div>
              </div>
              <label>
                <input id="filterLinkHints" type="checkbox"/>
                Use the link's name and numbers for link-hint filtering
              </label>
            </td>
          </tr>
          <tr id="waitForEnterForFilteredHintsContainer">
            <td class="caption"></td>
            <td verticalAlign="top" class="booleanOption">
              <div class="help">
                <div class="example">
                  You activate the link with <tt>Enter</tt>, <em>always</em>; so you never accidentally type Vimium
                  commands.
                </div>
              </div>
              <label>
                <input id="waitForEnterForFilteredHints" type="checkbox"/>
                Require <tt>Enter</tt> when filtering hints
              </label>
            </td>
          </tr>
          <tr>
            <td class="caption"></td>
            <td verticalAlign="top" class="booleanOption">
              <div class="help">
                <div class="example">
                  Prevent pages from focusing an input on load (e.g. Google, Bing, etc.).

                </div>
              </div>
              <label>
                <input id="grabBackFocus" type="checkbox"/>
                Don't let pages steal the focus on load
              </label>
            </td>
          </tr>
          <tr>
            <td class="caption"></td>
            <td verticalAlign="top" class="booleanOption">
              <div class="help">
                <div class="example">
                  When enabled, the HUD will not be displayed in insert mode.
                </div>
              </div>
              <label>
                <input id="hideHud" type="checkbox"/>
                Hide the Heads Up Display (HUD) in insert mode
              </label>
            </td>
          </tr>
          <tr>
            <td class="caption"></td>
            <td verticalAlign="top" class="booleanOption">
              <div class="help">
                <div class="example">
                  Switch back to plain find mode by using the <code>\R</code> escape sequence.
                </div>
              </div>
              <label>
                <input id="regexFindMode" type="checkbox"/>
                Treat find queries as regular expressions
              </label>
            </td>
          </tr>
          <tr>
            <td class="caption">Previous patterns</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                    The "navigate to previous page" command uses these patterns to find the link to follow.

                  </div>
                </div>
                <input id="previousPatterns" type="text" />
                <div class="nonEmptyTextOption">
            </td>
          </tr>
          <tr>
            <td class="caption">Next patterns</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                    The "navigate to next page" command uses these patterns to find the link to follow.
                  </div>
                </div>
                <input id="nextPatterns" type="text" />
                <div class="nonEmptyTextOption">
            </td>
          </tr>
          <tr>
            <td class="caption">New tab URL</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                      The page to open with the "create new tab" command.
                      Set this to "<tt>pages/blank.html</tt>" for a blank page (except incognito mode).<br />
                  </div>
                </div>
                <input id="newTabUrl" type="text" />
                <div class="nonEmptyTextOption">
            </td>
          </tr>
          <tr>
            <td class="caption">Default search<br/>engine</td>
            <td verticalAlign="top">
                <div class="help">
                  <div class="example">
                     The search engine to use in the Vomnibar <br> (e.g.: "http://duckduckgo.com/?q=").
                  </div>
                </div>
                <input id="searchUrl" type="text" />
                <div class="nonEmptyTextOption">
            </td>
          </tr>
          <tr>
            <td class="caption">CSS for link hints</td>
            <td verticalAlign="top">
              <div class="help">
                <div class="example">
                  The CSS used to style the characters next to each link hint.<br/><br/>
                  These styles are used in addition to and take precedence over Vimium's
                  default styles.
                </div>
              </div>
              <textarea id="userDefinedLinkHintCss" class="code" type="text"></textarea>
              <div class="nonEmptyTextOption">
            </td>
          </tr>

          <!-- Vimium Labs -->
          <!--
          Disabled.  But we leave this code here as a template for the next time we need to introduce "Vimium Labs".
          <tr>
            <td colspan="2"><header>Vimium Labs</header></td>
          </tr>
          <tr>
            <td class="caption"></td>
            <td>
                <div class="help">
                  <div class="example">
                  </div>
                </div>
                These features are experimental and may be changed or removed in future releases.
            </td>
          </tr>
          <tr>
            <td class="caption">Search weighting</td>
            <td>
                <div class="help">
                  <div class="example">
                    How prominent should suggestions be in the vomnibar?
                    <tt>0</tt> disables suggestions altogether.
                  </div>
                </div>
              <input id="omniSearchWeight" type="number" min="0.0" max="1.0" step="0.05" />(0 to 1)
            </td>
          </tr>
          -->
        </tbody>
      </table>
    </div>

    <!-- Some extra space which is hidden underneath the footer. -->
    <div id="endSpace"/>

    <div id="footer">
      <div id="footerWrapper">
        <table id="footerTable">
          <tr>
            <td id="footerTableData">
              <span id="helpText">
                Type <strong>?</strong> to show the help dialog.
                <br/>
                Type <strong>Ctrl-Enter</strong> to save <i>all</i> options.
              </span>
            </td>
            <td id="saveOptionsTableData" nowrap>
              <button id="advancedOptionsButton"></button>
              <button id="saveOptions" disabled="true">No Changes</button>
            </td>
          </tr>
        </table>

      </div>



    </div>
  </body>
</html>