Bug 1267810 - Add support for omnibox.onInputStarted. r?aswan draft
authorMatthew Wein <mwein@mozilla.com>
Fri, 30 Sep 2016 16:45:29 -0700
changeset 419849 bbb0bf7d65567a092ac091113bd21404322a2afb
parent 419848 9fdfeaca75343a89bd91788551d11c3bc48d8039
child 532664 545a8ad40a13827b5a417dc09169f20a9a3d1aca
push id31028
push usermwein@mozilla.com
push dateSat, 01 Oct 2016 05:46:25 +0000
reviewersaswan
bugs1267810
milestone52.0a1
Bug 1267810 - Add support for omnibox.onInputStarted. r?aswan MozReview-Commit-ID: GYzueGSijyd
browser/components/extensions/ext-omnibox.js
browser/components/extensions/extensions-browser.manifest
browser/components/extensions/jar.mn
browser/components/extensions/schemas/jar.mn
browser/components/extensions/schemas/omnibox.json
browser/components/extensions/test/browser/browser.ini
browser/components/extensions/test/browser/browser_ext_omnibox_onInputStarted.js
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-omnibox.js
@@ -0,0 +1,43 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://devtools/shared/event-emitter.js");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
+                                  "resource://gre/modules/ExtensionSearchHandler.jsm");
+
+var {
+  EventManager,
+} = ExtensionUtils;
+
+let keyword = null;
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_omnibox", (type, directive, extension, manifest) => {
+  keyword = manifest.omnibox.keyword;
+  ExtensionSearchHandler.registerKeyword(keyword, extension);
+});
+
+extensions.on("shutdown", (type, extension) => {
+  if (keyword) {
+    ExtensionSearchHandler.unregisterKeyword(keyword);
+  }
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("omnibox", "addon_parent", context => {
+  let {extension} = context;
+  return {
+    omnibox: {
+      onInputStarted: new EventManager(context, "omnibox.onInputStarted", fire => {
+        extension.on(ExtensionSearchHandler.EVENT_INPUT_STARTED, fire);
+        return () => {
+          extension.off(ExtensionSearchHandler.EVENT_INPUT_STARTED, fire);
+        };
+      }).api(),
+    },
+  };
+});
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -1,25 +1,27 @@
 # scripts
 category webextension-scripts bookmarks chrome://browser/content/ext-bookmarks.js
 category webextension-scripts browserAction chrome://browser/content/ext-browserAction.js
 category webextension-scripts commands chrome://browser/content/ext-commands.js
 category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
 category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
 category webextension-scripts history chrome://browser/content/ext-history.js
+category webextension-scripts omnibox chrome://browser/content/ext-omnibox.js
 category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
 category webextension-scripts tabs chrome://browser/content/ext-tabs.js
 category webextension-scripts utils chrome://browser/content/ext-utils.js
 category webextension-scripts windows chrome://browser/content/ext-windows.js
 
 # scripts that must run in the same process as addon code.
 category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
 
 # schemas
 category webextension-schemas bookmarks chrome://browser/content/schemas/bookmarks.json
 category webextension-schemas browser_action chrome://browser/content/schemas/browser_action.json
 category webextension-schemas commands chrome://browser/content/schemas/commands.json
 category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
 category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
 category webextension-schemas history chrome://browser/content/schemas/history.json
+category webextension-schemas omnibox chrome://browser/content/schemas/omnibox.json
 category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
 category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
 category webextension-schemas windows chrome://browser/content/schemas/windows.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -13,13 +13,14 @@ browser.jar:
 #endif
     content/browser/extension.svg
     content/browser/ext-bookmarks.js
     content/browser/ext-browserAction.js
     content/browser/ext-commands.js
     content/browser/ext-contextMenus.js
     content/browser/ext-desktop-runtime.js
     content/browser/ext-history.js
+    content/browser/ext-omnibox.js
     content/browser/ext-pageAction.js
     content/browser/ext-tabs.js
     content/browser/ext-utils.js
     content/browser/ext-windows.js
     content/browser/ext-c-tabs.js
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -4,11 +4,12 @@
 
 browser.jar:
     content/browser/schemas/bookmarks.json
     content/browser/schemas/browser_action.json
     content/browser/schemas/commands.json
     content/browser/schemas/context_menus.json
     content/browser/schemas/context_menus_internal.json
     content/browser/schemas/history.json
+    content/browser/schemas/omnibox.json
     content/browser/schemas/page_action.json
     content/browser/schemas/tabs.json
     content/browser/schemas/windows.json
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/omnibox.json
@@ -0,0 +1,216 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "omnibox": {
+            "type": "object",
+            "additionalProperties": { "$ref": "UnrecognizedProperty" },
+            "properties": {
+              "keyword": {
+                "type": "string",
+                "optional": true
+              }
+            },
+            "optional": true
+          }
+        }
+      }
+    ]
+  },
+  {
+    "namespace": "omnibox",
+    "description": "The omnibox API allows you to register a keyword with Firefox's address bar.",
+    "permissions": ["manifest:omnibox"],
+    "types": [
+      {
+        "id": "DescriptionStyleType",
+        "type": "string",
+        "description": "The style type.",
+        "enum": ["url", "match", "dim"]
+      },
+      {
+        "id": "OnInputEnteredDisposition",
+        "type": "string",
+        "enum": ["currentTab", "newForegroundTab", "newBackgroundTab"],
+        "description": "The window disposition for the omnibox query. This is the recommended context to display results. For example, if the omnibox command is to navigate to a certain URL, a disposition of 'newForegroundTab' means the navigation should take place in a new selected tab."
+      },
+      {
+        "id": "SuggestResult",
+        "type": "object",
+        "description": "A suggest result.",
+        "properties": {
+          "content": {
+            "type": "string",
+            "minLength": 1,
+            "description": "The text that is put into the URL bar, and that is sent to the extension when the user chooses this entry."
+          },
+          "description": {
+            "type": "string",
+            "minLength": 1,
+            "description": "The text that is displayed in the URL dropdown. Can contain XML-style markup for styling. The supported tags are 'url' (for a literal URL), 'match' (for highlighting text that matched what the user's query), and 'dim' (for dim helper text). The styles can be nested, eg. <dim><match>dimmed match</match></dim>. You must escape the five predefined entities to display them as text: stackoverflow.com/a/1091953/89484 "
+          },
+          "descriptionStyles": {
+            "optional": true,
+            "type": "array",
+            "description": "An array of style ranges for the description, as provided by the extension.",
+            "items": {
+              "type": "object",
+              "description": "The style ranges for the description, as provided by the extension.",
+              "properties": {
+                "offset": { "type": "integer" },
+                "type": { "description": "The style type", "$ref": "DescriptionStyleType"},
+                "length": { "type": "integer", "optional": true }
+              }
+            }
+          },
+          "descriptionStylesRaw": {
+            "optional": true,
+            "type": "array",
+            "description": "An array of style ranges for the description, as provided by ToValue().",
+            "items": {
+              "type": "object",
+              "description": "The style ranges for the description, as provided by ToValue().",
+              "properties": {
+                "offset": { "type": "integer" },
+                "type": { "type": "integer" }
+              }
+            }
+          }
+        }
+      },
+      {
+        "id": "DefaultSuggestResult",
+        "type": "object",
+        "description": "A suggest result.",
+        "properties": {
+          "description": {
+            "type": "string",
+            "minLength": 1,
+            "description": "The text that is displayed in the URL dropdown. Can contain XML-style markup for styling. The supported tags are 'url' (for a literal URL), 'match' (for highlighting text that matched what the user's query), and 'dim' (for dim helper text). The styles can be nested, eg. <dim><match>dimmed match</match></dim>."
+          },
+          "descriptionStyles": {
+            "optional": true,
+            "type": "array",
+            "description": "An array of style ranges for the description, as provided by the extension.",
+            "items": {
+              "type": "object",
+              "description": "The style ranges for the description, as provided by the extension.",
+              "properties": {
+                "offset": { "type": "integer" },
+                "type": { "description": "The style type", "$ref": "DescriptionStyleType"},
+                "length": { "type": "integer", "optional": true }
+              }
+            }
+          },
+          "descriptionStylesRaw": {
+            "optional": true,
+            "type": "array",
+            "description": "An array of style ranges for the description, as provided by ToValue().",
+            "items": {
+              "type": "object",
+              "description": "The style ranges for the description, as provided by ToValue().",
+              "properties": {
+                "offset": { "type": "integer" },
+                "type": { "type": "integer" }
+              }
+            }
+          }
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "sendSuggestions",
+        "type": "function",
+        "unsupported": true,
+        "description": "A callback passed to the onInputChanged event used for sending suggestions back to the browser.",
+        "parameters": [
+          {"type": "integer", "name": "requestId"},
+          {
+            "name": "suggestResults",
+            "type": "array",
+            "description": "An array of suggest results",
+            "items": {
+              "$ref": "SuggestResult"
+            }
+          }
+        ]
+      },
+      {
+        "name": "setDefaultSuggestion",
+        "type": "function",
+        "description": "Sets the description and styling for the default suggestion. The default suggestion is the text that is displayed in the first suggestion row underneath the URL bar.",
+        "parameters": [
+          {
+            "name": "suggestion",
+            "$ref": "DefaultSuggestResult",
+            "description": "A partial SuggestResult object, without the 'content' parameter."
+          }
+        ]
+      }
+    ],
+    "events": [
+      {
+        "name": "onInputStarted",
+        "type": "function",
+        "description": "User has started a keyword input session by typing the extension's keyword. This is guaranteed to be sent exactly once per input session, and before any onInputChanged events.",
+        "parameters": []
+      },
+      {
+        "name": "onInputChanged",
+        "type": "function",
+        "unsupported": true,
+        "description": "User has changed what is typed into the omnibox.",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "text"
+          },
+          {
+            "name": "suggest",
+            "type": "function",
+            "description": "A callback passed to the onInputChanged event used for sending suggestions back to the browser.",
+            "parameters": [
+              {
+                "name": "suggestResults",
+                "type": "array",
+                "description": "Array of suggest results",
+                "items": {
+                  "$ref": "SuggestResult"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "onInputEntered",
+        "type": "function",
+        "unsupported": true,
+        "description": "User has accepted what is typed into the omnibox.",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "text"
+          },
+          {
+            "name": "disposition",
+            "$ref": "OnInputEnteredDisposition"
+          }
+        ]
+      },
+      {
+        "name": "onInputCancelled",
+        "type": "function",
+        "description": "User has ended the keyword input session without accepting the input.",
+        "parameters": []
+      }
+    ]
+  }
+]
\ No newline at end of file
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -37,16 +37,17 @@ tags = webextensions
 [browser_ext_contextMenus_radioGroups.js]
 [browser_ext_contextMenus_uninstall.js]
 [browser_ext_contextMenus_urlPatterns.js]
 [browser_ext_currentWindow.js]
 [browser_ext_getViews.js]
 [browser_ext_incognito_popup.js]
 [browser_ext_lastError.js]
 [browser_ext_legacy_extension_context_contentscript.js]
+[browser_ext_omnibox_onInputStarted.js]
 [browser_ext_optionsPage_privileges.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_pageAction_popup_resize.js]
 [browser_ext_pageAction_simple.js]
 [browser_ext_pageAction_title.js]
 [browser_ext_popup_api_injection.js]
 [browser_ext_popup_background.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_omnibox_onInputStarted.js
@@ -0,0 +1,82 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* () {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "omnibox": {"keyword": "test"},
+    },
+
+    background: function() {
+      let expected = 0;
+
+      browser.omnibox.onInputStarted.addListener(() => {
+        expected++;
+      });
+
+      browser.test.onMessage.addListener((message, actual) => {
+        if (message == "assert-onInputStarted-fired-count") {
+          browser.test.assertEq(actual, expected,
+            "onInputStarted fired the correct number of times");
+          browser.test.sendMessage("continue");
+        } else if (message == "finish") {
+          browser.test.notifyPass("omnibox-on-input-entered");
+        }
+      });
+    },
+  });
+
+  yield extension.startup();
+
+  gURLBar.focus();
+
+  // input = "t"
+  EventUtils.synthesizeKey("t", {});
+  extension.sendMessage("assert-onInputStarted-fired-count", 0);
+  yield extension.awaitMessage("continue");
+
+  // input = "te"
+  EventUtils.synthesizeKey("e", {});
+  extension.sendMessage("assert-onInputStarted-fired-count", 0);
+  yield extension.awaitMessage("continue");
+
+  // input = "tes"
+  EventUtils.synthesizeKey("s", {});
+  extension.sendMessage("assert-onInputStarted-fired-count", 0);
+  yield extension.awaitMessage("continue");
+
+  // input = "test"
+  EventUtils.synthesizeKey("t", {});
+  extension.sendMessage("assert-onInputStarted-fired-count", 0);
+  yield extension.awaitMessage("continue");
+
+  // input = "test "
+  EventUtils.synthesizeKey(" ", {});
+  extension.sendMessage("assert-onInputStarted-fired-count", 1);
+  yield extension.awaitMessage("continue");
+
+  // input = "test a"
+  EventUtils.synthesizeKey("a", {});
+  extension.sendMessage("assert-onInputStarted-fired-count", 1);
+  yield extension.awaitMessage("continue");
+
+  // input = "test "
+  EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+  extension.sendMessage("assert-onInputStarted-fired-count", 1);
+  yield extension.awaitMessage("continue");
+
+  // input = "test"
+  EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+  extension.sendMessage("assert-onInputStarted-fired-count", 1);
+  yield extension.awaitMessage("continue");
+
+  // input = "test "
+  EventUtils.synthesizeKey(" ", {});
+  extension.sendMessage("assert-onInputStarted-fired-count", 2);
+  yield extension.awaitMessage("continue");
+
+  extension.sendMessage("finish");
+  yield extension.awaitFinish("omnibox-on-input-entered");
+  yield extension.unload();
+});