blob: b9aad4f0374aa194fbc76020491617051507f0b3 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Components.JavaScriptAutocomplete = {};
/** @typedef {{title:(string|undefined), items:Array<string>}} */
Components.JavaScriptAutocomplete.CompletionGroup;
/**
* @param {string} text
* @param {string} query
* @param {boolean=} force
* @return {!Promise<!UI.SuggestBox.Suggestions>}
*/
Components.JavaScriptAutocomplete.completionsForTextInCurrentContext = function(text, query, force) {
var index;
var stopChars = new Set('=:({;,!+-*/&|^<>`'.split(''));
var whiteSpaceChars = new Set(' \r\n\t'.split(''));
var continueChars = new Set('[. \r\n\t'.split(''));
for (index = text.length - 1; index >= 0; index--) {
// Pass less stop characters to rangeOfWord so the range will be a more complete expression.
if (stopChars.has(text.charAt(index)))
break;
if (whiteSpaceChars.has(text.charAt(index)) && !continueChars.has(text.charAt(index - 1)))
break;
}
var clippedExpression = text.substring(index + 1).trim();
var bracketCount = 0;
index = clippedExpression.length - 1;
while (index >= 0) {
var character = clippedExpression.charAt(index);
if (character === ']')
bracketCount++;
// Allow an open bracket at the end for property completion.
if (character === '[' && index < clippedExpression.length - 1) {
bracketCount--;
if (bracketCount < 0)
break;
}
index--;
}
clippedExpression = clippedExpression.substring(index + 1).trim();
return Components.JavaScriptAutocomplete.completionsForExpression(clippedExpression, query, force);
};
/**
* @param {string} expressionString
* @param {string} query
* @param {boolean=} force
* @return {!Promise<!UI.SuggestBox.Suggestions>}
*/
Components.JavaScriptAutocomplete.completionsForExpression = function(expressionString, query, force) {
var executionContext = UI.context.flavor(SDK.ExecutionContext);
if (!executionContext)
return Promise.resolve([]);
var lastIndex = expressionString.length - 1;
var dotNotation = (expressionString[lastIndex] === '.');
var bracketNotation = (expressionString.length > 1 && expressionString[lastIndex] === '[');
if (dotNotation || bracketNotation)
expressionString = expressionString.substr(0, lastIndex);
else
expressionString = '';
// User is entering float value, do not suggest anything.
if ((expressionString && !isNaN(expressionString)) || (!expressionString && query && !isNaN(query)))
return Promise.resolve([]);
if (!query && !expressionString && !force)
return Promise.resolve([]);
var fufill;
var promise = new Promise(x => fufill = x);
var selectedFrame = executionContext.debuggerModel.selectedCallFrame();
if (!expressionString && selectedFrame)
variableNamesInScopes(selectedFrame, receivedPropertyNames);
else
executionContext.evaluate(expressionString, 'completion', true, true, false, false, false, evaluated);
return promise;
/**
* @param {?SDK.RemoteObject} result
* @param {!Protocol.Runtime.ExceptionDetails=} exceptionDetails
*/
function evaluated(result, exceptionDetails) {
if (!result || !!exceptionDetails) {
fufill([]);
return;
}
/**
* @param {?SDK.RemoteObject} object
* @return {!Promise<?SDK.RemoteObject>}
*/
function extractTarget(object) {
if (!object)
return Promise.resolve(/** @type {?SDK.RemoteObject} */ (null));
if (object.type !== 'object' || object.subtype !== 'proxy')
return Promise.resolve(/** @type {?SDK.RemoteObject} */ (object));
return object.getOwnPropertiesPromise().then(extractTargetFromProperties).then(extractTarget);
}
/**
* @param {!{properties: ?Array<!SDK.RemoteObjectProperty>, internalProperties: ?Array<!SDK.RemoteObjectProperty>}} properties
* @return {?SDK.RemoteObject}
*/
function extractTargetFromProperties(properties) {
var internalProperties = properties.internalProperties || [];
var target = internalProperties.find(property => property.name === '[[Target]]');
return target ? target.value : null;
}
/**
* @param {string=} type
* @return {!Object}
* @suppressReceiverCheck
* @this {Object}
*/
function getCompletions(type) {
var object;
if (type === 'string')
object = new String('');
else if (type === 'number')
object = new Number(0);
else if (type === 'boolean')
object = new Boolean(false);
else
object = this;
var result = [];
try {
for (var o = object; o; o = Object.getPrototypeOf(o)) {
if ((type === 'array' || type === 'typedarray') && o === object && ArrayBuffer.isView(o) && o.length > 9999)
continue;
var group = {items: [], __proto__: null};
try {
if (typeof o === 'object' && o.constructor && o.constructor.name)
group.title = o.constructor.name;
} catch (ee) {
// we could break upon cross origin check.
}
result[result.length] = group;
var names = Object.getOwnPropertyNames(o);
var isArray = Array.isArray(o);
for (var i = 0; i < names.length; ++i) {
// Skip array elements indexes.
if (isArray && /^[0-9]/.test(names[i]))
continue;
group.items[group.items.length] = names[i];
}
}
} catch (e) {
}
return result;
}
/**
* @param {?SDK.RemoteObject} object
*/
function completionsForObject(object) {
if (!object) {
receivedPropertyNames(null);
} else if (object.type === 'object' || object.type === 'function') {
object.callFunctionJSON(
getCompletions, [SDK.RemoteObject.toCallArgument(object.subtype)], receivedPropertyNames);
} else if (object.type === 'string' || object.type === 'number' || object.type === 'boolean') {
executionContext.evaluate(
'(' + getCompletions + ')("' + result.type + '")', 'completion', false, true, true, false, false,
receivedPropertyNamesFromEval);
}
}
extractTarget(result).then(completionsForObject);
}
/**
* @param {!SDK.DebuggerModel.CallFrame} callFrame
* @param {function(!Array<!Components.JavaScriptAutocomplete.CompletionGroup>)} callback
*/
function variableNamesInScopes(callFrame, callback) {
var result = [{items: ['this']}];
/**
* @param {string} name
* @param {?Array<!SDK.RemoteObjectProperty>} properties
*/
function propertiesCollected(name, properties) {
var group = {title: name, items: []};
result.push(group);
for (var i = 0; properties && i < properties.length; ++i)
group.items.push(properties[i].name);
if (--pendingRequests === 0)
callback(result);
}
var scopeChain = callFrame.scopeChain();
var pendingRequests = scopeChain.length;
for (var i = 0; i < scopeChain.length; ++i) {
var scope = scopeChain[i];
var object = scope.object();
object.getAllProperties(false, propertiesCollected.bind(null, scope.typeName()));
}
}
/**
* @param {?SDK.RemoteObject} result
* @param {!Protocol.Runtime.ExceptionDetails=} exceptionDetails
*/
function receivedPropertyNamesFromEval(result, exceptionDetails) {
executionContext.target().runtimeAgent().releaseObjectGroup('completion');
if (result && !exceptionDetails)
receivedPropertyNames(/** @type {!Object} */ (result.value));
else
fufill([]);
}
/**
* @param {?Object} object
*/
function receivedPropertyNames(object) {
executionContext.target().runtimeAgent().releaseObjectGroup('completion');
if (!object) {
fufill([]);
return;
}
var propertyGroups = /** @type {!Array<!Components.JavaScriptAutocomplete.CompletionGroup>} */ (object);
var includeCommandLineAPI = (!dotNotation && !bracketNotation);
if (includeCommandLineAPI) {
const commandLineAPI = [
'dir',
'dirxml',
'keys',
'values',
'profile',
'profileEnd',
'monitorEvents',
'unmonitorEvents',
'inspect',
'copy',
'clear',
'getEventListeners',
'debug',
'undebug',
'monitor',
'unmonitor',
'table',
'$',
'$$',
'$x'
];
propertyGroups.push({items: commandLineAPI});
}
fufill(Components.JavaScriptAutocomplete._completionsForQuery(
dotNotation, bracketNotation, expressionString, query, propertyGroups));
}
};
/**
* @param {boolean} dotNotation
* @param {boolean} bracketNotation
* @param {string} expressionString
* @param {string} query
* @param {!Array<!Components.JavaScriptAutocomplete.CompletionGroup>} propertyGroups
* @return {!UI.SuggestBox.Suggestions}
*/
Components.JavaScriptAutocomplete._completionsForQuery = function(
dotNotation, bracketNotation, expressionString, query, propertyGroups) {
if (bracketNotation) {
if (query.length && query[0] === '\'')
var quoteUsed = '\'';
else
var quoteUsed = '"';
}
if (!expressionString) {
const keywords = [
'break', 'case', 'catch', 'continue', 'default', 'delete', 'do', 'else', 'finally',
'for', 'function', 'if', 'in', 'instanceof', 'new', 'return', 'switch', 'this',
'throw', 'try', 'typeof', 'var', 'void', 'while', 'with'
];
propertyGroups.push({title: Common.UIString('keywords'), items: keywords});
}
var result = [];
var lastGroupTitle;
for (var group of propertyGroups) {
group.items.sort(itemComparator);
var caseSensitivePrefix = [];
var caseInsensitivePrefix = [];
var caseSensitiveAnywhere = [];
var caseInsensitiveAnywhere = [];
for (var property of group.items) {
// Assume that all non-ASCII characters are letters and thus can be used as part of identifier.
if (!bracketNotation && !/^[a-zA-Z_$\u008F-\uFFFF][a-zA-Z0-9_$\u008F-\uFFFF]*$/.test(property))
continue;
if (bracketNotation) {
if (!/^[0-9]+$/.test(property))
property = quoteUsed + property.escapeCharacters(quoteUsed + '\\') + quoteUsed;
property += ']';
}
if (property.length < query.length)
continue;
if (query.length && property.toLowerCase().indexOf(query.toLowerCase()) === -1)
continue;
// Substitute actual newlines with newline characters. @see crbug.com/498421
var prop = property.split('\n').join('\\n');
if (property.startsWith(query))
caseSensitivePrefix.push({title: prop, priority: 4});
else if (property.toLowerCase().startsWith(query.toLowerCase()))
caseInsensitivePrefix.push({title: prop, priority: 3});
else if (property.indexOf(query) !== -1)
caseSensitiveAnywhere.push({title: prop, priority: 2});
else
caseInsensitiveAnywhere.push({title: prop, priority: 1});
}
var structuredGroup =
caseSensitivePrefix.concat(caseInsensitivePrefix, caseSensitiveAnywhere, caseInsensitiveAnywhere);
if (structuredGroup.length && group.title !== lastGroupTitle) {
structuredGroup[0].subtitle = group.title;
lastGroupTitle = group.title;
}
result = result.concat(structuredGroup);
}
return result;
/**
* @param {string} a
* @param {string} b
* @return {number}
*/
function itemComparator(a, b) {
var aStartsWithUnderscore = a.startsWith('_');
var bStartsWithUnderscore = b.startsWith('_');
if (aStartsWithUnderscore && !bStartsWithUnderscore)
return 1;
if (bStartsWithUnderscore && !aStartsWithUnderscore)
return -1;
return String.naturalOrderComparator(a, b);
}
};