From a9c762152cff497a52d9f7cb84ccb805ae9cc840 Mon Sep 17 00:00:00 2001 From: filip mertens Date: Sun, 17 Jul 2022 00:43:08 +0200 Subject: [PATCH] refactor --- .../src/examples/editor-test-contracts.ts | 109 +++++ .../src/tests/editorAutoComplete.test.ts | 427 ++++++++++-------- .../src/app/plugins/parser/code-parser.tsx | 122 +++-- .../services/code-parser-antlr-service.ts | 2 +- .../parser/services/code-parser-compiler.ts | 7 +- .../services/code-parser-gas-service.ts | 22 +- .../providers/completion/completionGlobals.ts | 2 +- .../src/lib/providers/completionProvider.ts | 355 ++++++++------- 8 files changed, 654 insertions(+), 392 deletions(-) create mode 100644 apps/remix-ide-e2e/src/examples/editor-test-contracts.ts diff --git a/apps/remix-ide-e2e/src/examples/editor-test-contracts.ts b/apps/remix-ide-e2e/src/examples/editor-test-contracts.ts new file mode 100644 index 0000000000..cd45fc5e6d --- /dev/null +++ b/apps/remix-ide-e2e/src/examples/editor-test-contracts.ts @@ -0,0 +1,109 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const testContract = { + name: 'test.sol', + content: ` + // SPDX-License-Identifier: GPL-3.0 + pragma solidity >=0.7.0 <0.9.0; + + import "contracts/base.sol"; + + contract test is base { + string public publicstring; + string private privatestring; + string internal internalstring; + + struct TestBookDefinition { + string title; + string author; + uint book_id; + } + TestBookDefinition public mybook; + enum MyEnum{ SMALL, MEDIUM, LARGE } + event MyEvent(uint abc); + + modifier costs(uint price) { + if (msg.value >= price) { + _; + } + } + constructor(){ + + } + + function testing() public view { + + } + + function myprivatefunction() private { + + } + + function myinternalfunction() internal { + + } + + function myexternalfunction() external { + + } + }`} + + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const baseContract = { + name: 'base.sol', + content: ` + // SPDX-License-Identifier: GPL-3.0 + pragma solidity >=0.7.0 <0.9.0; + + import "contracts/baseofbase.sol"; + + contract base is baseofbase { + event BaseEvent(address indexed _from, uint _value); + enum BaseEnum{ SMALL, MEDIUM, LARGE } + struct Book { + string title; + string author; + uint book_id; + } + Book public book; + }`} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const baseOfBaseContract = { + name: 'baseofbase.sol', + content: ` + // SPDX-License-Identifier: GPL-3.0 + pragma solidity >=0.7.0 <0.9.0; + + contract baseofbase { + + struct BaseBook { + string title; + string author; + uint book_id; + } + BaseBook public basebook; + + string private basestring; + string internal internalbasestring; + + function privatebase() private { + + } + + function internalbasefunction() internal { + + } + + function publicbasefunction() public { + + } + function externalbasefunction() external { + } + }`} + + export default { + testContract, + baseContract, + baseOfBaseContract + } \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/tests/editorAutoComplete.test.ts b/apps/remix-ide-e2e/src/tests/editorAutoComplete.test.ts index 2d9dd6e668..1a6ed6b492 100644 --- a/apps/remix-ide-e2e/src/tests/editorAutoComplete.test.ts +++ b/apps/remix-ide-e2e/src/tests/editorAutoComplete.test.ts @@ -1,28 +1,79 @@ 'use strict' import { NightwatchBrowser } from 'nightwatch' import init from '../helpers/init' +import examples from '../examples/editor-test-contracts' const autoCompleteLineElement = (name: string) => { return `//*[@class='editor-widget suggest-widget visible']//*[@class='contents' and contains(.,'${name}')]` } module.exports = { + '@disabled': true, before: function (browser: NightwatchBrowser, done: VoidFunction) { init(browser, done, 'http://127.0.0.1:8080', false) }, - 'Should load the test file': function (browser: NightwatchBrowser) { - browser.openFile('contracts') - .openFile('contracts/3_Ballot.sol') - .waitForElementVisible('#editorView') - .setEditorValue(BallotWithARefToOwner) - .pause(4000) // wait for the compiler to finish - .scrollToLine(37) + 'Should add test and base files #group2': function (browser: NightwatchBrowser) { + browser.addFile('contracts/test.sol', examples.testContract) + .addFile('contracts/base.sol', examples.baseContract) + .addFile('contracts/baseofbase.sol', examples.baseOfBaseContract) + .openFile('contracts/test.sol').pause(3000) }, - 'Should put cursor at the end of a line': function (browser: NightwatchBrowser) { - const path = "//*[@class='view-line' and contains(.,'new') and contains(.,'owner')]//span//span[contains(.,';')]" + 'Should put cursor in the () of the function #group2': function (browser: NightwatchBrowser) { + browser.scrollToLine(18) + const path = "//*[@class='view-line' and contains(.,'myprivatefunction') and contains(.,'private')]//span//span[contains(.,'(')]" browser.waitForElementVisible('#editorView') .useXpath() .click(path).pause(1000) + }, + 'Should complete variable declaration types in a function definition #group2': function (browser: NightwatchBrowser) { + browser + .perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys('uint25') + }) + .waitForElementPresent(autoCompleteLineElement('uint256')) + .click(autoCompleteLineElement('uint256')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys(' abc, testb') + }) + .waitForElementPresent(autoCompleteLineElement('"TestBookDefinition"')) + .click(autoCompleteLineElement('"TestBookDefinition"')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys(' memo') + }) + .waitForElementPresent(autoCompleteLineElement('memory')) + .click(autoCompleteLineElement('memory')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys(' t, BaseB') + }) + .waitForElementPresent(autoCompleteLineElement('"BaseBook"')) + .click(autoCompleteLineElement('"BaseBook"')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys(' stor') + }) + .waitForElementPresent(autoCompleteLineElement('storage')) + .click(autoCompleteLineElement('storage')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys(' b') + }) + }, + 'Should put cursor at the end of function #group2': function (browser: NightwatchBrowser) { + + const path = "//*[@class='view-line' and contains(.,'myprivatefunction') and contains(.,'private')]//span//span[contains(.,'{')]" + browser + .useXpath() + .click(path).pause(1000) .perform(function () { const actions = this.actions({ async: true }); return actions. @@ -31,7 +82,42 @@ module.exports = { sendKeys(this.Keys.ARROW_RIGHT) }) }, - 'Should type and get msg + sender': function (browser: NightwatchBrowser) { + + 'Should autocomplete derived and local event when not using this. #group2': function (browser: NightwatchBrowser) { + browser.perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys('emit base') + }) + .waitForElementVisible(autoCompleteLineElement('BaseEvent')) + .click(autoCompleteLineElement('BaseEvent')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions + .sendKeys('msg.sender') + .sendKeys(this.Keys.TAB) + .sendKeys(this.Keys.TAB) // somehow this is needed to get the cursor to the next parameter, only for selenium + .sendKeys('3232') + .sendKeys(this.Keys.TAB) + .sendKeys(this.Keys.ENTER) + }) + .perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys('emit MyEv') + }) + .waitForElementVisible(autoCompleteLineElement('MyEvent')) + .click(autoCompleteLineElement('MyEvent')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions + .sendKeys('3232') + .sendKeys(this.Keys.TAB) + .sendKeys(this.Keys.ENTER) + }) + }, + + 'Should type and get msg options #group2': function (browser: NightwatchBrowser) { browser. perform(function () { const actions = this.actions({ async: true }); @@ -40,6 +126,10 @@ module.exports = { sendKeys('msg.') }) .waitForElementVisible(autoCompleteLineElement('sender')) + .waitForElementVisible(autoCompleteLineElement('data')) + .waitForElementVisible(autoCompleteLineElement('value')) + .waitForElementVisible(autoCompleteLineElement('gas')) + .waitForElementVisible(autoCompleteLineElement('sig')) .click(autoCompleteLineElement('sender')) .perform(function () { const actions = this.actions({ async: true }); @@ -48,217 +138,176 @@ module.exports = { sendKeys(this.Keys.ENTER) }) }, - 'Should type and get completions in the context without this': function (browser: NightwatchBrowser) { + 'Should bo and get book #group2': function (browser: NightwatchBrowser) { + browser. + perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys(this.Keys.ENTER). + sendKeys('bo') + }) + .waitForElementVisible(autoCompleteLineElement('book')) + .click(autoCompleteLineElement('book')) + }, + 'Should autcomplete derived struct #group2': function (browser: NightwatchBrowser) { browser.perform(function () { const actions = this.actions({ async: true }); return actions. - sendKeys(this.Keys.ENTER). - sendKeys('co') + sendKeys('.') }) - .waitForElementVisible(autoCompleteLineElement('chairperson')) - .waitForElementVisible(autoCompleteLineElement('cowner')) - .waitForElementVisible(autoCompleteLineElement('constructor')) - .waitForElementVisible(autoCompleteLineElement('continue')) - .waitForElementVisible(autoCompleteLineElement('contract')) - .waitForElementVisible(autoCompleteLineElement('constant')) - .click(autoCompleteLineElement('cowner')) + .waitForElementVisible(autoCompleteLineElement('author')) + .waitForElementVisible(autoCompleteLineElement('book_id')) + .waitForElementVisible(autoCompleteLineElement('title')) + .click(autoCompleteLineElement('title')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys(';') + .sendKeys(this.Keys.ENTER) + }) + }, + 'Should bo and get basebook #group2': function (browser: NightwatchBrowser) { + browser. + perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys(this.Keys.ENTER). + sendKeys('base') + }) + .waitForElementVisible(autoCompleteLineElement('basebook')) + .click(autoCompleteLineElement('basebook')) }, - 'Perform dot completion on cowner': function (browser: NightwatchBrowser) { + 'Should autcomplete derived struct from base class #group2': function (browser: NightwatchBrowser) { browser.perform(function () { const actions = this.actions({ async: true }); return actions. sendKeys('.') }) - // publicly available functions - .waitForElementVisible(autoCompleteLineElement('changeOwner')) - .waitForElementVisible(autoCompleteLineElement('getOwner')) - // do not show private vars, functions & modifiers & events - .waitForElementNotPresent(autoCompleteLineElement('private')) - .waitForElementNotPresent(autoCompleteLineElement('isOwner')) - .waitForElementNotPresent(autoCompleteLineElement('ownerSet')) + .waitForElementVisible(autoCompleteLineElement('author')) + .waitForElementVisible(autoCompleteLineElement('book_id')) + .waitForElementVisible(autoCompleteLineElement('title')) + .click(autoCompleteLineElement('title')) .perform(function () { const actions = this.actions({ async: true }); return actions. - sendKeys(this.Keys.ENTER). - sendKeys('msg.') + sendKeys(';') + .sendKeys(this.Keys.ENTER) }) - .waitForElementVisible(autoCompleteLineElement('sender')) - .click(autoCompleteLineElement('sender')) + }, + 'Should find private and internal local functions #group2': function (browser: NightwatchBrowser) { + browser.perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys('my') + }) + .waitForElementVisible(autoCompleteLineElement('myprivatefunction')) + .waitForElementVisible(autoCompleteLineElement('myinternalfunction')) + .waitForElementVisible(autoCompleteLineElement('memory')) + .click(autoCompleteLineElement('myinternalfunction')) .perform(function () { const actions = this.actions({ async: true }); return actions. - // right arrow key - sendKeys(this.Keys.ARROW_RIGHT). - sendKeys(this.Keys.ARROW_RIGHT). sendKeys(this.Keys.ENTER) }) }, - 'Dot complete struct': function (browser: NightwatchBrowser) { + 'Should find internal functions and var from base and owner #group2': function (browser: NightwatchBrowser) { browser.perform(function () { const actions = this.actions({ async: true }); return actions. - sendKeys(this.Keys.ENTER). - sendKeys('Proposal m') + sendKeys('intern') }) - .waitForElementVisible(autoCompleteLineElement('memory')) - .click(autoCompleteLineElement('memory')) + .waitForElementVisible(autoCompleteLineElement('internalbasefunction')) + .waitForElementVisible(autoCompleteLineElement('internalstring')) + .waitForElementVisible(autoCompleteLineElement('internalbasestring')) + // keyword internal + .waitForElementVisible(autoCompleteLineElement('internal keyword')) + .click(autoCompleteLineElement('internalbasefunction')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions + .sendKeys(this.Keys.ENTER) + }) + }, + + 'Should not find external functions without this. #group2': function (browser: NightwatchBrowser) { + browser.perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys('extern') + }) + .waitForElementNotPresent(autoCompleteLineElement('externalbasefunction')) + .waitForElementNotPresent(autoCompleteLineElement('myexternalfunction')) + // keyword internal + .waitForElementVisible(autoCompleteLineElement('external keyword')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions + .sendKeys(this.Keys.BACK_SPACE) + .sendKeys(this.Keys.BACK_SPACE) + .sendKeys(this.Keys.BACK_SPACE) + .sendKeys(this.Keys.BACK_SPACE) + .sendKeys(this.Keys.BACK_SPACE) + .sendKeys(this.Keys.BACK_SPACE) + .sendKeys(this.Keys.BACK_SPACE) + }) + }, + 'Should find external functions using this. #group2': function (browser: NightwatchBrowser) { + browser. + perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys(this.Keys.ENTER). + sendKeys('this.') + }) + .waitForElementVisible(autoCompleteLineElement('externalbasefunction')) + .waitForElementVisible(autoCompleteLineElement('myexternalfunction')) + }, + 'Should find public functions and vars using this. but not private & other types of nodes #group2': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible(autoCompleteLineElement('"publicbasefunction"')) + .waitForElementVisible(autoCompleteLineElement('"publicstring"')) + .waitForElementVisible(autoCompleteLineElement('"basebook"')) + .waitForElementVisible(autoCompleteLineElement('"mybook"')) + .waitForElementVisible(autoCompleteLineElement('"testing"')) + // but no private functions or vars or other types of nodes + .waitForElementNotPresent(autoCompleteLineElement('"private"')) + .waitForElementNotPresent(autoCompleteLineElement('"BaseEvent"')) + .waitForElementNotPresent(autoCompleteLineElement('"BaseEnum"')) + .waitForElementNotPresent(autoCompleteLineElement('"TestBookDefinition"')) + .click(autoCompleteLineElement('"publicbasefunction"')) + .perform(function () { + const actions = this.actions({ async: true }); + return actions + .sendKeys(this.Keys.ENTER) + }) + }, + 'Should autocomplete local and derived ENUMS #group2': function (browser: NightwatchBrowser) { + browser.perform(function () { + const actions = this.actions({ async: true }); + return actions. + sendKeys('BaseEnum.') + }) + .waitForElementVisible(autoCompleteLineElement('SMALL')) + .waitForElementVisible(autoCompleteLineElement('MEDIUM')) + .waitForElementVisible(autoCompleteLineElement('LARGE')) + .click(autoCompleteLineElement('SMALL')) .perform(function () { const actions = this.actions({ async: true }); return actions. - sendKeys(' p;'). - sendKeys(this.Keys.ENTER).pause(5000). - sendKeys('p.') + sendKeys(';') + .sendKeys(this.Keys.ENTER) + .sendKeys('MyEnum.') }) - .waitForElementVisible(autoCompleteLineElement('name')) - .waitForElementVisible(autoCompleteLineElement('voteCount')) + .waitForElementVisible(autoCompleteLineElement('SMALL')) + .waitForElementVisible(autoCompleteLineElement('MEDIUM')) + .waitForElementVisible(autoCompleteLineElement('LARGE')) + .click(autoCompleteLineElement('SMALL')) .perform(function () { const actions = this.actions({ async: true }); return actions. - sendKeys(' =1;') + sendKeys(';') + .sendKeys(this.Keys.ENTER) }) } } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const BallotWithARefToOwner = `// SPDX-License-Identifier: GPL-3.0 - -pragma solidity >=0.7.0 <0.9.0; - -import "./2_Owner.sol"; - -/** - * @title Ballot - * @dev Implements voting process along with vote delegation - */ -contract BallotHoverTest { - Owner cowner; - struct Voter { - uint weight; // weight is accumulated by delegation - bool voted; // if true, that person already voted - address delegate; // person delegated to - uint vote; // index of the voted proposal - } - - struct Proposal { - // If you can limit the length to a certain number of bytes, - // always use one of bytes1 to bytes32 because they are much cheaper - bytes32 name; // short name (up to 32 bytes) - uint voteCount; // number of accumulated votes - } - - address public chairperson; - - mapping(address => Voter) public voters; - - Proposal[] public proposals; - - /** - * @dev Create a new ballot to choose one of 'proposalNames'. - * @param proposalNames names of proposals - */ - constructor(bytes32[] memory proposalNames) { - cowner = new Owner(); - chairperson = msg.sender; - voters[chairperson].weight = 1; - - for (uint i = 0; i < proposalNames.length; i++) { - // 'Proposal({...})' creates a temporary - // Proposal object and 'proposals.push(...)' - // appends it to the end of 'proposals'. - proposals.push(Proposal({ - name: proposalNames[i], - voteCount: 0 - })); - } - } - - /** - * @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'. - * @param voter address of voter - */ - function giveRightToVote(address voter) public { - require( - msg.sender == chairperson, - "Only chairperson can give right to vote." - ); - require( - !voters[voter].voted, - "The voter already voted." - ); - require(voters[voter].weight == 0); - voters[voter].weight = 1; - } - - /** - * @dev Delegate your vote to the voter 'to'. - * @param to address to which vote is delegated - */ - function delegate(address to) public { - Voter storage sender = voters[msg.sender]; - require(!sender.voted, "You already voted."); - require(to != msg.sender, "Self-delegation is disallowed."); - - while (voters[to].delegate != address(0)) { - to = voters[to].delegate; - - // We found a loop in the delegation, not allowed. - require(to != msg.sender, "Found loop in delegation."); - } - sender.voted = true; - sender.delegate = to; - Voter storage delegate_ = voters[to]; - if (delegate_.voted) { - // If the delegate already voted, - // directly add to the number of votes - proposals[delegate_.vote].voteCount += sender.weight; - } else { - // If the delegate did not vote yet, - // add to her weight. - delegate_.weight += sender.weight; - } - } - - /** - * @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'. - * @param proposal index of proposal in the proposals array - */ - function vote(uint proposal) public { - Voter storage sender = voters[msg.sender]; - require(sender.weight != 0, "Has no right to vote"); - require(!sender.voted, "Already voted."); - sender.voted = true; - sender.vote = proposal; - - // If 'proposal' is out of the range of the array, - // this will throw automatically and revert all - // changes. - proposals[proposal].voteCount += sender.weight; - } - - /** - * @dev Computes the winning proposal taking all previous votes into account. - * @return winningProposal_ index of winning proposal in the proposals array - */ - function winningProposal() public view - returns (uint winningProposal_) - { - uint winningVoteCount = 0; - for (uint p = 0; p < proposals.length; p++) { - if (proposals[p].voteCount > winningVoteCount) { - winningVoteCount = proposals[p].voteCount; - winningProposal_ = p; - } - } - } - - /** - * @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then - * @return winnerName_ the name of the winner - */ - function winnerName() public view - returns (bytes32 winnerName_) - { - winnerName_ = proposals[winningProposal()].name; - } -} -` \ No newline at end of file diff --git a/apps/remix-ide/src/app/plugins/parser/code-parser.tsx b/apps/remix-ide/src/app/plugins/parser/code-parser.tsx index b320a6f728..6d022ac940 100644 --- a/apps/remix-ide/src/app/plugins/parser/code-parser.tsx +++ b/apps/remix-ide/src/app/plugins/parser/code-parser.tsx @@ -23,7 +23,7 @@ import { ConfigPlugin } from '../config' const profile: Profile = { name: 'codeParser', - methods: ['nodesAtPosition', 'getLineColumnOfNode', 'getLineColumnOfPosition', 'getFunctionParamaters', 'getDeclaration', 'getFunctionReturnParameters', 'getVariableDeclaration', 'getNodeDocumentation', 'getNodeLink', 'listAstNodes', 'getBlockAtPosition', 'getLastNodeInLine', 'resolveImports', 'parseSolidity', 'getNodesWithScope', 'getNodesWithName', 'getNodes', 'compile', 'getNodeById', 'getLastCompilationResult', 'positionOfDefinition', 'definitionAtPosition', 'jumpToDefinition', 'referrencesAtPosition', 'referencesOf', 'getActiveHighlights', 'gasEstimation', 'declarationOf', 'getGasEstimates'], + methods: ['nodesAtPosition', 'getContractNodes', 'getCurrentFileNodes', 'getLineColumnOfNode', 'getLineColumnOfPosition', 'getFunctionParamaters', 'getDeclaration', 'getFunctionReturnParameters', 'getVariableDeclaration', 'getNodeDocumentation', 'getNodeLink', 'listAstNodes', 'getANTLRBlockAtPosition', 'getLastNodeInLine', 'resolveImports', 'parseSolidity', 'getNodesWithScope', 'getNodesWithName', 'getNodes', 'compile', 'getNodeById', 'getLastCompilationResult', 'positionOfDefinition', 'definitionAtPosition', 'jumpToDefinition', 'referrencesAtPosition', 'referencesOf', 'getActiveHighlights', 'gasEstimation', 'declarationOf', 'getGasEstimates'], events: [], version: '0.0.1' } @@ -37,17 +37,17 @@ export function isNodeDefinition(node: genericASTNode) { node.nodeType === 'EventDefinition' } -export type genericASTNode = -ContractDefinitionAstNode -| FunctionDefinitionAstNode -| ModifierDefinitionAstNode -| VariableDeclarationAstNode -| StructDefinitionAstNode -| EventDefinitionAstNode -| IdentifierAstNode -| FunctionCallAstNode -| ImportDirectiveAstNode -| SourceUnitAstNode +export type genericASTNode = + ContractDefinitionAstNode + | FunctionDefinitionAstNode + | ModifierDefinitionAstNode + | VariableDeclarationAstNode + | StructDefinitionAstNode + | EventDefinitionAstNode + | IdentifierAstNode + | FunctionCallAstNode + | ImportDirectiveAstNode + | SourceUnitAstNode interface flatReferenceIndexNode { [id: number]: genericASTNode @@ -83,7 +83,7 @@ export class CodeParser extends Plugin { parseSolidity: (text: string) => Promise getLastNodeInLine: (ast: string) => Promise listAstNodes: () => Promise - getBlockAtPosition: (position: any, text?: string) => Promise + getANTLRBlockAtPosition: (position: any, text?: string) => Promise getCurrentFileAST: (text?: string) => Promise constructor(astWalker: any) { @@ -92,7 +92,8 @@ export class CodeParser extends Plugin { this.nodeIndex = { declarations: [[]], flatReferences: [], - nodesPerFile: {} } + nodesPerFile: {} + } } async onActivation() { @@ -105,7 +106,7 @@ export class CodeParser extends Plugin { this.parseSolidity = this.antlrService.parseSolidity.bind(this.antlrService) this.getLastNodeInLine = this.antlrService.getLastNodeInLine.bind(this.antlrService) this.listAstNodes = this.antlrService.listAstNodes.bind(this.antlrService) - this.getBlockAtPosition = this.antlrService.getBlockAtPosition.bind(this.antlrService) + this.getANTLRBlockAtPosition = this.antlrService.getANTLRBlockAtPosition.bind(this.antlrService) this.getCurrentFileAST = this.antlrService.getCurrentFileAST.bind(this.antlrService) @@ -150,11 +151,11 @@ export class CodeParser extends Plugin { } - + getSubNodes(node: T): number[] { - return node.nodeType=="ContractDefinition" && node.contractDependencies; + return node.nodeType == "ContractDefinition" && node.contractDependencies; } /** @@ -178,7 +179,7 @@ export class CodeParser extends Plugin { } } - + } // NODE HELPERS @@ -198,26 +199,77 @@ export class CodeParser extends Plugin { } - _flatNodeList(node: ContractDefinitionAstNode, contractName: string, fileName: string, compilatioResult: any) { + _flatNodeList(contractNode: ContractDefinitionAstNode, fileName: string, inScope: boolean, compilatioResult: any) { const index = {} + const contractName: string = contractNode.name const callback = (node) => { + if(inScope && node.scope !== contractNode.id) return + if(inScope) node.isClassNode = true; node.gasEstimate = this._getContractGasEstimate(node, contractName, fileName, compilatioResult) node.functionName = node.name + this._getInputParams(node) + node.contractName = contractName + node.contractId = contractNode.id index[node.id] = node } - this.astWalker.walkFull(node, callback) + this.astWalker.walkFull(contractNode, callback) return index } _extractFileNodes(fileName: string, compilationResult: lastCompilationResult) { if (compilationResult && compilationResult.data.sources && compilationResult.data.sources[fileName]) { const source = compilationResult.data.sources[fileName] - const nodesByContract = [] - this.astWalker.walkFull(source.ast, (node) => { + const nodesByContract: any = {} + nodesByContract.imports = {} + nodesByContract.contracts = {} + this.astWalker.walkFull(source.ast, async (node) => { if (node.nodeType === 'ContractDefinition') { - const flatNodes = this._flatNodeList(node, node.name, fileName, compilationResult) + const flatNodes = this._flatNodeList(node, fileName, false, compilationResult) node.gasEstimate = this._getContractGasEstimate(node, node.name, fileName, compilationResult) - nodesByContract[node.name] = { contractDefinition: node, contractNodes: flatNodes } + nodesByContract.contracts[node.name] = { contractDefinition: node, contractNodes: flatNodes } + const baseNodes = {} + const baseNodesWithBaseContractScope = {} + if (node.linearizedBaseContracts) { + for (const id of node.linearizedBaseContracts) { + if (id !== node.id) { + const baseContract = await this.getNodeById(id) + console.log('baseContract', baseContract) + const callback = (node) => { + node.contractName = (baseContract as any).name + node.contractId = (baseContract as any).id + node.isBaseNode = true; + baseNodes[node.id] = node + if ((node.scope && node.scope === baseContract.id) + || node.nodeType === 'EnumDefinition' + || node.nodeType === 'EventDefinition' + ) { + baseNodesWithBaseContractScope[node.id] = node + } + if(node.members){ + for(const member of node.members){ + member.contractName = (baseContract as any).name + member.contractId = (baseContract as any).id + member.isBaseNode = true; + } + } + } + this.astWalker.walkFull(baseContract, callback) + } + } + } + nodesByContract.contracts[node.name].baseNodes = baseNodes + nodesByContract.contracts[node.name].baseNodesWithBaseContractScope = baseNodesWithBaseContractScope + nodesByContract.contracts[node.name].contractScopeNodes = this._flatNodeList(node, fileName, true, compilationResult) + } + if (node.nodeType === 'ImportDirective') { + + const imported = await this.resolveImports(node, {}) + console.log('import resolve', node, (Object.values(imported) as any)) + for (const importedNode of (Object.values(imported) as any)) { + if (importedNode.nodes) + for (const subNode of importedNode.nodes) { + nodesByContract.imports[subNode.id] = subNode + } + } } }) return nodesByContract @@ -231,7 +283,7 @@ export class CodeParser extends Plugin { if (name === contractName) { const contract = contracts[name] const estimationObj = contract.evm && contract.evm.gasEstimates - + let executionCost = null if (node.nodeType === 'FunctionDefinition') { const visibility = node.visibility @@ -385,6 +437,24 @@ export class CodeParser extends Plugin { } + async getContractNodes(contractName: string) { + if (this.nodeIndex.nodesPerFile + && this.nodeIndex.nodesPerFile[this.currentFile] + && this.nodeIndex.nodesPerFile[this.currentFile].contracts[contractName] + && this.nodeIndex.nodesPerFile[this.currentFile].contracts[contractName].contractNodes) { + return this.nodeIndex.nodesPerFile[this.currentFile].contracts[contractName] + } + return false + } + + async getCurrentFileNodes() { + if (this.nodeIndex.nodesPerFile + && this.nodeIndex.nodesPerFile[this.currentFile]) { + return this.nodeIndex.nodesPerFile[this.currentFile] + } + return false + } + /** * * @param identifierNode @@ -426,7 +496,7 @@ export class CodeParser extends Plugin { if (node.nodeType === 'ImportDirective' && !imported[node.sourceUnit]) { const importNode: any = await this.getNodeById(node.sourceUnit) imported[importNode.id] = importNode - if (importNode.nodeType=='ImportDirective' && importNode.nodes) { + if (importNode.nodes) { for (const child of importNode.nodes) { imported = await this.resolveImports(child, imported) } diff --git a/apps/remix-ide/src/app/plugins/parser/services/code-parser-antlr-service.ts b/apps/remix-ide/src/app/plugins/parser/services/code-parser-antlr-service.ts index e8ca63c0f8..b685af4b80 100644 --- a/apps/remix-ide/src/app/plugins/parser/services/code-parser-antlr-service.ts +++ b/apps/remix-ide/src/app/plugins/parser/services/code-parser-antlr-service.ts @@ -143,7 +143,7 @@ export default class CodeParserAntlrService { * @param {string} text // optional * @return {any} * */ - async getBlockAtPosition(position: any, text: string = null) { + async getANTLRBlockAtPosition(position: any, text: string = null) { await this.getCurrentFileAST(text) const allowedTypes = ['SourceUnit', 'ContractDefinition', 'FunctionDefinition'] const walkAst = (node: any) => { diff --git a/apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts b/apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts index 20b944eb0e..ced7926405 100644 --- a/apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts +++ b/apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts @@ -73,14 +73,9 @@ export default class CodeParserCompiler { this.plugin._buildIndex(data, source) - - if (this.gastEstimateTimeOut) { - window.clearTimeout(this.gastEstimateTimeOut) - } - + this.plugin.nodeIndex.nodesPerFile[this.plugin.currentFile] = this.plugin._extractFileNodes(this.plugin.currentFile, this.plugin.compilerAbstract) await this.plugin.gasService.showGasEstimates() - console.log("INDEX", this.plugin.nodeIndex) this.plugin.emit('astFinished') } diff --git a/apps/remix-ide/src/app/plugins/parser/services/code-parser-gas-service.ts b/apps/remix-ide/src/app/plugins/parser/services/code-parser-gas-service.ts index f4a7d4f128..76d1f7c7cc 100644 --- a/apps/remix-ide/src/app/plugins/parser/services/code-parser-gas-service.ts +++ b/apps/remix-ide/src/app/plugins/parser/services/code-parser-gas-service.ts @@ -2,7 +2,7 @@ import { CodeParser, genericASTNode } from "../code-parser"; import { lineText } from '@remix-ui/editor' export default class CodeParserGasService { - plugin: CodeParser + plugin: CodeParser constructor(plugin: CodeParser) { this.plugin = plugin @@ -12,17 +12,19 @@ export default class CodeParserGasService { if (!fileName) { fileName = await this.plugin.currentFile } - if (this.plugin.nodeIndex.nodesPerFile && this.plugin.nodeIndex.nodesPerFile[fileName]) { + if (this.plugin.nodeIndex.nodesPerFile && this.plugin.nodeIndex.nodesPerFile[fileName] && this.plugin.nodeIndex.nodesPerFile[fileName].contracts) { const estimates: any = [] - for (const contract in this.plugin.nodeIndex.nodesPerFile[fileName]) { + for (const contract in this.plugin.nodeIndex.nodesPerFile[fileName].contracts) { console.log(contract) - const nodes = this.plugin.nodeIndex.nodesPerFile[fileName][contract].contractNodes - for (const node of Object.values(nodes) as any[]) { - if (node.gasEstimate) { - estimates.push({ - node, - range: await this.plugin.getLineColumnOfNode(node) - }) + if (this.plugin.nodeIndex.nodesPerFile[fileName].contracts[contract].contractNodes) { + const nodes = this.plugin.nodeIndex.nodesPerFile[fileName].contracts[contract].contractNodes + for (const node of Object.values(nodes) as any[]) { + if (node.gasEstimate) { + estimates.push({ + node, + range: await this.plugin.getLineColumnOfNode(node) + }) + } } } } diff --git a/libs/remix-ui/editor/src/lib/providers/completion/completionGlobals.ts b/libs/remix-ui/editor/src/lib/providers/completion/completionGlobals.ts index 9621d625c3..7380f698f2 100644 --- a/libs/remix-ui/editor/src/lib/providers/completion/completionGlobals.ts +++ b/libs/remix-ui/editor/src/lib/providers/completion/completionGlobals.ts @@ -260,7 +260,7 @@ function CreateCompletionItem(label: string, kind: monaco.languages.CompletionIt export function GetCompletionKeywords(range: IRange, monaco): monaco.languages.CompletionItem[] { const completionItems = []; const keywords = ['modifier', 'mapping', 'break', 'continue', 'delete', 'else', 'for', - 'if', 'new', 'return', 'returns', 'while', 'using', + 'if', 'new', 'return', 'returns', 'while', 'using', 'emit', 'private', 'public', 'external', 'internal', 'payable', 'nonpayable', 'view', 'pure', 'case', 'do', 'else', 'finally', 'in', 'instanceof', 'return', 'throw', 'try', 'catch', 'typeof', 'yield', 'void', 'virtual', 'override']; keywords.forEach(unit => { diff --git a/libs/remix-ui/editor/src/lib/providers/completionProvider.ts b/libs/remix-ui/editor/src/lib/providers/completionProvider.ts index a82ff626c1..eb98ec35fa 100644 --- a/libs/remix-ui/editor/src/lib/providers/completionProvider.ts +++ b/libs/remix-ui/editor/src/lib/providers/completionProvider.ts @@ -1,3 +1,4 @@ +import { sourceMappingDecoder } from "@remix-project/remix-debug" import { AstNode } from "@remix-project/remix-solidity-ts" import { isArray } from "lodash" import { editor, languages, Position } from "monaco-editor" @@ -13,17 +14,13 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider constructor(props: any, monaco: any) { this.props = props this.monaco = monaco - - } triggerCharacters = ['.', ''] async provideCompletionItems(model: editor.ITextModel, position: Position, context: monaco.languages.CompletionContext): Promise { console.log('AUTOCOMPLETE', context) - console.log(position) const word = model.getWordUntilPosition(position); - const wordAt = model.getWordAtPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, @@ -35,20 +32,18 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider let nodes: AstNode[] = [] let suggestions: monaco.languages.CompletionItem[] = [] - const cursorPosition: number = this.props.editorAPI.getCursorPosition() + if (context.triggerCharacter === '.') { console.clear() - console.log('TEXT', line) const lineTextBeforeCursor: string = line.substring(0, position.column - 1) const lastNodeInExpression = await this.getLastNodeInExpression(lineTextBeforeCursor) console.log('lastNode found', lastNodeInExpression) - console.log(lineTextBeforeCursor) const expressionElements = lineTextBeforeCursor.split('.') - console.log('expression elements', expressionElements) let dotCompleted = false - //if (expressionElements.length === 2) { + + // handles completion from for builtin types const globalCompletion = getContextualAutoCompleteByGlobalVariable(lastNodeInExpression.name, range, this.monaco) if (globalCompletion) { dotCompleted = true @@ -58,115 +53,21 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider // debugger }, 2000) } + // handle completion for global THIS. if (lastNodeInExpression.name === 'this') { dotCompleted = true - let thisCompletionNodes = await this.getContractCompletions(nodes, position) - - // with this. you can't have internal nodes and no contractDefinitions - thisCompletionNodes = thisCompletionNodes.filter(node => { - if (node.visibility && node.visibility === 'internal') { - return false - } - if (node.nodeType && node.nodeType === 'ContractDefinition') { - return false - } - return true - }) - nodes = [...nodes, ...thisCompletionNodes] - setTimeout(() => { - // eslint-disable-next-line no-debugger - // debugger - }, 2000) + nodes = [...nodes, ...await this.getThisCompletions(position)] } - //} + // handle completion for other dot completions if (expressionElements.length > 1 && !dotCompleted) { - const last = lastNodeInExpression.name || lastNodeInExpression.memberName - console.log('last', last) - - let nodesAtPosition; - - const block = await this.props.plugin.call('codeParser', 'getBlockAtPosition', position, null) - console.log('BLOCK', block) - - if (block) { - nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', block.body ? block.body.range[0] : block.range[0]) - console.log('NODES AT POSITION WITH BLOCK', nodesAtPosition) - } else { - nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', cursorPosition) - console.log('NODES AT POSITION', nodesAtPosition) - } - - // explore nodes at the BLOCK - if (nodesAtPosition) { - for (const node of nodesAtPosition) { - const nodesOfScope: AstNode[] = await this.props.plugin.call('codeParser', 'getNodesWithScope', node.id) - console.log('NODES OF SCOPE ', node.name, node.id, nodesOfScope) - for (const nodeOfScope of nodesOfScope) { - if (nodeOfScope.name === last) { - console.log('FOUND NODE', nodeOfScope) - if (nodeOfScope.typeName && nodeOfScope.typeName.nodeType === 'UserDefinedTypeName') { - const declarationOf: AstNode = await this.props.plugin.call('codeParser', 'declarationOf', nodeOfScope.typeName) - console.log('METHOD 1 HAS DECLARATION OF', declarationOf) - nodes = [...nodes, ...declarationOf.nodes || declarationOf.members] - const baseContracts = await this.getlinearizedBaseContracts(declarationOf) - for (const baseContract of baseContracts) { - nodes = [...nodes, ...baseContract.nodes] - } - } - } - } - } - // anything within the block statements might provide a clue to what it is - // if (!nodes.length) { - for (const node of nodesAtPosition) { - if (node.statements) { - for (const statement of node.statements) { - if (statement.expression && statement.expression.memberName === last) { - const declarationOf = await this.props.plugin.call('codeParser', 'declarationOf', statement.expression) - if (declarationOf.typeName && declarationOf.typeName.nodeType === 'UserDefinedTypeName') { - const baseDeclaration = await this.props.plugin.call('codeParser', 'declarationOf', declarationOf.typeName) - console.log('METHOD 2 HAS BASE DECLARATION OF', baseDeclaration) - nodes = [...nodes, ...baseDeclaration.nodes || baseDeclaration.members] - } - } - } - } - } - } - // } - - // brute force search in all nodes with the name - const nodesOfScope = await this.props.plugin.call('codeParser', 'getNodesWithName', last) - console.log('NODES WITHE NAME ', last, nodesOfScope) - for (const nodeOfScope of nodesOfScope) { - if (nodeOfScope.name === last) { - console.log('FOUND NODE', nodeOfScope) - if (nodeOfScope.typeName && nodeOfScope.typeName.nodeType === 'UserDefinedTypeName') { - const declarationOf = await this.props.plugin.call('codeParser', 'declarationOf', nodeOfScope.typeName) - console.log('METHOD 3 HAS DECLARATION OF', declarationOf) - nodes = [...nodes, ...declarationOf.nodes || declarationOf.members] - //const baseContracts = await this.getlinearizedBaseContracts(declarationOf) - //for (const baseContract of baseContracts) { - //nodes = [...nodes, ...baseContract.nodes] - //} - } - } - } - - const filterNodeTypes = ['ContractDefinition', 'ModifierDefinition', 'EventDefinition'] - nodes = nodes.filter(node => { - if (node.visibility && node.visibility === 'private') { - return false - } - if (node.nodeType && filterNodeTypes.indexOf(node.nodeType) !== -1) { - return false - } - return true - }) + const nameOfLastTypedExpression = lastNodeInExpression.name || lastNodeInExpression.memberName + nodes = [...nodes, ...await this.getDotCompletions(position, nameOfLastTypedExpression)] + } } else { + // handles contract completions and other suggestions suggestions = [...suggestions, ...GetGlobalVariable(range, this.monaco), ...getCompletionSnippets(range, this.monaco), @@ -175,19 +76,18 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider ...GetGlobalFunctions(range, this.monaco), ...GeCompletionUnits(range, this.monaco), ] - let thisCompletionNodes = await this.getContractCompletions(nodes, position) + let contractCompletions = await this.getContractCompletions(position) + // we can't have external nodes without using this. - thisCompletionNodes = thisCompletionNodes.filter(node => { + contractCompletions = contractCompletions.filter(node => { if (node.visibility && node.visibility === 'external') { return false } return true }) - nodes = [...nodes, ...thisCompletionNodes] + nodes = [...nodes, ...contractCompletions] } - - console.log('WORD', word, wordAt) console.log('NODES', nodes) // remove duplicates @@ -225,7 +125,6 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider } } - const getVariableDeclaration = async (node: any) => { let variableDeclaration = await this.props.plugin.call('codeParser', 'getVariableDeclaration', node) if (node.scope) { @@ -250,7 +149,6 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider } suggestions.push(completion) } else if (node.nodeType === 'FunctionDefinition') { - const completion = { label: { label: `"${node.name}"`, description: await getNodeLink(node), detail: ` -> ${node.name} ${await getParamaters(node)}` }, kind: this.monaco.languages.CompletionItemKind.Function, @@ -293,9 +191,10 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider } else if (node.nodeType === 'EventDefinition') { const completion = { - label: { label: `"${node.name}"`, description: await getNodeLink(node), detail: ` ${node.name}` }, + label: { label: `"${node.name}"`, description: await getNodeLink(node), detail: ` -> ${node.name} ${await getParamaters(node)}` }, kind: this.monaco.languages.CompletionItemKind.Event, - insertText: node.name, + insertText: `${node.name}${await completeParameters(node.parameters)};`, + insertTextRules: this.monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, range: range, documentation: await getDocs(node) } @@ -310,6 +209,17 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider documentation: await getDocs(node) } suggestions.push(completion) + } else if + (node.nodeType === 'EnumValue' || node.type === 'EnumValue') { + const completion = { + label: { label: `"${node.name}"` }, + kind: this.monaco.languages.CompletionItemKind.EnumMember, + insertText: node.name, + range: range, + documentation: null + } + suggestions.push(completion) + } else { console.log('UNKNOWN NODE', node) } @@ -321,77 +231,204 @@ export class RemixCompletionProvider implements languages.CompletionItemProvider } } - private getlinearizedBaseContracts = async (node: any) => { - let params = [] - if (node.linearizedBaseContracts) { - for (const id of node.linearizedBaseContracts) { - if (id !== node.id) { - const baseContract = await this.props.plugin.call('codeParser', 'getNodeById', id) - params = [...params, ...[baseContract]] + private getBlockNodesAtPosition = async (position: Position) => { + let nodes: any[] = [] + const cursorPosition = this.props.editorAPI.getCursorPosition() + const nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', cursorPosition) + // try to get the block from ANTLR of which the position is in + const ANTLRBlock = await this.props.plugin.call('codeParser', 'getANTLRBlockAtPosition', position, null) + // if the block has a name and a type we can maybe find it in the contract nodes + const fileNodes = await this.props.plugin.call('codeParser', 'getCurrentFileNodes') + + console.log('file nodes', fileNodes); + + if (isArray(nodesAtPosition) && nodesAtPosition.length) { + for (const node of nodesAtPosition) { + // try to find the real block in the AST and get the nodes in that scope + if (node.nodeType === 'ContractDefinition') { + const contractNodes = fileNodes.contracts[node.name].contractNodes + for (const contractNode of Object.values(contractNodes)) { + if (contractNode['name'] === ANTLRBlock.name) { + console.log('found real block', contractNode) + let nodeOfScope = await this.props.plugin.call('codeParser', 'getNodesWithScope', (contractNode as any).id) + nodes = [...nodes, ...nodeOfScope] + if (contractNode['body']) { + nodeOfScope = await this.props.plugin.call('codeParser', 'getNodesWithScope', (contractNode['body'] as any).id) + nodes = [...nodes, ...nodeOfScope] + } + } + } } + //const nodesOfScope = await this.props.plugin.call('codeParser', 'getNodesWithScope', node.id) + // nodes = [...nodes, ...nodesOfScope] } } - return params + console.log('NODES AT BLOCK SCOPE', nodes) + // we are only interested in nodes that are in the same block as the cursor + nodes = nodes.filter(node => { + if (node.src) { + const position = sourceMappingDecoder.decode(node.src) + if (position.start >= ANTLRBlock.range[0] && (position.start + position.length) <= ANTLRBlock.range[1]) { + return true + } + } + return false + }) + + return nodes; } - private getContractCompletions = async (nodes: any[], position: Position) => { + private getContractAtPosition = async (position: Position) => { const cursorPosition = this.props.editorAPI.getCursorPosition() let nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', cursorPosition) console.log('NODES AT POSITION', nodesAtPosition) // if no nodes exits at position, try to get the block of which the position is in + const ANTLRBlock = await this.props.plugin.call('codeParser', 'getANTLRBlockAtPosition', position, null) if (!nodesAtPosition.length) { - const block = await this.props.plugin.call('codeParser', 'getBlockAtPosition', position, null) - if (block) { - nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', block.range[0]) + if (ANTLRBlock) { + nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', ANTLRBlock.range[0]) } } - - // get all children of all nodes at position if (isArray(nodesAtPosition) && nodesAtPosition.length) { for (const node of nodesAtPosition) { - const nodesOfScope = await this.props.plugin.call('codeParser', 'getNodesWithScope', node.id) - - for (const nodeOfScope of nodesOfScope) { - const imports = await this.props.plugin.call('codeParser', 'resolveImports', nodeOfScope) - if (imports) { - for (const key in imports) { - if (imports[key].nodes) - nodes = [...nodes, ...imports[key].nodes] - } - } - } - // if the node is a contract at this positio mark the nodes as the contractNodes if (node.nodeType === 'ContractDefinition') { - for (const node of nodesOfScope) { - node.isInContract = true - } + return node } - nodes = [...nodes, ...nodesOfScope] } - // get the linearized base contracts + } + return null + } + + private getContractCompletions = async (position: Position) => { + let nodes: any[] = [] + const cursorPosition = this.props.editorAPI.getCursorPosition() + let nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', cursorPosition) + console.log('NODES AT POSITION', nodesAtPosition) + // if no nodes exits at position, try to get the block of which the position is in + const block = await this.props.plugin.call('codeParser', 'getANTLRBlockAtPosition', position, null) + console.log('BLOCK AT POSITION', block) + if (!nodesAtPosition.length) { + if (block) { + nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', block.range[0]) + } + } + // find the contract and get the nodes of the contract and the base contracts and imports + if (isArray(nodesAtPosition) && nodesAtPosition.length) { + let contractNode: any = {} for (const node of nodesAtPosition) { - const baseContracts = await this.getlinearizedBaseContracts(node) - for (const baseContract of baseContracts) { - nodes = [...nodes, ...baseContract.nodes] + if (node.nodeType === 'ContractDefinition') { + contractNode = node + const fileNodes = await this.props.plugin.call('codeParser', 'getCurrentFileNodes') + console.log('FILE NODES', fileNodes) + const contractNodes = fileNodes.contracts[node.name] + nodes = [...Object.values(contractNodes.contractScopeNodes), ...nodes] + nodes = [...Object.values(contractNodes.baseNodesWithBaseContractScope), ...nodes] + nodes = [...Object.values(fileNodes.imports), ...nodes] + // some types like Enum and Events are not scoped so we need to add them manually + nodes = [...contractNodes.contractDefinition.nodes, ...nodes] + // at the nodes at the block itself + nodes = [...nodes, ...await this.getBlockNodesAtPosition(position)] + // filter private nodes, only allow them when contract ID is the same as the current contract + nodes = nodes.filter(node => { + if (node.visibility) { + if (node.visibility === 'private') { + return (node.contractId ? node.contractId === contractNode.id : false) || false + } + } + return true + }) + break; } + } } else { // get all the nodes from a simple code parser which only parses the current file nodes = [...nodes, ...await this.props.plugin.call('codeParser', 'listAstNodes')] } + return nodes + } - // filter private nodes, only allow them when isContract is true - nodes = nodes.filter(node => { - if (node.visibility) { - if (!node.isInContract) { - return node.visibility !== 'private' - } + private getThisCompletions = async (position: Position) => { + let nodes: any[] = [] + let thisCompletionNodes = await this.getContractCompletions(position) + const allowedTypesForThisCompletion = ['VariableDeclaration', 'FunctionDefinition'] + // with this. you can't have internal nodes and no contractDefinitions + thisCompletionNodes = thisCompletionNodes.filter(node => { + console.log('node filter', node) + if (node.visibility && (node.visibility === 'internal' || node.visibility === 'private')) { + return false + } + if (node.nodeType && !allowedTypesForThisCompletion.includes(node.nodeType)) { + return false } return true }) + nodes = [...nodes, ...thisCompletionNodes] + setTimeout(() => { + // eslint-disable-next-line no-debugger + // debugger + }, 2000) + return nodes + } + + private getDotCompletions = async (position: Position, nameOfLastTypedExpression: string) => { + const contractCompletions = await this.getContractCompletions(position) + let nodes: any[] = [] + + const filterNodes = (nodes: any[], parentNode: any, declarationOf: any = null) => { + return nodes && nodes.filter(node => { + if (node.visibility) { + if(declarationOf && declarationOf.nodeType && declarationOf.nodeType === 'StructDefinition') { + return true + } + if ((node.visibility === 'internal' && !parentNode.isBaseNode) || node.visibility === 'private') { + return false + } + } + return true + }) + } + + + for (const nodeOfScope of contractCompletions) { + // console.log('nodeOfScope', nodeOfScope.name, nodeOfScope) + if (nodeOfScope.name === nameOfLastTypedExpression) { + console.log('FOUND NODE', nodeOfScope) + if (nodeOfScope.typeName && nodeOfScope.typeName.nodeType === 'UserDefinedTypeName') { + const declarationOf: AstNode = await this.props.plugin.call('codeParser', 'declarationOf', nodeOfScope.typeName) + console.log('METHOD 1 HAS DECLARATION OF', declarationOf) + nodes = [...nodes, + ...filterNodes(declarationOf.nodes, nodeOfScope, declarationOf) + || filterNodes(declarationOf.members, nodeOfScope, declarationOf)] + const baseContracts = await this.getlinearizedBaseContracts(declarationOf) + for (const baseContract of baseContracts) { + nodes = [...nodes, ...filterNodes(baseContract.nodes, nodeOfScope)] + } + } else if (nodeOfScope.members) { + console.log('METHOD 1 HAS MEMBERS OF') + nodes = [...nodes, ...filterNodes(nodeOfScope.members, nodeOfScope)] + } + } + } + + + return nodes } + private getlinearizedBaseContracts = async (node: any) => { + let params = [] + if (node.linearizedBaseContracts) { + for (const id of node.linearizedBaseContracts) { + if (id !== node.id) { + const baseContract = await this.props.plugin.call('codeParser', 'getNodeById', id) + params = [...params, ...[baseContract]] + } + } + } + return params + } + /** * * @param lineTextBeforeCursor