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 */
19cr.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
462document.addEventListener('DOMContentLoaded', omniboxDebug.initialize);
463