diff --git a/.circleci/config.yml b/.circleci/config.yml index dd164a4383..492f14f187 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,6 +111,12 @@ jobs: steps: - browser-tools/install-chrome - browser-tools/install-chromedriver + - run: + command: | + google-chrome --version + chromedriver --version + java -jar /usr/local/bin/selenium.jar --version + name: Check install - checkout - attach_workspace: at: . @@ -119,10 +125,9 @@ jobs: keys: - v1-deps-{{ checksum "package-lock.json" }} - run: npm install - - run: npm run selenium-install - run: name: Start Selenium - command: npx selenium-standalone start + command: java -jar /usr/local/bin/selenium.jar background: true - run: ./apps/remix-ide/ci/browser_test.sh chrome - store_test_results: @@ -149,6 +154,12 @@ jobs: steps: - browser-tools/install-firefox - browser-tools/install-geckodriver + - run: + command: | + firefox --version + geckodriver --version + java -jar /usr/local/bin/selenium.jar --version + name: Check install - checkout - attach_workspace: at: . @@ -157,10 +168,9 @@ jobs: keys: - v1-deps-{{ checksum "package-lock.json" }} - run: npm install - - run: npm run selenium-install - run: name: Start Selenium - command: npx selenium-standalone start + command: java -jar /usr/local/bin/selenium.jar background: true - run: ./apps/remix-ide/ci/browser_test.sh firefox - store_test_results: @@ -187,6 +197,13 @@ jobs: steps: - browser-tools/install-chrome - browser-tools/install-chromedriver + - run: + command: | + google-chrome --version + chromedriver --version + java -jar /usr/local/bin/selenium.jar --version + name: Check install + - checkout - checkout - attach_workspace: at: . @@ -195,10 +212,9 @@ jobs: keys: - v1-deps-{{ checksum "package-lock.json" }} - run: npm install - - run: npm run selenium-install - run: name: Start Selenium - command: npx selenium-standalone start + command: java -jar /usr/local/bin/selenium.jar background: true - run: ./apps/remix-ide/ci/browser_tests_plugin_api.sh - store_test_results: diff --git a/apps/remix-ide-e2e/src/commands/goToVMTraceStep.ts b/apps/remix-ide-e2e/src/commands/goToVMTraceStep.ts index e84ab52367..5a3ae6ae82 100644 --- a/apps/remix-ide-e2e/src/commands/goToVMTraceStep.ts +++ b/apps/remix-ide-e2e/src/commands/goToVMTraceStep.ts @@ -12,6 +12,10 @@ class GoToVmTraceStep extends EventEmitter { function goToVMtraceStep (browser: NightwatchBrowser, step: number, incr: number, done: VoidFunction) { browser.execute(function (step) { (document.getElementById('slider') as HTMLInputElement).value = (step - 1).toString() }, [step]) .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) + .execute((step) => { + (document.querySelector('*[data-id="slider"]') as any).internal_onmouseup({ target: { value: step }}) + }, [step]) + .pause(10000) .perform(() => { done() }) diff --git a/apps/remix-ide-e2e/src/select_tests.sh b/apps/remix-ide-e2e/src/select_tests.sh index bded4e84fe..4cd3f36c00 100644 --- a/apps/remix-ide-e2e/src/select_tests.sh +++ b/apps/remix-ide-e2e/src/select_tests.sh @@ -26,7 +26,7 @@ do done npm run build:e2e PS3='Select a test or command: ' -TESTFILES=( $(grep -IRiL "disabled" "dist/apps/remix-ide-e2e/src/tests" | grep "\.spec\|\.test" | sort ) ) +TESTFILES=( $(grep -IRiL "disabled" "dist/apps/remix-ide-e2e/src/tests" | grep "\.spec\|\.test\|plugin_api" | sort ) ) # declare -p TESTFILES TESTFILES+=("list") diff --git a/apps/remix-ide-e2e/src/tests/debugger.test.ts b/apps/remix-ide-e2e/src/tests/debugger.test.ts index cd3fafc3b3..56a1174521 100644 --- a/apps/remix-ide-e2e/src/tests/debugger.test.ts +++ b/apps/remix-ide-e2e/src/tests/debugger.test.ts @@ -39,12 +39,9 @@ module.exports = { 'Should debug transaction using slider #group1': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="verticalIconsKindudapp"]') .waitForElementVisible('*[data-id="slider"]') - // eslint-disable-next-line dot-notation - .execute(function () { document.getElementById('slider')['value'] = '50' }) // It only moves slider to 50 but vm traces are not updated - .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) - .pause(2000) - .click('*[data-id="dropdownPanelSolidityLocals"]') - .waitForElementContainsText('*[data-id="solidityLocals"]', 'no locals', 60000) + .goToVMTraceStep(51) + .waitForElementContainsText('*[data-id="solidityLocals"]', 'toast', 60000) + .waitForElementContainsText('*[data-id="solidityLocals"]', '999', 60000) .waitForElementContainsText('*[data-id="stepdetail"]', 'vm trace step:\n51', 60000) }, @@ -159,10 +156,7 @@ module.exports = { .pause(2000) .debugTransaction(0) .waitForElementVisible('*[data-id="slider"]').pause(2000) - // .setValue('*[data-id="slider"]', '5000') // Like this, setValue doesn't work properly for input type = range - // eslint-disable-next-line dot-notation - .execute(function () { document.getElementById('slider')['value'] = '7450' }).pause(10000) // It only moves slider to 7450 but vm traces are not updated - .setValue('*[data-id="slider"]', new Array(3).fill(browser.Keys.RIGHT_ARROW)) // This will press NEXT 3 times and will update the trace details + .goToVMTraceStep(7453) .waitForElementPresent('*[data-id="treeViewDivtreeViewItemarray"]') .click('*[data-id="treeViewDivtreeViewItemarray"]') .waitForElementPresent('*[data-id="treeViewDivtreeViewLoadMore"]') @@ -210,15 +204,7 @@ module.exports = { .pause(3000) .clickLaunchIcon('debugger') .waitForElementVisible('*[data-id="slider"]') - // eslint-disable-next-line dot-notation - .execute(function () { document.getElementById('slider')['value'] = '153' }) // It only moves slider to 153 but vm traces are not updated - .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) - .pause(1000) - /* - setting the slider to 5 leads to "vm trace step: 91" for chrome and "vm trace step: 92" for firefox - => There is something going wrong with the nightwatch API here. - As we are only testing if debugger is active, this is ok to keep that for now. - */ + .goToVMTraceStep(154) .waitForElementContainsText('*[data-id="stepdetail"]', 'vm trace step:\n154', 60000) }, @@ -241,8 +227,23 @@ module.exports = { .waitForElementVisible('*[data-id="solidityLocals"]', 60000) .pause(10000) .checkVariableDebug('soliditylocals', { num: { value: '2', type: 'uint256' } }) - .checkVariableDebug('soliditystate', { number: { value: '0', type: 'uint256', constant: false, immutable: false } }) - .end() + .checkVariableDebug('soliditystate', { number: { value: '0', type: 'uint256', constant: false, immutable: false } }) + }, + + 'Should debug reverted transactions #group5': function (browser: NightwatchBrowser) { + browser + .testContracts('reverted.sol', sources[6]['reverted.sol'], ['A', 'B', 'C']) + .clickLaunchIcon('udapp') + .selectContract('A') + .createContract('') + .pause(500) + .clickInstance(0) + .clickFunction('callA - transact (not payable)') + .debugTransaction(1) + .goToVMTraceStep(79) + .waitForElementVisible('*[data-id="debugGoToRevert"]', 60000) + .click('*[data-id="debugGoToRevert"]') + .waitForElementContainsText('*[data-id="asmitems"] div[selected="selected"]', '117 REVERT') } } @@ -366,6 +367,46 @@ const sources = [ } ` } + }, + { + 'reverted.sol': { + content: `contract A { + B b; + uint p; + constructor () { + b = new B(); + } + function callA() public { + p = 123; + try b.callB() { + + } + catch (bytes memory reason) { + + } + } + } + + contract B { + C c; + uint p; + constructor () { + c = new C(); + } + function callB() public { + p = 124; + revert("revert!"); + c.callC(); + } + } + + contract C { + uint p; + function callC() public { + p = 125; + } + }` + } } ] diff --git a/apps/remix-ide-e2e/src/tests/editor.test.ts b/apps/remix-ide-e2e/src/tests/editor.test.ts index 59c3211e79..993f43039f 100644 --- a/apps/remix-ide-e2e/src/tests/editor.test.ts +++ b/apps/remix-ide-e2e/src/tests/editor.test.ts @@ -4,7 +4,7 @@ import { NightwatchBrowser } from 'nightwatch' import init from '../helpers/init' module.exports = { - + '@disabled': true, before: function (browser: NightwatchBrowser, done: VoidFunction) { init(browser, done, 'http://127.0.0.1:8080', true) }, @@ -92,13 +92,13 @@ module.exports = { .executeScript('remix.exeCurrent()') .scrollToLine(32) .waitForElementPresent('.highlightLine33', 60000) - .checkElementStyle('.highlightLine33', 'background-color', 'rgb(52, 152, 219)') + .checkElementStyle('.highlightLine33', 'background-color', 'rgb(44, 62, 80)') .scrollToLine(40) .waitForElementPresent('.highlightLine41', 60000) - .checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)') + .checkElementStyle('.highlightLine41', 'background-color', 'rgb(44, 62, 80)') .scrollToLine(50) .waitForElementPresent('.highlightLine51', 60000) - .checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)') + .checkElementStyle('.highlightLine51', 'background-color', 'rgb(44, 62, 80)') }, 'Should remove 1 highlight from source code #group1': '' + function (browser: NightwatchBrowser) { @@ -111,8 +111,8 @@ module.exports = { .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .waitForElementNotPresent('.highlightLine33', 60000) - .checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)') - .checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)') + .checkElementStyle('.highlightLine41', 'background-color', 'rgb(44, 62, 80)') + .checkElementStyle('.highlightLine51', 'background-color', 'rgb(44, 62, 80)') }, 'Should remove all highlights from source code #group1': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts b/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts index bbf57ca3db..dab5c3a879 100644 --- a/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts +++ b/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts @@ -4,7 +4,8 @@ import init from '../helpers/init' const testData = { validURL: 'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol', - invalidURL: 'https://github.com/Oppelin/Roles.sol' + invalidURL: 'https://github.com/Oppelin/Roles.sol', + JSON: 'https://github.com/ethereum/remix-project/blob/master/package.json' } module.exports = { @@ -57,6 +58,27 @@ module.exports = { .scrollAndClick('[data-id="homeTab-modal-footer-ok-react"]') .openFile('github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol') .waitForElementVisible("div[title='default_workspace/github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol'") + .getEditorValue((content) => { + browser.assert.ok(content.indexOf('library Roles {') !== -1, 'content does contain "library Roles {"') + }) + }, + 'Import JSON From Github For Valid URL': function (browser: NightwatchBrowser) { + browser + .click('div[title="home"]') + .scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]') + .waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]') + .clearValue('*[data-id="homeTabModalDialogCustomPromptText"]') + .execute(() => { + (document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus() + }, [], () => {}) + .setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.JSON) + .waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]') + .scrollAndClick('[data-id="homeTab-modal-footer-ok-react"]') + .openFile('github/ethereum/remix-project/package.json') + .waitForElementVisible("div[title='default_workspace/github/ethereum/remix-project/package.json'") + .getEditorValue((content) => { + browser.assert.ok(content.indexOf('"name": "remix-project",') !== -1, 'content does contain "name": "remix-project"') + }) .end() } } diff --git a/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts b/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts index f10e6c713b..3d04a3edfc 100644 --- a/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts +++ b/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts @@ -1,25 +1,90 @@ 'use strict' import { NightwatchBrowser } from 'nightwatch' -import init from '../helpers/init' + module.exports = { - before: function (browser: NightwatchBrowser, done: VoidFunction) { - init(browser, done, 'http://127.0.0.1:8080?e2e_testmigration=true', false) + '@disabled': true, + 'Should load the testmigration url #group1': function (browser: NightwatchBrowser) { + browser.url('http://127.0.0.1:8080?e2e_testmigration=true') + .pause(6000) + .switchBrowserTab(0) + .maximizeWindow() + .waitForElementVisible('*[data-id="skipbackup-btn"]', 5000) + .click('*[data-id="skipbackup-btn"]') + .waitForElementVisible('[id="remixTourSkipbtn"]') + .click('[id="remixTourSkipbtn"]') }, - 'Should have README file with TEST README as content': function (browser: NightwatchBrowser) { - browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) + 'Should load the testmigration url and refresh and still have test data #group7': function (browser: NightwatchBrowser) { + browser.url('http://127.0.0.1:8080?e2e_testmigration=true') + .pause(6000) + .switchBrowserTab(0) + .maximizeWindow() + .waitForElementVisible('*[data-id="skipbackup-btn"]', 5000) + .click('*[data-id="skipbackup-btn"]') + .waitForElementVisible('[id="remixTourSkipbtn"]') + .click('[id="remixTourSkipbtn"]').refresh() + }, + 'should have indexedDB storage in terminal #group1 #group7': function (browser: NightwatchBrowser) { + browser.assert.containsText('*[data-id="terminalJournal"]', 'indexedDB') + }, + 'Should fallback to localstorage with default data #group2': function (browser: NightwatchBrowser) { + browser.url('http://127.0.0.1:8080?e2e_testmigration_fallback=true') + .pause(6000) + .switchBrowserTab(0) + .maximizeWindow() + .waitForElementVisible('[id="remixTourSkipbtn"]') + .click('[id="remixTourSkipbtn"]') + .waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) + .waitForElementVisible('div[data-id="filePanelFileExplorerTree"]') + .openFile('README.txt') + .getEditorValue((content) => { + browser.assert.ok(content.includes('Output from script will appear in remix terminal.')) + }) + .click('*[data-id="treeViewLitreeViewItemcontracts"]') + .openFile('contracts/1_Storage.sol') + .getEditorValue((content) => { + browser.assert.ok(content.includes('function retrieve() public view returns (uint256){')) + }) + }, + 'Should load the testmigration url with local storage anabled #group3': function (browser: NightwatchBrowser) { + browser.url('http://127.0.0.1:8080?e2e_testmigration=true&e2e_testmigration_fallback=true') + .pause(6000) + .switchBrowserTab(0) + .maximizeWindow() + .waitForElementVisible('*[data-id="skipbackup-btn"]', 5000) + .click('*[data-id="skipbackup-btn"]') + .waitForElementVisible('[id="remixTourSkipbtn"]') + .click('[id="remixTourSkipbtn"]') + }, + 'Should generate error in migration by deleting indexedDB and falling back to local storage with test #group5': function (browser: NightwatchBrowser) { + browser.url('http://127.0.0.1:8080?e2e_testmigration=true') + .pause(6000) + .switchBrowserTab(0) + .maximizeWindow().execute(('delete window.indexedDB')) + .waitForElementVisible('*[data-id="skipbackup-btn"]', 5000) + .click('*[data-id="skipbackup-btn"]') + .waitForElementVisible('[id="remixTourSkipbtn"]') + .click('[id="remixTourSkipbtn"]') + }, + 'should have localstorage storage in terminal #group2 #group3 #group5': function (browser: NightwatchBrowser) { + browser.assert.containsText('*[data-id="terminalJournal"]', 'localstorage') + }, + 'Should have README file with TEST README as content #group1 #group3': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) .waitForElementVisible('div[data-id="filePanelFileExplorerTree"]') .openFile('TEST_README.txt') .getEditorValue((content) => { browser.assert.equal(content, 'TEST README') }) }, - 'Should have a workspace_test': function (browser: NightwatchBrowser) { + // these are test data entries + 'Should have a workspace_test #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) .click('*[data-id="workspacesSelect"] option[value="workspace_test"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest_contracts"]') }, - 'Should have a sol file with test data': function (browser: NightwatchBrowser) { + 'Should have a sol file with test data #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) .click('*[data-id="treeViewLitreeViewItemtest_contracts"]') .openFile('test_contracts/1_Storage.sol') @@ -27,7 +92,7 @@ module.exports = { browser.assert.equal(content, 'testing') }) }, - 'Should have a artifacts file with JSON test data': function (browser: NightwatchBrowser) { + 'Should have a artifacts file with JSON test data #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) .click('*[data-id="treeViewLitreeViewItemtest_contracts/artifacts"]') .openFile('test_contracts/artifacts/Storage_metadata.json') @@ -35,5 +100,27 @@ module.exports = { const metadata = JSON.parse(content) browser.assert.equal(metadata.test, 'data') }) - } + }, + 'Should have a empty workspace #group1 #group3 #group5 #group7': function (browser: NightwatchBrowser) { + browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 5000) + .click('*[data-id="workspacesSelect"] option[value="emptyspace"]') + }, + // end of test data entries + 'Should load with all storage blocked #group4': function (browser: NightwatchBrowser) { + browser.url('http://127.0.0.1:8080?e2e_testblock_storage=true') + .pause(6000) + .switchBrowserTab(0) + .maximizeWindow() + .assert.containsText('.alert-warning', 'Your browser does not support') + }, + 'Should with errors #group6': function (browser: NightwatchBrowser) { + browser.url('http://127.0.0.1:8080?e2e_testmigration=true') + .pause(6000) + .switchBrowserTab(0) + .maximizeWindow().execute('delete window.localStorage') + .waitForElementVisible('*[data-id="skipbackup-btn"]', 5000) + .click('*[data-id="skipbackup-btn"]') + .assert.containsText('.alert-danger', 'An unknown error') + }, + } diff --git a/apps/remix-ide-e2e/src/tests/plugin_api.ts b/apps/remix-ide-e2e/src/tests/plugin_api.ts index 063487794a..b6c1aa3087 100644 --- a/apps/remix-ide-e2e/src/tests/plugin_api.ts +++ b/apps/remix-ide-e2e/src/tests/plugin_api.ts @@ -81,13 +81,13 @@ const clickButton = async (browser: NightwatchBrowser, buttonText: string, waitR const checkForAcceptAndRemember = async function (browser: NightwatchBrowser) { return new Promise((resolve) => { browser.frameParent(() => { - browser.pause(1000).element('xpath', '//*[@data-id="permissionHandlerRememberUnchecked"]', (visible:any) => { + browser.pause(1000).element('xpath', '//*[@data-id="permissionHandlerRememberUnchecked"]', (visible: any) => { if (visible.status && visible.status === -1) { - // @ts-ignore + // @ts-ignore browser.frame(0, () => { resolve(true) }) } else { browser.waitForElementVisible('//*[@data-id="permissionHandlerRememberUnchecked"]').click('//*[@data-id="permissionHandlerRememberUnchecked"]').waitForElementVisible('//*[@data-id="PermissionHandler-modal-footer-ok-react"]').click('//*[@data-id="PermissionHandler-modal-footer-ok-react"]', () => { - // @ts-ignore + // @ts-ignore browser.frame(0, () => { resolve(true) }) }) } @@ -190,7 +190,7 @@ module.exports = { }) .waitForElementVisible('[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]') .rightClick('[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]').useXpath().waitForElementVisible('//*[@id="menuitemtestcommand"]').click('//*[@id="menuitemtestcommand"]', async () => { - // @ts-ignore + // @ts-ignore browser.click('//*[@data-id="verticalIconsKindlocalPlugin"]').frame(0, async () => { await clickAndCheckLog(browser, null, { id: 'localPlugin', name: 'testCommand', label: 'testCommand', type: [], extension: ['.sol'], path: ['contracts/1_Storage.sol'], pattern: [] }, null, null) }) @@ -378,12 +378,30 @@ module.exports = { // MODAL - 'Should open 2 alert in a row and trigger 2 toaster in between #group9': function (browser: NightwatchBrowser) { + 'Should open alerts from script #group9': function (browser: NightwatchBrowser) { browser .frameParent() .useCss() .addFile('test_modal.js', { content: testModalToasterApi }) .executeScript('remix.execute(\'test_modal.js\')') + .useCss() + .waitForElementVisible('*[data-id="test_id_1_ModalDialogModalBody-react"]') + .assert.containsText('*[data-id="test_id_1_ModalDialogModalBody-react"]', 'message 1') + .modalFooterOKClick('test_id_1_') + // check the script runner notifications + .waitForElementVisible('*[data-id="test_id_2_ModalDialogModalBody-react"]') + .assert.containsText('*[data-id="test_id_2_ModalDialogModalBody-react"]', 'message 2') + .modalFooterOKClick('test_id_2_') + .waitForElementVisible('*[data-id="test_id_3_ModalDialogModalBody-react"]') + .modalFooterOKClick('test_id_3_') + .journalLastChildIncludes('default value... ') // check the return value of the prompt + .waitForElementVisible('*[data-shared="tooltipPopup"]') + .waitForElementContainsText('*[data-shared="tooltipPopup"]', 'I am a toast') + .waitForElementContainsText('*[data-shared="tooltipPopup"]', 'I am a re-toast') + + }, + 'Should open 2 alerts from localplugin #group9': function (browser: NightwatchBrowser) { + browser .clickLaunchIcon('localPlugin') .useXpath() // @ts-ignore @@ -398,21 +416,9 @@ module.exports = { .waitForElementVisible('*[data-id="test_id_1_local_pluginModalDialogModalBody-react"]') .assert.containsText('*[data-id="test_id_1_local_pluginModalDialogModalBody-react"]', 'message from local plugin') .modalFooterOKClick('test_id_1_local_plugin') - // check the script runner notifications - .waitForElementVisible('*[data-id="test_id_1_ModalDialogModalBody-react"]') - .assert.containsText('*[data-id="test_id_1_ModalDialogModalBody-react"]', 'message 1') - .modalFooterOKClick('test_id_1_') - .waitForElementVisible('*[data-id="test_id_2_ModalDialogModalBody-react"]') - .assert.containsText('*[data-id="test_id_2_ModalDialogModalBody-react"]', 'message 2') - .modalFooterOKClick('test_id_2_') - .waitForElementVisible('*[data-id="test_id_3_ModalDialogModalBody-react"]') - .modalFooterOKClick('test_id_3_') - .journalLastChildIncludes('default value... ') // check the return value of the prompt // check the toasters .waitForElementVisible('*[data-shared="tooltipPopup"]') .waitForElementContainsText('*[data-shared="tooltipPopup"]', 'message toast from local plugin') - .waitForElementContainsText('*[data-shared="tooltipPopup"]', 'I am a toast') - .waitForElementContainsText('*[data-shared="tooltipPopup"]', 'I am a re-toast') } } diff --git a/apps/remix-ide-e2e/src/tests/search.test.ts b/apps/remix-ide-e2e/src/tests/search.test.ts new file mode 100644 index 0000000000..19fe83da3c --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/search.test.ts @@ -0,0 +1,122 @@ +'use strict' + +import { NightwatchBrowser } from 'nightwatch' +import init from '../helpers/init' + +module.exports = { + + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done, 'http://127.0.0.1:8080', true) + }, + 'Should find text': function (browser: NightwatchBrowser) { + browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]') + .click('*[plugin="search"]').waitForElementVisible('*[id="search_input"]') + .setValue('*[id="search_input"]', 'read').pause(1000) + .waitForElementContainsText('*[data-id="search_results"]', '3_BALLOT.SOL', 60000) + .waitForElementContainsText('*[data-id="search_results"]', 'contracts', 60000) + .waitForElementContainsText('*[data-id="search_results"]', 'README.TXT', 60000) + .waitForElementContainsText('*[data-id="search_results"]', 'file must') + .waitForElementContainsText('*[data-id="search_results"]', 'be compiled') + .waitForElementContainsText('*[data-id="search_results"]', 'that person al') + .waitForElementContainsText('*[data-id="search_results"]', 'sender.voted') + .waitForElementContainsText('*[data-id="search_results"]', 'read') + .elements('css selector','.search_plugin_search_line', (res) => { + Array.isArray(res.value) && browser.assert.equal(res.value.length, 6) + }) + }, + 'Should find regex': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="search_use_regex"]').click('*[data-id="search_use_regex"]') + .waitForElementVisible('*[id="search_input"]') + .clearValue('*[id="search_input"]') + .setValue('*[id="search_input"]', '^contract').pause(1000) + .waitForElementContainsText('*[data-id="search_results"]', '3_BALLOT.SOL', 60000) + .waitForElementContainsText('*[data-id="search_results"]', '2_OWNER.SOL', 60000) + .waitForElementContainsText('*[data-id="search_results"]', '1_STORAGE.SOL', 60000) + .waitForElementContainsText('*[data-id="search_results"]', '4_BALLOT_TEST.SOL', 60000) + .waitForElementContainsText('*[data-id="search_results"]', 'tests', 60000) + .elements('css selector','.search_plugin_search_line', (res) => { + Array.isArray(res.value) && browser.assert.equal(res.value.length, 4) + }) + }, + 'Should find matchcase': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="search_use_regex"]').click('*[data-id="search_use_regex"]') + .waitForElementVisible('*[data-id="search_case_sensitive"]').click('*[data-id="search_case_sensitive"]') + .elements('css selector','.search_plugin_search_line', (res) => { + Array.isArray(res.value) && browser.assert.equal(res.value.length, 0) + }) + .clearValue('*[id="search_input"]') + .setValue('*[id="search_input"]', 'Contract').pause(1000) + .elements('css selector','.search_plugin_search_line', (res) => { + Array.isArray(res.value) && browser.assert.equal(res.value.length, 6) + }) + .waitForElementContainsText('*[data-id="search_results"]', 'DEPLOY_ETHERS.JS', 60000) + .waitForElementContainsText('*[data-id="search_results"]', 'DEPLOY_WEB3.JS', 60000) + .waitForElementContainsText('*[data-id="search_results"]', 'scripts', 60000) + }, + 'Should find matchword': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[data-id="search_case_sensitive"]').click('*[data-id="search_case_sensitive"]') + .waitForElementVisible('*[data-id="search_whole_word"]').click('*[data-id="search_whole_word"]') + .clearValue('*[id="search_input"]') + .setValue('*[id="search_input"]', 'contract').pause(1000) + .elements('css selector','.search_plugin_search_line', (res) => { + Array.isArray(res.value) && browser.assert.equal(res.value.length, 27) + }) + }, + 'Should replace text': function (browser: NightwatchBrowser) { + browser + .setValue('*[id="search_replace"]', 'replacing').pause(1000) + .waitForElementVisible('*[data-id="contracts/2_Owner.sol-30-71"]') + .moveToElement('*[data-id="contracts/2_Owner.sol-30-71"]', 10, 10) + .waitForElementVisible('*[data-id="replace-contracts/2_Owner.sol-30-71"]') + .click('*[data-id="replace-contracts/2_Owner.sol-30-71"]').pause(2000). + modalFooterOKClick('confirmreplace').pause(2000). + getEditorValue((content) => { + browser.assert.ok(content.includes('replacing deployer for a constructor'), 'should replace text ok') + }) + }, + 'Should replace text without confirmation': function (browser: NightwatchBrowser) { + browser.click('*[data-id="confirm_replace_label"]').pause(500) + .clearValue('*[id="search_input"]') + .setValue('*[id="search_input"]', 'replacing').pause(1000) + .setValue('*[id="search_replace"]', '2').pause(1000) + .waitForElementVisible('*[data-id="contracts/2_Owner.sol-30-71"]') + .moveToElement('*[data-id="contracts/2_Owner.sol-30-71"]', 10, 10) + .waitForElementVisible('*[data-id="replace-contracts/2_Owner.sol-30-71"]') + .click('*[data-id="replace-contracts/2_Owner.sol-30-71"]').pause(2000). + getEditorValue((content) => { + browser.assert.ok(content.includes('replacing2 deployer for a constructor'), 'should replace text ok') + }) + }, + 'Should find text with include': function (browser: NightwatchBrowser) { + browser + .clearValue('*[id="search_input"]') + .setValue('*[id="search_input"]', 'contract').pause(1000) + .setValue('*[id="search_include"]', 'contracts/**').pause(2000) + .elements('css selector','.search_plugin_search_line', (res) => { + Array.isArray(res.value) && browser.assert.equal(res.value.length, 4) + }) + }, + 'Should find text with exclude': function (browser: NightwatchBrowser) { + browser + .clearValue('*[id="search_include"]').pause(2000) + .setValue('*[id="search_include"]', '**').pause(2000) + .elements('css selector','.search_plugin_search_line', (res) => { + Array.isArray(res.value) && browser.assert.equal(res.value.length, 26) + }) + .setValue('*[id="search_exclude"]', ',contracts/**').pause(2000) + .elements('css selector','.search_plugin_search_line', (res) => { + Array.isArray(res.value) && browser.assert.equal(res.value.length, 22) + }) + }, + 'should clear search': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[id="search_input"]') + .setValue('*[id="search_input"]', 'nodata').pause(1000) + .elements('css selector','.search_plugin_search_line', (res) => { + Array.isArray(res.value) && browser.assert.equal(res.value.length, 0) + }) + } +} \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts index 30d1a2c83c..bc1bb12cd7 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts @@ -283,9 +283,7 @@ module.exports = { .waitForElementVisible('*[data-id="dropdownPanelSolidityLocals"]').pause(1000) .click('*[data-id="dropdownPanelSolidityLocals"]') .waitForElementContainsText('*[data-id="solidityLocals"]', 'no locals', 60000) - // eslint-disable-next-line dot-notation - .execute(function () { document.getElementById('slider')['value'] = '315' }) // It only moves slider to 315 but vm traces are not updated - .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) + .goToVMTraceStep(316) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'vote(proposal)', 60000) .pause(5000) @@ -295,9 +293,7 @@ module.exports = { .scrollAndClick('#Check_winning_proposal_passed') .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalPassed()', 60000) - // eslint-disable-next-line dot-notation - .execute(function () { document.getElementById('slider')['value'] = '1450' }) - .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) + .goToVMTraceStep(1451) .waitForElementContainsText('*[data-id="functionPanel"]', 'equal(a, b, message)', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalPassed()', 60000) // remix_test.sol should be opened in editor @@ -307,9 +303,7 @@ module.exports = { .scrollAndClick('#Check_winning_proposal_again') .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalAgain()', 60000) - // eslint-disable-next-line dot-notation - .execute(function () { document.getElementById('slider')['value'] = '1150' }) - .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) + .goToVMTraceStep(1151) .waitForElementContainsText('*[data-id="functionPanel"]', 'equal(a, b, message)', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalAgain()', 60000) .pause(5000) @@ -317,9 +311,7 @@ module.exports = { .scrollAndClick('#Check_winnin_proposal_with_return_value').pause(5000) .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinninProposalWithReturnValue()', 60000) - // eslint-disable-next-line dot-notation - .execute(function () { document.getElementById('slider')['value'] = '320' }) - .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) + .goToVMTraceStep(321) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinninProposalWithReturnValue()', 60000) .clickLaunchIcon('filePanel') .pause(2000) diff --git a/apps/remix-ide-e2e/src/tests/stress.editor.ts b/apps/remix-ide-e2e/src/tests/stress.editor.ts new file mode 100644 index 0000000000..ddb007b12a --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/stress.editor.ts @@ -0,0 +1,195 @@ +'use strict' + +import { NightwatchBrowser } from 'nightwatch' +import init from '../helpers/init' + +module.exports = { + + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done) + }, + + 'Should create 10 files, reload, and check if the files are saved': function (browser: NightwatchBrowser) { + const contents = {} + const checkContent = function (i, done) { + const name = 'test_' + i + '.sol' + browser + .openFile(name) + .pause(500) + .getEditorValue((content) => { + browser.assert.ok(content === contents[i]) + done() + }) + } + browser.clickLaunchIcon('filePanel').perform((done) => { + let contentEditSet = content.slice() + for (let i = 0; i < 10; i++) { + contentEditSet += contentEditSet + contents[i] = contentEditSet + const name = 'test_' + i + '.sol' + browser.click('[data-id="fileExplorerNewFilecreateNewFile"]') + .waitForElementContainsText('*[data-id$="/blank"]', '', 60000) + .sendKeys('*[data-id$="/blank"] .remixui_items', name) + .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER) + .waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000) + .setEditorValue(contentEditSet) + } + done() + }).pause(10000).refresh() + .perform(done => checkContent(0, done)) + .perform(done => checkContent(1, done)) + .perform(done => checkContent(2, done)) + .perform(done => checkContent(3, done)) + .perform(done => checkContent(4, done)) + .perform(done => checkContent(5, done)) + .perform(done => checkContent(6, done)) + .perform(done => checkContent(7, done)) + .perform(done => checkContent(8, done)) + .perform(done => checkContent(9, done)) + .end() + } +} + +const content = ` +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title Ballot + * @dev Implements voting process along with vote delegation| + */ +contract Ballot { + + 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; + +function () test { + + /** + * @dev Create a new ballot to choose one of 'proposalNames'. + * @param proposalNames names of proposals + */ + constructor(bytes32[] memory proposalNames) { + 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; + } +} + +` diff --git a/apps/remix-ide-e2e/src/tests/url.spec.ts b/apps/remix-ide-e2e/src/tests/url.spec.ts index 13227238f7..56a94e34a9 100644 --- a/apps/remix-ide-e2e/src/tests/url.spec.ts +++ b/apps/remix-ide-e2e/src/tests/url.spec.ts @@ -104,6 +104,21 @@ module.exports = { .verify.elementPresent('#runs:disabled') .click('[for="optimize"') .verify.attributeEquals('#runs', 'value', '200') + }, + + 'Should load json files from link passed in remix URL': function (browser: NightwatchBrowser) { + browser + .url('http://localhost:8080/#optimize=false&runs=200&evmVersion=null&version=soljson-v0.6.12+commit.27d51765.js&url=https://raw.githubusercontent.com/EthVM/evm-source-verification/main/contracts/1/0x011e5846975c6463a8c6337eecf3cbf64e328884/input.json') + .refresh() + .pause(5000) + .waitForElementPresent('*[data-id="workspacesSelect"] option[value="code-sample"]') + .openFile('@openzeppelin') + .openFile('@openzeppelin/contracts') + .openFile('@openzeppelin/contracts/access') + .openFile('@openzeppelin/contracts/access/AccessControl.sol') + .openFile('contracts') + .openFile('contracts/governance') + .openFile('contracts/governance/UnionGovernor.sol') .end() } } diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 033c1b1bb0..732ef24c4a 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -30,6 +30,7 @@ const isElectron = require('is-electron') const remixLib = require('@remix-project/remix-lib') import { QueryParams } from '@remix-project/remix-lib' +import { SearchPlugin } from './app/tabs/search' const Storage = remixLib.Storage const RemixDProvider = require('./app/files/remixDProvider') const Config = require('./config') @@ -147,6 +148,9 @@ class AppComponent { // ----------------- Storage plugin --------------------------------- const storagePlugin = new StoragePlugin() + //----- search + const search = new SearchPlugin() + // ----------------- import content service ------------------------ const contentImport = new CompilerImports() @@ -221,7 +225,8 @@ class AppComponent { dGitProvider, storagePlugin, hardhatProvider, - this.walkthroughService + this.walkthroughService, + search ]) // LAYOUT & SYSTEM VIEWS @@ -332,7 +337,7 @@ class AppComponent { await this.appManager.activatePlugin(['settings', 'config']) await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['settings']) - await this.appManager.activatePlugin(['walkthrough','storage']) + await this.appManager.activatePlugin(['walkthrough','storage', 'search']) this.appManager.on( 'filePanel', diff --git a/apps/remix-ide/src/app/components/hidden-panel.tsx b/apps/remix-ide/src/app/components/hidden-panel.tsx index bfdff5a11a..31c7a0cb0b 100644 --- a/apps/remix-ide/src/app/components/hidden-panel.tsx +++ b/apps/remix-ide/src/app/components/hidden-panel.tsx @@ -1,9 +1,9 @@ // eslint-disable-next-line no-use-before-define import React from 'react' -import ReactDOM from 'react-dom' // eslint-disable-line import { AbstractPanel } from './panel' import * as packageJson from '../../../../../package.json' import { RemixPluginPanel } from '@remix-ui/panel' +import { PluginViewWrapper } from '@remix-ui/helper' const profile = { name: 'hiddenPanel', @@ -15,6 +15,7 @@ const profile = { export class HiddenPanel extends AbstractPanel { el: HTMLElement + dispatch: React.Dispatch = () => {} constructor () { super(profile) this.el = document.createElement('div') @@ -27,11 +28,23 @@ export class HiddenPanel extends AbstractPanel { this.renderComponent() } - render () { - return this.el + updateComponent (state: any) { + return } plugins={state.plugins}/> + } + + setDispatch (dispatch: React.Dispatch) { + this.dispatch = dispatch + } + + render() { + return ( +
+ ); } renderComponent () { - ReactDOM.render(} plugins={this.plugins}/>, this.el) + this.dispatch({ + plugins: this.plugins, + }) } } diff --git a/apps/remix-ide/src/app/components/main-panel.tsx b/apps/remix-ide/src/app/components/main-panel.tsx index b9d180f194..615e03690d 100644 --- a/apps/remix-ide/src/app/components/main-panel.tsx +++ b/apps/remix-ide/src/app/components/main-panel.tsx @@ -1,8 +1,8 @@ import React from 'react' // eslint-disable-line import { AbstractPanel } from './panel' -import ReactDOM from 'react-dom' // eslint-disable-line import { RemixPluginPanel } from '@remix-ui/panel' import packageJson from '../../../../../package.json' +import { PluginViewWrapper } from '@remix-ui/helper' const profile = { name: 'mainPanel', @@ -14,6 +14,7 @@ const profile = { export class MainPanel extends AbstractPanel { element: HTMLDivElement + dispatch: React.Dispatch = () => {} constructor (config) { super(profile) this.element = document.createElement('div') @@ -22,6 +23,10 @@ export class MainPanel extends AbstractPanel { // this.config = config } + setDispatch (dispatch: React.Dispatch) { + this.dispatch = dispatch + } + onActivation () { this.renderComponent() } @@ -47,11 +52,17 @@ export class MainPanel extends AbstractPanel { this.renderComponent() } - render () { - return this.element + renderComponent () { + this.dispatch({ + plugins: this.plugins + }) + } + + render() { + return
} - renderComponent () { - ReactDOM.render(} plugins={this.plugins}/>, this.element) + updateComponent (state: any) { + return } plugins={state.plugins}/> } } diff --git a/apps/remix-ide/src/app/components/plugin-manager-component.js b/apps/remix-ide/src/app/components/plugin-manager-component.js index 72acd7cbf6..00013ee545 100644 --- a/apps/remix-ide/src/app/components/plugin-manager-component.js +++ b/apps/remix-ide/src/app/components/plugin-manager-component.js @@ -1,8 +1,8 @@ import { ViewPlugin } from '@remixproject/engine-web' import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import {RemixUiPluginManager} from '@remix-ui/plugin-manager' // eslint-disable-line import * as packageJson from '../../../../../package.json' +import { PluginViewWrapper } from '@remix-ui/helper' const _paq = window._paq = window._paq || [] const profile = { @@ -31,6 +31,7 @@ class PluginManagerComponent extends ViewPlugin { this.inactivePlugins = [] this.activeProfiles = this.appManager.actives this._paq = _paq + this.dispatch = null this.listenOnEvent() } @@ -40,7 +41,7 @@ class PluginManagerComponent extends ViewPlugin { * RemixAppManager * @param {string} name name of Plugin */ - isActive (name) { + isActive = (name) =>{ return this.appManager.actives.includes(name) } @@ -49,7 +50,7 @@ class PluginManagerComponent extends ViewPlugin { * RemixAppManager to enable plugin activation * @param {string} name name of Plugin */ - activateP (name) { + activateP = (name) => { this.appManager.activatePlugin(name) _paq.push(['trackEvent', 'manager', 'activate', name]) } @@ -60,7 +61,7 @@ class PluginManagerComponent extends ViewPlugin { * @param {Profile} pluginName * @returns {void} */ - async activateAndRegisterLocalPlugin (localPlugin) { + activateAndRegisterLocalPlugin = async (localPlugin) => { if (localPlugin) { this.engine.register(localPlugin) this.appManager.activatePlugin(localPlugin.profile.name) @@ -75,28 +76,33 @@ class PluginManagerComponent extends ViewPlugin { * of the plugin * @param {string} name name of Plugin */ - deactivateP (name) { + deactivateP = (name) => { this.call('manager', 'deactivatePlugin', name) _paq.push(['trackEvent', 'manager', 'deactivate', name]) } - onActivation () { + setDispatch (dispatch) { + this.dispatch = dispatch this.renderComponent() } + updateComponent(state){ + return + } + renderComponent () { - ReactDOM.render( - , - this.htmlElement) + if(this.dispatch) this.dispatch({...this, activePlugins: this.activePlugins, inactivePlugins: this.inactivePlugins}) } render () { - return this.htmlElement + return ( +
+ ); + } - getAndFilterPlugins (filter) { + getAndFilterPlugins = (filter) => { this.filter = typeof filter === 'string' ? filter.toLowerCase() : this.filter const isFiltered = (profile) => (profile.displayName ? profile.displayName : profile.name).toLowerCase().includes(this.filter) diff --git a/apps/remix-ide/src/app/components/preload.tsx b/apps/remix-ide/src/app/components/preload.tsx new file mode 100644 index 0000000000..f599f9f8b3 --- /dev/null +++ b/apps/remix-ide/src/app/components/preload.tsx @@ -0,0 +1,130 @@ +import { RemixApp } from '@remix-ui/app' +import React, { useEffect, useRef, useState } from 'react' +import { render } from 'react-dom' +import * as packageJson from '../../../../../package.json' +import { fileSystem, fileSystems } from '../files/fileSystem' +import { indexedDBFileSystem } from '../files/filesystems/indexedDB' +import { localStorageFS } from '../files/filesystems/localStorage' +import { fileSystemUtility, migrationTestData } from '../files/filesystems/fileSystemUtility' +import './styles/preload.css' +const _paq = window._paq = window._paq || [] + +export const Preload = () => { + + const [supported, setSupported] = useState(true) + const [error, setError] = useState(false) + const [showDownloader, setShowDownloader] = useState(false) + const remixFileSystems = useRef(new fileSystems()) + const remixIndexedDB = useRef(new indexedDBFileSystem()) + const localStorageFileSystem = useRef(new localStorageFS()) + // url parameters to e2e test the fallbacks and error warnings + const testmigrationFallback = useRef(window.location.hash.includes('e2e_testmigration_fallback=true') && window.location.host === '127.0.0.1:8080' && window.location.protocol === 'http:') + const testmigrationResult = useRef(window.location.hash.includes('e2e_testmigration=true') && window.location.host === '127.0.0.1:8080' && window.location.protocol === 'http:') + const testBlockStorage = useRef(window.location.hash.includes('e2e_testblock_storage=true') && window.location.host === '127.0.0.1:8080' && window.location.protocol === 'http:') + + function loadAppComponent() { + import('../../app').then((AppComponent) => { + const appComponent = new AppComponent.default() + appComponent.run().then(() => { + render( + <> + + , + document.getElementById('root') + ) + }) + }).catch(err => { + _paq.push(['trackEvent', 'Preload', 'error', err && err.message]) + console.log('Error loading Remix:', err) + setError(true) + }) + } + + const downloadBackup = async () => { + setShowDownloader(false) + const fsUtility = new fileSystemUtility() + await fsUtility.downloadBackup(remixFileSystems.current.fileSystems['localstorage']) + await migrateAndLoad() + } + + const migrateAndLoad = async () => { + setShowDownloader(false) + const fsUtility = new fileSystemUtility() + const migrationResult = await fsUtility.migrate(localStorageFileSystem.current, remixIndexedDB.current) + _paq.push(['trackEvent', 'Migrate', 'result', migrationResult?'success' : 'fail']) + await setFileSystems() + } + + const setFileSystems = async() => { + const fsLoaded = await remixFileSystems.current.setFileSystem([(testmigrationFallback.current || testBlockStorage.current)? null: remixIndexedDB.current, testBlockStorage.current? null:localStorageFileSystem.current]) + if (fsLoaded) { + console.log(fsLoaded.name + ' activated') + _paq.push(['trackEvent', 'Storage', 'activate', fsLoaded.name]) + loadAppComponent() + } else { + _paq.push(['trackEvent', 'Storage', 'error', 'no supported storage']) + setSupported(false) + } + } + + const testmigration = async() => { + if (testmigrationResult.current) { + const fsUtility = new fileSystemUtility() + fsUtility.populateWorkspace(migrationTestData, remixFileSystems.current.fileSystems['localstorage'].fs) + } + } + + useEffect(() => { + async function loadStorage() { + await remixFileSystems.current.addFileSystem(remixIndexedDB.current) || _paq.push(['trackEvent', 'Storage', 'error', 'indexedDB not supported']) + await remixFileSystems.current.addFileSystem(localStorageFileSystem.current) || _paq.push(['trackEvent', 'Storage', 'error', 'localstorage not supported']) + await testmigration() + remixIndexedDB.current.loaded && await remixIndexedDB.current.checkWorkspaces() + localStorageFileSystem.current.loaded && await localStorageFileSystem.current.checkWorkspaces() + remixIndexedDB.current.loaded && ( (remixIndexedDB.current.hasWorkSpaces || !localStorageFileSystem.current.hasWorkSpaces)? await setFileSystems():setShowDownloader(true)) + !remixIndexedDB.current.loaded && await setFileSystems() + } + loadStorage() + }, []) + + return <> +
+
+ {logo} +
+ REMIX IDE +
+ v{packageJson.version} +
+
+ {!supported ? +
+ Your browser does not support any of the filesytems required by Remix. + Either change the settings in your browser or use a supported browser. +
: null} + {error ? +
+ An unknown error has occured loading the application. +
: null} + {showDownloader ? +
+ This app will be updated now. Please download a backup of your files now to make sure you don't lose your work. +

+ You don't need to do anything else, your files will be available when the app loads. +
{ await downloadBackup() }} data-id='downloadbackup-btn' className='btn btn-primary mt-1'>download backup
+
{ await migrateAndLoad() }} data-id='skipbackup-btn' className='btn btn-primary mt-1'>skip backup
+
: null} + {(supported && !error && !showDownloader) ? +
+ +
: null} +
+ +} + + +const logo = + + + + \ No newline at end of file diff --git a/apps/remix-ide/src/app/components/side-panel.tsx b/apps/remix-ide/src/app/components/side-panel.tsx index b5e9dcd3df..bf82defe55 100644 --- a/apps/remix-ide/src/app/components/side-panel.tsx +++ b/apps/remix-ide/src/app/components/side-panel.tsx @@ -1,10 +1,10 @@ // eslint-disable-next-line no-use-before-define import React from 'react' -import ReactDOM from 'react-dom' import { AbstractPanel } from './panel' import { RemixPluginPanel } from '@remix-ui/panel' import packageJson from '../../../../../package.json' import RemixUIPanelHeader from 'libs/remix-ui/panel/src/lib/plugins/panel-header' +import { PluginViewWrapper } from '@remix-ui/helper' // const csjs = require('csjs-inject') const sidePanel = { @@ -17,6 +17,7 @@ const sidePanel = { export class SidePanel extends AbstractPanel { sideelement: any + dispatch: React.Dispatch = () => {} constructor() { super(sidePanel) this.sideelement = document.createElement('section') @@ -78,11 +79,23 @@ export class SidePanel extends AbstractPanel { this.renderComponent() } - render() { - return this.sideelement + setDispatch (dispatch: React.Dispatch) { + this.dispatch = dispatch + } + + render() { + return ( +
+ ); + } + + updateComponent(state: any) { + return } plugins={state.plugins} /> } renderComponent() { - ReactDOM.render(} plugins={this.plugins} />, this.sideelement) + this.dispatch({ + plugins: this.plugins + }) } } diff --git a/apps/remix-ide/src/app/components/styles/preload.css b/apps/remix-ide/src/app/components/styles/preload.css new file mode 100644 index 0000000000..266cbfe834 --- /dev/null +++ b/apps/remix-ide/src/app/components/styles/preload.css @@ -0,0 +1,23 @@ +.preload-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; +} + +.preload-info-container { + display: flex; + flex-direction: column; + text-align: center; + max-width: 400px; +} + +.preload-info-container .btn { + cursor: pointer; +} + +.preload-logo { + min-width: 200px; + padding-bottom: 1.5rem !important; +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/components/vertical-icons.tsx b/apps/remix-ide/src/app/components/vertical-icons.tsx index eea114b637..d8fe5706f8 100644 --- a/apps/remix-ide/src/app/components/vertical-icons.tsx +++ b/apps/remix-ide/src/app/components/vertical-icons.tsx @@ -1,11 +1,11 @@ // eslint-disable-next-line no-use-before-define import React from 'react' -import ReactDOM from 'react-dom' import packageJson from '../../../../../package.json' import { Plugin } from '@remixproject/engine' import { EventEmitter } from 'events' import { IconRecord, RemixUiVerticalIconsPanel } from '@remix-ui/vertical-icons-panel' import { Profile } from '@remixproject/plugin-utils' +import { PluginViewWrapper } from '@remix-ui/helper' const profile = { name: 'menuicons', @@ -20,6 +20,7 @@ export class VerticalIcons extends Plugin { events: EventEmitter htmlElement: HTMLDivElement icons: Record = {} + dispatch: React.Dispatch = () => {} constructor () { super(profile) this.events = new EventEmitter() @@ -28,7 +29,7 @@ export class VerticalIcons extends Plugin { } renderComponent () { - const fixedOrder = ['filePanel', 'solidity','udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager'] + const fixedOrder = ['filePanel', 'search', 'solidity','udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager'] const divived = Object.values(this.icons).map((value) => { return { ...value, @@ -46,12 +47,15 @@ export class VerticalIcons extends Plugin { ...divived.filter((value) => { return !value.isRequired }) ] - ReactDOM.render( - , - this.htmlElement) + this.dispatch({ + verticalIconsPlugin: this, + icons: sorted + }) + + } + + setDispatch (dispatch: React.Dispatch) { + this.dispatch = dispatch } onActivation () { @@ -107,7 +111,16 @@ export class VerticalIcons extends Plugin { this.events.emit('toggleContent', name) } - render () { - return this.htmlElement + updateComponent(state: any){ + return + } + + render() { + return ( +
+ ); } } diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index d15fe531a1..875835d8ff 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -1,9 +1,9 @@ 'use strict' import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import { EditorUI } from '@remix-ui/editor' // eslint-disable-line import { Plugin } from '@remixproject/engine' import * as packageJson from '../../../../../package.json' +import { PluginViewWrapper } from '@remix-ui/helper' const EventManager = require('../../lib/events') @@ -25,11 +25,12 @@ class Editor extends Plugin { remixDark: 'remix-dark' } + this.registeredDecorations = { sourceAnnotationsPerFile: {}, markerPerFile: {} } + this.currentDecorations = { sourceAnnotationsPerFile: {}, markerPerFile: {} } + // Init this.event = new EventManager() this.sessions = {} - this.sourceAnnotationsPerFile = {} - this.markerPerFile = {} this.readOnlySessions = {} this.previousInput = '' this.saveTimeout = null @@ -61,10 +62,27 @@ class Editor extends Plugin { // to be implemented by the react component this.api = {} + this.dispatch = null + this.ref = null + } + + setDispatch (dispatch) { + this.dispatch = dispatch + } + + updateComponent(state) { + return } render () { - if (this.el) return this.el + +/* if (this.el) return this.el this.el = document.createElement('div') this.el.setAttribute('id', 'editorView') @@ -76,22 +94,35 @@ class Editor extends Plugin { } } this.el.gotoLine = (line, column) => this.gotoLine(line, column || 0) - this.el.getCursorPosition = () => this.getCursorPosition() - return this.el + this.el.getCursorPosition = () => this.getCursorPosition() */ + + return
{ + this.ref = element + this.ref.currentContent = () => this.currentContent() // used by e2e test + this.ref.setCurrentContent = (value) => { + if (this.sessions[this.currentFile]) { + this.sessions[this.currentFile].setValue(value) + this._onChange(this.currentFile) + } + } + this.ref.gotoLine = (line, column) => this.gotoLine(line, column || 0) + this.ref.getCursorPosition = () => this.getCursorPosition() + this.ref.addDecoration = (marker, filePath, typeOfDecoration) => this.addDecoration(marker, filePath, typeOfDecoration) + this.ref.clearDecorationsByPlugin = (filePath, plugin, typeOfDecoration) => this.clearDecorationsByPlugin(filePath, plugin, typeOfDecoration) + this.ref.keepDecorationsFor = (name, typeOfDecoration) => this.keepDecorationsFor(name, typeOfDecoration) + }} id='editorView'> + +
} renderComponent () { - ReactDOM.render( - - , this.el) + this.dispatch({ + api: this.api, + currentThemeType: this.currentThemeType, + currentFile: this.currentFile, + events: this.events, + plugin: this + }) } triggerEvent (name, params) { @@ -108,7 +139,11 @@ class Editor extends Plugin { this.on('sidePanel', 'pluginDisabled', (name) => { this.clearAllDecorationsFor(name) }) - + this.on('fileManager', 'fileClosed', (name) => { + if (name === this.currentFile) { + this.currentFile = null + } + }) this.on('theme', 'themeLoaded', (theme) => { this.currentThemeType = theme.quality this.renderComponent() @@ -379,27 +414,15 @@ class Editor extends Plugin { if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath) const path = filePath || this.currentFile - const currentAnnotations = this[typeOfDecoration][path] - if (!currentAnnotations) return - - const newAnnotations = [] - for (const annotation of currentAnnotations) { - if (annotation.from !== plugin) newAnnotations.push(annotation) - } - - this[typeOfDecoration][path] = newAnnotations - this.renderComponent() + const { currentDecorations, registeredDecorations } = this.api.clearDecorationsByPlugin(path, plugin, typeOfDecoration, this.registeredDecorations[typeOfDecoration][filePath] || [], this.currentDecorations[typeOfDecoration][filePath] || []) + this.currentDecorations[typeOfDecoration][filePath] = currentDecorations + this.registeredDecorations[typeOfDecoration][filePath] = registeredDecorations } - keepDecorationsFor (name, typeOfDecoration) { + keepDecorationsFor (plugin, typeOfDecoration) { if (!this.currentFile) return - if (!this[typeOfDecoration][this.currentFile]) return - - const annotations = this[typeOfDecoration][this.currentFile] - for (const annotation of annotations) { - annotation.hide = annotation.from !== name - } - this.renderComponent() + const { currentDecorations } = this.api.keepDecorationsFor(this.currentFile, plugin, typeOfDecoration, this.registeredDecorations[typeOfDecoration][this.currentFile] || [], this.currentDecorations[typeOfDecoration][this.currentFile] || []) + this.currentDecorations[typeOfDecoration][this.currentFile] = currentDecorations } /** @@ -442,10 +465,13 @@ class Editor extends Plugin { const path = filePath || this.currentFile const { from } = this.currentRequest - if (!this[typeOfDecoration][path]) this[typeOfDecoration][path] = [] decoration.from = from - this[typeOfDecoration][path].push(decoration) - this.renderComponent() + + const { currentDecorations, registeredDecorations } = this.api.addDecoration(decoration, path, typeOfDecoration) + if (!this.registeredDecorations[typeOfDecoration][filePath]) this.registeredDecorations[typeOfDecoration][filePath] = [] + this.registeredDecorations[typeOfDecoration][filePath].push(...registeredDecorations) + if (!this.currentDecorations[typeOfDecoration][filePath]) this.currentDecorations[typeOfDecoration][filePath] = [] + this.currentDecorations[typeOfDecoration][filePath].push(...currentDecorations) } /** @@ -475,7 +501,7 @@ class Editor extends Plugin { discardHighlight () { const { from } = this.currentRequest for (const session in this.sessions) { - this.clearDecorationsByPlugin(session, from, 'markerPerFile') + this.clearDecorationsByPlugin(session, from, 'markerPerFile', this.registeredDecorations, this.currentDecorations) } } } diff --git a/apps/remix-ide/src/app/files/fileManager.ts b/apps/remix-ide/src/app/files/fileManager.ts index d9789f4512..2ad2ea4aa9 100644 --- a/apps/remix-ide/src/app/files/fileManager.ts +++ b/apps/remix-ide/src/app/files/fileManager.ts @@ -4,7 +4,7 @@ import * as packageJson from '../../../../../package.json' import Registry from '../state/registry' import { EventEmitter } from 'events' import { RemixAppManager } from '../../../../../libs/remix-ui/plugin-manager/src/types' -import { fileChangedToastMsg } from '@remix-ui/helper' +import { fileChangedToastMsg, storageFullMessage } from '@remix-ui/helper' import helper from '../../lib/helper.js' /* @@ -19,7 +19,7 @@ const profile = { icon: 'assets/img/fileManager.webp', permission: true, version: packageJson.version, - methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles'], + methods: ['closeAllFiles', 'closeFile', 'file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'dirList', 'fileList', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile', 'refresh', 'getProviderOf', 'getProviderByName', 'getPathFromUrl', 'getUrlFromPath', 'saveCurrentFile', 'setBatchFiles'], kind: 'file-system' } const errorMsg = { @@ -608,9 +608,10 @@ class FileManager extends Plugin { this.events.emit('noFileSelected') } else { file = this.normalize(file) - await this.saveCurrentFile() const resolved = this.getPathFromUrl(file) file = resolved.file + await this.saveCurrentFile() + if (this.currentFile() === file) return const provider = resolved.provider this._deps.config.set('currentFile', file) this.openedFiles[file] = file @@ -704,7 +705,14 @@ class FileManager extends Plugin { return collectList(path) } - isRemixDActive() { + async fileList (dirPath) { + const paths: any = await this.readdir(dirPath) + for( const path in paths) + if(paths[path].isDirectory) delete paths[path] + return Object.keys(paths) + } + + isRemixDActive () { return this.appManager.isActive('remixd') } @@ -715,8 +723,22 @@ class FileManager extends Plugin { if ((input !== null) && (input !== undefined)) { const provider = this.fileProviderOf(currentFile) if (provider) { - await provider.set(currentFile, input) + // use old content as default if save operation fails. + provider.get(currentFile, (error, oldContent) => { + provider.set(currentFile, input, (error) => { + if (error) { + if (error.message ) this.call('notification', 'toast', + error.message.indexOf( + 'LocalStorage is full') !== -1 ? storageFullMessage() + : error.message + ) + provider.set(currentFile, oldContent) + return console.error(error) + } else { this.emit('fileSaved', currentFile) + } + }) + }) } else { console.log('cannot save ' + currentFile + '. Does not belong to any explorer') } @@ -731,7 +753,7 @@ class FileManager extends Plugin { if (provider) { try{ const content = await provider.get(currentFile) - this.editor.setText(content) + if(content) this.editor.setText(content) }catch(error){ console.log(error) } diff --git a/apps/remix-ide/src/app/files/fileSystem.ts b/apps/remix-ide/src/app/files/fileSystem.ts new file mode 100644 index 0000000000..80b7663680 --- /dev/null +++ b/apps/remix-ide/src/app/files/fileSystem.ts @@ -0,0 +1,72 @@ +export class fileSystem { + name: string + enabled: boolean + available: boolean + fs: any + fsCallBack: any; + hasWorkSpaces: boolean + loaded: boolean + load: () => Promise + test: () => Promise + + constructor() { + this.available = false + this.enabled = false + this.hasWorkSpaces = false + this.loaded = false + } + + checkWorkspaces = async () => { + try { + await this.fs.stat('.workspaces') + this.hasWorkSpaces = true + } catch (e) { + + } + } + + set = async () => { + const w = (window as any) + if (!this.loaded) return false + w.remixFileSystem = this.fs + w.remixFileSystem.name = this.name + w.remixFileSystemCallback = this.fsCallBack + return true + } +} + +export class fileSystems { + fileSystems: Record + constructor() { + this.fileSystems = {} + } + + addFileSystem = async (fs: fileSystem): Promise => { + try { + this.fileSystems[fs.name] = fs + await fs.test() && await fs.load() + console.log(fs.name + ' is loaded...') + return true + } catch (e) { + console.log(fs.name + ' not available...') + return false + } + } + /** + * sets filesystem using list as fallback + * @param {string[]} names + * @returns {Promise} + */ + setFileSystem = async (filesystems?: fileSystem[]): Promise => { + for (const fs of filesystems) { + if (fs && this.fileSystems[fs.name]) { + const result = await this.fileSystems[fs.name].set() + if (result) return this.fileSystems[fs.name] + } + } + return null + } + + +} + diff --git a/apps/remix-ide/src/app/files/filesystems/fileSystemUtility.ts b/apps/remix-ide/src/app/files/filesystems/fileSystemUtility.ts new file mode 100644 index 0000000000..1b927a01e0 --- /dev/null +++ b/apps/remix-ide/src/app/files/filesystems/fileSystemUtility.ts @@ -0,0 +1,190 @@ +import { hashMessage } from "ethers/lib/utils" +import JSZip from "jszip" +import { fileSystem } from "../fileSystem" +const _paq = window._paq = window._paq || [] +export class fileSystemUtility { + migrate = async (fsFrom: fileSystem, fsTo: fileSystem) => { + try { + await fsFrom.checkWorkspaces() + await fsTo.checkWorkspaces() + + if (fsTo.hasWorkSpaces) { + console.log(`${fsTo.name} already has files`) + return true + } + + if (!fsFrom.hasWorkSpaces) { + console.log('no files to migrate') + return true + } + + const fromFiles = await this.copyFolderToJson('/', null, null, fsFrom.fs) + await this.populateWorkspace(fromFiles, fsTo.fs) + const toFiles = await this.copyFolderToJson('/', null, null, fsTo.fs) + + if (hashMessage(JSON.stringify(toFiles)) === hashMessage(JSON.stringify(fromFiles))) { + console.log('file migration successful') + return true + } else { + _paq.push(['trackEvent', 'Migrate', 'error', 'hash mismatch']) + console.log('file migration failed falling back to ' + fsFrom.name) + fsTo.loaded = false + return false + } + } catch (err) { + console.log(err) + _paq.push(['trackEvent', 'Migrate', 'error', err && err.message]) + console.log('file migration failed falling back to ' + fsFrom.name) + fsTo.loaded = false + return false + } + } + + downloadBackup = async (fs: fileSystem) => { + try { + const zip = new JSZip() + await fs.checkWorkspaces() + await this.copyFolderToJson('/', null, null, fs.fs, ({ path, content }) => { + zip.file(path, content) + }) + const blob = await zip.generateAsync({ type: 'blob' }) + const today = new Date() + const date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate() + const time = today.getHours() + 'h' + today.getMinutes() + 'min' + this.saveAs(blob, `remix-backup-at-${time}-${date}.zip`) + _paq.push(['trackEvent','Backup','download','preload']) + } catch (err) { + _paq.push(['trackEvent','Backup','error',err && err.message]) + console.log(err) + } + } + + populateWorkspace = async (json, fs) => { + for (const item in json) { + const isFolder = json[item].content === undefined + if (isFolder) { + await this.createDir(item, fs) + await this.populateWorkspace(json[item].children, fs) + } else { + await fs.writeFile(item, json[item].content, 'utf8') + } + } + } + + + /** + * copy the folder recursively + * @param {string} path is the folder to be copied over + * @param {Function} visitFile is a function called for each visited files + * @param {Function} visitFolder is a function called for each visited folders + */ + copyFolderToJson = async (path, visitFile, visitFolder, fs, cb = null) => { + visitFile = visitFile || (() => { }) + visitFolder = visitFolder || (() => { }) + return await this._copyFolderToJsonInternal(path, visitFile, visitFolder, fs, cb) + } + + /** + * copy the folder recursively (internal use) + * @param {string} path is the folder to be copied over + * @param {Function} visitFile is a function called for each visited files + * @param {Function} visitFolder is a function called for each visited folders + */ + async _copyFolderToJsonInternal(path, visitFile, visitFolder, fs, cb) { + visitFile = visitFile || function () { /* do nothing. */ } + visitFolder = visitFolder || function () { /* do nothing. */ } + + const json = {} + // path = this.removePrefix(path) + if (await fs.exists(path)) { + const items = await fs.readdir(path) + visitFolder({ path }) + if (items.length !== 0) { + for (const item of items) { + const file: any = {} + const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}` + if ((await fs.stat(curPath)).isDirectory()) { + file.children = await this._copyFolderToJsonInternal(curPath, visitFile, visitFolder, fs, cb) + } else { + file.content = await fs.readFile(curPath, 'utf8') + if (cb) cb({ path: curPath, content: file.content }) + visitFile({ path: curPath, content: file.content }) + + } + json[curPath] = file + } + } + } + return json + } + + createDir = async (path, fs) => { + const paths = path.split('/') + if (paths.length && paths[0] === '') paths.shift() + let currentCheck = '' + for (const value of paths) { + currentCheck = currentCheck + (currentCheck ? '/' : '') + value + if (!await fs.exists(currentCheck)) { + await fs.mkdir(currentCheck) + } + } + } + + saveAs = (blob, name) => { + const node = document.createElement('a') + node.download = name + node.rel = 'noopener' + node.href = URL.createObjectURL(blob) + setTimeout(function () { URL.revokeObjectURL(node.href) }, 4E4) // 40s + setTimeout(function () { + try { + node.dispatchEvent(new MouseEvent('click')) + } catch (e) { + const evt = document.createEvent('MouseEvents') + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, + 20, false, false, false, false, 0, null) + node.dispatchEvent(evt) + } + }, 0) // 40s + } +} + + +/* eslint-disable no-template-curly-in-string */ +export const migrationTestData = { + '.workspaces': { + children: { + '.workspaces/default_workspace': { + children: { + '.workspaces/default_workspace/README.txt': { + content: 'TEST README' + } + } + }, + '.workspaces/emptyspace': { + + }, + '.workspaces/workspace_test': { + children: { + '.workspaces/workspace_test/TEST_README.txt': { + content: 'TEST README' + }, + '.workspaces/workspace_test/test_contracts': { + children: { + '.workspaces/workspace_test/test_contracts/1_Storage.sol': { + content: 'testing' + }, + '.workspaces/workspace_test/test_contracts/artifacts': { + children: { + '.workspaces/workspace_test/test_contracts/artifacts/Storage_metadata.json': { + content: '{ "test": "data" }' + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/files/filesystems/indexedDB.ts b/apps/remix-ide/src/app/files/filesystems/indexedDB.ts new file mode 100644 index 0000000000..5d20a52061 --- /dev/null +++ b/apps/remix-ide/src/app/files/filesystems/indexedDB.ts @@ -0,0 +1,91 @@ +import LightningFS from "@isomorphic-git/lightning-fs" +import { fileSystem } from "../fileSystem" + +export class IndexedDBStorage extends LightningFS { + base: LightningFS.PromisifedFS + addSlash: (file: string) => string + extended: { exists: (path: string) => Promise; rmdir: (path: any) => Promise; readdir: (path: any) => Promise; unlink: (path: any) => Promise; mkdir: (path: any) => Promise; readFile: (path: any, options: any) => Promise; rename: (from: any, to: any) => Promise; writeFile: (path: any, content: any, options: any) => Promise; stat: (path: any) => Promise; init(name: string, opt?: LightningFS.FSConstructorOptions): void; activate(): Promise; deactivate(): Promise; lstat(filePath: string): Promise; readlink(filePath: string): Promise; symlink(target: string, filePath: string): Promise } + constructor(name: string) { + super(name) + this.addSlash = (file) => { + if (!file.startsWith('/')) file = '/' + file + return file + } + this.base = this.promises + this.extended = { + ...this.promises, + exists: async (path: string) => { + return new Promise((resolve) => { + this.base.stat(this.addSlash(path)).then(() => resolve(true)).catch(() => resolve(false)) + }) + }, + rmdir: async (path) => { + return this.base.rmdir(this.addSlash(path)) + }, + readdir: async (path) => { + return this.base.readdir(this.addSlash(path)) + }, + unlink: async (path) => { + return this.base.unlink(this.addSlash(path)) + }, + mkdir: async (path) => { + return this.base.mkdir(this.addSlash(path)) + }, + readFile: async (path, options) => { + return this.base.readFile(this.addSlash(path), options) + }, + rename: async (from, to) => { + return this.base.rename(this.addSlash(from), this.addSlash(to)) + }, + writeFile: async (path, content, options) => { + return this.base.writeFile(this.addSlash(path), content, options) + }, + stat: async (path) => { + return this.base.stat(this.addSlash(path)) + } + } + } +} + + +export class indexedDBFileSystem extends fileSystem { + constructor() { + super() + this.name = 'indexedDB' + } + + load = async () => { + return new Promise((resolve, reject) => { + try { + const fs = new IndexedDBStorage('RemixFileSystem') + fs.init('RemixFileSystem') + this.fs = fs.extended + this.fsCallBack = fs + this.loaded = true + resolve(true) + } catch (e) { + reject(e) + } + }) + } + + test = async () => { + return new Promise((resolve, reject) => { + if (!window.indexedDB) { + this.available = false + reject('No indexedDB on window') + } + const request = window.indexedDB.open("RemixTestDataBase"); + request.onerror = () => { + this.available = false + reject('Error creating test database') + }; + request.onsuccess = () => { + window.indexedDB.deleteDatabase("RemixTestDataBase"); + this.available = true + resolve(true) + }; + }) + } + +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/files/filesystems/localStorage.ts b/apps/remix-ide/src/app/files/filesystems/localStorage.ts new file mode 100644 index 0000000000..8346a37976 --- /dev/null +++ b/apps/remix-ide/src/app/files/filesystems/localStorage.ts @@ -0,0 +1,57 @@ +import { fileSystem } from "../fileSystem"; + +export class localStorageFS extends fileSystem { + + constructor() { + super() + this.name = 'localstorage' + } + load = async () => { + const me = this + return new Promise((resolve, reject) => { + try { + const w = window as any + w.BrowserFS.install(window) + w.BrowserFS.configure({ + fs: 'LocalStorage' + }, async function (e) { + if (e) { + console.log('BrowserFS Error: ' + e) + reject(e) + } else { + me.fs = { ...window.require('fs') } + me.fsCallBack = window.require('fs') + me.fs.readdir = me.fs.readdirSync + me.fs.readFile = me.fs.readFileSync + me.fs.writeFile = me.fs.writeFileSync + me.fs.stat = me.fs.statSync + me.fs.unlink = me.fs.unlinkSync + me.fs.rmdir = me.fs.rmdirSync + me.fs.mkdir = me.fs.mkdirSync + me.fs.rename = me.fs.renameSync + me.fs.exists = me.fs.existsSync + me.loaded = true + resolve(true) + } + }) + } catch (e) { + console.log('BrowserFS is not ready!') + reject(e) + } + }) + } + + test = async () => { + return new Promise((resolve, reject) => { + const test = 'test'; + try { + localStorage.setItem(test, test); + localStorage.removeItem(test); + resolve(true) + } catch(e) { + reject(e) + } + }) + } + +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/files/remixDProvider.js b/apps/remix-ide/src/app/files/remixDProvider.js index a36b6d8ec9..c52055e04f 100644 --- a/apps/remix-ide/src/app/files/remixDProvider.js +++ b/apps/remix-ide/src/app/files/remixDProvider.js @@ -98,20 +98,19 @@ module.exports = class RemixDProvider extends FileProvider { }) } - get (path, cb) { + async get (path, cb) { if (!this._isReady) return cb && cb('provider not ready') var unprefixedpath = this.removePrefix(path) - this._appManager.call('remixd', 'get', { path: unprefixedpath }) - .then((file) => { - this.filesContent[path] = file.content - if (file.readonly) { this._readOnlyFiles[path] = 1 } - cb(null, file.content) - }).catch((error) => { - if (error) console.log(error) - // display the last known content. - // TODO should perhaps better warn the user that the file is not synced. - return cb(null, this.filesContent[path]) - }) + try{ + const file = await this._appManager.call('remixd', 'get', { path: unprefixedpath }) + this.filesContent[path] = file.content + if (file.readonly) { this._readOnlyFiles[path] = 1 } + if(cb) cb(null, file.content) + return file.content + } catch(error) { + if (error) console.log(error) + if(cb) return cb(null, this.filesContent[path]) + } } async set (path, content, cb) { diff --git a/apps/remix-ide/src/app/files/workspaceFileProvider.js b/apps/remix-ide/src/app/files/workspaceFileProvider.js index c616d3bd74..505be72b8c 100644 --- a/apps/remix-ide/src/app/files/workspaceFileProvider.js +++ b/apps/remix-ide/src/app/files/workspaceFileProvider.js @@ -32,6 +32,7 @@ class WorkspaceFileProvider extends FileProvider { removePrefix (path) { path = path.replace(/^\/|\/$/g, '') // remove first and last slash + path = path.replace(/^\.\/+/, '') // remove ./ from start of string if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path const splitPath = path.split('/') diff --git a/apps/remix-ide/src/app/panels/file-panel.js b/apps/remix-ide/src/app/panels/file-panel.js index 47e40a2f5f..6c5e4d0235 100644 --- a/apps/remix-ide/src/app/panels/file-panel.js +++ b/apps/remix-ide/src/app/panels/file-panel.js @@ -2,7 +2,6 @@ import { ViewPlugin } from '@remixproject/engine-web' import * as packageJson from '../../../../../package.json' import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line import Registry from '../state/registry' import { RemixdHandle } from '../plugins/remixd-handle' @@ -58,18 +57,8 @@ module.exports = class Filepanel extends ViewPlugin { this.currentWorkspaceMetadata = {} } - onActivation () { - this.renderComponent() - } - render () { - return this.el - } - - renderComponent () { - ReactDOM.render( - - , this.el) + return
} /** diff --git a/apps/remix-ide/src/app/panels/tab-proxy.js b/apps/remix-ide/src/app/panels/tab-proxy.js index a713b4bd02..1671648a36 100644 --- a/apps/remix-ide/src/app/panels/tab-proxy.js +++ b/apps/remix-ide/src/app/panels/tab-proxy.js @@ -1,8 +1,7 @@ import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import { Plugin } from '@remixproject/engine' import { TabsUI } from '@remix-ui/tabs' -import { getPathIcon } from '@remix-ui/helper' +import { PluginViewWrapper, getPathIcon } from '@remix-ui/helper' const EventEmitter = require('events') const profile = { @@ -11,7 +10,6 @@ const profile = { kind: 'other' } -// @todo(#650) Merge this with MainPanel into one plugin export class TabProxy extends Plugin { constructor (fileManager, editor) { super(profile) @@ -23,6 +21,7 @@ export class TabProxy extends Plugin { this._handlers = {} this.loadedTabs = [] this.el = document.createElement('div') + this.dispatch = null } onActivation () { @@ -286,6 +285,15 @@ export class TabProxy extends Plugin { this.handlers[type] = fn } + setDispatch (dispatch) { + this.dispatch = dispatch + this.renderComponent() + } + + updateComponent(state) { + return + } + renderComponent () { const onSelect = (index) => { if (this.loadedTabs[index]) { @@ -308,12 +316,17 @@ export class TabProxy extends Plugin { const onReady = (api) => { this.tabsApi = api } - ReactDOM.render( - - , this.el) + this.dispatch({ + loadedTabs: this.loadedTabs, + onSelect, + onClose, + onZoomIn, + onZoomOut, + onReady + }) } renderTabsbar () { - return this.el + return
} } diff --git a/apps/remix-ide/src/app/panels/terminal.js b/apps/remix-ide/src/app/panels/terminal.js index 0854f7586e..6eaa65f6e9 100644 --- a/apps/remix-ide/src/app/panels/terminal.js +++ b/apps/remix-ide/src/app/panels/terminal.js @@ -1,15 +1,16 @@ /* global Node, requestAnimationFrame */ // eslint-disable-line import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import { RemixUiTerminal } from '@remix-ui/terminal' // eslint-disable-line import { Plugin } from '@remixproject/engine' import * as packageJson from '../../../../../package.json' import Registry from '../state/registry' +import { PluginViewWrapper } from '@remix-ui/helper' const vm = require('vm') const EventManager = require('../../lib/events') import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line + const KONSOLES = [] function register (api) { KONSOLES.push(api) } @@ -79,9 +80,12 @@ class Terminal extends Plugin { this.call('menuicons', 'select', 'debugger') this.call('debugger', 'debug', hash) }) + this.dispatch = null + } + - onActivation () { + onActivation() { this.renderComponent() } @@ -100,19 +104,27 @@ class Terminal extends Plugin { this.terminalApi.log(message) } + setDispatch(dispatch) { + this.dispatch = dispatch + } + render () { - return this.element + return
+ } + + updateComponent(state) { + return } renderComponent () { const onReady = (api) => { this.terminalApi = api } - ReactDOM.render( - , - this.element - ) + this.dispatch({ + plugin: this, + onReady: onReady + }) } scroll2bottom () { diff --git a/apps/remix-ide/src/app/plugins/config.ts b/apps/remix-ide/src/app/plugins/config.ts index e44d403a12..3102d555df 100644 --- a/apps/remix-ide/src/app/plugins/config.ts +++ b/apps/remix-ide/src/app/plugins/config.ts @@ -18,7 +18,7 @@ export class ConfigPlugin extends Plugin { const queryParams = new QueryParams() const params = queryParams.get() const config = Registry.getInstance().get('config').api - const param = params[name] ? params[name] : config.get(name) + let param = params[name] || config.get(name) || config.get('settings/' + name) if (param === 'true') return true if (param === 'false') return false return param diff --git a/apps/remix-ide/src/app/plugins/storage.ts b/apps/remix-ide/src/app/plugins/storage.ts index 2bf50fdb58..d52e2302eb 100644 --- a/apps/remix-ide/src/app/plugins/storage.ts +++ b/apps/remix-ide/src/app/plugins/storage.ts @@ -4,7 +4,7 @@ const profile = { name: 'storage', displayName: 'Storage', description: 'Storage', - methods: ['getStorage'] + methods: ['getStorage', 'formatString'] }; export class StoragePlugin extends Plugin { @@ -13,10 +13,47 @@ export class StoragePlugin extends Plugin { } async getStorage() { - if ('storage' in navigator && 'estimate' in navigator.storage) { - return navigator.storage.estimate() + let storage = null + if ('storage' in navigator && 'estimate' in navigator.storage && (window as any).remixFileSystem.name !== 'localstorage') { + storage = navigator.storage.estimate() } else { - throw new Error("Can't get storage quota"); + storage ={ + usage: parseFloat(this.calculateLocalStorage()) * 1000, + quota: 5000000, + } } + const _paq = window._paq = window._paq || [] + _paq.push(['trackEvent', 'Storage', 'used', this.formatString(storage)]); + return storage + } + + formatString(storage) { + return `${this.formatBytes(storage.usage)} / ${this.formatBytes(storage.quota)}`; + } + + calculateLocalStorage() { + var _lsTotal = 0 + var _xLen; var _x + for (_x in localStorage) { + // eslint-disable-next-line no-prototype-builtins + if (!localStorage.hasOwnProperty(_x)) { + continue + } + _xLen = ((localStorage[_x].length + _x.length) * 2) + _lsTotal += _xLen + } + return (_lsTotal / 1024).toFixed(2) + } + + formatBytes(bytes: number, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } } diff --git a/apps/remix-ide/src/app/tabs/analysis-tab.js b/apps/remix-ide/src/app/tabs/analysis-tab.js index dcd894d02c..f17b0da52b 100644 --- a/apps/remix-ide/src/app/tabs/analysis-tab.js +++ b/apps/remix-ide/src/app/tabs/analysis-tab.js @@ -1,10 +1,10 @@ import React from 'react' // eslint-disable-line import { ViewPlugin } from '@remixproject/engine-web' -import ReactDOM from 'react-dom' import { EventEmitter } from 'events' import {RemixUiStaticAnalyser} from '@remix-ui/static-analyser' // eslint-disable-line import * as packageJson from '../../../../../package.json' import Registry from '../state/registry' +import { PluginViewWrapper } from '@remix-ui/helper' var EventManager = require('../../lib/events') @@ -35,6 +35,7 @@ class AnalysisTab extends ViewPlugin { offsetToLineColumnConverter: this.registry.get( 'offsettolinecolumnconverter').api } + this.dispatch = null } async onActivation () { @@ -43,33 +44,40 @@ class AnalysisTab extends ViewPlugin { await this.call('manager', 'activatePlugin', 'solidity') } this.renderComponent() + this.event.register('staticAnaysisWarning', (count) => { + if (count > 0) { + this.emit('statusChanged', { key: count, title: `${count} warning${count === 1 ? '' : 's'}`, type: 'warning' }) + } else if (count === 0) { + this.emit('statusChanged', { key: 'succeed', title: 'no warning', type: 'success' }) + } else { + // count ==-1 no compilation result + this.emit('statusChanged', { key: 'none' }) + } + }) + } + + setDispatch (dispatch) { + this.dispatch = dispatch } render () { - return this.element + return
+ } + + updateComponent(state) { + return } renderComponent () { - ReactDOM.render( - , - this.element, - () => { - this.event.register('staticAnaysisWarning', (count) => { - if (count > 0) { - this.emit('statusChanged', { key: count, title: `${count} warning${count === 1 ? '' : 's'}`, type: 'warning' }) - } else if (count === 0) { - this.emit('statusChanged', { key: 'succeed', title: 'no warning', type: 'success' }) - } else { - // count ==-1 no compilation result - this.emit('statusChanged', { key: 'none' }) - } - }) - } - ) + this.dispatch({ + registry: this.registry, + analysisModule: this, + event: this.event + }) } } diff --git a/apps/remix-ide/src/app/tabs/compile-tab.js b/apps/remix-ide/src/app/tabs/compile-tab.js index 4718b4dee6..8e3d17e366 100644 --- a/apps/remix-ide/src/app/tabs/compile-tab.js +++ b/apps/remix-ide/src/app/tabs/compile-tab.js @@ -1,6 +1,5 @@ /* global */ import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import { SolidityCompiler } from '@remix-ui/solidity-compiler' // eslint-disable-line import { CompileTabLogic } from '@remix-ui/solidity-compiler' // eslint-disable-line import { CompilerApiMixin } from '@remixproject/solidity-compiler-plugin' // eslint-disable-line @@ -42,18 +41,16 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA } renderComponent () { - ReactDOM.render( - - , this.el) + // empty method, is a state update needed? } onCurrentFileChanged () { this.renderComponent() } - onResetResults () { - this.renderComponent() - } + // onResetResults () { + // this.renderComponent() + // } onSetWorkspace () { this.renderComponent() @@ -63,14 +60,16 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA this.renderComponent() } - onCompilationFinished () { + onFileClosed () { this.renderComponent() } - render () { + onCompilationFinished () { this.renderComponent() + } - return this.el + render () { + return
} async compileWithParameters (compilationTargets, settings) { diff --git a/apps/remix-ide/src/app/tabs/debugger-tab.js b/apps/remix-ide/src/app/tabs/debugger-tab.js index 6ac7235c09..63debf4553 100644 --- a/apps/remix-ide/src/app/tabs/debugger-tab.js +++ b/apps/remix-ide/src/app/tabs/debugger-tab.js @@ -3,7 +3,6 @@ import { DebuggerApiMixin } from '@remixproject/debugger-plugin' // eslint-disab import { ViewPlugin } from '@remixproject/engine-web' import * as packageJson from '../../../../../package.json' import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import * as remixBleach from '../../lib/remixBleach' import { compilationFinishedToastMsg, compilingToastMsg, localCompilationToastMsg, notFoundToastMsg, sourceVerificationNotAvailableToastMsg } from '@remix-ui/helper' const css = require('./styles/debugger-tab-styles') @@ -51,9 +50,7 @@ export class DebuggerTab extends DebuggerApiMixin(ViewPlugin) { this.on('fetchAndCompile', 'sourceVerificationNotAvailable', () => { this.call('notification', 'toast', sourceVerificationNotAvailableToastMsg()) }) - - this.renderComponent() - return this.el + return
} showMessage (title, message) { @@ -68,9 +65,4 @@ export class DebuggerTab extends DebuggerApiMixin(ViewPlugin) { } } - renderComponent () { - ReactDOM.render( - - , this.el) - } } diff --git a/apps/remix-ide/src/app/tabs/hardhat-provider.tsx b/apps/remix-ide/src/app/tabs/hardhat-provider.tsx index 1ca0547d9a..d17f4d1281 100644 --- a/apps/remix-ide/src/app/tabs/hardhat-provider.tsx +++ b/apps/remix-ide/src/app/tabs/hardhat-provider.tsx @@ -49,11 +49,13 @@ export class HardhatProvider extends Plugin { } hardhatProviderDialogBody (): JSX.Element { - return (
Note: To run Hardhat network node on your system, go to hardhat project folder and run command: -
npx hardhat node
- For more info, visit: Hardhat Documentation - Hardhat JSON-RPC Endpoint -
) + return ( +
Note: To run Hardhat network node on your system, go to hardhat project folder and run command: +
npx hardhat node
+ For more info, visit: Hardhat Documentation +
Hardhat JSON-RPC Endpoint:
+
+ ) } sendAsync (data: JsonDataRequest): Promise { diff --git a/apps/remix-ide/src/app/tabs/search.tsx b/apps/remix-ide/src/app/tabs/search.tsx new file mode 100644 index 0000000000..6914ed0150 --- /dev/null +++ b/apps/remix-ide/src/app/tabs/search.tsx @@ -0,0 +1,32 @@ +import { ViewPlugin } from '@remixproject/engine-web' +import * as packageJson from '../../../../../package.json' +import React from 'react' // eslint-disable-line +import { SearchTab } from '@remix-ui/search' +const profile = { + name: 'search', + displayName: 'Search', + methods: [''], + events: [], + icon: 'assets/img/Search_Icon.svg', + description: '', + kind: '', + location: 'sidePanel', + documentation: '', + version: packageJson.version + } + +export class SearchPlugin extends ViewPlugin { + + constructor () { + super(profile) + } + + render() { + return ( +
+ +
+ ); + } + +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/tabs/settings-tab.js b/apps/remix-ide/src/app/tabs/settings-tab.tsx similarity index 62% rename from apps/remix-ide/src/app/tabs/settings-tab.js rename to apps/remix-ide/src/app/tabs/settings-tab.tsx index 133ae6ee5f..8440ffbfa7 100644 --- a/apps/remix-ide/src/app/tabs/settings-tab.js +++ b/apps/remix-ide/src/app/tabs/settings-tab.tsx @@ -1,9 +1,9 @@ import React from 'react' // eslint-disable-line import { ViewPlugin } from '@remixproject/engine-web' -import ReactDOM from 'react-dom' import * as packageJson from '../../../../../package.json' import { RemixUiSettings } from '@remix-ui/settings' //eslint-disable-line import Registry from '../state/registry' +import { PluginViewWrapper } from '@remix-ui/helper' const profile = { name: 'settings', @@ -20,6 +20,15 @@ const profile = { } module.exports = class SettingsTab extends ViewPlugin { + config: any = {} + editor: any + private _deps: { + themeModule: any // eslint-disable-line + + } + element: HTMLDivElement + public useMatomoAnalytics: any + dispatch: React.Dispatch = () => {} constructor (config, editor) { super(profile) this.config = config @@ -32,24 +41,29 @@ module.exports = class SettingsTab extends ViewPlugin { this.useMatomoAnalytics = null } - onActivation () { + setDispatch (dispatch: React.Dispatch) { + this.dispatch = dispatch this.renderComponent() } - render () { - return this.element + render() { + return
+ +
+ } + + updateComponent(state: any){ + return } renderComponent () { - ReactDOM.render( - , - this.element - ) + this.dispatch(this) } get (key) { @@ -59,6 +73,8 @@ module.exports = class SettingsTab extends ViewPlugin { updateMatomoAnalyticsChoice (isChecked) { this.config.set('settings/matomo-analytics', isChecked) this.useMatomoAnalytics = isChecked - this.renderComponent() + this.dispatch({ + ...this + }) } -} +} \ No newline at end of file diff --git a/apps/remix-ide/src/app/tabs/test-tab.js b/apps/remix-ide/src/app/tabs/test-tab.js index a03d74a846..7f634bd9d1 100644 --- a/apps/remix-ide/src/app/tabs/test-tab.js +++ b/apps/remix-ide/src/app/tabs/test-tab.js @@ -1,12 +1,12 @@ /* global */ import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import { SolidityUnitTesting } from '@remix-ui/solidity-unit-testing' // eslint-disable-line import { TestTabLogic } from '@remix-ui/solidity-unit-testing' // eslint-disable-line import { ViewPlugin } from '@remixproject/engine-web' import helper from '../../lib/helper' import { canUseWorker, urlFromVersion } from '@remix-project/remix-solidity' +import { PluginViewWrapper } from '@remix-ui/helper' var { UnitTestRunner, assertLibCode } = require('@remix-project/remix-tests') @@ -34,6 +34,7 @@ module.exports = class TestTab extends ViewPlugin { this.offsetToLineColumnConverter = offsetToLineColumnConverter this.allFilesInvolved = ['.deps/remix-tests/remix_tests.sol', '.deps/remix-tests/remix_accounts.sol'] this.element = document.createElement('div') + this.dispatch = null } onActivationInternal () { @@ -88,12 +89,12 @@ module.exports = class TestTab extends ViewPlugin { this.createTestLibs() }) - this.testRunner.event.on('compilationFinished', (success, data, source) => { + this.testRunner.event.on('compilationFinished', (success, data, source, input, version) => { if (success) { this.allFilesInvolved.push(...Object.keys(data.sources)) // forwarding the event to the appManager infra // This is listened by compilerArtefacts to show data while debugging - this.emit('compilationFinished', source.target, source, 'soljson', data) + this.emit('compilationFinished', source.target, source, 'soljson', data, input, version) } }) } @@ -128,15 +129,25 @@ module.exports = class TestTab extends ViewPlugin { }) } + setDispatch (dispatch) { + this.dispatch = dispatch + this.renderComponent('tests') + } + render () { this.onActivationInternal() - this.renderComponent('tests') - return this.element + return
+ } + + updateComponent(state) { + return } renderComponent (testDirPath) { - ReactDOM.render( - - , this.element) + this.dispatch({ + testTab: this, + helper: this.helper, + testDirPath: testDirPath + }) } } diff --git a/apps/remix-ide/src/app/tabs/theme-module.js b/apps/remix-ide/src/app/tabs/theme-module.js index 78c8f1788d..2fd9e92e1a 100644 --- a/apps/remix-ide/src/app/tabs/theme-module.js +++ b/apps/remix-ide/src/app/tabs/theme-module.js @@ -31,17 +31,20 @@ export class ThemeModule extends Plugin { super(profile) this.events = new EventEmitter() this._deps = { - config: Registry.getInstance().get('config').api + config: Registry.getInstance().get('config') && Registry.getInstance().get('config').api } - this.themes = themes.reduce((acc, theme) => { - theme.url = window.location.origin + window.location.pathname + theme.url - return { ...acc, [theme.name.toLocaleLowerCase()]: theme } - }, {}) + this.themes = {} + themes.map((theme) => { + this.themes[theme.name.toLocaleLowerCase()] = { + ...theme, + url: window.location.origin + window.location.pathname + theme.url + } + }) this._paq = _paq let queryTheme = (new QueryParams()).get().theme queryTheme = queryTheme && queryTheme.toLocaleLowerCase() queryTheme = this.themes[queryTheme] ? queryTheme : null - let currentTheme = this._deps.config.get('settings/theme') + let currentTheme = (this._deps.config && this._deps.config.get('settings/theme')) || null currentTheme = currentTheme && currentTheme.toLocaleLowerCase() currentTheme = this.themes[currentTheme] ? currentTheme : null this.currentThemeState = { queryTheme, currentTheme } @@ -65,6 +68,7 @@ export class ThemeModule extends Plugin { initTheme (callback) { // callback is setTimeOut in app.js which is always passed if (callback) this.initCallback = callback if (this.active) { + document.getElementById('theme-link') ? document.getElementById('theme-link').remove():null const nextTheme = this.themes[this.active] // Theme document.documentElement.style.setProperty('--theme', nextTheme.quality) @@ -93,9 +97,9 @@ export class ThemeModule extends Plugin { _paq.push(['trackEvent', 'themeModule', 'switchTo', next]) const nextTheme = this.themes[next] // Theme if (!this.forced) this._deps.config.set('settings/theme', next) - document.getElementById('theme-link').remove() - const theme = document.createElement('link') + document.getElementById('theme-link') ? document.getElementById('theme-link').remove():null + const theme = document.createElement('link') theme.setAttribute('rel', 'stylesheet') theme.setAttribute('href', nextTheme.url) theme.setAttribute('id', 'theme-link') diff --git a/apps/remix-ide/src/app/udapp/run-tab.js b/apps/remix-ide/src/app/udapp/run-tab.js index 5fe56053fa..05a015c4fc 100644 --- a/apps/remix-ide/src/app/udapp/run-tab.js +++ b/apps/remix-ide/src/app/udapp/run-tab.js @@ -1,5 +1,4 @@ import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import { RunTabUI } from '@remix-ui/run-tab' import { ViewPlugin } from '@remixproject/engine-web' import * as packageJson from '../../../../../package.json' @@ -40,9 +39,6 @@ export class RunTab extends ViewPlugin { this.el = document.createElement('div') } - onActivation () { - this.renderComponent() - } setupEvents () { this.blockchain.events.on('newTransaction', (tx, receipt) => { @@ -86,14 +82,9 @@ export class RunTab extends ViewPlugin { } render () { - return this.el + return
} - renderComponent () { - ReactDOM.render( - - , this.el) - } onReady (api) { this.REACT_API = api diff --git a/apps/remix-ide/src/app/ui/landing-page/landing-page.js b/apps/remix-ide/src/app/ui/landing-page/landing-page.js index b05271dc96..2f8819fc5f 100644 --- a/apps/remix-ide/src/app/ui/landing-page/landing-page.js +++ b/apps/remix-ide/src/app/ui/landing-page/landing-page.js @@ -1,6 +1,5 @@ /* global */ import React from 'react' // eslint-disable-line -import ReactDOM from 'react-dom' import * as packageJson from '../../../../../../package.json' import { ViewPlugin } from '@remixproject/engine-web' import { RemixUiHomeTab } from '@remix-ui/home-tab' // eslint-disable-line @@ -31,15 +30,9 @@ export class LandingPage extends ViewPlugin { } render () { - this.renderComponent() - return this.el + return
} - renderComponent () { - ReactDOM.render( - - , this.el) - } } diff --git a/apps/remix-ide/src/assets/img/Search_Icon.svg b/apps/remix-ide/src/assets/img/Search_Icon.svg new file mode 100644 index 0000000000..00a6fcde04 --- /dev/null +++ b/apps/remix-ide/src/assets/img/Search_Icon.svg @@ -0,0 +1,77 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/apps/remix-ide/src/assets/js/init.js b/apps/remix-ide/src/assets/js/init.js index f3cd64d79f..8265067b82 100644 --- a/apps/remix-ide/src/assets/js/init.js +++ b/apps/remix-ide/src/assets/js/init.js @@ -1,5 +1,5 @@ /* eslint-disable prefer-promise-reject-errors */ -function urlParams () { +function urlParams() { var qs = window.location.hash.substr(1) if (window.location.search.length > 0) { @@ -41,67 +41,12 @@ for (const k in assets[versionToLoad]) { } window.onload = () => { - // eslint-disable-next-line no-undef - class RemixFileSystem extends LightningFS { - constructor (...t) { - super(...t) - this.addSlash = (file) => { - if (!file.startsWith('/')) file = '/' + file - return file - } - this.base = this.promises - this.promises = { - ...this.promises, - - exists: async (path) => { - return new Promise((resolve, reject) => { - this.base.stat(this.addSlash(path)).then(() => resolve(true)).catch(() => resolve(false)) - }) - }, - rmdir: async (path) => { - return this.base.rmdir(this.addSlash(path)) - }, - readdir: async (path) => { - return this.base.readdir(this.addSlash(path)) - }, - unlink: async (path) => { - return this.base.unlink(this.addSlash(path)) - }, - mkdir: async (path) => { - return this.base.mkdir(this.addSlash(path)) - }, - readFile: async (path, options) => { - return this.base.readFile(this.addSlash(path), options) - }, - rename: async (from, to) => { - return this.base.rename(this.addSlash(from), this.addSlash(to)) - }, - writeFile: async (path, content, options) => { - return this.base.writeFile(this.addSlash(path), content, options) - }, - stat: async (path) => { - return this.base.stat(this.addSlash(path)) - } - } - } - } - - function loadApp () { + function loadApp() { const app = document.createElement('script') app.setAttribute('src', versions[versionToLoad]) document.body.appendChild(app) } - window.remixFileSystemCallback = new RemixFileSystem() - window.remixFileSystemCallback.init('RemixFileSystem').then(() => { - window.remixFileSystem = window.remixFileSystemCallback.promises - // check if .workspaces is present in indexeddb - window.remixFileSystem.stat('.workspaces').then((dir) => { - if (dir.isDirectory()) loadApp() - }).catch(() => { - // no indexeddb .workspaces -> run migration - // eslint-disable-next-line no-undef - migrateFilesFromLocalStorage(loadApp) - }) - }) + loadApp() + return } diff --git a/apps/remix-ide/src/assets/js/lightning-fs.min.js b/apps/remix-ide/src/assets/js/lightning-fs.min.js deleted file mode 100644 index a306197743..0000000000 --- a/apps/remix-ide/src/assets/js/lightning-fs.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.LightningFS=e():t.LightningFS=e()}(self,function(){return function(t){var e={};function i(n){if(e[n])return e[n].exports;var s=e[n]={i:n,l:!1,exports:{}};return t[n].call(s.exports,s,s.exports,i),s.l=!0,s.exports}return i.m=t,i.c=e,i.d=function(t,e,n){i.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},i.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i.t=function(t,e){if(1&e&&(t=i(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var s in t)i.d(n,s,function(e){return t[e]}.bind(null,s));return n},i.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(e,"a",e),e},i.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},i.p="",i(i.s=3)}([function(t,e){function i(t){if(0===t.length)return".";let e=s(t);return e=e.reduce(r,[]),n(...e)}function n(...t){if(0===t.length)return"";let e=t.join("/");return e=e.replace(/\/{2,}/g,"/")}function s(t){if(0===t.length)return[];if("/"===t)return["/"];let e=t.split("/");return""===e[e.length-1]&&e.pop(),"/"===t[0]?e[0]="/":"."!==e[0]&&e.unshift("."),e}function r(t,e){if(0===t.length)return t.push(e),t;if("."===e)return t;if(".."===e){if(1===t.length){if("/"===t[0])throw new Error("Unable to normalize path - traverses above root directory");if("."===t[0])return t.push(e),t}return".."===t[t.length-1]?(t.push(".."),t):(t.pop(),t)}return t.push(e),t}t.exports={join:n,normalize:i,split:s,basename:function(t){if("/"===t)throw new Error(`Cannot get basename of "${t}"`);const e=t.lastIndexOf("/");return-1===e?t:t.slice(e+1)},dirname:function(t){const e=t.lastIndexOf("/");if(-1===e)throw new Error(`Cannot get dirname of "${t}"`);return 0===e?"/":t.slice(0,e)},resolve:function(...t){let e="";for(let s of t)e=s.startsWith("/")?s:i(n(e,s));return e}}},function(t,e){function i(t){return class extends Error{constructor(...e){super(...e),this.code=t,this.message?this.message=t+": "+this.message:this.message=t}}}const n=i("EEXIST"),s=i("ENOENT"),r=i("ENOTDIR"),o=i("ENOTEMPTY"),a=i("ETIMEDOUT");t.exports={EEXIST:n,ENOENT:s,ENOTDIR:r,ENOTEMPTY:o,ETIMEDOUT:a}},function(t,e,i){"use strict";i.r(e),i.d(e,"Store",function(){return n}),i.d(e,"get",function(){return o}),i.d(e,"set",function(){return a}),i.d(e,"update",function(){return h}),i.d(e,"del",function(){return c}),i.d(e,"clear",function(){return l}),i.d(e,"keys",function(){return u}),i.d(e,"close",function(){return d});class n{constructor(t="keyval-store",e="keyval"){this.storeName=e,this._dbName=t,this._storeName=e,this._init()}_init(){this._dbp||(this._dbp=new Promise((t,e)=>{const i=indexedDB.open(this._dbName);i.onerror=(()=>e(i.error)),i.onsuccess=(()=>t(i.result)),i.onupgradeneeded=(()=>{i.result.createObjectStore(this._storeName)})}))}_withIDBStore(t,e){return this._init(),this._dbp.then(i=>new Promise((n,s)=>{const r=i.transaction(this.storeName,t);r.oncomplete=(()=>n()),r.onabort=r.onerror=(()=>s(r.error)),e(r.objectStore(this.storeName))}))}_close(){return this._init(),this._dbp.then(t=>{t.close(),this._dbp=void 0})}}let s;function r(){return s||(s=new n),s}function o(t,e=r()){let i;return e._withIDBStore("readwrite",e=>{i=e.get(t)}).then(()=>i.result)}function a(t,e,i=r()){return i._withIDBStore("readwrite",i=>{i.put(e,t)})}function h(t,e,i=r()){return i._withIDBStore("readwrite",i=>{const n=i.get(t);n.onsuccess=(()=>{i.put(e(n.result),t)})})}function c(t,e=r()){return e._withIDBStore("readwrite",e=>{e.delete(t)})}function l(t=r()){return t._withIDBStore("readwrite",t=>{t.clear()})}function u(t=r()){const e=[];return t._withIDBStore("readwrite",t=>{(t.openKeyCursor||t.openCursor).call(t).onsuccess=function(){this.result&&(e.push(this.result.key),this.result.continue())}}).then(()=>e)}function d(t=r()){return t._close()}},function(t,e,i){const n=i(4),s=i(5);function r(t,e){"function"==typeof t&&(e=t);return[(...t)=>e(null,...t),e=n(e)]}t.exports=class{constructor(...t){this.promises=new s(...t),this.init=this.init.bind(this),this.readFile=this.readFile.bind(this),this.writeFile=this.writeFile.bind(this),this.unlink=this.unlink.bind(this),this.readdir=this.readdir.bind(this),this.mkdir=this.mkdir.bind(this),this.rmdir=this.rmdir.bind(this),this.rename=this.rename.bind(this),this.stat=this.stat.bind(this),this.lstat=this.lstat.bind(this),this.readlink=this.readlink.bind(this),this.symlink=this.symlink.bind(this),this.backFile=this.backFile.bind(this),this.du=this.du.bind(this)}init(t,e){return this.promises.init(t,e)}readFile(t,e,i){const[n,s]=r(e,i);this.promises.readFile(t,e).then(n).catch(s)}writeFile(t,e,i,n){const[s,o]=r(i,n);this.promises.writeFile(t,e,i).then(s).catch(o)}unlink(t,e,i){const[n,s]=r(e,i);this.promises.unlink(t,e).then(n).catch(s)}readdir(t,e,i){const[n,s]=r(e,i);this.promises.readdir(t,e).then(n).catch(s)}mkdir(t,e,i){const[n,s]=r(e,i);this.promises.mkdir(t,e).then(n).catch(s)}rmdir(t,e,i){const[n,s]=r(e,i);this.promises.rmdir(t,e).then(n).catch(s)}rename(t,e,i){const[n,s]=r(i);this.promises.rename(t,e).then(n).catch(s)}stat(t,e,i){const[n,s]=r(e,i);this.promises.stat(t).then(n).catch(s)}lstat(t,e,i){const[n,s]=r(e,i);this.promises.lstat(t).then(n).catch(s)}readlink(t,e,i){const[n,s]=r(e,i);this.promises.readlink(t).then(n).catch(s)}symlink(t,e,i){const[n,s]=r(i);this.promises.symlink(t,e).then(n).catch(s)}backFile(t,e,i){const[n,s]=r(e,i);this.promises.backFile(t,e).then(n).catch(s)}du(t,e){const[i,n]=r(e);this.promises.du(t).then(i).catch(n)}}},function(t,e){t.exports=function(t){var e,i;if("function"!=typeof t)throw new Error("expected a function but got "+t);return function(){return e?i:(e=!0,i=t.apply(this,arguments))}}},function(t,e,i){const n=i(6),s=i(16),r=i(0);function o(t,e,...i){return void 0!==e&&"function"!=typeof e||(e={}),"string"==typeof e&&(e={encoding:e}),[t=r.normalize(t),e,...i]}function a(t,e,i,...n){return void 0!==i&&"function"!=typeof i||(i={}),"string"==typeof i&&(i={encoding:i}),[t=r.normalize(t),e,i,...n]}function h(t,e,...i){return[r.normalize(t),r.normalize(e),...i]}t.exports=class{constructor(t,e={}){this.init=this.init.bind(this),this.readFile=this._wrap(this.readFile,o,!1),this.writeFile=this._wrap(this.writeFile,a,!0),this.unlink=this._wrap(this.unlink,o,!0),this.readdir=this._wrap(this.readdir,o,!1),this.mkdir=this._wrap(this.mkdir,o,!0),this.rmdir=this._wrap(this.rmdir,o,!0),this.rename=this._wrap(this.rename,h,!0),this.stat=this._wrap(this.stat,o,!1),this.lstat=this._wrap(this.lstat,o,!1),this.readlink=this._wrap(this.readlink,o,!1),this.symlink=this._wrap(this.symlink,h,!0),this.backFile=this._wrap(this.backFile,o,!0),this.du=this._wrap(this.du,o,!1),this._deactivationPromise=null,this._deactivationTimeout=null,this._activationPromise=null,this._operations=new Set,t&&this.init(t,e)}async init(...t){return this._initPromiseResolve&&await this._initPromise,this._initPromise=this._init(...t),this._initPromise}async _init(t,e={}){await this._gracefulShutdown(),this._activationPromise&&await this._deactivate(),this._backend&&this._backend.destroy&&await this._backend.destroy(),this._backend=e.backend||new n,this._backend.init&&await this._backend.init(t,e),this._initPromiseResolve&&(this._initPromiseResolve(),this._initPromiseResolve=null),e.defer||this.stat("/")}async _gracefulShutdown(){this._operations.size>0&&(this._isShuttingDown=!0,await new Promise(t=>this._gracefulShutdownResolve=t),this._isShuttingDown=!1,this._gracefulShutdownResolve=null)}_wrap(t,e,i){return async(...n)=>{n=e(...n);let s={name:t.name,args:n};this._operations.add(s);try{return await this._activate(),await t.apply(this,n)}finally{this._operations.delete(s),i&&this._backend.saveSuperblock(),0===this._operations.size&&(this._deactivationTimeout||clearTimeout(this._deactivationTimeout),this._deactivationTimeout=setTimeout(this._deactivate.bind(this),500))}}}async _activate(){this._initPromise||console.warn(new Error(`Attempted to use LightningFS ${this._name} before it was initialized.`)),await this._initPromise,this._deactivationTimeout&&(clearTimeout(this._deactivationTimeout),this._deactivationTimeout=null),this._deactivationPromise&&await this._deactivationPromise,this._deactivationPromise=null,this._activationPromise||(this._activationPromise=this._backend.activate?this._backend.activate():Promise.resolve()),await this._activationPromise}async _deactivate(){return this._activationPromise&&await this._activationPromise,this._deactivationPromise||(this._deactivationPromise=this._backend.deactivate?this._backend.deactivate():Promise.resolve()),this._activationPromise=null,this._gracefulShutdownResolve&&this._gracefulShutdownResolve(),this._deactivationPromise}async readFile(t,e){return this._backend.readFile(t,e)}async writeFile(t,e,i){return await this._backend.writeFile(t,e,i),null}async unlink(t,e){return await this._backend.unlink(t,e),null}async readdir(t,e){return this._backend.readdir(t,e)}async mkdir(t,e){return await this._backend.mkdir(t,e),null}async rmdir(t,e){return await this._backend.rmdir(t,e),null}async rename(t,e){return await this._backend.rename(t,e),null}async stat(t,e){const i=await this._backend.stat(t,e);return new s(i)}async lstat(t,e){const i=await this._backend.lstat(t,e);return new s(i)}async readlink(t,e){return this._backend.readlink(t,e)}async symlink(t,e){return await this._backend.symlink(t,e),null}async backFile(t,e){return await this._backend.backFile(t,e),null}async du(t){return this._backend.du(t)}}},function(t,e,i){const{encode:n,decode:s}=i(7),r=i(10),o=i(11),{ENOENT:a,ENOTEMPTY:h,ETIMEDOUT:c}=i(1),l=i(12),u=i(13),d=i(14),_=i(15),p=i(0);t.exports=class{constructor(){this.saveSuperblock=r(()=>{this._saveSuperblock()},500)}async init(t,{wipe:e,url:i,urlauto:n,fileDbName:s=t,fileStoreName:r=t+"_files",lockDbName:a=t+"_lock",lockStoreName:h=t+"_lock"}={}){this._name=t,this._idb=new l(s,r),this._mutex=navigator.locks?new _(t):new d(a,h),this._cache=new o(t),this._opts={wipe:e,url:i},this._needsWipe=!!e,i&&(this._http=new u(i),this._urlauto=!!n)}async activate(){if(this._cache.activated)return;this._needsWipe&&(this._needsWipe=!1,await this._idb.wipe(),await this._mutex.release({force:!0})),await this._mutex.has()||await this._mutex.wait();const t=await this._idb.loadSuperblock();if(t)this._cache.activate(t);else if(this._http){const t=await this._http.loadSuperblock();this._cache.activate(t),await this._saveSuperblock()}else this._cache.activate();if(!await this._mutex.has())throw new c}async deactivate(){await this._mutex.has()&&await this._saveSuperblock(),this._cache.deactivate();try{await this._mutex.release()}catch(t){console.log(t)}await this._idb.close()}async _saveSuperblock(){this._cache.activated&&(this._lastSavedAt=Date.now(),await this._idb.saveSuperblock(this._cache._root))}_writeStat(t,e,i){let n=p.split(p.dirname(t)),s=n.shift();for(let t of n){s=p.join(s,t);try{this._cache.mkdir(s,{mode:511})}catch(t){}}return this._cache.writeStat(t,e,i)}async readFile(t,e){const{encoding:i}=e;if(i&&"utf8"!==i)throw new Error('Only "utf8" encoding is supported in readFile');let n=null,r=null;try{r=this._cache.stat(t),n=await this._idb.readFile(r.ino)}catch(t){if(!this._urlauto)throw t}if(!n&&this._http){let e=this._cache.lstat(t);for(;"symlink"===e.type;)t=p.resolve(p.dirname(t),e.target),e=this._cache.lstat(t);n=await this._http.readFile(t)}if(n&&(r&&r.size==n.byteLength||(r=await this._writeStat(t,n.byteLength,{mode:r?r.mode:438}),this.saveSuperblock()),"utf8"===i&&(n=s(n))),!r)throw new a(t);return n}async writeFile(t,e,i){const{mode:s,encoding:r="utf8"}=i;if("string"==typeof e){if("utf8"!==r)throw new Error('Only "utf8" encoding is supported in writeFile');e=n(e)}const o=await this._cache.writeStat(t,e.byteLength,{mode:s});await this._idb.writeFile(o.ino,e)}async unlink(t,e){const i=this._cache.lstat(t);this._cache.unlink(t),"symlink"!==i.type&&await this._idb.unlink(i.ino)}readdir(t,e){return this._cache.readdir(t)}mkdir(t,e){const{mode:i=511}=e;this._cache.mkdir(t,{mode:i})}rmdir(t,e){if("/"===t)throw new h;this._cache.rmdir(t)}rename(t,e){this._cache.rename(t,e)}stat(t,e){return this._cache.stat(t)}lstat(t,e){return this._cache.lstat(t)}readlink(t,e){return this._cache.readlink(t)}symlink(t,e){this._cache.symlink(t,e)}async backFile(t,e){let i=await this._http.sizeFile(t);await this._writeStat(t,i,e)}du(t){return this._cache.du(t)}}},function(t,e,i){i(8),t.exports={encode:t=>(new TextEncoder).encode(t),decode:t=>(new TextDecoder).decode(t)}},function(t,e,i){(function(t){!function(t){function e(t){if("utf-8"!==(t=void 0===t?"utf-8":t))throw new RangeError("Failed to construct 'TextEncoder': The encoding label provided ('"+t+"') is invalid.")}function i(t,e){if(e=void 0===e?{fatal:!1}:e,"utf-8"!==(t=void 0===t?"utf-8":t))throw new RangeError("Failed to construct 'TextDecoder': The encoding label provided ('"+t+"') is invalid.");if(e.fatal)throw Error("Failed to construct 'TextDecoder': the 'fatal' option is unsupported.")}if(t.TextEncoder&&t.TextDecoder)return!1;Object.defineProperty(e.prototype,"encoding",{value:"utf-8"}),e.prototype.encode=function(t,e){if((e=void 0===e?{stream:!1}:e).stream)throw Error("Failed to encode: the 'stream' option is unsupported.");e=0;for(var i=t.length,n=0,s=Math.max(32,i+(i>>1)+7),r=new Uint8Array(s>>3<<3);e=o){if(e=o)continue}if(n+4>r.length&&(s+=8,s=(s*=1+e/t.length*2)>>3<<3,(a=new Uint8Array(s)).set(r),r=a),0==(4294967168&o))r[n++]=o;else{if(0==(4294965248&o))r[n++]=o>>6&31|192;else if(0==(4294901760&o))r[n++]=o>>12&15|224,r[n++]=o>>6&63|128;else{if(0!=(4292870144&o))continue;r[n++]=o>>18&7|240,r[n++]=o>>12&63|128,r[n++]=o>>6&63|128}r[n++]=63&o|128}}return r.slice(0,n)},Object.defineProperty(i.prototype,"encoding",{value:"utf-8"}),Object.defineProperty(i.prototype,"fatal",{value:!1}),Object.defineProperty(i.prototype,"ignoreBOM",{value:!1}),i.prototype.decode=function(t,e){if((e=void 0===e?{stream:!1}:e).stream)throw Error("Failed to decode: the 'stream' option is unsupported.");e=0;for(var i=(t=new Uint8Array(t)).length,n=[];e>>10&1023|55296),s=56320|1023&s),n.push(s)}}return String.fromCharCode.apply(null,n)},t.TextEncoder=e,t.TextDecoder=i}("undefined"!=typeof window?window:void 0!==t?t:this)}).call(this,i(9))},function(t,e){var i;i=function(){return this}();try{i=i||new Function("return this")()}catch(t){"object"==typeof window&&(i=window)}t.exports=i},function(t,e){t.exports=function(t,e,i){var n;return function(){if(!e)return t.apply(this,arguments);var s=this,r=arguments,o=i&&!n;return clearTimeout(n),n=setTimeout(function(){if(n=null,!o)return t.apply(s,r)},e),o?t.apply(this,arguments):void 0}}},function(t,e,i){const n=i(0),{EEXIST:s,ENOENT:r,ENOTDIR:o,ENOTEMPTY:a}=i(1),h=0;t.exports=class{constructor(){}_makeRoot(t=new Map){return t.set(h,{mode:511,type:"dir",size:0,ino:0,mtimeMs:Date.now()}),t}activate(t=null){this._root=null===t?new Map([["/",this._makeRoot()]]):"string"==typeof t?new Map([["/",this._makeRoot(this.parse(t))]]):t}get activated(){return!!this._root}deactivate(){this._root=void 0}size(){return this._countInodes(this._root.get("/"))-1}_countInodes(t){let e=1;for(let[i,n]of t)i!==h&&(e+=this._countInodes(n));return e}autoinc(){return this._maxInode(this._root.get("/"))+1}_maxInode(t){let e=t.get(h).ino;for(let[i,n]of t)i!==h&&(e=Math.max(e,this._maxInode(n)));return e}print(t=this._root.get("/")){let e="";const i=(t,n)=>{for(let[s,r]of t){if(0===s)continue;let t=r.get(h),o=t.mode.toString(8);e+=`${"\t".repeat(n)}${s}\t${o}`,"file"===t.type?e+=`\t${t.size}\t${t.mtimeMs}\n`:(e+="\n",i(r,n+1))}};return i(t,0),e}parse(t){let e=0;function i(t){const i=++e,n=1===t.length?"dir":"file";let[s,r,o]=t;return s=parseInt(s,8),r=r?parseInt(r):0,o=o?parseInt(o):Date.now(),new Map([[h,{mode:s,type:n,size:r,mtimeMs:o,ino:i}]])}let n=t.trim().split("\n"),s=this._makeRoot(),r=[{indent:-1,node:s},{indent:0,node:null}];for(let t of n){let e=t.match(/^\t*/)[0].length;t=t.slice(e);let[n,...s]=t.split("\t"),o=i(s);if(e<=r[r.length-1].indent)for(;e<=r[r.length-1].indent;)r.pop();r.push({indent:e,node:o}),r[r.length-2].node.set(n,o)}return s}_lookup(t,e=!0){let i=this._root,s="/",o=n.split(t);for(let a=0;a1)throw new a;let i=this._lookup(n.dirname(t)),s=n.basename(t);i.delete(s)}readdir(t){let e=this._lookup(t);if("dir"!==e.get(h).type)throw new o;return[...e.keys()].filter(t=>"string"==typeof t)}writeStat(t,e,{mode:i}){let s;try{let e=this.stat(t);null==i&&(i=e.mode),s=e.ino}catch(t){}null==i&&(i=438),null==s&&(s=this.autoinc());let r=this._lookup(n.dirname(t)),o=n.basename(t),a={mode:i,type:"file",size:e,mtimeMs:Date.now(),ino:s},c=new Map;return c.set(h,a),r.set(o,c),a}unlink(t){let e=this._lookup(n.dirname(t)),i=n.basename(t);e.delete(i)}rename(t,e){let i=n.basename(e),s=this._lookup(t);this._lookup(n.dirname(e)).set(i,s),this.unlink(t)}stat(t){return this._lookup(t).get(h)}lstat(t){return this._lookup(t,!1).get(h)}readlink(t){return this._lookup(t,!1).get(h).target}symlink(t,e){let i,s;try{let t=this.stat(e);null===s&&(s=t.mode),i=t.ino}catch(t){}null==s&&(s=40960),null==i&&(i=this.autoinc());let r=this._lookup(n.dirname(e)),o=n.basename(e),a={mode:s,type:"symlink",target:t,size:0,mtimeMs:Date.now(),ino:i},c=new Map;return c.set(h,a),r.set(o,c),a}_du(t){let e=0;for(const[i,n]of t.entries())e+=i===h?n.size:this._du(n);return e}du(t){let e=this._lookup(t);return this._du(e)}}},function(t,e,i){const n=i(2);t.exports=class{constructor(t,e){this._database=t,this._storename=e,this._store=new n.Store(this._database,this._storename)}saveSuperblock(t){return n.set("!root",t,this._store)}loadSuperblock(){return n.get("!root",this._store)}readFile(t){return n.get(t,this._store)}writeFile(t,e){return n.set(t,e,this._store)}unlink(t){return n.del(t,this._store)}wipe(){return n.clear(this._store)}close(){return n.close(this._store)}}},function(t,e){t.exports=class{constructor(t){this._url=t}loadSuperblock(){return fetch(this._url+"/.superblock.txt").then(t=>t.ok?t.text():null)}async readFile(t){const e=await fetch(this._url+t);if(200===e.status)return e.arrayBuffer();throw new Error("ENOENT")}async sizeFile(t){const e=await fetch(this._url+t,{method:"HEAD"});if(200===e.status)return e.headers.get("content-length");throw new Error("ENOENT")}}},function(t,e,i){const n=i(2),s=t=>new Promise(e=>setTimeout(e,t));t.exports=class{constructor(t,e){this._id=Math.random(),this._database=t,this._storename=e,this._store=new n.Store(this._database,this._storename),this._lock=null}async has({margin:t=2e3}={}){if(this._lock&&this._lock.holder===this._id){const e=Date.now();return this._lock.expires>e+t||await this.renew()}return!1}async renew({ttl:t=5e3}={}){let e;return await n.update("lock",i=>{const n=Date.now()+t;return e=i&&i.holder===this._id,this._lock=e?{holder:this._id,expires:n}:i,this._lock},this._store),e}async acquire({ttl:t=5e3}={}){let e,i,s;if(await n.update("lock",n=>{const r=Date.now(),o=r+t;return i=n&&n.expires(e=t||n&&n.holder===this._id,i=void 0===n,s=n&&n.holder!==this._id,this._lock=e?void 0:n,this._lock),this._store),await n.close(this._store),!e&&!t){if(i)throw new Error("Mutex double-freed");if(s)throw new Error("Mutex lost ownership")}return e}}},function(t,e){t.exports=class{constructor(t){this._id=Math.random(),this._database=t,this._has=!1,this._release=null}async has(){return this._has}async acquire(){return new Promise(t=>{navigator.locks.request(this._database+"_lock",{ifAvailable:!0},e=>(this._has=!!e,t(!!e),new Promise(t=>{this._release=t})))})}async wait({timeout:t=6e5}={}){return new Promise((e,i)=>{const n=new AbortController;setTimeout(()=>{n.abort(),i(new Error("Mutex timeout"))},t),navigator.locks.request(this._database+"_lock",{signal:n.signal},t=>(this._has=!!t,e(!!t),new Promise(t=>{this._release=t})))})}async release({force:t=!1}={}){this._has=!1,this._release?this._release():t&&navigator.locks.request(this._database+"_lock",{steal:!0},t=>!0)}}},function(t,e){t.exports=class{constructor(t){this.type=t.type,this.mode=t.mode,this.size=t.size,this.ino=t.ino,this.mtimeMs=t.mtimeMs,this.ctimeMs=t.ctimeMs||t.mtimeMs,this.uid=1,this.gid=1,this.dev=1}isFile(){return"file"===this.type}isDirectory(){return"dir"===this.type}isSymbolicLink(){return"symlink"===this.type}}}])}); \ No newline at end of file diff --git a/apps/remix-ide/src/assets/js/migrate.js b/apps/remix-ide/src/assets/js/migrate.js deleted file mode 100644 index 1de4cd0cb1..0000000000 --- a/apps/remix-ide/src/assets/js/migrate.js +++ /dev/null @@ -1,139 +0,0 @@ -// eslint-disable-next-line no-unused-vars -async function migrateFilesFromLocalStorage (cb) { - let testmigration = false // migration loads test data into localstorage with browserfs - // indexeddb will be empty by this point, so there is no danger but do a check for the origin to load test data so it runs only locally - testmigration = window.location.hash.includes('e2e_testmigration=true') && window.location.host === '127.0.0.1:8080' && window.location.protocol === 'http:' - // eslint-disable-next-line no-undef - BrowserFS.install(window) - // eslint-disable-next-line no-undef - BrowserFS.configure({ - fs: 'LocalStorage' - }, async function (e) { - if (e) console.log(e) - - const browserFS = window.require('fs') - - /** - * copy the folder recursively (internal use) - * @param {string} path is the folder to be copied over - * @param {Function} visitFile is a function called for each visited files - * @param {Function} visitFolder is a function called for each visited folders - */ - async function _copyFolderToJsonInternal (path, visitFile, visitFolder, fs) { - visitFile = visitFile || (() => { }) - visitFolder = visitFolder || (() => { }) - return new Promise((resolve, reject) => { - const json = {} - if (fs.existsSync(path)) { - try { - const items = fs.readdirSync(path) - visitFolder({ path }) - if (items.length !== 0) { - items.forEach(async (item, index) => { - const file = {} - const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}` - if (fs.statSync(curPath).isDirectory()) { - file.children = await _copyFolderToJsonInternal(curPath, visitFile, visitFolder, fs) - } else { - file.content = fs.readFileSync(curPath, 'utf8') - visitFile({ path: curPath, content: file.content }) - } - json[curPath] = file - }) - } - } catch (e) { - console.log(e) - return reject(e) - } - } - return resolve(json) - }) - } - - /** - * copy the folder recursively - * @param {string} path is the folder to be copied over - * @param {Function} visitFile is a function called for each visited files - * @param {Function} visitFolder is a function called for each visited folders - */ - async function copyFolderToJson (path, visitFile, visitFolder, fs) { - visitFile = visitFile || (() => { }) - visitFolder = visitFolder || (() => { }) - return _copyFolderToJsonInternal(path, visitFile, visitFolder, fs) - } - - const populateWorkspace = async (json, fs) => { - for (const item in json) { - const isFolder = json[item].content === undefined - if (isFolder) { - await createDir(item, fs) - await populateWorkspace(json[item].children, fs) - } else { - try { - await fs.writeFile(item, json[item].content, 'utf8') - } catch (error) { - console.log(error) - } - } - } - } - - const createDir = async (path, fs) => { - const paths = path.split('/') - if (paths.length && paths[0] === '') paths.shift() - let currentCheck = '' - for (const value of paths) { - currentCheck = currentCheck + (currentCheck ? '/' : '') + value - if (!await fs.exists(currentCheck)) { - try { - await fs.mkdir(currentCheck) - } catch (error) { - console.log(error) - } - } - } - } - // - if (testmigration) await populateWorkspace(testData, browserFS) - const files = await copyFolderToJson('/', null, null, browserFS) - await populateWorkspace(files, window.remixFileSystem) - // eslint-disable-next-line no-undef - if (cb) cb() - }) -} - -/* eslint-disable no-template-curly-in-string */ -const testData = { - '.workspaces': { - children: { - '.workspaces/default_workspace': { - children: { - '.workspaces/default_workspace/README.txt': { - content: 'TEST README' - } - } - }, - '.workspaces/workspace_test': { - children: { - '.workspaces/workspace_test/TEST_README.txt': { - content: 'TEST README' - }, - '.workspaces/workspace_test/test_contracts': { - children: { - '.workspaces/workspace_test/test_contracts/1_Storage.sol': { - content: 'testing' - }, - '.workspaces/workspace_test/test_contracts/artifacts': { - children: { - '.workspaces/workspace_test/test_contracts/artifacts/Storage_metadata.json': { - content: '{ "test": "data" }' - } - } - } - } - } - } - } - } - } -} diff --git a/apps/remix-ide/src/blockchain/blockchain.js b/apps/remix-ide/src/blockchain/blockchain.js index 8b0ebfba87..3382d3a649 100644 --- a/apps/remix-ide/src/blockchain/blockchain.js +++ b/apps/remix-ide/src/blockchain/blockchain.js @@ -355,10 +355,14 @@ export class Blockchain extends Plugin { this.executionContext.detectNetwork((error, network) => { if (error || !network) return if (network.name === 'VM') return - this.call('terminal', 'logHtml', + const viewEtherScanLink = etherScanLink(network.name, txhash) + + if (viewEtherScanLink) { + this.call('terminal', 'logHtml', ( view on etherscan )) + } }) }) this.txRunner = new TxRunner(web3Runner, { runAsync: true }) diff --git a/apps/remix-ide/src/index.html b/apps/remix-ide/src/index.html index 3587ad1233..1fa68cd3b5 100644 --- a/apps/remix-ide/src/index.html +++ b/apps/remix-ide/src/index.html @@ -28,8 +28,6 @@ - - - -