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 |