Merge branch 'master' into themeFcolors

themeFcolors
Liana Husikyan 3 years ago committed by GitHub
commit 4eb9957eae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 28
      .circleci/config.yml
  2. 4
      apps/remix-ide-e2e/src/commands/goToVMTraceStep.ts
  3. 2
      apps/remix-ide-e2e/src/select_tests.sh
  4. 83
      apps/remix-ide-e2e/src/tests/debugger.test.ts
  5. 12
      apps/remix-ide-e2e/src/tests/editor.test.ts
  6. 24
      apps/remix-ide-e2e/src/tests/importFromGithub.test.ts
  7. 105
      apps/remix-ide-e2e/src/tests/migrateFileSystem.test.ts
  8. 40
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  9. 122
      apps/remix-ide-e2e/src/tests/search.test.ts
  10. 16
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  11. 195
      apps/remix-ide-e2e/src/tests/stress.editor.ts
  12. 15
      apps/remix-ide-e2e/src/tests/url.spec.ts
  13. 9
      apps/remix-ide/src/app.js
  14. 21
      apps/remix-ide/src/app/components/hidden-panel.tsx
  15. 21
      apps/remix-ide/src/app/components/main-panel.tsx
  16. 32
      apps/remix-ide/src/app/components/plugin-manager-component.js
  17. 130
      apps/remix-ide/src/app/components/preload.tsx
  18. 21
      apps/remix-ide/src/app/components/side-panel.tsx
  19. 23
      apps/remix-ide/src/app/components/styles/preload.css
  20. 33
      apps/remix-ide/src/app/components/vertical-icons.tsx
  21. 106
      apps/remix-ide/src/app/editor/editor.js
  22. 34
      apps/remix-ide/src/app/files/fileManager.ts
  23. 72
      apps/remix-ide/src/app/files/fileSystem.ts
  24. 190
      apps/remix-ide/src/app/files/filesystems/fileSystemUtility.ts
  25. 91
      apps/remix-ide/src/app/files/filesystems/indexedDB.ts
  26. 57
      apps/remix-ide/src/app/files/filesystems/localStorage.ts
  27. 23
      apps/remix-ide/src/app/files/remixDProvider.js
  28. 1
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  29. 13
      apps/remix-ide/src/app/panels/file-panel.js
  30. 27
      apps/remix-ide/src/app/panels/tab-proxy.js
  31. 32
      apps/remix-ide/src/app/panels/terminal.js
  32. 2
      apps/remix-ide/src/app/plugins/config.ts
  33. 45
      apps/remix-ide/src/app/plugins/storage.ts
  34. 52
      apps/remix-ide/src/app/tabs/analysis-tab.js
  35. 19
      apps/remix-ide/src/app/tabs/compile-tab.js
  36. 10
      apps/remix-ide/src/app/tabs/debugger-tab.js
  37. 12
      apps/remix-ide/src/app/tabs/hardhat-provider.tsx
  38. 32
      apps/remix-ide/src/app/tabs/search.tsx
  39. 46
      apps/remix-ide/src/app/tabs/settings-tab.tsx
  40. 27
      apps/remix-ide/src/app/tabs/test-tab.js
  41. 20
      apps/remix-ide/src/app/tabs/theme-module.js
  42. 11
      apps/remix-ide/src/app/udapp/run-tab.js
  43. 13
      apps/remix-ide/src/app/ui/landing-page/landing-page.js
  44. 77
      apps/remix-ide/src/assets/img/Search_Icon.svg
  45. 63
      apps/remix-ide/src/assets/js/init.js
  46. 1
      apps/remix-ide/src/assets/js/lightning-fs.min.js
  47. 139
      apps/remix-ide/src/assets/js/migrate.js
  48. 6
      apps/remix-ide/src/blockchain/blockchain.js
  49. 4
      apps/remix-ide/src/index.html
  50. 50
      apps/remix-ide/src/index.tsx
  51. 57
      apps/remix-ide/src/lib/commands.js
  52. 2
      apps/remix-ide/src/production.index.html
  53. 12
      apps/remix-ide/src/remixAppManager.js
  54. 2
      apps/remix-ide/src/remixEngine.js
  55. 71
      apps/solidity-compiler/src/app/compiler-api.ts
  56. 108
      libs/remix-core-plugin/src/lib/compiler-artefacts.ts
  57. 55
      libs/remix-core-plugin/src/lib/compiler-fetch-and-compile.ts
  58. 28
      libs/remix-core-plugin/src/lib/compiler-metadata.ts
  59. 2
      libs/remix-core-plugin/src/lib/editor-context-listener.ts
  60. 59
      libs/remix-core-plugin/src/lib/helpers/fetch-etherscan.ts
  61. 46
      libs/remix-core-plugin/src/lib/helpers/fetch-sourcify.ts
  62. 2
      libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts
  63. 1
      libs/remix-debug/src/Ethdebugger.ts
  64. 6
      libs/remix-debug/src/debugger/debugger.ts
  65. 17
      libs/remix-debug/src/debugger/stepManager.ts
  66. 8
      libs/remix-debug/src/solidity-decoder/decodeInfo.ts
  67. 12
      libs/remix-debug/src/solidity-decoder/types/FunctionType.ts
  68. 2
      libs/remix-debug/src/source/offsetToLineColumnConverter.ts
  69. 2
      libs/remix-debug/src/storage/storageResolver.ts
  70. 5
      libs/remix-lib/README.md
  71. 23
      libs/remix-lib/src/execution/eventsDecoder.ts
  72. 11
      libs/remix-lib/src/index.ts
  73. 16
      libs/remix-lib/src/types/ICompilerApi.ts
  74. 55
      libs/remix-lib/src/web3Provider/dummyProvider.ts
  75. 38
      libs/remix-lib/src/web3Provider/web3Providers.ts
  76. 49
      libs/remix-simulator/src/VmProxy.ts
  77. 40
      libs/remix-simulator/src/vm-context.ts
  78. 8
      libs/remix-solidity/src/compiler/compiler-abstract.ts
  79. 4
      libs/remix-solidity/src/compiler/compiler-helpers.ts
  80. 8
      libs/remix-solidity/src/compiler/compiler-input.ts
  81. 1
      libs/remix-solidity/src/compiler/compiler-worker.ts
  82. 22
      libs/remix-solidity/src/compiler/compiler.ts
  83. 1
      libs/remix-solidity/src/compiler/types.ts
  84. 6
      libs/remix-tests/src/compiler.ts
  85. 2
      libs/remix-ui/app/src/index.ts
  86. 4
      libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx
  87. 10
      libs/remix-ui/app/src/lib/remix-app/context/provider.tsx
  88. 3
      libs/remix-ui/app/src/lib/remix-app/interface/index.ts
  89. 3
      libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts
  90. 39
      libs/remix-ui/app/src/lib/remix-app/remix-app.tsx
  91. 4
      libs/remix-ui/debugger-ui/src/lib/button-navigator/button-navigator.css
  92. 9
      libs/remix-ui/debugger-ui/src/lib/button-navigator/button-navigator.tsx
  93. 36
      libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx
  94. 32
      libs/remix-ui/debugger-ui/src/lib/slider/slider.tsx
  95. 2
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/assembly-items.tsx
  96. 5
      libs/remix-ui/editor/src/lib/remix-ui-editor.css
  97. 160
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  98. 1
      libs/remix-ui/helper/src/index.ts
  99. 24
      libs/remix-ui/helper/src/lib/components/PluginViewWrapper.tsx
  100. 9
      libs/remix-ui/helper/src/lib/helper-components.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -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:

@ -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()
})

@ -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")

@ -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;
}
}`
}
}
]

@ -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) {

@ -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()
}
}

@ -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')
},
}

@ -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')
}
}

@ -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)
})
}
}

@ -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)

@ -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;
}
}
`

@ -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()
}
}

@ -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',

@ -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<any> = () => {}
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 <RemixPluginPanel header={<></>} plugins={state.plugins}/>
}
setDispatch (dispatch: React.Dispatch<any>) {
this.dispatch = dispatch
}
render() {
return (
<div className='pluginsContainer'><PluginViewWrapper plugin={this} /></div>
);
}
renderComponent () {
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.el)
this.dispatch({
plugins: this.plugins,
})
}
}

@ -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<any> = () => {}
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<any>) {
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 <div style={{height: '100%', width: '100%'}} data-id='mainPanelPluginsContainer'><PluginViewWrapper plugin={this} /></div>
}
renderComponent () {
ReactDOM.render(<RemixPluginPanel header={<></>} plugins={this.plugins}/>, this.element)
updateComponent (state: any) {
return <RemixPluginPanel header={<></>} plugins={state.plugins}/>
}
}

@ -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 <RemixUiPluginManager
pluginComponent={state}/>
}
renderComponent () {
ReactDOM.render(
<RemixUiPluginManager
pluginComponent={this}
/>,
this.htmlElement)
if(this.dispatch) this.dispatch({...this, activePlugins: this.activePlugins, inactivePlugins: this.inactivePlugins})
}
render () {
return this.htmlElement
return (
<div id='pluginManager'><PluginViewWrapper plugin={this} /></div>
);
}
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)

@ -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<boolean>(true)
const [error, setError] = useState<boolean>(false)
const [showDownloader, setShowDownloader] = useState<boolean>(false)
const remixFileSystems = useRef<fileSystems>(new fileSystems())
const remixIndexedDB = useRef<fileSystem>(new indexedDBFileSystem())
const localStorageFileSystem = useRef<fileSystem>(new localStorageFS())
// url parameters to e2e test the fallbacks and error warnings
const testmigrationFallback = useRef<boolean>(window.location.hash.includes('e2e_testmigration_fallback=true') && window.location.host === '127.0.0.1:8080' && window.location.protocol === 'http:')
const testmigrationResult = useRef<boolean>(window.location.hash.includes('e2e_testmigration=true') && window.location.host === '127.0.0.1:8080' && window.location.protocol === 'http:')
const testBlockStorage = useRef<boolean>(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(
<>
<RemixApp app={appComponent} />
</>,
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 <>
<div className='preload-container'>
<div className='preload-logo pb-4'>
{logo}
<div className="info-secondary splash">
REMIX IDE
<br />
<span className='version'> v{packageJson.version}</span>
</div>
</div>
{!supported ?
<div className='preload-info-container alert alert-warning'>
Your browser does not support any of the filesytems required by Remix.
Either change the settings in your browser or use a supported browser.
</div> : null}
{error ?
<div className='preload-info-container alert alert-danger'>
An unknown error has occured loading the application.
</div> : null}
{showDownloader ?
<div className='preload-info-container alert alert-info'>
This app will be updated now. Please download a backup of your files now to make sure you don't lose your work.
<br></br>
You don't need to do anything else, your files will be available when the app loads.
<div onClick={async () => { await downloadBackup() }} data-id='downloadbackup-btn' className='btn btn-primary mt-1'>download backup</div>
<div onClick={async () => { await migrateAndLoad() }} data-id='skipbackup-btn' className='btn btn-primary mt-1'>skip backup</div>
</div> : null}
{(supported && !error && !showDownloader) ?
<div>
<i className="fas fa-spinner fa-spin fa-2x"></i>
</div> : null}
</div>
</>
}
const logo = <svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 105 100">
<path d="M91.84,35a.09.09,0,0,1-.1-.07,41,41,0,0,0-79.48,0,.09.09,0,0,1-.1.07C9.45,35,1,35.35,1,42.53c0,8.56,1,16,6,20.32,2.16,1.85,5.81,2.3,9.27,2.22a44.4,44.4,0,0,0,6.45-.68.09.09,0,0,0,.06-.15A34.81,34.81,0,0,1,17,45c0-.1,0-.21,0-.31a35,35,0,0,1,70,0c0,.1,0,.21,0,.31a34.81,34.81,0,0,1-5.78,19.24.09.09,0,0,0,.06.15,44.4,44.4,0,0,0,6.45.68c3.46.08,7.11-.37,9.27-2.22,5-4.27,6-11.76,6-20.32C103,35.35,94.55,35,91.84,35Z" />
<path d="M52,74,25.4,65.13a.1.1,0,0,0-.1.17L51.93,91.93a.1.1,0,0,0,.14,0L78.7,65.3a.1.1,0,0,0-.1-.17L52,74A.06.06,0,0,1,52,74Z" />
<path d="M75.68,46.9,82,45a.09.09,0,0,0,.08-.09,29.91,29.91,0,0,0-.87-6.94.11.11,0,0,0-.09-.08l-6.43-.58a.1.1,0,0,1-.06-.18l4.78-4.18a.13.13,0,0,0,0-.12,30.19,30.19,0,0,0-3.65-6.07.09.09,0,0,0-.11,0l-5.91,2a.1.1,0,0,1-.12-.14L72.19,23a.11.11,0,0,0,0-.12,29.86,29.86,0,0,0-5.84-4.13.09.09,0,0,0-.11,0l-4.47,4.13a.1.1,0,0,1-.17-.07l.09-6a.1.1,0,0,0-.07-.1,30.54,30.54,0,0,0-7-1.47.1.1,0,0,0-.1.07l-2.38,5.54a.1.1,0,0,1-.18,0l-2.37-5.54a.11.11,0,0,0-.11-.06,30,30,0,0,0-7,1.48.12.12,0,0,0-.07.1l.08,6.05a.09.09,0,0,1-.16.07L37.8,18.76a.11.11,0,0,0-.12,0,29.75,29.75,0,0,0-5.83,4.13.11.11,0,0,0,0,.12l2.59,5.6a.11.11,0,0,1-.13.14l-5.9-2a.11.11,0,0,0-.12,0,30.23,30.23,0,0,0-3.62,6.08.11.11,0,0,0,0,.12l4.79,4.19a.1.1,0,0,1-.06.17L23,37.91a.1.1,0,0,0-.09.07A29.9,29.9,0,0,0,22,44.92a.1.1,0,0,0,.07.1L28.4,47a.1.1,0,0,1,0,.18l-5.84,3.26a.16.16,0,0,0,0,.11,30.17,30.17,0,0,0,2.1,6.76c.32.71.67,1.4,1,2.08a.1.1,0,0,0,.06,0L52,68.16H52l26.34-8.78a.1.1,0,0,0,.06-.05,30.48,30.48,0,0,0,3.11-8.88.1.1,0,0,0-.05-.11l-5.83-3.26A.1.1,0,0,1,75.68,46.9Z" />
</svg>

@ -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<any> = () => {}
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<any>) {
this.dispatch = dispatch
}
render() {
return (
<section className='panel plugin-manager'> <PluginViewWrapper plugin={this} /></section>
);
}
updateComponent(state: any) {
return <RemixPluginPanel header={<RemixUIPanelHeader plugins={state.plugins}></RemixUIPanelHeader>} plugins={state.plugins} />
}
renderComponent() {
ReactDOM.render(<RemixPluginPanel header={<RemixUIPanelHeader plugins={this.plugins}></RemixUIPanelHeader>} plugins={this.plugins} />, this.sideelement)
this.dispatch({
plugins: this.plugins
})
}
}

