| OLD | NEW |
| (Empty) |
| 1 // Copyright 2016 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 Components.JavaScriptAutocomplete = {}; | |
| 6 | |
| 7 /** @typedef {{title:(string|undefined), items:Array<string>}} */ | |
| 8 Components.JavaScriptAutocomplete.CompletionGroup; | |
| 9 | |
| 10 /** | |
| 11 * @param {string} text | |
| 12 * @param {string} query | |
| 13 * @param {boolean=} force | |
| 14 * @return {!Promise<!UI.SuggestBox.Suggestions>} | |
| 15 */ | |
| 16 Components.JavaScriptAutocomplete.completionsForTextInCurrentContext = function(
text, query, force) { | |
| 17 var clippedExpression = Components.JavaScriptAutocomplete._clipExpression(text
, true); | |
| 18 var mapCompletionsPromise = Components.JavaScriptAutocomplete._mapCompletions(
text, query); | |
| 19 return Components.JavaScriptAutocomplete.completionsForExpression(clippedExpre
ssion, query, force) | |
| 20 .then(completions => mapCompletionsPromise.then(mapCompletions => mapCompl
etions.concat(completions))); | |
| 21 }; | |
| 22 | |
| 23 /** | |
| 24 * @param {string} text | |
| 25 * @param {boolean=} allowEndingBracket | |
| 26 * @return {string} | |
| 27 */ | |
| 28 Components.JavaScriptAutocomplete._clipExpression = function(text, allowEndingBr
acket) { | |
| 29 var index; | |
| 30 var stopChars = new Set('=:({;,!+-*/&|^<>`'.split('')); | |
| 31 var whiteSpaceChars = new Set(' \r\n\t'.split('')); | |
| 32 var continueChars = new Set('[. \r\n\t'.split('')); | |
| 33 | |
| 34 for (index = text.length - 1; index >= 0; index--) { | |
| 35 if (stopChars.has(text.charAt(index))) | |
| 36 break; | |
| 37 if (whiteSpaceChars.has(text.charAt(index)) && !continueChars.has(text.charA
t(index - 1))) | |
| 38 break; | |
| 39 } | |
| 40 var clippedExpression = text.substring(index + 1).trim(); | |
| 41 var bracketCount = 0; | |
| 42 | |
| 43 index = clippedExpression.length - 1; | |
| 44 while (index >= 0) { | |
| 45 var character = clippedExpression.charAt(index); | |
| 46 if (character === ']') | |
| 47 bracketCount++; | |
| 48 // Allow an open bracket at the end for property completion. | |
| 49 if (character === '[' && (index < clippedExpression.length - 1 || !allowEndi
ngBracket)) { | |
| 50 bracketCount--; | |
| 51 if (bracketCount < 0) | |
| 52 break; | |
| 53 } | |
| 54 index--; | |
| 55 } | |
| 56 return clippedExpression.substring(index + 1).trim(); | |
| 57 }; | |
| 58 | |
| 59 /** | |
| 60 * @param {string} text | |
| 61 * @param {string} query | |
| 62 * @return {!Promise<!UI.SuggestBox.Suggestions>} | |
| 63 */ | |
| 64 Components.JavaScriptAutocomplete._mapCompletions = function(text, query) { | |
| 65 var mapMatch = text.match(/\.\s*(get|set|delete)\s*\(\s*$/); | |
| 66 var executionContext = UI.context.flavor(SDK.ExecutionContext); | |
| 67 if (!executionContext || !mapMatch) | |
| 68 return Promise.resolve([]); | |
| 69 | |
| 70 var clippedExpression = Components.JavaScriptAutocomplete._clipExpression(text
.substring(0, mapMatch.index)); | |
| 71 var fulfill; | |
| 72 var promise = new Promise(x => fulfill = x); | |
| 73 executionContext.evaluate(clippedExpression, 'completion', true, true, false,
false, false, evaluated); | |
| 74 return promise; | |
| 75 | |
| 76 /** | |
| 77 * @param {?SDK.RemoteObject} result | |
| 78 * @param {!Protocol.Runtime.ExceptionDetails=} exceptionDetails | |
| 79 */ | |
| 80 function evaluated(result, exceptionDetails) { | |
| 81 if (!result || !!exceptionDetails || result.subtype !== 'map') { | |
| 82 fulfill([]); | |
| 83 return; | |
| 84 } | |
| 85 result.getOwnPropertiesPromise(false).then(extractEntriesProperty); | |
| 86 } | |
| 87 | |
| 88 /** | |
| 89 * @param {!{properties: ?Array<!SDK.RemoteObjectProperty>, internalProperties
: ?Array<!SDK.RemoteObjectProperty>}} properties | |
| 90 */ | |
| 91 function extractEntriesProperty(properties) { | |
| 92 var internalProperties = properties.internalProperties || []; | |
| 93 var entriesProperty = internalProperties.find(property => property.name ===
'[[Entries]]'); | |
| 94 if (!entriesProperty) { | |
| 95 fulfill([]); | |
| 96 return; | |
| 97 } | |
| 98 entriesProperty.value.callFunctionJSONPromise(getEntries).then(keysObj => go
tKeys(Object.keys(keysObj))); | |
| 99 } | |
| 100 | |
| 101 /** | |
| 102 * @suppressReceiverCheck | |
| 103 * @this {!Array<{key:?, value:?}>} | |
| 104 * @return {!Object} | |
| 105 */ | |
| 106 function getEntries() { | |
| 107 var result = {__proto__: null}; | |
| 108 for (var i = 0; i < this.length; i++) { | |
| 109 if (typeof this[i].key === 'string') | |
| 110 result[this[i].key] = true; | |
| 111 } | |
| 112 return result; | |
| 113 } | |
| 114 | |
| 115 /** | |
| 116 * @param {!Array<string>} rawKeys | |
| 117 */ | |
| 118 function gotKeys(rawKeys) { | |
| 119 var caseSensitivePrefix = []; | |
| 120 var caseInsensitivePrefix = []; | |
| 121 var caseSensitiveAnywhere = []; | |
| 122 var caseInsensitiveAnywhere = []; | |
| 123 var quoteChar = '"'; | |
| 124 if (query.startsWith('\'')) | |
| 125 quoteChar = '\''; | |
| 126 var endChar = ')'; | |
| 127 if (mapMatch[0].indexOf('set') !== -1) | |
| 128 endChar = ', '; | |
| 129 | |
| 130 var sorter = rawKeys.length < 1000 ? String.naturalOrderComparator : undefin
ed; | |
| 131 var keys = rawKeys.sort(sorter).map(key => quoteChar + key + quoteChar); | |
| 132 | |
| 133 for (var key of keys) { | |
| 134 if (key.length < query.length) | |
| 135 continue; | |
| 136 if (query.length && key.toLowerCase().indexOf(query.toLowerCase()) === -1) | |
| 137 continue; | |
| 138 // Substitute actual newlines with newline characters. @see crbug.com/4984
21 | |
| 139 var title = key.split('\n').join('\\n'); | |
| 140 var text = title + endChar; | |
| 141 | |
| 142 if (key.startsWith(query)) | |
| 143 caseSensitivePrefix.push({text: text, title: title, priority: 4}); | |
| 144 else if (key.toLowerCase().startsWith(query.toLowerCase())) | |
| 145 caseInsensitivePrefix.push({text: text, title: title, priority: 3}); | |
| 146 else if (key.indexOf(query) !== -1) | |
| 147 caseSensitiveAnywhere.push({text: text, title: title, priority: 2}); | |
| 148 else | |
| 149 caseInsensitiveAnywhere.push({text: text, title: title, priority: 1}); | |
| 150 } | |
| 151 var suggestions = caseSensitivePrefix.concat(caseInsensitivePrefix, caseSens
itiveAnywhere, caseInsensitiveAnywhere); | |
| 152 if (suggestions.length) | |
| 153 suggestions[0].subtitle = Common.UIString('Keys'); | |
| 154 fulfill(suggestions); | |
| 155 } | |
| 156 }; | |
| 157 | |
| 158 /** | |
| 159 * @param {string} expressionString | |
| 160 * @param {string} query | |
| 161 * @param {boolean=} force | |
| 162 * @return {!Promise<!UI.SuggestBox.Suggestions>} | |
| 163 */ | |
| 164 Components.JavaScriptAutocomplete.completionsForExpression = function(expression
String, query, force) { | |
| 165 var executionContext = UI.context.flavor(SDK.ExecutionContext); | |
| 166 if (!executionContext) | |
| 167 return Promise.resolve([]); | |
| 168 | |
| 169 var lastIndex = expressionString.length - 1; | |
| 170 | |
| 171 var dotNotation = (expressionString[lastIndex] === '.'); | |
| 172 var bracketNotation = (expressionString.length > 1 && expressionString[lastInd
ex] === '['); | |
| 173 | |
| 174 if (dotNotation || bracketNotation) | |
| 175 expressionString = expressionString.substr(0, lastIndex); | |
| 176 else | |
| 177 expressionString = ''; | |
| 178 | |
| 179 // User is entering float value, do not suggest anything. | |
| 180 if ((expressionString && !isNaN(expressionString)) || (!expressionString && qu
ery && !isNaN(query))) | |
| 181 return Promise.resolve([]); | |
| 182 | |
| 183 | |
| 184 if (!query && !expressionString && !force) | |
| 185 return Promise.resolve([]); | |
| 186 | |
| 187 var fulfill; | |
| 188 var promise = new Promise(x => fulfill = x); | |
| 189 var selectedFrame = executionContext.debuggerModel.selectedCallFrame(); | |
| 190 if (!expressionString && selectedFrame) | |
| 191 variableNamesInScopes(selectedFrame, receivedPropertyNames); | |
| 192 else | |
| 193 executionContext.evaluate(expressionString, 'completion', true, true, false,
false, false, evaluated); | |
| 194 | |
| 195 return promise; | |
| 196 /** | |
| 197 * @param {?SDK.RemoteObject} result | |
| 198 * @param {!Protocol.Runtime.ExceptionDetails=} exceptionDetails | |
| 199 */ | |
| 200 function evaluated(result, exceptionDetails) { | |
| 201 if (!result || !!exceptionDetails) { | |
| 202 fulfill([]); | |
| 203 return; | |
| 204 } | |
| 205 | |
| 206 /** | |
| 207 * @param {?SDK.RemoteObject} object | |
| 208 * @return {!Promise<?SDK.RemoteObject>} | |
| 209 */ | |
| 210 function extractTarget(object) { | |
| 211 if (!object) | |
| 212 return Promise.resolve(/** @type {?SDK.RemoteObject} */ (null)); | |
| 213 if (object.type !== 'object' || object.subtype !== 'proxy') | |
| 214 return Promise.resolve(/** @type {?SDK.RemoteObject} */ (object)); | |
| 215 return object.getOwnPropertiesPromise(false /* generatePreview */) | |
| 216 .then(extractTargetFromProperties) | |
| 217 .then(extractTarget); | |
| 218 } | |
| 219 | |
| 220 /** | |
| 221 * @param {!{properties: ?Array<!SDK.RemoteObjectProperty>, internalProperti
es: ?Array<!SDK.RemoteObjectProperty>}} properties | |
| 222 * @return {?SDK.RemoteObject} | |
| 223 */ | |
| 224 function extractTargetFromProperties(properties) { | |
| 225 var internalProperties = properties.internalProperties || []; | |
| 226 var target = internalProperties.find(property => property.name === '[[Targ
et]]'); | |
| 227 return target ? target.value : null; | |
| 228 } | |
| 229 | |
| 230 /** | |
| 231 * @param {string=} type | |
| 232 * @return {!Object} | |
| 233 * @suppressReceiverCheck | |
| 234 * @this {Object} | |
| 235 */ | |
| 236 function getCompletions(type) { | |
| 237 var object; | |
| 238 if (type === 'string') | |
| 239 object = new String(''); | |
| 240 else if (type === 'number') | |
| 241 object = new Number(0); | |
| 242 else if (type === 'boolean') | |
| 243 object = new Boolean(false); | |
| 244 else | |
| 245 object = this; | |
| 246 | |
| 247 var result = []; | |
| 248 try { | |
| 249 for (var o = object; o; o = Object.getPrototypeOf(o)) { | |
| 250 if ((type === 'array' || type === 'typedarray') && o === object && Arr
ayBuffer.isView(o) && o.length > 9999) | |
| 251 continue; | |
| 252 | |
| 253 var group = {items: [], __proto__: null}; | |
| 254 try { | |
| 255 if (typeof o === 'object' && o.constructor && o.constructor.name) | |
| 256 group.title = o.constructor.name; | |
| 257 } catch (ee) { | |
| 258 // we could break upon cross origin check. | |
| 259 } | |
| 260 result[result.length] = group; | |
| 261 var names = Object.getOwnPropertyNames(o); | |
| 262 var isArray = Array.isArray(o); | |
| 263 for (var i = 0; i < names.length; ++i) { | |
| 264 // Skip array elements indexes. | |
| 265 if (isArray && /^[0-9]/.test(names[i])) | |
| 266 continue; | |
| 267 group.items[group.items.length] = names[i]; | |
| 268 } | |
| 269 } | |
| 270 } catch (e) { | |
| 271 } | |
| 272 return result; | |
| 273 } | |
| 274 | |
| 275 /** | |
| 276 * @param {?SDK.RemoteObject} object | |
| 277 */ | |
| 278 function completionsForObject(object) { | |
| 279 if (!object) { | |
| 280 receivedPropertyNames(null); | |
| 281 } else if (object.type === 'object' || object.type === 'function') { | |
| 282 object.callFunctionJSON( | |
| 283 getCompletions, [SDK.RemoteObject.toCallArgument(object.subtype)], r
eceivedPropertyNames); | |
| 284 } else if (object.type === 'string' || object.type === 'number' || object.
type === 'boolean') { | |
| 285 executionContext.evaluate( | |
| 286 '(' + getCompletions + ')("' + result.type + '")', 'completion', fal
se, true, true, false, false, | |
| 287 receivedPropertyNamesFromEval); | |
| 288 } | |
| 289 } | |
| 290 | |
| 291 extractTarget(result).then(completionsForObject); | |
| 292 } | |
| 293 | |
| 294 /** | |
| 295 * @param {!SDK.DebuggerModel.CallFrame} callFrame | |
| 296 * @param {function(!Array<!Components.JavaScriptAutocomplete.CompletionGroup>
)} callback | |
| 297 */ | |
| 298 function variableNamesInScopes(callFrame, callback) { | |
| 299 var result = [{items: ['this']}]; | |
| 300 | |
| 301 /** | |
| 302 * @param {string} name | |
| 303 * @param {?Array<!SDK.RemoteObjectProperty>} properties | |
| 304 */ | |
| 305 function propertiesCollected(name, properties) { | |
| 306 var group = {title: name, items: []}; | |
| 307 result.push(group); | |
| 308 for (var i = 0; properties && i < properties.length; ++i) | |
| 309 group.items.push(properties[i].name); | |
| 310 if (--pendingRequests === 0) | |
| 311 callback(result); | |
| 312 } | |
| 313 | |
| 314 var scopeChain = callFrame.scopeChain(); | |
| 315 var pendingRequests = scopeChain.length; | |
| 316 for (var i = 0; i < scopeChain.length; ++i) { | |
| 317 var scope = scopeChain[i]; | |
| 318 var object = scope.object(); | |
| 319 object.getAllProperties( | |
| 320 false /* accessorPropertiesOnly */, false /* generatePreview */, | |
| 321 propertiesCollected.bind(null, scope.typeName())); | |
| 322 } | |
| 323 } | |
| 324 | |
| 325 /** | |
| 326 * @param {?SDK.RemoteObject} result | |
| 327 * @param {!Protocol.Runtime.ExceptionDetails=} exceptionDetails | |
| 328 */ | |
| 329 function receivedPropertyNamesFromEval(result, exceptionDetails) { | |
| 330 executionContext.target().runtimeAgent().releaseObjectGroup('completion'); | |
| 331 if (result && !exceptionDetails) | |
| 332 receivedPropertyNames(/** @type {!Object} */ (result.value)); | |
| 333 else | |
| 334 fulfill([]); | |
| 335 } | |
| 336 | |
| 337 /** | |
| 338 * @param {?Object} object | |
| 339 */ | |
| 340 function receivedPropertyNames(object) { | |
| 341 executionContext.target().runtimeAgent().releaseObjectGroup('completion'); | |
| 342 if (!object) { | |
| 343 fulfill([]); | |
| 344 return; | |
| 345 } | |
| 346 var propertyGroups = /** @type {!Array<!Components.JavaScriptAutocomplete.Co
mpletionGroup>} */ (object); | |
| 347 var includeCommandLineAPI = (!dotNotation && !bracketNotation); | |
| 348 if (includeCommandLineAPI) { | |
| 349 const commandLineAPI = [ | |
| 350 'dir', | |
| 351 'dirxml', | |
| 352 'keys', | |
| 353 'values', | |
| 354 'profile', | |
| 355 'profileEnd', | |
| 356 'monitorEvents', | |
| 357 'unmonitorEvents', | |
| 358 'inspect', | |
| 359 'copy', | |
| 360 'clear', | |
| 361 'getEventListeners', | |
| 362 'debug', | |
| 363 'undebug', | |
| 364 'monitor', | |
| 365 'unmonitor', | |
| 366 'table', | |
| 367 '$', | |
| 368 '$$', | |
| 369 '$x' | |
| 370 ]; | |
| 371 propertyGroups.push({items: commandLineAPI}); | |
| 372 } | |
| 373 fulfill(Components.JavaScriptAutocomplete._completionsForQuery( | |
| 374 dotNotation, bracketNotation, expressionString, query, propertyGroups)); | |
| 375 } | |
| 376 }; | |
| 377 | |
| 378 /** | |
| 379 * @param {boolean} dotNotation | |
| 380 * @param {boolean} bracketNotation | |
| 381 * @param {string} expressionString | |
| 382 * @param {string} query | |
| 383 * @param {!Array<!Components.JavaScriptAutocomplete.CompletionGroup>} propert
yGroups | |
| 384 * @return {!UI.SuggestBox.Suggestions} | |
| 385 */ | |
| 386 Components.JavaScriptAutocomplete._completionsForQuery = function( | |
| 387 dotNotation, bracketNotation, expressionString, query, propertyGroups) { | |
| 388 if (bracketNotation) { | |
| 389 if (query.length && query[0] === '\'') | |
| 390 var quoteUsed = '\''; | |
| 391 else | |
| 392 var quoteUsed = '"'; | |
| 393 } | |
| 394 | |
| 395 if (!expressionString) { | |
| 396 const keywords = [ | |
| 397 'break', 'case', 'catch', 'continue', 'default', 'delete', 'do',
'else', 'finally', | |
| 398 'for', 'function', 'if', 'in', 'instanceof', 'new', 'return
', 'switch', 'this', | |
| 399 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with' | |
| 400 ]; | |
| 401 propertyGroups.push({title: Common.UIString('keywords'), items: keywords}); | |
| 402 } | |
| 403 | |
| 404 var result = []; | |
| 405 var lastGroupTitle; | |
| 406 for (var group of propertyGroups) { | |
| 407 group.items.sort(itemComparator.bind(null, group.items.length > 1000)); | |
| 408 var caseSensitivePrefix = []; | |
| 409 var caseInsensitivePrefix = []; | |
| 410 var caseSensitiveAnywhere = []; | |
| 411 var caseInsensitiveAnywhere = []; | |
| 412 | |
| 413 for (var property of group.items) { | |
| 414 // Assume that all non-ASCII characters are letters and thus can be used a
s part of identifier. | |
| 415 if (!bracketNotation && !/^[a-zA-Z_$\u008F-\uFFFF][a-zA-Z0-9_$\u008F-\uFFF
F]*$/.test(property)) | |
| 416 continue; | |
| 417 | |
| 418 if (bracketNotation) { | |
| 419 if (!/^[0-9]+$/.test(property)) | |
| 420 property = quoteUsed + property.escapeCharacters(quoteUsed + '\\') + q
uoteUsed; | |
| 421 property += ']'; | |
| 422 } | |
| 423 | |
| 424 if (property.length < query.length) | |
| 425 continue; | |
| 426 if (query.length && property.toLowerCase().indexOf(query.toLowerCase()) ==
= -1) | |
| 427 continue; | |
| 428 // Substitute actual newlines with newline characters. @see crbug.com/4984
21 | |
| 429 var prop = property.split('\n').join('\\n'); | |
| 430 | |
| 431 if (property.startsWith(query)) | |
| 432 caseSensitivePrefix.push({text: prop, priority: 4}); | |
| 433 else if (property.toLowerCase().startsWith(query.toLowerCase())) | |
| 434 caseInsensitivePrefix.push({text: prop, priority: 3}); | |
| 435 else if (property.indexOf(query) !== -1) | |
| 436 caseSensitiveAnywhere.push({text: prop, priority: 2}); | |
| 437 else | |
| 438 caseInsensitiveAnywhere.push({text: prop, priority: 1}); | |
| 439 } | |
| 440 var structuredGroup = | |
| 441 caseSensitivePrefix.concat(caseInsensitivePrefix, caseSensitiveAnywhere,
caseInsensitiveAnywhere); | |
| 442 if (structuredGroup.length && group.title !== lastGroupTitle) { | |
| 443 structuredGroup[0].subtitle = group.title; | |
| 444 lastGroupTitle = group.title; | |
| 445 } | |
| 446 result = result.concat(structuredGroup); | |
| 447 result.forEach(item => { | |
| 448 if (item.text.endsWith(']')) | |
| 449 item.title = item.text.substring(0, item.text.length - 1); | |
| 450 }); | |
| 451 } | |
| 452 return result; | |
| 453 | |
| 454 /** | |
| 455 * @param {boolean} naturalOrder | |
| 456 * @param {string} a | |
| 457 * @param {string} b | |
| 458 * @return {number} | |
| 459 */ | |
| 460 function itemComparator(naturalOrder, a, b) { | |
| 461 var aStartsWithUnderscore = a.startsWith('_'); | |
| 462 var bStartsWithUnderscore = b.startsWith('_'); | |
| 463 if (aStartsWithUnderscore && !bStartsWithUnderscore) | |
| 464 return 1; | |
| 465 if (bStartsWithUnderscore && !aStartsWithUnderscore) | |
| 466 return -1; | |
| 467 return naturalOrder ? String.naturalOrderComparator(a, b) : a.localeCompare(
b); | |
| 468 } | |
| 469 }; | |
| OLD | NEW |