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