Home | History | Annotate | Download | only in omnibox
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * Javascript for omnibox.html, served from chrome://omnibox/
      7  * This is used to debug omnibox ranking.  The user enters some text
      8  * into a box, submits it, and then sees lots of debug information
      9  * from the autocompleter that shows what omnibox would do with that
     10  * input.
     11  *
     12  * The simple object defined in this javascript file listens for
     13  * certain events on omnibox.html, sends (when appropriate) the
     14  * input text to C++ code to start the omnibox autcomplete controller
     15  * working, and listens from callbacks from the C++ code saying that
     16  * results are available.  When results (possibly intermediate ones)
     17  * are available, the Javascript formats them and displays them.
     18  */
     19 cr.define('omniboxDebug', function() {
     20   'use strict';
     21 
     22   /**
     23    * Register our event handlers.
     24    */
     25   function initialize() {
     26     $('omnibox-input-form').addEventListener(
     27         'submit', startOmniboxQuery, false);
     28     $('prevent-inline-autocomplete').addEventListener(
     29         'change', startOmniboxQuery);
     30     $('prefer-keyword').addEventListener('change', startOmniboxQuery);
     31     $('show-details').addEventListener('change', refresh);
     32     $('show-incomplete-results').addEventListener('change', refresh);
     33     $('show-all-providers').addEventListener('change', refresh);
     34   }
     35 
     36   /**
     37    * @type {Array.<Object>} an array of all autocomplete results we've seen
     38    *     for this query.  We append to this list once for every call to
     39    *     handleNewAutocompleteResult.  For details on the structure of
     40    *     the object inside, see the comments by addResultToOutput.
     41    */
     42   var progressiveAutocompleteResults = [];
     43 
     44   /**
     45    * @type {number} the value for cursor position we sent with the most
     46    *     recent request.  We need to remember this in order to display it
     47    *     in the output; otherwise it's hard or impossible to determine
     48    *     from screen captures or print-to-PDFs.
     49    */
     50   var cursorPositionUsed = -1;
     51 
     52   /**
     53    * Extracts the input text from the text field and sends it to the
     54    * C++ portion of chrome to handle.  The C++ code will iteratively
     55    * call handleNewAutocompleteResult as results come in.
     56    */
     57   function startOmniboxQuery(event) {
     58     // First, clear the results of past calls (if any).
     59     progressiveAutocompleteResults = [];
     60     // Then, call chrome with a four-element list:
     61     // - first element: the value in the text box
     62     // - second element: the location of the cursor in the text box
     63     // - third element: the value of prevent-inline-autocomplete
     64     // - forth element: the value of prefer-keyword
     65     cursorPositionUsed = $('input-text').selectionEnd;
     66     chrome.send('startOmniboxQuery', [
     67         $('input-text').value,
     68         cursorPositionUsed,
     69         $('prevent-inline-autocomplete').checked,
     70         $('prefer-keyword').checked]);
     71     // Cancel the submit action.  i.e., don't submit the form.  (We handle
     72     // display the results solely with Javascript.)
     73     event.preventDefault();
     74   }
     75 
     76   /**
     77    * Returns a simple object with information about how to display an
     78    * autocomplete result data field.
     79    * @param {string} header the label for the top of the column/table.
     80    * @param {string} urlLabelForHeader the URL that the header should point
     81    *     to (if non-empty).
     82    * @param {string} propertyName the name of the property in the autocomplete
     83    *     result record that we lookup.
     84    * @param {boolean} displayAlways whether the property should be displayed
     85    *     regardless of whether we're in detailed more.
     86    * @param {string} tooltip a description of the property that will be
     87    *     presented as a tooltip when the mouse is hovered over the column title.
     88    * @constructor
     89    */
     90   function PresentationInfoRecord(header, url, propertyName, displayAlways,
     91                                   tooltip) {
     92     this.header = header;
     93     this.urlLabelForHeader = url;
     94     this.propertyName = propertyName;
     95     this.displayAlways = displayAlways;
     96     this.tooltip = tooltip;
     97   }
     98 
     99   /**
    100    * A constant that's used to decide what autocomplete result
    101    * properties to output in what order.  This is an array of
    102    * PresentationInfoRecord() objects; for details see that
    103    * function.
    104    * @type {Array.<Object>}
    105    * @const
    106    */
    107   var PROPERTY_OUTPUT_ORDER = [
    108     new PresentationInfoRecord('Provider', '', 'provider_name', true,
    109         'The AutocompleteProvider suggesting this result.'),
    110     new PresentationInfoRecord('Type', '', 'type', true,
    111         'The type of the result.'),
    112     new PresentationInfoRecord('Relevance', '', 'relevance', true,
    113         'The result score. Higher is more relevant.'),
    114     new PresentationInfoRecord('Contents', '', 'contents', true,
    115         'The text that is presented identifying the result.'),
    116     new PresentationInfoRecord('Starred', '', 'starred', false,
    117         'A green checkmark indicates that the result has been bookmarked.'),
    118     new PresentationInfoRecord(
    119         'HWYT', '', 'is_history_what_you_typed_match', false,
    120         'A green checkmark indicates that the result is an History What You ' +
    121         'Typed Match'),
    122     new PresentationInfoRecord('Description', '', 'description', false,
    123         'The page title of the result.'),
    124     new PresentationInfoRecord('URL', '', 'destination_url', true,
    125         'The URL for the result.'),
    126     new PresentationInfoRecord('Fill Into Edit', '', 'fill_into_edit', false,
    127         'The text shown in the omnibox when the result is selected.'),
    128     new PresentationInfoRecord(
    129         'Inline Autocompletion', '', 'inline_autocompletion', false,
    130         'The text shown in the omnibox as a blue highlight selection ' +
    131         'following the cursor, if this match is shown inline.'),
    132     new PresentationInfoRecord('Del', '', 'deletable', false,
    133         'A green checkmark indicates that the results can be deleted from ' +
    134         'the visit history.'),
    135     new PresentationInfoRecord('Prev', '', 'from_previous', false, ''),
    136     new PresentationInfoRecord(
    137         'Tran',
    138         'http://code.google.com/codesearch#OAMlx_jo-ck/src/content/public/' +
    139         'common/page_transition_types.h&exact_package=chromium&l=24',
    140         'transition', false,
    141         'How the user got to the result.'),
    142     new PresentationInfoRecord(
    143         'Done', '', 'provider_done', false,
    144         'A green checkmark indicates that the provider is done looking for ' +
    145         'more results.'),
    146     new PresentationInfoRecord(
    147         'Template URL', '', 'template_url', false, ''),
    148     new PresentationInfoRecord(
    149         'Associated Keyword', '', 'associated_keyword', false,
    150         'If non-empty, a "press tab to search" hint will be shown and will ' +
    151         'engage this keyword.'),
    152     new PresentationInfoRecord(
    153         'Keyword', '', 'keyword', false,
    154         'The keyword of the search engine to be used.'),
    155     new PresentationInfoRecord(
    156         'Additional Info', '', 'additional_info', false,
    157         'Provider-specific information about the result.')
    158   ];
    159 
    160   /**
    161    * Returns an HTML Element of type table row that contains the
    162    * headers we'll use for labeling the columns.  If we're in
    163    * detailed_mode, we use all the headers.  If not, we only use ones
    164    * marked displayAlways.
    165    */
    166   function createAutocompleteResultTableHeader() {
    167     var row = document.createElement('tr');
    168     var inDetailedMode = $('show-details').checked;
    169     for (var i = 0; i < PROPERTY_OUTPUT_ORDER.length; i++) {
    170       if (inDetailedMode || PROPERTY_OUTPUT_ORDER[i].displayAlways) {
    171         var headerCell = document.createElement('th');
    172         if (PROPERTY_OUTPUT_ORDER[i].urlLabelForHeader != '') {
    173           // Wrap header text in URL.
    174           var linkNode = document.createElement('a');
    175           linkNode.href = PROPERTY_OUTPUT_ORDER[i].urlLabelForHeader;
    176           linkNode.textContent = PROPERTY_OUTPUT_ORDER[i].header;
    177           headerCell.appendChild(linkNode);
    178         } else {
    179           // Output header text without a URL.
    180           headerCell.textContent = PROPERTY_OUTPUT_ORDER[i].header;
    181           headerCell.className = 'table-header';
    182           headerCell.title = PROPERTY_OUTPUT_ORDER[i].tooltip;
    183         }
    184         row.appendChild(headerCell);
    185       }
    186     }
    187     return row;
    188   }
    189 
    190   /**
    191    * @param {Object} autocompleteSuggestion the particular autocomplete
    192    *     suggestion we're in the process of displaying.
    193    * @param {string} propertyName the particular property of the autocomplete
    194    *     suggestion that should go in this cell.
    195    * @return {HTMLTableCellElement} that contains the value within this
    196    *     autocompleteSuggestion associated with propertyName.
    197    */
    198   function createCellForPropertyAndRemoveProperty(autocompleteSuggestion,
    199                                                   propertyName) {
    200     var cell = document.createElement('td');
    201     if (propertyName in autocompleteSuggestion) {
    202       if (propertyName == 'additional_info') {
    203         // |additional_info| embeds a two-column table of provider-specific data
    204         // within this cell.
    205         var additionalInfoTable = document.createElement('table');
    206         for (var additionalInfoKey in autocompleteSuggestion[propertyName]) {
    207           var additionalInfoRow = document.createElement('tr');
    208 
    209           // Set the title (name of property) cell text.
    210           var propertyCell = document.createElement('td');
    211           propertyCell.textContent = additionalInfoKey + ':';
    212           propertyCell.className = 'additional-info-property';
    213           additionalInfoRow.appendChild(propertyCell);
    214 
    215           // Set the value of the property cell text.
    216           var valueCell = document.createElement('td');
    217           valueCell.textContent =
    218               autocompleteSuggestion[propertyName][additionalInfoKey];
    219           valueCell.className = 'additional-info-value';
    220           additionalInfoRow.appendChild(valueCell);
    221 
    222           additionalInfoTable.appendChild(additionalInfoRow);
    223         }
    224         cell.appendChild(additionalInfoTable);
    225       } else if (typeof autocompleteSuggestion[propertyName] == 'boolean') {
    226         // If this is a boolean, display a checkmark or an X instead of
    227         // the strings true or false.
    228         if (autocompleteSuggestion[propertyName]) {
    229           cell.className = 'check-mark';
    230           cell.textContent = '';
    231         } else {
    232           cell.className = 'x-mark';
    233           cell.textContent = '';
    234         }
    235       } else {
    236         var text = String(autocompleteSuggestion[propertyName]);
    237         // If it's a URL wrap it in an href.
    238         var re = /^(http|https|ftp|chrome|file):\/\//;
    239         if (re.test(text)) {
    240           var aCell = document.createElement('a');
    241           aCell.textContent = text;
    242           aCell.href = text;
    243           cell.appendChild(aCell);
    244         } else {
    245           // All other data types (integer, strings, etc.) display their
    246           // normal toString() output.
    247           cell.textContent = autocompleteSuggestion[propertyName];
    248         }
    249       }
    250     }  // else: if propertyName is undefined, we leave the cell blank
    251     return cell;
    252   }
    253 
    254   /**
    255    * Called by C++ code when we get an update from the
    256    * AutocompleteController.  We simply append the result to
    257    * progressiveAutocompleteResults and refresh the page.
    258    */
    259   function handleNewAutocompleteResult(result) {
    260     progressiveAutocompleteResults.push(result);
    261     refresh();
    262   }
    263 
    264   /**
    265    * Appends some human-readable information about the provided
    266    * autocomplete result to the HTML node with id omnibox-debug-text.
    267    * The current human-readable form is a few lines about general
    268    * autocomplete result statistics followed by a table with one line
    269    * for each autocomplete match.  The input parameter result is a
    270    * complex Object with lots of information about various
    271    * autocomplete matches.  Here's an example of what it looks like:
    272    * <pre>
    273    * {@code
    274    * {
    275    *   'done': false,
    276    *   'time_since_omnibox_started_ms': 15,
    277    *   'host': 'mai',
    278    *   'is_typed_host': false,
    279    *   'combined_results' : {
    280    *     'num_items': 4,
    281    *     'item_0': {
    282    *       'destination_url': 'http://mail.google.com',
    283    *       'provider_name': 'HistoryURL',
    284    *       'relevance': 1410,
    285    *       ...
    286    *     }
    287    *     'item_1: {
    288    *       ...
    289    *     }
    290    *     ...
    291    *   }
    292    *   'results_by_provider': {
    293    *     'HistoryURL' : {
    294    *       'num_items': 3,
    295    *         ...
    296    *       }
    297    *     'Search' : {
    298    *       'num_items': 1,
    299    *       ...
    300    *     }
    301    *     ...
    302    *   }
    303    * }
    304    * }
    305    * </pre>
    306    * For more information on how the result is packed, see the
    307    * corresponding code in chrome/browser/ui/webui/omnibox_ui.cc
    308    */
    309   function addResultToOutput(result) {
    310     var output = $('omnibox-debug-text');
    311     var inDetailedMode = $('show-details').checked;
    312     var showIncompleteResults = $('show-incomplete-results').checked;
    313     var showPerProviderResults = $('show-all-providers').checked;
    314 
    315     // Always output cursor position.
    316     var p = document.createElement('p');
    317     p.textContent = 'cursor position = ' + cursorPositionUsed;
    318     output.appendChild(p);
    319 
    320     // Output the result-level features in detailed mode and in
    321     // show incomplete results mode.  We do the latter because without
    322     // these result-level features, one can't make sense of each
    323     // batch of results.
    324     if (inDetailedMode || showIncompleteResults) {
    325       var p1 = document.createElement('p');
    326       p1.textContent = 'elapsed time = ' +
    327           result.time_since_omnibox_started_ms + 'ms';
    328       output.appendChild(p1);
    329       var p2 = document.createElement('p');
    330       p2.textContent = 'all providers done = ' + result.done;
    331       output.appendChild(p2);
    332       var p3 = document.createElement('p');
    333       p3.textContent = 'host = ' + result.host;
    334       if ('is_typed_host' in result) {
    335         // Only output the is_typed_host information if available.  (It may
    336         // be missing if the history database lookup failed.)
    337         p3.textContent = p3.textContent + ' has is_typed_host = ' +
    338             result.is_typed_host;
    339       }
    340       output.appendChild(p3);
    341     }
    342 
    343     // Combined results go after the lines below.
    344     var group = document.createElement('a');
    345     group.className = 'group-separator';
    346     group.textContent = 'Combined results.';
    347     output.appendChild(group);
    348 
    349     // Add combined/merged result table.
    350     var p = document.createElement('p');
    351     p.appendChild(addResultTableToOutput(result.combined_results));
    352     output.appendChild(p);
    353 
    354     // Move forward only if you want to display per provider results.
    355     if (!showPerProviderResults) {
    356       return;
    357     }
    358 
    359     // Individual results go after the lines below.
    360     var group = document.createElement('a');
    361     group.className = 'group-separator';
    362     group.textContent = 'Results for individual providers.';
    363     output.appendChild(group);
    364 
    365     // Add the per-provider result tables with labels. We do not append the
    366     // combined/merged result table since we already have the per provider
    367     // results.
    368     for (var provider in result.results_by_provider) {
    369       var results = result.results_by_provider[provider];
    370       // If we have no results we do not display anything.
    371       if (results.num_items == 0) {
    372         continue;
    373       }
    374       var p = document.createElement('p');
    375       p.appendChild(addResultTableToOutput(results));
    376       output.appendChild(p);
    377     }
    378   }
    379 
    380   /**
    381    * @param {Object} result either the combined_results component of
    382    *     the structure described in the comment by addResultToOutput()
    383    *     above or one of the per-provider results in the structure.
    384    *     (Both have the same format).
    385    * @return {HTMLTableCellElement} that is a user-readable HTML
    386    *     representation of this object.
    387    */
    388   function addResultTableToOutput(result) {
    389     var inDetailedMode = $('show-details').checked;
    390     // Create a table to hold all the autocomplete items.
    391     var table = document.createElement('table');
    392     table.className = 'autocomplete-results-table';
    393     table.appendChild(createAutocompleteResultTableHeader());
    394     // Loop over every autocomplete item and add it as a row in the table.
    395     for (var i = 0; i < result.num_items; i++) {
    396       var autocompleteSuggestion = result['item_' + i];
    397       var row = document.createElement('tr');
    398       // Loop over all the columns/properties and output either them
    399       // all (if we're in detailed mode) or only the ones marked displayAlways.
    400       // Keep track of which properties we displayed.
    401       var displayedProperties = {};
    402       for (var j = 0; j < PROPERTY_OUTPUT_ORDER.length; j++) {
    403         if (inDetailedMode || PROPERTY_OUTPUT_ORDER[j].displayAlways) {
    404           row.appendChild(createCellForPropertyAndRemoveProperty(
    405               autocompleteSuggestion, PROPERTY_OUTPUT_ORDER[j].propertyName));
    406           displayedProperties[PROPERTY_OUTPUT_ORDER[j].propertyName] = true;
    407         }
    408       }
    409 
    410       // Now, if we're in detailed mode, add all the properties that
    411       // haven't already been output.  (We know which properties have
    412       // already been output because we delete the property when we output
    413       // it.  The only way we have properties left at this point if
    414       // we're in detailed mode and we're getting back properties
    415       // not listed in PROPERTY_OUTPUT_ORDER.  Perhaps someone added
    416       // something to the C++ code but didn't bother to update this
    417       // Javascript?  In any case, we want to display them.)
    418       if (inDetailedMode) {
    419         for (var key in autocompleteSuggestion) {
    420           if (!displayedProperties[key]) {
    421             var cell = document.createElement('td');
    422             cell.textContent = key + '=' + autocompleteSuggestion[key];
    423             row.appendChild(cell);
    424           }
    425         }
    426       }
    427 
    428       table.appendChild(row);
    429     }
    430     return table;
    431   }
    432 
    433   /* Repaints the page based on the contents of the array
    434    * progressiveAutocompleteResults, which represents consecutive
    435    * autocomplete results.  We only display the last (most recent)
    436    * entry unless we're asked to display incomplete results.  For an
    437    * example of the output, play with chrome://omnibox/
    438    */
    439   function refresh() {
    440     // Erase whatever is currently being displayed.
    441     var output = $('omnibox-debug-text');
    442     output.innerHTML = '';
    443 
    444     if (progressiveAutocompleteResults.length > 0) {  // if we have results
    445       // Display the results.
    446       var showIncompleteResults = $('show-incomplete-results').checked;
    447       var startIndex = showIncompleteResults ? 0 :
    448           progressiveAutocompleteResults.length - 1;
    449       for (var i = startIndex; i < progressiveAutocompleteResults.length; i++) {
    450         addResultToOutput(progressiveAutocompleteResults[i]);
    451       }
    452     }
    453   }
    454 
    455   return {
    456     initialize: initialize,
    457     startOmniboxQuery: startOmniboxQuery,
    458     handleNewAutocompleteResult: handleNewAutocompleteResult
    459   };
    460 });
    461 
    462 document.addEventListener('DOMContentLoaded', omniboxDebug.initialize);
    463