diff --git a/apps/remix-ide-e2e/src/tests/debugger.test.ts b/apps/remix-ide-e2e/src/tests/debugger.test.ts index b33dbd0ced..69bc648801 100644 --- a/apps/remix-ide-e2e/src/tests/debugger.test.ts +++ b/apps/remix-ide-e2e/src/tests/debugger.test.ts @@ -42,7 +42,6 @@ module.exports = { .waitForElementVisible('*[data-id="slider"]') .click('*[data-id="slider"]') .setValue('*[data-id="slider"]', '50') - .pause(2000) .assert.containsText('*[data-id="solidityLocals"]', 'no locals') .assert.containsText('*[data-id="stepdetail"]', 'vm trace step:\n92') }, @@ -130,6 +129,25 @@ module.exports = { .goToVMTraceStep(717) .pause(5000) .checkVariableDebug('soliditylocals', localVariable_step717_ABIEncoder) // all locals should be initiaed + }, + + 'Should load more solidity locals array': function (browser: NightwatchBrowser) { + browser.addFile('locals.sol', sources[3]['browser/locals.sol']) + .clickLaunchIcon('udapp') + .createContract('') + .clickInstance(3) + .clickFunction('t - transact (not payable)') + .pause(2000) + .debugTransaction(6) + .waitForElementVisible('*[data-id="slider"]') + .click('*[data-id="slider"]') + .setValue('*[data-id="slider"]', '5000') + .waitForElementPresent('*[data-id="treeViewTogglearray"]') + .click('*[data-id="treeViewTogglearray"]') + .waitForElementPresent('*[data-id="treeViewLoadMore"]') + .click('*[data-id="treeViewLoadMore"]') + .assert.containsText('*[data-id="solidityLocals"]', '149: 0 uint256') + .notContainsText('*[data-id="solidityLocals"]', '150: 0 uint256') .end() }, @@ -157,7 +175,7 @@ const sources = [ constructor() public { - } + } function createProject(string memory name, uint goal) public { Project storage project = projects[projects.length]; @@ -166,7 +184,7 @@ const sources = [ project.state = State.Started; project.goal = goal; } - } + } ` } }, @@ -194,6 +212,21 @@ const sources = [ } } `} + }, + { + 'browser/locals.sol': { + content: ` + pragma solidity ^0.6.0; + contract test { + function t () public { + uint[] memory array = new uint[](150); + for (uint k = 0; k < 150; k++) { + array[k] = k; + } + } + } + ` + } } ] diff --git a/apps/remix-ide-e2e/src/tests/terminal.test.ts b/apps/remix-ide-e2e/src/tests/terminal.test.ts index b141204f68..a95f08aa6e 100644 --- a/apps/remix-ide-e2e/src/tests/terminal.test.ts +++ b/apps/remix-ide-e2e/src/tests/terminal.test.ts @@ -80,7 +80,7 @@ const asyncAwait = ` resolve("Promise Resolved") }, 5000) }) - } + } var run = async () => { console.log('Waiting Promise') diff --git a/apps/remix-ide/src/app/tabs/debugger/debuggerUI/VmDebugger.js b/apps/remix-ide/src/app/tabs/debugger/debuggerUI/VmDebugger.js index 4088021ba6..5df574bab0 100644 --- a/apps/remix-ide/src/app/tabs/debugger/debuggerUI/VmDebugger.js +++ b/apps/remix-ide/src/app/tabs/debugger/debuggerUI/VmDebugger.js @@ -83,9 +83,13 @@ function VmDebugger (vmDebuggerLogic) { this.vmDebuggerLogic.event.register('solidityStateUpdating', this.solidityState.setUpdating.bind(this.solidityState)) this.solidityLocals = new SolidityLocals() + this.solidityLocals.event.register('solidityLocalsLoadMore', (cursor) => { + this.vmDebuggerLogic.event.trigger('solidityLocalsLoadMore', [cursor]) + }) this.vmDebuggerLogic.event.register('solidityLocals', this.solidityLocals.update.bind(this.solidityLocals)) this.vmDebuggerLogic.event.register('solidityLocalsMessage', this.solidityLocals.setMessage.bind(this.solidityLocals)) this.vmDebuggerLogic.event.register('solidityLocalsUpdating', this.solidityLocals.setUpdating.bind(this.solidityLocals)) + this.vmDebuggerLogic.event.register('solidityLocalsLoadMoreCompleted', this.solidityLocals.loadMore.bind(this.solidityLocals)) this.returnValuesPanel = new DropdownPanel('Return Value', {json: true}) this.returnValuesPanel.data = {} diff --git a/apps/remix-ide/src/app/tabs/debugger/debuggerUI/vmDebugger/SolidityLocals.js b/apps/remix-ide/src/app/tabs/debugger/debuggerUI/vmDebugger/SolidityLocals.js index 592b2ef060..513dc962ea 100644 --- a/apps/remix-ide/src/app/tabs/debugger/debuggerUI/vmDebugger/SolidityLocals.js +++ b/apps/remix-ide/src/app/tabs/debugger/debuggerUI/vmDebugger/SolidityLocals.js @@ -6,18 +6,28 @@ var yo = require('yo-yo') class SolidityLocals { - constructor (_parent, _traceManager, _internalTreeCall) { + constructor () { this.event = new EventManager() this.basicPanel = new DropdownPanel('Solidity Locals', { json: true, formatSelf: solidityTypeFormatter.formatSelf, - extractData: solidityTypeFormatter.extractData + extractData: solidityTypeFormatter.extractData, + loadMore: (cursor) => { + this.event.trigger('solidityLocalsLoadMore', [cursor]) + } }) this.view + this._data = null } update (data) { - this.basicPanel.update(data) + this._data = data + this.basicPanel.update(this._data) + } + + loadMore (data) { + this._data = this.mergeLocals(data, this._data) + this.basicPanel.update(this._data) } setMessage (message) { @@ -28,6 +38,18 @@ class SolidityLocals { this.basicPanel.setUpdating() } + mergeLocals (locals1, locals2) { + Object.keys(locals2).map(item => { + if (locals2[item].cursor && (parseInt(locals2[item].cursor) < parseInt(locals1[item].cursor))) { + locals2[item] = { + ...locals1[item], + value: [...locals2[item].value, ...locals1[item].value] + } + } + }) + return locals2 + } + render () { this.view = yo`
${this.basicPanel.render()}
` return this.view diff --git a/apps/remix-ide/src/app/tabs/debugger/debuggerUI/vmDebugger/utils/SolidityTypeFormatter.js b/apps/remix-ide/src/app/tabs/debugger/debuggerUI/vmDebugger/utils/SolidityTypeFormatter.js index 025f78bb18..2ddeef72ca 100644 --- a/apps/remix-ide/src/app/tabs/debugger/debuggerUI/vmDebugger/utils/SolidityTypeFormatter.js +++ b/apps/remix-ide/src/app/tabs/debugger/debuggerUI/vmDebugger/utils/SolidityTypeFormatter.js @@ -34,6 +34,8 @@ function extractData (item, parent, key) { }) ret.isArray = true ret.self = parent.isArray ? '' : item.type + ret.cursor = item.cursor + ret.hasNext = item.hasNext } else if (item.type.indexOf('struct') === 0) { ret.children = Object.keys((item.value || {})).map(function (key) { return {key: key, value: item.value[key]} diff --git a/apps/remix-ide/src/app/ui/TreeView.js b/apps/remix-ide/src/app/ui/TreeView.js index 5fc62d9552..59866a43c5 100644 --- a/apps/remix-ide/src/app/ui/TreeView.js +++ b/apps/remix-ide/src/app/ui/TreeView.js @@ -34,6 +34,9 @@ var css = csjs` .label_value { min-width: 10%; } + .cursor_pointer { + cursor: pointer; + } ` var EventManager = require('../../lib/events') @@ -49,6 +52,7 @@ class TreeView { this.event = new EventManager() this.extractData = opts.extractData || this.extractDataDefault this.formatSelf = opts.formatSelf || this.formatSelfDefault + this.loadMore = opts.loadMore this.view = null this.expandPath = [] } @@ -111,6 +115,9 @@ class TreeView { self.event.trigger('nodeRightClick', [keyPath, data, label, event]) } li.appendChild(list) + if (data.hasNext) { + list.appendChild(yo`
  • Load more
  • `) + } } else { caret.style.visibility = 'hidden' label.oncontextmenu = function (event) { diff --git a/libs/remix-debug/src/debugger/VmDebugger.js b/libs/remix-debug/src/debugger/VmDebugger.js index 374ba948fb..d92166c56d 100644 --- a/libs/remix-debug/src/debugger/VmDebugger.js +++ b/libs/remix-debug/src/debugger/VmDebugger.js @@ -228,6 +228,10 @@ class VmDebuggerLogic { listenToSolidityLocalsEvents () { this.event.register('sourceLocationChanged', this.debuggerSolidityLocals.init.bind(this.debuggerSolidityLocals)) + this.event.register('solidityLocalsLoadMore', this.debuggerSolidityLocals.decodeMore.bind(this.debuggerSolidityLocals)) + this.debuggerSolidityLocals.event.register('solidityLocalsLoadMoreCompleted', (locals) => { + this.event.trigger('solidityLocalsLoadMoreCompleted', [locals]) + }) this.debuggerSolidityLocals.event.register('solidityLocals', (state) => { this.event.trigger('solidityLocals', [state]) }) diff --git a/libs/remix-debug/src/debugger/solidityLocals.js b/libs/remix-debug/src/debugger/solidityLocals.js index 7f2c0e7905..a93f828803 100644 --- a/libs/remix-debug/src/debugger/solidityLocals.js +++ b/libs/remix-debug/src/debugger/solidityLocals.js @@ -15,6 +15,7 @@ class DebuggerSolidityLocals { } init (sourceLocation) { + this._sourceLocation = sourceLocation var decodeTimeout = null if (!this.storageResolver) { return this.event.trigger('solidityLocalsMessage', ['storage not ready']) @@ -28,7 +29,7 @@ class DebuggerSolidityLocals { }, 500) } - decode (sourceLocation) { + decode (sourceLocation, cursor) { const self = this this.event.trigger('solidityLocalsMessage', ['']) this.traceManager.waterfall([ @@ -65,12 +66,18 @@ class DebuggerSolidityLocals { var memory = result[1].value try { var storageViewer = new StorageViewer({ stepIndex: this.stepManager.currentStepIndex, tx: this.tx, address: result[2].value }, this.storageResolver, this.traceManager) - localDecoder.solidityLocals(this.stepManager.currentStepIndex, this.internalTreeCall, stack, memory, storageViewer, sourceLocation).then((locals) => { - if (!locals.error) { - this.event.trigger('solidityLocals', [locals]) - } - if (!Object.keys(locals).length) { - this.event.trigger('solidityLocalsMessage', ['no locals']) + localDecoder.solidityLocals(this.stepManager.currentStepIndex, this.internalTreeCall, stack, memory, storageViewer, sourceLocation, cursor).then((locals) => { + if (!cursor) { + if (!locals.error) { + this.event.trigger('solidityLocals', [locals]) + } + if (!Object.keys(locals).length) { + this.event.trigger('solidityLocalsMessage', ['no locals']) + } + } else { + if (!locals.error) { + this.event.trigger('solidityLocalsLoadMoreCompleted', [locals]) + } } }) } catch (e) { @@ -79,6 +86,15 @@ class DebuggerSolidityLocals { }) } + decodeMore (cursor) { + let decodeTimeout = null + if (!this.storageResolver) return this.event.trigger('solidityLocalsMessage', ['storage not ready']) + if (decodeTimeout) window.clearTimeout(decodeTimeout) + decodeTimeout = setTimeout(() => { + this.decode(this._sourceLocation, cursor) + }, 500) + } + } module.exports = DebuggerSolidityLocals diff --git a/libs/remix-debug/src/solidity-decoder/localDecoder.js b/libs/remix-debug/src/solidity-decoder/localDecoder.js index 1b87c933ff..d88b0f4fe1 100644 --- a/libs/remix-debug/src/solidity-decoder/localDecoder.js +++ b/libs/remix-debug/src/solidity-decoder/localDecoder.js @@ -1,6 +1,6 @@ 'use strict' -async function solidityLocals (vmtraceIndex, internalTreeCall, stack, memory, storageResolver, currentSourceLocation) { +async function solidityLocals (vmtraceIndex, internalTreeCall, stack, memory, storageResolver, currentSourceLocation, cursor) { const scope = internalTreeCall.findScope(vmtraceIndex) if (!scope) { const error = { 'message': 'Can\'t display locals. reason: compilation result might not have been provided' } @@ -18,7 +18,7 @@ async function solidityLocals (vmtraceIndex, internalTreeCall, stack, memory, st anonymousIncr++ } try { - locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver) + locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver, cursor) } catch (e) { console.log(e) locals[name] = '' diff --git a/libs/remix-debug/src/solidity-decoder/types/ArrayType.js b/libs/remix-debug/src/solidity-decoder/types/ArrayType.js index e508032b7c..d21ec1b015 100644 --- a/libs/remix-debug/src/solidity-decoder/types/ArrayType.js +++ b/libs/remix-debug/src/solidity-decoder/types/ArrayType.js @@ -72,7 +72,7 @@ class ArrayType extends RefType { return {value: ret, length: '0x' + size.toString(16), type: this.typeName} } - decodeFromMemoryInternal (offset, memory) { + decodeFromMemoryInternal (offset, memory, skip) { const ret = [] let length = this.arraySize if (this.arraySize === 'dynamic') { @@ -80,12 +80,28 @@ class ArrayType extends RefType { length = parseInt(length, 16) offset = offset + 32 } - for (var k = 0; k < length; k++) { + if (isNaN(length)) { + return { + value: '', + type: this.typeName + } + } + if (!skip) skip = 0 + if (skip) offset = offset + (32 * skip) + let limit = length - skip + if (limit > 100) limit = 100 + for (var k = 0; k < limit; k++) { var contentOffset = offset ret.push(this.underlyingType.decodeFromMemory(contentOffset, memory)) offset += 32 } - return {value: ret, length: '0x' + length.toString(16), type: this.typeName} + return { + value: ret, + length: '0x' + length.toString(16), + type: this.typeName, + cursor: skip + limit, + hasNext: length > (skip + limit) + } } } diff --git a/libs/remix-debug/src/solidity-decoder/types/RefType.js b/libs/remix-debug/src/solidity-decoder/types/RefType.js index c6f02e1ca0..f4ca0a16f2 100644 --- a/libs/remix-debug/src/solidity-decoder/types/RefType.js +++ b/libs/remix-debug/src/solidity-decoder/types/RefType.js @@ -19,7 +19,7 @@ class RefType { * @param {Object} - storageResolver * @return {Object} decoded value */ - async decodeFromStack (stackDepth, stack, memory, storageResolver) { + async decodeFromStack (stackDepth, stack, memory, storageResolver, cursor) { if (stack.length - 1 < stackDepth) { return {error: '', type: this.typeName} } @@ -34,7 +34,7 @@ class RefType { } } else if (this.isInMemory()) { offset = parseInt(offset, 16) - return this.decodeFromMemoryInternal(offset, memory) + return this.decodeFromMemoryInternal(offset, memory, cursor) } else { return {error: '', type: this.typeName} } diff --git a/libs/remix-debug/test/debugger.js b/libs/remix-debug/test/debugger.js index 1e2b208a3c..43d8a9e5f8 100644 --- a/libs/remix-debug/test/debugger.js +++ b/libs/remix-debug/test/debugger.js @@ -257,7 +257,7 @@ function testDebugging (debugManager) { tape('traceManager.decodeLocalsAt', async (t) => { t.plan(1) - const tested = JSON.parse('{"proposalNames":{"value":[{"value":"0x48656C6C6F20576F726C64210000000000000000000000000000000000000000","type":"bytes32"}],"length":"0x1","type":"bytes32[]"},"p":{"value":"45","type":"uint256"},"addressLocal":{"value":"0x4B0897B0513FDC7C541B6D9D7E929C4E5364D2DB","type":"address"},"i":{"value":"2","type":"uint256"},"proposalsLocals":{"value":[{"value":{"name":{"value":"0x48656C6C6F20576F726C64210000000000000000000000000000000000000000","type":"bytes32"},"voteCount":{"value":"0","type":"uint256"}},"type":"struct Ballot.Proposal"}],"length":"0x1","type":"struct Ballot.Proposal[]"}}') + const tested = JSON.parse('{"proposalNames":{"value":[{"value":"0x48656C6C6F20576F726C64210000000000000000000000000000000000000000","type":"bytes32"}],"length":"0x1","type":"bytes32[]","cursor":1,"hasNext":false},"p":{"value":"45","type":"uint256"},"addressLocal":{"value":"0x4B0897B0513FDC7C541B6D9D7E929C4E5364D2DB","type":"address"},"i":{"value":"2","type":"uint256"},"proposalsLocals":{"value":[{"value":{"name":{"value":"0x48656C6C6F20576F726C64210000000000000000000000000000000000000000","type":"bytes32"},"voteCount":{"value":"0","type":"uint256"}},"type":"struct Ballot.Proposal"}],"length":"0x1","type":"struct Ballot.Proposal[]"}}') try { const address = debugManager.traceManager.getCurrentCalledAddressAt(330) const location = await debugManager.sourceLocationFromVMTraceIndex(address, 330)