@ -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;
}

@ -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<string, IconRecord> = {}
dispatch: React.Dispatch<any> = () => {}
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(
<RemixUiVerticalIconsPanel
verticalIconsPlugin={this}
icons={sorted}
/>,
this.htmlElement)
this.dispatch({
verticalIconsPlugin: this,
icons: sorted
})
}
setDispatch (dispatch: React.Dispatch<any>) {
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 <RemixUiVerticalIconsPanel
verticalIconsPlugin={state.verticalIconsPlugin}
icons={state.icons}
/>
}
render() {
return (
<div id='icon-panel'><PluginViewWrapper plugin={this} /></div>
);
}
}

@ -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 <EditorUI
editorAPI={state.api}
themeType={state.currentThemeType}
currentFile={state.currentFile}
events={state.events}
plugin={state.plugin}
/>
}
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 <div ref={(element)=>{
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'>
<PluginViewWrapper plugin={this} />
</div>
}
renderComponent () {
ReactDOM.render(
<EditorUI
editorAPI={this.api}
themeType={this.currentThemeType}
currentFile={this.currentFile}
sourceAnnotationsPerFile={this.sourceAnnotationsPerFile}
markerPerFile={this.markerPerFile}
events={this.events}
plugin={this}
/>
, 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)
}
}
}

@ -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)
}

@ -0,0 +1,72 @@
export class fileSystem {
name: string
enabled: boolean
available: boolean
fs: any
fsCallBack: any;
hasWorkSpaces: boolean
loaded: boolean
load: () => Promise<unknown>
test: () => Promise<unknown>
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<string, fileSystem>
constructor() {
this.fileSystems = {}
}
addFileSystem = async (fs: fileSystem): Promise<boolean> => {
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<fileSystem> => {
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
}
}

@ -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" }'
}
}
}
}
}
}
}
}
}
}

@ -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<unknown>; rmdir: (path: any) => Promise<void>; readdir: (path: any) => Promise<string[]>; unlink: (path: any) => Promise<void>; mkdir: (path: any) => Promise<void>; readFile: (path: any, options: any) => Promise<Uint8Array>; rename: (from: any, to: any) => Promise<void>; writeFile: (path: any, content: any, options: any) => Promise<void>; stat: (path: any) => Promise<import("fs").Stats>; init(name: string, opt?: LightningFS.FSConstructorOptions): void; activate(): Promise<void>; deactivate(): Promise<void>; lstat(filePath: string): Promise<import("fs").Stats>; readlink(filePath: string): Promise<string>; symlink(target: string, filePath: string): Promise<void> }
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)
};
})
}
}

@ -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)
}
})
}
}

