diff --git a/src/chrome/chromeConnection.ts b/src/chrome/chromeConnection.ts index 2849df6a8..3742ef9d8 100644 --- a/src/chrome/chromeConnection.ts +++ b/src/chrome/chromeConnection.ts @@ -249,8 +249,8 @@ export class ChromeConnection { return this.sendMessage('Debugger.setVariableValue', { callFrameId, scopeNumber, variableName, newValue }); } - public runtime_getProperties(objectId: string, ownProperties: boolean, accessorPropertiesOnly: boolean): Promise { - return this.sendMessage('Runtime.getProperties', { objectId, ownProperties, accessorPropertiesOnly, generatePreview: true }); + public runtime_getProperties(objectId: string, ownProperties: boolean, accessorPropertiesOnly: boolean, generatePreview?: boolean): Promise { + return this.sendMessage('Runtime.getProperties', { objectId, ownProperties, accessorPropertiesOnly, generatePreview }); } public runtime_evaluate(expression: string, objectGroup = 'dummyObjectGroup', contextId = 1, returnByValue = false): Promise { diff --git a/src/chrome/chromeDebugAdapter.ts b/src/chrome/chromeDebugAdapter.ts index 0b4f20ef0..ac8d68eb8 100644 --- a/src/chrome/chromeDebugAdapter.ts +++ b/src/chrome/chromeDebugAdapter.ts @@ -12,7 +12,7 @@ import {ChromeConnection} from './chromeConnection'; import * as ChromeUtils from './chromeUtils'; import {formatConsoleMessage} from './consoleHelper'; import * as Chrome from './chromeDebugProtocol'; -import {PropertyContainer, ScopeContainer, IVariableContainer} from './variables'; +import {PropertyContainer, ScopeContainer, IVariableContainer, isIndexedPropName} from './variables'; import * as errors from '../errors'; import * as utils from '../utils'; @@ -618,11 +618,15 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { } } - public getVariablesForObjectId(objectId: string): Promise { + public getVariablesForObjectId(objectId: string, filter?: string, start?: number, count?: number): Promise { + if (typeof start === 'number' && typeof count === 'number') { + return this.getFilteredVariablesForObjectId(objectId, filter, start, count); + } + return Promise.all([ // Need to make two requests to get all properties - this._chromeConnection.runtime_getProperties(objectId, /*ownProperties=*/false, /*accessorPropertiesOnly=*/true), - this._chromeConnection.runtime_getProperties(objectId, /*ownProperties=*/true, /*accessorPropertiesOnly=*/false) + this._chromeConnection.runtime_getProperties(objectId, /*ownProperties=*/false, /*accessorPropertiesOnly=*/true, /*generatePreview=*/true), + this._chromeConnection.runtime_getProperties(objectId, /*ownProperties=*/true, /*accessorPropertiesOnly=*/false, /*generatePreview=*/true) ]).then(getPropsResponses => { // Sometimes duplicates will be returned - merge all descriptors by name const propsByName = new Map(); @@ -647,6 +651,12 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { return Promise.all(variables); }).then(variables => { + // After retrieving all props, apply the filter + if (filter === 'indexed' || filter === 'named') { + const keepIndexed = filter === 'indexed'; + variables = variables.filter(variable => keepIndexed === isIndexedPropName(variable.name)); + } + // Sort all variables properly return variables.sort((var1, var2) => ChromeUtils.compareVariableNames(var1.name, var2.name)); }); @@ -656,6 +666,36 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { return this.remoteObjectToVariable(propDesc.name, propDesc.value); } + private getFilteredVariablesForObjectId(objectId: string, filter: string, start: number, count: number): Promise { + // No ES6, in case we talk to an old runtime + const getIndexedVariablesFn = ` + function getIndexedVariables(start, count) { + var result = []; + for (var i = start; i < (start + count); i++) result[i] = this[i]; + return result; + }`; + // TODO order?? + const getNamedVariablesFn = ` + function getNamedVariablesFn(start, count) { + var result = []; + var ownProps = Object.getOwnProperties(this); + for (var i = start; i < (start + count); i++) result[i] = ownProps[i]; + return result; + }`; + + const getVarsFn = filter === 'indexed' ? getIndexedVariablesFn : getNamedVariablesFn; + return this._chromeConnection.runtime_callFunctionOn(objectId, getVarsFn, [{ value: start }, { value: count }], /*silent=*/true).then(evalResponse => { + if (evalResponse.error) { + return Promise.reject(errors.errorFromEvaluate(evalResponse.error.message)); + } else if (evalResponse.result.exceptionDetails) { + const errMsg = ChromeUtils.errorMessageFromExceptionDetails(evalResponse.result.exceptionDetails); + return Promise.reject(errors.errorFromEvaluate(errMsg)); + } else { + return this.getVariablesForObjectId(evalResponse.result.result.objectId, filter); + } + }); + } + public source(args: DebugProtocol.SourceArguments): Promise { return this._chromeConnection.debugger_getScriptSource(sourceReferenceToScriptId(args.sourceReference)).then(chromeResponse => { return { @@ -804,16 +844,16 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { public createObjectVariable(name: string, object: Chrome.Runtime.RemoteObject, stringify?: boolean): Promise { let propCountP: Promise; if (object.subtype === 'array' || object.subtype === 'typedarray') { - if (object.preview.overflow) { - propCountP = this.getArrayNumPropsByEval(object.objectId); - } else { + if (object.preview && !object.preview.overflow) { propCountP = Promise.resolve(this.getArrayNumPropsByPreview(object)); + } else { + propCountP = this.getArrayNumPropsByEval(object.objectId); } } else { - if (object.preview.overflow) { - propCountP = this.getObjectNumPropsByEval(object.objectId); - } else { + if (object.preview && !object.preview.overflow) { propCountP = Promise.resolve(this.getObjectNumPropsByPreview(object)); + } else { + propCountP = this.getObjectNumPropsByEval(object.objectId); } } @@ -829,7 +869,7 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { } private getArrayNumPropsByEval(objectId: string): Promise { - const getNumPropsFn = 'function() { return [this.length, Object.keys(this).length - this.length ];}'; + const getNumPropsFn = `function() {return [this.length, Object.keys(this).length - this.length ];}`; return this._chromeConnection.runtime_callFunctionOn(objectId, getNumPropsFn, undefined, /*silent=*/true, /*returnByValue=*/true).then(response => { if (response.error) { return Promise.reject(errors.errorFromEvaluate(response.error.message)); @@ -850,20 +890,27 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { private getArrayNumPropsByPreview(object: Chrome.Runtime.RemoteObject): IPropCount { let indexedVariables = 0; let namedVariables = 0; - object.preview.properties.forEach(prop => isNaN(parseInt(prop.name, 10)) ? namedVariables++ : indexedVariables++); + object.preview.properties.forEach(prop => isIndexedPropName(prop.name) ? indexedVariables++ : namedVariables++); return { indexedVariables, namedVariables }; } private getObjectNumPropsByEval(objectId: string): Promise { - const getNumPropsFn = 'function() { return Object.keys(this).length - this.length;}'; - return this._chromeConnection.runtime_callFunctionOn(objectId, getNumPropsFn, undefined, /*silent=*/true).then(response => { + // TODO - counting and order? + const getNumPropsFn = `function() { + function isIndexed(name) { return !isNaN(parseInt(name, 10)); } + function isNamed(name) { return isNaN(parseInt(name, 10)); } + + var keys = Object.keys(this); + return [keys.filter(isIndexed).length, keys.filter(isNamed).length]; + }`; + return this._chromeConnection.runtime_callFunctionOn(objectId, getNumPropsFn, undefined, /*silent=*/true, /*returnByValue=*/true).then(response => { if (response.error) { return Promise.reject(errors.errorFromEvaluate(response.error.message)); } else if (response.result.exceptionDetails) { const errMsg = ChromeUtils.errorMessageFromExceptionDetails(response.result.exceptionDetails); return Promise.reject(errors.errorFromEvaluate(errMsg)); } else { - return { indexedVariables: 0, namedVariables: response.result.result.value }; + return { indexedVariables: response.result.result.value[0], namedVariables: response.result.result.value[1] }; } }); } diff --git a/src/chrome/variables.ts b/src/chrome/variables.ts index abb16efce..9f20238f8 100644 --- a/src/chrome/variables.ts +++ b/src/chrome/variables.ts @@ -4,16 +4,12 @@ import * as DebugProtocol from 'vscode-debugadapter'; -import {Handles} from 'vscode-debugadapter'; - import {ChromeDebugAdapter} from './chromeDebugAdapter'; import * as Chrome from './chromeDebugProtocol.d'; -import * as utils from '../utils'; - export interface IVariableContainer { objectId: string; - expand(adapter: ChromeDebugAdapter, filter: string, start: number, count: number): Promise; + expand(adapter: ChromeDebugAdapter, filter?: string, start?: number, count?: number): Promise; setValue(adapter: ChromeDebugAdapter, name: string, value: string): Promise; } @@ -21,17 +17,8 @@ export abstract class BaseVariableContainer implements IVariableContainer { constructor(public objectId: string) { } - public expand(adapter: ChromeDebugAdapter, filter: string, start: number, count: number): Promise { - return adapter.getVariablesForObjectId(this.objectId).then(variables => { - let filteredVars: DebugProtocol.Variable[] = []; - if (filter === 'indexed' && typeof start === 'number' && typeof count === 'number') { - for (let i = start; i < (count + start) && i < variables.length; i++) filteredVars[i] = variables[i]; - } else { - filteredVars = variables; - } - - return filteredVars; - }); + public expand(adapter: ChromeDebugAdapter, filter?: string, start?: number, count?: number): Promise { + return adapter.getVariablesForObjectId(this.objectId, filter, start, count); } public abstract setValue(adapter: ChromeDebugAdapter, name: string, value: string): Promise; @@ -59,8 +46,9 @@ export class ScopeContainer extends BaseVariableContainer { /** * Call super then insert the 'this' object if needed */ - public expand(adapter: ChromeDebugAdapter, filter: string, start: number, count: number): Promise { - return super.expand(adapter, filter, start, count).then(variables => { + public expand(adapter: ChromeDebugAdapter, filter?: string, start?: number, count?: number): Promise { + // No filtering in scopes right now + return super.expand(adapter, 'all', start, count).then(variables => { if (this.thisObj) { // If this is a scope that should have the 'this', prop, insert it at the top of the list return adapter.propertyDescriptorToVariable({ name: 'this', value: this.thisObj }).then(thisObjVar => { @@ -77,3 +65,7 @@ export class ScopeContainer extends BaseVariableContainer { return adapter._setVariableValue(this._frameId, this._scopeIndex, name, value); } } + +export function isIndexedPropName(name: string): boolean { + return !isNaN(parseInt(name, 10)); +}