[omnibox chrome:omnibox] Export and Import JSON files

This CL adds a `Download as JSON` button to download the query inputs, display
inputs, and autocomplete responses to a .JSON file named
`omnibox_debug_export_<query>_<timestamp>.json`. This replaces the current
`Copy as JSON` button. The `Copy as Text` button, which only extracts the visual
output and not the entire responses or inputs, remains as is.

This CL also adds an `Import JSON` button to import previously exported JSON's.
Clicking the button will open a file selector; dragging the .JSON file or a
JSON string to the button also works. Selecting or dragging an invalid JSON file
or dragging an invalid JSON string results in a console.error message.

After successfully importing, a red 'Viewing imported data' warning will be
displayed under the import button to signify that the output may not reflect the
output the input would currently produce.

Bug: 891303
Change-Id: I1a17cda783fa9170baca8841bb80b19a3a67ab82
Reviewed-on: https://chromium-review.googlesource.com/c/1391917
Commit-Queue: manuk hovanesian <manukh@chromium.org>
Reviewed-by: Justin Donnelly <jdonnelly@chromium.org>
Cr-Commit-Position: refs/heads/master@{#620789}
diff --git a/chrome/browser/resources/omnibox/omnibox.css b/chrome/browser/resources/omnibox/omnibox.css
index 3ace27a6..473e409 100644
--- a/chrome/browser/resources/omnibox/omnibox.css
+++ b/chrome/browser/resources/omnibox/omnibox.css
@@ -338,10 +338,10 @@
 /* stylized checkbox */
 
 .checkbox-container,
-.button {
+.button,
+.warning-text {
   align-items: center;
   border-radius: 5px;
-  cursor: pointer;
   display: inline-flex;
   min-height: var(--row-height);
   padding-inline-end: var(--input-alignment-indentation);
@@ -349,12 +349,13 @@
   user-select: none;
 }
 
-input[type=checkbox] {
-  margin-inline-start: 0;
+.checkbox-container,
+.button {
+  cursor: pointer;
 }
 
-.checkbox-container span {
-  align-self: center;
+input[type=checkbox] {
+  margin-inline-start: 0;
 }
 
 /* button */
@@ -363,6 +364,13 @@
   color: var(--action-color);
 }
 
+/* warning text */
+
+.warning-text {
+  background-color: red;
+  color: white;
+}
+
 /* icons */
 
 .icon {
@@ -378,23 +386,11 @@
   background-image: url(../../../../ui/webui/resources/images/icon_search.svg);
 }
 
-.filter-icon {
-  background-image: url(../../../../ui/webui/resources/images/icon_search.svg);
-}
-
 .reset-icon {
   background-image: url(../../../../ui/webui/resources/images/icon_refresh.svg);
 }
 
-.copy-white-icon {
-  background-image: url(../../../../ui/webui/resources/images/icon_tabs.svg);
-}
-
-.copy-black-icon {
-  background-image: url(../../../../ui/webui/resources/images/icon_tabs.svg);
-}
-
-.copy-red-icon {
+.copy-icon {
   background-image: url(../../../../ui/webui/resources/images/icon_tabs.svg);
 }
 
@@ -436,3 +432,17 @@
   font-size: 12px;
   margin-inline-start: var(--input-alignment-indentation);
 }
+
+/* drag */
+
+.drag-container * {
+  pointer-events: none;
+}
+
+.drag-container input[type=file] {
+  display: none;
+}
+
+.drag-container.drag-hover {
+  background: lightblue;
+}
diff --git a/chrome/browser/resources/omnibox/omnibox.html b/chrome/browser/resources/omnibox/omnibox.html
index 875c8a7..bc0aa41c 100644
--- a/chrome/browser/resources/omnibox/omnibox.html
+++ b/chrome/browser/resources/omnibox/omnibox.html
@@ -133,25 +133,38 @@
       <div class="top-column">
         <p class="row section-header">Output controls</p>
         <input id="filter-text" type="text"
-               class="row input-icon-container filter-icon" accesskey="g"
+               class="row input-icon-container search-icon" accesskey="g"
                autocomplete="off"
                placeholder="Enter filter (e.g. 'google', 'is:star', 'not:del') [Alt+G]"
                title="Checks each cell individually; i.e. filter text should not span multiple columns. Supports fuzzyness; each character of filter text must be present in the cell, either adjacent to the previous matched character, or at the start of a new word. Words are defined as being delimited by either capital letters, groups of digits, or non alpha characters. E.g. 'abc' matches 'abc', 'a big cat', 'a-bigCat', 'a very big cat', and 'an amBer cat'; but does not match 'abigcat' or 'an amber cat'. 'green rainbow' is matched by 'gre rain', but not by 'gre bow'. One exception is the first character, which may be matched mid-word. E.g. 'een rain' can also match 'green rainbow'. Boolean properties can be searched for via the property name prefixed by 'is:' or 'not:'. Boolean property names are: 'Can Be Default', 'Starred', 'Has Tab Match', 'Del', 'Prev', and 'Done'.">
         <div class="row">
           <span id="copy-text" class="button"
                 title="Copy visible table in text format. This is affected by the visibility of ouput; i.e. toggling 'Show all details' affects what will be copied.">
-            <i class="icon copy-white-icon"></i>
+            <i class="icon copy-icon"></i>
             <span>Copy as text</span>
           </span>
         </div>
         <div class="row">
-          <span id="copy-json" class="button"
-                title="Copy responses in JSON format. This is not affected by the visibility of output and will copy responses in their entirety.">
-            <i class="icon copy-black-icon"></i>
-            <span>Copy as JSON</span>
+          <span id="download-json" class="button"
+                title="Download responses in JSON format. This is not affected by the visibility of output and will include responses in their entirety as well as query and display inputs.">
+            <i class="icon copy-icon"></i>
+            <span>Download as JSON</span>
           </span>
         </div>
-        <!--TODO(manukh): import button-->
+        <div class="row">
+          <label id="import-json" class="icon-button button drag-container"
+                 title="Import JSON">
+            <input id="import-json-input" type="file" accept=".json">
+            <i class="icon copy-icon"></i>
+            <span>Import JSON</span>
+          </label>
+        </div>
+        <div id="imported-warning" class="row" hidden>
+          <span class="warning-text"
+                title="The output you are currently viewing was imported and may not be the same as if you had entered these inputs now.">
+            Viewing imported data
+          </span>
+        </div>
       </div>
     </div>
   </template>
diff --git a/chrome/browser/resources/omnibox/omnibox.js b/chrome/browser/resources/omnibox/omnibox.js
index 2190eab..4eade986 100644
--- a/chrome/browser/resources/omnibox/omnibox.js
+++ b/chrome/browser/resources/omnibox/omnibox.js
@@ -17,69 +17,152 @@
  * are available, the Javascript formats them and displays them.
  */
 
-(function () {
-  class BrowserProxy {
-    /** @param {!omnibox_output.OmniboxOutput} omniboxOutput */
-    constructor(omniboxOutput) {
-      /** @private {!mojom.OmniboxPageCallbackRouter} */
-      this.callbackRouter_ = new mojom.OmniboxPageCallbackRouter;
+(function() {
+/**
+ * @typedef {{
+ *   queryInputs: QueryInputs,
+ *   displayInputs: DisplayInputs,
+ *   responses: !Array<!mojom.OmniboxResult>,
+ * }}
+ */
+let OmniboxExport;
 
-      // TODO (manukh) rename method to handleNewAutocompleteResponse in order
-      // to keep terminology consistent. Result refers to a single autocomplete
-      // match. Response refers to the data returned from the C++
-      // AutocompleteController.
-      this.callbackRouter_.handleNewAutocompleteResult.addListener(
-          omniboxOutput.addAutocompleteResponse.bind(omniboxOutput));
-      this.callbackRouter_.handleAnswerImageData.addListener(
-          omniboxOutput.updateAnswerImage.bind(omniboxOutput));
+class BrowserProxy {
+  /** @param {!omnibox_output.OmniboxOutput} omniboxOutput */
+  constructor(omniboxOutput) {
+    /** @private {!mojom.OmniboxPageCallbackRouter} */
+    this.callbackRouter_ = new mojom.OmniboxPageCallbackRouter;
 
-      /** @private {!mojom.OmniboxPageHandlerProxy} */
-      this.handler_ = mojom.OmniboxPageHandler.getProxy();
-      this.handler_.setClientPage(this.callbackRouter_.createProxy());
+    // TODO (manukh) rename method to handleNewAutocompleteResponse in order
+    // to keep terminology consistent. Result refers to a single autocomplete
+    // match. Response refers to the data returned from the C++
+    // AutocompleteController.
+    this.callbackRouter_.handleNewAutocompleteResult.addListener(
+        omniboxOutput.addAutocompleteResponse.bind(omniboxOutput));
+    this.callbackRouter_.handleAnswerImageData.addListener(
+        omniboxOutput.updateAnswerImage.bind(omniboxOutput));
 
-      /**
-       * @type {function(string, boolean, number, boolean, boolean, boolean,
-       *     string, number)}
-       */
-      this.makeRequest = this.handler_.startOmniboxQuery.bind(this.handler_);
-    }
+    /** @private {!mojom.OmniboxPageHandlerProxy} */
+    this.handler_ = mojom.OmniboxPageHandler.getProxy();
+    this.handler_.setClientPage(this.callbackRouter_.createProxy());
+
+    /**
+     * @type {function(string, boolean, number, boolean, boolean, boolean,
+     *     string, number)}
+     */
+    this.makeRequest = this.handler_.startOmniboxQuery.bind(this.handler_);
+  }
+}
+
+/** @type {!BrowserProxy} */
+let browserProxy;
+/** @type {!OmniboxInput} */
+let omniboxInput;
+/** @type {!omnibox_output.OmniboxOutput} */
+let omniboxOutput;
+/** @type {!ExportDelegate} */
+let exportDelegate;
+
+document.addEventListener('DOMContentLoaded', () => {
+  omniboxInput = /** @type {!OmniboxInput} */ ($('omnibox-input'));
+  omniboxOutput =
+      /** @type {!omnibox_output.OmniboxOutput} */ ($('omnibox-output'));
+  browserProxy = new BrowserProxy(omniboxOutput);
+  exportDelegate = new ExportDelegate(omniboxOutput, omniboxInput);
+
+  omniboxInput.addEventListener('query-inputs-changed', event => {
+    omniboxOutput.clearAutocompleteResponses();
+    omniboxOutput.updateQueryInputs(event.detail);
+    browserProxy.makeRequest(
+        event.detail.inputText, event.detail.resetAutocompleteController,
+        event.detail.cursorPosition, event.detail.zeroSuggest,
+        event.detail.preventInlineAutocomplete, event.detail.preferKeyword,
+        event.detail.currentUrl, event.detail.pageClassification);
+  });
+  omniboxInput.addEventListener(
+      'display-inputs-changed',
+      event => omniboxOutput.updateDisplayInputs(event.detail));
+  omniboxInput.addEventListener(
+      'filter-input-changed',
+      event => omniboxOutput.updateFilterText(event.detail));
+
+  omniboxInput.addEventListener(
+      'import-json', event => exportDelegate.importJson(event.detail));
+  omniboxInput.addEventListener('copy-text', () => exportDelegate.copyText());
+  omniboxInput.addEventListener(
+      'download-json', () => exportDelegate.downloadJson());
+});
+
+class ExportDelegate {
+  /**
+   * @param {!omnibox_output.OmniboxOutput} omniboxOutput
+   * @param {!OmniboxInput} omniboxInput
+   */
+  constructor(omniboxOutput, omniboxInput) {
+    /** @private {!OmniboxInput} */
+    this.omniboxInput_ = omniboxInput;
+    /** @private {!omnibox_output.OmniboxOutput} */
+    this.omniboxOutput_ = omniboxOutput;
   }
 
-  /** @type {!BrowserProxy} */
-  let browserProxy;
-  /** @type {!OmniboxInput} */
-  let omniboxInput;
-  /** @type {!omnibox_output.OmniboxOutput} */
-  let omniboxOutput;
+  /** @param {OmniboxExport} importData */
+  importJson(importData) {
+    // This is the minimum validation required to ensure no console errors.
+    // Invalid importData that passes validation will be processed with a
+    // best-attempt; e.g. if responses are missing 'relevance' values, then
+    // those cells will be left blank.
+    const valid = importData && importData.queryInputs &&
+        importData.displayInputs && Array.isArray(importData.responses) &&
+        importData.responses.every(
+            response => Array.isArray(response.combinedResults) &&
+                Array.isArray(response.resultsByProvider));
+    if (!valid) {
+      return console.error(
+          'invalid import format:',
+          'expected {queryInputs: {}, displayInputs: {}, responses: []}');
+    }
+    this.omniboxInput_.queryInputs = importData.queryInputs;
+    this.omniboxInput_.displayInputs = importData.displayInputs;
+    this.omniboxOutput_.updateQueryInputs(importData.queryInputs);
+    this.omniboxOutput_.updateDisplayInputs(importData.displayInputs);
+    this.omniboxOutput_.setAutocompleteResponses(importData.responses);
+  }
 
-  document.addEventListener('DOMContentLoaded', () => {
-    omniboxInput = /** @type {!OmniboxInput} */ ($('omnibox-input'));
-    omniboxOutput =
-        /** @type {!omnibox_output.OmniboxOutput} */ ($('omnibox-output'));
-    browserProxy = new BrowserProxy(omniboxOutput);
+  copyText() {
+    ExportDelegate.copy_(this.omniboxOutput_.visibleTableText);
+  }
 
-    omniboxInput.addEventListener('query-inputs-changed', event => {
-      omniboxOutput.clearAutocompleteResponses();
-      omniboxOutput.updateQueryInputs(event.detail);
-      browserProxy.makeRequest(
-          event.detail.inputText,
-          event.detail.resetAutocompleteController,
-          event.detail.cursorPosition,
-          event.detail.zeroSuggest,
-          event.detail.preventInlineAutocomplete,
-          event.detail.preferKeyword,
-          event.detail.currentUrl,
-          event.detail.pageClassification);
-    });
-    omniboxInput.addEventListener(
-        'display-inputs-changed',
-        event => omniboxOutput.updateDisplayInputs(event.detail));
-    omniboxInput.addEventListener(
-        'filter-input-changed',
-        event => omniboxOutput.updateFilterText(event.detail));
-    omniboxInput.addEventListener('copy-request', event => {
-      event.detail === 'text' ? omniboxOutput.copyDelegate.copyTextOutput() :
-                                omniboxOutput.copyDelegate.copyJsonOutput();
-    });
-  });
+  downloadJson() {
+    /** @type {OmniboxExport} */
+    const exportObj = {
+      queryInputs: this.omniboxInput_.queryInputs,
+      displayInputs: this.omniboxInput_.displayInputs,
+      responses: this.omniboxOutput_.responses,
+    };
+    const fileName = `omnibox_debug_export_${exportObj.queryInputs.inputText}_${
+        new Date().toISOString()}.json`;
+    ExportDelegate.download_(exportObj, fileName);
+  }
+
+  /** @private @param {string} value */
+  static copy_(value) {
+    navigator.clipboard.writeText(value).catch(
+        error => console.error('unable to copy to clipboard:', error));
+  }
+
+  /**
+   * @private
+   * @param {Object} object
+   * @param {string} fileName
+   */
+  static download_(object, fileName) {
+    const content = JSON.stringify(object, null, 2);
+    const blob = new Blob([content], {type: 'application/json'});
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = fileName;
+    a.click();
+  }
+}
 })();
diff --git a/chrome/browser/resources/omnibox/omnibox_input.js b/chrome/browser/resources/omnibox/omnibox_input.js
index 8faaa89..a3aee3c 100644
--- a/chrome/browser/resources/omnibox/omnibox_input.js
+++ b/chrome/browser/resources/omnibox/omnibox_input.js
@@ -6,9 +6,12 @@
  * @typedef {{
  *   inputText: string,
  *   resetAutocompleteController: boolean,
+ *   cursorLock: boolean,
  *   cursorPosition: number,
+ *   zeroSuggest: boolean,
  *   preventInlineAutocomplete: boolean,
  *   preferKeyword: boolean,
+ *   currentUrl: string,
  *   pageClassification: number,
  * }}
  */
@@ -26,8 +29,52 @@
 class OmniboxInput extends OmniboxElement {
   constructor() {
     super('omnibox-input-template');
+    this.displayInputs = OmniboxInput.defaultDisplayInputs;
+  }
 
-    const displayInputs = OmniboxInput.defaultDisplayInputs;
+  /** @return {QueryInputs} */
+  get queryInputs() {
+    return {
+      inputText: this.$$('#input-text').value,
+      resetAutocompleteController:
+          this.$$('#reset-autocomplete-controller').checked,
+      cursorLock: this.$$('#lock-cursor-position').checked,
+      cursorPosition: this.cursorPosition_,
+      zeroSuggest: this.$$('#zero-suggest').checked,
+      preventInlineAutocomplete:
+          this.$$('#prevent-inline-autocomplete').checked,
+      preferKeyword: this.$$('#prefer-keyword').checked,
+      currentUrl: this.$$('#current-url').value,
+      pageClassification: this.$$('#page-classification').value,
+    };
+  }
+
+  /** @param {QueryInputs} queryInputs */
+  set queryInputs(queryInputs) {
+    this.$$('#input-text').value = queryInputs.inputText;
+    this.$$('#reset-autocomplete-controller').checked =
+        queryInputs.resetAutocompleteController;
+    this.$$('#lock-cursor-position').checked = queryInputs.cursorLock;
+    this.cursorPosition_ = queryInputs.cursorPosition;
+    this.$$('#zero-suggest').checked = queryInputs.zeroSuggest;
+    this.$$('#prevent-inline-autocomplete').checked =
+        queryInputs.preventInlineAutocomplete;
+    this.$$('#prefer-keyword').checked = queryInputs.preferKeyword;
+    this.$$('#current-url').value = queryInputs.currentUrl;
+    this.$$('#page-classification').value = queryInputs.pageClassification;
+  }
+
+  /** @return {DisplayInputs} */
+  get displayInputs() {
+    return {
+      showIncompleteResults: this.$$('#show-incomplete-results').checked,
+      showDetails: this.$$('#show-details').checked,
+      showAllProviders: this.$$('#show-all-providers').checked,
+    };
+  }
+
+  /** @param {DisplayInputs} displayInputs */
+  set displayInputs(displayInputs) {
     this.$$('#show-incomplete-results').checked =
         displayInputs.showIncompleteResults;
     this.$$('#show-details').checked = displayInputs.showDetails;
@@ -62,59 +109,58 @@
             query => this.$$(query).addEventListener(
                 'input', this.onDisplayInputsChanged_.bind(this)));
 
-    this.$$('#copy-text')
-        .addEventListener('click', () => this.onCopyOutput_('text'));
-    this.$$('#copy-json')
-        .addEventListener('click', () => this.onCopyOutput_('json'));
-
     this.$$('#filter-text')
         .addEventListener('input', this.onFilterInputsChanged_.bind(this));
 
+    this.$$('#copy-text')
+        .addEventListener('click', this.onCopyText_.bind(this));
+    this.$$('#download-json')
+        .addEventListener('click', this.onDownloadJson_.bind(this));
+    this.setupDragListeners_(this.$$('#import-json'));
+    this.$$('#import-json')
+        .addEventListener('drop', this.onImportDropped_.bind(this));
+    this.$$('#import-json-input')
+        .addEventListener('input', this.onImportFileSelected_.bind(this));
+
     // Set text of .arrow-padding to substring of #input-text text, from
     // beginning until cursor position, in order to correctly align .arrow-up.
     this.$$('#input-text')
         .addEventListener(
-            'input',
-            () => this.$$('.arrow-padding').textContent =
-                this.$$('#input-text')
-                    .value.substring(0, this.cursorPosition_));
+            'input', this.positionCursorPositionIndicators_.bind(this));
+  }
+
+  /**
+   * Sets up boilerplate event listeners for an element that is able to receive
+   * drag events.
+   * @private @param {!Element} element
+   */
+  setupDragListeners_(element) {
+    element.addEventListener(
+        'dragenter', () => element.classList.add('drag-hover'));
+    element.addEventListener(
+        'dragleave', () => element.classList.remove('drag-hover'));
+    element.addEventListener('dragover', e => e.preventDefault());
+    element.addEventListener('drop', e => {
+      e.preventDefault();
+      element.classList.remove('drag-hover');
+    });
   }
 
   /** @private */
   onQueryInputsChanged_() {
-    const zeroSuggest = this.$$('#zero-suggest').checked;
-    this.$$('#current-url').disabled = zeroSuggest;
-    if (zeroSuggest) {
+    this.$$('#imported-warning').hidden = true;
+    this.$$('#current-url').disabled = this.$$('#zero-suggest').checked;
+    if (this.$$('#zero-suggest').checked) {
       this.$$('#current-url').value = this.$$('#input-text').value;
     }
-
-    /** @type {!QueryInputs} */
-    const queryInputs = {
-      inputText: this.$$('#input-text').value,
-      resetAutocompleteController:
-          this.$$('#reset-autocomplete-controller').checked,
-      cursorPosition: this.cursorPosition_,
-      zeroSuggest: zeroSuggest,
-      preventInlineAutocomplete:
-          this.$$('#prevent-inline-autocomplete').checked,
-      preferKeyword: this.$$('#prefer-keyword').checked,
-      currentUrl: this.$$('#current-url').value,
-      pageClassification: this.$$('#page-classification').value,
-    };
     this.dispatchEvent(
-        new CustomEvent('query-inputs-changed', {detail: queryInputs}));
+        new CustomEvent('query-inputs-changed', {detail: this.queryInputs}));
   }
 
   /** @private */
   onDisplayInputsChanged_() {
-    /** @type {!DisplayInputs} */
-    const displayInputs = {
-      showIncompleteResults: this.$$('#show-incomplete-results').checked,
-      showDetails: this.$$('#show-details').checked,
-      showAllProviders: this.$$('#show-all-providers').checked,
-    };
-    this.dispatchEvent(
-        new CustomEvent('display-inputs-changed', {detail: displayInputs}));
+    this.dispatchEvent(new CustomEvent(
+        'display-inputs-changed', {detail: this.displayInputs}));
   }
 
   /** @private */
@@ -123,9 +169,53 @@
         'filter-input-changed', {detail: this.$$('#filter-text').value}));
   }
 
-  /** @private @param {string} format Either 'text' or 'json'. */
-  onCopyOutput_(format) {
-    this.dispatchEvent(new CustomEvent('copy-request', {detail: format}));
+  /** @private */
+  onCopyText_() {
+    this.dispatchEvent(new CustomEvent('copy-text'));
+  }
+
+  /** @private */
+  onDownloadJson_() {
+    this.dispatchEvent(new CustomEvent('download-json'));
+  }
+
+  /** @private @param {!Event} event */
+  onImportDropped_(event) {
+    const dragText = event.dataTransfer.getData('Text');
+    if (dragText) {
+      this.import_(dragText);
+    } else if (event.dataTransfer.files[0]) {
+      this.importFile_(event.dataTransfer.files[0]);
+    }
+  }
+
+  /** @private @param {!Event} event */
+  onImportFileSelected_(event) {
+    this.importFile_(event.target.files[0]);
+  }
+
+  /** @private @param {!File} file */
+  importFile_(file) {
+    const reader = new FileReader();
+    reader.onloadend = () => {
+      if (reader.readyState === FileReader.DONE) {
+        this.import_(/** @type {string} */ (reader.result));
+      } else {
+        console.error('error importing, unable to read file:', reader.error);
+      }
+    };
+    reader.readAsText(file);
+  }
+
+  /** @private @param {string} importString */
+  import_(importString) {
+    try {
+      const importData = JSON.parse(importString);
+      this.$$('#imported-warning').hidden = false;
+      this.dispatchEvent(new CustomEvent('import-json', {detail: importData}));
+    } catch (error) {
+      console.error('error during import, invalid json:', error);
+    }
   }
 
   /** @private @return {number} */
@@ -135,6 +225,18 @@
         this.$$('#input-text').selectionEnd;
   }
 
+  /** @private @param {number} value */
+  set cursorPosition_(value) {
+    this.$$('#input-text').setSelectionRange(value, value);
+    this.positionCursorPositionIndicators_();
+  }
+
+  /** @private */
+  positionCursorPositionIndicators_() {
+    this.$$('.arrow-padding').textContent =
+        this.$$('#input-text').value.substring(0, this.cursorPosition_);
+  }
+
   /** @return {DisplayInputs} */
   static get defaultDisplayInputs() {
     return {
diff --git a/chrome/browser/resources/omnibox/omnibox_output.js b/chrome/browser/resources/omnibox/omnibox_output.js
index 9524d14..4695615 100644
--- a/chrome/browser/resources/omnibox/omnibox_output.js
+++ b/chrome/browser/resources/omnibox/omnibox_output.js
@@ -25,9 +25,6 @@
     constructor() {
       super('omnibox-output-template');
 
-      /** @type {!CopyDelegate} */
-      this.copyDelegate = new CopyDelegate(this);
-
       /** @type {!Array<!mojom.OmniboxResult>} */
       this.responses = [];
       /** @private {!Array<!OutputResultsGroup>} */
@@ -76,6 +73,12 @@
       this.updateFilterHighlights_();
     }
 
+    /** @param {!Array<!mojom.OmniboxResult>} responses */
+    setAutocompleteResponses(responses) {
+      this.clearAutocompleteResponses();
+      responses.forEach(this.addAutocompleteResponse.bind(this));
+    }
+
     /**
      * @param {string} url
      * @param {string} data
@@ -769,32 +772,6 @@
     }
   }
 
-  /** Responsible for setting clipboard contents. */
-  class CopyDelegate {
-    /** @param {!omnibox_output.OmniboxOutput} omniboxOutput */
-    constructor(omniboxOutput) {
-      /** @private {!omnibox_output.OmniboxOutput} */
-      this.omniboxOutput_ = omniboxOutput;
-    }
-
-    copyTextOutput() {
-      this.copy_(this.omniboxOutput_.visibleTableText);
-    }
-
-    copyJsonOutput() {
-      this.copy_(JSON.stringify(this.omniboxOutput_.responses, null, 2));
-    }
-
-    /**
-     * @private
-     * @param {string} value
-     */
-    copy_(value) {
-      navigator.clipboard.writeText(value).catch(
-          error => console.error('unable to copy to clipboard:', error));
-    }
-  }
-
   /** Responsible for highlighting and hiding rows using filter text. */
   class FilterUtil {
     /**