@ -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) {

@ -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('/')

@ -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(
<FileSystemProvider plugin={this} />
, this.el)
return <div id='fileExplorerView'><FileSystemProvider plugin={this} /></div>
}
/**

@ -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 <TabsUI tabs={state.loadedTabs} onSelect={state.onSelect} onClose={state.onClose} onZoomIn={state.onZoomIn} onZoomOut={state.onZoomOut} onReady={state.onReady} />
}
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(
<TabsUI tabs={this.loadedTabs} onSelect={onSelect} onClose={onClose} onZoomIn={onZoomIn} onZoomOut={onZoomOut} onReady={onReady} />
, this.el)
this.dispatch({
loadedTabs: this.loadedTabs,
onSelect,
onClose,
onZoomIn,
onZoomOut,
onReady
})
}
renderTabsbar () {
return this.el
return <div><PluginViewWrapper plugin={this} /></div>
}
}

@ -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 <div id='terminal-view' className='panel' data-id='terminalContainer-view'><PluginViewWrapper plugin={this}/></div>
}
updateComponent(state) {
return <RemixUiTerminal
plugin={state.plugin}
onReady={state.onReady}
/>
}
renderComponent () {
const onReady = (api) => { this.terminalApi = api }
ReactDOM.render(
<RemixUiTerminal
plugin={this}
onReady={onReady}
/>,
this.element
)
this.dispatch({
plugin: this,
onReady: onReady
})
}
scroll2bottom () {

@ -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

@ -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];
}
}

@ -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 <div id='staticAnalyserView'><PluginViewWrapper plugin={this} /></div>
}
updateComponent(state) {
return <RemixUiStaticAnalyser
registry={state.registry}
analysisModule={state.analysisModule}
event={state.event}
/>
}
renderComponent () {
ReactDOM.render(
<RemixUiStaticAnalyser
registry={this.registry}
analysisModule={this}
event={this.event}
/>,
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
})
}
}

@ -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(
<SolidityCompiler api={this}/>
, 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 <div id='compileTabView'><SolidityCompiler api={this}/></div>
}
async compileWithParameters (compilationTargets, settings) {

@ -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 <div className={css.debuggerTabView} id='debugView'><DebuggerUI debuggerAPI={this} /></div>
}
showMessage (title, message) {
@ -68,9 +65,4 @@ export class DebuggerTab extends DebuggerApiMixin(ViewPlugin) {
}
}
renderComponent () {
ReactDOM.render(
<DebuggerUI debuggerAPI={this} />
, this.el)
}
}

@ -49,11 +49,13 @@ export class HardhatProvider extends Plugin {
}
hardhatProviderDialogBody (): JSX.Element {
return (<div> Note: To run Hardhat network node on your system, go to hardhat project folder and run command:
<div className="border p-1">npx hardhat node</div>
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a>
Hardhat JSON-RPC Endpoint
</div>)
return (
<div> Note: To run Hardhat network node on your system, go to hardhat project folder and run command:
<div className="border p-1">npx hardhat node</div>
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a>
<div>Hardhat JSON-RPC Endpoint:</div>
</div>
)
}
sendAsync (data: JsonDataRequest): Promise<any> {

@ -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 (
<div id='searchTab'>
<SearchTab plugin={this}></SearchTab>
</div>
);
}
}

@ -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<any> = () => {}
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<any>) {
this.dispatch = dispatch
this.renderComponent()
}
render () {
return this.element
render() {
return <div id='settingsTab'>
<PluginViewWrapper plugin={this} />
</div>
}
updateComponent(state: any){
return <RemixUiSettings
config={state.config}
editor={state.editor}
_deps={state._deps}
useMatomoAnalytics={state.useMatomoAnalytics}
themeModule = {state._deps.themeModule}
/>
}
renderComponent () {
ReactDOM.render(
<RemixUiSettings
config = { this.config }
editor = { this.editor }
_deps = { this._deps }
useMatomoAnalytics = {this.useMatomoAnalytics}
/>,
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
})
}
}
}

@ -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 <div><PluginViewWrapper plugin={this} /></div>
}
updateComponent(state) {
return <SolidityUnitTesting testTab={state.testTab} helper={state.helper} initialPath={state.testDirPath} />
}
renderComponent (testDirPath) {
ReactDOM.render(
<SolidityUnitTesting testTab={this} helper={helper} initialPath={testDirPath} />
, this.element)
this.dispatch({
testTab: this,
helper: this.helper,
testDirPath: testDirPath
})
}
}

@ -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')

@ -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 <div><RunTabUI plugin={this} /></div>
}
renderComponent () {
ReactDOM.render(
<RunTabUI plugin={this} />
, this.el)
}
onReady (api) {
this.REACT_API = api

@ -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 <div id='landingPageHomeContainer' className='remixui_homeContainer justify-content-between bg-light d-flex' data-id='landingPageHomeContainer'><RemixUiHomeTab
plugin={this}
/></div>
}
renderComponent () {
ReactDOM.render(
<RemixUiHomeTab
plugin={this}
/>
, this.el)
}
}

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="500"
height="500"
viewBox="0 0 500.00001 500.00001"
id="svg4162"
version="1.1"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="Search_Icon.svg">
<defs
id="defs4164" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.954"
inkscape:cx="250"
inkscape:cy="250"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata4167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-552.36216)">
<g
id="g1400"
transform="translate(-4.3609793,-7.6704785)">
<path
inkscape:connector-curvature="0"
id="path4714"
d="M 232.83952,614.96702 A 154.04816,154.04794 0 0 0 78.79153,769.01382 154.04816,154.04794 0 0 0 232.83952,923.06184 154.04816,154.04794 0 0 0 386.88751,769.01382 154.04816,154.04794 0 0 0 232.83952,614.96702 Z m 0,26.77613 A 129.95832,127.2707 0 0 1 362.79832,769.01382 129.95832,127.2707 0 0 1 232.83952,896.28449 129.95832,127.2707 0 0 1 102.88194,769.01382 129.95832,127.2707 0 0 1 232.83952,641.74315 Z"
style="opacity:1;fill:#2b0000;fill-opacity:1;stroke:none;stroke-opacity:1" />
<rect
ry="18.08342"
rx="33.249443"
transform="matrix(0.65316768,0.7572133,-0.60689051,0.79478545,0,0)"
y="319.55432"
x="794.8775"
height="36.16684"
width="173.02675"
id="rect4721"
style="opacity:1;fill:#2b0000;fill-opacity:1;stroke:none;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -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
}

File diff suppressed because one or more lines are too long

@ -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" }'
}
}
}
}
}
}
}
}
}
}

@ -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',
(<a href={etherScanLink(network.name, txhash)} target="_blank">
view on etherscan
</a>))
}
})
})
this.txRunner = new TxRunner(web3Runner, { runAsync: true })

@ -28,8 +28,6 @@
<link rel="icon" type="x-icon" href="assets/img/icon.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intro.js/4.1.0/introjs.min.css">
<script src="assets/js/browserfs.min.js"></script>
<script src="assets/js/migrate.js"></script>
<script src="assets/js/lightning-fs.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!-- Matomo -->
<script type="text/javascript">
@ -42,7 +40,7 @@
var _paq = window._paq = window._paq || []
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['disableCookies']);
_paq.push(['enableJSErrorTracking']);
_paq.push(['enableJSErrorTracking']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {

@ -1,45 +1,27 @@
// eslint-disable-next-line no-use-before-define
import React from 'react'
import { render } from 'react-dom'
// eslint-disable-next-line no-unused-vars
import { RemixApp } from '@remix-ui/app'
import * as packageJson from '../../../package.json'
import './index.css'
import { ThemeModule } from './app/tabs/theme-module'
import { Preload } from './app/components/preload'
import Config from './config'
import Registry from './app/state/registry'
import { Storage } from '@remix-project/remix-lib'
(async function() {
// load current theme befor anything else
try {
const configStorage = new Storage('config-v0.8:')
const config = new Config(configStorage);
Registry.getInstance().put({ api: config, name: 'config' })
} catch (e) {}
const theme = new ThemeModule()
theme.initTheme()
(function () {
render(
<React.StrictMode>
<div style={{ display: 'block' }} className='centered'>
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 105 100">
<path d="M91.84,35a.09.09,0,0,1-.1-.07,41,41,0,0,0-79.48,0,.09.09,0,0,1-.1.07C9.45,35,1,35.35,1,42.53c0,8.56,1,16,6,20.32,2.16,1.85,5.81,2.3,9.27,2.22a44.4,44.4,0,0,0,6.45-.68.09.09,0,0,0,.06-.15A34.81,34.81,0,0,1,17,45c0-.1,0-.21,0-.31a35,35,0,0,1,70,0c0,.1,0,.21,0,.31a34.81,34.81,0,0,1-5.78,19.24.09.09,0,0,0,.06.15,44.4,44.4,0,0,0,6.45.68c3.46.08,7.11-.37,9.27-2.22,5-4.27,6-11.76,6-20.32C103,35.35,94.55,35,91.84,35Z"/>
<path d="M52,74,25.4,65.13a.1.1,0,0,0-.1.17L51.93,91.93a.1.1,0,0,0,.14,0L78.7,65.3a.1.1,0,0,0-.1-.17L52,74A.06.06,0,0,1,52,74Z"/>
<path d="M75.68,46.9,82,45a.09.09,0,0,0,.08-.09,29.91,29.91,0,0,0-.87-6.94.11.11,0,0,0-.09-.08l-6.43-.58a.1.1,0,0,1-.06-.18l4.78-4.18a.13.13,0,0,0,0-.12,30.19,30.19,0,0,0-3.65-6.07.09.09,0,0,0-.11,0l-5.91,2a.1.1,0,0,1-.12-.14L72.19,23a.11.11,0,0,0,0-.12,29.86,29.86,0,0,0-5.84-4.13.09.09,0,0,0-.11,0l-4.47,4.13a.1.1,0,0,1-.17-.07l.09-6a.1.1,0,0,0-.07-.1,30.54,30.54,0,0,0-7-1.47.1.1,0,0,0-.1.07l-2.38,5.54a.1.1,0,0,1-.18,0l-2.37-5.54a.11.11,0,0,0-.11-.06,30,30,0,0,0-7,1.48.12.12,0,0,0-.07.1l.08,6.05a.09.09,0,0,1-.16.07L37.8,18.76a.11.11,0,0,0-.12,0,29.75,29.75,0,0,0-5.83,4.13.11.11,0,0,0,0,.12l2.59,5.6a.11.11,0,0,1-.13.14l-5.9-2a.11.11,0,0,0-.12,0,30.23,30.23,0,0,0-3.62,6.08.11.11,0,0,0,0,.12l4.79,4.19a.1.1,0,0,1-.06.17L23,37.91a.1.1,0,0,0-.09.07A29.9,29.9,0,0,0,22,44.92a.1.1,0,0,0,.07.1L28.4,47a.1.1,0,0,1,0,.18l-5.84,3.26a.16.16,0,0,0,0,.11,30.17,30.17,0,0,0,2.1,6.76c.32.71.67,1.4,1,2.08a.1.1,0,0,0,.06,0L52,68.16H52l26.34-8.78a.1.1,0,0,0,.06-.05,30.48,30.48,0,0,0,3.11-8.88.1.1,0,0,0-.05-.11l-5.83-3.26A.1.1,0,0,1,75.68,46.9Z"/>
</svg>
<div className="info-secondary splash">
REMIX IDE
<br />
<span className='version'> v{ packageJson.version }</span>
</div>
<div style={{ marginTop: '50%', textAlign: 'center' }}>
<i className="fas fa-spinner fa-spin fa-2x"></i>
</div>
</div>
<Preload></Preload>
</React.StrictMode>,
document.getElementById('root')
)
})()
import ('./app').then((AppComponent) => {
const appComponent = new AppComponent.default()
appComponent.run().then(() => {
render(
<React.StrictMode>
<RemixApp app={appComponent} />
</React.StrictMode>,
document.getElementById('root')
)
})
}).catch(err => {
console.log('Error on loading Remix:', err)
})

@ -1,57 +0,0 @@
const allPrograms = [
{ ethers: 'The ethers.js library is a compact and complete JavaScript library for Ethereum.' },
{ remix: 'Ethereum IDE and tools for the web.' },
{ web3: 'The web3.js library is a collection of modules which contain specific functionality for the ethereum ecosystem.' },
{ swarmgw: 'This library can be used to upload/download files to Swarm via https://swarm-gateways.net/.' }
]
const allCommands = [
{ 'remix.execute(filepath)': 'Run the script specified by file path. If filepath is empty, script currently displayed in the editor is executed.' },
{ 'remix.exeCurrent()': 'Run the script currently displayed in the editor.' },
{ 'remix.help()': 'Display this help message.' },
{ 'remix.loadgist(id)': 'Load a gist in the file explorer.' },
{ 'remix.loadurl(url)': 'Load the given url in the file explorer. The url can be of type github, swarm or ipfs.' },
{ 'swarmgw.get(url, cb)': 'Download files from Swarm via https://swarm-gateways.net/' },
{ 'swarmgw.put(content, cb)': 'Upload files to Swarm via https://swarm-gateways.net/' },
{ 'ethers.Contract': 'This API provides a graceful connection to a contract deployed on the blockchain, simplifying calling and querying its functions and handling all the binary protocol and conversion as necessarily.' },
{ 'ethers.HDNode': 'A Hierarchical Deterministic Wallet represents a large tree of private keys which can reliably be reproduced from an initial seed.' },
{ 'ethers.Interface': 'The Interface Object is a meta-class that accepts a Solidity (or compatible) Application Binary Interface (ABI) and populates functions to deal with encoding and decoding the parameters to pass in and results returned.' },
{ 'ethers.providers': 'A Provider abstracts a connection to the Ethereum blockchain, for issuing queries and sending state changing transactions.' },
{ 'ethers.SigningKey': 'The SigningKey interface provides an abstraction around the secp256k1 elliptic curve cryptography library.' },
{ 'ethers.utils': 'The utility functions exposed in both the ethers umbrella package and the ethers-utils.' },
{ 'ethers.utils.AbiCoder': 'Create a new ABI Coder object' },
{ 'ethers.utils.RLP': 'This encoding method is used internally for several aspects of Ethereum, such as encoding transactions and determining contract addresses.' },
{ 'ethers.Wallet': 'A wallet manages a private/public key pair which is used to cryptographically sign transactions and prove ownership on the Ethereum network.' },
{ 'ethers.version': 'Contains the version of the ethers container object.' },
{ 'web3.eth': 'Eth module for interacting with the Ethereum network.' },
{ 'web3.eth.accounts': 'The web3.eth.accounts contains functions to generate Ethereum accounts and sign transactions and data.' },
{ 'web3.eth.abi': 'The web3.eth.abi functions let you de- and encode parameters to ABI (Application Binary Interface) for function calls to the EVM (Ethereum Virtual Machine).' },
{ 'web3.eth.ens': 'The web3.eth.ens functions let you interacting with ENS.' },
{ 'web3.eth.Iban': 'The web3.eth.Iban function lets convert Ethereum addresses from and to IBAN and BBAN.' },
{ 'web3.eth.net': 'Net module for interacting with network properties.' },
{ 'web3.eth.personal': 'Personal module for interacting with the Ethereum accounts.' },
{ 'web3.eth.subscribe': 'The web3.eth.subscribe function lets you subscribe to specific events in the blockchain.' },
{ 'web3.givenProvider': 'When using web3.js in an Ethereum compatible browser, it will set with the current native provider by that browser. Will return the given provider by the (browser) environment, otherwise null.' },
{ 'web3.modules': 'Contains the version of the web3 container object.' },
{ 'web3.providers': 'Contains the current available providers.' },
{ 'web3.shh': 'Shh module for interacting with the whisper protocol' },
{ 'web3.utils': 'This package provides utility functions for Ethereum dapps and other web3.js packages.' },
{ 'web3.version': 'Contains the version of the web3 container object.' },
{ 'web3.eth.clearSubscriptions();': 'Resets subscriptions.' },
{ 'web3.eth.Contract(jsonInterface[, address][, options])': 'The web3.eth.Contract object makes it easy to interact with smart contracts on the ethereum blockchain.' },
{ 'web3.eth.accounts.create([entropy]);': 'The web3.eth.accounts contains functions to generate Ethereum accounts and sign transactions and data.' },
{ 'web3.eth.getAccounts();': 'Retrieve the list of accounts' },
{ 'web3.eth.accounts.privateKeyToAccount(privateKey [, ignoreLength ]);': 'Get the account from the private key' },
{ 'web3.eth.accounts.signTransaction(tx, privateKey [, callback]);': 'Sign Transaction' },
{ 'web3.eth.accounts.recoverTransaction(rawTransaction);': 'Sign Transaction' },
{ 'web3.eth.accounts.hashMessage(message);': 'Hash message' }
]
module.exports = {
allPrograms,
allCommands
}

@ -28,8 +28,6 @@
<link rel="icon" type="x-icon" href="assets/img/icon.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intro.js/4.1.0/introjs.min.css">
<script src="assets/js/browserfs.min.js"></script>
<script src="assets/js/migrate.js"></script>
<script src="assets/js/lightning-fs.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!-- Matomo -->
<script type="text/javascript">

@ -7,7 +7,7 @@ const _paq = window._paq = window._paq || []
const requiredModules = [ // services + layout views + system views
'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme',
'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons',
'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity-logic', 'gistHandler', 'layout', 'notification', 'permissionhandler', 'walkthrough', 'storage']
'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity-logic', 'gistHandler', 'layout', 'notification', 'permissionhandler', 'walkthrough', 'storage', 'search']
const dependentModules = ['git', 'hardhat', 'slither'] // module which shouldn't be manually activated (e.g git is activated by remixd)
@ -148,6 +148,16 @@ export class RemixAppManager extends PluginManager {
pattern: [],
sticky: true
})
await this.call('filePanel', 'registerContextMenuItem', {
id: 'nahmii-compiler',
name: 'compileCustomAction',
label: 'Compile for Nahmii',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true
})
}
}

@ -15,6 +15,8 @@ export class RemixEngine extends Engine {
if (name === 'hardhat') return { queueTimeout: 60000 * 4 }
if (name === 'localPlugin') return { queueTimeout: 60000 * 4 }
if (name === 'notification') return { queueTimeout: 60000 * 4 }
if (name === 'sourcify') return { queueTimeout: 60000 * 4 }
if (name === 'fetchAndCompile') return { queueTimeout: 60000 * 4 }
return { queueTimeout: 10000 }
}

@ -4,23 +4,26 @@ import type { ConfigurationSettings } from '@remix-project/remix-lib-ts'
export const CompilerApiMixin = (Base) => class extends Base {
currentFile: string
contractMap: {
file: string
} | Record<string, any>
compilationDetails: {
contractMap: {
file: string
} | Record<string, any>,
contractsDetails: Record<string, any>,
target?: string
}
compileErrors: any
compileTabLogic: CompileTabLogic
contractsDetails: Record<string, any>
configurationSettings: ConfigurationSettings
onCurrentFileChanged: (fileName: string) => void
onResetResults: () => void
// onResetResults: () => void
onSetWorkspace: (workspace: any) => void
onNoFileSelected: () => void
onCompilationFinished: (contractsDetails: any, contractMap: any) => void
onCompilationFinished: (compilationDetails: { contractMap: { file: string } | Record<string, any>, contractsDetails: Record<string, any> }) => void
onSessionSwitched: () => void
onContentChanged: () => void
onFileClosed: (name: string) => void
statusChanged: (data: { key: string, title?: string, type?: string }) => void
initCompilerApi () {
this.configurationSettings = null
@ -31,15 +34,15 @@ export const CompilerApiMixin = (Base) => class extends Base {
contractEl: null
}
this.contractsDetails = {}
this.compilationDetails = {
contractsDetails:{},
contractMap: {}
}
this.data = {
eventHandlers: {},
loading: false
}
this.contractMap = {}
this.contractsDetails = {}
this.compileErrors = {}
this.compiledFileName = ''
this.currentFile = ''
@ -188,32 +191,35 @@ export const CompilerApiMixin = (Base) => class extends Base {
resetResults () {
this.currentFile = ''
this.contractsDetails = {}
this.emit('statusChanged', { key: 'none' })
if (this.onResetResults) this.onResetResults()
this.compilationDetails = {
contractsDetails: {},
contractMap: {}
}
this.statusChanged({ key: 'none' })
// if (this.onResetResults) this.onResetResults()
}
listenToEvents () {
this.on('editor', 'contentChanged', () => {
this.emit('statusChanged', { key: 'edited', title: 'the content has changed, needs recompilation', type: 'info' })
this.statusChanged({ key: 'edited', title: 'the content has changed, needs recompilation', type: 'info' })
if (this.onContentChanged) this.onContentChanged()
})
this.data.eventHandlers.onLoadingCompiler = (url) => {
this.data.loading = true
this.data.loadingUrl = url
this.emit('statusChanged', { key: 'loading', title: 'loading compiler...', type: 'info' })
this.statusChanged({ key: 'loading', title: 'loading compiler...', type: 'info' })
}
this.compiler.event.register('loadingCompiler', this.data.eventHandlers.onLoadingCompiler)
this.data.eventHandlers.onCompilerLoaded = () => {
this.data.loading = false
this.emit('statusChanged', { key: 'none' })
this.statusChanged({ key: 'none' })
}
this.compiler.event.register('compilerLoaded', this.data.eventHandlers.onCompilerLoaded)
this.data.eventHandlers.onStartingCompilation = () => {
this.emit('statusChanged', { key: 'loading', title: 'compiling...', type: 'info' })
this.statusChanged({ key: 'loading', title: 'compiling...', type: 'info' })
}
this.data.eventHandlers.onRemoveAnnotations = () => {
@ -249,22 +255,28 @@ export const CompilerApiMixin = (Base) => class extends Base {
}
this.on('fileManager', 'noFileSelected', this.data.eventHandlers.onNoFileSelected)
this.data.eventHandlers.onCompilationFinished = (success, data, source) => {
this.data.eventHandlers.onFileClosed = (name: string) => {
this.onFileClosed(name)
}
this.on('fileManager', 'fileClosed', this.data.eventHandlers.onFileClosed)
this.data.eventHandlers.onCompilationFinished = (success, data, source, input, version) => {
this.compileErrors = data
if (success) {
// forwarding the event to the appManager infra
this.emit('compilationFinished', source.target, source, 'soljson', data)
this.emit('compilationFinished', source.target, source, 'soljson', data, input, version)
if (data.errors && data.errors.length > 0) {
this.emit('statusChanged', {
this.statusChanged({
key: data.errors.length,
title: `compilation finished successful with warning${data.errors.length > 1 ? 's' : ''}`,
type: 'warning'
})
} else this.emit('statusChanged', { key: 'succeed', title: 'compilation successful', type: 'success' })
} else this.statusChanged({ key: 'succeed', title: 'compilation successful', type: 'success' })
// Store the contracts
this.contractsDetails = {}
this.compilationDetails.contractsDetails = {}
this.compiler.visitContracts((contract) => {
this.contractsDetails[contract.name] = parseContracts(
this.compilationDetails.contractsDetails[contract.name] = parseContracts(
contract.name,
contract.object,
this.compiler.getSource(contract.file)
@ -272,12 +284,13 @@ export const CompilerApiMixin = (Base) => class extends Base {
})
} else {
const count = (data.errors ? data.errors.filter(error => error.severity === 'error').length : 0 + (data.error ? 1 : 0))
this.emit('statusChanged', { key: count, title: `compilation failed with ${count} error${count > 1 ? 's' : ''}`, type: 'error' })
this.statusChanged({ key: count, title: `compilation failed with ${count} error${count > 1 ? 's' : ''}`, type: 'error' })
}
// Update contract Selection
this.contractMap = {}
if (success) this.compiler.visitContracts((contract) => { this.contractMap[contract.name] = contract })
if (this.onCompilationFinished) this.onCompilationFinished(this.contractsDetails, this.contractMap)
this.compilationDetails.contractMap = {}
if (success) this.compiler.visitContracts((contract) => { this.compilationDetails.contractMap[contract.name] = contract })
this.compilationDetails.target = source.target
if (this.onCompilationFinished) this.onCompilationFinished(this.compilationDetails)
}
this.compiler.event.register('compilationFinished', this.data.eventHandlers.onCompilationFinished)

@ -4,7 +4,7 @@ import { CompilerAbstract } from '@remix-project/remix-solidity'
const profile = {
name: 'compilerArtefacts',
methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas', 'getLastCompilationResult'],
methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas', 'getLastCompilationResult', 'getArtefactsByContractName'],
events: [],
version: '0.0.1'
}
@ -24,12 +24,12 @@ export class CompilerArtefacts extends Plugin {
}
onActivation () {
const saveCompilationPerFileResult = (file, source, languageVersion, data) => {
this.compilersArtefactsPerFile[file] = new CompilerAbstract(languageVersion, data, source)
const saveCompilationPerFileResult = (file, source, languageVersion, data, input?) => {
this.compilersArtefactsPerFile[file] = new CompilerAbstract(languageVersion, data, source, input)
}
this.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => {
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source)
this.on('solidity', 'compilationFinished', (file, source, languageVersion, data, input, version) => {
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source, input)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
@ -48,16 +48,29 @@ export class CompilerArtefacts extends Plugin {
saveCompilationPerFileResult(file, source, languageVersion, data)
})
this.on('solidityUnitTesting', 'compilationFinished', (file, source, languageVersion, data) => {
this.on('solidityUnitTesting', 'compilationFinished', (file, source, languageVersion, data, input, version) => {
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source, input)
saveCompilationPerFileResult(file, source, languageVersion, data, input)
})
this.on('nahmii-compiler', 'compilationFinished', (file, source, languageVersion, data) => {
this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source)
saveCompilationPerFileResult(file, source, languageVersion, data)
})
}
/**
* Get artefacts for last compiled contract
* * @returns last compiled contract compiler abstract
*/
getLastCompilationResult () {
return this.compilersArtefacts.__last
}
/**
* Get compilation output for contracts compiled during a session of Remix IDE
* @returns compilatin output
*/
getAllContractDatas () {
const contractsData = {}
Object.keys(this.compilersArtefactsPerFile).map((targetFile) => {
@ -72,6 +85,87 @@ export class CompilerArtefacts extends Plugin {
return contractsData
}
/**
* Get a particular contract output/artefacts from a compiler output of a Solidity file compilation
* @param compilerOutput compiler output
* @param contractName contract name
* @returns arefacts object, with fully qualified name (e.g; contracts/1_Storage.sol:Storage) as key
*/
_getAllContractArtefactsfromOutput (compilerOutput, contractName) {
const contractArtefacts = {}
for (const filename in compilerOutput) {
if(Object.keys(compilerOutput[filename]).includes(contractName)) contractArtefacts[filename + ':' + contractName] = compilerOutput[filename][contractName]
}
return contractArtefacts
}
/**
* Populate resultant object with a particular contract output/artefacts by processing all the artifacts stored in file explorer
* @param path path to start looking from
* @param contractName contract to be looked for
* @param contractArtefacts populated resultant artefacts object, with fully qualified name (e.g: contracts/1_Storage.sol:Storage) as key
* Once method execution completes, contractArtefacts object will hold all possible artefacts for contract
*/
async _populateAllContractArtefactsFromFE (path, contractName, contractArtefacts) {
const dirList = await this.call('fileManager', 'dirList', path)
if(dirList && dirList.length) {
for (const dirPath of dirList) {
// check if directory contains an 'artifacts' folder and a 'build-info' folder inside 'artifacts'
if(dirPath === path + '/artifacts' && await this.call('fileManager', 'exists', dirPath + '/build-info')) {
const buildFileList = await this.call('fileManager', 'fileList', dirPath + '/build-info')
// process each build-info file to populate the artefacts for contractName
for (const buildFile of buildFileList) {
let content = await this.call('fileManager', 'readFile', buildFile)
if (content) content = JSON.parse(content)
const compilerOutput = content.output.contracts
const artefacts = this._getAllContractArtefactsfromOutput(compilerOutput, contractName)
// populate the resultant object with artefacts
Object.assign(contractArtefacts, artefacts)
}
} else await this._populateAllContractArtefactsFromFE (dirPath, contractName, contractArtefacts)
}
} else return
}
/**
* Get artefacts for a contract (called by script-runner)
* @param name contract name or fully qualified name i.e. <filename>:<contractname> e.g: contracts/1_Storage.sol:Storage
* @returns artefacts for the contract
*/
async getArtefactsByContractName (name) {
const contractsDataByFilename = this.getAllContractDatas()
// check if name is a fully qualified name
if (name.includes(':')) {
const fullyQualifiedName = name
const nameArr = fullyQualifiedName.split(':')
const filename = nameArr[0]
const contract = nameArr[1]
if(Object.keys(contractsDataByFilename).includes(filename) && contractsDataByFilename[filename][contract])
return contractsDataByFilename[filename][contract]
else {
const allContractsData = {}
await this._populateAllContractArtefactsFromFE ('contracts', contract, allContractsData)
if(allContractsData[fullyQualifiedName]) return { fullyQualifiedName, artefact: allContractsData[fullyQualifiedName]}
else throw new Error(`Could not find artifacts for ${fullyQualifiedName}. Compile contract to generate artifacts.`)
}
} else {
const contractName = name
const contractArtefacts = this._getAllContractArtefactsfromOutput(contractsDataByFilename, contractName)
let keys = Object.keys(contractArtefacts)
if (!keys.length) {
await this._populateAllContractArtefactsFromFE ('contracts', contractName, contractArtefacts)
keys = Object.keys(contractArtefacts)
}
if (keys.length === 1) return { fullyQualifiedName: keys[0], artefact: contractArtefacts[keys[0]] }
else if (keys.length > 1) {
throw new Error(`There are multiple artifacts for contract "${contractName}", please use a fully qualified name.\n
Please replace ${contractName} for one of these options wherever you are trying to read its artifact: \n
${keys.join()}\n
OR just compile the required contract again`)
} else throw new Error(`Could not find artifacts for ${contractName}. Compile contract to generate artifacts.`)
}
}
getCompilerAbstract (file) {
return this.compilersArtefactsPerFile[file]
}

@ -3,6 +3,8 @@ import { Plugin } from '@remixproject/engine'
import { compile } from '@remix-project/remix-solidity'
import { util } from '@remix-project/remix-lib'
import { toChecksumAddress } from 'ethereumjs-util'
import { fetchContractFromEtherscan } from './helpers/fetch-etherscan'
import { fetchContractFromSourcify } from './helpers/fetch-sourcify'
const profile = {
name: 'fetchAndCompile',
@ -68,48 +70,31 @@ export class FetchAndCompile extends Plugin {
let data
try {
data = await this.call('sourcify', 'fetchByNetwork', contractAddress, network.id)
data = await fetchContractFromSourcify(this, network, contractAddress, targetPath)
} catch (e) {
setTimeout(_ => this.emit('notFound', contractAddress), 0) // plugin framework returns a time out error although it actually didn't find the source...
this.unresolvedAddresses.push(contractAddress)
return localCompilation()
}
if (!data || !data.metadata) {
setTimeout(_ => this.emit('notFound', contractAddress), 0)
this.unresolvedAddresses.push(contractAddress)
return localCompilation()
this.call('notification', 'toast', e.message)
console.log(e) // and fallback to getting the compilation result from etherscan
}
// set the solidity contract code using metadata
await this.call('fileManager', 'setFile', `${targetPath}/${network.id}/${contractAddress}/metadata.json`, JSON.stringify(data.metadata, null, '\t'))
const compilationTargets = {}
for (let file in data.metadata.sources) {
const urls = data.metadata.sources[file].urls
for (const url of urls) {
if (url.includes('ipfs')) {
const stdUrl = `ipfs://${url.split('/')[2]}`
const source = await this.call('contentImport', 'resolve', stdUrl)
if (await this.call('contentImport', 'isExternalUrl', file)) {
// nothing to do, the compiler callback will handle those
} else {
file = file.replace('browser/', '') // should be fixed in the remix IDE end.
const path = `${targetPath}/${network.id}/${contractAddress}/${file}`
await this.call('fileManager', 'setFile', path, source.content)
compilationTargets[path] = { content: source.content }
}
break
}
if (!data) {
this.call('notification', 'toast', `contract ${contractAddress} not found in Sourcify, checking in Etherscan..`)
try {
data = await fetchContractFromEtherscan(this, network, contractAddress, targetPath)
} catch (e) {
this.call('notification', 'toast', e.message)
setTimeout(_ => this.emit('notFound', contractAddress), 0) // plugin framework returns a time out error although it actually didn't find the source...
this.unresolvedAddresses.push(contractAddress)
return localCompilation()
}
}
// compile
const settings = {
version: data.metadata.compiler.version,
language: data.metadata.language,
evmVersion: data.metadata.settings.evmVersion,
optimize: data.metadata.settings.optimizer.enabled,
runs: data.metadata.settings.runs
if (!data) {
setTimeout(_ => this.emit('notFound', contractAddress), 0)
this.unresolvedAddresses.push(contractAddress)
return localCompilation()
}
const { settings, compilationTargets } = data
try {
setTimeout(_ => this.emit('compiling', settings), 0)
const compData = await compile(

@ -1,6 +1,7 @@
'use strict'
import { Plugin } from '@remixproject/engine'
import { CompilerAbstract } from '@remix-project/remix-solidity'
import { createHash } from 'crypto'
const profile = {
name: 'compilerMetadata',
@ -28,10 +29,11 @@ export class CompilerMetadata extends Plugin {
onActivation () {
const self = this
this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data) => {
this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data, input, version) => {
if (!await this.call('settings', 'get', 'settings/generate-contract-metadata')) return
const compiler = new CompilerAbstract(languageVersion, data, source)
const compiler = new CompilerAbstract(languageVersion, data, source, input)
const path = self._extractPathOf(source.target)
await this.setBuildInfo(version, input, data, path)
compiler.visitContracts((contract) => {
if (contract.file !== source.target) return
(async () => {
@ -43,6 +45,23 @@ export class CompilerMetadata extends Plugin {
})
}
async setBuildInfo (version, input, output, path) {
input = JSON.parse(input)
const solcLongVersion = version.replace('.Emscripten.clang', '')
const solcVersion = solcLongVersion.substring(0, solcLongVersion.indexOf('+commit'))
const format = 'hh-sol-build-info-1'
const json = JSON.stringify({
_format: format,
solcVersion,
solcLongVersion,
input
})
const id = createHash('md5').update(Buffer.from(json)).digest().toString('hex')
const buildFilename = this.joinPath(path, this.innerPath, 'build-info/' + id + '.json')
const buildData = {id, _format: format, solcVersion, solcLongVersion, input, output}
await this.call('fileManager', 'writeFile', buildFilename, JSON.stringify(buildData, null, '\t'))
}
_extractPathOf (file) {
const reg = /(.*)(\/).*/
const path = reg.exec(file)
@ -51,14 +70,15 @@ export class CompilerMetadata extends Plugin {
async _setArtefacts (content, contract, path) {
content = content || '{}'
const fileName = this._JSONFileName(path, contract.name)
const metadataFileName = this._MetadataFileName(path, contract.name)
let metadata
try {
metadata = JSON.parse(content)
} catch (e) {
console.log(e)
}
const fileName = this._JSONFileName(path, contract.name)
const metadataFileName = this._MetadataFileName(path, contract.name)
const deploy = metadata.deploy || {}
this.networks.forEach((network) => {

@ -42,7 +42,7 @@ export class EditorContextListener extends Plugin {
onActivation () {
this.on('editor', 'contentChanged', () => { this._stopHighlighting() })
this.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => {
this.on('solidity', 'compilationFinished', (file, source, languageVersion, data, input, version) => {
if (languageVersion.indexOf('soljson') !== 0) return
this._stopHighlighting()
this._index = {

@ -0,0 +1,59 @@
export const fetchContractFromEtherscan = async (plugin, network, contractAddress, targetPath) => {
let data
const compilationTargets = {}
const etherscanKey = await plugin.call('config', 'getAppParameter', 'etherscan-access-token')
if (etherscanKey) {
const endpoint = network.id == 1 ? 'api.etherscan.io' : 'api-' + network.name + '.etherscan.io'
data = await fetch('https://' + endpoint + '/api?module=contract&action=getsourcecode&address=' + contractAddress + '&apikey=' + etherscanKey)
data = await data.json()
// etherscan api doc https://docs.etherscan.io/api-endpoints/contracts
if (data.message === 'OK' && data.status === "1") {
if (data.result.length) {
if (data.result[0].SourceCode === '') throw new Error('contract not verified')
if (data.result[0].SourceCode.startsWith('{')) {
data.result[0].SourceCode = JSON.parse(data.result[0].SourceCode.replace(/(?:\r\n|\r|\n)/g, '').replace(/^{{/,'{').replace(/}}$/,'}'))
}
}
} else throw new Error('unable to retrieve contract data ' + data.message)
} else throw new Error('unable to try fetching the source code from etherscan: etherscan access token not found. please go to the Remix settings page and provide an access token.')
if (!data || !data.result) {
return null
}
if (typeof data.result[0].SourceCode === 'string') {
const fileName = `${targetPath}/${network.id}/${contractAddress}/${data.result[0].ContractName}.sol`
await plugin.call('fileManager', 'setFile', fileName , data.result[0].SourceCode)
compilationTargets[fileName] = { content: data.result[0].SourceCode }
} else if (data.result[0].SourceCode && typeof data.result[0].SourceCode == 'object') {
const sources = data.result[0].SourceCode.sources
for (let [file, source] of Object.entries(sources)) { // eslint-disable-line
file = file.replace('browser/', '') // should be fixed in the remix IDE end.
file = file.replace(/^\//g, '') // remove first slash.
if (await plugin.call('contentImport', 'isExternalUrl', file)) {
// nothing to do, the compiler callback will handle those
} else {
const path = `${targetPath}/${network.id}/${contractAddress}/${file}`
const content = (source as any).content
await plugin.call('fileManager', 'setFile', path, content)
compilationTargets[path] = { content }
}
}
}
let runs = 0
try {
runs = parseInt(data.result[0].Runs)
} catch (e) {}
const settings = {
version: data.result[0].CompilerVersion.replace(/^v/, ''),
language: 'Solidity',
evmVersion: data.result[0].EVMVersion.toLowerCase(),
optimize: data.result[0].OptimizationUsed === '1',
runs
}
return {
settings,
compilationTargets
}
}

@ -0,0 +1,46 @@
export const fetchContractFromSourcify = async (plugin, network, contractAddress, targetPath) => {
let data
const compilationTargets = {}
try {
data = await plugin.call('sourcify', 'fetchByNetwork', contractAddress, network.id)
} catch (e) {
console.log(e)
}
if (!data || !data.metadata) {
return null
}
// set the solidity contract code using metadata
await plugin.call('fileManager', 'setFile', `${targetPath}/${network.id}/${contractAddress}/metadata.json`, JSON.stringify(data.metadata, null, '\t'))
for (let file in data.metadata.sources) {
const urls = data.metadata.sources[file].urls
for (const url of urls) {
if (url.includes('ipfs')) {
const stdUrl = `ipfs://${url.split('/')[2]}`
const source = await plugin.call('contentImport', 'resolve', stdUrl)
file = file.replace('browser/', '') // should be fixed in the remix IDE end.
if (await plugin.call('contentImport', 'isExternalUrl', file)) {
// nothing to do, the compiler callback will handle those
} else {
const path = `${targetPath}/${network.id}/${contractAddress}/${file}`
await plugin.call('fileManager', 'setFile', path, source.content)
compilationTargets[path] = { content: source.content }
}
break
}
}
}
const settings = {
version: data.metadata.compiler.version,
language: data.metadata.language,
evmVersion: data.metadata.settings.evmVersion,
optimize: data.metadata.settings.optimizer.enabled,
runs: data.metadata.settings.optimizer.runs
}
return {
settings,
compilationTargets
}
}

@ -70,7 +70,7 @@ export class OffsetToLineColumnConverter extends Plugin {
* called by plugin API
*/
activate () {
this.on('solidity', 'compilationFinished', () => {
this.on('solidity', 'compilationFinished', (success, data, source, input, version) => {
this.clear()
})
}

@ -11,7 +11,6 @@ import { SolidityProxy, stateDecoder, localDecoder, InternalCallTree } from './s
/**
* Ethdebugger is a wrapper around a few classes that helps debugging a transaction
*
* - Web3Providers - define which environment (web3) the transaction will be retrieved from
* - TraceManager - Load / Analyze the trace and retrieve details of specific test
* - CodeManager - Retrieve loaded byte code and help to resolve AST item from vmtrace index
* - SolidityProxy - Basically used to extract state variable from AST

@ -68,7 +68,10 @@ export class Debugger {
try {
const address = this.debugger.traceManager.getCurrentCalledAddressAt(index)
const compilationResultForAddress = await this.compilationResult(address)
if (!compilationResultForAddress) return
if (!compilationResultForAddress) {
this.event.trigger('newSourceLocation', [null])
return
}
this.debugger.callTree.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, index, compilationResultForAddress.data.contracts).then(async (rawLocation) => {
if (compilationResultForAddress && compilationResultForAddress.data) {
@ -91,6 +94,7 @@ export class Debugger {
})
// })
} catch (error) {
this.event.trigger('newSourceLocation', [null])
return console.log(error)
}
}

@ -51,15 +51,18 @@ export class DebuggerStepManager {
this.traceManager.buildCallPath(index).then((callsPath) => {
this.currentCall = callsPath[callsPath.length - 1]
if (this.currentCall.reverted) {
const revertedReason = this.currentCall.outofgas ? 'outofgas' : ''
const revertedReason = this.currentCall.outofgas ? 'outofgas' : 'reverted'
this.revertionPoint = this.currentCall.return
return this.event.trigger('revertWarning', [revertedReason])
this.event.trigger('revertWarning', [revertedReason])
return
}
for (let k = callsPath.length - 2; k >= 0; k--) {
const parent = callsPath[k]
if (!parent.reverted) continue
this.revertionPoint = parent.return
this.event.trigger('revertWarning', ['parenthasthrown'])
if (parent.reverted) {
this.revertionPoint = parent.return
this.event.trigger('revertWarning', ['parenthasthrown'])
return
}
}
this.event.trigger('revertWarning', [''])
}).catch((error) => {
@ -118,6 +121,7 @@ export class DebuggerStepManager {
if (solidityMode) {
step = this.resolveToReducedTrace(step, -1)
}
if (this.currentStepIndex === step) return
this.currentStepIndex = step
this.triggerStepChanged(step)
}
@ -133,6 +137,7 @@ export class DebuggerStepManager {
if (solidityMode) {
step = this.resolveToReducedTrace(step, 1)
}
if (this.currentStepIndex === step) return
this.currentStepIndex = step
this.triggerStepChanged(step)
}
@ -143,12 +148,14 @@ export class DebuggerStepManager {
if (solidityMode) {
step = this.resolveToReducedTrace(step, 0)
}
if (this.currentStepIndex === step) return
this.currentStepIndex = step
this.triggerStepChanged(step)
}
jumpTo (step) {
if (!this.traceManager.inRange(step)) return
if (this.currentStepIndex === step) return
this.currentStepIndex = step
this.triggerStepChanged(step)
}

@ -11,6 +11,7 @@ import { Struct as StructType } from './types/Struct'
import { Int as IntType } from './types/Int'
import { Uint as UintType } from './types/Uint'
import { Mapping as MappingType } from './types/Mapping'
import { FunctionType } from './types/FunctionType'
import { extractLocation, removeLocation } from './types/util'
/**
@ -78,6 +79,10 @@ function bool (type) {
return new BoolType()
}
function functionType (type, stateDefinitions, contractName, location) {
return new FunctionType(type, stateDefinitions, contractName, location)
}
/**
* DynamicByteArray decode the given @arg type
*
@ -300,7 +305,8 @@ function parseType (type, stateDefinitions, contractName, location) {
struct: struct,
int: int,
uint: uint,
mapping: mapping
mapping: mapping,
function: functionType
}
const currentType = typeClass(type)
if (currentType === null) {

@ -0,0 +1,12 @@
'use strict'
import { ValueType } from './ValueType'
export class FunctionType extends ValueType {
constructor (type, stateDefinitions, contractName, location) {
super(1, 8, 'function')
}
decodeValue (value) {
return 'at program counter ' + value
}
}

@ -8,7 +8,7 @@ export class OffsetToColumnConverter {
constructor (compilerEvent) {
this.lineBreakPositionsByContent = {}
if (compilerEvent) {
compilerEvent.register('compilationFinished', (success, data, source) => {
compilerEvent.register('compilationFinished', (success, data, source, input, version) => {
this.clear()
})
}

@ -133,7 +133,7 @@ export class StorageResolver {
resolve([{}, null])
} else {
this.web3.debug.storageRangeAt(
tx.blockHash, tx.hash,
tx.blockHash, tx.transactionIndex,
address,
start,
maxSize,

@ -23,11 +23,6 @@
ui: uiHelper,
compiler: compilerHelper
},
vm: {
Web3Providers: Web3Providers,
DummyProvider: DummyProvider,
Web3VMProvider: Web3VmProvider
},
Storage: Storage,
util: util,
execution: {

@ -56,18 +56,19 @@ export class EventsDecoder {
return eventsABI
}
_event (hash, eventsABI) {
for (const k in eventsABI) {
if (eventsABI[k][hash]) {
const event = eventsABI[k][hash]
for (const input of event.inputs) {
if (input.type === 'function') {
input.type = 'bytes24'
input.baseType = 'bytes24'
}
_event (hash: string, eventsABI: Record<string, unknown>, contractName: string) {
const events = eventsABI[contractName]
if (!events) return null
if (events[hash]) {
const event = events[hash]
for (const input of event.inputs) {
if (input.type === 'function') {
input.type = 'bytes24'
input.baseType = 'bytes24'
}
return event
}
return event
}
return null
}
@ -94,7 +95,7 @@ export class EventsDecoder {
// [address, topics, mem]
const log = logs[i]
const topicId = log.topics[0]
const eventAbi = this._event(topicId.replace('0x', ''), eventsABI)
const eventAbi = this._event(topicId.replace('0x', ''), eventsABI, contractName)
if (eventAbi) {
const decodedlog = eventAbi.abi.parseLog(log)
const decoded = {}

@ -2,9 +2,6 @@ import { EventManager } from './eventManager'
import * as uiHelper from './helpers/uiHelper'
import * as compilerHelper from './helpers/compilerHelper'
import * as util from './util'
import { Web3Providers } from './web3Provider/web3Providers'
import { DummyProvider } from './web3Provider/dummyProvider'
import { Web3VmProvider } from './web3Provider/web3VmProvider'
import { Storage } from './storage'
import { EventsDecoder } from './execution/eventsDecoder'
import * as txExecution from './execution/txExecution'
@ -18,6 +15,7 @@ import * as typeConversion from './execution/typeConversion'
import { TxRunnerVM } from './execution/txRunnerVM'
import { TxRunnerWeb3 } from './execution/txRunnerWeb3'
import * as txResultHelper from './helpers/txResultHelper'
export { ConsoleLogs } from './helpers/hhconsoleSigs'
export { ICompilerApi, ConfigurationSettings } from './types/ICompilerApi'
export { QueryParams } from './query-params'
@ -26,11 +24,6 @@ const helpers = {
compiler: compilerHelper,
txResultHelper
}
const vm = {
Web3Providers: Web3Providers,
DummyProvider: DummyProvider,
Web3VMProvider: Web3VmProvider
}
const execution = {
EventsDecoder: EventsDecoder,
txExecution: txExecution,
@ -44,4 +37,4 @@ const execution = {
LogsManager,
forkAt
}
export { EventManager, helpers, vm, Storage, util, execution }
export { EventManager, helpers, Storage, util, execution }

@ -1,11 +1,14 @@
export interface ICompilerApi {
currentFile: string
contractMap: {
file: string
} | Record<string, any>
compilationDetails: {
contractMap: {
file: string
} | Record<string, any>,
contractsDetails: Record<string, any>,
target?: string
}
compileErrors: any
compileTabLogic: any
contractsDetails: Record<string, any>
configurationSettings: ConfigurationSettings
getCompilerParameters: () => ConfigurationSettings
@ -20,12 +23,13 @@ export interface ICompilerApi {
getCompilationResult: () => any
onCurrentFileChanged: (fileName: string) => void
onResetResults: () => void,
// onResetResults: () => void,
onSetWorkspace: (workspace: any) => void
onNoFileSelected: () => void
onCompilationFinished: (contractsDetails: any, contractMap: any) => void
onSessionSwitched: () => void
onContentChanged: () => void
onFileClosed: (name: string) => void
resolveContentAndSave: (url: string) => Promise<string>
fileExists: (file: string) => Promise<boolean>
@ -37,6 +41,8 @@ export interface ICompilerApi {
logToTerminal: (log: terminalLog) => void
compileWithHardhat: (configPath: string) => Promise<string>
statusChanged: (data: { key: string, title?: string, type?: string }) => void,
emit?: (key: string, ...payload: any) => void
}
export type terminalLog = {

@ -1,55 +0,0 @@
export class DummyProvider {
eth
debug
providers
currentProvider
constructor () {
this.eth = {}
this.debug = {}
this.eth.getCode = (address, cb) => { return this.getCode(address, cb) }
this.eth.getTransaction = (hash, cb) => { return this.getTransaction(hash, cb) }
this.eth.getTransactionFromBlock = (blockNumber, txIndex, cb) => { return this.getTransactionFromBlock(blockNumber, txIndex, cb) }
this.eth.getBlockNumber = (cb) => { return this.getBlockNumber(cb) }
this.debug.traceTransaction = (hash, options, cb) => { return this.traceTransaction(hash, options, cb) }
this.debug.storageRangeAt = (blockNumber, txIndex, address, start, end, maxLength, cb) => { return this.storageRangeAt(blockNumber, txIndex, address, start, end, maxLength, cb) }
this.providers = { HttpProvider: function (url) {} }
this.currentProvider = { host: '' }
}
getCode (address, cb) {
cb(null, '')
}
setProvider (provider) {}
traceTransaction (txHash, options, cb) {
if (cb) {
cb(null, {})
}
return {}
}
storageRangeAt (blockNumber, txIndex, address, start, end, maxLength, cb) {
if (cb) {
cb(null, {})
}
return {}
}
getBlockNumber (cb) { cb(null, '') }
getTransaction (txHash, cb) {
if (cb) {
cb(null, {})
}
return {}
}
getTransactionFromBlock (blockNumber, txIndex, cb) {
if (cb) {
cb(null, {})
}
return {}
}
}

@ -1,38 +0,0 @@
import { Web3VmProvider } from './web3VmProvider'
import { loadWeb3, extendWeb3 } from '../init'
export class Web3Providers {
modes
constructor () {
this.modes = {}
}
addProvider (type, obj) {
if (type === 'INTERNAL') {
const web3 = loadWeb3()
this.addWeb3(type, web3)
} else if (type === 'vm') {
this.addVM(type, obj)
} else {
extendWeb3(obj)
this.addWeb3(type, obj)
}
}
get (type, cb) {
if (this.modes[type]) {
return cb(null, this.modes[type])
}
cb('error: this provider has not been setup (' + type + ')', null)
}
addWeb3 (type, web3) {
this.modes[type] = web3
}
addVM (type, vm) {
const vmProvider = new Web3VmProvider()
vmProvider.setVM(vm)
this.modes[type] = vmProvider
}
}

@ -1,12 +1,16 @@
import { hexListFromBNs, formatMemory } from '../util'
import { normalizeHexAddress } from '../helpers/uiHelper'
import { ConsoleLogs } from '../helpers/hhconsoleSigs'
import { util } from '@remix-project/remix-lib'
const { hexListFromBNs, formatMemory } = util
import { helpers } from '@remix-project/remix-lib'
const { normalizeHexAddress } = helpers.ui
import { ConsoleLogs } from '@remix-project/remix-lib'
import { toChecksumAddress, BN, bufferToHex, Address } from 'ethereumjs-util'
import Web3 from 'web3'
import { ethers } from 'ethers'
import { VMContext } from './vm-context'
export class Web3VmProvider {
web3
export class VmProxy {
vmContext: VMContext
web3: Web3
vm
vmTraces
txs
@ -38,7 +42,8 @@ export class Web3VmProvider {
blocks
latestBlockNumber
constructor () {
constructor (vmContext: VMContext) {
this.vmContext = vmContext
this.web3 = new Web3()
this.vm = null
this.vmTraces = {}
@ -66,15 +71,15 @@ export class Web3VmProvider {
this.lastProcessedStorageTxHash = {}
this.sha3Preimages = {}
// util
this.sha3 = (...args) => this.web3.utils.sha3(...args)
this.toHex = (...args) => this.web3.utils.toHex(...args)
this.toAscii = (...args) => this.web3.utils.hexToAscii(...args)
this.fromAscii = (...args) => this.web3.utils.asciiToHex(...args)
this.fromDecimal = (...args) => this.web3.utils.numberToHex(...args)
this.fromWei = (...args) => this.web3.utils.fromWei(...args)
this.toWei = (...args) => this.web3.utils.toWei(...args)
this.toBigNumber = (...args) => this.web3.utils.toBN(...args)
this.isAddress = (...args) => this.web3.utils.isAddress(...args)
this.sha3 = (...args) => this.web3.utils.sha3.apply(this, args)
this.toHex = (...args) => this.web3.utils.toHex.apply(this, args)
this.toAscii = (...args) => this.web3.utils.toAscii.apply(this, args)
this.fromAscii = (...args) => this.web3.utils.fromAscii.apply(this, args)
this.fromDecimal = (...args) => this.web3.utils.fromDecimal.apply(this, args)
this.fromWei = (...args) => this.web3.utils.fromWei.apply(this, args)
this.toWei = (...args) => this.web3.utils.toWei.apply(this, args)
this.toBigNumber = (...args) => this.web3.utils.toBN.apply(this, args)
this.isAddress = (...args) => this.web3.utils.isAddress.apply(this, args)
this.utils = Web3.utils || []
this.txsMapBlock = {}
this.blocks = {}
@ -289,16 +294,22 @@ export class Web3VmProvider {
}
}
storageRangeAt (blockNumber, txIndex, address, start, maxLength, cb) { // txIndex is the hash in the case of the VM
storageRangeAt (blockNumber, txIndex, address, start, maxLength, cb) {
// we don't use the range params here
address = toChecksumAddress(address)
let txHash
if (txIndex === 'latest') {
txIndex = this.lastProcessedStorageTxHash[address]
txHash = this.lastProcessedStorageTxHash[address]
} else {
const block = this.vmContext.blocks[blockNumber]
txHash = '0x' + block.transactions[txIndex].hash().toString('hex')
}
if (this.storageCache[txIndex] && this.storageCache[txIndex][address]) {
const storage = this.storageCache[txIndex][address]
if (this.storageCache[txHash] && this.storageCache[txHash][address]) {
const storage = this.storageCache[txHash][address]
return cb(null, {
storage: JSON.parse(JSON.stringify(storage)),
nextKey: null

@ -2,11 +2,15 @@
'use strict'
import Web3 from 'web3'
import { rlp, keccak, bufferToHex } from 'ethereumjs-util'
import { vm as remixLibVm, execution } from '@remix-project/remix-lib'
import { execution } from '@remix-project/remix-lib'
const { LogsManager } = execution
import { VmProxy } from './VmProxy'
import VM from '@ethereumjs/vm'
import Common from '@ethereumjs/common'
import StateManager from '@ethereumjs/vm/dist/state/stateManager'
import { StorageDump } from '@ethereumjs/vm/dist/state/interface'
import { Block } from '@ethereumjs/block'
import { Transaction } from '@ethereumjs/tx'
/*
extend vm state manager and instanciate VM
@ -75,6 +79,13 @@ class StateManagerCommonStorageDump extends StateManager {
}
}
export type CurrentVm = {
vm: VM,
web3vm: VmProxy,
stateManager: StateManagerCommonStorageDump,
common: Common
}
/*
trigger contextChanged, web3EndpointChanged
*/
@ -82,15 +93,14 @@ export class VMContext {
currentFork: string
blockGasLimitDefault: number
blockGasLimit: number
customNetWorks
blocks
latestBlockNumber
blockByTxHash
txByHash
currentVm
web3vm
logsManager
exeResults
blocks: Record<string, Block>
latestBlockNumber: string
blockByTxHash: Record<string, Block>
txByHash: Record<string, Transaction>
currentVm: CurrentVm
web3vm: VmProxy
logsManager: any // LogsManager
exeResults: Record<string, Transaction>
constructor (fork?) {
this.blockGasLimitDefault = 4300000
@ -98,11 +108,11 @@ export class VMContext {
this.currentFork = fork || 'london'
this.currentVm = this.createVm(this.currentFork)
this.blocks = {}
this.latestBlockNumber = 0
this.latestBlockNumber = "0x0"
this.blockByTxHash = {}
this.txByHash = {}
this.exeResults = {}
this.logsManager = new execution.LogsManager()
this.logsManager = new LogsManager()
}
createVm (hardfork) {
@ -115,7 +125,9 @@ export class VMContext {
allowUnlimitedContractSize: true
})
const web3vm = new remixLibVm.Web3VMProvider()
// VmProxy and VMContext are very intricated.
// VmProxy is used to track the EVM execution (to listen on opcode execution, in order for instance to generate the VM trace)
const web3vm = new VmProxy(this)
web3vm.setVM(vm)
return { vm, web3vm, stateManager, common }
}
@ -140,7 +152,7 @@ export class VMContext {
return this.currentVm
}
addBlock (block) {
addBlock (block: Block) {
let blockNumber = '0x' + block.header.number.toString('hex')
if (blockNumber === '0x') {
blockNumber = '0x0'

@ -5,10 +5,12 @@ export class CompilerAbstract {
languageversion: any
data: any
source: any
constructor (languageversion, data, source) {
input: any
constructor (languageversion, data, source, input?) {
this.languageversion = languageversion
this.data = data
this.source = source // source code
this.input = input // source code
}
getContracts () {
@ -27,6 +29,10 @@ export class CompilerAbstract {
return this.data
}
getInput () {
return this.input
}
getAsts () {
return this.data.sources // ast
}

@ -12,8 +12,8 @@ export const compile = async (compilationTargets, settings, contentResolverCallb
compiler.set('language', settings.language)
compiler.set('runs', settings.runs)
compiler.loadVersion(canUseWorker(settings.version), urlFromVersion(settings.version))
compiler.event.register('compilationFinished', (success, compilationData, source) => {
resolve(new CompilerAbstract(settings.version, compilationData, source))
compiler.event.register('compilationFinished', (success, compilationData, source, input, version) => {
resolve(new CompilerAbstract(settings.version, compilationData, source, input))
})
compiler.event.register('compilerLoaded', _ => compiler.compile(compilationTargets, ''))
})

@ -19,9 +19,13 @@ export default (sources: Source, opts: CompilerInputOptions): string => {
}
}
}
}
}
if (opts.evmVersion) {
o.settings.evmVersion = opts.evmVersion
if (opts.evmVersion.toLowerCase() == 'default') {
opts.evmVersion = null
} else {
o.settings.evmVersion = opts.evmVersion
}
}
if (opts.language) {
o.language = opts.language

@ -45,6 +45,7 @@ export default function (self) { // eslint-disable-line @typescript-eslint/expli
cmd: 'compiled',
job: data.job,
data: compileJSON(data.input),
input: data.input,
missingInputs: missingInputs
})
}

@ -37,7 +37,7 @@ export class Compiler {
}
}
this.event.register('compilationFinished', (success: boolean, data: CompilationResult, source: SourceWithTarget) => {
this.event.register('compilationFinished', (success: boolean, data: CompilationResult, source: SourceWithTarget, input: string, version: string) => {
if (success && this.state.compilationStartTime) {
this.event.trigger('compilationDuration', [(new Date().getTime()) - this.state.compilationStartTime])
}
@ -70,7 +70,7 @@ export class Compiler {
this.gatherImports(files, missingInputs, (error, input) => {
if (error) {
this.state.lastCompilationResult = null
this.event.trigger('compilationFinished', [false, { error: { formattedMessage: error, severity: 'error' } }, files])
this.event.trigger('compilationFinished', [false, { error: { formattedMessage: error, severity: 'error' } }, files, input, this.state.currentVersion])
} else if (this.state.compileJSON && input) { this.state.compileJSON(input) }
})
}
@ -111,16 +111,17 @@ export class Compiler {
return { error: 'Deferred import' }
}
let result: CompilationResult = {}
let input
try {
if (source && source.sources) {
const { optimize, runs, evmVersion, language } = this.state
const input = compilerInput(source.sources, { optimize, runs, evmVersion, language })
input = compilerInput(source.sources, { optimize, runs, evmVersion, language })
result = JSON.parse(compiler.compile(input, { import: missingInputsCallback }))
}
} catch (exception) {
result = { error: { formattedMessage: 'Uncaught JavaScript exception:\n' + exception, severity: 'error', mode: 'panic' } }
}
this.onCompilationFinished(result, missingInputs, source)
this.onCompilationFinished(result, missingInputs, source, input, this.state.currentVersion)
}
this.onCompilerLoaded(compiler.version())
}
@ -133,7 +134,7 @@ export class Compiler {
* @param source Source
*/
onCompilationFinished (data: CompilationResult, missingInputs?: string[], source?: SourceWithTarget): void {
onCompilationFinished (data: CompilationResult, missingInputs?: string[], source?: SourceWithTarget, input?: string, version?: string): void {
let noFatalErrors = true // ie warnings are ok
const checkIfFatalError = (error: CompilationError) => {
@ -146,7 +147,7 @@ export class Compiler {
if (!noFatalErrors) {
// There are fatal errors, abort here
this.state.lastCompilationResult = null
this.event.trigger('compilationFinished', [false, data, source])
this.event.trigger('compilationFinished', [false, data, source, input, version])
} else if (missingInputs !== undefined && missingInputs.length > 0 && source && source.sources) {
// try compiling again with the new set of inputs
this.internalCompile(source.sources, missingInputs)
@ -159,7 +160,7 @@ export class Compiler {
source: source
}
}
this.event.trigger('compilationFinished', [true, data, source])
this.event.trigger('compilationFinished', [true, data, source, input, version])
}
}
@ -182,16 +183,17 @@ export class Compiler {
return { error: 'Deferred import' }
}
let result: CompilationResult = {}
let input: string
try {
if (source && source.sources) {
const { optimize, runs, evmVersion, language } = this.state
const input = compilerInput(source.sources, { optimize, runs, evmVersion, language })
input = compilerInput(source.sources, { optimize, runs, evmVersion, language })
result = JSON.parse(remoteCompiler.compile(input, { import: missingInputsCallback }))
}
} catch (exception) {
result = { error: { formattedMessage: 'Uncaught JavaScript exception:\n' + exception, severity: 'error', mode: 'panic' } }
}
this.onCompilationFinished(result, missingInputs, source)
this.onCompilationFinished(result, missingInputs, source, input, version)
}
this.onCompilerLoaded(version)
}
@ -273,7 +275,7 @@ export class Compiler {
sources = jobs[data.job].sources
delete jobs[data.job]
}
this.onCompilationFinished(result, data.missingInputs, sources)
this.onCompilationFinished(result, data.missingInputs, sources, data.input, this.state.currentVersion)
}
break
}

@ -186,6 +186,7 @@ export interface MessageFromWorker {
cmd: string,
job?: number,
missingInputs?: string[],
input?: any,
data?: string
}

@ -142,7 +142,7 @@ export function compileFileOrFiles (filename: string, isDirectory: boolean, opts
},
function doCompilation (next) {
// @ts-ignore
compiler.event.register('compilationFinished', this, (success, data, source) => {
compiler.event.register('compilationFinished', this, (success, data, source, input, version) => {
next(null, data)
})
compiler.compile(sources, filepath)
@ -201,10 +201,10 @@ export function compileContractSources (sources: SrcIfc, newCompConfig: any, imp
}
},
(next) => {
const compilationFinishedCb = (success, data, source) => {
const compilationFinishedCb = (success, data, source, input, version) => {
// data.error usually exists for exceptions like worker error etc.
if (!data.error) UTRunner.compiler = compiler
if (opts && opts.event) opts.event.emit('compilationFinished', success, data, source)
if (opts && opts.event) opts.event.emit('compilationFinished', success, data, source, input, version)
next(null, data)
}
compiler.event.unregister('compilationFinished', compilationFinishedCb)

@ -1,6 +1,6 @@
export { default as RemixApp } from './lib/remix-app/remix-app'
export { dispatchModalContext, AppContext } from './lib/remix-app/context/context'
export { ModalProvider } from './lib/remix-app/context/provider'
export { ModalProvider, useDialogDispatchers } from './lib/remix-app/context/provider'
export { AppModal } from './lib/remix-app/interface/index'
export { AlertModal } from './lib/remix-app/interface/index'
export { ModalTypes } from './lib/remix-app/types/index'

@ -10,6 +10,7 @@ interface ModalWrapperProps extends ModalDialogProps {
const ModalWrapper = (props: ModalWrapperProps) => {
const [state, setState] = useState<ModalDialogProps>()
const ref = useRef()
const data = useRef()
const onFinishPrompt = async () => {
if (ref.current === undefined) {
@ -21,7 +22,7 @@ const ModalWrapper = (props: ModalWrapperProps) => {
}
const onOkFn = async () => {
(props.okFn) ? props.okFn() : props.resolve(true)
(props.okFn) ? props.okFn(data.current) : props.resolve(data.current || true)
}
const onCancelFn = async () => {
@ -37,6 +38,7 @@ const ModalWrapper = (props: ModalWrapperProps) => {
}
useEffect(() => {
data.current = props.data
if (props.modalType) {
switch (props.modalType) {
case ModalTypes.prompt:

@ -15,18 +15,18 @@ export const ModalProvider = ({ children = [], reducer = modalReducer, initialSt
})
}
const modal = (data: AppModal) => {
const { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType, defaultValue, hideFn } = data
const modal = (modalData: AppModal) => {
const { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType, defaultValue, hideFn, data } = modalData
return new Promise((resolve, reject) => {
dispatch({
type: modalActionTypes.setModal,
payload: { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType: modalType || ModalTypes.default, defaultValue: defaultValue, hideFn, resolve, next: onNextFn }
payload: { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType: modalType || ModalTypes.default, defaultValue: defaultValue, hideFn, resolve, next: onNextFn, data }
})
})
}
const alert = (data: AlertModal) => {
return modal({ id: data.id, title: data.title || 'Alert', message: data.message || data.title, okLabel: 'OK', okFn: (value?:any) => {}, cancelLabel: '', cancelFn: () => {} })
const alert = (modalData: AlertModal) => {
return modal({ id: modalData.id, title: modalData.title || 'Alert', message: modalData.message || modalData.title, okLabel: 'OK', okFn: (value?:any) => {}, cancelLabel: '', cancelFn: () => {} })
}
const handleHideModal = () => {

@ -15,7 +15,8 @@ export interface AppModal {
defaultValue?: string
hideFn?: () => void,
resolve?: (value?:any) => void,
next?: () => void
next?: () => void,
data?: any
}
export interface AlertModal {

@ -20,7 +20,8 @@ export const modalReducer = (state: ModalState = ModalInitialState, action: Moda
defaultValue: action.payload.defaultValue,
hideFn: action.payload.hideFn,
resolve: action.payload.resolve,
next: action.payload.next
next: action.payload.next,
data: action.payload.data
}
const modalList: AppModal[] = state.modals.slice()

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react'
import React, { useContext, useEffect, useRef, useState } from 'react'
import './style/remix-app.css'
import { RemixUIMainPanel } from '@remix-ui/panel'
import MatomoDialog from './components/modals/matomo'
@ -8,6 +8,7 @@ import { AppProvider } from './context/provider'
import AppDialogs from './components/modals/dialogs'
import DialogViewPlugin from './components/modals/dialogViewPlugin'
import { AppContext } from './context/context'
import { RemixUiVerticalIconsPanel } from '@remix-ui/vertical-icons-panel'
interface IRemixAppUi {
app: any
@ -17,31 +18,8 @@ const RemixApp = (props: IRemixAppUi) => {
const [appReady, setAppReady] = useState<boolean>(false)
const [hideSidePanel, setHideSidePanel] = useState<boolean>(false)
const sidePanelRef = useRef(null)
const mainPanelRef = useRef(null)
const iconPanelRef = useRef(null)
const hiddenPanelRef = useRef(null)
useEffect(() => {
if (sidePanelRef.current) {
if (props.app.sidePanel) {
sidePanelRef.current.appendChild(props.app.sidePanel.render())
}
}
if (mainPanelRef.current) {
if (props.app.mainview) {
mainPanelRef.current.appendChild(props.app.mainview.render())
}
}
if (iconPanelRef.current) {
if (props.app.menuicons) {
iconPanelRef.current.appendChild(props.app.menuicons.render())
}
}
if (hiddenPanelRef.current) {
if (props.app.hiddenPanel) {
hiddenPanelRef.current.appendChild(props.app.hiddenPanel.render())
}
}
async function activateApp () {
props.app.themeModule.initTheme(() => {
setAppReady(true)
@ -72,13 +50,6 @@ const RemixApp = (props: IRemixAppUi) => {
})
}
const components = {
iconPanel: <div ref={iconPanelRef} id="icon-panel" data-id="remixIdeIconPanel" className="iconpanel bg-light"></div>,
sidePanel: <div ref={sidePanelRef} id="side-panel" data-id="remixIdeSidePanel" className={`sidepanel border-right border-left ${hideSidePanel ? 'd-none' : ''}`}></div>,
mainPanel: <div ref={mainPanelRef} id="main-panel" data-id="remixIdeMainPanel" className='mainpanel'></div>,
hiddenPanel: <div ref={hiddenPanelRef}></div>
}
const value = {
settings: props.app.settings,
showMatamo: props.app.showMatamo,
@ -93,14 +64,14 @@ const RemixApp = (props: IRemixAppUi) => {
<MatomoDialog hide={!appReady}></MatomoDialog>
<div className={`remixIDE ${appReady ? '' : 'd-none'}`} data-id="remixIDE">
{components.iconPanel}
{components.sidePanel}
<div id="icon-panel" data-id="remixIdeIconPanel" className="iconpanel bg-light">{props.app.menuicons.render()}</div>
<div ref={sidePanelRef} id="side-panel" data-id="remixIdeSidePanel" className={`sidepanel border-right border-left ${hideSidePanel ? 'd-none' : ''}`}>{props.app.sidePanel.render()}</div>
<DragBar minWidth={250} refObject={sidePanelRef} hidden={hideSidePanel} setHideStatus={setHideSidePanel}></DragBar>
<div id="main-panel" data-id="remixIdeMainPanel" className='mainpanel'>
<RemixUIMainPanel Context={AppContext}></RemixUIMainPanel>
</div>
</div>
{components.hiddenPanel}
<div>{props.app.hiddenPanel.render()}</div>
<AppDialogs></AppDialogs>
<DialogViewPlugin></DialogViewPlugin>
</AppProvider>

@ -19,4 +19,8 @@
.navigator {
}
.navigator:hover {
}
.cursorPointerRemixDebugger {
cursor: pointer;
}

@ -65,11 +65,10 @@ export const ButtonNavigation = ({ stepOverBack, stepIntoBack, stepIntoForward,
<button className='btn btn-primary btn-sm navigator jumpButton fas fa-step-forward' id='jumpnextbreakpoint' data-id="buttonNavigatorJumpNextBreakpoint" title='Jump to the next breakpoint' onClick={() => { jumpNextBreakpoint && jumpNextBreakpoint() }} disabled={state.jumpNextBreakpointDisabled}></button>
</div>
<div id='reverted' style={{ display: revertedReason === '' ? 'none' : 'block' }}>
<button id='jumptoexception' title='Jump to exception' className='btn btn-danger btn-sm navigator button fas fa-exclamation-triangle' onClick={() => { jumpToException && jumpToException() }} disabled={state.jumpOutDisabled}>
</button>
<span>State changes made during this call will be reverted.</span>
<span id='outofgas' style={{ display: revertedReason === 'outofgas' ? 'inline' : 'none' }}>This call will run out of gas.</span>
<span id='parenthasthrown' style={{ display: revertedReason === 'parenthasthrown' ? 'inline' : 'none' }}>The parent call will throw an exception</span>
<span className='text-warning'>This call has reverted, state changes made during the call will be reverted.</span>
<span className='text-warning' id='outofgas' style={{ display: revertedReason === 'outofgas' ? 'inline' : 'none' }}>This call will run out of gas.</span>
<span className='text-warning' id='parenthasthrown' style={{ display: revertedReason === 'parenthasthrown' ? 'inline' : 'none' }}>The parent call will throw an exception</span>
<div className='text-warning'>Click <u data-id="debugGoToRevert" className="cursorPointerRemixDebugger" role="button" onClick={() => { jumpToException && jumpToException() }}>here</u> to jump where the call reverted.</div>
</div>
</div>
)

@ -32,7 +32,8 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
toastMessage: '',
validationError: '',
txNumberIsEmpty: true,
isLocalNodeUsed: false
isLocalNodeUsed: false,
sourceLocationStatus: ''
})
useEffect(() => {
@ -87,7 +88,13 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
})
debuggerInstance.event.register('newSourceLocation', async (lineColumnPos, rawLocation, generatedSources, address) => {
if (!lineColumnPos) return
if (!lineColumnPos) {
await debuggerModule.discardHighlight()
setState(prevState => {
return { ...prevState, sourceLocationStatus: 'Source location not available, neither in Sourcify nor in Etherscan. Please make sure the Etherscan api key is provided in the settings.' }
})
return
}
const contracts = await debuggerModule.fetchContractAndCompile(
address || currentReceipt.contractAddress || currentReceipt.to,
currentReceipt)
@ -113,6 +120,9 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
}
}
if (path) {
setState(prevState => {
return { ...prevState, sourceLocationStatus: '' }
})
await debuggerModule.discardHighlight()
await debuggerModule.highlight(lineColumnPos, path)
}
@ -138,6 +148,12 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
const unloadRequested = (blockNumber, txIndex, tx) => {
unLoad()
setState(prevState => {
return {
...prevState,
sourceLocationStatus: ''
}
})
}
const unLoad = () => {
@ -168,7 +184,8 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
setState(prevState => {
return {
...prevState,
txNumber: txNumber
txNumber: txNumber,
sourceLocationStatus: ''
}
})
if (!isValidHash(txNumber)) {
@ -266,7 +283,8 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
return {
...prevState,
validationError: '',
txNumber: txHash
txNumber: txHash,
sourceLocationStatus: ''
}
})
startDebugging(null, txHash, null, web3)
@ -315,6 +333,16 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
{ state.validationError && <span className="w-100 py-1 text-danger validationError">{state.validationError}</span> }
</div>
<TxBrowser requestDebug={ requestDebug } unloadRequested={ unloadRequested } updateTxNumberFlag={ updateTxNumberFlag } transactionNumber={ state.txNumber } debugging={ state.debugging } />
{ state.debugging && state.sourceLocationStatus && <div className="text-warning"><i className="fas fa-exclamation-triangle" aria-hidden="true"></i> {state.sourceLocationStatus}</div> }
{ !state.debugging &&
<div>
<i className="fas fa-info-triangle" aria-hidden="true"></i>
<span>
When Debugging with a transaction hash,
if the contract is verified, Remix will try to fetch the source code from Sourcify or Etherscan. Put in your Etherscan API key in the Remix settings.
For supported networks, please see: <a href="https://sourcify.dev" target="__blank" >https://sourcify.dev</a> & <a href="https://sourcify.dev" target="__blank">https://etherscan.io/contractsVerified</a>
</span>
</div> }
{ state.debugging && <StepManager stepManager={ stepManager } /> }
{ state.debugging && <VmDebuggerHead vmDebugger={ vmDebugger } /> }
</div>

@ -1,38 +1,42 @@
import React, { useState, useEffect } from 'react' // eslint-disable-line
import React, { useState, useEffect, useRef } from 'react' // eslint-disable-line
export const Slider = ({ jumpTo, sliderValue, traceLength }) => {
const [state, setState] = useState({
currentValue: 0
})
const onChangeId = useRef(null)
const slider = useRef(null)
useEffect(() => {
setValue(sliderValue)
}, [sliderValue])
const setValue = (value) => {
if (value === state.currentValue) return
setState(prevState => {
return { ...prevState, currentValue: value }
})
jumpTo && jumpTo(value)
if (value === slider.current.value) return
slider.current.value = value
if (onChangeId.current) {
clearTimeout(onChangeId.current)
}
((value) => {
onChangeId.current = setTimeout(() => {
jumpTo && jumpTo(value)
}, 100)
})(value)
}
const handleChange = (e) => {
const value = parseInt(e.target.value)
setValue(value)
setValue(parseInt(e.target.value))
}
if (slider.current) slider.current.internal_onmouseup = handleChange
return (
<div>
<input id='slider'
data-id="slider"
className='w-100 my-0'
ref={slider}
type='range'
min={0}
max={traceLength ? traceLength - 1 : 0}
value={state.currentValue}
onChange={handleChange}
onMouseUp={handleChange}
disabled={traceLength ? traceLength === 0 : true}
/>
</div>

@ -125,7 +125,7 @@ export const AssemblyItems = ({ registerEvent }) => {
<div className="border rounded px-1 mt-1 bg-light">
<div className='dropdownpanel'>
<div className='dropdowncontent'>
<div className="pl-2 my-1 small instructions" id='asmitems' ref={asmItemsRef}>
<div className="pl-2 my-1 small instructions" data-id="asmitems" id='asmitems' ref={asmItemsRef}>
{
assemblyItems.display.map((item, i) => {
return <div className="px-1" key={i} ref={ref => { refs.current[i] = ref }}><span>{item}</span></div>

@ -18,4 +18,9 @@
.contextview {
opacity: 1;
position: absolute;
}
.inline-class {
background: var(--primary) !important;
color: var(--text) !important;
}

@ -38,24 +38,19 @@ type sourceMarker = {
hide: boolean
}
type sourceAnnotationMap = {
[key: string]: [sourceAnnotation];
}
loader.config({ paths: { vs: 'assets/js/monaco-editor/dev/vs' } })
type sourceMarkerMap = {
[key: string]: [sourceMarker];
export type DecorationsReturn = {
currentDecorations: Array<string>
registeredDecorations?: Array<any>
}
loader.config({ paths: { vs: 'assets/js/monaco-editor/dev/vs' } })
/* eslint-disable-next-line */
export interface EditorUIProps {
contextualListener: any
activated: boolean
themeType: string
currentFile: string
sourceAnnotationsPerFile: sourceAnnotationMap
markerPerFile: sourceMarkerMap
events: {
onBreakPointAdded: (file: string, line: number) => void
onBreakPointCleared: (file: string, line: number) => void
@ -71,17 +66,20 @@ export interface EditorUIProps {
getFontSize: () => number,
getValue: (uri: string) => string
getCursorPosition: () => cursorPosition
addDecoration: (marker: sourceMarker, filePath: string, typeOfDecoration: string) => DecorationsReturn
clearDecorationsByPlugin: (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => DecorationsReturn
keepDecorationsFor: (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => DecorationsReturn
}
}
export const EditorUI = (props: EditorUIProps) => {
const [, setCurrentBreakpoints] = useState({})
const [currentAnnotations, setCurrentAnnotations] = useState({})
const [currentMarkers, setCurrentMarkers] = useState({})
const editorRef = useRef(null)
const monacoRef = useRef(null)
const currentFileRef = useRef('')
// const currentDecorations = useRef({ sourceAnnotationsPerFile: {}, markerPerFile: {} }) // decorations that are currently in use by the editor
// const registeredDecorations = useRef({}) // registered decorations
const [editorModelsState, dispatch] = useReducer(reducerActions, initialState)
const formatColor = (name) => {
@ -226,55 +224,6 @@ export const EditorUI = (props: EditorUIProps) => {
defineAndSetTheme(monacoRef.current)
})
const setAnnotationsbyFile = (uri) => {
if (props.sourceAnnotationsPerFile[uri]) {
const model = editorModelsState[uri]?.model
const newAnnotations = []
for (const annotation of props.sourceAnnotationsPerFile[uri]) {
if (!annotation.hide) {
newAnnotations.push({
range: new monacoRef.current.Range(annotation.row + 1, 1, annotation.row + 1, 1),
options: {
isWholeLine: false,
glyphMarginHoverMessage: { value: (annotation.from ? `from ${annotation.from}:\n` : '') + annotation.text },
glyphMarginClassName: `fal fa-exclamation-square text-${annotation.type === 'error' ? 'danger' : (annotation.type === 'warning' ? 'warning' : 'info')}`
}
})
}
}
setCurrentAnnotations(prevState => {
prevState[uri] = model.deltaDecorations(currentAnnotations[uri] || [], newAnnotations)
return prevState
})
}
}
const setMarkerbyFile = (uri) => {
if (props.markerPerFile[uri]) {
const model = editorModelsState[uri]?.model
const newMarkers = []
for (const marker of props.markerPerFile[uri]) {
if (!marker.hide) {
let isWholeLine = false
if (marker.position.start.line === marker.position.end.line && marker.position.end.column - marker.position.start.column < 3) {
// in this case we force highlighting the whole line (doesn't make sense to highlight 2 chars)
isWholeLine = true
}
newMarkers.push({
range: new monacoRef.current.Range(marker.position.start.line + 1, marker.position.start.column + 1, marker.position.end.line + 1, marker.position.end.column + 1),
options: {
isWholeLine,
inlineClassName: `alert-info border-0 highlightLine${marker.position.start.line + 1}`
}
})
}
}
setCurrentMarkers(prevState => {
prevState[uri] = model.deltaDecorations(currentMarkers[uri] || [], newMarkers)
return prevState
})
}
}
useEffect(() => {
if (!editorRef.current) return
@ -286,18 +235,89 @@ export const EditorUI = (props: EditorUIProps) => {
monacoRef.current.editor.setModelLanguage(file.model, 'remix-solidity')
} else if (file.language === 'cairo') {
monacoRef.current.editor.setModelLanguage(file.model, 'remix-cairo')
}
setAnnotationsbyFile(props.currentFile)
setMarkerbyFile(props.currentFile)
}
}, [props.currentFile])
useEffect(() => {
setAnnotationsbyFile(props.currentFile)
}, [JSON.stringify(props.sourceAnnotationsPerFile)])
const convertToMonacoDecoration = (decoration: sourceAnnotation | sourceMarker, typeOfDecoration: string) => {
if (typeOfDecoration === 'sourceAnnotationsPerFile') {
decoration = decoration as sourceAnnotation
return {
type: typeOfDecoration,
range: new monacoRef.current.Range(decoration.row + 1, 1, decoration.row + 1, 1),
options: {
isWholeLine: false,
glyphMarginHoverMessage: { value: (decoration.from ? `from ${decoration.from}:\n` : '') + decoration.text },
glyphMarginClassName: `fal fa-exclamation-square text-${decoration.type === 'error' ? 'danger' : (decoration.type === 'warning' ? 'warning' : 'info')}`
}
}
}
if (typeOfDecoration === 'markerPerFile') {
decoration = decoration as sourceMarker
let isWholeLine = false
if (decoration.position.start.line === decoration.position.end.line && decoration.position.end.column - decoration.position.start.column < 3) {
// in this case we force highlighting the whole line (doesn't make sense to highlight 2 chars)
isWholeLine = true
}
return {
type: typeOfDecoration,
range: new monacoRef.current.Range(decoration.position.start.line + 1, decoration.position.start.column + 1, decoration.position.end.line + 1, decoration.position.end.column + 1),
options: {
isWholeLine,
inlineClassName: `inline-class border-0 selectionHighlight highlightLine${decoration.position.start.line + 1}`
}
}
}
}
useEffect(() => {
setMarkerbyFile(props.currentFile)
}, [JSON.stringify(props.markerPerFile)])
props.editorAPI.clearDecorationsByPlugin = (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => {
const model = editorModelsState[filePath]?.model
if (!model) return
const decorations = []
const newRegisteredDecorations = []
if (registeredDecorations) {
for (const decoration of registeredDecorations) {
if (decoration.type === typeOfDecoration && decoration.value.from !== plugin) {
decorations.push(convertToMonacoDecoration(decoration.value, typeOfDecoration))
newRegisteredDecorations.push(decoration)
}
}
}
return {
currentDecorations: model.deltaDecorations(currentDecorations, decorations),
registeredDecorations: newRegisteredDecorations
}
}
props.editorAPI.keepDecorationsFor = (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => {
const model = editorModelsState[filePath]?.model
if (!model) return
const decorations = []
if (registeredDecorations) {
for (const decoration of registeredDecorations) {
if (decoration.value.from === plugin) {
decorations.push(convertToMonacoDecoration(decoration.value, typeOfDecoration))
}
}
}
return {
currentDecorations: model.deltaDecorations(currentDecorations, decorations)
}
}
const addDecoration = (decoration: sourceAnnotation | sourceMarker, filePath: string, typeOfDecoration: string) => {
const model = editorModelsState[filePath]?.model
if (!model) return { currentDecorations: [] }
const monacoDecoration = convertToMonacoDecoration(decoration, typeOfDecoration)
return {
currentDecorations: model.deltaDecorations([], [monacoDecoration]),
registeredDecorations: [{ value: decoration, type: typeOfDecoration }]
}
}
props.editorAPI.addDecoration = (marker: sourceMarker, filePath: string, typeOfDecoration: string) => {
return addDecoration(marker, filePath, typeOfDecoration)
}
props.editorAPI.findMatches = (uri: string, value: string) => {
if (!editorRef.current) return

@ -1,2 +1,3 @@
export * from './lib/remix-ui-helper'
export * from './lib/helper-components'
export * from './lib/components/PluginViewWrapper'

@ -0,0 +1,24 @@
import React from "react"
import { useEffect, useState } from "react"
interface IPluginViewWrapperProps {
plugin: any
}
export const PluginViewWrapper = (props: IPluginViewWrapperProps) => {
const [state, setState] = useState<any>(null)
useEffect(() => {
if(props.plugin.setDispatch){
props.plugin.setDispatch(setState)
}
}, [])
return (
<>{state?
<>{props.plugin.updateComponent(state)}</>
:<></>
}</>
)
}

@ -69,3 +69,12 @@ export const envChangeNotification = (env: { context: string, fork: string }, fr
</span>
</div>
)
export const storageFullMessage = () => (
<div>
<i className="fas fa-exclamation-triangle text-danger mr-1"></i>
<span className="font-weight-bold">
<span>Cannot save this file due to full LocalStorage. Backup existing files and free up some space.</span>
</span>
</div>
)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save