1spacelint
bunsenstraat 1 year ago
parent c37fa3d924
commit 3c43c1d797
  1. 4
      .eslintrc.json
  2. 8
      apps/remix-ide/background.js
  3. 88
      apps/remix-ide/ci/download_e2e_assets.js
  4. 20
      apps/remix-ide/ci/lint-targets.js
  5. 124
      apps/remix-ide/ci/makeMockCompiler.js
  6. 102
      apps/remix-ide/ci/sauceDisconnect.js
  7. 30
      apps/remix-ide/ci/splice_tests.js
  8. 836
      apps/remix-ide/src/app.js
  9. 66
      apps/remix-ide/src/app/components/hidden-panel.tsx
  10. 96
      apps/remix-ide/src/app/components/main-panel.tsx
  11. 82
      apps/remix-ide/src/app/components/panel.ts
  12. 224
      apps/remix-ide/src/app/components/plugin-manager-component.js
  13. 206
      apps/remix-ide/src/app/components/preload.tsx
  14. 150
      apps/remix-ide/src/app/components/side-panel.tsx
  15. 202
      apps/remix-ide/src/app/components/vertical-icons.tsx
  16. 848
      apps/remix-ide/src/app/editor/editor.js
  17. 1136
      apps/remix-ide/src/app/files/dgitProvider.js
  18. 1500
      apps/remix-ide/src/app/files/fileManager.ts
  19. 552
      apps/remix-ide/src/app/files/fileProvider.js
  20. 104
      apps/remix-ide/src/app/files/fileSystem.ts
  21. 308
      apps/remix-ide/src/app/files/filesystems/fileSystemUtility.ts
  22. 156
      apps/remix-ide/src/app/files/filesystems/indexedDB.ts
  23. 98
      apps/remix-ide/src/app/files/filesystems/localStorage.ts
  24. 32
      apps/remix-ide/src/app/files/foundry-handle.js
  25. 32
      apps/remix-ide/src/app/files/git-handle.js
  26. 32
      apps/remix-ide/src/app/files/hardhat-handle.js
  27. 406
      apps/remix-ide/src/app/files/remixDProvider.js
  28. 34
      apps/remix-ide/src/app/files/slither-handle.js
  29. 32
      apps/remix-ide/src/app/files/truffle-handle.js
  30. 132
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  31. 286
      apps/remix-ide/src/app/panels/file-panel.js
  32. 166
      apps/remix-ide/src/app/panels/layout.ts
  33. 666
      apps/remix-ide/src/app/panels/tab-proxy.js
  34. 218
      apps/remix-ide/src/app/panels/terminal.js
  35. 482
      apps/remix-ide/src/app/plugins/code-format.ts
  36. 64
      apps/remix-ide/src/app/plugins/code-format/index.ts
  37. 356
      apps/remix-ide/src/app/plugins/code-format/parser.ts
  38. 42
      apps/remix-ide/src/app/plugins/config.ts
  39. 112
      apps/remix-ide/src/app/plugins/contractFlattener.tsx
  40. 118
      apps/remix-ide/src/app/plugins/file-decorator.ts
  41. 40
      apps/remix-ide/src/app/plugins/notification.tsx
  42. 976
      apps/remix-ide/src/app/plugins/parser/code-parser.tsx
  43. 72
      apps/remix-ide/src/app/plugins/parser/services/antlr-worker.ts
  44. 410
      apps/remix-ide/src/app/plugins/parser/services/code-parser-antlr-service.ts
  45. 432
      apps/remix-ide/src/app/plugins/parser/services/code-parser-compiler.ts
  46. 112
      apps/remix-ide/src/app/plugins/parser/services/code-parser-gas-service.ts
  47. 130
      apps/remix-ide/src/app/plugins/parser/services/code-parser-imports.ts
  48. 232
      apps/remix-ide/src/app/plugins/parser/types/antlr-types.ts
  49. 280
      apps/remix-ide/src/app/plugins/permission-handler-plugin.tsx
  50. 284
      apps/remix-ide/src/app/plugins/remixd-handle.tsx
  51. 144
      apps/remix-ide/src/app/plugins/solidity-script.tsx
  52. 626
      apps/remix-ide/src/app/plugins/solidity-umlgen.tsx
  53. 92
      apps/remix-ide/src/app/plugins/storage.ts
  54. 170
      apps/remix-ide/src/app/providers/abstract-provider.tsx
  55. 186
      apps/remix-ide/src/app/providers/custom-vm-fork-provider.tsx
  56. 56
      apps/remix-ide/src/app/providers/external-http-provider.tsx
  57. 40
      apps/remix-ide/src/app/providers/foundry-provider.tsx
  58. 40
      apps/remix-ide/src/app/providers/ganache-provider.tsx
  59. 44
      apps/remix-ide/src/app/providers/goerli-vm-fork-provider.tsx
  60. 38
      apps/remix-ide/src/app/providers/hardhat-provider.tsx
  61. 84
      apps/remix-ide/src/app/providers/injected-L2-provider.tsx
  62. 18
      apps/remix-ide/src/app/providers/injected-arbitrum-one-provider.tsx
  63. 18
      apps/remix-ide/src/app/providers/injected-optimism-provider.tsx
  64. 48
      apps/remix-ide/src/app/providers/injected-provider-default.tsx
  65. 30
      apps/remix-ide/src/app/providers/injected-provider-trustwallet.tsx
  66. 138
      apps/remix-ide/src/app/providers/injected-provider.tsx
  67. 44
      apps/remix-ide/src/app/providers/mainnet-vm-fork-provider.tsx
  68. 44
      apps/remix-ide/src/app/providers/sepolia-vm-fork-provider.tsx
  69. 154
      apps/remix-ide/src/app/providers/vm-provider.tsx
  70. 50
      apps/remix-ide/src/app/state/registry.ts
  71. 148
      apps/remix-ide/src/app/tabs/analysis-tab.js
  72. 142
      apps/remix-ide/src/app/tabs/compile-and-run.ts
  73. 270
      apps/remix-ide/src/app/tabs/compile-tab.js
  74. 174
      apps/remix-ide/src/app/tabs/debugger-tab.js
  75. 106
      apps/remix-ide/src/app/tabs/locale-module.js
  76. 24
      apps/remix-ide/src/app/tabs/locales/en/index.js
  77. 24
      apps/remix-ide/src/app/tabs/locales/zh/index.js
  78. 92
      apps/remix-ide/src/app/tabs/network-module.js
  79. 494
      apps/remix-ide/src/app/tabs/runTab/model/recorder.js
  80. 42
      apps/remix-ide/src/app/tabs/search.tsx
  81. 128
      apps/remix-ide/src/app/tabs/settings-tab.tsx
  82. 264
      apps/remix-ide/src/app/tabs/test-tab.js
  83. 194
      apps/remix-ide/src/app/tabs/theme-module.js
  84. 132
      apps/remix-ide/src/app/tabs/web3-provider.js
  85. 58
      apps/remix-ide/src/app/udapp/make-udapp.js
  86. 326
      apps/remix-ide/src/app/udapp/run-tab.js
  87. 52
      apps/remix-ide/src/app/ui/landing-page/landing-page.js
  88. 1252
      apps/remix-ide/src/app/ui/styles-guide/styleGuideClean.js
  89. 1506
      apps/remix-ide/src/blockchain/blockchain.tsx
  90. 322
      apps/remix-ide/src/blockchain/execution-context.js
  91. 18
      apps/remix-ide/src/blockchain/helper.ts
  92. 86
      apps/remix-ide/src/blockchain/providers/injected.ts
  93. 104
      apps/remix-ide/src/blockchain/providers/node.ts
  94. 176
      apps/remix-ide/src/blockchain/providers/vm.ts
  95. 130
      apps/remix-ide/src/blockchain/providers/worker-vm.ts
  96. 82
      apps/remix-ide/src/config.js
  97. 26
      apps/remix-ide/src/index.tsx
  98. 272
      apps/remix-ide/src/lib/helper.js
  99. 38
      apps/remix-ide/src/registry.js
  100. 408
      apps/remix-ide/src/remixAppManager.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -48,7 +48,7 @@
"no-empty": "off", "no-empty": "off",
"jsx-a11y/anchor-is-valid": "off", "jsx-a11y/anchor-is-valid": "off",
"@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/no-inferrable-types": "off",
"indent": ["error", 2] "indent": ["error", 1]
} }
}, },
{ {
@ -60,7 +60,7 @@
"plugin:@nrwl/nx/javascript" "plugin:@nrwl/nx/javascript"
], ],
"rules": { "rules": {
"indent": ["error", 2] "indent": ["error", 1]
} }
} }
], ],

@ -2,9 +2,9 @@
'use strict' 'use strict'
chrome.browserAction.onClicked.addListener(function (tab) { chrome.browserAction.onClicked.addListener(function (tab) {
chrome.storage.sync.set({ 'chrome-app-sync': true }) chrome.storage.sync.set({ 'chrome-app-sync': true })
chrome.tabs.create({ 'url': chrome.extension.getURL('index.html') }, function (tab) { chrome.tabs.create({ 'url': chrome.extension.getURL('index.html') }, function (tab) {
// tab opened // tab opened
}) })
}) })

@ -5,8 +5,8 @@ const { exit } = require('process');
const child = child_process.spawnSync('grep -r --include="*.json" --include="*.ts" --include="*.tsx" "+commit" apps/**/* libs/**/*', [], { encoding: 'utf8', cwd: process.cwd(), shell: true }); const child = child_process.spawnSync('grep -r --include="*.json" --include="*.ts" --include="*.tsx" "+commit" apps/**/* libs/**/*', [], { encoding: 'utf8', cwd: process.cwd(), shell: true });
if (child.error) { if (child.error) {
console.log("ERROR: ", child); console.log("ERROR: ", child);
exit(1); exit(1);
} }
@ -15,63 +15,63 @@ let soljson =[];
const quotedVersionsRegex = /['"v]\d*\.\d*\.\d*\+commit\.[\d\w]*/g; const quotedVersionsRegex = /['"v]\d*\.\d*\.\d*\+commit\.[\d\w]*/g;
let quotedVersionsRegexMatch = child.stdout.match(quotedVersionsRegex) let quotedVersionsRegexMatch = child.stdout.match(quotedVersionsRegex)
if(quotedVersionsRegexMatch){ if(quotedVersionsRegexMatch){
let soljson2 = quotedVersionsRegexMatch.map((item) => item.replace('\'', 'v').replace('"', 'v')) let soljson2 = quotedVersionsRegexMatch.map((item) => item.replace('\'', 'v').replace('"', 'v'))
console.log('non nightly soljson versions found: ', soljson2); console.log('non nightly soljson versions found: ', soljson2);
if(soljson2) soljson = soljson.concat(soljson2); if(soljson2) soljson = soljson.concat(soljson2);
} }
const nightlyVersionsRegex = /\d*\.\d*\.\d-nightly.*\+commit\.[\d\w]*/g const nightlyVersionsRegex = /\d*\.\d*\.\d-nightly.*\+commit\.[\d\w]*/g
const nightlyVersionsRegexMatch = child.stdout.match(nightlyVersionsRegex) const nightlyVersionsRegexMatch = child.stdout.match(nightlyVersionsRegex)
if(nightlyVersionsRegexMatch){ if(nightlyVersionsRegexMatch){
let soljson3 = nightlyVersionsRegexMatch.map((item) => 'v' + item); let soljson3 = nightlyVersionsRegexMatch.map((item) => 'v' + item);
console.log('nightly soljson versions found: ', soljson3); console.log('nightly soljson versions found: ', soljson3);
if(soljson3) soljson = soljson.concat(soljson3); if(soljson3) soljson = soljson.concat(soljson3);
} }
if (soljson) { if (soljson) {
// filter out duplicates // filter out duplicates
soljson = soljson.filter((item, index) => soljson.indexOf(item) === index); soljson = soljson.filter((item, index) => soljson.indexOf(item) === index);
// manually add some versions // manually add some versions
soljson.push('v0.7.6+commit.7338295f'); soljson.push('v0.7.6+commit.7338295f');
console.log('soljson versions found: ', soljson, soljson.length); console.log('soljson versions found: ', soljson, soljson.length);
for (let i = 0; i < soljson.length; i++) { for (let i = 0; i < soljson.length; i++) {
const version = soljson[i]; const version = soljson[i];
if (version) { if (version) {
let url = '' let url = ''
// if nightly // if nightly
if (version.includes('nightly')) { if (version.includes('nightly')) {
url = `https://binaries.soliditylang.org/bin/soljson-${version}.js`; url = `https://binaries.soliditylang.org/bin/soljson-${version}.js`;
}else{ }else{
url = `https://binaries.soliditylang.org/wasm/soljson-${version}.js`; url = `https://binaries.soliditylang.org/wasm/soljson-${version}.js`;
} }
const dir = './dist/apps/remix-ide/assets/js/soljson'; const dir = './dist/apps/remix-ide/assets/js/soljson';
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir); fs.mkdirSync(dir);
} }
const path = `./dist/apps/remix-ide/assets/js/soljson/soljson-${version}.js`; const path = `./dist/apps/remix-ide/assets/js/soljson/soljson-${version}.js`;
// check if the file exists // check if the file exists
const exists = fs.existsSync(path); const exists = fs.existsSync(path);
if (!exists) { if (!exists) {
console.log('URL:', url) console.log('URL:', url)
try { try {
// use curl to download the file // use curl to download the file
child_process.exec(`curl -o ${path} ${url}`, { encoding: 'utf8', cwd: process.cwd(), shell: true }) child_process.exec(`curl -o ${path} ${url}`, { encoding: 'utf8', cwd: process.cwd(), shell: true })
} catch (e) { } catch (e) {
console.log('Failed to download soljson' + version + ' from ' + url) console.log('Failed to download soljson' + version + ' from ' + url)
} }
} }
} }
} }
} }

@ -5,15 +5,15 @@ const file = fs.readFileSync('projects.json')
const projects = JSON.parse(file) const projects = JSON.parse(file)
console.log(Object.keys(projects.graph.nodes)) console.log(Object.keys(projects.graph.nodes))
for(let node of Object.keys(projects.graph.nodes)){ for(let node of Object.keys(projects.graph.nodes)){
if(projects.graph.nodes[node].data.targets.lint){ if(projects.graph.nodes[node].data.targets.lint){
console.log(projects.graph.nodes[node].data.name) console.log(projects.graph.nodes[node].data.name)
const result = spawnSync('yarn', ['lint', projects.graph.nodes[node].data.name]) const result = spawnSync('yarn', ['lint', projects.graph.nodes[node].data.name])
if(result.status == 0){ if(result.status == 0){
console.log('success') console.log('success')
}else{ }else{
console.log(result.stdout.toString()) console.log(result.stdout.toString())
console.log(result.stderr.toString()) console.log(result.stderr.toString())
exit(1) exit(1)
}
} }
}
} }

@ -7,82 +7,82 @@ var defaultVersion = 'soljson-v0.8.18+commit.87f61d96.js'
const path = require('path') const path = require('path')
compiler.loadRemoteVersion(defaultVersion, (error, solcSnapshot) => { compiler.loadRemoteVersion(defaultVersion, (error, solcSnapshot) => {
console.log('solcSnapshot: ', solcSnapshot) console.log('solcSnapshot: ', solcSnapshot)
if (error) console.log(error) if (error) console.log(error)
var compilationResult = {} var compilationResult = {}
const testsFolder = path.resolve(__dirname + '/../test-browser/tests/') + '/' // eslint-disable-line const testsFolder = path.resolve(__dirname + '/../test-browser/tests/') + '/' // eslint-disable-line
gatherCompilationResults(testsFolder, compilationResult, solcSnapshot) gatherCompilationResults(testsFolder, compilationResult, solcSnapshot)
replaceSolCompiler(compilationResult, solcSnapshot) replaceSolCompiler(compilationResult, solcSnapshot)
}) })
function gatherCompilationResults (dir, compilationResult, solcSnapshot) { function gatherCompilationResults (dir, compilationResult, solcSnapshot) {
var filenames = fs.readdirSync(dir, 'utf8') var filenames = fs.readdirSync(dir, 'utf8')
filenames.map(function (item, i) { filenames.map(function (item, i) {
if (item.endsWith('.js')) { if (item.endsWith('.js')) {
var testDef = require(dir + item) var testDef = require(dir + item)
if ('@sources' in testDef) { if ('@sources' in testDef) {
var sources = testDef['@sources']() var sources = testDef['@sources']()
for (var files in sources) { for (var files in sources) {
compile(solcSnapshot, sources[files], true, function (result) { compile(solcSnapshot, sources[files], true, function (result) {
compilationResult[result.key] = result compilationResult[result.key] = result
}) })
compile(solcSnapshot, sources[files], false, function (result) { compile(solcSnapshot, sources[files], false, function (result) {
compilationResult[result.key] = result compilationResult[result.key] = result
}) })
}
}
} }
}) }
return compilationResult }
})
return compilationResult
} }
function compile (solcSnapshot, source, optimization, runs, addCompilationResult) { function compile (solcSnapshot, source, optimization, runs, addCompilationResult) {
var missingInputs = [] var missingInputs = []
try { try {
var input = compilerInput(source, {optimize: optimization, runs: runs}) var input = compilerInput(source, {optimize: optimization, runs: runs})
var result = solcSnapshot.compileStandardWrapper(input, function (path) { var result = solcSnapshot.compileStandardWrapper(input, function (path) {
missingInputs.push(path) missingInputs.push(path)
}) })
input = input.replace(/(\t)|(\n)|(\\n)|( )/g, '') input = input.replace(/(\t)|(\n)|(\\n)|( )/g, '')
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }
if (result && (result.error || (result.errors && result.errors.length > 0))) { if (result && (result.error || (result.errors && result.errors.length > 0))) {
console.log(result.error, result.errors) console.log(result.error, result.errors)
} }
if (result) { if (result) {
console.log(result.error, result.errors) console.log(result.error, result.errors)
} }
var ret = { var ret = {
key: input, key: input,
source: source, source: source,
optimization: optimization, optimization: optimization,
missingInputs: missingInputs, missingInputs: missingInputs,
result: result result: result
} }
addCompilationResult(ret) addCompilationResult(ret)
} }
function replaceSolCompiler (results, solcSnapshot) { function replaceSolCompiler (results, solcSnapshot) {
const compilerPath = path.resolve(__dirname + '/../test-browser/mockcompiler/compiler.js') // eslint-disable-line const compilerPath = path.resolve(__dirname + '/../test-browser/mockcompiler/compiler.js') // eslint-disable-line
const soljsonPath = path.resolve(__dirname + '/../soljson.js') // eslint-disable-line const soljsonPath = path.resolve(__dirname + '/../soljson.js') // eslint-disable-line
fs.readFile(compilerPath, 'utf8', function (error, data) { fs.readFile(compilerPath, 'utf8', function (error, data) {
if (error) { if (error) {
console.log(error) console.log(error)
process.exit(1) process.exit(1)
return return
} }
console.log(solcSnapshot.version()) console.log(solcSnapshot.version())
data = data + '\n\nvar mockCompilerVersion = \'' + solcSnapshot.version() + '\'' data = data + '\n\nvar mockCompilerVersion = \'' + solcSnapshot.version() + '\''
data = data + '\n\nvar mockData = ' + JSON.stringify(results) + ';\n' data = data + '\n\nvar mockData = ' + JSON.stringify(results) + ';\n'
fs.writeFile(soljsonPath, data, 'utf8', function (error) { fs.writeFile(soljsonPath, data, 'utf8', function (error) {
if (error) { if (error) {
console.log(error) console.log(error)
process.exit(1) process.exit(1)
return return
} }
})
}) })
})
} }

@ -7,69 +7,69 @@ var accessKey = process.argv[3]
var tunnelName = process.argv[4] var tunnelName = process.argv[4]
function removeTunnel () { function removeTunnel () {
const requestPath = `/rest/v1/${userName}/tunnels` const requestPath = `/rest/v1/${userName}/tunnels`
console.log(requestPath) console.log(requestPath)
callSauce(requestPath, 'GET', function (error, result) { callSauce(requestPath, 'GET', function (error, result) {
if (error) { if (error) {
console.log(error)
} else {
var data = JSON.parse(result)
for (var k in data) {
retrieveTunnel(data[k], function (error, result) {
if (error) {
console.log(error) console.log(error)
} else { } else if (result.identtifier === tunnelName) {
var data = JSON.parse(result) deleteTunnel(result.id, function () {
for (var k in data) { console.log('tunnel deleted ' + data[k] + ' ' + tunnelName)
retrieveTunnel(data[k], function (error, result) { })
if (error) { }
console.log(error) })
} else if (result.identtifier === tunnelName) { }
deleteTunnel(result.id, function () { }
console.log('tunnel deleted ' + data[k] + ' ' + tunnelName) })
})
}
})
}
}
})
} }
function retrieveTunnel (tunnelid, callback) { function retrieveTunnel (tunnelid, callback) {
const requestPath = `/rest/v1/${userName}/tunnels/${tunnelid}` const requestPath = `/rest/v1/${userName}/tunnels/${tunnelid}`
callSauce(requestPath, 'GET', function (error, result) { callSauce(requestPath, 'GET', function (error, result) {
if (error) { if (error) {
callback(error) callback(error)
} else { } else {
callback(null, {'identtifier': JSON.parse(result).tunnel_identifier, 'id': tunnelid}) callback(null, {'identtifier': JSON.parse(result).tunnel_identifier, 'id': tunnelid})
} }
}) })
} }
function deleteTunnel (tunnelid, callback) { function deleteTunnel (tunnelid, callback) {
const requestPath = `/rest/v1/${userName}/tunnels/${tunnelid}` const requestPath = `/rest/v1/${userName}/tunnels/${tunnelid}`
callSauce(requestPath, 'DELETE', callback) callSauce(requestPath, 'DELETE', callback)
} }
function callSauce (requestPath, type, callback) { function callSauce (requestPath, type, callback) {
function responseCallback (res) { function responseCallback (res) {
res.setEncoding('utf8') res.setEncoding('utf8')
console.log('Response: ', res.statusCode, JSON.stringify(res.headers)) console.log('Response: ', res.statusCode, JSON.stringify(res.headers))
res.on('data', function onData (chunk) { res.on('data', function onData (chunk) {
console.log('BODY: ' + chunk) console.log('BODY: ' + chunk)
callback(null, chunk) callback(null, chunk)
}) })
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
res.on('end', function onEnd () {}) res.on('end', function onEnd () {})
} }
var req = https.request({ var req = https.request({
hostname: 'saucelabs.com', hostname: 'saucelabs.com',
path: requestPath, path: requestPath,
method: type, method: type,
auth: userName + ':' + accessKey auth: userName + ':' + accessKey
}, responseCallback) }, responseCallback)
req.on('error', function onError (e) { req.on('error', function onError (e) {
console.log('problem with request: ' + e.message) console.log('problem with request: ' + e.message)
callback(e.message) callback(e.message)
}) })
req.write('') req.write('')
req.end() req.end()
} }
removeTunnel() removeTunnel()

@ -7,21 +7,21 @@ let args = process.argv.slice(2)
const jobsize = args[0] || 10; const jobsize = args[0] || 10;
const job = args[1] || 0; const job = args[1] || 0;
exec(cmd, (error, stdout, stderr) => { exec(cmd, (error, stdout, stderr) => {
if (error) { if (error) {
console.error(`error: ${error.message}`); console.error(`error: ${error.message}`);
return; return;
} }
if (stderr) { if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
return; return;
} }
let files = stdout.split('\n').filter(f => f.includes('.test')).map(f => f.replace('dist/apps/remix-ide-e2e/src/tests/', '')).map(f => f.replace('.js', '')) let files = stdout.split('\n').filter(f => f.includes('.test')).map(f => f.replace('dist/apps/remix-ide-e2e/src/tests/', '')).map(f => f.replace('.js', ''))
let splitIndex = Math.ceil(files.length / jobsize); let splitIndex = Math.ceil(files.length / jobsize);
const parts = [] const parts = []
for (let i = 0; i < jobsize; i++) { for (let i = 0; i < jobsize; i++) {
parts.push(files.slice(i * splitIndex, (i + 1) * splitIndex)) parts.push(files.slice(i * splitIndex, (i + 1) * splitIndex))
} }
console.log(parts[job].join('\n')) console.log(parts[job].join('\n'))
}); });

@ -75,436 +75,436 @@ const Terminal = require('./app/panels/terminal')
const { TabProxy } = require('./app/panels/tab-proxy.js') const { TabProxy } = require('./app/panels/tab-proxy.js')
class AppComponent { class AppComponent {
constructor() { constructor() {
this.appManager = new RemixAppManager({}) this.appManager = new RemixAppManager({})
this.queryParams = new QueryParams() this.queryParams = new QueryParams()
this._components = {} this._components = {}
// setup storage // setup storage
const configStorage = new Storage('config-v0.8:') const configStorage = new Storage('config-v0.8:')
// load app config // load app config
const config = new Config(configStorage) const config = new Config(configStorage)
Registry.getInstance().put({ api: config, name: 'config' }) Registry.getInstance().put({ api: config, name: 'config' })
// load file system // load file system
this._components.filesProviders = {} this._components.filesProviders = {}
this._components.filesProviders.browser = new FileProvider('browser') this._components.filesProviders.browser = new FileProvider('browser')
Registry.getInstance().put({ Registry.getInstance().put({
api: this._components.filesProviders.browser, api: this._components.filesProviders.browser,
name: 'fileproviders/browser' name: 'fileproviders/browser'
}) })
this._components.filesProviders.localhost = new RemixDProvider( this._components.filesProviders.localhost = new RemixDProvider(
this.appManager this.appManager
) )
Registry.getInstance().put({ Registry.getInstance().put({
api: this._components.filesProviders.localhost, api: this._components.filesProviders.localhost,
name: 'fileproviders/localhost' name: 'fileproviders/localhost'
}) })
this._components.filesProviders.workspace = new WorkspaceFileProvider() this._components.filesProviders.workspace = new WorkspaceFileProvider()
Registry.getInstance().put({ Registry.getInstance().put({
api: this._components.filesProviders.workspace, api: this._components.filesProviders.workspace,
name: 'fileproviders/workspace' name: 'fileproviders/workspace'
}) })
Registry.getInstance().put({ Registry.getInstance().put({
api: this._components.filesProviders, api: this._components.filesProviders,
name: 'fileproviders' name: 'fileproviders'
}) })
}
async run() {
// APP_MANAGER
const appManager = this.appManager
const pluginLoader = this.appManager.pluginLoader
this.panels = {}
this.workspace = pluginLoader.get()
this.engine = new RemixEngine()
this.engine.register(appManager);
const matomoDomains = {
'remix-alpha.ethereum.org': 27,
'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23,
'6fd22d6fe5549ad4c4d8fd3ca0b7816b.mod': 35 // remix desktop
} }
this.showMatamo =
async run() {
// APP_MANAGER
const appManager = this.appManager
const pluginLoader = this.appManager.pluginLoader
this.panels = {}
this.workspace = pluginLoader.get()
this.engine = new RemixEngine()
this.engine.register(appManager);
const matomoDomains = {
'remix-alpha.ethereum.org': 27,
'remix-beta.ethereum.org': 25,
'remix.ethereum.org': 23,
'6fd22d6fe5549ad4c4d8fd3ca0b7816b.mod': 35 // remix desktop
}
this.showMatamo =
matomoDomains[window.location.hostname] && matomoDomains[window.location.hostname] &&
!Registry.getInstance() !Registry.getInstance()
.get('config') .get('config')
.api.exists('settings/matomo-analytics') .api.exists('settings/matomo-analytics')
this.walkthroughService = new WalkthroughService( this.walkthroughService = new WalkthroughService(
appManager, appManager,
this.showMatamo this.showMatamo
) )
const hosts = ['127.0.0.1:8080', '192.168.0.101:8080', 'localhost:8080'] const hosts = ['127.0.0.1:8080', '192.168.0.101:8080', 'localhost:8080']
// workaround for Electron support // workaround for Electron support
if (!isElectron() && !hosts.includes(window.location.host)) { if (!isElectron() && !hosts.includes(window.location.host)) {
// Oops! Accidentally trigger refresh or bookmark. // Oops! Accidentally trigger refresh or bookmark.
window.onbeforeunload = function () { window.onbeforeunload = function () {
return 'Are you sure you want to leave?' return 'Are you sure you want to leave?'
} }
} }
// SERVICES // SERVICES
// ----------------- gist service --------------------------------- // ----------------- gist service ---------------------------------
this.gistHandler = new GistHandler() this.gistHandler = new GistHandler()
// ----------------- theme service --------------------------------- // ----------------- theme service ---------------------------------
this.themeModule = new ThemeModule() this.themeModule = new ThemeModule()
// ----------------- locale service --------------------------------- // ----------------- locale service ---------------------------------
this.localeModule = new LocaleModule() this.localeModule = new LocaleModule()
Registry.getInstance().put({ api: this.themeModule, name: 'themeModule' }) Registry.getInstance().put({ api: this.themeModule, name: 'themeModule' })
Registry.getInstance().put({ api: this.localeModule, name: 'localeModule' }) Registry.getInstance().put({ api: this.localeModule, name: 'localeModule' })
// ----------------- editor service ---------------------------- // ----------------- editor service ----------------------------
const editor = new Editor() // wrapper around ace editor const editor = new Editor() // wrapper around ace editor
Registry.getInstance().put({ api: editor, name: 'editor' }) Registry.getInstance().put({ api: editor, name: 'editor' })
editor.event.register('requiringToSaveCurrentfile', () => editor.event.register('requiringToSaveCurrentfile', () =>
fileManager.saveCurrentFile() fileManager.saveCurrentFile()
) )
// ----------------- fileManager service ---------------------------- // ----------------- fileManager service ----------------------------
const fileManager = new FileManager(editor, appManager) const fileManager = new FileManager(editor, appManager)
Registry.getInstance().put({ api: fileManager, name: 'filemanager' }) Registry.getInstance().put({ api: fileManager, name: 'filemanager' })
// ----------------- dGit provider --------------------------------- // ----------------- dGit provider ---------------------------------
const dGitProvider = new DGitProvider() const dGitProvider = new DGitProvider()
// ----------------- Storage plugin --------------------------------- // ----------------- Storage plugin ---------------------------------
const storagePlugin = new StoragePlugin() const storagePlugin = new StoragePlugin()
// ------- FILE DECORATOR PLUGIN ------------------ // ------- FILE DECORATOR PLUGIN ------------------
const fileDecorator = new FileDecorator() const fileDecorator = new FileDecorator()
// ------- CODE FORMAT PLUGIN ------------------ // ------- CODE FORMAT PLUGIN ------------------
const codeFormat = new CodeFormat() const codeFormat = new CodeFormat()
//----- search //----- search
const search = new SearchPlugin() const search = new SearchPlugin()
//---------------- Solidity UML Generator ------------------------- //---------------- Solidity UML Generator -------------------------
const solidityumlgen = new SolidityUmlGen(appManager) const solidityumlgen = new SolidityUmlGen(appManager)
// ----------------- ContractFlattener ---------------------------- // ----------------- ContractFlattener ----------------------------
const contractFlattener = new ContractFlattener() const contractFlattener = new ContractFlattener()
// ----------------- import content service ------------------------ // ----------------- import content service ------------------------
const contentImport = new CompilerImports() const contentImport = new CompilerImports()
const blockchain = new Blockchain(Registry.getInstance().get('config').api) const blockchain = new Blockchain(Registry.getInstance().get('config').api)
// ----------------- compilation metadata generation service --------- // ----------------- compilation metadata generation service ---------
const compilerMetadataGenerator = new CompilerMetadata() const compilerMetadataGenerator = new CompilerMetadata()
// ----------------- compilation result service (can keep track of compilation results) ---------------------------- // ----------------- compilation result service (can keep track of compilation results) ----------------------------
const compilersArtefacts = new CompilerArtefacts() // store all the compilation results (key represent a compiler name) const compilersArtefacts = new CompilerArtefacts() // store all the compilation results (key represent a compiler name)
Registry.getInstance().put({ Registry.getInstance().put({
api: compilersArtefacts, api: compilersArtefacts,
name: 'compilersartefacts' name: 'compilersartefacts'
}) })
// service which fetch contract artifacts from sourve-verify, put artifacts in remix and compile it // service which fetch contract artifacts from sourve-verify, put artifacts in remix and compile it
const fetchAndCompile = new FetchAndCompile() const fetchAndCompile = new FetchAndCompile()
// ----------------- network service (resolve network id / name) ----- // ----------------- network service (resolve network id / name) -----
const networkModule = new NetworkModule(blockchain) const networkModule = new NetworkModule(blockchain)
// ----------------- represent the current selected web3 provider ---- // ----------------- represent the current selected web3 provider ----
const web3Provider = new Web3ProviderModule(blockchain) const web3Provider = new Web3ProviderModule(blockchain)
const vmProviderCustomFork = new CustomForkVMProvider(blockchain) const vmProviderCustomFork = new CustomForkVMProvider(blockchain)
const vmProviderMainnetFork = new MainnetForkVMProvider(blockchain) const vmProviderMainnetFork = new MainnetForkVMProvider(blockchain)
const vmProviderSepoliaFork = new SepoliaForkVMProvider(blockchain) const vmProviderSepoliaFork = new SepoliaForkVMProvider(blockchain)
const vmProviderGoerliFork = new GoerliForkVMProvider(blockchain) const vmProviderGoerliFork = new GoerliForkVMProvider(blockchain)
const vmProviderShanghai = new ShanghaiVMProvider(blockchain) const vmProviderShanghai = new ShanghaiVMProvider(blockchain)
const vmProviderMerge = new MergeVMProvider(blockchain) const vmProviderMerge = new MergeVMProvider(blockchain)
const vmProviderBerlin = new BerlinVMProvider(blockchain) const vmProviderBerlin = new BerlinVMProvider(blockchain)
const vmProviderLondon = new LondonVMProvider(blockchain) const vmProviderLondon = new LondonVMProvider(blockchain)
const hardhatProvider = new HardhatProvider(blockchain) const hardhatProvider = new HardhatProvider(blockchain)
const ganacheProvider = new GanacheProvider(blockchain) const ganacheProvider = new GanacheProvider(blockchain)
const foundryProvider = new FoundryProvider(blockchain) const foundryProvider = new FoundryProvider(blockchain)
const externalHttpProvider = new ExternalHttpProvider(blockchain) const externalHttpProvider = new ExternalHttpProvider(blockchain)
const trustWalletInjectedProvider = new InjectedProviderTrustWallet() const trustWalletInjectedProvider = new InjectedProviderTrustWallet()
const defaultInjectedProvider = new InjectedProviderDefault const defaultInjectedProvider = new InjectedProviderDefault
const injected0ptimismProvider = new Injected0ptimismProvider() const injected0ptimismProvider = new Injected0ptimismProvider()
const injectedArbitrumOneProvider = new InjectedArbitrumOneProvider() const injectedArbitrumOneProvider = new InjectedArbitrumOneProvider()
// ----------------- convert offset to line/column service ----------- // ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new OffsetToLineColumnConverter() const offsetToLineColumnConverter = new OffsetToLineColumnConverter()
Registry.getInstance().put({ Registry.getInstance().put({
api: offsetToLineColumnConverter, api: offsetToLineColumnConverter,
name: 'offsettolinecolumnconverter' name: 'offsettolinecolumnconverter'
}) })
// ----------------- run script after each compilation results ----------- // ----------------- run script after each compilation results -----------
const compileAndRun = new CompileAndRun() const compileAndRun = new CompileAndRun()
// -------------------Terminal---------------------------------------- // -------------------Terminal----------------------------------------
makeUdapp(blockchain, compilersArtefacts, domEl => terminal.logHtml(domEl)) makeUdapp(blockchain, compilersArtefacts, domEl => terminal.logHtml(domEl))
const terminal = new Terminal( const terminal = new Terminal(
{ appManager, blockchain }, { appManager, blockchain },
{ {
getPosition: event => { getPosition: event => {
const limitUp = 36 const limitUp = 36
const limitDown = 20 const limitDown = 20
const height = window.innerHeight const height = window.innerHeight
let newpos = event.pageY < limitUp ? limitUp : event.pageY let newpos = event.pageY < limitUp ? limitUp : event.pageY
newpos = newpos < height - limitDown ? newpos : height - limitDown newpos = newpos < height - limitDown ? newpos : height - limitDown
return height - newpos return height - newpos
}
}
)
const codeParser = new CodeParser(new AstWalker())
const solidityScript = new SolidityScript()
this.notification = new NotificationPlugin()
const configPlugin = new ConfigPlugin()
this.layout = new Layout()
const permissionHandler = new PermissionHandlerPlugin()
this.engine.register([
permissionHandler,
this.layout,
this.notification,
this.gistHandler,
configPlugin,
blockchain,
contentImport,
this.themeModule,
this.localeModule,
editor,
fileManager,
compilerMetadataGenerator,
compilersArtefacts,
networkModule,
offsetToLineColumnConverter,
codeParser,
fileDecorator,
codeFormat,
terminal,
web3Provider,
compileAndRun,
fetchAndCompile,
dGitProvider,
storagePlugin,
vmProviderShanghai,
vmProviderMerge,
vmProviderBerlin,
vmProviderLondon,
vmProviderSepoliaFork,
vmProviderGoerliFork,
vmProviderMainnetFork,
vmProviderCustomFork,
hardhatProvider,
ganacheProvider,
foundryProvider,
externalHttpProvider,
defaultInjectedProvider,
trustWalletInjectedProvider,
injected0ptimismProvider,
injectedArbitrumOneProvider,
this.walkthroughService,
search,
solidityumlgen,
contractFlattener,
solidityScript
])
// LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel()
Registry.getInstance().put({ api: this.mainview, name: 'mainview' })
const tabProxy = new TabProxy(fileManager, editor)
this.engine.register([appPanel, tabProxy])
// those views depend on app_manager
this.menuicons = new VerticalIcons()
this.sidePanel = new SidePanel()
this.hiddenPanel = new HiddenPanel()
const pluginManagerComponent = new PluginManagerComponent(
appManager,
this.engine
)
const filePanel = new FilePanel(appManager)
const landingPage = new LandingPage(
appManager,
this.menuicons,
fileManager,
filePanel,
contentImport
)
this.settings = new SettingsTab(
Registry.getInstance().get('config').api,
editor,
appManager
)
this.engine.register([
this.menuicons,
landingPage,
this.hiddenPanel,
this.sidePanel,
filePanel,
pluginManagerComponent,
this.settings
])
// CONTENT VIEWS & DEFAULT PLUGINS
const openZeppelinProxy = new OpenZeppelinProxy(blockchain)
const linkLibraries = new LinkLibraries(blockchain)
const deployLibraries = new DeployLibraries(blockchain)
const compileTab = new CompileTab(
Registry.getInstance().get('config').api,
Registry.getInstance().get('filemanager').api
)
const run = new RunTab(
blockchain,
Registry.getInstance().get('config').api,
Registry.getInstance().get('filemanager').api,
Registry.getInstance().get('editor').api,
filePanel,
Registry.getInstance().get('compilersartefacts').api,
networkModule,
Registry.getInstance().get('fileproviders/browser').api
)
const analysis = new AnalysisTab()
const debug = new DebuggerTab()
const test = new TestTab(
Registry.getInstance().get('filemanager').api,
Registry.getInstance().get('offsettolinecolumnconverter').api,
filePanel,
compileTab,
appManager,
contentImport
)
this.engine.register([
compileTab,
run,
debug,
analysis,
test,
filePanel.remixdHandle,
filePanel.hardhatHandle,
filePanel.foundryHandle,
filePanel.truffleHandle,
filePanel.slitherHandle,
linkLibraries,
deployLibraries,
openZeppelinProxy,
run.recorder
])
this.layout.panels = {
tabs: { plugin: tabProxy, active: true },
editor: { plugin: editor, active: true },
main: { plugin: appPanel, active: false },
terminal: { plugin: terminal, active: true, minimized: false }
} }
}
)
const codeParser = new CodeParser(new AstWalker())
const solidityScript = new SolidityScript()
this.notification = new NotificationPlugin()
const configPlugin = new ConfigPlugin()
this.layout = new Layout()
const permissionHandler = new PermissionHandlerPlugin()
this.engine.register([
permissionHandler,
this.layout,
this.notification,
this.gistHandler,
configPlugin,
blockchain,
contentImport,
this.themeModule,
this.localeModule,
editor,
fileManager,
compilerMetadataGenerator,
compilersArtefacts,
networkModule,
offsetToLineColumnConverter,
codeParser,
fileDecorator,
codeFormat,
terminal,
web3Provider,
compileAndRun,
fetchAndCompile,
dGitProvider,
storagePlugin,
vmProviderShanghai,
vmProviderMerge,
vmProviderBerlin,
vmProviderLondon,
vmProviderSepoliaFork,
vmProviderGoerliFork,
vmProviderMainnetFork,
vmProviderCustomFork,
hardhatProvider,
ganacheProvider,
foundryProvider,
externalHttpProvider,
defaultInjectedProvider,
trustWalletInjectedProvider,
injected0ptimismProvider,
injectedArbitrumOneProvider,
this.walkthroughService,
search,
solidityumlgen,
contractFlattener,
solidityScript
])
// LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel()
Registry.getInstance().put({ api: this.mainview, name: 'mainview' })
const tabProxy = new TabProxy(fileManager, editor)
this.engine.register([appPanel, tabProxy])
// those views depend on app_manager
this.menuicons = new VerticalIcons()
this.sidePanel = new SidePanel()
this.hiddenPanel = new HiddenPanel()
const pluginManagerComponent = new PluginManagerComponent(
appManager,
this.engine
)
const filePanel = new FilePanel(appManager)
const landingPage = new LandingPage(
appManager,
this.menuicons,
fileManager,
filePanel,
contentImport
)
this.settings = new SettingsTab(
Registry.getInstance().get('config').api,
editor,
appManager
)
this.engine.register([
this.menuicons,
landingPage,
this.hiddenPanel,
this.sidePanel,
filePanel,
pluginManagerComponent,
this.settings
])
// CONTENT VIEWS & DEFAULT PLUGINS
const openZeppelinProxy = new OpenZeppelinProxy(blockchain)
const linkLibraries = new LinkLibraries(blockchain)
const deployLibraries = new DeployLibraries(blockchain)
const compileTab = new CompileTab(
Registry.getInstance().get('config').api,
Registry.getInstance().get('filemanager').api
)
const run = new RunTab(
blockchain,
Registry.getInstance().get('config').api,
Registry.getInstance().get('filemanager').api,
Registry.getInstance().get('editor').api,
filePanel,
Registry.getInstance().get('compilersartefacts').api,
networkModule,
Registry.getInstance().get('fileproviders/browser').api
)
const analysis = new AnalysisTab()
const debug = new DebuggerTab()
const test = new TestTab(
Registry.getInstance().get('filemanager').api,
Registry.getInstance().get('offsettolinecolumnconverter').api,
filePanel,
compileTab,
appManager,
contentImport
)
this.engine.register([
compileTab,
run,
debug,
analysis,
test,
filePanel.remixdHandle,
filePanel.hardhatHandle,
filePanel.foundryHandle,
filePanel.truffleHandle,
filePanel.slitherHandle,
linkLibraries,
deployLibraries,
openZeppelinProxy,
run.recorder
])
this.layout.panels = {
tabs: { plugin: tabProxy, active: true },
editor: { plugin: editor, active: true },
main: { plugin: appPanel, active: false },
terminal: { plugin: terminal, active: true, minimized: false }
} }
}
async activate() { async activate() {
const queryParams = new QueryParams() const queryParams = new QueryParams()
const params = queryParams.get() const params = queryParams.get()
try { try {
this.engine.register(await this.appManager.registeredPlugins()) this.engine.register(await this.appManager.registeredPlugins())
} catch (e) { } catch (e) {
console.log("couldn't register iframe plugins", e.message) console.log("couldn't register iframe plugins", e.message)
} }
await this.appManager.activatePlugin(['layout']) await this.appManager.activatePlugin(['layout'])
await this.appManager.activatePlugin(['notification']) await this.appManager.activatePlugin(['notification'])
await this.appManager.activatePlugin(['editor']) await this.appManager.activatePlugin(['editor'])
await this.appManager.activatePlugin(['permissionhandler', 'theme', 'locale', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter']) await this.appManager.activatePlugin(['permissionhandler', 'theme', 'locale', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
await this.appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs']) await this.appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs'])
await this.appManager.activatePlugin(['sidePanel']) // activating host plugin separately await this.appManager.activatePlugin(['sidePanel']) // activating host plugin separately
await this.appManager.activatePlugin(['home']) await this.appManager.activatePlugin(['home'])
await this.appManager.activatePlugin(['settings', 'config']) await this.appManager.activatePlugin(['settings', 'config'])
await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'codeFormatter', 'fileDecorator', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'codeFormatter', 'fileDecorator', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler'])
await this.appManager.activatePlugin(['settings']) await this.appManager.activatePlugin(['settings'])
await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder']) await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder'])
await this.appManager.activatePlugin(['solidity-script']) await this.appManager.activatePlugin(['solidity-script'])
this.appManager.on( this.appManager.on(
'filePanel', 'filePanel',
'workspaceInitializationCompleted', 'workspaceInitializationCompleted',
async () => { async () => {
// for e2e tests // for e2e tests
const loadedElement = document.createElement('span') const loadedElement = document.createElement('span')
loadedElement.setAttribute('data-id', 'workspaceloaded') loadedElement.setAttribute('data-id', 'workspaceloaded')
document.body.appendChild(loadedElement) document.body.appendChild(loadedElement)
await this.appManager.registerContextMenuItems() await this.appManager.registerContextMenuItems()
}
)
await this.appManager.activatePlugin(['filePanel'])
// Set workspace after initial activation
this.appManager.on('editor', 'editorMounted', () => {
if (Array.isArray(this.workspace)) {
this.appManager
.activatePlugin(this.workspace)
.then(async () => {
try {
if (params.deactivate) {
await this.appManager.deactivatePlugin(
params.deactivate.split(',')
)
}
} catch (e) {
console.log(e)
} }
) if (params.code && (!params.activate || params.activate.split(',').includes('solidity'))) {
// if code is given in url we focus on solidity plugin
await this.appManager.activatePlugin(['filePanel']) this.menuicons.select('solidity')
// Set workspace after initial activation } else {
this.appManager.on('editor', 'editorMounted', () => { // If plugins are loaded from the URL params, we focus on the last one.
if (Array.isArray(this.workspace)) { if (
this.appManager this.appManager.pluginLoader.current === 'queryParams' &&
.activatePlugin(this.workspace)
.then(async () => {
try {
if (params.deactivate) {
await this.appManager.deactivatePlugin(
params.deactivate.split(',')
)
}
} catch (e) {
console.log(e)
}
if (params.code && (!params.activate || params.activate.split(',').includes('solidity'))) {
// if code is given in url we focus on solidity plugin
this.menuicons.select('solidity')
} else {
// If plugins are loaded from the URL params, we focus on the last one.
if (
this.appManager.pluginLoader.current === 'queryParams' &&
this.workspace.length > 0 this.workspace.length > 0
) { ) {
this.menuicons.select(this.workspace[this.workspace.length - 1]) this.menuicons.select(this.workspace[this.workspace.length - 1])
} else { } else {
this.appManager.call('tabs', 'focus', 'home') this.appManager.call('tabs', 'focus', 'home')
} }
}
if (params.call) {
const callDetails = params.call.split('//')
if (callDetails.length > 1) {
this.appManager.call('notification', 'toast', `initiating ${callDetails[0]} and calling "${callDetails[1]}" ...`)
// @todo(remove the timeout when activatePlugin is on 0.3.0)
await this.appManager.call(...callDetails).catch(console.error)
}
}
if (params.calls) {
const calls = params.calls.split("///");
// call all functions in the list, one after the other
for (const call of calls) {
const callDetails = call.split("//");
if (callDetails.length > 1) {
this.appManager.call(
"notification",
"toast",
`initiating ${callDetails[0]} and calling "${callDetails[1]}" ...`
);
// @todo(remove the timeout when activatePlugin is on 0.3.0)
try {
await this.appManager.call(...callDetails)
} catch (e) {
console.error(e)
}
}
}
}
})
.catch(console.error)
} }
const loadedElement = document.createElement('span')
loadedElement.setAttribute('data-id', 'apploaded')
document.body.appendChild(loadedElement)
})
// activate solidity plugin if (params.call) {
this.appManager.activatePlugin(['solidity', 'udapp', 'deploy-libraries', 'link-libraries', 'openzeppelin-proxy']) const callDetails = params.call.split('//')
} if (callDetails.length > 1) {
this.appManager.call('notification', 'toast', `initiating ${callDetails[0]} and calling "${callDetails[1]}" ...`)
// @todo(remove the timeout when activatePlugin is on 0.3.0)
await this.appManager.call(...callDetails).catch(console.error)
}
}
if (params.calls) {
const calls = params.calls.split("///");
// call all functions in the list, one after the other
for (const call of calls) {
const callDetails = call.split("//");
if (callDetails.length > 1) {
this.appManager.call(
"notification",
"toast",
`initiating ${callDetails[0]} and calling "${callDetails[1]}" ...`
);
// @todo(remove the timeout when activatePlugin is on 0.3.0)
try {
await this.appManager.call(...callDetails)
} catch (e) {
console.error(e)
}
}
}
}
})
.catch(console.error)
}
const loadedElement = document.createElement('span')
loadedElement.setAttribute('data-id', 'apploaded')
document.body.appendChild(loadedElement)
})
// activate solidity plugin
this.appManager.activatePlugin(['solidity', 'udapp', 'deploy-libraries', 'link-libraries', 'openzeppelin-proxy'])
}
} }
export default AppComponent export default AppComponent

@ -6,45 +6,45 @@ import { RemixPluginPanel } from '@remix-ui/panel'
import { PluginViewWrapper } from '@remix-ui/helper' import { PluginViewWrapper } from '@remix-ui/helper'
const profile = { const profile = {
name: 'hiddenPanel', name: 'hiddenPanel',
displayName: 'Hidden Panel', displayName: 'Hidden Panel',
description: 'Remix IDE hidden panel', description: 'Remix IDE hidden panel',
version: packageJson.version, version: packageJson.version,
methods: ['addView', 'removeView'] methods: ['addView', 'removeView']
} }
export class HiddenPanel extends AbstractPanel { export class HiddenPanel extends AbstractPanel {
el: HTMLElement el: HTMLElement
dispatch: React.Dispatch<any> = () => {} dispatch: React.Dispatch<any> = () => {}
constructor () { constructor () {
super(profile) super(profile)
this.el = document.createElement('div') this.el = document.createElement('div')
this.el.setAttribute('class', 'pluginsContainer') this.el.setAttribute('class', 'pluginsContainer')
} }
addView (profile: any, view: any): void { addView (profile: any, view: any): void {
super.removeView(profile) super.removeView(profile)
super.addView(profile, view) super.addView(profile, view)
this.renderComponent() this.renderComponent()
} }
updateComponent (state: any) { updateComponent (state: any) {
return <RemixPluginPanel header={<></>} plugins={state.plugins}/> return <RemixPluginPanel header={<></>} plugins={state.plugins}/>
} }
setDispatch (dispatch: React.Dispatch<any>) { setDispatch (dispatch: React.Dispatch<any>) {
this.dispatch = dispatch this.dispatch = dispatch
} }
render() { render() {
return ( return (
<div className='pluginsContainer'><PluginViewWrapper plugin={this} /></div> <div className='pluginsContainer'><PluginViewWrapper plugin={this} /></div>
); );
} }
renderComponent () { renderComponent () {
this.dispatch({ this.dispatch({
plugins: this.plugins, plugins: this.plugins,
}) })
} }
} }

@ -5,64 +5,64 @@ import packageJson from '../../../../../package.json'
import { PluginViewWrapper } from '@remix-ui/helper' import { PluginViewWrapper } from '@remix-ui/helper'
const profile = { const profile = {
name: 'mainPanel', name: 'mainPanel',
displayName: 'Main Panel', displayName: 'Main Panel',
description: 'Remix IDE main panel', description: 'Remix IDE main panel',
version: packageJson.version, version: packageJson.version,
methods: ['addView', 'removeView', 'showContent'] methods: ['addView', 'removeView', 'showContent']
} }
export class MainPanel extends AbstractPanel { export class MainPanel extends AbstractPanel {
element: HTMLDivElement element: HTMLDivElement
dispatch: React.Dispatch<any> = () => {} dispatch: React.Dispatch<any> = () => {}
constructor (config) { constructor (config) {
super(profile) super(profile)
this.element = document.createElement('div') this.element = document.createElement('div')
this.element.setAttribute('data-id', 'mainPanelPluginsContainer') this.element.setAttribute('data-id', 'mainPanelPluginsContainer')
this.element.setAttribute('style', 'height: 100%; width: 100%;') this.element.setAttribute('style', 'height: 100%; width: 100%;')
// this.config = config // this.config = config
} }
setDispatch (dispatch: React.Dispatch<any>) { setDispatch (dispatch: React.Dispatch<any>) {
this.dispatch = dispatch this.dispatch = dispatch
} }
onActivation () { onActivation () {
this.renderComponent() this.renderComponent()
} }
focus (name) { focus (name) {
this.emit('focusChanged', name) this.emit('focusChanged', name)
super.focus(name) super.focus(name)
this.renderComponent() this.renderComponent()
} }
addView (profile, view) { addView (profile, view) {
super.addView(profile, view) super.addView(profile, view)
this.renderComponent() this.renderComponent()
} }
removeView (profile) { removeView (profile) {
super.removeView(profile) super.removeView(profile)
this.renderComponent() this.renderComponent()
} }
async showContent (name) { async showContent (name) {
super.showContent(name) super.showContent(name)
this.renderComponent() this.renderComponent()
} }
renderComponent () { renderComponent () {
this.dispatch({ this.dispatch({
plugins: this.plugins plugins: this.plugins
}) })
} }
render() { render() {
return <div style={{height: '100%', width: '100%'}} data-id='mainPanelPluginsContainer'><PluginViewWrapper plugin={this} /></div> return <div style={{height: '100%', width: '100%'}} data-id='mainPanelPluginsContainer'><PluginViewWrapper plugin={this} /></div>
} }
updateComponent (state: any) { updateComponent (state: any) {
return <RemixPluginPanel header={<></>} plugins={state.plugins}/> return <RemixPluginPanel header={<></>} plugins={state.plugins}/>
} }
} }

@ -5,58 +5,58 @@ import { PluginRecord } from '@remix-ui/panel'
import EventManager from '../../lib/events' import EventManager from '../../lib/events'
export class AbstractPanel extends HostPlugin { export class AbstractPanel extends HostPlugin {
events: EventEmitter events: EventEmitter
event: any event: any
public plugins: Record<string, PluginRecord> = {} public plugins: Record<string, PluginRecord> = {}
constructor (profile) { constructor (profile) {
super(profile) super(profile)
this.events = new EventEmitter() this.events = new EventEmitter()
this.event = new EventManager() this.event = new EventManager()
} }
currentFocus (): string { currentFocus (): string {
return Object.values(this.plugins).find(plugin => { return Object.values(this.plugins).find(plugin => {
return plugin.active return plugin.active
}).profile.name }).profile.name
} }
addView (profile, view) { addView (profile, view) {
if (this.plugins[profile.name]) throw new Error(`Plugin ${profile.name} already rendered`) if (this.plugins[profile.name]) throw new Error(`Plugin ${profile.name} already rendered`)
this.plugins[profile.name] = { this.plugins[profile.name] = {
profile: profile, profile: profile,
view: view, view: view,
active: false, active: false,
class: 'plugItIn active' class: 'plugItIn active'
}
} }
}
removeView (profile) { removeView (profile) {
this.emit('pluginDisabled', profile.name) this.emit('pluginDisabled', profile.name)
this.call('menuicons', 'unlinkContent', profile) this.call('menuicons', 'unlinkContent', profile)
this.remove(profile.name) this.remove(profile.name)
} }
/** /**
* Remove a plugin from the panel * Remove a plugin from the panel
* @param {String} name The name of the plugin to remove * @param {String} name The name of the plugin to remove
*/ */
remove (name) { remove (name) {
delete this.plugins[name] delete this.plugins[name]
} }
/** /**
* Display the content of this specific plugin * Display the content of this specific plugin
* @param {String} name The name of the plugin to display the content * @param {String} name The name of the plugin to display the content
*/ */
showContent (name) { showContent (name) {
if (!this.plugins[name]) throw new Error(`Plugin ${name} is not yet activated`) if (!this.plugins[name]) throw new Error(`Plugin ${name} is not yet activated`)
Object.values(this.plugins).forEach(plugin => { Object.values(this.plugins).forEach(plugin => {
plugin.active = false plugin.active = false
}) })
this.plugins[name].active = true this.plugins[name].active = true
} }
focus (name) { focus (name) {
this.showContent(name) this.showContent(name)
} }
} }

@ -6,145 +6,145 @@ import { PluginViewWrapper } from '@remix-ui/helper'
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const profile = { const profile = {
name: 'pluginManager', name: 'pluginManager',
displayName: 'Plugin manager', displayName: 'Plugin manager',
methods: [], methods: [],
events: [], events: [],
icon: 'assets/img/pluginManager.webp', icon: 'assets/img/pluginManager.webp',
description: 'Start/stop services, modules and plugins', description: 'Start/stop services, modules and plugins',
kind: 'settings', kind: 'settings',
location: 'sidePanel', location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/plugin_manager.html', documentation: 'https://remix-ide.readthedocs.io/en/latest/plugin_manager.html',
version: packageJson.version, version: packageJson.version,
maintainedBy: "Remix" maintainedBy: "Remix"
} }
class PluginManagerComponent extends ViewPlugin { class PluginManagerComponent extends ViewPlugin {
constructor (appManager, engine) { constructor (appManager, engine) {
super(profile) super(profile)
this.appManager = appManager this.appManager = appManager
this.engine = engine this.engine = engine
this.htmlElement = document.createElement('div') this.htmlElement = document.createElement('div')
this.htmlElement.setAttribute('id', 'pluginManager') this.htmlElement.setAttribute('id', 'pluginManager')
this.filter = '' this.filter = ''
this.activePlugins = [] this.activePlugins = []
this.inactivePlugins = [] this.inactivePlugins = []
this.activeProfiles = this.appManager.actives this.activeProfiles = this.appManager.actives
this._paq = _paq this._paq = _paq
this.dispatch = null this.dispatch = null
this.listenOnEvent() this.listenOnEvent()
} }
/** /**
* Checks and returns true or false if plugin name * Checks and returns true or false if plugin name
* passed in exists in the actives string array in * passed in exists in the actives string array in
* RemixAppManager * RemixAppManager
* @param {string} name name of Plugin * @param {string} name name of Plugin
*/ */
isActive = (name) =>{ isActive = (name) =>{
return this.appManager.actives.includes(name) return this.appManager.actives.includes(name)
} }
/** /**
* Delegates to method activatePlugin in * Delegates to method activatePlugin in
* RemixAppManager to enable plugin activation * RemixAppManager to enable plugin activation
* @param {string} name name of Plugin * @param {string} name name of Plugin
*/ */
activateP = (name) => { activateP = (name) => {
this.appManager.activatePlugin(name) this.appManager.activatePlugin(name)
_paq.push(['trackEvent', 'manager', 'activate', name]) _paq.push(['trackEvent', 'manager', 'activate', name])
} }
/** /**
* Takes the name of a local plugin and does both * Takes the name of a local plugin and does both
* activation and registration * activation and registration
* @param {Profile} pluginName * @param {Profile} pluginName
* @returns {void} * @returns {void}
*/ */
activateAndRegisterLocalPlugin = async (localPlugin) => { activateAndRegisterLocalPlugin = async (localPlugin) => {
if (localPlugin) { if (localPlugin) {
this.engine.register(localPlugin) this.engine.register(localPlugin)
this.appManager.activatePlugin(localPlugin.profile.name) this.appManager.activatePlugin(localPlugin.profile.name)
this.getAndFilterPlugins() this.getAndFilterPlugins()
localStorage.setItem('plugins/local', JSON.stringify(localPlugin.profile)) localStorage.setItem('plugins/local', JSON.stringify(localPlugin.profile))
}
} }
}
/** /**
* Calls and triggers the event deactivatePlugin * Calls and triggers the event deactivatePlugin
* with with manager permission passing in the name * with with manager permission passing in the name
* of the plugin * of the plugin
* @param {string} name name of Plugin * @param {string} name name of Plugin
*/ */
deactivateP = (name) => { deactivateP = (name) => {
this.call('manager', 'deactivatePlugin', name) this.call('manager', 'deactivatePlugin', name)
_paq.push(['trackEvent', 'manager', 'deactivate', name]) _paq.push(['trackEvent', 'manager', 'deactivate', name])
} }
setDispatch (dispatch) { setDispatch (dispatch) {
this.dispatch = dispatch this.dispatch = dispatch
this.renderComponent() this.renderComponent()
} }
updateComponent(state){ updateComponent(state){
return <RemixUiPluginManager return <RemixUiPluginManager
pluginComponent={state}/> pluginComponent={state}/>
} }
renderComponent () { renderComponent () {
if(this.dispatch) this.dispatch({...this, activePlugins: this.activePlugins, inactivePlugins: this.inactivePlugins}) if(this.dispatch) this.dispatch({...this, activePlugins: this.activePlugins, inactivePlugins: this.inactivePlugins})
} }
render () { render () {
return ( return (
<div id='pluginManager'><PluginViewWrapper plugin={this} /></div> <div id='pluginManager'><PluginViewWrapper plugin={this} /></div>
); );
} }
getAndFilterPlugins = (filter) => { getAndFilterPlugins = (filter) => {
this.filter = typeof filter === 'string' ? filter.toLowerCase() : this.filter this.filter = typeof filter === 'string' ? filter.toLowerCase() : this.filter
const isFiltered = (profile) => (profile.displayName ? profile.displayName : profile.name).toLowerCase().includes(this.filter) const isFiltered = (profile) => (profile.displayName ? profile.displayName : profile.name).toLowerCase().includes(this.filter)
const isNotRequired = (profile) => !this.appManager.isRequired(profile.name) const isNotRequired = (profile) => !this.appManager.isRequired(profile.name)
const isNotDependent = (profile) => !this.appManager.isDependent(profile.name) const isNotDependent = (profile) => !this.appManager.isDependent(profile.name)
const isNotHome = (profile) => profile.name !== 'home' const isNotHome = (profile) => profile.name !== 'home'
const sortByName = (profileA, profileB) => { const sortByName = (profileA, profileB) => {
const nameA = ((profileA.displayName) ? profileA.displayName : profileA.name).toUpperCase() const nameA = ((profileA.displayName) ? profileA.displayName : profileA.name).toUpperCase()
const nameB = ((profileB.displayName) ? profileB.displayName : profileB.name).toUpperCase() const nameB = ((profileB.displayName) ? profileB.displayName : profileB.name).toUpperCase()
return (nameA < nameB) ? -1 : (nameA > nameB) ? 1 : 0 return (nameA < nameB) ? -1 : (nameA > nameB) ? 1 : 0
}
const activatedPlugins = []
const deactivatedPlugins = []
const tempArray = this.appManager.getAll()
.filter(isFiltered)
.filter(isNotRequired)
.filter(isNotDependent)
.filter(isNotHome)
.sort(sortByName)
tempArray.forEach(profile => {
if (this.appManager.actives.includes(profile.name)) {
activatedPlugins.push(profile)
} else {
deactivatedPlugins.push(profile)
}
})
this.activePlugins = activatedPlugins
this.inactivePlugins = deactivatedPlugins
this.renderComponent()
}
listenOnEvent () {
this.engine.event.on('onRegistration', () => this.renderComponent())
this.appManager.event.on('activate', () => {
this.getAndFilterPlugins()
})
this.appManager.event.on('deactivate', () => {
this.getAndFilterPlugins()
})
} }
const activatedPlugins = []
const deactivatedPlugins = []
const tempArray = this.appManager.getAll()
.filter(isFiltered)
.filter(isNotRequired)
.filter(isNotDependent)
.filter(isNotHome)
.sort(sortByName)
tempArray.forEach(profile => {
if (this.appManager.actives.includes(profile.name)) {
activatedPlugins.push(profile)
} else {
deactivatedPlugins.push(profile)
}
})
this.activePlugins = activatedPlugins
this.inactivePlugins = deactivatedPlugins
this.renderComponent()
}
listenOnEvent () {
this.engine.event.on('onRegistration', () => this.renderComponent())
this.appManager.event.on('activate', () => {
this.getAndFilterPlugins()
})
this.appManager.event.on('deactivate', () => {
this.getAndFilterPlugins()
})
}
} }
module.exports = PluginManagerComponent module.exports = PluginManagerComponent

@ -11,134 +11,134 @@ const _paq = window._paq = window._paq || []
export const Preload = () => { export const Preload = () => {
const [supported, setSupported] = useState<boolean>(true) const [supported, setSupported] = useState<boolean>(true)
const [error, setError] = useState<boolean>(false) const [error, setError] = useState<boolean>(false)
const [showDownloader, setShowDownloader] = useState<boolean>(false) const [showDownloader, setShowDownloader] = useState<boolean>(false)
const remixFileSystems = useRef<fileSystems>(new fileSystems()) const remixFileSystems = useRef<fileSystems>(new fileSystems())
const remixIndexedDB = useRef<fileSystem>(new indexedDBFileSystem()) const remixIndexedDB = useRef<fileSystem>(new indexedDBFileSystem())
const localStorageFileSystem = useRef<fileSystem>(new localStorageFS()) const localStorageFileSystem = useRef<fileSystem>(new localStorageFS())
// url parameters to e2e test the fallbacks and error warnings // 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 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 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:') 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() { function loadAppComponent() {
import('../../app').then((AppComponent) => { import('../../app').then((AppComponent) => {
const appComponent = new AppComponent.default() const appComponent = new AppComponent.default()
appComponent.run().then(() => { appComponent.run().then(() => {
render( render(
<> <>
<RemixApp app={appComponent} /> <RemixApp app={appComponent} />
</>, </>,
document.getElementById('root') document.getElementById('root')
) )
}) })
}).catch(err => { }).catch(err => {
_paq.push(['trackEvent', 'Preload', 'error', err && err.message]) _paq.push(['trackEvent', 'Preload', 'error', err && err.message])
console.error('Error loading Remix:', err) console.error('Error loading Remix:', err)
setError(true) setError(true)
}) })
} }
const downloadBackup = async () => { const downloadBackup = async () => {
setShowDownloader(false) setShowDownloader(false)
const fsUtility = new fileSystemUtility() const fsUtility = new fileSystemUtility()
await fsUtility.downloadBackup(remixFileSystems.current.fileSystems['localstorage']) await fsUtility.downloadBackup(remixFileSystems.current.fileSystems['localstorage'])
await migrateAndLoad() await migrateAndLoad()
} }
const migrateAndLoad = async () => { const migrateAndLoad = async () => {
setShowDownloader(false) setShowDownloader(false)
const fsUtility = new fileSystemUtility() const fsUtility = new fileSystemUtility()
const migrationResult = await fsUtility.migrate(localStorageFileSystem.current, remixIndexedDB.current) const migrationResult = await fsUtility.migrate(localStorageFileSystem.current, remixIndexedDB.current)
_paq.push(['trackEvent', 'Migrate', 'result', migrationResult?'success' : 'fail']) _paq.push(['trackEvent', 'Migrate', 'result', migrationResult?'success' : 'fail'])
await setFileSystems() await setFileSystems()
} }
const setFileSystems = async() => { const setFileSystems = async() => {
const fsLoaded = await remixFileSystems.current.setFileSystem([(testmigrationFallback.current || testBlockStorage.current)? null: remixIndexedDB.current, testBlockStorage.current? null:localStorageFileSystem.current]) const fsLoaded = await remixFileSystems.current.setFileSystem([(testmigrationFallback.current || testBlockStorage.current)? null: remixIndexedDB.current, testBlockStorage.current? null:localStorageFileSystem.current])
if (fsLoaded) { if (fsLoaded) {
console.log(fsLoaded.name + ' activated') console.log(fsLoaded.name + ' activated')
_paq.push(['trackEvent', 'Storage', 'activate', fsLoaded.name]) _paq.push(['trackEvent', 'Storage', 'activate', fsLoaded.name])
loadAppComponent() loadAppComponent()
} else { } else {
_paq.push(['trackEvent', 'Storage', 'error', 'no supported storage']) _paq.push(['trackEvent', 'Storage', 'error', 'no supported storage'])
setSupported(false) setSupported(false)
}
} }
}
const testmigration = async() => { const testmigration = async() => {
if (testmigrationResult.current) { if (testmigrationResult.current) {
const fsUtility = new fileSystemUtility() const fsUtility = new fileSystemUtility()
fsUtility.populateWorkspace(migrationTestData, remixFileSystems.current.fileSystems['localstorage'].fs) fsUtility.populateWorkspace(migrationTestData, remixFileSystems.current.fileSystems['localstorage'].fs)
}
} }
}
useEffect(() => { useEffect(() => {
async function loadStorage() { async function loadStorage() {
await remixFileSystems.current.addFileSystem(remixIndexedDB.current) || _paq.push(['trackEvent', 'Storage', 'error', 'indexedDB not supported']) 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 remixFileSystems.current.addFileSystem(localStorageFileSystem.current) || _paq.push(['trackEvent', 'Storage', 'error', 'localstorage not supported'])
await testmigration() await testmigration()
remixIndexedDB.current.loaded && await remixIndexedDB.current.checkWorkspaces() remixIndexedDB.current.loaded && await remixIndexedDB.current.checkWorkspaces()
localStorageFileSystem.current.loaded && await localStorageFileSystem.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 && ( (remixIndexedDB.current.hasWorkSpaces || !localStorageFileSystem.current.hasWorkSpaces)? await setFileSystems():setShowDownloader(true))
!remixIndexedDB.current.loaded && await setFileSystems() !remixIndexedDB.current.loaded && await setFileSystems()
} }
loadStorage() loadStorage()
}, []) }, [])
return <> return <>
<div className='preload-container'> <div className='preload-container'>
<div className='preload-logo pb-4'> <div className='preload-logo pb-4'>
{logo} {logo}
<div className="info-secondary splash"> <div className="info-secondary splash">
REMIX IDE REMIX IDE
<br /> <br />
<span className='version'> v{packageJson.version}</span> <span className='version'> v{packageJson.version}</span>
</div> </div>
</div> </div>
{!supported ? {!supported ?
<div className='preload-info-container alert alert-warning'> <div className='preload-info-container alert alert-warning'>
Your browser does not support any of the filesystems required by Remix. Your browser does not support any of the filesystems required by Remix.
Either change the settings in your browser or use a supported browser. Either change the settings in your browser or use a supported browser.
</div> : null} </div> : null}
{error ? {error ?
<div className='preload-info-container alert alert-danger text-left'> <div className='preload-info-container alert alert-danger text-left'>
An unknown error has occurred while loading the application.<br></br> An unknown error has occurred while loading the application.<br></br>
Doing a hard refresh might fix this issue:<br></br> Doing a hard refresh might fix this issue:<br></br>
<div className='pt-2'> <div className='pt-2'>
Windows:<br></br> Windows:<br></br>
- Chrome: CTRL + F5 or CTRL + Reload Button<br></br> - Chrome: CTRL + F5 or CTRL + Reload Button<br></br>
- Firefox: CTRL + SHIFT + R or CTRL + F5<br></br> - Firefox: CTRL + SHIFT + R or CTRL + F5<br></br>
</div> </div>
<div className='pt-2'> <div className='pt-2'>
MacOS:<br></br> MacOS:<br></br>
- Chrome & FireFox: CMD + SHIFT + R or SHIFT + Reload Button<br></br> - Chrome & FireFox: CMD + SHIFT + R or SHIFT + Reload Button<br></br>
</div> </div>
<div className='pt-2'> <div className='pt-2'>
Linux:<br></br> Linux:<br></br>
- Chrome & FireFox: CTRL + SHIFT + R<br></br> - Chrome & FireFox: CTRL + SHIFT + R<br></br>
</div> </div>
</div> : null} </div> : null}
{showDownloader ? {showDownloader ?
<div className='preload-info-container alert alert-info'> <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. 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> <br></br>
You don't need to do anything else, your files will be available when the app loads. 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 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 onClick={async () => { await migrateAndLoad() }} data-id='skipbackup-btn' className='btn btn-primary mt-1'>skip backup</div>
</div> : null} </div> : null}
{(supported && !error && !showDownloader) ? {(supported && !error && !showDownloader) ?
<div> <div>
<i className="fas fa-spinner fa-spin fa-2x"></i> <i className="fas fa-spinner fa-spin fa-2x"></i>
</div> : null} </div> : null}
</div> </div>
</> </>
} }
const logo = <svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 105 100"> 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="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="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" /> <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> </svg>

@ -8,94 +8,94 @@ import { PluginViewWrapper } from '@remix-ui/helper'
// const csjs = require('csjs-inject') // const csjs = require('csjs-inject')
const sidePanel = { const sidePanel = {
name: 'sidePanel', name: 'sidePanel',
displayName: 'Side Panel', displayName: 'Side Panel',
description: 'Remix IDE side panel', description: 'Remix IDE side panel',
version: packageJson.version, version: packageJson.version,
methods: ['addView', 'removeView', 'currentFocus'] methods: ['addView', 'removeView', 'currentFocus']
} }
export class SidePanel extends AbstractPanel { export class SidePanel extends AbstractPanel {
sideelement: any sideelement: any
dispatch: React.Dispatch<any> = () => {} dispatch: React.Dispatch<any> = () => {}
constructor() { constructor() {
super(sidePanel) super(sidePanel)
this.sideelement = document.createElement('section') this.sideelement = document.createElement('section')
this.sideelement.setAttribute('class', 'panel plugin-manager') this.sideelement.setAttribute('class', 'panel plugin-manager')
} }
onActivation() { onActivation() {
this.renderComponent() this.renderComponent()
// Toggle content // Toggle content
this.on('menuicons', 'toggleContent', (name) => { this.on('menuicons', 'toggleContent', (name) => {
if (!this.plugins[name]) return if (!this.plugins[name]) return
if (this.plugins[name].active) { if (this.plugins[name].active) {
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)
this.emit('toggle', name) this.emit('toggle', name)
this.events.emit('toggle', name) this.events.emit('toggle', name)
return return
} }
this.showContent(name) this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name) this.emit('showing', name)
this.events.emit('showing', name) this.events.emit('showing', name)
}) })
// Force opening // Force opening
this.on('menuicons', 'showContent', (name) => { this.on('menuicons', 'showContent', (name) => {
if (!this.plugins[name]) return if (!this.plugins[name]) return
this.showContent(name) this.showContent(name)
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)
this.emit('showing', name) this.emit('showing', name)
this.events.emit('showing', name) this.events.emit('showing', name)
}) })
} }
focus(name) { focus(name) {
this.emit('focusChanged', name) this.emit('focusChanged', name)
super.focus(name) super.focus(name)
} }
removeView(profile) { removeView(profile) {
if (this.plugins[profile.name].active) this.call('menuicons', 'select', 'filePanel') if (this.plugins[profile.name].active) this.call('menuicons', 'select', 'filePanel')
super.removeView(profile) super.removeView(profile)
this.emit('pluginDisabled', profile.name) this.emit('pluginDisabled', profile.name)
this.call('menuicons', 'unlinkContent', profile) this.call('menuicons', 'unlinkContent', profile)
this.renderComponent() this.renderComponent()
} }
addView(profile, view) { addView(profile, view) {
super.addView(profile, view) super.addView(profile, view)
this.call('menuicons', 'linkContent', profile) this.call('menuicons', 'linkContent', profile)
this.renderComponent() this.renderComponent()
} }
/** /**
* Display content and update the header * Display content and update the header
* @param {String} name The name of the plugin to display * @param {String} name The name of the plugin to display
*/ */
async showContent(name) { async showContent(name) {
super.showContent(name) super.showContent(name)
this.emit('focusChanged', name) this.emit('focusChanged', name)
this.renderComponent() this.renderComponent()
} }
setDispatch (dispatch: React.Dispatch<any>) { setDispatch (dispatch: React.Dispatch<any>) {
this.dispatch = dispatch this.dispatch = dispatch
} }
render() { render() {
return ( return (
<section className='panel plugin-manager'> <PluginViewWrapper plugin={this} /></section> <section className='panel plugin-manager'> <PluginViewWrapper plugin={this} /></section>
); );
} }
updateComponent(state: any) { updateComponent(state: any) {
return <RemixPluginPanel header={<RemixUIPanelHeader plugins={state.plugins}></RemixUIPanelHeader>} plugins={state.plugins} /> return <RemixPluginPanel header={<RemixUIPanelHeader plugins={state.plugins}></RemixUIPanelHeader>} plugins={state.plugins} />
} }
renderComponent() { renderComponent() {
this.dispatch({ this.dispatch({
plugins: this.plugins plugins: this.plugins
}) })
} }
} }

@ -8,119 +8,119 @@ import { Profile } from '@remixproject/plugin-utils'
import { PluginViewWrapper } from '@remix-ui/helper' import { PluginViewWrapper } from '@remix-ui/helper'
const profile = { const profile = {
name: 'menuicons', name: 'menuicons',
displayName: 'Vertical Icons', displayName: 'Vertical Icons',
description: 'Remix IDE vertical icons', description: 'Remix IDE vertical icons',
version: packageJson.version, version: packageJson.version,
methods: ['select', 'unlinkContent', 'linkContent'], methods: ['select', 'unlinkContent', 'linkContent'],
events: ['toggleContent', 'showContent'] events: ['toggleContent', 'showContent']
} }
export class VerticalIcons extends Plugin { export class VerticalIcons extends Plugin {
events: EventEmitter events: EventEmitter
htmlElement: HTMLDivElement htmlElement: HTMLDivElement
icons: Record<string, IconRecord> = {} icons: Record<string, IconRecord> = {}
dispatch: React.Dispatch<any> = () => {} dispatch: React.Dispatch<any> = () => {}
constructor () { constructor () {
super(profile) super(profile)
this.events = new EventEmitter() this.events = new EventEmitter()
this.htmlElement = document.createElement('div') this.htmlElement = document.createElement('div')
this.htmlElement.setAttribute('id', 'icon-panel') this.htmlElement.setAttribute('id', 'icon-panel')
}
renderComponent () {
const fixedOrder = ['filePanel', 'search', 'solidity','udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager']
const divived = Object.values(this.icons).map((value) => { return {
...value,
isRequired: fixedOrder.indexOf(value.profile.name) > -1
}}).sort((a,b) => {
return a.timestamp - b.timestamp
})
const required = divived.filter((value) => value.isRequired).sort((a,b) => {
return fixedOrder.indexOf(a.profile.name) - fixedOrder.indexOf(b.profile.name)
})
const sorted: IconRecord[] = [
...required,
...divived.filter((value) => { return !value.isRequired })
]
this.dispatch({
verticalIconsPlugin: this,
icons: sorted
})
}
setDispatch (dispatch: React.Dispatch<any>) {
this.dispatch = dispatch
}
onActivation () {
this.renderComponent()
this.on('sidePanel', 'focusChanged', (name: string) => {
Object.keys(this.icons).map((o) => {
this.icons[o].active = false
})
this.icons[name].active = true
this.renderComponent()
})
}
async linkContent (profile: Profile) {
if (!profile.icon) return
if (!profile.kind) profile.kind = 'none'
this.icons[profile.name] = {
profile: profile,
active: false,
canbeDeactivated: await this.call('manager', 'canDeactivate', this.profile, profile),
timestamp: Date.now()
} }
this.renderComponent()
}
renderComponent () { unlinkContent (profile: Profile) {
const fixedOrder = ['filePanel', 'search', 'solidity','udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager'] delete this.icons[profile.name]
this.renderComponent()
}
const divived = Object.values(this.icons).map((value) => { return { async activateHome() {
...value, await this.call('manager', 'activatePlugin', 'home')
isRequired: fixedOrder.indexOf(value.profile.name) > -1 await this.call('tabs', 'focus', 'home')
}}).sort((a,b) => { }
return a.timestamp - b.timestamp
})
const required = divived.filter((value) => value.isRequired).sort((a,b) => { /**
return fixedOrder.indexOf(a.profile.name) - fixedOrder.indexOf(b.profile.name)
})
const sorted: IconRecord[] = [
...required,
...divived.filter((value) => { return !value.isRequired })
]
this.dispatch({
verticalIconsPlugin: this,
icons: sorted
})
}
setDispatch (dispatch: React.Dispatch<any>) {
this.dispatch = dispatch
}
onActivation () {
this.renderComponent()
this.on('sidePanel', 'focusChanged', (name: string) => {
Object.keys(this.icons).map((o) => {
this.icons[o].active = false
})
this.icons[name].active = true
this.renderComponent()
})
}
async linkContent (profile: Profile) {
if (!profile.icon) return
if (!profile.kind) profile.kind = 'none'
this.icons[profile.name] = {
profile: profile,
active: false,
canbeDeactivated: await this.call('manager', 'canDeactivate', this.profile, profile),
timestamp: Date.now()
}
this.renderComponent()
}
unlinkContent (profile: Profile) {
delete this.icons[profile.name]
this.renderComponent()
}
async activateHome() {
await this.call('manager', 'activatePlugin', 'home')
await this.call('tabs', 'focus', 'home')
}
/**
* Set an icon as active * Set an icon as active
* @param {string} name Name of profile of the module to activate * @param {string} name Name of profile of the module to activate
*/ */
select (name: string) { select (name: string) {
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)
this.emit('showContent', name) this.emit('showContent', name)
this.events.emit('showContent', name) this.events.emit('showContent', name)
} }
/** /**
* Toggles the side panel for plugin * Toggles the side panel for plugin
* @param {string} name Name of profile of the module to activate * @param {string} name Name of profile of the module to activate
*/ */
toggle (name: string) { toggle (name: string) {
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)
this.emit('toggleContent', name) this.emit('toggleContent', name)
this.events.emit('toggleContent', name) this.events.emit('toggleContent', name)
} }
updateComponent(state: any){ updateComponent(state: any){
return <RemixUiVerticalIconsPanel return <RemixUiVerticalIconsPanel
verticalIconsPlugin={state.verticalIconsPlugin} verticalIconsPlugin={state.verticalIconsPlugin}
icons={state.icons} icons={state.icons}
/> />
} }
render() { render() {
return ( return (
<div id='icon-panel'><PluginViewWrapper plugin={this} /></div> <div id='icon-panel'><PluginViewWrapper plugin={this} /></div>
); );
} }
} }

@ -9,448 +9,448 @@ import { PluginViewWrapper } from '@remix-ui/helper'
const EventManager = require('../../lib/events') const EventManager = require('../../lib/events')
const profile = { const profile = {
displayName: 'Editor', displayName: 'Editor',
name: 'editor', name: 'editor',
description: 'service - editor', description: 'service - editor',
version: packageJson.version, version: packageJson.version,
methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addLineText', 'discardLineTexts', 'addAnnotation', 'gotoLine', 'revealRange', 'getCursorPosition', 'open', 'addModel','addErrorMarker', 'clearErrorMarkers', 'getText'], methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addLineText', 'discardLineTexts', 'addAnnotation', 'gotoLine', 'revealRange', 'getCursorPosition', 'open', 'addModel','addErrorMarker', 'clearErrorMarkers', 'getText'],
} }
class Editor extends Plugin { class Editor extends Plugin {
constructor () { constructor () {
super(profile) super(profile)
this._themes = { this._themes = {
light: 'light', light: 'light',
dark: 'vs-dark', dark: 'vs-dark',
remixDark: 'remix-dark' remixDark: 'remix-dark'
} }
this.registeredDecorations = { sourceAnnotationsPerFile: {}, markerPerFile: {}, lineTextPerFile: {} } this.registeredDecorations = { sourceAnnotationsPerFile: {}, markerPerFile: {}, lineTextPerFile: {} }
this.currentDecorations = { sourceAnnotationsPerFile: {}, markerPerFile: {}, lineTextPerFile: {} } this.currentDecorations = { sourceAnnotationsPerFile: {}, markerPerFile: {}, lineTextPerFile: {} }
// Init // Init
this.event = new EventManager() this.event = new EventManager()
this.sessions = {} this.sessions = {}
this.readOnlySessions = {} this.readOnlySessions = {}
this.previousInput = '' this.previousInput = ''
this.saveTimeout = null this.saveTimeout = null
this.emptySession = null this.emptySession = null
this.modes = { this.modes = {
sol: 'sol', sol: 'sol',
yul: 'sol', yul: 'sol',
mvir: 'move', mvir: 'move',
js: 'javascript', js: 'javascript',
py: 'python', py: 'python',
vy: 'python', vy: 'python',
zok: 'zokrates', zok: 'zokrates',
lex: 'lexon', lex: 'lexon',
txt: 'text', txt: 'text',
json: 'json', json: 'json',
abi: 'json', abi: 'json',
rs: 'rust', rs: 'rust',
cairo: 'cairo', cairo: 'cairo',
ts: 'typescript', ts: 'typescript',
move: 'move' move: 'move'
} }
this.activated = false this.activated = false
this.events = { this.events = {
onBreakPointAdded: (file, line) => this.triggerEvent('breakpointAdded', [file, line]), onBreakPointAdded: (file, line) => this.triggerEvent('breakpointAdded', [file, line]),
onBreakPointCleared: (file, line) => this.triggerEvent('breakpointCleared', [file, line]), onBreakPointCleared: (file, line) => this.triggerEvent('breakpointCleared', [file, line]),
onDidChangeContent: (file) => this._onChange(file), onDidChangeContent: (file) => this._onChange(file),
onEditorMounted: () => this.triggerEvent('editorMounted', []) onEditorMounted: () => this.triggerEvent('editorMounted', [])
} }
// to be implemented by the react component // to be implemented by the react component
this.api = {} this.api = {}
this.dispatch = null this.dispatch = null
this.ref = null this.ref = null
} }
setDispatch (dispatch) { setDispatch (dispatch) {
this.dispatch = dispatch this.dispatch = dispatch
} }
updateComponent(state) { updateComponent(state) {
return <EditorUI return <EditorUI
editorAPI={state.api} editorAPI={state.api}
themeType={state.currentThemeType} themeType={state.currentThemeType}
currentFile={state.currentFile} currentFile={state.currentFile}
events={state.events} events={state.events}
plugin={state.plugin} plugin={state.plugin}
/> />
} }
render () { render () {
return <div ref={(element)=>{ return <div ref={(element)=>{
this.ref = element this.ref = element
this.ref.currentContent = () => this.currentContent() // used by e2e test this.ref.currentContent = () => this.currentContent() // used by e2e test
this.ref.setCurrentContent = (value) => { this.ref.setCurrentContent = (value) => {
if (this.sessions[this.currentFile]) { if (this.sessions[this.currentFile]) {
this.sessions[this.currentFile].setValue(value) this.sessions[this.currentFile].setValue(value)
this._onChange(this.currentFile) 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 () {
this.dispatch({
api: this.api,
currentThemeType: this.currentThemeType,
currentFile: this.currentFile,
events: this.events,
plugin: this
})
}
triggerEvent (name, params) {
this.event.trigger(name, params) // internal stack
this.emit(name, ...params) // plugin stack
}
async onActivation () {
this.activated = true
this.on('sidePanel', 'focusChanged', (name) => {
this.keepDecorationsFor(name, 'sourceAnnotationsPerFile')
this.keepDecorationsFor(name, 'markerPerFile')
})
this.on('sidePanel', 'pluginDisabled', (name) => {
this.clearAllDecorationsFor(name)
})
this.on('theme', 'themeLoaded', (theme) => {
this.currentThemeType = theme.quality
this.renderComponent()
})
this.on('fileManager', 'noFileSelected', async () => {
this.currentFile = null
this.renderComponent()
})
try {
this.currentThemeType = (await this.call('theme', 'currentTheme')).quality
} catch (e) {
console.log('unable to select the theme ' + e.message)
}
this.renderComponent()
}
onDeactivation () {
this.off('sidePanel', 'focusChanged')
this.off('sidePanel', 'pluginDisabled')
}
async _onChange (file) {
this.triggerEvent('didChangeFile', [file])
const currentFile = await this.call('fileManager', 'file')
if (!currentFile) {
return
}
if (currentFile !== file) {
return
}
const input = this.get(currentFile)
if (!input) {
return
}
// if there's no change, don't do anything
if (input === this.previousInput) {
return
}
this.previousInput = input
// fire storage update
// NOTE: save at most once per 5 seconds
if (this.saveTimeout) {
window.clearTimeout(this.saveTimeout)
} }
}
this.saveTimeout = window.setTimeout(() => { this.ref.gotoLine = (line, column) => this.gotoLine(line, column || 0)
this.triggerEvent('contentChanged', []) this.ref.getCursorPosition = () => this.getCursorPosition()
this.triggerEvent('requiringToSaveCurrentfile', []) this.ref.addDecoration = (marker, filePath, typeOfDecoration) => this.addDecoration(marker, filePath, typeOfDecoration)
}, 500) this.ref.clearDecorationsByPlugin = (filePath, plugin, typeOfDecoration) => this.clearDecorationsByPlugin(filePath, plugin, typeOfDecoration)
} this.ref.keepDecorationsFor = (name, typeOfDecoration) => this.keepDecorationsFor(name, typeOfDecoration)
}} id='editorView'>
_switchSession (path) { <PluginViewWrapper plugin={this} />
if (path === this.currentFile) return </div>
this.triggerEvent('sessionSwitched', []) }
this.currentFile = path
this.renderComponent() renderComponent () {
} this.dispatch({
api: this.api,
/** currentThemeType: this.currentThemeType,
currentFile: this.currentFile,
events: this.events,
plugin: this
})
}
triggerEvent (name, params) {
this.event.trigger(name, params) // internal stack
this.emit(name, ...params) // plugin stack
}
async onActivation () {
this.activated = true
this.on('sidePanel', 'focusChanged', (name) => {
this.keepDecorationsFor(name, 'sourceAnnotationsPerFile')
this.keepDecorationsFor(name, 'markerPerFile')
})
this.on('sidePanel', 'pluginDisabled', (name) => {
this.clearAllDecorationsFor(name)
})
this.on('theme', 'themeLoaded', (theme) => {
this.currentThemeType = theme.quality
this.renderComponent()
})
this.on('fileManager', 'noFileSelected', async () => {
this.currentFile = null
this.renderComponent()
})
try {
this.currentThemeType = (await this.call('theme', 'currentTheme')).quality
} catch (e) {
console.log('unable to select the theme ' + e.message)
}
this.renderComponent()
}
onDeactivation () {
this.off('sidePanel', 'focusChanged')
this.off('sidePanel', 'pluginDisabled')
}
async _onChange (file) {
this.triggerEvent('didChangeFile', [file])
const currentFile = await this.call('fileManager', 'file')
if (!currentFile) {
return
}
if (currentFile !== file) {
return
}
const input = this.get(currentFile)
if (!input) {
return
}
// if there's no change, don't do anything
if (input === this.previousInput) {
return
}
this.previousInput = input
// fire storage update
// NOTE: save at most once per 5 seconds
if (this.saveTimeout) {
window.clearTimeout(this.saveTimeout)
}
this.saveTimeout = window.setTimeout(() => {
this.triggerEvent('contentChanged', [])
this.triggerEvent('requiringToSaveCurrentfile', [])
}, 500)
}
_switchSession (path) {
if (path === this.currentFile) return
this.triggerEvent('sessionSwitched', [])
this.currentFile = path
this.renderComponent()
}
/**
* Get Ace mode base of the extension of the session file * Get Ace mode base of the extension of the session file
* @param {string} path Path of the file * @param {string} path Path of the file
*/ */
_getMode (path) { _getMode (path) {
if (!path) return this.modes.txt if (!path) return this.modes.txt
const root = path.split('#')[0].split('?')[0] const root = path.split('#')[0].split('?')[0]
let ext = root.indexOf('.') !== -1 ? /[^.]+$/.exec(root) : null let ext = root.indexOf('.') !== -1 ? /[^.]+$/.exec(root) : null
if (ext) ext = ext[0] if (ext) ext = ext[0]
else ext = 'txt' else ext = 'txt'
return ext && this.modes[ext] ? this.modes[ext] : this.modes.txt return ext && this.modes[ext] ? this.modes[ext] : this.modes.txt
} }
async handleTypeScriptDependenciesOf (path, content, readFile, exists) { async handleTypeScriptDependenciesOf (path, content, readFile, exists) {
if (path.endsWith('.ts')) { if (path.endsWith('.ts')) {
// extract the import, resolve their content // extract the import, resolve their content
// and add the imported files to Monaco through the `addModel` // and add the imported files to Monaco through the `addModel`
// so Monaco can provide auto completion // so Monaco can provide auto completion
const paths = path.split('/') const paths = path.split('/')
paths.pop() paths.pop()
const fromPath = paths.join('/') // get current execution context path const fromPath = paths.join('/') // get current execution context path
for (const match of content.matchAll(/import\s+.*\s+from\s+(?:"(.*?)"|'(.*?)')/g)) { for (const match of content.matchAll(/import\s+.*\s+from\s+(?:"(.*?)"|'(.*?)')/g)) {
let pathDep = match[2] let pathDep = match[2]
if (pathDep.startsWith('./') || pathDep.startsWith('../')) pathDep = resolve(fromPath, pathDep) if (pathDep.startsWith('./') || pathDep.startsWith('../')) pathDep = resolve(fromPath, pathDep)
if (pathDep.startsWith('/')) pathDep = pathDep.substring(1) if (pathDep.startsWith('/')) pathDep = pathDep.substring(1)
if (!pathDep.endsWith('.ts')) pathDep = pathDep + '.ts' if (!pathDep.endsWith('.ts')) pathDep = pathDep + '.ts'
try { try {
// we can't use the fileManager plugin call directly // we can't use the fileManager plugin call directly
// because it's itself called in a plugin context, and that causes a timeout in the plugin stack // because it's itself called in a plugin context, and that causes a timeout in the plugin stack
const pathExists = await exists(pathDep) const pathExists = await exists(pathDep)
let contentDep = '' let contentDep = ''
if (pathExists) { if (pathExists) {
contentDep = await readFile(pathDep) contentDep = await readFile(pathDep)
if (contentDep !== '') { if (contentDep !== '') {
this.emit('addModel', contentDep, 'typescript', pathDep, this.readOnlySessions[path]) this.emit('addModel', contentDep, 'typescript', pathDep, this.readOnlySessions[path])
}
} else {
console.log("The file ", pathDep, " can't be found.")
}
} catch (e) {
console.log(e)
}
} }
} else {
console.log("The file ", pathDep, " can't be found.")
}
} catch (e) {
console.log(e)
} }
}
} }
}
/** /**
* Create an editor session * Create an editor session
* @param {string} path path of the file * @param {string} path path of the file
* @param {string} content Content of the file to open * @param {string} content Content of the file to open
* @param {string} mode Mode for this file [Default is `text`] * @param {string} mode Mode for this file [Default is `text`]
*/ */
async _createSession (path, content, mode) { async _createSession (path, content, mode) {
if (!this.activated) return if (!this.activated) return
this.emit('addModel', content, mode, path, this.readOnlySessions[path]) this.emit('addModel', content, mode, path, this.readOnlySessions[path])
return { return {
path, path,
language: mode, language: mode,
setValue: (content) => { setValue: (content) => {
this.emit('setValue', path, content) this.emit('setValue', path, content)
}, },
getValue: () => { getValue: () => {
return this.api.getValue(path, content) return this.api.getValue(path, content)
}, },
dispose: () => { dispose: () => {
this.emit('disposeModel', path) this.emit('disposeModel', path)
} }
} }
} }
/** /**
* Attempts to find the string in the current document * Attempts to find the string in the current document
* @param {string} string * @param {string} string
*/ */
find (string) { find (string) {
return this.api.findMatches(this.currentFile, string) return this.api.findMatches(this.currentFile, string)
} }
addModel(path, content) { addModel(path, content) {
this.emit('addModel', content, this._getMode(path), path, this.readOnlySessions[path]) this.emit('addModel', content, this._getMode(path), path, this.readOnlySessions[path])
} }
/** /**
* Display an Empty read-only session * Display an Empty read-only session
*/ */
displayEmptyReadOnlySession () { displayEmptyReadOnlySession () {
if (!this.activated) return if (!this.activated) return
this.currentFile = null this.currentFile = null
this.emit('addModel', '', 'text', '_blank', true) this.emit('addModel', '', 'text', '_blank', true)
} }
/** /**
* Set the text in the current session, if any. * Set the text in the current session, if any.
* @param {string} url Address of the text to replace. * @param {string} url Address of the text to replace.
* @param {string} text New text to be place. * @param {string} text New text to be place.
*/ */
setText (url, text) { setText (url, text) {
if (this.sessions[url]) { if (this.sessions[url]) {
this.sessions[url].setValue(text) this.sessions[url].setValue(text)
}
} }
}
/** /**
* Get the text in the current session, if any. * Get the text in the current session, if any.
* @param {string} url Address of the content to retrieve. * @param {string} url Address of the content to retrieve.
*/ */
getText (url) { getText (url) {
if (this.sessions[url]) { if (this.sessions[url]) {
return this.sessions[url].getValue() return this.sessions[url].getValue()
}
} }
}
/** /**
* Upsert and open a session. * Upsert and open a session.
* @param {string} path Path of the session to open. * @param {string} path Path of the session to open.
* @param {string} content Content of the document or update. * @param {string} content Content of the document or update.
*/ */
async open (path, content) { async open (path, content) {
/* /*
we have the following cases: we have the following cases:
- URL prepended with "localhost" - URL prepended with "localhost"
- URL prepended with "browser" - URL prepended with "browser"
- URL not prepended with the file explorer. We assume (as it is in the whole app, that this is a "browser" URL - URL not prepended with the file explorer. We assume (as it is in the whole app, that this is a "browser" URL
*/ */
if (!this.sessions[path]) { if (!this.sessions[path]) {
this.readOnlySessions[path] = false this.readOnlySessions[path] = false
const session = await this._createSession(path, content, this._getMode(path)) const session = await this._createSession(path, content, this._getMode(path))
this.sessions[path] = session this.sessions[path] = session
} else if (this.sessions[path].getValue() !== content) { } else if (this.sessions[path].getValue() !== content) {
this.sessions[path].setValue(content) this.sessions[path].setValue(content)
}
this._switchSession(path)
} }
this._switchSession(path)
}
/** /**
* Upsert and Open a session and set it as Read-only. * Upsert and Open a session and set it as Read-only.
* @param {string} path Path of the session to open. * @param {string} path Path of the session to open.
* @param {string} content Content of the document or update. * @param {string} content Content of the document or update.
*/ */
async openReadOnly (path, content) { async openReadOnly (path, content) {
if (!this.sessions[path]) { if (!this.sessions[path]) {
this.readOnlySessions[path] = true this.readOnlySessions[path] = true
const session = await this._createSession(path, content, this._getMode(path)) const session = await this._createSession(path, content, this._getMode(path))
this.sessions[path] = session this.sessions[path] = session
}
this._switchSession(path)
} }
this._switchSession(path)
}
/** /**
* Content of the current session * Content of the current session
* @return {String} content of the file referenced by @arg path * @return {String} content of the file referenced by @arg path
*/ */
currentContent () { currentContent () {
return this.get(this.current()) return this.get(this.current())
} }
/** /**
* Content of the session targeted by @arg path * Content of the session targeted by @arg path
* if @arg path is null, the content of the current session is returned * if @arg path is null, the content of the current session is returned
* @param {string} path Path of the session to get. * @param {string} path Path of the session to get.
* @return {String} content of the file referenced by @arg path * @return {String} content of the file referenced by @arg path
*/ */
get (path) { get (path) {
if (!path || this.currentFile === path) { if (!path || this.currentFile === path) {
return this.api.getValue(path) return this.api.getValue(path)
} else if (this.sessions[path]) { } else if (this.sessions[path]) {
return this.sessions[path].getValue() return this.sessions[path].getValue()
}
} }
}
/** /**
* Path of the currently editing file * Path of the currently editing file
* returns `undefined` if no session is being editer * returns `undefined` if no session is being editer
* @return {String} path of the current session * @return {String} path of the current session
*/ */
current () { current () {
return this.currentFile return this.currentFile
} }
/** /**
* The position of the cursor * The position of the cursor
*/ */
getCursorPosition (offset = true) { getCursorPosition (offset = true) {
return this.api.getCursorPosition(offset) return this.api.getCursorPosition(offset)
} }
/** /**
* Remove the current session from the list of sessions. * Remove the current session from the list of sessions.
*/ */
discardCurrentSession () { discardCurrentSession () {
if (this.sessions[this.currentFile]) { if (this.sessions[this.currentFile]) {
delete this.sessions[this.currentFile] delete this.sessions[this.currentFile]
this.currentFile = null this.currentFile = null
}
} }
}
/** /**
* Remove a session based on its path. * Remove a session based on its path.
* @param {string} path * @param {string} path
*/ */
discard (path) { discard (path) {
if (this.sessions[path]) { if (this.sessions[path]) {
this.sessions[path].dispose() this.sessions[path].dispose()
delete this.sessions[path] delete this.sessions[path]
}
if (this.currentFile === path) this.currentFile = null
} }
if (this.currentFile === path) this.currentFile = null
}
/** /**
* Increment the font size (in pixels) for the editor text. * Increment the font size (in pixels) for the editor text.
* @param {number} incr The amount of pixels to add to the font. * @param {number} incr The amount of pixels to add to the font.
*/ */
editorFontSize (incr) { editorFontSize (incr) {
if (!this.activated) return if (!this.activated) return
const newSize = this.api.getFontSize() + incr const newSize = this.api.getFontSize() + incr
if (newSize >= 6) { if (newSize >= 6) {
this.emit('setFontSize', newSize) this.emit('setFontSize', newSize)
}
} }
}
/** /**
* Resize the editor, and sets whether or not line wrapping is enabled. * Resize the editor, and sets whether or not line wrapping is enabled.
* @param {boolean} useWrapMode Enable (or disable) wrap mode * @param {boolean} useWrapMode Enable (or disable) wrap mode
*/ */
resize (useWrapMode) { resize (useWrapMode) {
if (!this.activated) return if (!this.activated) return
this.emit('setWordWrap', useWrapMode) this.emit('setWordWrap', useWrapMode)
} }
/** /**
* Moves the cursor and focus to the specified line and column number * Moves the cursor and focus to the specified line and column number
* @param {number} line * @param {number} line
* @param {number} col * @param {number} col
*/ */
gotoLine (line, col) { gotoLine (line, col) {
if (!this.activated) return if (!this.activated) return
this.emit('focus') this.emit('focus')
this.emit('revealLine', line + 1, col) this.emit('revealLine', line + 1, col)
} }
/** /**
* Reveals the range in the editor. * Reveals the range in the editor.
* @param {number} startLineNumber * @param {number} startLineNumber
* @param {number} startColumn * @param {number} startColumn
* @param {number} endLineNumber * @param {number} endLineNumber
* @param {number} endColumn * @param {number} endColumn
*/ */
revealRange (startLineNumber, startColumn, endLineNumber, endColumn) { revealRange (startLineNumber, startColumn, endLineNumber, endColumn) {
if (!this.activated) return if (!this.activated) return
this.emit('focus') this.emit('focus')
console.log(startLineNumber, startColumn, endLineNumber, endColumn) console.log(startLineNumber, startColumn, endLineNumber, endColumn)
this.emit('revealRange', startLineNumber, startColumn, endLineNumber, endColumn) this.emit('revealRange', startLineNumber, startColumn, endLineNumber, endColumn)
} }
/** /**
* Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to). * Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to).
* @param {number} line The line to scroll to * @param {number} line The line to scroll to
*/ */
scrollToLine (line) { scrollToLine (line) {
if (!this.activated) return if (!this.activated) return
this.emit('revealLine', line + 1, 0) this.emit('revealLine', line + 1, 0)
} }
/** /**
* Clears all the decorations for the given @arg filePath and @arg plugin, if none is given, the current sesssion is used. * Clears all the decorations for the given @arg filePath and @arg plugin, if none is given, the current sesssion is used.
* An annotation has the following shape: * An annotation has the following shape:
column: -1 column: -1
@ -461,22 +461,22 @@ class Editor extends Plugin {
* @param {String} plugin * @param {String} plugin
* @param {String} typeOfDecoration * @param {String} typeOfDecoration
*/ */
clearDecorationsByPlugin (filePath, plugin, typeOfDecoration) { clearDecorationsByPlugin (filePath, plugin, typeOfDecoration) {
if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath) if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath)
const path = filePath || this.currentFile const path = filePath || this.currentFile
const { currentDecorations, registeredDecorations } = this.api.clearDecorationsByPlugin(path, plugin, typeOfDecoration, this.registeredDecorations[typeOfDecoration][filePath] || [], this.currentDecorations[typeOfDecoration][filePath] || []) const { currentDecorations, registeredDecorations } = this.api.clearDecorationsByPlugin(path, plugin, typeOfDecoration, this.registeredDecorations[typeOfDecoration][filePath] || [], this.currentDecorations[typeOfDecoration][filePath] || [])
this.currentDecorations[typeOfDecoration][filePath] = currentDecorations this.currentDecorations[typeOfDecoration][filePath] = currentDecorations
this.registeredDecorations[typeOfDecoration][filePath] = registeredDecorations this.registeredDecorations[typeOfDecoration][filePath] = registeredDecorations
} }
keepDecorationsFor (plugin, typeOfDecoration) { keepDecorationsFor (plugin, typeOfDecoration) {
if (!this.currentFile) return if (!this.currentFile) return
const { currentDecorations } = this.api.keepDecorationsFor(this.currentFile, plugin, typeOfDecoration, this.registeredDecorations[typeOfDecoration][this.currentFile] || [], this.currentDecorations[typeOfDecoration][this.currentFile] || []) 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 this.currentDecorations[typeOfDecoration][this.currentFile] = currentDecorations
} }
/** /**
* Clears all the decorations and for all the sessions for the given @arg plugin * Clears all the decorations and for all the sessions for the given @arg plugin
* An annotation has the following shape: * An annotation has the following shape:
column: -1 column: -1
@ -485,25 +485,25 @@ class Editor extends Plugin {
type: "warning" type: "warning"
* @param {String} filePath * @param {String} filePath
*/ */
clearAllDecorationsFor (plugin) { clearAllDecorationsFor (plugin) {
for (const session in this.sessions) { for (const session in this.sessions) {
this.clearDecorationsByPlugin(session, plugin, 'sourceAnnotationsPerFile') this.clearDecorationsByPlugin(session, plugin, 'sourceAnnotationsPerFile')
this.clearDecorationsByPlugin(session, plugin, 'markerPerFile') this.clearDecorationsByPlugin(session, plugin, 'markerPerFile')
}
} }
}
// error markers // error markers
async addErrorMarker (error){ async addErrorMarker (error){
const { from } = this.currentRequest const { from } = this.currentRequest
this.api.addErrorMarker(error, from) this.api.addErrorMarker(error, from)
} }
async clearErrorMarkers(sources){ async clearErrorMarkers(sources){
const { from } = this.currentRequest const { from } = this.currentRequest
this.api.clearErrorMarkers(sources, from) this.api.clearErrorMarkers(sources, from)
} }
/** /**
* Clears all the annotations for the given @arg filePath, the plugin name is retrieved from the context, if none is given, the current sesssion is used. * Clears all the annotations for the given @arg filePath, the plugin name is retrieved from the context, if none is given, the current sesssion is used.
* An annotation has the following shape: * An annotation has the following shape:
column: -1 column: -1
@ -513,30 +513,30 @@ class Editor extends Plugin {
* @param {String} filePath * @param {String} filePath
* @param {String} plugin * @param {String} plugin
*/ */
clearAnnotations (filePath) { clearAnnotations (filePath) {
filePath = filePath || this.currentFile filePath = filePath || this.currentFile
const { from } = this.currentRequest const { from } = this.currentRequest
this.clearDecorationsByPlugin(filePath, from, 'sourceAnnotationsPerFile') this.clearDecorationsByPlugin(filePath, from, 'sourceAnnotationsPerFile')
} }
async addDecoration (decoration, filePath, typeOfDecoration) { async addDecoration (decoration, filePath, typeOfDecoration) {
if (!filePath) return if (!filePath) return
filePath = await this.call('fileManager', 'getPathFromUrl', filePath) filePath = await this.call('fileManager', 'getPathFromUrl', filePath)
filePath = filePath.file filePath = filePath.file
if (!this.sessions[filePath]) return if (!this.sessions[filePath]) return
const path = filePath || this.currentFile const path = filePath || this.currentFile
const { from } = this.currentRequest const { from } = this.currentRequest
decoration.from = from decoration.from = from
const { currentDecorations, registeredDecorations } = this.api.addDecoration(decoration, path, typeOfDecoration) const { currentDecorations, registeredDecorations } = this.api.addDecoration(decoration, path, typeOfDecoration)
if (!this.registeredDecorations[typeOfDecoration][filePath]) this.registeredDecorations[typeOfDecoration][filePath] = [] if (!this.registeredDecorations[typeOfDecoration][filePath]) this.registeredDecorations[typeOfDecoration][filePath] = []
this.registeredDecorations[typeOfDecoration][filePath].push(...registeredDecorations) this.registeredDecorations[typeOfDecoration][filePath].push(...registeredDecorations)
if (!this.currentDecorations[typeOfDecoration][filePath]) this.currentDecorations[typeOfDecoration][filePath] = [] if (!this.currentDecorations[typeOfDecoration][filePath]) this.currentDecorations[typeOfDecoration][filePath] = []
this.currentDecorations[typeOfDecoration][filePath].push(...currentDecorations) this.currentDecorations[typeOfDecoration][filePath].push(...currentDecorations)
} }
/** /**
* Add an annotation to the current session. * Add an annotation to the current session.
* An annotation has the following shape: * An annotation has the following shape:
column: -1 column: -1
@ -546,38 +546,38 @@ class Editor extends Plugin {
* @param {Object} annotation * @param {Object} annotation
* @param {String} filePath * @param {String} filePath
*/ */
async addAnnotation (annotation, filePath) { async addAnnotation (annotation, filePath) {
filePath = filePath || this.currentFile filePath = filePath || this.currentFile
await this.addDecoration(annotation, filePath, 'sourceAnnotationsPerFile') await this.addDecoration(annotation, filePath, 'sourceAnnotationsPerFile')
} }
async highlight (position, filePath, highlightColor, opt = { focus: true }) { async highlight (position, filePath, highlightColor, opt = { focus: true }) {
filePath = filePath || this.currentFile filePath = filePath || this.currentFile
if (opt.focus) { if (opt.focus) {
await this.call('fileManager', 'open', filePath) await this.call('fileManager', 'open', filePath)
this.scrollToLine(position.start.line) this.scrollToLine(position.start.line)
} }
await this.addDecoration({ position }, filePath, 'markerPerFile') await this.addDecoration({ position }, filePath, 'markerPerFile')
} }
discardHighlight () { discardHighlight () {
const { from } = this.currentRequest const { from } = this.currentRequest
for (const session in this.sessions) { for (const session in this.sessions) {
this.clearDecorationsByPlugin(session, from, 'markerPerFile', this.registeredDecorations, this.currentDecorations) this.clearDecorationsByPlugin(session, from, 'markerPerFile', this.registeredDecorations, this.currentDecorations)
} }
} }
async addLineText (lineText, filePath) { async addLineText (lineText, filePath) {
filePath = filePath || this.currentFile filePath = filePath || this.currentFile
await this.addDecoration(lineText, filePath, 'lineTextPerFile') await this.addDecoration(lineText, filePath, 'lineTextPerFile')
} }
discardLineTexts() { discardLineTexts() {
const { from } = this.currentRequest const { from } = this.currentRequest
for (const session in this.sessions) { for (const session in this.sessions) {
this.clearDecorationsByPlugin(session, from, 'lineTextPerFile', this.registeredDecorations, this.currentDecorations) this.clearDecorationsByPlugin(session, from, 'lineTextPerFile', this.registeredDecorations, this.currentDecorations)
} }
} }
} }
module.exports = Editor module.exports = Editor

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -7,318 +7,318 @@ const pathModule = require('path')
const Storage = remixLib.Storage const Storage = remixLib.Storage
class FileProvider { class FileProvider {
constructor (name) { constructor (name) {
this.event = new EventManager() this.event = new EventManager()
this.type = name this.type = name
this.providerExternalsStorage = new Storage('providerExternals:') this.providerExternalsStorage = new Storage('providerExternals:')
this.externalFolders = [this.type + '/swarm', this.type + '/ipfs', this.type + '/github', this.type + '/gists', this.type + '/https'] this.externalFolders = [this.type + '/swarm', this.type + '/ipfs', this.type + '/github', this.type + '/gists', this.type + '/https']
this.reverseKey = this.type + '-reverse-' this.reverseKey = this.type + '-reverse-'
} }
addNormalizedName (path, url) { addNormalizedName (path, url) {
if (this.type) path = this.type + '/' + path if (this.type) path = this.type + '/' + path
this.providerExternalsStorage.set(path, url) this.providerExternalsStorage.set(path, url)
this.providerExternalsStorage.set(this.reverseKey + url, path) this.providerExternalsStorage.set(this.reverseKey + url, path)
} }
removeNormalizedName (path) { removeNormalizedName (path) {
const value = this.providerExternalsStorage.get(path) const value = this.providerExternalsStorage.get(path)
this.providerExternalsStorage.remove(path) this.providerExternalsStorage.remove(path)
this.providerExternalsStorage.remove(this.reverseKey + value) this.providerExternalsStorage.remove(this.reverseKey + value)
} }
normalizedNameExists (path) { normalizedNameExists (path) {
return this.providerExternalsStorage.exists(path) return this.providerExternalsStorage.exists(path)
} }
getNormalizedName (path) { getNormalizedName (path) {
return this.providerExternalsStorage.get(path) return this.providerExternalsStorage.get(path)
} }
getPathFromUrl (url) { getPathFromUrl (url) {
return this.providerExternalsStorage.get(this.reverseKey + url) return this.providerExternalsStorage.get(this.reverseKey + url)
} }
getUrlFromPath (path) { getUrlFromPath (path) {
if (!path.startsWith(this.type)) path = this.type + '/' + path if (!path.startsWith(this.type)) path = this.type + '/' + path
return this.providerExternalsStorage.get(path) return this.providerExternalsStorage.get(path)
} }
isExternalFolder (path) { isExternalFolder (path) {
return this.externalFolders.includes(path) return this.externalFolders.includes(path)
} }
async discardChanges (path, toastCb, modalCb) { async discardChanges (path, toastCb, modalCb) {
this.remove(path) this.remove(path)
const compilerImport = new CompilerImports() const compilerImport = new CompilerImports()
this.providerExternalsStorage.keys().map(value => { this.providerExternalsStorage.keys().map(value => {
if (value.indexOf(path) === 0) { if (value.indexOf(path) === 0) {
compilerImport.import( compilerImport.import(
this.getNormalizedName(value), this.getNormalizedName(value),
true, true,
(loadingMsg) => { toastCb(loadingMsg) }, (loadingMsg) => { toastCb(loadingMsg) },
async (error, content, cleanUrl, type, url) => { async (error, content, cleanUrl, type, url) => {
if (error) { if (error) {
modalCb(error) modalCb(error)
} else { } else {
await this.addExternal(type + '/' + cleanUrl, content, url) await this.addExternal(type + '/' + cleanUrl, content, url)
}
}
)
} }
}) }
)
}
})
}
async exists (path) {
// todo check the type (directory/file) as well #2386
// currently it is not possible to have a file and folder with same path
const ret = await this._exists(path)
return ret
}
async _exists (path) {
path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here
var unprefixedpath = this.removePrefix(path)
return path === this.type ? true : await window.remixFileSystem.exists(unprefixedpath)
}
init (cb) {
cb()
}
async get (path, cb) {
cb = cb || function () { /* do nothing. */ }
path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here
var unprefixedpath = this.removePrefix(path)
try {
const content = await window.remixFileSystem.readFile(unprefixedpath, 'utf8')
if (cb) cb(null, content)
return content
} catch (err) {
if (cb) cb(err, null)
throw new Error(err)
} }
}
async exists (path) {
// todo check the type (directory/file) as well #2386 async set (path, content, cb) {
// currently it is not possible to have a file and folder with same path cb = cb || function () { /* do nothing. */ }
const ret = await this._exists(path) var unprefixedpath = this.removePrefix(path)
const exists = await window.remixFileSystem.exists(unprefixedpath)
return ret if (exists && await window.remixFileSystem.readFile(unprefixedpath, 'utf8') === content) {
} if (cb) cb()
return null
async _exists (path) {
path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here
var unprefixedpath = this.removePrefix(path)
return path === this.type ? true : await window.remixFileSystem.exists(unprefixedpath)
} }
await this.createDir(path.substr(0, path.lastIndexOf('/')))
init (cb) { try {
cb() await window.remixFileSystem.writeFile(unprefixedpath, content, 'utf8')
} catch (e) {
if (cb) cb(e)
return false
} }
if (!exists) {
async get (path, cb) { this.event.emit('fileAdded', this._normalizePath(unprefixedpath), false)
cb = cb || function () { /* do nothing. */ } } else {
path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here this.event.emit('fileChanged', this._normalizePath(unprefixedpath))
var unprefixedpath = this.removePrefix(path)
try {
const content = await window.remixFileSystem.readFile(unprefixedpath, 'utf8')
if (cb) cb(null, content)
return content
} catch (err) {
if (cb) cb(err, null)
throw new Error(err)
}
} }
if (cb) cb()
async set (path, content, cb) { return true
cb = cb || function () { /* do nothing. */ } }
var unprefixedpath = this.removePrefix(path)
const exists = await window.remixFileSystem.exists(unprefixedpath) async createDir (path, cb) {
if (exists && await window.remixFileSystem.readFile(unprefixedpath, 'utf8') === content) { const unprefixedpath = this.removePrefix(path)
if (cb) cb()
return null await this.forceCreateDir(unprefixedpath)
} if (cb) cb()
await this.createDir(path.substr(0, path.lastIndexOf('/'))) }
async forceCreateDir (path) {
const paths = path.split('/')
if (paths.length && paths[0] === '') paths.shift()
let currentCheck = ''
for (const value of paths) {
currentCheck = currentCheck + '/' + value
if (!await window.remixFileSystem.exists(currentCheck)) {
try { try {
await window.remixFileSystem.writeFile(unprefixedpath, content, 'utf8') await window.remixFileSystem.mkdir(currentCheck)
} catch (e) { this.event.emit('folderAdded', this._normalizePath(currentCheck))
if (cb) cb(e) } catch (error) {
return false console.log(error)
}
if (!exists) {
this.event.emit('fileAdded', this._normalizePath(unprefixedpath), false)
} else {
this.event.emit('fileChanged', this._normalizePath(unprefixedpath))
}
if (cb) cb()
return true
}
async createDir (path, cb) {
const unprefixedpath = this.removePrefix(path)
await this.forceCreateDir(unprefixedpath)
if (cb) cb()
}
async forceCreateDir (path) {
const paths = path.split('/')
if (paths.length && paths[0] === '') paths.shift()
let currentCheck = ''
for (const value of paths) {
currentCheck = currentCheck + '/' + value
if (!await window.remixFileSystem.exists(currentCheck)) {
try {
await window.remixFileSystem.mkdir(currentCheck)
this.event.emit('folderAdded', this._normalizePath(currentCheck))
} catch (error) {
console.log(error)
}
}
} }
}
} }
}
// this will not add a folder as readonly but keep the original url to be able to restore it later
async addExternal (path, content, url) { // this will not add a folder as readonly but keep the original url to be able to restore it later
if (url) this.addNormalizedName(path, url) async addExternal (path, content, url) {
return await this.set(path, content) if (url) this.addNormalizedName(path, url)
} return await this.set(path, content)
}
isReadOnly (path) {
return false isReadOnly (path) {
} return false
}
async isDirectory (path) {
const unprefixedpath = this.removePrefix(path) async isDirectory (path) {
return path === this.type ? true : (await window.remixFileSystem.stat(unprefixedpath)).isDirectory() const unprefixedpath = this.removePrefix(path)
} return path === this.type ? true : (await window.remixFileSystem.stat(unprefixedpath)).isDirectory()
}
async isFile (path) {
path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here async isFile (path) {
path = this.removePrefix(path) path = this.getPathFromUrl(path) || path // ensure we actually use the normalized path from here
return (await window.remixFileSystem.stat(path)).isFile() path = this.removePrefix(path)
} return (await window.remixFileSystem.stat(path)).isFile()
}
/**
/**
* Removes the folder recursively * Removes the folder recursively
* @param {*} path is the folder to be removed * @param {*} path is the folder to be removed
*/ */
async remove (path) { async remove (path) {
path = this.removePrefix(path) path = this.removePrefix(path)
if (await window.remixFileSystem.exists(path)) { if (await window.remixFileSystem.exists(path)) {
const stat = await window.remixFileSystem.stat(path) const stat = await window.remixFileSystem.stat(path)
try { try {
if (!stat.isDirectory()) { if (!stat.isDirectory()) {
return await this.removeFile(path) return await this.removeFile(path)
} else { } else {
const items = await window.remixFileSystem.readdir(path) const items = await window.remixFileSystem.readdir(path)
if (items.length !== 0) { if (items.length !== 0) {
for (const item of items) { for (const item of items) {
const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}` const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}`
if ((await window.remixFileSystem.stat(curPath)).isDirectory()) { // delete folder if ((await window.remixFileSystem.stat(curPath)).isDirectory()) { // delete folder
await this.remove(curPath) await this.remove(curPath)
} else { // delete file } else { // delete file
await this.removeFile(curPath) await this.removeFile(curPath)
} }
}
await window.remixFileSystem.rmdir(path)
this.event.emit('fileRemoved', this._normalizePath(path))
} else {
// folder is empty
await window.remixFileSystem.rmdir(path)
this.event.emit('fileRemoved', this._normalizePath(path))
}
}
} catch (e) {
console.log(e)
return false
} }
await window.remixFileSystem.rmdir(path)
this.event.emit('fileRemoved', this._normalizePath(path))
} else {
// folder is empty
await window.remixFileSystem.rmdir(path)
this.event.emit('fileRemoved', this._normalizePath(path))
}
} }
} catch (e) {
console.log(e)
return false
}
} }
}
/** /**
* copy the folder recursively (internal use) * copy the folder recursively (internal use)
* @param {string} path is the folder to be copied over * @param {string} path is the folder to be copied over
* @param {Function} visitFile is a function called for each visited files * @param {Function} visitFile is a function called for each visited files
* @param {Function} visitFolder is a function called for each visited folders * @param {Function} visitFolder is a function called for each visited folders
*/ */
async _copyFolderToJsonInternal (path, visitFile, visitFolder) { async _copyFolderToJsonInternal (path, visitFile, visitFolder) {
visitFile = visitFile || function () { /* do nothing. */ } visitFile = visitFile || function () { /* do nothing. */ }
visitFolder = visitFolder || function () { /* do nothing. */ } visitFolder = visitFolder || function () { /* do nothing. */ }
const json = {} const json = {}
path = this.removePrefix(path) path = this.removePrefix(path)
if (await window.remixFileSystem.exists(path)) { if (await window.remixFileSystem.exists(path)) {
try { try {
const items = await window.remixFileSystem.readdir(path) const items = await window.remixFileSystem.readdir(path)
visitFolder({ path }) visitFolder({ path })
if (items.length !== 0) { if (items.length !== 0) {
for (const item of items) { for (const item of items) {
const file = {} const file = {}
const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}` const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}`
if ((await window.remixFileSystem.stat(curPath)).isDirectory()) { if ((await window.remixFileSystem.stat(curPath)).isDirectory()) {
file.children = await this._copyFolderToJsonInternal(curPath, visitFile, visitFolder) file.children = await this._copyFolderToJsonInternal(curPath, visitFile, visitFolder)
} else { } else {
file.content = await window.remixFileSystem.readFile(curPath, 'utf8') file.content = await window.remixFileSystem.readFile(curPath, 'utf8')
visitFile({ path: curPath, content: file.content }) visitFile({ path: curPath, content: file.content })
}
json[curPath] = file
}
}
} catch (e) {
console.log(e)
throw new Error(e)
} }
json[curPath] = file
}
} }
return json } catch (e) {
console.log(e)
throw new Error(e)
}
} }
return json
}
/** /**
* copy the folder recursively * copy the folder recursively
* @param {string} path is the folder to be copied over * @param {string} path is the folder to be copied over
* @param {Function} visitFile is a function called for each visited files * @param {Function} visitFile is a function called for each visited files
* @param {Function} visitFolder is a function called for each visited folders * @param {Function} visitFolder is a function called for each visited folders
*/ */
async copyFolderToJson (path, visitFile, visitFolder) { async copyFolderToJson (path, visitFile, visitFolder) {
visitFile = visitFile || function () { /* do nothing. */ } visitFile = visitFile || function () { /* do nothing. */ }
visitFolder = visitFolder || function () { /* do nothing. */ } visitFolder = visitFolder || function () { /* do nothing. */ }
return await this._copyFolderToJsonInternal(path, visitFile, visitFolder) return await this._copyFolderToJsonInternal(path, visitFile, visitFolder)
} }
async removeFile (path) { async removeFile (path) {
path = this.removePrefix(path) path = this.removePrefix(path)
if (await window.remixFileSystem.exists(path) && !(await window.remixFileSystem.stat(path)).isDirectory()) { if (await window.remixFileSystem.exists(path) && !(await window.remixFileSystem.stat(path)).isDirectory()) {
await window.remixFileSystem.unlink(path) await window.remixFileSystem.unlink(path)
this.event.emit('fileRemoved', this._normalizePath(path)) this.event.emit('fileRemoved', this._normalizePath(path))
return true return true
} else return false } else return false
} }
async rename (oldPath, newPath, isFolder) { async rename (oldPath, newPath, isFolder) {
var unprefixedoldPath = this.removePrefix(oldPath) var unprefixedoldPath = this.removePrefix(oldPath)
var unprefixednewPath = this.removePrefix(newPath) var unprefixednewPath = this.removePrefix(newPath)
if (await this._exists(unprefixedoldPath)) { if (await this._exists(unprefixedoldPath)) {
await window.remixFileSystem.rename(unprefixedoldPath, unprefixednewPath) await window.remixFileSystem.rename(unprefixedoldPath, unprefixednewPath)
this.event.emit('fileRenamed', this.event.emit('fileRenamed',
this._normalizePath(unprefixedoldPath), this._normalizePath(unprefixedoldPath),
this._normalizePath(unprefixednewPath), this._normalizePath(unprefixednewPath),
isFolder isFolder
) )
return true return true
}
return false
} }
return false
async resolveDirectory (path, cb) { }
path = this.removePrefix(path)
if (path.indexOf('/') !== 0) path = '/' + path async resolveDirectory (path, cb) {
try { path = this.removePrefix(path)
const files = await window.remixFileSystem.readdir(path) if (path.indexOf('/') !== 0) path = '/' + path
const ret = {} try {
if (files) { const files = await window.remixFileSystem.readdir(path)
for (let element of files) { const ret = {}
path = path.replace(/^\/|\/$/g, '') // remove first and last slash if (files) {
element = element.replace(/^\/|\/$/g, '') // remove first and last slash for (let element of files) {
const absPath = (path === '/' ? '' : path) + '/' + element path = path.replace(/^\/|\/$/g, '') // remove first and last slash
ret[absPath.indexOf('/') === 0 ? absPath.substr(1, absPath.length) : absPath] = { isDirectory: (await window.remixFileSystem.stat(absPath)).isDirectory() } element = element.replace(/^\/|\/$/g, '') // remove first and last slash
// ^ ret does not accept path starting with '/' const absPath = (path === '/' ? '' : path) + '/' + element
} ret[absPath.indexOf('/') === 0 ? absPath.substr(1, absPath.length) : absPath] = { isDirectory: (await window.remixFileSystem.stat(absPath)).isDirectory() }
} // ^ ret does not accept path starting with '/'
if (cb) cb(null, ret)
return ret
} catch (error) {
if (cb) cb(error, null)
} }
}
if (cb) cb(null, ret)
return ret
} catch (error) {
if (cb) cb(error, null)
} }
}
removePrefix (path) { removePrefix (path) {
path = path.indexOf(this.type) === 0 ? path.replace(this.type, '') : path path = path.indexOf(this.type) === 0 ? path.replace(this.type, '') : path
if (path === '') return '/' if (path === '') return '/'
return path return path
} }
_normalizePath (path) { _normalizePath (path) {
return this.type + path return this.type + path
} }
isSubDirectory (parent, child) { isSubDirectory (parent, child) {
if (!parent) return false if (!parent) return false
if (parent === child) return true if (parent === child) return true
const relative = pathModule.relative(parent, child) const relative = pathModule.relative(parent, child)
return !!relative && relative.split(pathModule.sep)[0] !== '..' return !!relative && relative.split(pathModule.sep)[0] !== '..'
} }
} }
module.exports = FileProvider module.exports = FileProvider

@ -1,71 +1,71 @@
export class fileSystem { export class fileSystem {
name: string name: string
enabled: boolean enabled: boolean
available: boolean available: boolean
fs: any fs: any
fsCallBack: any; fsCallBack: any;
hasWorkSpaces: boolean hasWorkSpaces: boolean
loaded: boolean loaded: boolean
load: () => Promise<unknown> load: () => Promise<unknown>
test: () => Promise<unknown> test: () => Promise<unknown>
constructor() { constructor() {
this.available = false this.available = false
this.enabled = false this.enabled = false
this.hasWorkSpaces = false this.hasWorkSpaces = false
this.loaded = false this.loaded = false
} }
checkWorkspaces = async () => { checkWorkspaces = async () => {
try { try {
await this.fs.stat('.workspaces') await this.fs.stat('.workspaces')
this.hasWorkSpaces = true this.hasWorkSpaces = true
} catch (e) { } catch (e) {
}
} }
}
set = async () => { set = async () => {
const w = (window as any) const w = (window as any)
if (!this.loaded) return false if (!this.loaded) return false
w.remixFileSystem = this.fs w.remixFileSystem = this.fs
w.remixFileSystem.name = this.name w.remixFileSystem.name = this.name
w.remixFileSystemCallback = this.fsCallBack w.remixFileSystemCallback = this.fsCallBack
return true return true
} }
} }
export class fileSystems { export class fileSystems {
fileSystems: Record<string, fileSystem> fileSystems: Record<string, fileSystem>
constructor() { constructor() {
this.fileSystems = {} this.fileSystems = {}
} }
addFileSystem = async (fs: fileSystem): Promise<boolean> => { addFileSystem = async (fs: fileSystem): Promise<boolean> => {
try { try {
this.fileSystems[fs.name] = fs this.fileSystems[fs.name] = fs
await fs.test() && await fs.load() await fs.test() && await fs.load()
console.log(fs.name + ' is loaded...') console.log(fs.name + ' is loaded...')
return true return true
} catch (e) { } catch (e) {
console.log(fs.name + ' not available...') console.log(fs.name + ' not available...')
return false return false
}
} }
/** }
/**
* sets filesystem using list as fallback * sets filesystem using list as fallback
* @param {string[]} names * @param {string[]} names
* @returns {Promise} * @returns {Promise}
*/ */
setFileSystem = async (filesystems?: fileSystem[]): Promise<fileSystem> => { setFileSystem = async (filesystems?: fileSystem[]): Promise<fileSystem> => {
for (const fs of filesystems) { for (const fs of filesystems) {
if (fs && this.fileSystems[fs.name]) { if (fs && this.fileSystems[fs.name]) {
const result = await this.fileSystems[fs.name].set() const result = await this.fileSystems[fs.name].set()
if (result) return this.fileSystems[fs.name] if (result) return this.fileSystems[fs.name]
} }
}
return null
} }
return null
}
} }

@ -3,189 +3,189 @@ import JSZip from "jszip"
import { fileSystem } from "../fileSystem" import { fileSystem } from "../fileSystem"
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
export class fileSystemUtility { export class fileSystemUtility {
migrate = async (fsFrom: fileSystem, fsTo: fileSystem) => { migrate = async (fsFrom: fileSystem, fsTo: fileSystem) => {
try { try {
await fsFrom.checkWorkspaces() await fsFrom.checkWorkspaces()
await fsTo.checkWorkspaces() await fsTo.checkWorkspaces()
if (fsTo.hasWorkSpaces) { if (fsTo.hasWorkSpaces) {
console.log(`${fsTo.name} already has files`) console.log(`${fsTo.name} already has files`)
return true return true
} }
if (!fsFrom.hasWorkSpaces) { if (!fsFrom.hasWorkSpaces) {
console.log('no files to migrate') console.log('no files to migrate')
return true return true
} }
const fromFiles = await this.copyFolderToJson('/', null, null, fsFrom.fs) const fromFiles = await this.copyFolderToJson('/', null, null, fsFrom.fs)
await this.populateWorkspace(fromFiles, fsTo.fs) await this.populateWorkspace(fromFiles, fsTo.fs)
const toFiles = await this.copyFolderToJson('/', null, null, fsTo.fs) const toFiles = await this.copyFolderToJson('/', null, null, fsTo.fs)
if (hashMessage(JSON.stringify(toFiles)) === hashMessage(JSON.stringify(fromFiles))) { if (hashMessage(JSON.stringify(toFiles)) === hashMessage(JSON.stringify(fromFiles))) {
console.log('file migration successful') console.log('file migration successful')
return true return true
} else { } else {
_paq.push(['trackEvent', 'Migrate', 'error', 'hash mismatch']) _paq.push(['trackEvent', 'Migrate', 'error', 'hash mismatch'])
console.log('file migration failed falling back to ' + fsFrom.name) console.log('file migration failed falling back to ' + fsFrom.name)
fsTo.loaded = false fsTo.loaded = false
return false return false
} }
} catch (err) { } catch (err) {
console.log(err) console.log(err)
_paq.push(['trackEvent', 'Migrate', 'error', err && err.message]) _paq.push(['trackEvent', 'Migrate', 'error', err && err.message])
console.log('file migration failed falling back to ' + fsFrom.name) console.log('file migration failed falling back to ' + fsFrom.name)
fsTo.loaded = false fsTo.loaded = false
return false return false
}
} }
}
downloadBackup = async (fs: fileSystem) => {
try { downloadBackup = async (fs: fileSystem) => {
const zip = new JSZip() try {
zip.file("readme.txt", "This is a Remix backup file.\nThis zip should be used by the restore backup tool in Remix.\nThe .workspaces directory contains your workspaces.") const zip = new JSZip()
await fs.checkWorkspaces() zip.file("readme.txt", "This is a Remix backup file.\nThis zip should be used by the restore backup tool in Remix.\nThe .workspaces directory contains your workspaces.")
await this.copyFolderToJson('/', null, null, fs.fs, ({ path, content }) => { await fs.checkWorkspaces()
zip.file(path, content) 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 blob = await zip.generateAsync({ type: 'blob' })
const date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate() const today = new Date()
const time = today.getHours() + 'h' + today.getMinutes() + 'min' const date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate()
this.saveAs(blob, `remix-backup-at-${time}-${date}.zip`) const time = today.getHours() + 'h' + today.getMinutes() + 'min'
_paq.push(['trackEvent','Backup','download','preload']) this.saveAs(blob, `remix-backup-at-${time}-${date}.zip`)
} catch (err) { _paq.push(['trackEvent','Backup','download','preload'])
_paq.push(['trackEvent','Backup','error',err && err.message]) } catch (err) {
console.log(err) _paq.push(['trackEvent','Backup','error',err && err.message])
} console.log(err)
} }
}
populateWorkspace = async (json, fs) => {
for (const item in json) { populateWorkspace = async (json, fs) => {
const isFolder = json[item].content === undefined for (const item in json) {
if (isFolder) { const isFolder = json[item].content === undefined
await this.createDir(item, fs) if (isFolder) {
await this.populateWorkspace(json[item].children, fs) await this.createDir(item, fs)
} else { await this.populateWorkspace(json[item].children, fs)
await fs.writeFile(item, json[item].content, 'utf8') } else {
} await fs.writeFile(item, json[item].content, 'utf8')
} }
} }
}
/** /**
* copy the folder recursively * copy the folder recursively
* @param {string} path is the folder to be copied over * @param {string} path is the folder to be copied over
* @param {Function} visitFile is a function called for each visited files * @param {Function} visitFile is a function called for each visited files
* @param {Function} visitFolder is a function called for each visited folders * @param {Function} visitFolder is a function called for each visited folders
*/ */
copyFolderToJson = async (path, visitFile, visitFolder, fs, cb = null) => { copyFolderToJson = async (path, visitFile, visitFolder, fs, cb = null) => {
visitFile = visitFile || (() => { }) visitFile = visitFile || (() => { })
visitFolder = visitFolder || (() => { }) visitFolder = visitFolder || (() => { })
return await this._copyFolderToJsonInternal(path, visitFile, visitFolder, fs, cb) return await this._copyFolderToJsonInternal(path, visitFile, visitFolder, fs, cb)
} }
/** /**
* copy the folder recursively (internal use) * copy the folder recursively (internal use)
* @param {string} path is the folder to be copied over * @param {string} path is the folder to be copied over
* @param {Function} visitFile is a function called for each visited files * @param {Function} visitFile is a function called for each visited files
* @param {Function} visitFolder is a function called for each visited folders * @param {Function} visitFolder is a function called for each visited folders
*/ */
async _copyFolderToJsonInternal(path, visitFile, visitFolder, fs, cb) { async _copyFolderToJsonInternal(path, visitFile, visitFolder, fs, cb) {
visitFile = visitFile || function () { /* do nothing. */ } visitFile = visitFile || function () { /* do nothing. */ }
visitFolder = visitFolder || function () { /* do nothing. */ } visitFolder = visitFolder || function () { /* do nothing. */ }
const json = {} const json = {}
// path = this.removePrefix(path) // path = this.removePrefix(path)
if (await fs.exists(path)) { if (await fs.exists(path)) {
const items = await fs.readdir(path) const items = await fs.readdir(path)
visitFolder({ path }) visitFolder({ path })
if (items.length !== 0) { if (items.length !== 0) {
for (const item of items) { for (const item of items) {
const file: any = {} const file: any = {}
const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}` const curPath = `${path}${path.endsWith('/') ? '' : '/'}${item}`
if ((await fs.stat(curPath)).isDirectory()) { if ((await fs.stat(curPath)).isDirectory()) {
file.children = await this._copyFolderToJsonInternal(curPath, visitFile, visitFolder, fs, cb) file.children = await this._copyFolderToJsonInternal(curPath, visitFile, visitFolder, fs, cb)
} else { } else {
file.content = await fs.readFile(curPath, 'utf8') file.content = await fs.readFile(curPath, 'utf8')
if (cb) cb({ path: curPath, content: file.content }) if (cb) cb({ path: curPath, content: file.content })
visitFile({ path: curPath, content: file.content }) visitFile({ path: curPath, content: file.content })
} }
json[curPath] = file 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)
}
} }
}
} }
return json
saveAs = (blob, name) => { }
const node = document.createElement('a')
node.download = name createDir = async (path, fs) => {
node.rel = 'noopener' const paths = path.split('/')
node.href = URL.createObjectURL(blob) if (paths.length && paths[0] === '') paths.shift()
setTimeout(function () { URL.revokeObjectURL(node.href) }, 4E4) // 40s let currentCheck = ''
setTimeout(function () { for (const value of paths) {
try { currentCheck = currentCheck + (currentCheck ? '/' : '') + value
node.dispatchEvent(new MouseEvent('click')) if (!await fs.exists(currentCheck)) {
} catch (e) { await fs.mkdir(currentCheck)
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
} }
}
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 */ /* eslint-disable no-template-curly-in-string */
export const migrationTestData = { export const migrationTestData = {
'.workspaces': { '.workspaces': {
children: {
'.workspaces/default_workspace': {
children: { children: {
'.workspaces/default_workspace': { '.workspaces/default_workspace/README.txt': {
children: { content: 'TEST README'
'.workspaces/default_workspace/README.txt': { }
content: 'TEST README' }
} },
} '.workspaces/emptyspace': {
},
'.workspaces/emptyspace': {
}, },
'.workspaces/workspace_test': { '.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: { children: {
'.workspaces/workspace_test/TEST_README.txt': { '.workspaces/workspace_test/test_contracts/artifacts/Storage_metadata.json': {
content: 'TEST README' content: '{ "test": "data" }'
}, }
'.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" }'
}
}
}
}
}
} }
}
} }
}
} }
}
} }
}
} }

@ -2,90 +2,90 @@ import LightningFS from "@isomorphic-git/lightning-fs"
import { fileSystem } from "../fileSystem" import { fileSystem } from "../fileSystem"
export class IndexedDBStorage extends LightningFS { export class IndexedDBStorage extends LightningFS {
base: LightningFS.PromisifedFS base: LightningFS.PromisifedFS
addSlash: (file: string) => string 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> } 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) { constructor(name: string) {
super(name) super(name)
this.addSlash = (file) => { this.addSlash = (file) => {
if (!file.startsWith('/')) file = '/' + file if (!file.startsWith('/')) file = '/' + file
return 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))
}
}
} }
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 { export class indexedDBFileSystem extends fileSystem {
constructor() { constructor() {
super() super()
this.name = 'indexedDB' this.name = 'indexedDB'
} }
load = async () => { load = async () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const fs = new IndexedDBStorage('RemixFileSystem') const fs = new IndexedDBStorage('RemixFileSystem')
fs.init('RemixFileSystem') fs.init('RemixFileSystem')
this.fs = fs.extended this.fs = fs.extended
this.fsCallBack = fs this.fsCallBack = fs
this.loaded = true this.loaded = true
resolve(true) resolve(true)
} catch (e) { } catch (e) {
reject(e) reject(e)
} }
}) })
} }
test = async () => { test = async () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!window.indexedDB) { if (!window.indexedDB) {
this.available = false this.available = false
reject('No indexedDB on window') reject('No indexedDB on window')
} }
const request = window.indexedDB.open("RemixTestDataBase"); const request = window.indexedDB.open("RemixTestDataBase");
request.onerror = () => { request.onerror = () => {
this.available = false this.available = false
reject('Error creating test database') reject('Error creating test database')
}; };
request.onsuccess = () => { request.onsuccess = () => {
window.indexedDB.deleteDatabase("RemixTestDataBase"); window.indexedDB.deleteDatabase("RemixTestDataBase");
this.available = true this.available = true
resolve(true) resolve(true)
}; };
}) })
} }
} }

@ -2,56 +2,56 @@ import { fileSystem } from "../fileSystem";
export class localStorageFS extends fileSystem { export class localStorageFS extends fileSystem {
constructor() { constructor() {
super() super()
this.name = 'localstorage' this.name = 'localstorage'
} }
load = async () => { load = async () => {
const me = this const me = this
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const w = window as any const w = window as any
w.BrowserFS.install(window) w.BrowserFS.install(window)
w.BrowserFS.configure({ w.BrowserFS.configure({
fs: 'LocalStorage' fs: 'LocalStorage'
}, async function (e) { }, async function (e) {
if (e) { if (e) {
console.log('BrowserFS Error: ' + e) console.log('BrowserFS Error: ' + e)
reject(e) reject(e)
} else { } else {
me.fs = { ...window.require('fs') } me.fs = { ...window.require('fs') }
me.fsCallBack = window.require('fs') me.fsCallBack = window.require('fs')
me.fs.readdir = me.fs.readdirSync me.fs.readdir = me.fs.readdirSync
me.fs.readFile = me.fs.readFileSync me.fs.readFile = me.fs.readFileSync
me.fs.writeFile = me.fs.writeFileSync me.fs.writeFile = me.fs.writeFileSync
me.fs.stat = me.fs.statSync me.fs.stat = me.fs.statSync
me.fs.unlink = me.fs.unlinkSync me.fs.unlink = me.fs.unlinkSync
me.fs.rmdir = me.fs.rmdirSync me.fs.rmdir = me.fs.rmdirSync
me.fs.mkdir = me.fs.mkdirSync me.fs.mkdir = me.fs.mkdirSync
me.fs.rename = me.fs.renameSync me.fs.rename = me.fs.renameSync
me.fs.exists = me.fs.existsSync me.fs.exists = me.fs.existsSync
me.loaded = true me.loaded = true
resolve(true) resolve(true)
} }
})
} catch (e) {
console.log('BrowserFS is not ready!')
reject(e)
}
}) })
} } catch (e) {
console.log('BrowserFS is not ready!')
reject(e)
}
})
}
test = async () => { test = async () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const test = 'test'; const test = 'test';
try { try {
localStorage.setItem(test, test); localStorage.setItem(test, test);
localStorage.removeItem(test); localStorage.removeItem(test);
resolve(true) resolve(true)
} catch(e) { } catch(e) {
reject(e) reject(e)
} }
}) })
} }
} }

@ -2,25 +2,25 @@ import { WebsocketPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
const profile = { const profile = {
name: 'foundry', name: 'foundry',
displayName: 'Foundry', displayName: 'Foundry',
url: 'ws://127.0.0.1:65525', url: 'ws://127.0.0.1:65525',
methods: ['sync'], methods: ['sync'],
description: 'Using Remixd daemon, allow to access foundry API', description: 'Using Remixd daemon, allow to access foundry API',
kind: 'other', kind: 'other',
version: packageJson.version version: packageJson.version
} }
export class FoundryHandle extends WebsocketPlugin { export class FoundryHandle extends WebsocketPlugin {
constructor () { constructor () {
super(profile) super(profile)
} }
callPluginMethod(key, payload = []) { callPluginMethod(key, payload = []) {
if (this.socket.readyState !== this.socket.OPEN) { if (this.socket.readyState !== this.socket.OPEN) {
console.log(`${this.profile.name} connection is not opened.`) console.log(`${this.profile.name} connection is not opened.`)
return false return false
}
return super.callPluginMethod(key, payload)
} }
return super.callPluginMethod(key, payload)
}
} }

@ -2,25 +2,25 @@ import { WebsocketPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
const profile = { const profile = {
name: 'git', name: 'git',
displayName: 'Git', displayName: 'Git',
url: 'ws://127.0.0.1:65521', url: 'ws://127.0.0.1:65521',
methods: ['execute'], methods: ['execute'],
description: 'Using Remixd daemon, allow to access git API', description: 'Using Remixd daemon, allow to access git API',
kind: 'other', kind: 'other',
version: packageJson.version version: packageJson.version
} }
export class GitHandle extends WebsocketPlugin { export class GitHandle extends WebsocketPlugin {
constructor () { constructor () {
super(profile) super(profile)
} }
callPluginMethod(key, payload = []) { callPluginMethod(key, payload = []) {
if (this.socket.readyState !== this.socket.OPEN) { if (this.socket.readyState !== this.socket.OPEN) {
console.log(`${this.profile.name} connection is not opened.`) console.log(`${this.profile.name} connection is not opened.`)
return false return false
}
return super.callPluginMethod(key, payload)
} }
return super.callPluginMethod(key, payload)
}
} }

@ -2,25 +2,25 @@ import { WebsocketPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
const profile = { const profile = {
name: 'hardhat', name: 'hardhat',
displayName: 'Hardhat', displayName: 'Hardhat',
url: 'ws://127.0.0.1:65522', url: 'ws://127.0.0.1:65522',
methods: ['compile', 'sync'], methods: ['compile', 'sync'],
description: 'Using Remixd daemon, allow to access hardhat API', description: 'Using Remixd daemon, allow to access hardhat API',
kind: 'other', kind: 'other',
version: packageJson.version version: packageJson.version
} }
export class HardhatHandle extends WebsocketPlugin { export class HardhatHandle extends WebsocketPlugin {
constructor () { constructor () {
super(profile) super(profile)
} }
callPluginMethod(key, payload = []) { callPluginMethod(key, payload = []) {
if (this.socket.readyState !== this.socket.OPEN) { if (this.socket.readyState !== this.socket.OPEN) {
console.log(`${this.profile.name} connection is not opened.`) console.log(`${this.profile.name} connection is not opened.`)
return false return false
}
return super.callPluginMethod(key, payload)
} }
return super.callPluginMethod(key, payload)
}
} }

@ -2,209 +2,209 @@
const FileProvider = require('./fileProvider') const FileProvider = require('./fileProvider')
module.exports = class RemixDProvider extends FileProvider { module.exports = class RemixDProvider extends FileProvider {
constructor (appManager) { constructor (appManager) {
super('localhost') super('localhost')
this._appManager = appManager this._appManager = appManager
this.error = { EEXIST: 'File already exists' } this.error = { EEXIST: 'File already exists' }
this._isReady = false this._isReady = false
this._readOnlyFiles = {} this._readOnlyFiles = {}
this._readOnlyMode = false this._readOnlyMode = false
this.filesContent = {} this.filesContent = {}
this.files = {} this.files = {}
} }
_registerEvent () { _registerEvent () {
var remixdEvents = ['connecting', 'connected', 'errored', 'closed'] var remixdEvents = ['connecting', 'connected', 'errored', 'closed']
remixdEvents.forEach((event) => { remixdEvents.forEach((event) => {
this._appManager.on('remixd', event, (value) => { this._appManager.on('remixd', event, (value) => {
this.event.emit(event, value) this.event.emit(event, value)
}) })
}) })
this._appManager.on('remixd', 'folderAdded', (path) => { this._appManager.on('remixd', 'folderAdded', (path) => {
this.event.emit('folderAdded', path) this.event.emit('folderAdded', path)
}) })
this._appManager.on('remixd', 'fileAdded', (path) => { this._appManager.on('remixd', 'fileAdded', (path) => {
this.event.emit('fileAdded', path) this.event.emit('fileAdded', path)
}) })
this._appManager.on('remixd', 'fileChanged', (path) => { this._appManager.on('remixd', 'fileChanged', (path) => {
this.event.emit('fileChanged', path) this.event.emit('fileChanged', path)
}) })
this._appManager.on('remixd', 'fileRemoved', (path) => { this._appManager.on('remixd', 'fileRemoved', (path) => {
this.event.emit('fileRemoved', path) this.event.emit('fileRemoved', path)
}) })
this._appManager.on('remixd', 'fileRenamed', (oldPath, newPath) => { this._appManager.on('remixd', 'fileRenamed', (oldPath, newPath) => {
this.event.emit('fileRenamed', oldPath, newPath) this.event.emit('fileRenamed', oldPath, newPath)
}) })
this._appManager.on('remixd', 'rootFolderChanged', (path) => { this._appManager.on('remixd', 'rootFolderChanged', (path) => {
this.event.emit('rootFolderChanged', path) this.event.emit('rootFolderChanged', path)
})
this._appManager.on('remixd', 'removed', (path) => {
this.event.emit('fileRemoved', path)
})
this._appManager.on('remixd', 'changed', (path) => {
this.get(path, (_error, content) => {
this.event.emit('fileExternallyChanged', path, content)
})
})
}
isConnected () {
return this._isReady
}
close (cb) {
this._isReady = false
cb()
this.event.emit('disconnected')
}
preInit () {
this.event.emit('loadingLocalhost')
}
init (cb) {
if (this._isReady) return cb && cb()
this._appManager.call('remixd', 'folderIsReadOnly', {})
.then((result) => {
this._isReady = true
this._readOnlyMode = result
this.event.emit('readOnlyModeChanged', result)
this._registerEvent()
this.event.emit('connected')
cb && cb()
}).catch((error) => {
cb && cb(error)
})
}
exists (path) {
if (!this._isReady) throw new Error('provider not ready')
const unprefixedpath = this.removePrefix(path)
return this._appManager.call('remixd', 'exists', { path: unprefixedpath })
.then((result) => {
return result
})
.catch((error) => {
throw new Error(error)
})
}
async get (path, cb) {
if (!this._isReady) return cb && cb('provider not ready')
var unprefixedpath = this.removePrefix(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) {
if (!this._isReady) return cb && cb('provider not ready')
const unprefixedpath = this.removePrefix(path)
return this._appManager.call('remixd', 'set', { path: unprefixedpath, content: content }).then(async (result) => {
if (cb) return cb(null, result)
}).catch((error) => {
if (cb) return cb(error)
throw new Error(error)
})
}
async createDir (path, cb) {
if (!this._isReady) return cb && cb('provider not ready')
const unprefixedpath = this.removePrefix(path)
return this._appManager.call('remixd', 'createDir', { path: unprefixedpath })
}
isReadOnly (path) {
return this._readOnlyMode || this._readOnlyFiles[path] === 1
}
remove (path) {
return new Promise((resolve, reject) => {
if (!this._isReady) return reject(new Error('provider not ready'))
const unprefixedpath = this.removePrefix(path)
this._appManager.call('remixd', 'remove', { path: unprefixedpath })
.then(result => {
const path = unprefixedpath
delete this.filesContent[path]
resolve(true)
this.init()
}).catch(error => {
if (error) console.log(error)
resolve(false)
}) })
})
this._appManager.on('remixd', 'removed', (path) => { }
this.event.emit('fileRemoved', path)
}) rename (oldPath, newPath, isFolder) {
const unprefixedoldPath = this.removePrefix(oldPath)
this._appManager.on('remixd', 'changed', (path) => { const unprefixednewPath = this.removePrefix(newPath)
this.get(path, (_error, content) => { if (!this._isReady) return new Promise((resolve, reject) => reject(new Error('provider not ready')))
this.event.emit('fileExternallyChanged', path, content) return this._appManager.call('remixd', 'rename', { oldPath: unprefixedoldPath, newPath: unprefixednewPath })
}) .then(result => {
const newPath = unprefixednewPath
const oldPath = unprefixedoldPath
this.filesContent[newPath] = this.filesContent[oldPath]
delete this.filesContent[oldPath]
this.init(() => {
this.event.emit('fileRenamed', oldPath, newPath, isFolder)
}) })
} return result
}).catch(error => {
isConnected () { console.log(error)
return this._isReady if (this.error[error.code]) error = this.error[error.code]
} this.event.emit('fileRenamedError', this.error[error.code])
})
close (cb) { }
this._isReady = false
cb() isExternalFolder (path) {
this.event.emit('disconnected') return false
} }
preInit () { removePrefix (path) {
this.event.emit('loadingLocalhost') path = path.indexOf(this.type) === 0 ? path.replace(this.type, '') : path
} if (path[0] === '/') return path.substring(1)
if (path === '') return '/'
init (cb) { return path
if (this._isReady) return cb && cb() }
this._appManager.call('remixd', 'folderIsReadOnly', {})
.then((result) => { resolveDirectory (path, callback) {
this._isReady = true if (path[0] === '/') path = path.substring(1)
this._readOnlyMode = result const unprefixedpath = this.removePrefix(path)
this.event.emit('readOnlyModeChanged', result)
this._registerEvent() if (!this._isReady) return callback && callback('provider not ready')
this.event.emit('connected') this._appManager.call('remixd', 'resolveDirectory', { path: unprefixedpath }).then((result) => {
cb && cb() callback(null, result)
}).catch((error) => { }).catch(callback)
cb && cb(error) }
})
} async isDirectory (path) {
const unprefixedpath = this.removePrefix(path)
exists (path) { if (!this._isReady) throw new Error('provider not ready')
if (!this._isReady) throw new Error('provider not ready') return await this._appManager.call('remixd', 'isDirectory', { path: unprefixedpath })
const unprefixedpath = this.removePrefix(path) }
return this._appManager.call('remixd', 'exists', { path: unprefixedpath }) async isFile (path) {
.then((result) => { const unprefixedpath = this.removePrefix(path)
return result if (!this._isReady) throw new Error('provider not ready')
}) return await this._appManager.call('remixd', 'isFile', { path: unprefixedpath })
.catch((error) => { }
throw new Error(error)
})
}
async get (path, cb) {
if (!this._isReady) return cb && cb('provider not ready')
var unprefixedpath = this.removePrefix(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) {
if (!this._isReady) return cb && cb('provider not ready')
const unprefixedpath = this.removePrefix(path)
return this._appManager.call('remixd', 'set', { path: unprefixedpath, content: content }).then(async (result) => {
if (cb) return cb(null, result)
}).catch((error) => {
if (cb) return cb(error)
throw new Error(error)
})
}
async createDir (path, cb) {
if (!this._isReady) return cb && cb('provider not ready')
const unprefixedpath = this.removePrefix(path)
return this._appManager.call('remixd', 'createDir', { path: unprefixedpath })
}
isReadOnly (path) {
return this._readOnlyMode || this._readOnlyFiles[path] === 1
}
remove (path) {
return new Promise((resolve, reject) => {
if (!this._isReady) return reject(new Error('provider not ready'))
const unprefixedpath = this.removePrefix(path)
this._appManager.call('remixd', 'remove', { path: unprefixedpath })
.then(result => {
const path = unprefixedpath
delete this.filesContent[path]
resolve(true)
this.init()
}).catch(error => {
if (error) console.log(error)
resolve(false)
})
})
}
rename (oldPath, newPath, isFolder) {
const unprefixedoldPath = this.removePrefix(oldPath)
const unprefixednewPath = this.removePrefix(newPath)
if (!this._isReady) return new Promise((resolve, reject) => reject(new Error('provider not ready')))
return this._appManager.call('remixd', 'rename', { oldPath: unprefixedoldPath, newPath: unprefixednewPath })
.then(result => {
const newPath = unprefixednewPath
const oldPath = unprefixedoldPath
this.filesContent[newPath] = this.filesContent[oldPath]
delete this.filesContent[oldPath]
this.init(() => {
this.event.emit('fileRenamed', oldPath, newPath, isFolder)
})
return result
}).catch(error => {
console.log(error)
if (this.error[error.code]) error = this.error[error.code]
this.event.emit('fileRenamedError', this.error[error.code])
})
}
isExternalFolder (path) {
return false
}
removePrefix (path) {
path = path.indexOf(this.type) === 0 ? path.replace(this.type, '') : path
if (path[0] === '/') return path.substring(1)
if (path === '') return '/'
return path
}
resolveDirectory (path, callback) {
if (path[0] === '/') path = path.substring(1)
const unprefixedpath = this.removePrefix(path)
if (!this._isReady) return callback && callback('provider not ready')
this._appManager.call('remixd', 'resolveDirectory', { path: unprefixedpath }).then((result) => {
callback(null, result)
}).catch(callback)
}
async isDirectory (path) {
const unprefixedpath = this.removePrefix(path)
if (!this._isReady) throw new Error('provider not ready')
return await this._appManager.call('remixd', 'isDirectory', { path: unprefixedpath })
}
async isFile (path) {
const unprefixedpath = this.removePrefix(path)
if (!this._isReady) throw new Error('provider not ready')
return await this._appManager.call('remixd', 'isFile', { path: unprefixedpath })
}
} }

@ -2,26 +2,26 @@ import { WebsocketPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
const profile = { const profile = {
name: 'slither', name: 'slither',
displayName: 'Slither', displayName: 'Slither',
url: 'ws://127.0.0.1:65523', url: 'ws://127.0.0.1:65523',
methods: ['analyse'], methods: ['analyse'],
description: 'Using Remixd daemon, run slither static analysis', description: 'Using Remixd daemon, run slither static analysis',
kind: 'other', kind: 'other',
version: packageJson.version, version: packageJson.version,
documentation: 'https://remix-ide.readthedocs.io/en/latest/slither.html' documentation: 'https://remix-ide.readthedocs.io/en/latest/slither.html'
} }
export class SlitherHandle extends WebsocketPlugin { export class SlitherHandle extends WebsocketPlugin {
constructor () { constructor () {
super(profile) super(profile)
} }
callPluginMethod(key, payload = []) { callPluginMethod(key, payload = []) {
if (this.socket.readyState !== this.socket.OPEN) { if (this.socket.readyState !== this.socket.OPEN) {
console.log(`${this.profile.name} connection is not opened.`) console.log(`${this.profile.name} connection is not opened.`)
return false return false
}
return super.callPluginMethod(key, payload)
} }
return super.callPluginMethod(key, payload)
}
} }

@ -2,26 +2,26 @@ import { WebsocketPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
const profile = { const profile = {
name: 'truffle', name: 'truffle',
displayName: 'truffle', displayName: 'truffle',
url: 'ws://127.0.0.1:65524', url: 'ws://127.0.0.1:65524',
methods: ['compile', 'sync'], methods: ['compile', 'sync'],
description: 'Using Remixd daemon, allow to access truffle API', description: 'Using Remixd daemon, allow to access truffle API',
kind: 'other', kind: 'other',
version: packageJson.version version: packageJson.version
} }
export class TruffleHandle extends WebsocketPlugin { export class TruffleHandle extends WebsocketPlugin {
constructor () { constructor () {
super(profile) super(profile)
} }
callPluginMethod(key, payload = []) { callPluginMethod(key, payload = []) {
if (this.socket.readyState !== this.socket.OPEN) { if (this.socket.readyState !== this.socket.OPEN) {
console.log(`${this.profile.name} connection is not opened.`) console.log(`${this.profile.name} connection is not opened.`)
return false return false
}
return super.callPluginMethod(key, payload)
} }
return super.callPluginMethod(key, payload)
}
} }

@ -4,84 +4,84 @@ const EventManager = require('events')
const FileProvider = require('./fileProvider') const FileProvider = require('./fileProvider')
class WorkspaceFileProvider extends FileProvider { class WorkspaceFileProvider extends FileProvider {
constructor () { constructor () {
super('') super('')
this.workspacesPath = '.workspaces' this.workspacesPath = '.workspaces'
this.workspace = null this.workspace = null
this.event = new EventManager() this.event = new EventManager()
} }
setWorkspace (workspace) { setWorkspace (workspace) {
if (!workspace) return if (!workspace) return
workspace = workspace.replace(/^\/|\/$/g, '') // remove first and last slash workspace = workspace.replace(/^\/|\/$/g, '') // remove first and last slash
this.workspace = workspace this.workspace = workspace
} }
getWorkspace () { getWorkspace () {
return this.workspace return this.workspace
} }
isReady () { isReady () {
return this.workspace !== null return this.workspace !== null
} }
clearWorkspace () { clearWorkspace () {
this.workspace = null this.workspace = null
} }
removePrefix (path) { removePrefix (path) {
if (!path) path = '/' if (!path) path = '/'
path = path.replace(/^\/|\/$/g, '') // remove first and last slash path = path.replace(/^\/|\/$/g, '') // remove first and last slash
path = path.replace(/^\.\/+/, '') // remove ./ from start of string path = path.replace(/^\.\/+/, '') // remove ./ from start of string
if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path
path = super.removePrefix(path) path = super.removePrefix(path)
let ret = this.workspacesPath + '/' + this.workspace + '/' + (path === '/' ? '' : path) let ret = this.workspacesPath + '/' + this.workspace + '/' + (path === '/' ? '' : path)
ret = ret.replace(/^\/|\/$/g, '') ret = ret.replace(/^\/|\/$/g, '')
if (!this.isSubDirectory(this.workspacesPath + '/' + this.workspace, ret)) throw new Error('Cannot read/write to path outside workspace') if (!this.isSubDirectory(this.workspacesPath + '/' + this.workspace, ret)) throw new Error('Cannot read/write to path outside workspace')
return ret return ret
} }
resolveDirectory (path, callback) { resolveDirectory (path, callback) {
super.resolveDirectory(path, (error, files) => { super.resolveDirectory(path, (error, files) => {
if (error) return callback(error) if (error) return callback(error)
const unscoped = {} const unscoped = {}
for (const file in files) { for (const file in files) {
unscoped[file.replace(this.workspacesPath + '/' + this.workspace + '/', '')] = files[file] unscoped[file.replace(this.workspacesPath + '/' + this.workspace + '/', '')] = files[file]
} }
callback(null, unscoped) callback(null, unscoped)
}) })
} }
async copyFolderToJson (directory, visitFile, visitFolder) { async copyFolderToJson (directory, visitFile, visitFolder) {
visitFile = visitFile || function () { /* do nothing. */ } visitFile = visitFile || function () { /* do nothing. */ }
visitFolder = visitFolder || function () { /* do nothing. */ } visitFolder = visitFolder || function () { /* do nothing. */ }
const regex = new RegExp(`.workspaces/${this.workspace}/`, 'g') const regex = new RegExp(`.workspaces/${this.workspace}/`, 'g')
let json = await super._copyFolderToJsonInternal(directory, ({ path, content }) => { let json = await super._copyFolderToJsonInternal(directory, ({ path, content }) => {
visitFile({ path: path.replace(regex, ''), content }) visitFile({ path: path.replace(regex, ''), content })
}, ({ path }) => { }, ({ path }) => {
visitFolder({ path: path.replace(regex, '') }) visitFolder({ path: path.replace(regex, '') })
}) })
json = JSON.stringify(json).replace(regex, '') json = JSON.stringify(json).replace(regex, '')
return JSON.parse(json) return JSON.parse(json)
} }
_normalizePath (path) { _normalizePath (path) {
return path.replace(this.workspacesPath + '/' + this.workspace + '/', '') return path.replace(this.workspacesPath + '/' + this.workspace + '/', '')
} }
async createWorkspace (name) { async createWorkspace (name) {
try { try {
if (!name) name = 'default_workspace' if (!name) name = 'default_workspace'
const path = this.workspacesPath + '/' + name const path = this.workspacesPath + '/' + name
await super.forceCreateDir(path) await super.forceCreateDir(path)
this.setWorkspace(name) this.setWorkspace(name)
this.event.emit('createWorkspace', name) this.event.emit('createWorkspace', name)
} catch (e) { } catch (e) {
throw new Error(e) throw new Error(e)
}
} }
}
} }
module.exports = WorkspaceFileProvider module.exports = WorkspaceFileProvider

@ -28,43 +28,43 @@ const { SlitherHandle } = require('../files/slither-handle.js')
*/ */
const profile = { const profile = {
name: 'filePanel', name: 'filePanel',
displayName: 'File explorer', displayName: 'File explorer',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getAvailableWorkspaceName', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'], methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getAvailableWorkspaceName', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'],
events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'], events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'],
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
description: 'Remix IDE file explorer', description: 'Remix IDE file explorer',
kind: 'fileexplorer', kind: 'fileexplorer',
location: 'sidePanel', location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/file_explorer.html', documentation: 'https://remix-ide.readthedocs.io/en/latest/file_explorer.html',
version: packageJson.version, version: packageJson.version,
maintainedBy: 'Remix' maintainedBy: 'Remix'
} }
module.exports = class Filepanel extends ViewPlugin { module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) { constructor (appManager) {
super(profile) super(profile)
this.registry = Registry.getInstance() this.registry = Registry.getInstance()
this.fileProviders = this.registry.get('fileproviders').api this.fileProviders = this.registry.get('fileproviders').api
this.fileManager = this.registry.get('filemanager').api this.fileManager = this.registry.get('filemanager').api
this.el = document.createElement('div') this.el = document.createElement('div')
this.el.setAttribute('id', 'fileExplorerView') this.el.setAttribute('id', 'fileExplorerView')
this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager) this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager)
this.hardhatHandle = new HardhatHandle() this.hardhatHandle = new HardhatHandle()
this.foundryHandle = new FoundryHandle() this.foundryHandle = new FoundryHandle()
this.truffleHandle = new TruffleHandle() this.truffleHandle = new TruffleHandle()
this.slitherHandle = new SlitherHandle() this.slitherHandle = new SlitherHandle()
this.workspaces = [] this.workspaces = []
this.appManager = appManager this.appManager = appManager
this.currentWorkspaceMetadata = null this.currentWorkspaceMetadata = null
} }
render () { render () {
return <div id='fileExplorerView'><FileSystemProvider plugin={this} /></div> return <div id='fileExplorerView'><FileSystemProvider plugin={this} /></div>
} }
/** /**
* @param item { id: string, name: string, type?: string[], path?: string[], extension?: string[], pattern?: string[] } * @param item { id: string, name: string, type?: string[], path?: string[], extension?: string[], pattern?: string[] }
* typically: * typically:
* group 0 for file manipulations * group 0 for file manipulations
@ -77,112 +77,112 @@ module.exports = class Filepanel extends ViewPlugin {
* group 7 for generating resource files (UML, documentation, ...) * group 7 for generating resource files (UML, documentation, ...)
* @param callback (...args) => void * @param callback (...args) => void
*/ */
registerContextMenuItem (item) { registerContextMenuItem (item) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.emit('registerContextMenuItemReducerEvent', item, (err, data) => { this.emit('registerContextMenuItemReducerEvent', item, (err, data) => {
if (err) reject(err) if (err) reject(err)
else resolve(data) else resolve(data)
}) })
}) })
} }
removePluginActions (plugin) { removePluginActions (plugin) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.emit('removePluginActionsReducerEvent', plugin, (err, data) => { this.emit('removePluginActionsReducerEvent', plugin, (err, data) => {
if (err) reject(err) if (err) reject(err)
else resolve(data) else resolve(data)
}) })
}) })
} }
getCurrentWorkspace () { getCurrentWorkspace () {
return this.currentWorkspaceMetadata return this.currentWorkspaceMetadata
} }
getWorkspaces () { getWorkspaces () {
return this.workspaces return this.workspaces
} }
getAvailableWorkspaceName (name) { getAvailableWorkspaceName (name) {
if(!this.workspaces) return name if(!this.workspaces) return name
let index = 1 let index = 1
let workspace = this.workspaces.find(workspace => workspace.name === name + ' - ' + index) let workspace = this.workspaces.find(workspace => workspace.name === name + ' - ' + index)
while (workspace) { while (workspace) {
index++ index++
workspace = this.workspaces.find(workspace => workspace.name === name + ' - ' + index) workspace = this.workspaces.find(workspace => workspace.name === name + ' - ' + index)
} }
return name + ' - ' + index return name + ' - ' + index
} }
setWorkspaces (workspaces) { setWorkspaces (workspaces) {
this.workspaces = workspaces this.workspaces = workspaces
} }
createNewFile () { createNewFile () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.emit('createNewFileInputReducerEvent', '/', (err, data) => { this.emit('createNewFileInputReducerEvent', '/', (err, data) => {
if (err) reject(err) if (err) reject(err)
else resolve(data) else resolve(data)
}) })
}) })
} }
uploadFile (target) { uploadFile (target) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
return this.emit('uploadFileReducerEvent', '/', target, (err, data) => { return this.emit('uploadFileReducerEvent', '/', target, (err, data) => {
if (err) reject(err) if (err) reject(err)
else resolve(data) else resolve(data)
}) })
}) })
} }
createWorkspace (workspaceName, workspaceTemplateName, isEmpty) { createWorkspace (workspaceName, workspaceTemplateName, isEmpty) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.emit('createWorkspaceReducerEvent', workspaceName, workspaceTemplateName, isEmpty, (err, data) => { this.emit('createWorkspaceReducerEvent', workspaceName, workspaceTemplateName, isEmpty, (err, data) => {
if (err) reject(err) if (err) reject(err)
else resolve(data || true) else resolve(data || true)
}) })
}) })
} }
renameWorkspace (oldName, workspaceName) { renameWorkspace (oldName, workspaceName) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.emit('renameWorkspaceReducerEvent', oldName, workspaceName, (err, data) => { this.emit('renameWorkspaceReducerEvent', oldName, workspaceName, (err, data) => {
if (err) reject(err) if (err) reject(err)
else resolve(data || true) else resolve(data || true)
}) })
}) })
} }
deleteWorkspace (workspaceName) { deleteWorkspace (workspaceName) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.emit('deleteWorkspaceReducerEvent', workspaceName, (err, data) => { this.emit('deleteWorkspaceReducerEvent', workspaceName, (err, data) => {
if (err) reject(err) if (err) reject(err)
else resolve(data || true) else resolve(data || true)
}) })
}) })
} }
setWorkspace (workspace) { setWorkspace (workspace) {
const workspaceProvider = this.fileProviders.workspace const workspaceProvider = this.fileProviders.workspace
this.currentWorkspaceMetadata = { name: workspace.name, isLocalhost: workspace.isLocalhost, absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}` } this.currentWorkspaceMetadata = { name: workspace.name, isLocalhost: workspace.isLocalhost, absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}` }
if (workspace.name !== " - connect to localhost - ") { if (workspace.name !== " - connect to localhost - ") {
localStorage.setItem('currentWorkspace', workspace.name) localStorage.setItem('currentWorkspace', workspace.name)
} }
this.emit('setWorkspace', workspace) this.emit('setWorkspace', workspace)
} }
workspaceRenamed (oldName, workspaceName) { workspaceRenamed (oldName, workspaceName) {
this.emit('workspaceRenamed', oldName, workspaceName) this.emit('workspaceRenamed', oldName, workspaceName)
} }
workspaceDeleted (workspace) { workspaceDeleted (workspace) {
this.emit('workspaceDeleted', workspace) this.emit('workspaceDeleted', workspace)
} }
workspaceCreated (workspace) { workspaceCreated (workspace) {
this.emit('workspaceCreated', workspace) this.emit('workspaceCreated', workspace)
} }
/** end section */ /** end section */
} }

@ -4,9 +4,9 @@ import { EventEmitter } from 'events'
import { QueryParams } from '@remix-project/remix-lib' import { QueryParams } from '@remix-project/remix-lib'
const profile: Profile = { const profile: Profile = {
name: 'layout', name: 'layout',
description: 'layout', description: 'layout',
methods: ['minimize', 'maximiseSidePanel', 'resetSidePanel'] methods: ['minimize', 'maximiseSidePanel', 'resetSidePanel']
} }
interface panelState { interface panelState {
@ -28,90 +28,90 @@ export type PanelConfiguration = {
} }
export class Layout extends Plugin { export class Layout extends Plugin {
event: any event: any
panels: panels panels: panels
maximised: { [key: string]: boolean } maximised: { [key: string]: boolean }
constructor () { constructor () {
super(profile) super(profile)
this.maximised = {} this.maximised = {}
this.event = new EventEmitter() this.event = new EventEmitter()
} }
async onActivation (): Promise<void> { async onActivation (): Promise<void> {
this.on('fileManager', 'currentFileChanged', () => { this.on('fileManager', 'currentFileChanged', () => {
this.panels.editor.active = true this.panels.editor.active = true
this.panels.main.active = false this.panels.main.active = false
this.event.emit('change', null) this.event.emit('change', null)
}) })
this.on('tabs', 'openFile', () => { this.on('tabs', 'openFile', () => {
this.panels.editor.active = true this.panels.editor.active = true
this.panels.main.active = false this.panels.main.active = false
this.event.emit('change', null) this.event.emit('change', null)
}) })
this.on('tabs', 'switchApp', (name: string) => { this.on('tabs', 'switchApp', (name: string) => {
this.call('mainPanel', 'showContent', name) this.call('mainPanel', 'showContent', name)
this.panels.editor.active = false this.panels.editor.active = false
this.panels.main.active = true this.panels.main.active = true
this.event.emit('change', null) this.event.emit('change', null)
}) })
this.on('tabs', 'closeApp', (name: string) => { this.on('tabs', 'closeApp', (name: string) => {
this.panels.editor.active = true this.panels.editor.active = true
this.panels.main.active = false this.panels.main.active = false
this.event.emit('change', null) this.event.emit('change', null)
}) })
this.on('manager', 'activate', (profile: Profile) => { this.on('manager', 'activate', (profile: Profile) => {
switch (profile.name) { switch (profile.name) {
case 'filePanel': case 'filePanel':
this.call('menuicons', 'select', 'filePanel') this.call('menuicons', 'select', 'filePanel')
break break
} }
}) })
this.on('sidePanel', 'focusChanged', async (name) => { this.on('sidePanel', 'focusChanged', async (name) => {
const current = await this.call('sidePanel', 'currentFocus') const current = await this.call('sidePanel', 'currentFocus')
if (this.maximised[current]) { if (this.maximised[current]) {
this.event.emit('maximisesidepanel') this.event.emit('maximisesidepanel')
} else { } else {
this.event.emit('resetsidepanel') this.event.emit('resetsidepanel')
} }
}) })
document.addEventListener('keypress', e => { document.addEventListener('keypress', e => {
if (e.shiftKey && e.ctrlKey) { if (e.shiftKey && e.ctrlKey) {
if (e.code === 'KeyF') { if (e.code === 'KeyF') {
// Ctrl+Shift+F // Ctrl+Shift+F
this.call('menuicons', 'select', 'filePanel') this.call('menuicons', 'select', 'filePanel')
} else if (e.code === 'KeyA') { } else if (e.code === 'KeyA') {
// Ctrl+Shift+A // Ctrl+Shift+A
this.call('menuicons', 'select', 'pluginManager') this.call('menuicons', 'select', 'pluginManager')
}
e.preventDefault()
}
})
const queryParams = new QueryParams()
const params = queryParams.get() as PanelConfiguration
if (params.minimizeterminal || params.embed) {
this.panels.terminal.minimized = true
this.event.emit('change', this.panels)
this.emit('change', this.panels)
}
if (params.minimizesidepanel || params.embed) {
this.event.emit('minimizesidepanel')
} }
e.preventDefault()
}
})
const queryParams = new QueryParams()
const params = queryParams.get() as PanelConfiguration
if (params.minimizeterminal || params.embed) {
this.panels.terminal.minimized = true
this.event.emit('change', this.panels)
this.emit('change', this.panels)
} }
if (params.minimizesidepanel || params.embed) {
minimize (name: string, minimized:boolean): void { this.event.emit('minimizesidepanel')
this.panels[name].minimized = minimized
this.event.emit('change', null)
} }
}
async maximiseSidePanel () { minimize (name: string, minimized:boolean): void {
this.event.emit('maximisesidepanel') this.panels[name].minimized = minimized
const current = await this.call('sidePanel', 'currentFocus') this.event.emit('change', null)
this.maximised[current] = true }
}
async resetSidePanel () { async maximiseSidePanel () {
this.event.emit('resetsidepanel') this.event.emit('maximisesidepanel')
const current = await this.call('sidePanel', 'currentFocus') const current = await this.call('sidePanel', 'currentFocus')
this.maximised[current] = false this.maximised[current] = true
} }
async resetSidePanel () {
this.event.emit('resetsidepanel')
const current = await this.call('sidePanel', 'currentFocus')
this.maximised[current] = false
}
} }

@ -5,359 +5,359 @@ import { PluginViewWrapper, getPathIcon } from '@remix-ui/helper'
const EventEmitter = require('events') const EventEmitter = require('events')
const profile = { const profile = {
name: 'tabs', name: 'tabs',
methods: ['focus'], methods: ['focus'],
kind: 'other' kind: 'other'
} }
export class TabProxy extends Plugin { export class TabProxy extends Plugin {
constructor (fileManager, editor) { constructor (fileManager, editor) {
super(profile) super(profile)
this.event = new EventEmitter() this.event = new EventEmitter()
this.fileManager = fileManager this.fileManager = fileManager
this.editor = editor this.editor = editor
this.data = {} this.data = {}
this._view = {} this._view = {}
this._handlers = {} this._handlers = {}
this.loadedTabs = [] this.loadedTabs = []
this.dispatch = null this.dispatch = null
this.themeQuality = 'dark' this.themeQuality = 'dark'
} }
async onActivation () { async onActivation () {
this.on('theme', 'themeChanged', (theme) => { this.on('theme', 'themeChanged', (theme) => {
this.themeQuality = theme.quality this.themeQuality = theme.quality
// update invert for all icons // update invert for all icons
this.renderComponent() this.renderComponent()
}) })
this.on('fileManager', 'filesAllClosed', () => { this.on('fileManager', 'filesAllClosed', () => {
this.call('manager', 'activatePlugin', 'home') this.call('manager', 'activatePlugin', 'home')
this.focus('home') this.focus('home')
}) })
this.on('fileManager', 'fileRemoved', (name) => { this.on('fileManager', 'fileRemoved', (name) => {
const workspace = this.fileManager.currentWorkspace() const workspace = this.fileManager.currentWorkspace()
if (this.fileManager.mode === 'browser') { if (this.fileManager.mode === 'browser') {
name = name.startsWith(workspace + '/') ? name : workspace + '/' + name name = name.startsWith(workspace + '/') ? name : workspace + '/' + name
// If deleted file is not current file and not an active tab in editor, // If deleted file is not current file and not an active tab in editor,
// ensure current file is active in the editor // ensure current file is active in the editor
if (this.fileManager.currentFile() && name !== this.fileManager.currentFile()) { if (this.fileManager.currentFile() && name !== this.fileManager.currentFile()) {
const currentFile = this.fileManager.currentFile() const currentFile = this.fileManager.currentFile()
const currentFileTabPath = currentFile.startsWith(workspace + '/') ? currentFile : workspace + '/' + currentFile const currentFileTabPath = currentFile.startsWith(workspace + '/') ? currentFile : workspace + '/' + currentFile
this.removeTab(name, { name: currentFileTabPath }) this.removeTab(name, { name: currentFileTabPath })
} else this.removeTab(name) } else this.removeTab(name)
} else { } else {
name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name
this.removeTab(name) this.removeTab(name)
} }
})
this.on('fileManager', 'fileClosed', (name) => {
const workspace = this.fileManager.currentWorkspace()
if (this.fileManager.mode === 'browser') {
name = name.startsWith(workspace + '/') ? name : workspace + '/' + name
let tabIndex = this.loadedTabs.findIndex(tab => tab.name === name)
// If tab doesn't exist, check if tab is opened because of abrupt disconnection with remixd
if (tabIndex === -1) {
const nameArray = name.split('/')
nameArray.shift()
name = 'localhost' + '/' + nameArray.join('/')
tabIndex = this.loadedTabs.findIndex(tab => tab.name === name)
if(tabIndex !== -1) this.removeTab(name)
} else this.removeTab(name)
} else {
name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name
this.removeTab(name)
}
})
this.on('fileManager', 'currentFileChanged', (file) => {
const workspace = this.fileManager.currentWorkspace()
if (this.fileManager.mode === 'browser') {
const workspacePath = workspace + '/' + file
if (this._handlers[workspacePath]) {
this.tabsApi.activateTab(workspacePath)
return
}
this.addTab(workspacePath, '', async () => {
await this.fileManager.open(file)
this.event.emit('openFile', file)
this.emit('openFile', file)
},
async () => {
await this.fileManager.closeFile(file)
this.event.emit('closeFile', file)
this.emit('closeFile', file)
}) })
this.tabsApi.activateTab(workspacePath)
} else {
const path = file.startsWith(this.fileManager.mode + '/') ? file : this.fileManager.mode + '/' + file
this.on('fileManager', 'fileClosed', (name) => { if (this._handlers[path]) {
const workspace = this.fileManager.currentWorkspace() this.tabsApi.activateTab(path)
if (this.fileManager.mode === 'browser') { return
name = name.startsWith(workspace + '/') ? name : workspace + '/' + name }
let tabIndex = this.loadedTabs.findIndex(tab => tab.name === name) this.addTab(path, '', async () => {
await this.fileManager.open(file)
// If tab doesn't exist, check if tab is opened because of abrupt disconnection with remixd this.event.emit('openFile', file)
if (tabIndex === -1) { this.emit('openFile', file)
const nameArray = name.split('/') },
nameArray.shift() async () => {
name = 'localhost' + '/' + nameArray.join('/') await this.fileManager.closeFile(file)
tabIndex = this.loadedTabs.findIndex(tab => tab.name === name) this.event.emit('closeFile', file)
if(tabIndex !== -1) this.removeTab(name) this.emit('closeFile', file)
} else this.removeTab(name)
} else {
name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name
this.removeTab(name)
}
}) })
this.tabsApi.activateTab(path)
this.on('fileManager', 'currentFileChanged', (file) => { }
const workspace = this.fileManager.currentWorkspace() })
if (this.fileManager.mode === 'browser') { this.on('fileManager', 'fileRenamed', (oldName, newName, isFolder) => {
const workspacePath = workspace + '/' + file const workspace = this.fileManager.currentWorkspace()
if (this._handlers[workspacePath]) { if (this.fileManager.mode === 'browser') {
this.tabsApi.activateTab(workspacePath) if (isFolder) {
return for (const tab of this.loadedTabs) {
} if (tab.name.indexOf(workspace + '/' + oldName + '/') === 0) {
this.addTab(workspacePath, '', async () => { const newTabName = workspace + '/' + newName + tab.name.slice(workspace + '/' + oldName.length, tab.name.length)
await this.fileManager.open(file) this.renameTab(tab.name, newTabName)
this.event.emit('openFile', file)
this.emit('openFile', file)
},
async () => {
await this.fileManager.closeFile(file)
this.event.emit('closeFile', file)
this.emit('closeFile', file)
})
this.tabsApi.activateTab(workspacePath)
} else {
const path = file.startsWith(this.fileManager.mode + '/') ? file : this.fileManager.mode + '/' + file
if (this._handlers[path]) {
this.tabsApi.activateTab(path)
return
}
this.addTab(path, '', async () => {
await this.fileManager.open(file)
this.event.emit('openFile', file)
this.emit('openFile', file)
},
async () => {
await this.fileManager.closeFile(file)
this.event.emit('closeFile', file)
this.emit('closeFile', file)
})
this.tabsApi.activateTab(path)
} }
}) }
return
this.on('fileManager', 'fileRenamed', (oldName, newName, isFolder) => { }
const workspace = this.fileManager.currentWorkspace() // should change the tab title too
this.renameTab(workspace + '/' + oldName, workspace + '/' + newName)
if (this.fileManager.mode === 'browser') { } else {
if (isFolder) { if (isFolder) {
for (const tab of this.loadedTabs) { for (const tab of this.loadedTabs) {
if (tab.name.indexOf(workspace + '/' + oldName + '/') === 0) { if (tab.name.indexOf(this.fileManager.mode + '/' + oldName + '/') === 0) {
const newTabName = workspace + '/' + newName + tab.name.slice(workspace + '/' + oldName.length, tab.name.length) const newTabName = this.fileManager.mode + '/' + newName + tab.name.slice(this.fileManager.mode + '/' + oldName.length, tab.name.length)
this.renameTab(tab.name, newTabName) this.renameTab(tab.name, newTabName)
}
}
return
}
// should change the tab title too
this.renameTab(workspace + '/' + oldName, workspace + '/' + newName)
} else {
if (isFolder) {
for (const tab of this.loadedTabs) {
if (tab.name.indexOf(this.fileManager.mode + '/' + oldName + '/') === 0) {
const newTabName = this.fileManager.mode + '/' + newName + tab.name.slice(this.fileManager.mode + '/' + oldName.length, tab.name.length)
this.renameTab(tab.name, newTabName)
}
}
return
}
// should change the tab title too
this.renameTab(this.fileManager.mode + '/' + oldName, this.fileManager.mode + '/' + newName)
} }
}) }
return
this.on('manager', 'pluginActivated', ({ name, location, displayName, icon, description }) => { }
if (location === 'mainPanel') { // should change the tab title too
this.addTab( this.renameTab(this.fileManager.mode + '/' + oldName, this.fileManager.mode + '/' + newName)
name, }
displayName, })
() => this.emit('switchApp', name),
() => { this.on('manager', 'pluginActivated', ({ name, location, displayName, icon, description }) => {
if (name === 'home' && this.loadedTabs.length === 1 && this.loadedTabs[0].id === "home") { if (location === 'mainPanel') {
const files = Object.keys(this.editor.sessions) this.addTab(
files.forEach(filepath => this.editor.discard(filepath)) name,
} displayName,
this.emit('closeApp', name) () => this.emit('switchApp', name),
this.call('manager', 'deactivatePlugin', name) () => {
}, if (name === 'home' && this.loadedTabs.length === 1 && this.loadedTabs[0].id === "home") {
icon, const files = Object.keys(this.editor.sessions)
description files.forEach(filepath => this.editor.discard(filepath))
)
this.switchTab(name)
} }
}) this.emit('closeApp', name)
this.call('manager', 'deactivatePlugin', name)
this.on('manager', 'pluginDeactivated', (profile) => { },
this.removeTab(profile.name) icon,
}) description
)
this.on('fileDecorator', 'fileDecoratorsChanged', async (items) => { this.switchTab(name)
this.tabsApi.setFileDecorations(items) }
}) })
this.on('manager', 'pluginDeactivated', (profile) => {
this.removeTab(profile.name)
})
this.on('fileDecorator', 'fileDecoratorsChanged', async (items) => {
this.tabsApi.setFileDecorations(items)
})
try { try {
this.themeQuality = (await this.call('theme', 'currentTheme') ).quality this.themeQuality = (await this.call('theme', 'currentTheme') ).quality
} catch (e) { } catch (e) {
console.log('theme plugin has an issue: ', e) console.log('theme plugin has an issue: ', e)
}
this.renderComponent()
} }
this.renderComponent()
focus (name) { }
this.emit('switchApp', name)
this.tabsApi.activateTab(name) focus (name) {
this.emit('switchApp', name)
this.tabsApi.activateTab(name)
}
switchTab (tabName) {
if (this._handlers[tabName]) {
this._handlers[tabName].switchTo()
this.tabsApi.activateTab(tabName)
} }
}
switchTab (tabName) {
if (this._handlers[tabName]) { switchNextTab () {
this._handlers[tabName].switchTo() const active = this.tabsApi.active()
this.tabsApi.activateTab(tabName) if (active && this._handlers[active]) {
} const handlers = Object.keys(this._handlers)
let i = handlers.indexOf(active)
if (i >= 0) {
i = handlers[i + 1] ? i + 1 : 0
this.switchTab(handlers[i])
}
} }
}
switchNextTab () {
const active = this.tabsApi.active() switchPreviousTab () {
if (active && this._handlers[active]) { const active = this.tabsApi.active()
const handlers = Object.keys(this._handlers) if (active && this._handlers[active]) {
let i = handlers.indexOf(active) const handlers = Object.keys(this._handlers)
if (i >= 0) { let i = handlers.indexOf(active)
i = handlers[i + 1] ? i + 1 : 0 if (i >= 0) {
this.switchTab(handlers[i]) i = handlers[i - 1] ? i - 1 : handlers.length - 1
} this.switchTab(handlers[i])
} }
} }
}
switchPreviousTab () {
const active = this.tabsApi.active() renameTab (oldName, newName) {
if (active && this._handlers[active]) { // The new tab is being added by FileManager
const handlers = Object.keys(this._handlers) this.removeTab(oldName)
let i = handlers.indexOf(active) }
if (i >= 0) {
i = handlers[i - 1] ? i - 1 : handlers.length - 1 addTab (name, title, switchTo, close, icon, description = '') {
this.switchTab(handlers[i]) if (this._handlers[name]) return this.renderComponent()
}
} var slash = name.split('/')
} const tabPath = slash.reverse()
const tempTitle = []
renameTab (oldName, newName) {
// The new tab is being added by FileManager if (!title) {
this.removeTab(oldName) for (let i = 0; i < tabPath.length; i++) {
} tempTitle.push(tabPath[i])
const formatPath = [...tempTitle].reverse()
addTab (name, title, switchTo, close, icon, description = '') { const index = this.loadedTabs.findIndex(({ title }) => title === formatPath.join('/'))
if (this._handlers[name]) return this.renderComponent()
if (index === -1) {
var slash = name.split('/') title = formatPath.join('/')
const tabPath = slash.reverse() const titleLength = formatPath.length
const tempTitle = [] this.loadedTabs.push({
id: name,
if (!title) { name,
for (let i = 0; i < tabPath.length; i++) { title,
tempTitle.push(tabPath[i]) icon,
const formatPath = [...tempTitle].reverse() tooltip: name,
const index = this.loadedTabs.findIndex(({ title }) => title === formatPath.join('/')) iconClass: getPathIcon(name)
})
if (index === -1) { formatPath.shift()
title = formatPath.join('/') if (formatPath.length > 0) {
const titleLength = formatPath.length const index = this.loadedTabs.findIndex(({ title }) => title === formatPath.join('/'))
this.loadedTabs.push({ if (index > -1) {
id: name, const duplicateTabName = this.loadedTabs[index].name
name, const duplicateTabTooltip = this.loadedTabs[index].description
title, const duplicateTabPath = duplicateTabName.split('/')
icon, const duplicateTabFormatPath = [...duplicateTabPath].reverse()
tooltip: name, const duplicateTabTitle = duplicateTabFormatPath.slice(0, titleLength).reverse().join('/')
iconClass: getPathIcon(name) this.loadedTabs[index] = {
}) id: duplicateTabName,
formatPath.shift() name: duplicateTabName,
if (formatPath.length > 0) { title: duplicateTabTitle,
const index = this.loadedTabs.findIndex(({ title }) => title === formatPath.join('/'))
if (index > -1) {
const duplicateTabName = this.loadedTabs[index].name
const duplicateTabTooltip = this.loadedTabs[index].description
const duplicateTabPath = duplicateTabName.split('/')
const duplicateTabFormatPath = [...duplicateTabPath].reverse()
const duplicateTabTitle = duplicateTabFormatPath.slice(0, titleLength).reverse().join('/')
this.loadedTabs[index] = {
id: duplicateTabName,
name: duplicateTabName,
title: duplicateTabTitle,
icon,
tooltip: duplicateTabTooltip || duplicateTabTitle,
iconClass: getPathIcon(duplicateTabName)
}
}
}
break
}
}
} else {
this.loadedTabs.push({
id: name,
name,
title,
icon, icon,
tooltip: description || title, tooltip: duplicateTabTooltip || duplicateTabTitle,
iconClass: getPathIcon(name) iconClass: getPathIcon(duplicateTabName)
}) }
}
this.renderComponent()
this._handlers[name] = { switchTo, close }
}
removeTab (name, currentFileTab) {
delete this._handlers[name]
let previous = currentFileTab
this.loadedTabs = this.loadedTabs.filter((tab, index) => {
if (!previous && tab.name === name) {
if(index - 1 >= 0 && this.loadedTabs[index - 1])
previous = this.loadedTabs[index - 1]
else if (index + 1 && this.loadedTabs[index + 1])
previous = this.loadedTabs[index + 1]
} }
return tab.name !== name }
}) break
this.renderComponent() }
if (previous) this.switchTab(previous.name) }
} } else {
this.loadedTabs.push({
addHandler (type, fn) { id: name,
this.handlers[type] = fn name,
} title,
icon,
setDispatch (dispatch) { tooltip: description || title,
this.dispatch = dispatch iconClass: getPathIcon(name)
this.renderComponent() })
} }
updateComponent(state) { this.renderComponent()
return <TabsUI this._handlers[name] = { switchTo, close }
plugin={state.plugin} }
tabs={state.loadedTabs}
onSelect={state.onSelect} removeTab (name, currentFileTab) {
onClose={state.onClose} delete this._handlers[name]
onZoomIn={state.onZoomIn} let previous = currentFileTab
onZoomOut={state.onZoomOut} this.loadedTabs = this.loadedTabs.filter((tab, index) => {
onReady={state.onReady} if (!previous && tab.name === name) {
themeQuality={state.themeQuality} if(index - 1 >= 0 && this.loadedTabs[index - 1])
/> previous = this.loadedTabs[index - 1]
else if (index + 1 && this.loadedTabs[index + 1])
previous = this.loadedTabs[index + 1]
}
return tab.name !== name
})
this.renderComponent()
if (previous) this.switchTab(previous.name)
}
addHandler (type, fn) {
this.handlers[type] = fn
}
setDispatch (dispatch) {
this.dispatch = dispatch
this.renderComponent()
}
updateComponent(state) {
return <TabsUI
plugin={state.plugin}
tabs={state.loadedTabs}
onSelect={state.onSelect}
onClose={state.onClose}
onZoomIn={state.onZoomIn}
onZoomOut={state.onZoomOut}
onReady={state.onReady}
themeQuality={state.themeQuality}
/>
}
renderComponent () {
const onSelect = (index) => {
if (this.loadedTabs[index]) {
const name = this.loadedTabs[index].name
if (this._handlers[name]) this._handlers[name].switchTo()
this.emit('tabCountChanged', this.loadedTabs.length)
}
} }
renderComponent () { const onClose = (index) => {
const onSelect = (index) => { if (this.loadedTabs[index]) {
if (this.loadedTabs[index]) { const name = this.loadedTabs[index].name
const name = this.loadedTabs[index].name if (this._handlers[name]) this._handlers[name].close()
if (this._handlers[name]) this._handlers[name].switchTo() this.emit('tabCountChanged', this.loadedTabs.length)
this.emit('tabCountChanged', this.loadedTabs.length) }
}
}
const onClose = (index) => {
if (this.loadedTabs[index]) {
const name = this.loadedTabs[index].name
if (this._handlers[name]) this._handlers[name].close()
this.emit('tabCountChanged', this.loadedTabs.length)
}
}
const onZoomIn = () => this.editor.editorFontSize(1)
const onZoomOut = () => this.editor.editorFontSize(-1)
const onReady = (api) => { this.tabsApi = api }
this.dispatch({
plugin: this,
loadedTabs: this.loadedTabs,
onSelect,
onClose,
onZoomIn,
onZoomOut,
onReady,
themeQuality: this.themeQuality
})
} }
renderTabsbar () { const onZoomIn = () => this.editor.editorFontSize(1)
return <div><PluginViewWrapper plugin={this} /></div> const onZoomOut = () => this.editor.editorFontSize(-1)
}
const onReady = (api) => { this.tabsApi = api }
this.dispatch({
plugin: this,
loadedTabs: this.loadedTabs,
onSelect,
onClose,
onZoomIn,
onZoomOut,
onReady,
themeQuality: this.themeQuality
})
}
renderTabsbar () {
return <div><PluginViewWrapper plugin={this} /></div>
}
} }

@ -16,122 +16,122 @@ const KONSOLES = []
function register (api) { KONSOLES.push(api) } function register (api) { KONSOLES.push(api) }
const profile = { const profile = {
displayName: 'Terminal', displayName: 'Terminal',
name: 'terminal', name: 'terminal',
methods: ['log', 'logHtml'], methods: ['log', 'logHtml'],
events: [], events: [],
description: 'Remix IDE terminal', description: 'Remix IDE terminal',
version: packageJson.version version: packageJson.version
} }
class Terminal extends Plugin { class Terminal extends Plugin {
constructor (opts, api) { constructor (opts, api) {
super(profile) super(profile)
this.fileImport = new CompilerImports() this.fileImport = new CompilerImports()
this.event = new EventManager() this.event = new EventManager()
this.globalRegistry = Registry.getInstance() this.globalRegistry = Registry.getInstance()
this.element = document.createElement('div') this.element = document.createElement('div')
this.element.setAttribute('class', 'panel') this.element.setAttribute('class', 'panel')
this.element.setAttribute('id', 'terminal-view') this.element.setAttribute('id', 'terminal-view')
this.element.setAttribute('data-id', 'terminalContainer-view') this.element.setAttribute('data-id', 'terminalContainer-view')
this.eventsDecoder = this.globalRegistry.get('eventsDecoder').api this.eventsDecoder = this.globalRegistry.get('eventsDecoder').api
this.txListener = this.globalRegistry.get('txlistener').api this.txListener = this.globalRegistry.get('txlistener').api
this._deps = { this._deps = {
fileManager: this.globalRegistry.get('filemanager').api, fileManager: this.globalRegistry.get('filemanager').api,
editor: this.globalRegistry.get('editor').api, editor: this.globalRegistry.get('editor').api,
compilersArtefacts: this.globalRegistry.get('compilersartefacts').api, compilersArtefacts: this.globalRegistry.get('compilersartefacts').api,
offsetToLineColumnConverter: this.globalRegistry.get('offsettolinecolumnconverter').api offsetToLineColumnConverter: this.globalRegistry.get('offsettolinecolumnconverter').api
}
this.commandHelp = {
'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, ipfs or raw http',
'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'
}
this.blockchain = opts.blockchain
this.vm = vm
this._api = api
this._opts = opts
this.config = this.globalRegistry.get('config').api
this.version = packageJson.version
this.data = {
lineLength: opts.lineLength || 80, // ????
session: [],
activeFilters: { commands: {}, input: '' },
filterFns: {}
}
this._view = { el: null, bar: null, input: null, term: null, journal: null, cli: null }
this._components = {}
this._commands = {}
this.commands = {}
this._JOURNAL = []
this._jobs = []
this._INDEX = {}
this._INDEX.all = []
this._INDEX.allMain = []
this._INDEX.commands = {}
this._INDEX.commandsMain = {}
if (opts.shell) this._shell = opts.shell // ???
register(this)
this.event.register('debuggingRequested', async (hash) => {
// TODO should probably be in the run module
if (!await this._opts.appManager.isActive('debugger')) await this._opts.appManager.activatePlugin('debugger')
this.call('menuicons', 'select', 'debugger')
this.call('debugger', 'debug', hash)
})
this.dispatch = null
}
onActivation() {
this.renderComponent()
}
onDeactivation () {
this.off('scriptRunner', 'log')
this.off('scriptRunner', 'info')
this.off('scriptRunner', 'warn')
this.off('scriptRunner', 'error')
} }
this.commandHelp = {
logHtml (html) { 'remix.loadgist(id)': 'Load a gist in the file explorer.',
this.terminalApi.logHtml(html) 'remix.loadurl(url)': 'Load the given url in the file explorer. The url can be of type github, swarm, ipfs or raw http',
'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'
} }
this.blockchain = opts.blockchain
log (message, type) { this.vm = vm
this.terminalApi.log(message, type) this._api = api
} this._opts = opts
this.config = this.globalRegistry.get('config').api
setDispatch(dispatch) { this.version = packageJson.version
this.dispatch = dispatch this.data = {
} lineLength: opts.lineLength || 80, // ????
session: [],
render () { activeFilters: { commands: {}, input: '' },
return <div id='terminal-view' className='panel' data-id='terminalContainer-view'><PluginViewWrapper plugin={this}/></div> filterFns: {}
}
updateComponent(state) {
return <RemixUiTerminal
plugin={state.plugin}
onReady={state.onReady}
/>
}
renderComponent () {
const onReady = (api) => { this.terminalApi = api }
this.dispatch({
plugin: this,
onReady: onReady
})
} }
this._view = { el: null, bar: null, input: null, term: null, journal: null, cli: null }
this._components = {}
this._commands = {}
this.commands = {}
this._JOURNAL = []
this._jobs = []
this._INDEX = {}
this._INDEX.all = []
this._INDEX.allMain = []
this._INDEX.commands = {}
this._INDEX.commandsMain = {}
if (opts.shell) this._shell = opts.shell // ???
register(this)
this.event.register('debuggingRequested', async (hash) => {
// TODO should probably be in the run module
if (!await this._opts.appManager.isActive('debugger')) await this._opts.appManager.activatePlugin('debugger')
this.call('menuicons', 'select', 'debugger')
this.call('debugger', 'debug', hash)
})
this.dispatch = null
}
scroll2bottom () { onActivation() {
setTimeout(function () { this.renderComponent()
// do nothing. }
}, 0)
} onDeactivation () {
this.off('scriptRunner', 'log')
this.off('scriptRunner', 'info')
this.off('scriptRunner', 'warn')
this.off('scriptRunner', 'error')
}
logHtml (html) {
this.terminalApi.logHtml(html)
}
log (message, type) {
this.terminalApi.log(message, type)
}
setDispatch(dispatch) {
this.dispatch = dispatch
}
render () {
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 }
this.dispatch({
plugin: this,
onReady: onReady
})
}
scroll2bottom () {
setTimeout(function () {
// do nothing.
}, 0)
}
} }
module.exports = Terminal module.exports = Terminal

@ -8,279 +8,279 @@ import toml from 'toml'
import { filePathFilter, AnyFilter } from '@jsdevtools/file-path-filter' import { filePathFilter, AnyFilter } from '@jsdevtools/file-path-filter'
const profile = { const profile = {
name: 'codeFormatter', name: 'codeFormatter',
desciption: 'prettier plugin for Remix', desciption: 'prettier plugin for Remix',
methods: ['format'], methods: ['format'],
events: [''], events: [''],
version: '0.0.1' version: '0.0.1'
} }
const defaultOptions = { const defaultOptions = {
"overrides": [ "overrides": [
{ {
"files": "*.sol", "files": "*.sol",
"options": { "options": {
"printWidth": 80, "printWidth": 80,
"tabWidth": 4, "tabWidth": 4,
"useTabs": false, "useTabs": false,
"singleQuote": false, "singleQuote": false,
"bracketSpacing": false, "bracketSpacing": false,
} }
}, },
{ {
"files": "*.yml", "files": "*.yml",
"options": { "options": {
} }
}, },
{ {
"files": "*.yaml", "files": "*.yaml",
"options": { "options": {
} }
}, },
{ {
"files": "*.toml", "files": "*.toml",
"options": { "options": {
} }
}, },
{ {
"files": "*.json", "files": "*.json",
"options": { "options": {
} }
}, },
{ {
"files": "*.js", "files": "*.js",
"options": { "options": {
} }
}, },
{ {
"files": "*.ts", "files": "*.ts",
"options": { "options": {
} }
} }
] ]
} }
export class CodeFormat extends Plugin { export class CodeFormat extends Plugin {
prettier: any prettier: any
ts: any ts: any
babel: any babel: any
espree: any espree: any
yml: any yml: any
sol: any sol: any
constructor() { constructor() {
super(profile) super(profile)
} }
async format(file: string) { async format(file: string) {
// lazy load // lazy load
if (!this.prettier) { if (!this.prettier) {
this.prettier = await import('prettier/standalone') this.prettier = await import('prettier/standalone')
this.ts = await import('prettier/parser-typescript') this.ts = await import('prettier/parser-typescript')
this.babel = await import('prettier/parser-babel') this.babel = await import('prettier/parser-babel')
this.espree = await import('prettier/parser-espree') this.espree = await import('prettier/parser-espree')
this.yml = await import('prettier/parser-yaml') this.yml = await import('prettier/parser-yaml')
} }
try { try {
const content = await this.call('fileManager', 'readFile', file) const content = await this.call('fileManager', 'readFile', file)
if (!content) return if (!content) return
let parserName = '' let parserName = ''
let options: Options = { let options: Options = {
} }
switch (path.extname(file)) { switch (path.extname(file)) {
case '.sol': case '.sol':
parserName = 'solidity-parse' parserName = 'solidity-parse'
break break
case '.ts': case '.ts':
parserName = 'typescript' parserName = 'typescript'
options = { options = {
...options, ...options,
trailingComma: 'all', trailingComma: 'all',
semi: false, semi: false,
singleQuote: true, singleQuote: true,
quoteProps: 'as-needed', quoteProps: 'as-needed',
bracketSpacing: true, bracketSpacing: true,
arrowParens: 'always', arrowParens: 'always',
} }
break break
case '.js': case '.js':
parserName = "espree" parserName = "espree"
options = { options = {
...options, ...options,
semi: false, semi: false,
singleQuote: true, singleQuote: true,
} }
break break
case '.json': case '.json':
parserName = 'json' parserName = 'json'
break break
case '.yml': case '.yml':
parserName = 'yaml' parserName = 'yaml'
break break
case '.yaml': case '.yaml':
parserName = 'yaml' parserName = 'yaml'
break break
} }
if (file === '.prettierrc') { if (file === '.prettierrc') {
parserName = 'json' parserName = 'json'
} }
const possibleFileNames = [ const possibleFileNames = [
'.prettierrc', '.prettierrc',
'.prettierrc.json', '.prettierrc.json',
'.prettierrc.yaml', '.prettierrc.yaml',
'.prettierrc.yml', '.prettierrc.yml',
'.prettierrc.toml', '.prettierrc.toml',
'.prettierrc.js', '.prettierrc.js',
'.prettierrc.cjs', '.prettierrc.cjs',
'prettier.config.js', 'prettier.config.js',
'prettier.config.cjs', 'prettier.config.cjs',
'.prettierrc.json5', '.prettierrc.json5',
] ]
const prettierConfigFile = await findAsync(possibleFileNames, async (fileName) => { const prettierConfigFile = await findAsync(possibleFileNames, async (fileName) => {
const exists = await this.call('fileManager', 'exists', fileName) const exists = await this.call('fileManager', 'exists', fileName)
return exists return exists
}) })
let parsed = null let parsed = null
if (prettierConfigFile) { if (prettierConfigFile) {
let prettierConfig = await this.call('fileManager', 'readFile', prettierConfigFile) let prettierConfig = await this.call('fileManager', 'readFile', prettierConfigFile)
if (prettierConfig) { if (prettierConfig) {
if (prettierConfigFile.endsWith('.yaml') || prettierConfigFile.endsWith('.yml')) { if (prettierConfigFile.endsWith('.yaml') || prettierConfigFile.endsWith('.yml')) {
try { try {
parsed = yaml.load(prettierConfig) parsed = yaml.load(prettierConfig)
} catch (e) { } catch (e) {
// do nothing // do nothing
}
} else if (prettierConfigFile.endsWith('.toml')) {
try {
parsed = toml.parse(prettierConfig)
} catch (e) {
// do nothing
}
} else if (prettierConfigFile.endsWith('.json') || prettierConfigFile.endsWith('.json5')) {
try {
parsed = JSON.parse(prettierConfig)
} catch (e) {
// do nothing
}
} else if (prettierConfigFile === '.prettierrc') {
try {
parsed = JSON.parse(prettierConfig)
} catch (e) {
// do nothing
}
if (!parsed) {
try {
parsed = yaml.load(prettierConfig)
} catch (e) {
// do nothing
}
}
} else if (prettierConfigFile.endsWith('.js') || prettierConfigFile.endsWith('.cjs')) {
// remove any comments
prettierConfig = prettierConfig.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '')
// add quotes to keys
prettierConfig = prettierConfig.replace(/([a-zA-Z0-9_]+)(\s*):/g, '"$1"$2:')
// remove comma from last key
prettierConfig = prettierConfig.replace(/,(\s*})/g, '$1')
// remove any semi-colons
prettierConfig = prettierConfig.replace(/;/g, '')
// convert single quotes to double quotes
prettierConfig = prettierConfig.replace(/'/g, '"')
try {
parsed = JSON.parse(prettierConfig.replace('module.exports = ', '').replace('module.exports=', ''))
} catch (e) {
// do nothing
}
}
}
} else {
parsed = defaultOptions
await this.call('fileManager', 'writeFile', '.prettierrc.json', JSON.stringify(parsed, null, 2))
await this.call('notification', 'toast', 'A prettier config file has been created in the workspace.')
} }
} else if (prettierConfigFile.endsWith('.toml')) {
if (!parsed && prettierConfigFile) { try {
this.call('notification', 'toast', `Error parsing prettier config file: ${prettierConfigFile}`) parsed = toml.parse(prettierConfig)
} catch (e) {
// do nothing
}
} else if (prettierConfigFile.endsWith('.json') || prettierConfigFile.endsWith('.json5')) {
try {
parsed = JSON.parse(prettierConfig)
} catch (e) {
// do nothing
}
} else if (prettierConfigFile === '.prettierrc') {
try {
parsed = JSON.parse(prettierConfig)
} catch (e) {
// do nothing
}
if (!parsed) {
try {
parsed = yaml.load(prettierConfig)
} catch (e) {
// do nothing
}
}
} else if (prettierConfigFile.endsWith('.js') || prettierConfigFile.endsWith('.cjs')) {
// remove any comments
prettierConfig = prettierConfig.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '')
// add quotes to keys
prettierConfig = prettierConfig.replace(/([a-zA-Z0-9_]+)(\s*):/g, '"$1"$2:')
// remove comma from last key
prettierConfig = prettierConfig.replace(/,(\s*})/g, '$1')
// remove any semi-colons
prettierConfig = prettierConfig.replace(/;/g, '')
// convert single quotes to double quotes
prettierConfig = prettierConfig.replace(/'/g, '"')
try {
parsed = JSON.parse(prettierConfig.replace('module.exports = ', '').replace('module.exports=', ''))
} catch (e) {
// do nothing
} }
}
}
} else {
parsed = defaultOptions
await this.call('fileManager', 'writeFile', '.prettierrc.json', JSON.stringify(parsed, null, 2))
await this.call('notification', 'toast', 'A prettier config file has been created in the workspace.')
}
if (!parsed && prettierConfigFile) {
this.call('notification', 'toast', `Error parsing prettier config file: ${prettierConfigFile}`)
}
// merge options
if (parsed) {
options = {
...options,
...parsed,
}
}
// search for overrides // merge options
if (parsed && parsed.overrides) { if (parsed) {
const override = parsed.overrides.find((override) => { options = {
if (override.files) { ...options,
const pathFilter: AnyFilter = {} ...parsed,
pathFilter.include = setGlobalExpression(override.files) }
const filteredFiles = [file] }
.filter(filePathFilter(pathFilter))
if (filteredFiles.length) {
return true
}
}
})
const validParsers = ['typescript', 'babel', 'espree', 'solidity-parse', 'json', 'yaml', 'solidity-parse']
if (override && override.options && override.options.parser) {
if (validParsers.includes(override.options.parser)) {
parserName = override.options.parser
} else {
this.call('notification', 'toast', `Invalid parser: ${override.options.parser}! Valid options are ${validParsers.join(', ')}`)
}
delete override.options.parser
}
if (override) { // search for overrides
options = { if (parsed && parsed.overrides) {
...options, const override = parsed.overrides.find((override) => {
...override.options, if (override.files) {
} const pathFilter: AnyFilter = {}
} pathFilter.include = setGlobalExpression(override.files)
const filteredFiles = [file]
.filter(filePathFilter(pathFilter))
if (filteredFiles.length) {
return true
} }
}
})
const validParsers = ['typescript', 'babel', 'espree', 'solidity-parse', 'json', 'yaml', 'solidity-parse']
if (override && override.options && override.options.parser) {
if (validParsers.includes(override.options.parser)) {
parserName = override.options.parser
} else {
this.call('notification', 'toast', `Invalid parser: ${override.options.parser}! Valid options are ${validParsers.join(', ')}`)
}
delete override.options.parser
}
if (override) {
const result = this.prettier.format(content, { options = {
plugins: [sol as any, this.ts, this.babel, this.espree, this.yml], ...options,
parser: parserName, ...override.options,
...options }
})
await this.call('fileManager', 'writeFile', file, result)
} catch (e) {
// do nothing
} }
}
const result = this.prettier.format(content, {
plugins: [sol as any, this.ts, this.babel, this.espree, this.yml],
parser: parserName,
...options
})
await this.call('fileManager', 'writeFile', file, result)
} catch (e) {
// do nothing
} }
}
} }
//*.sol, **/*.txt, contracts/* //*.sol, **/*.txt, contracts/*
const setGlobalExpression = (paths: string) => { const setGlobalExpression = (paths: string) => {
const results = [] const results = []
paths.split(',').forEach(path => { paths.split(',').forEach(path => {
path = path.trim() path = path.trim()
if (path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.') if (path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.')
if (path.endsWith('/*') && !path.endsWith('/**/*')) if (path.endsWith('/*') && !path.endsWith('/**/*'))
path = path.replace(/(\*)/g, '**/*.*') path = path.replace(/(\*)/g, '**/*.*')
results.push(path) results.push(path)
}) })
return results return results
} }
async function findAsync(arr, asyncCallback) { async function findAsync(arr, asyncCallback) {
const promises = arr.map(asyncCallback); const promises = arr.map(asyncCallback);
const results = await Promise.all(promises); const results = await Promise.all(promises);
const index = results.findIndex(result => result); const index = results.findIndex(result => result);
return arr[index]; return arr[index];
} }

@ -8,54 +8,54 @@ import { parse } from './parser'
// https://prettier.io/docs/en/plugins.html#languages // https://prettier.io/docs/en/plugins.html#languages
// https://github.com/ikatyang/linguist-languages/blob/master/data/Solidity.json // https://github.com/ikatyang/linguist-languages/blob/master/data/Solidity.json
const languages = [ const languages = [
{ {
linguistLanguageId: 237469032, linguistLanguageId: 237469032,
name: 'Solidity', name: 'Solidity',
type: 'programming', type: 'programming',
color: '#AA6746', color: '#AA6746',
aceMode: 'text', aceMode: 'text',
tmScope: 'source.solidity', tmScope: 'source.solidity',
extensions: ['.sol'], extensions: ['.sol'],
parsers: ['solidity-parse'], parsers: ['solidity-parse'],
vscodeLanguageIds: ['solidity'] vscodeLanguageIds: ['solidity']
} }
]; ];
// https://prettier.io/docs/en/plugins.html#parsers // https://prettier.io/docs/en/plugins.html#parsers
const parser = { astFormat: 'solidity-ast', parse, ...loc }; const parser = { astFormat: 'solidity-ast', parse, ...loc };
const parsers = { const parsers = {
'solidity-parse': parser 'solidity-parse': parser
}; };
const canAttachComment = (node) => const canAttachComment = (node) =>
node.type && node.type !== 'BlockComment' && node.type !== 'LineComment'; node.type && node.type !== 'BlockComment' && node.type !== 'LineComment';
// https://prettier.io/docs/en/plugins.html#printers // https://prettier.io/docs/en/plugins.html#printers
const printers = { const printers = {
'solidity-ast': { 'solidity-ast': {
canAttachComment, canAttachComment,
handleComments: { handleComments: {
ownLine: handleComments.handleOwnLineComment, ownLine: handleComments.handleOwnLineComment,
endOfLine: handleComments.handleEndOfLineComment, endOfLine: handleComments.handleEndOfLineComment,
remaining: handleComments.handleRemainingComment remaining: handleComments.handleRemainingComment
}, },
isBlockComment: handleComments.isBlockComment, isBlockComment: handleComments.isBlockComment,
massageAstNode, massageAstNode,
print, print,
printComment printComment
} }
}; };
// https://prettier.io/docs/en/plugins.html#defaultoptions // https://prettier.io/docs/en/plugins.html#defaultoptions
const defaultOptions = { const defaultOptions = {
bracketSpacing: false, bracketSpacing: false,
tabWidth: 4 tabWidth: 4
}; };
export default { export default {
languages, languages,
parsers, parsers,
printers, printers,
options, options,
defaultOptions defaultOptions
}; };

@ -5,193 +5,193 @@ const parser = (window as any).SolidityParser
import semver from 'semver'; import semver from 'semver';
const tryHug = (node, operators) => { const tryHug = (node, operators) => {
if (node.type === 'BinaryOperation' && operators.includes(node.operator)) if (node.type === 'BinaryOperation' && operators.includes(node.operator))
return { return {
type: 'TupleExpression', type: 'TupleExpression',
components: [node], components: [node],
isArray: false isArray: false
}; };
return node; return node;
}; };
export function parse(text, _parsers, options) { export function parse(text, _parsers, options) {
const compiler = semver.coerce(options.compiler); const compiler = semver.coerce(options.compiler);
const parsed = parser.parse(text, { loc: true, range: true }); const parsed = parser.parse(text, { loc: true, range: true });
parsed.comments = extractComments(text) parsed.comments = extractComments(text)
parser.visit(parsed, { parser.visit(parsed, {
PragmaDirective(ctx) { PragmaDirective(ctx) {
// if the pragma is not for solidity we leave. // if the pragma is not for solidity we leave.
if (ctx.name !== 'solidity') return; if (ctx.name !== 'solidity') return;
// if the compiler option has not been provided we leave. // if the compiler option has not been provided we leave.
if (!compiler) return; if (!compiler) return;
// we make a check against each pragma directive in the document. // we make a check against each pragma directive in the document.
if (!semver.satisfies(compiler, ctx.value)) { if (!semver.satisfies(compiler, ctx.value)) {
// @TODO: investigate the best way to warn that would apply to // @TODO: investigate the best way to warn that would apply to
// different editors. // different editors.
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn( console.warn(
`[prettier-solidity] The compiler option is set to '${options.compiler}', which does not satisfy 'pragma solidity ${ctx.value}'.` `[prettier-solidity] The compiler option is set to '${options.compiler}', which does not satisfy 'pragma solidity ${ctx.value}'.`
); );
} }
}, },
ModifierDefinition(ctx) { ModifierDefinition(ctx) {
if (!ctx.parameters) { if (!ctx.parameters) {
ctx.parameters = []; ctx.parameters = [];
} }
}, },
FunctionDefinition(ctx) { FunctionDefinition(ctx) {
if (!ctx.isConstructor) { if (!ctx.isConstructor) {
ctx.modifiers.forEach((modifier) => { ctx.modifiers.forEach((modifier) => {
if (modifier.arguments && modifier.arguments.length === 0) { if (modifier.arguments && modifier.arguments.length === 0) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
modifier.arguments = null; modifier.arguments = null;
} }
}); });
} }
}, },
ForStatement(ctx) { ForStatement(ctx) {
if (ctx.initExpression) { if (ctx.initExpression) {
ctx.initExpression.omitSemicolon = true; ctx.initExpression.omitSemicolon = true;
} }
ctx.loopExpression.omitSemicolon = true; ctx.loopExpression.omitSemicolon = true;
}, },
HexLiteral(ctx) { HexLiteral(ctx) {
ctx.value = options.singleQuote ctx.value = options.singleQuote
? `hex'${ctx.value.slice(4, -1)}'` ? `hex'${ctx.value.slice(4, -1)}'`
: `hex"${ctx.value.slice(4, -1)}"`; : `hex"${ctx.value.slice(4, -1)}"`;
}, },
ElementaryTypeName(ctx) { ElementaryTypeName(ctx) {
// if the compiler is below 0.8.0 we will recognize the type 'byte' as an // if the compiler is below 0.8.0 we will recognize the type 'byte' as an
// alias of 'bytes1'. Otherwise we will ignore this and enforce always // alias of 'bytes1'. Otherwise we will ignore this and enforce always
// 'bytes1'. // 'bytes1'.
const pre080 = compiler && semver.satisfies(compiler, '<0.8.0'); const pre080 = compiler && semver.satisfies(compiler, '<0.8.0');
if (!pre080 && ctx.name === 'byte') ctx.name = 'bytes1'; if (!pre080 && ctx.name === 'byte') ctx.name = 'bytes1';
if (options.explicitTypes === 'always') { if (options.explicitTypes === 'always') {
if (ctx.name === 'uint') ctx.name = 'uint256'; if (ctx.name === 'uint') ctx.name = 'uint256';
if (ctx.name === 'int') ctx.name = 'int256'; if (ctx.name === 'int') ctx.name = 'int256';
if (pre080 && ctx.name === 'byte') ctx.name = 'bytes1'; if (pre080 && ctx.name === 'byte') ctx.name = 'bytes1';
} else if (options.explicitTypes === 'never') { } else if (options.explicitTypes === 'never') {
if (ctx.name === 'uint256') ctx.name = 'uint'; if (ctx.name === 'uint256') ctx.name = 'uint';
if (ctx.name === 'int256') ctx.name = 'int'; if (ctx.name === 'int256') ctx.name = 'int';
if (pre080 && ctx.name === 'bytes1') ctx.name = 'byte'; if (pre080 && ctx.name === 'bytes1') ctx.name = 'byte';
} }
}, },
BinaryOperation(ctx) { BinaryOperation(ctx) {
switch (ctx.operator) { switch (ctx.operator) {
case '+': case '+':
case '-': case '-':
ctx.left = tryHug(ctx.left, ['%']); ctx.left = tryHug(ctx.left, ['%']);
ctx.right = tryHug(ctx.right, ['%']); ctx.right = tryHug(ctx.right, ['%']);
break; break;
case '*': case '*':
ctx.left = tryHug(ctx.left, ['/', '%']); ctx.left = tryHug(ctx.left, ['/', '%']);
break; break;
case '/': case '/':
ctx.left = tryHug(ctx.left, ['*', '%']); ctx.left = tryHug(ctx.left, ['*', '%']);
break; break;
case '%': case '%':
ctx.left = tryHug(ctx.left, ['*', '/', '%']); ctx.left = tryHug(ctx.left, ['*', '/', '%']);
break; break;
case '**': case '**':
// If the compiler has not been given as an option using we leave a**b**c. // If the compiler has not been given as an option using we leave a**b**c.
if (!compiler) break; if (!compiler) break;
if (semver.satisfies(compiler, '<0.8.0')) { if (semver.satisfies(compiler, '<0.8.0')) {
// If the compiler is less than 0.8.0 then a**b**c is formatted as // If the compiler is less than 0.8.0 then a**b**c is formatted as
// (a**b)**c. // (a**b)**c.
ctx.left = tryHug(ctx.left, ['**']); ctx.left = tryHug(ctx.left, ['**']);
break; break;
} }
if ( if (
ctx.left.type === 'BinaryOperation' && ctx.left.type === 'BinaryOperation' &&
ctx.left.operator === '**' ctx.left.operator === '**'
) { ) {
// the parser still organizes the a**b**c as (a**b)**c so we need // the parser still organizes the a**b**c as (a**b)**c so we need
// to restructure it. // to restructure it.
ctx.right = { ctx.right = {
type: 'TupleExpression', type: 'TupleExpression',
components: [ components: [
{ {
type: 'BinaryOperation', type: 'BinaryOperation',
operator: '**', operator: '**',
left: ctx.left.right, left: ctx.left.right,
right: ctx.right right: ctx.right
} }
], ],
isArray: false isArray: false
}; };
ctx.left = ctx.left.left; ctx.left = ctx.left.left;
}
break;
case '<<':
case '>>':
ctx.left = tryHug(ctx.left, ['+', '-', '*', '/', '**', '<<', '>>']);
ctx.right = tryHug(ctx.right, ['+', '-', '*', '/', '**']);
break;
case '&':
ctx.left = tryHug(ctx.left, ['+', '-', '*', '/', '**', '<<', '>>']);
ctx.right = tryHug(ctx.right, ['+', '-', '*', '/', '**', '<<', '>>']);
break;
case '|':
ctx.left = tryHug(ctx.left, [
'+',
'-',
'*',
'/',
'**',
'<<',
'>>',
'&',
'^'
]);
ctx.right = tryHug(ctx.right, [
'+',
'-',
'*',
'/',
'**',
'<<',
'>>',
'&',
'^'
]);
break;
case '^':
ctx.left = tryHug(ctx.left, [
'+',
'-',
'*',
'/',
'**',
'<<',
'>>',
'&'
]);
ctx.right = tryHug(ctx.right, [
'+',
'-',
'*',
'/',
'**',
'<<',
'>>',
'&'
]);
break;
case '||':
ctx.left = tryHug(ctx.left, ['&&']);
ctx.right = tryHug(ctx.right, ['&&']);
break;
case '&&':
default:
break;
}
} }
}); break;
case '<<':
case '>>':
ctx.left = tryHug(ctx.left, ['+', '-', '*', '/', '**', '<<', '>>']);
ctx.right = tryHug(ctx.right, ['+', '-', '*', '/', '**']);
break;
case '&':
ctx.left = tryHug(ctx.left, ['+', '-', '*', '/', '**', '<<', '>>']);
ctx.right = tryHug(ctx.right, ['+', '-', '*', '/', '**', '<<', '>>']);
break;
case '|':
ctx.left = tryHug(ctx.left, [
'+',
'-',
'*',
'/',
'**',
'<<',
'>>',
'&',
'^'
]);
ctx.right = tryHug(ctx.right, [
'+',
'-',
'*',
'/',
'**',
'<<',
'>>',
'&',
'^'
]);
break;
case '^':
ctx.left = tryHug(ctx.left, [
'+',
'-',
'*',
'/',
'**',
'<<',
'>>',
'&'
]);
ctx.right = tryHug(ctx.right, [
'+',
'-',
'*',
'/',
'**',
'<<',
'>>',
'&'
]);
break;
case '||':
ctx.left = tryHug(ctx.left, ['&&']);
ctx.right = tryHug(ctx.right, ['&&']);
break;
case '&&':
default:
break;
}
}
});
return parsed; return parsed;
} }

@ -3,30 +3,30 @@ import { QueryParams } from '@remix-project/remix-lib'
import Registry from '../state/registry' import Registry from '../state/registry'
const profile = { const profile = {
name: 'config', name: 'config',
displayName: 'Config', displayName: 'Config',
description: 'Config', description: 'Config',
methods: ['getAppParameter', 'setAppParameter'], methods: ['getAppParameter', 'setAppParameter'],
events: ['configChanged'] events: ['configChanged']
} }
export class ConfigPlugin extends Plugin { export class ConfigPlugin extends Plugin {
constructor () { constructor () {
super(profile) super(profile)
} }
getAppParameter (name: string) { getAppParameter (name: string) {
const queryParams = new QueryParams() const queryParams = new QueryParams()
const params = queryParams.get() const params = queryParams.get()
const config = Registry.getInstance().get('config').api const config = Registry.getInstance().get('config').api
const param = params[name] || config.get(name) || config.get('settings/' + name) const param = params[name] || config.get(name) || config.get('settings/' + name)
if (param === 'true') return true if (param === 'true') return true
if (param === 'false') return false if (param === 'false') return false
return param return param
} }
setAppParameter (name: string, value: any) { setAppParameter (name: string, value: any) {
const config = Registry.getInstance().get('config').api const config = Registry.getInstance().get('config').api
config.set(name, value) config.set(name, value)
} }
} }

@ -6,74 +6,74 @@ import { concatSourceFiles, getDependencyGraph, normalizeContractPath } from '@r
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const profile = { const profile = {
name: 'contractflattener', name: 'contractflattener',
displayName: 'Contract Flattener', displayName: 'Contract Flattener',
description: 'Flatten solidity contracts', description: 'Flatten solidity contracts',
methods: ['flattenAContract', 'flattenContract'], methods: ['flattenAContract', 'flattenContract'],
events: [], events: [],
maintainedBy: 'Remix', maintainedBy: 'Remix',
} }
export class ContractFlattener extends Plugin { export class ContractFlattener extends Plugin {
triggerFlattenContract: boolean = false triggerFlattenContract: boolean = false
constructor() { constructor() {
super(profile) super(profile)
} }
onActivation(): void { onActivation(): void {
this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data, input, version) => { this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data, input, version) => {
if(data.sources && Object.keys(data.sources).length > 1) { if(data.sources && Object.keys(data.sources).length > 1) {
if(this.triggerFlattenContract) { if(this.triggerFlattenContract) {
this.triggerFlattenContract = false this.triggerFlattenContract = false
await this.flattenContract(source, file, data) await this.flattenContract(source, file, data)
} }
} }
}) })
_paq.push(['trackEvent', 'plugin', 'activated', 'contractFlattener']) _paq.push(['trackEvent', 'plugin', 'activated', 'contractFlattener'])
} }
onDeactivation(): void { onDeactivation(): void {
this.off('solidity', 'compilationFinished') this.off('solidity', 'compilationFinished')
} }
async flattenAContract(action: customAction) { async flattenAContract(action: customAction) {
this.triggerFlattenContract = true this.triggerFlattenContract = true
await this.call('solidity', 'compile', action.path[0]) await this.call('solidity', 'compile', action.path[0])
} }
/** /**
* Takes currently compiled contract that has a bunch of imports at the top * Takes currently compiled contract that has a bunch of imports at the top
* and flattens them ready, ideally for UML creation or for other purposes. * and flattens them ready, ideally for UML creation or for other purposes.
* Takes the flattened result, writes it to a file and returns the result. * Takes the flattened result, writes it to a file and returns the result.
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
async flattenContract (source: { sources: any, target: string }, async flattenContract (source: { sources: any, target: string },
filePath: string, data: { contracts: any, sources: any }): Promise<string> { filePath: string, data: { contracts: any, sources: any }): Promise<string> {
const appendage = '_flattened.sol' const appendage = '_flattened.sol'
const normalized = normalizeContractPath(filePath) const normalized = normalizeContractPath(filePath)
const path = `${normalized[normalized.length - 2]}${appendage}` const path = `${normalized[normalized.length - 2]}${appendage}`
const ast = data.sources const ast = data.sources
let dependencyGraph let dependencyGraph
let sorted let sorted
let result let result
let sources let sources
try{ try{
dependencyGraph = getDependencyGraph(ast, filePath) dependencyGraph = getDependencyGraph(ast, filePath)
sorted = dependencyGraph.isEmpty() sorted = dependencyGraph.isEmpty()
? [filePath] ? [filePath]
: dependencyGraph.sort().reverse() : dependencyGraph.sort().reverse()
sources = source.sources sources = source.sources
result = concatSourceFiles(sorted, sources) result = concatSourceFiles(sorted, sources)
}catch(err){ }catch(err){
console.warn(err) console.warn(err)
}
await this.call('fileManager', 'writeFile', path , result)
_paq.push(['trackEvent', 'plugin', 'contractFlattener', 'flattenAContract'])
// clean up memory references & return result
sorted = null
sources = null
dependencyGraph = null
return result
} }
await this.call('fileManager', 'writeFile', path , result)
_paq.push(['trackEvent', 'plugin', 'contractFlattener', 'flattenAContract'])
// clean up memory references & return result
sorted = null
sources = null
dependencyGraph = null
return result
}
} }

@ -6,79 +6,79 @@ import { Plugin } from '@remixproject/engine'
import { fileDecoration } from '@remix-ui/file-decorators' import { fileDecoration } from '@remix-ui/file-decorators'
const profile = { const profile = {
name: 'fileDecorator', name: 'fileDecorator',
desciption: 'Keeps decorators of the files', desciption: 'Keeps decorators of the files',
methods: ['setFileDecorators', 'clearFileDecorators', 'clearAllFileDecorators'], methods: ['setFileDecorators', 'clearFileDecorators', 'clearAllFileDecorators'],
events: ['fileDecoratorsChanged'], events: ['fileDecoratorsChanged'],
version: '0.0.1' version: '0.0.1'
} }
export class FileDecorator extends Plugin { export class FileDecorator extends Plugin {
private _fileStates: fileDecoration[] = [] private _fileStates: fileDecoration[] = []
constructor() { constructor() {
super(profile) super(profile)
} }
onActivation(): void { onActivation(): void {
this.on('filePanel', 'setWorkspace', async () => { this.on('filePanel', 'setWorkspace', async () => {
await this.clearAllFileDecorators() await this.clearAllFileDecorators()
}) })
} }
/** /**
* @param fileStates Array of file states * @param fileStates Array of file states
*/ */
async setFileDecorators(fileStates: fileDecoration[] | fileDecoration) { async setFileDecorators(fileStates: fileDecoration[] | fileDecoration) {
const { from } = this.currentRequest const { from } = this.currentRequest
const workspace = await this.call('filePanel', 'getCurrentWorkspace') const workspace = await this.call('filePanel', 'getCurrentWorkspace')
const fileStatesPayload = Array.isArray(fileStates) ? fileStates : [fileStates] const fileStatesPayload = Array.isArray(fileStates) ? fileStates : [fileStates]
// clear all file states in the previous state of this owner on the files called // clear all file states in the previous state of this owner on the files called
fileStatesPayload.forEach((state) => { fileStatesPayload.forEach((state) => {
state.workspace = workspace state.workspace = workspace
state.owner = from state.owner = from
}) })
const filteredState = this._fileStates.filter((state) => { const filteredState = this._fileStates.filter((state) => {
const index = fileStatesPayload.findIndex((payloadFileState: fileDecoration) => { const index = fileStatesPayload.findIndex((payloadFileState: fileDecoration) => {
return from == state.owner && payloadFileState.path == state.path return from == state.owner && payloadFileState.path == state.path
}) })
return index == -1 return index == -1
}) })
const newState = [...filteredState, ...fileStatesPayload].sort(sortByPath) const newState = [...filteredState, ...fileStatesPayload].sort(sortByPath)
if (!deepequal(newState, this._fileStates)) { if (!deepequal(newState, this._fileStates)) {
this._fileStates = newState this._fileStates = newState
this.emit('fileDecoratorsChanged', this._fileStates) this.emit('fileDecoratorsChanged', this._fileStates)
}
} }
}
async clearFileDecorators(path?: string) { async clearFileDecorators(path?: string) {
const { from } = this.currentRequest const { from } = this.currentRequest
if (!from) return if (!from) return
const filteredState = this._fileStates.filter((state) => { const filteredState = this._fileStates.filter((state) => {
if(state.owner != from) return true if(state.owner != from) return true
if(path && state.path != path) return true if(path && state.path != path) return true
}) })
const newState = [...filteredState].sort(sortByPath) const newState = [...filteredState].sort(sortByPath)
if (!deepequal(newState, this._fileStates)) {
this._fileStates = newState
this.emit('fileDecoratorsChanged', this._fileStates)
}
if (!deepequal(newState, this._fileStates)) {
this._fileStates = newState
this.emit('fileDecoratorsChanged', this._fileStates)
} }
async clearAllFileDecorators() { }
this._fileStates = []
this.emit('fileDecoratorsChanged', []) async clearAllFileDecorators() {
} this._fileStates = []
this.emit('fileDecoratorsChanged', [])
}
} }
const sortByPath = (a: fileDecoration, b: fileDecoration) => { const sortByPath = (a: fileDecoration, b: fileDecoration) => {
if (a.path < b.path) { if (a.path < b.path) {
return -1; return -1;
} }
if (a.path > b.path) { if (a.path > b.path) {
return 1; return 1;
} }
return 0; return 0;
} }

@ -14,31 +14,31 @@ interface INotificationApi {
} }
const profile:LibraryProfile<INotificationApi> = { const profile:LibraryProfile<INotificationApi> = {
name: 'notification', name: 'notification',
displayName: 'Notification', displayName: 'Notification',
description: 'Displays notifications', description: 'Displays notifications',
methods: ['modal', 'alert', 'toast'] methods: ['modal', 'alert', 'toast']
} }
export class NotificationPlugin extends Plugin implements MethodApi<INotificationApi> { export class NotificationPlugin extends Plugin implements MethodApi<INotificationApi> {
dispatcher: dispatchModalInterface dispatcher: dispatchModalInterface
constructor () { constructor () {
super(profile) super(profile)
} }
setDispatcher (dispatcher: dispatchModalInterface) { setDispatcher (dispatcher: dispatchModalInterface) {
this.dispatcher = dispatcher this.dispatcher = dispatcher
} }
async modal (args: AppModal) { async modal (args: AppModal) {
return this.dispatcher.modal(args) return this.dispatcher.modal(args)
} }
async alert (args: AlertModal) { async alert (args: AlertModal) {
return this.dispatcher.alert(args) return this.dispatcher.alert(args)
} }
async toast (message: string | JSX.Element) { async toast (message: string | JSX.Element) {
this.dispatcher.toast(message) this.dispatcher.toast(message)
} }
} }

File diff suppressed because it is too large Load Diff

@ -1,45 +1,45 @@
let parser: any let parser: any
self.onmessage = (e: MessageEvent) => { self.onmessage = (e: MessageEvent) => {
const data: any = e.data const data: any = e.data
switch (data.cmd) { switch (data.cmd) {
case 'load': case 'load':
{ {
(self as any).importScripts(e.data.url) (self as any).importScripts(e.data.url)
// @ts-ignore // @ts-ignore
parser = SolidityParser as any; parser = SolidityParser as any;
self.postMessage({ self.postMessage({
cmd: 'loaded', cmd: 'loaded',
}) })
break break
} }
case 'parse': case 'parse':
if (data.text && parser) { if (data.text && parser) {
try { try {
let startTime = performance.now() let startTime = performance.now()
const blocks = parser.parseBlock(data.text, { loc: true, range: true, tolerant: true }) const blocks = parser.parseBlock(data.text, { loc: true, range: true, tolerant: true })
const blockDuration = performance.now() - startTime const blockDuration = performance.now() - startTime
startTime = performance.now() startTime = performance.now()
const ast = parser.parse(data.text, { loc: true, range: true, tolerant: true }) const ast = parser.parse(data.text, { loc: true, range: true, tolerant: true })
const endTime = performance.now() const endTime = performance.now()
self.postMessage({ self.postMessage({
cmd: 'parsed', cmd: 'parsed',
timestamp: data.timestamp, timestamp: data.timestamp,
ast, ast,
text: data.text, text: data.text,
file: data.file, file: data.file,
duration: endTime - startTime, duration: endTime - startTime,
blockDuration, blockDuration,
blocks blocks
}) })
} catch (e) { } catch (e) {
// do nothing // do nothing
} }
}
break
} }
break
}
} }

@ -20,13 +20,13 @@ interface BlockDefinition {
} }
export default class CodeParserAntlrService { export default class CodeParserAntlrService {
plugin: CodeParser plugin: CodeParser
worker: Worker worker: Worker
parserStartTime: number = 0 parserStartTime: number = 0
workerTimer: NodeJS.Timer workerTimer: NodeJS.Timer
parserThreshold: number = 10 parserThreshold: number = 10
parserThresholdSampleAmount = 3 parserThresholdSampleAmount = 3
cache: { cache: {
[name: string]: { [name: string]: {
text: string, text: string,
ast: antlr.ParseResult | null, ast: antlr.ParseResult | null,
@ -36,253 +36,253 @@ export default class CodeParserAntlrService {
blockDurations?: number[] blockDurations?: number[]
} }
} = {}; } = {};
constructor(plugin: CodeParser) { constructor(plugin: CodeParser) {
this.plugin = plugin this.plugin = plugin
this.createWorker() this.createWorker()
} }
createWorker() { createWorker() {
this.worker = new Worker(new URL('./antlr-worker', import.meta.url)) this.worker = new Worker(new URL('./antlr-worker', import.meta.url))
this.worker.postMessage({ this.worker.postMessage({
cmd: 'load', cmd: 'load',
url: document.location.protocol + '//' + document.location.host + '/assets/js/parser/antlr.js', url: document.location.protocol + '//' + document.location.host + '/assets/js/parser/antlr.js',
}); });
const self = this const self = this
this.worker.addEventListener('message', function (ev) { this.worker.addEventListener('message', function (ev) {
switch (ev.data.cmd) { switch (ev.data.cmd) {
case 'parsed': case 'parsed':
if (ev.data.ast && self.parserStartTime === ev.data.timestamp) { if (ev.data.ast && self.parserStartTime === ev.data.timestamp) {
self.cache[ev.data.file] = { self.cache[ev.data.file] = {
...self.cache[ev.data.file], ...self.cache[ev.data.file],
text: ev.data.text, text: ev.data.text,
ast: ev.data.ast, ast: ev.data.ast,
duration: ev.data.duration, duration: ev.data.duration,
blocks: ev.data.blocks, blocks: ev.data.blocks,
blockDurations: self.cache[ev.data.file].blockDurations? [...self.cache[ev.data.file].blockDurations.slice(-self.parserThresholdSampleAmount), ev.data.blockDuration]: [ev.data.blockDuration] blockDurations: self.cache[ev.data.file].blockDurations? [...self.cache[ev.data.file].blockDurations.slice(-self.parserThresholdSampleAmount), ev.data.blockDuration]: [ev.data.blockDuration]
} }
self.setFileParsingState(ev.data.file) self.setFileParsingState(ev.data.file)
} }
break; break;
} }
}); });
} }
setFileParsingState(file: string) { setFileParsingState(file: string) {
if (this.cache[file]) { if (this.cache[file]) {
if (this.cache[file].blockDurations && this.cache[file].blockDurations.length > this.parserThresholdSampleAmount) { if (this.cache[file].blockDurations && this.cache[file].blockDurations.length > this.parserThresholdSampleAmount) {
// calculate average of durations to determine if the parsing should be disabled // calculate average of durations to determine if the parsing should be disabled
const values = [...this.cache[file].blockDurations] const values = [...this.cache[file].blockDurations]
const average = values.reduce((a, b) => a + b, 0) / values.length const average = values.reduce((a, b) => a + b, 0) / values.length
if (average > this.parserThreshold) { if (average > this.parserThreshold) {
this.cache[file].parsingEnabled = false this.cache[file].parsingEnabled = false
} else { } else {
this.cache[file].parsingEnabled = true this.cache[file].parsingEnabled = true
} }
} }
}
} }
}
enableWorker() { enableWorker() {
if (!this.workerTimer) { if (!this.workerTimer) {
this.workerTimer = setInterval(() => { this.workerTimer = setInterval(() => {
this.setCurrentFileAST() this.setCurrentFileAST()
}, 5000) }, 5000)
}
} }
}
disableWorker() { disableWorker() {
clearInterval(this.workerTimer) clearInterval(this.workerTimer)
} }
async parseWithWorker(text: string, file: string) { async parseWithWorker(text: string, file: string) {
this.parserStartTime = Date.now() this.parserStartTime = Date.now()
this.worker.postMessage({ this.worker.postMessage({
cmd: 'parse', cmd: 'parse',
text, text,
timestamp: this.parserStartTime, timestamp: this.parserStartTime,
file, file,
parsingEnabled: (this.cache[file] && this.cache[file].parsingEnabled) || true parsingEnabled: (this.cache[file] && this.cache[file].parsingEnabled) || true
}); });
} }
async parseSolidity(text: string) { async parseSolidity(text: string) {
const ast: antlr.ParseResult = (SolidityParser as any).parse(text, { loc: true, range: true, tolerant: true }) const ast: antlr.ParseResult = (SolidityParser as any).parse(text, { loc: true, range: true, tolerant: true })
return ast return ast
} }
/** /**
* Tries to parse the current file or the given text and returns the AST * Tries to parse the current file or the given text and returns the AST
* If the parsing fails it returns the last successful AST for this file * If the parsing fails it returns the last successful AST for this file
* @param text * @param text
* @returns * @returns
*/ */
async setCurrentFileAST(text: string | null = null) { async setCurrentFileAST(text: string | null = null) {
try { try {
this.plugin.currentFile = await this.plugin.call('fileManager', 'file') this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (this.plugin.currentFile && this.plugin.currentFile.endsWith('.sol')) { if (this.plugin.currentFile && this.plugin.currentFile.endsWith('.sol')) {
const fileContent = text || await this.plugin.call('fileManager', 'readFile', this.plugin.currentFile) const fileContent = text || await this.plugin.call('fileManager', 'readFile', this.plugin.currentFile)
if (!this.cache[this.plugin.currentFile]) { if (!this.cache[this.plugin.currentFile]) {
this.cache[this.plugin.currentFile] = { this.cache[this.plugin.currentFile] = {
text: '', text: '',
ast: null, ast: null,
parsingEnabled: true, parsingEnabled: true,
blockDurations: [] blockDurations: []
} }
} }
if (this.cache[this.plugin.currentFile] && this.cache[this.plugin.currentFile].text !== fileContent) { if (this.cache[this.plugin.currentFile] && this.cache[this.plugin.currentFile].text !== fileContent) {
try { try {
await this.parseWithWorker(fileContent, this.plugin.currentFile) await this.parseWithWorker(fileContent, this.plugin.currentFile)
} catch (e) { } catch (e) {
// do nothing
}
}
}
} catch (e) {
// do nothing // do nothing
}
} }
}
} catch (e) {
// do nothing
} }
}
/** /**
* Lists the AST nodes from the current file parser * Lists the AST nodes from the current file parser
* These nodes need to be changed to match the node types returned by the compiler * These nodes need to be changed to match the node types returned by the compiler
* @returns * @returns
*/ */
async listAstNodes() { async listAstNodes() {
this.plugin.currentFile = await this.plugin.call('fileManager', 'file') this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (!this.cache[this.plugin.currentFile]) return if (!this.cache[this.plugin.currentFile]) return
const nodes: AstNode[] = []; const nodes: AstNode[] = [];
(SolidityParser as any).visit(this.cache[this.plugin.currentFile].ast, { (SolidityParser as any).visit(this.cache[this.plugin.currentFile].ast, {
StateVariableDeclaration: (node: antlr.StateVariableDeclaration) => { StateVariableDeclaration: (node: antlr.StateVariableDeclaration) => {
if (node.variables) { if (node.variables) {
for (const variable of node.variables) { for (const variable of node.variables) {
nodes.push({ ...variable, nodeType: 'VariableDeclaration', id: null, src: null }) nodes.push({ ...variable, nodeType: 'VariableDeclaration', id: null, src: null })
} }
} }
}, },
VariableDeclaration: (node: antlr.VariableDeclaration) => { VariableDeclaration: (node: antlr.VariableDeclaration) => {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
UserDefinedTypeName: (node: antlr.UserDefinedTypeName) => { UserDefinedTypeName: (node: antlr.UserDefinedTypeName) => {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
FunctionDefinition: (node: antlr.FunctionDefinition) => { FunctionDefinition: (node: antlr.FunctionDefinition) => {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
ContractDefinition: (node: antlr.ContractDefinition) => { ContractDefinition: (node: antlr.ContractDefinition) => {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
MemberAccess: function (node: antlr.MemberAccess) { MemberAccess: function (node: antlr.MemberAccess) {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
Identifier: function (node: antlr.Identifier) { Identifier: function (node: antlr.Identifier) {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
EventDefinition: function (node: antlr.EventDefinition) { EventDefinition: function (node: antlr.EventDefinition) {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
ModifierDefinition: function (node: antlr.ModifierDefinition) { ModifierDefinition: function (node: antlr.ModifierDefinition) {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
InvalidNode: function (node: antlr.InvalidNode) { InvalidNode: function (node: antlr.InvalidNode) {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
EnumDefinition: function (node: antlr.EnumDefinition) { EnumDefinition: function (node: antlr.EnumDefinition) {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
}, },
StructDefinition: function (node: antlr.StructDefinition) { StructDefinition: function (node: antlr.StructDefinition) {
nodes.push({ ...node, nodeType: node.type, id: null, src: null }) nodes.push({ ...node, nodeType: node.type, id: null, src: null })
} }
}) })
return nodes return nodes
} }
/** /**
* *
* @param ast * @param ast
* @returns * @returns
*/ */
async getLastNodeInLine(ast: string) { async getLastNodeInLine(ast: string) {
let lastNode: any let lastNode: any
const checkLastNode = (node: antlr.MemberAccess | antlr.Identifier) => { const checkLastNode = (node: antlr.MemberAccess | antlr.Identifier) => {
if (lastNode && lastNode.range && lastNode.range[1]) { if (lastNode && lastNode.range && lastNode.range[1]) {
if (node.range[1] > lastNode.range[1]) { if (node.range[1] > lastNode.range[1]) {
lastNode = node lastNode = node
}
} else {
lastNode = node
}
} }
} else {
lastNode = node
}
}
(SolidityParser as any).visit(ast, { (SolidityParser as any).visit(ast, {
MemberAccess: function (node: antlr.MemberAccess) { MemberAccess: function (node: antlr.MemberAccess) {
checkLastNode(node) checkLastNode(node)
}, },
Identifier: function (node: antlr.Identifier) { Identifier: function (node: antlr.Identifier) {
checkLastNode(node) checkLastNode(node)
} }
}) })
if (lastNode && lastNode.expression) { if (lastNode && lastNode.expression) {
return lastNode.expression return lastNode.expression
}
return lastNode
} }
/* return lastNode
}
/*
* get the code blocks of the current file * get the code blocks of the current file
*/ */
async getCurrentFileBlocks(text: string | null = null) { async getCurrentFileBlocks(text: string | null = null) {
this.plugin.currentFile = await this.plugin.call('fileManager', 'file') this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (this.cache[this.plugin.currentFile]) { if (this.cache[this.plugin.currentFile]) {
if (!this.cache[this.plugin.currentFile].parsingEnabled) { if (!this.cache[this.plugin.currentFile].parsingEnabled) {
return return
} }
} }
if (this.plugin.currentFile && this.plugin.currentFile.endsWith('.sol')) { if (this.plugin.currentFile && this.plugin.currentFile.endsWith('.sol')) {
const fileContent = text || await this.plugin.call('fileManager', 'readFile', this.plugin.currentFile) const fileContent = text || await this.plugin.call('fileManager', 'readFile', this.plugin.currentFile)
try { try {
const startTime = Date.now() const startTime = Date.now()
const blocks = (SolidityParser as any).parseBlock(fileContent, { loc: true, range: true, tolerant: true }) const blocks = (SolidityParser as any).parseBlock(fileContent, { loc: true, range: true, tolerant: true })
if(this.cache[this.plugin.currentFile] && this.cache[this.plugin.currentFile].blockDurations){ if(this.cache[this.plugin.currentFile] && this.cache[this.plugin.currentFile].blockDurations){
this.cache[this.plugin.currentFile].blockDurations = [...this.cache[this.plugin.currentFile].blockDurations.slice(-this.parserThresholdSampleAmount), Date.now() - startTime] this.cache[this.plugin.currentFile].blockDurations = [...this.cache[this.plugin.currentFile].blockDurations.slice(-this.parserThresholdSampleAmount), Date.now() - startTime]
this.setFileParsingState(this.plugin.currentFile) this.setFileParsingState(this.plugin.currentFile)
}
if (blocks) this.cache[this.plugin.currentFile].blocks = blocks
return blocks
} catch (e) {
// do nothing
}
} }
if (blocks) this.cache[this.plugin.currentFile].blocks = blocks
return blocks
} catch (e) {
// do nothing
}
} }
}
/** /**
* Returns the block surrounding the given position * Returns the block surrounding the given position
* For example if the position is in the middle of a function, it will return the function * For example if the position is in the middle of a function, it will return the function
* @param {position} position * @param {position} position
* @param {string} text // optional * @param {string} text // optional
* @return {any} * @return {any}
* */ * */
async getANTLRBlockAtPosition(position: any, text: string = null) { async getANTLRBlockAtPosition(position: any, text: string = null) {
const blocks: any[] = await this.getCurrentFileBlocks(text) const blocks: any[] = await this.getCurrentFileBlocks(text)
const walkAst = (blocks) => { const walkAst = (blocks) => {
let nodeFound = null let nodeFound = null
for (const object of blocks) { for (const object of blocks) {
if (object.start <= position) { if (object.start <= position) {
nodeFound = object nodeFound = object
break break
}
}
return nodeFound
} }
if (!blocks) return }
blocks.reverse() return nodeFound
const block = walkAst(blocks)
return block
} }
if (!blocks) return
blocks.reverse()
const block = walkAst(blocks)
return block
}
} }

@ -34,253 +34,253 @@ type errorMarker = {
file: string file: string
} }
export default class CodeParserCompiler { export default class CodeParserCompiler {
plugin: CodeParser
compiler: any // used to compile the current file seperately from the main compiler
onAstFinished: (success: any, data: CompilationResult, source: CompilationSourceCode, input: any, version: any) => Promise<void>;
errorState: boolean;
gastEstimateTimeOut: any
constructor(
plugin: CodeParser plugin: CodeParser
compiler: any // used to compile the current file seperately from the main compiler ) {
onAstFinished: (success: any, data: CompilationResult, source: CompilationSourceCode, input: any, version: any) => Promise<void>; this.plugin = plugin
errorState: boolean; }
gastEstimateTimeOut: any
constructor(
plugin: CodeParser
) {
this.plugin = plugin
}
init() {
this.onAstFinished = async (success, data: CompilationResult, source: CompilationSourceCode, input: any, version) => { init() {
this.plugin.call('editor', 'clearAnnotations')
this.errorState = true
const result = new CompilerAbstract('soljson', data, source, input)
let allErrors: errorMarker[] = []
if (data.errors || data.error) {
const file = await this.plugin.call('fileManager', 'getCurrentFile')
const currentFileContent = await this.plugin.call('fileManager', 'readFile', file)
const sources = result.getSourceCode().sources || []
if (data.error) {
if (data.error.formattedMessage) {
// mark this file as error
const errorMarker = await this.createErrorMarker(data.error, file, { start: { line: 0, column: 0 }, end: { line: 0, column: 100 } })
allErrors = [...allErrors, errorMarker]
}
} else {
for (const error of data.errors) {
if (!error.sourceLocation) {
// mark this file as error
const errorMarker = await this.createErrorMarker(error, file, { start: { line: 0, column: 0 }, end: { line: 0, column: 100 } })
allErrors = [...allErrors, errorMarker]
} else {
const lineBreaks = sourceMappingDecoder.getLinebreakPositions(sources[error.sourceLocation.file].content)
const lineColumn = sourceMappingDecoder.convertOffsetToLineColumn({
start: error.sourceLocation.start,
length: error.sourceLocation.end - error.sourceLocation.start
}, lineBreaks)
this.onAstFinished = async (success, data: CompilationResult, source: CompilationSourceCode, input: any, version) => {
this.plugin.call('editor', 'clearAnnotations')
this.errorState = true
const result = new CompilerAbstract('soljson', data, source, input)
let allErrors: errorMarker[] = []
if (data.errors || data.error) {
const file = await this.plugin.call('fileManager', 'getCurrentFile')
const currentFileContent = await this.plugin.call('fileManager', 'readFile', file)
const sources = result.getSourceCode().sources || []
if (data.error) {
if (data.error.formattedMessage) {
// mark this file as error
const errorMarker = await this.createErrorMarker(data.error, file, { start: { line: 0, column: 0 }, end: { line: 0, column: 100 } })
allErrors = [...allErrors, errorMarker]
}
} else {
for (const error of data.errors) {
if (!error.sourceLocation) {
// mark this file as error
const errorMarker = await this.createErrorMarker(error, file, { start: { line: 0, column: 0 }, end: { line: 0, column: 100 } })
allErrors = [...allErrors, errorMarker]
} else {
const lineBreaks = sourceMappingDecoder.getLinebreakPositions(sources[error.sourceLocation.file].content)
const lineColumn = sourceMappingDecoder.convertOffsetToLineColumn({
start: error.sourceLocation.start,
length: error.sourceLocation.end - error.sourceLocation.start
}, lineBreaks)
const filePath = error.sourceLocation.file
const fileTarget = await this.plugin.call('fileManager', 'getUrlFromPath', filePath)
const importFilePositions = await this.getPositionForImportErrors(fileTarget.file, currentFileContent) const filePath = error.sourceLocation.file
for (const importFilePosition of importFilePositions) { const fileTarget = await this.plugin.call('fileManager', 'getUrlFromPath', filePath)
for (const line of importFilePosition.lines) {
allErrors = [...allErrors, await this.createErrorMarker(error, file, line.position)]
}
}
allErrors = [...allErrors, await this.createErrorMarker(error, filePath, lineColumn)] const importFilePositions = await this.getPositionForImportErrors(fileTarget.file, currentFileContent)
} for (const importFilePosition of importFilePositions) {
} for (const line of importFilePosition.lines) {
allErrors = [...allErrors, await this.createErrorMarker(error, file, line.position)]
} }
}
const displayErrors = await this.plugin.call('config', 'getAppParameter', 'display-errors') allErrors = [...allErrors, await this.createErrorMarker(error, filePath, lineColumn)]
if (displayErrors) await this.plugin.call('editor', 'addErrorMarker', allErrors)
this.addDecorators(allErrors, sources)
} else {
await this.plugin.call('editor', 'clearErrorMarkers', result.getSourceCode().sources)
await this.clearDecorators(result.getSourceCode().sources)
} }
}
}
const displayErrors = await this.plugin.call('config', 'getAppParameter', 'display-errors')
if (displayErrors) await this.plugin.call('editor', 'addErrorMarker', allErrors)
this.addDecorators(allErrors, sources)
} else {
await this.plugin.call('editor', 'clearErrorMarkers', result.getSourceCode().sources)
await this.clearDecorators(result.getSourceCode().sources)
}
if (!data.sources) return
if (data.sources && Object.keys(data.sources).length === 0) return
this.plugin.compilerAbstract = new CompilerAbstract('soljson', data, source, input)
this.errorState = false
this.plugin.nodeIndex = {
declarations: {},
flatReferences: {},
nodesPerFile: {},
}
if (!data.sources) return
if (data.sources && Object.keys(data.sources).length === 0) return
this.plugin.compilerAbstract = new CompilerAbstract('soljson', data, source, input)
this.errorState = false
this.plugin.nodeIndex = {
declarations: {},
flatReferences: {},
nodesPerFile: {},
}
this.plugin._buildIndex(data, source)
// cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository this.plugin._buildIndex(data, source)
this.plugin.nodeIndex.nodesPerFile[this.plugin.currentFile] = this.plugin._extractFileNodes(this.plugin.currentFile, this.plugin.compilerAbstract as unknown as lastCompilationResult) // cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository
await this.plugin.gasService.showGasEstimates() this.plugin.nodeIndex.nodesPerFile[this.plugin.currentFile] = this.plugin._extractFileNodes(this.plugin.currentFile, this.plugin.compilerAbstract as unknown as lastCompilationResult)
this.plugin.emit('astFinished') await this.plugin.gasService.showGasEstimates()
} this.plugin.emit('astFinished')
this.compiler = new Compiler((url, cb) => this.plugin.call('contentImport', 'resolveAndSave', url, undefined).then((result) => cb(null, result)).catch((error) => cb(error.message)))
this.compiler.event.register('compilationFinished', this.onAstFinished)
} }
this.compiler = new Compiler((url, cb) => this.plugin.call('contentImport', 'resolveAndSave', url, undefined).then((result) => cb(null, result)).catch((error) => cb(error.message)))
this.compiler.event.register('compilationFinished', this.onAstFinished)
}
// COMPILER // COMPILER
/** /**
* *
* @returns * @returns
*/ */
async compile() { async compile() {
try { try {
this.plugin.currentFile = await this.plugin.call('fileManager', 'file') this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (this.plugin.currentFile && this.plugin.currentFile.endsWith('.sol')) { if (this.plugin.currentFile && this.plugin.currentFile.endsWith('.sol')) {
const state = await this.plugin.call('solidity', 'getCompilerState') const state = await this.plugin.call('solidity', 'getCompilerState')
this.compiler.set('optimize', state.optimize) this.compiler.set('optimize', state.optimize)
this.compiler.set('evmVersion', state.evmVersion) this.compiler.set('evmVersion', state.evmVersion)
this.compiler.set('language', state.language) this.compiler.set('language', state.language)
this.compiler.set('runs', state.runs) this.compiler.set('runs', state.runs)
this.compiler.set('useFileConfiguration', true) this.compiler.set('useFileConfiguration', true)
this.compiler.set('compilerRetriggerMode', CompilerRetriggerMode.retrigger) this.compiler.set('compilerRetriggerMode', CompilerRetriggerMode.retrigger)
const configFileContent = { const configFileContent = {
"language": "Solidity", "language": "Solidity",
"settings": { "settings": {
"optimizer": { "optimizer": {
"enabled": state.optimize, "enabled": state.optimize,
"runs": state.runs "runs": state.runs
}, },
"outputSelection": { "outputSelection": {
"*": { "*": {
"": ["ast"], "": ["ast"],
"*": ["evm.gasEstimates"] "*": ["evm.gasEstimates"]
} }
}, },
"evmVersion": state.evmVersion && state.evmVersion.toString() || "berlin", "evmVersion": state.evmVersion && state.evmVersion.toString() || "berlin",
} }
}
this.compiler.set('configFileContent', JSON.stringify(configFileContent))
this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (!this.plugin.currentFile) return
const content = await this.plugin.call('fileManager', 'readFile', this.plugin.currentFile)
const sources = { [this.plugin.currentFile]: { content } }
this.compiler.compile(sources, this.plugin.currentFile)
}
} catch (e) {
// do nothing
} }
}
async addDecorators(allErrors: errorMarker[], sources: any) { this.compiler.set('configFileContent', JSON.stringify(configFileContent))
const displayErrors = await this.plugin.call('config', 'getAppParameter', 'display-errors') this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
if (!displayErrors) return if (!this.plugin.currentFile) return
const errorsPerFiles: { [fileName: string]: errorMarker[] } = {} const content = await this.plugin.call('fileManager', 'readFile', this.plugin.currentFile)
for (const error of allErrors) { const sources = { [this.plugin.currentFile]: { content } }
if (!errorsPerFiles[error.file]) { this.compiler.compile(sources, this.plugin.currentFile)
errorsPerFiles[error.file] = [] }
} } catch (e) {
errorsPerFiles[error.file].push(error) // do nothing
} }
}
const errorPriority = { async addDecorators(allErrors: errorMarker[], sources: any) {
'error': 0, const displayErrors = await this.plugin.call('config', 'getAppParameter', 'display-errors')
'warning': 1, if (!displayErrors) return
} const errorsPerFiles: { [fileName: string]: errorMarker[] } = {}
for (const error of allErrors) {
if (!errorsPerFiles[error.file]) {
errorsPerFiles[error.file] = []
}
errorsPerFiles[error.file].push(error)
}
// sort errorPerFiles by error priority const errorPriority = {
const sortedErrorsPerFiles: { [fileName: string]: errorMarker[] } = {} 'error': 0,
for (const fileName in errorsPerFiles) { 'warning': 1,
const errors = errorsPerFiles[fileName] }
errors.sort((a, b) => {
return errorPriority[a.severity] - errorPriority[b.severity]
}
)
sortedErrorsPerFiles[fileName] = errors
}
const filesWithOutErrors = Object.keys(sources).filter((fileName) => !sortedErrorsPerFiles[fileName])
// add decorators
const decorators: fileDecoration[] = []
for (const fileName in sortedErrorsPerFiles) {
const errors = sortedErrorsPerFiles[fileName]
const fileTarget = await this.plugin.call('fileManager', 'getPathFromUrl', fileName)
const decorator: fileDecoration = {
path: fileTarget.file,
isDirectory: false,
fileStateType: errors[0].severity == MarkerSeverity.Error ? fileDecorationType.Error : fileDecorationType.Warning,
fileStateLabelClass: errors[0].severity == MarkerSeverity.Error ? 'text-danger' : 'text-warning',
fileStateIconClass: '',
fileStateIcon: '',
text: errors.length.toString(),
owner: 'code-parser',
bubble: true,
comment: errors.map((error) => error.message),
}
decorators.push(decorator)
}
for (const fileName of filesWithOutErrors) {
const fileTarget = await this.plugin.call('fileManager', 'getPathFromUrl', fileName)
const decorator: fileDecoration = {
path: fileTarget.file,
isDirectory: false,
fileStateType: fileDecorationType.None,
fileStateLabelClass: '',
fileStateIconClass: '',
fileStateIcon: '',
text: '',
owner: 'code-parser',
bubble: false
}
decorators.push(decorator)
}
await this.plugin.call('fileDecorator', 'setFileDecorators', decorators)
await this.plugin.call('editor', 'clearErrorMarkers', filesWithOutErrors)
// sort errorPerFiles by error priority
const sortedErrorsPerFiles: { [fileName: string]: errorMarker[] } = {}
for (const fileName in errorsPerFiles) {
const errors = errorsPerFiles[fileName]
errors.sort((a, b) => {
return errorPriority[a.severity] - errorPriority[b.severity]
}
)
sortedErrorsPerFiles[fileName] = errors
}
const filesWithOutErrors = Object.keys(sources).filter((fileName) => !sortedErrorsPerFiles[fileName])
// add decorators
const decorators: fileDecoration[] = []
for (const fileName in sortedErrorsPerFiles) {
const errors = sortedErrorsPerFiles[fileName]
const fileTarget = await this.plugin.call('fileManager', 'getPathFromUrl', fileName)
const decorator: fileDecoration = {
path: fileTarget.file,
isDirectory: false,
fileStateType: errors[0].severity == MarkerSeverity.Error ? fileDecorationType.Error : fileDecorationType.Warning,
fileStateLabelClass: errors[0].severity == MarkerSeverity.Error ? 'text-danger' : 'text-warning',
fileStateIconClass: '',
fileStateIcon: '',
text: errors.length.toString(),
owner: 'code-parser',
bubble: true,
comment: errors.map((error) => error.message),
}
decorators.push(decorator)
}
for (const fileName of filesWithOutErrors) {
const fileTarget = await this.plugin.call('fileManager', 'getPathFromUrl', fileName)
const decorator: fileDecoration = {
path: fileTarget.file,
isDirectory: false,
fileStateType: fileDecorationType.None,
fileStateLabelClass: '',
fileStateIconClass: '',
fileStateIcon: '',
text: '',
owner: 'code-parser',
bubble: false
}
decorators.push(decorator)
} }
await this.plugin.call('fileDecorator', 'setFileDecorators', decorators)
await this.plugin.call('editor', 'clearErrorMarkers', filesWithOutErrors)
async createErrorMarker(error: any, filePath: string, lineColumn): Promise<errorMarker> { }
return {
message: error.formattedMessage, async createErrorMarker(error: any, filePath: string, lineColumn): Promise<errorMarker> {
severity: error.severity === 'error' ? MarkerSeverity.Error : MarkerSeverity.Warning, return {
position: { message: error.formattedMessage,
start: { severity: error.severity === 'error' ? MarkerSeverity.Error : MarkerSeverity.Warning,
line: ((lineColumn.start && lineColumn.start.line) || 0) + 1, position: {
column: ((lineColumn.start && lineColumn.start.column) || 0) + 1 start: {
}, line: ((lineColumn.start && lineColumn.start.line) || 0) + 1,
end: { column: ((lineColumn.start && lineColumn.start.column) || 0) + 1
line: ((lineColumn.end && lineColumn.end.line) || 0) + 1, },
column: ((lineColumn.end && lineColumn.end.column) || 0) + 1 end: {
} line: ((lineColumn.end && lineColumn.end.line) || 0) + 1,
} column: ((lineColumn.end && lineColumn.end.column) || 0) + 1
, file: filePath
} }
}
, file: filePath
} }
}
async clearDecorators(sources: any) { async clearDecorators(sources: any) {
const decorators: fileDecoration[] = [] const decorators: fileDecoration[] = []
if (!sources) return if (!sources) return
for (const fileName of Object.keys(sources)) { for (const fileName of Object.keys(sources)) {
const decorator: fileDecoration = { const decorator: fileDecoration = {
path: fileName, path: fileName,
isDirectory: false, isDirectory: false,
fileStateType: fileDecorationType.None, fileStateType: fileDecorationType.None,
fileStateLabelClass: '', fileStateLabelClass: '',
fileStateIconClass: '', fileStateIconClass: '',
fileStateIcon: '', fileStateIcon: '',
text: '', text: '',
owner: 'code-parser', owner: 'code-parser',
bubble: false bubble: false
} }
decorators.push(decorator) decorators.push(decorator)
} }
await this.plugin.call('fileDecorator', 'setFileDecorators', decorators) await this.plugin.call('fileDecorator', 'setFileDecorators', decorators)
} }
async getPositionForImportErrors(importedFileName: string, text: string) { async getPositionForImportErrors(importedFileName: string, text: string) {
const re = new RegExp(importedFileName, 'gi') const re = new RegExp(importedFileName, 'gi')
const result: SearchResultLine[] = findLinesInStringWithMatch( const result: SearchResultLine[] = findLinesInStringWithMatch(
text, text,
re re
) )
return result return result
} }
} }

@ -3,76 +3,76 @@ import { lineText } from '@remix-ui/editor'
import { lastCompilationResult } from '@remixproject/plugin-api'; import { lastCompilationResult } from '@remixproject/plugin-api';
export default class CodeParserGasService { export default class CodeParserGasService {
plugin: CodeParser plugin: CodeParser
constructor(plugin: CodeParser) { constructor(plugin: CodeParser) {
this.plugin = plugin this.plugin = plugin
} }
async getGasEstimates(fileName: string) { async getGasEstimates(fileName: string) {
if (!fileName) { if (!fileName) {
fileName = await this.plugin.currentFile fileName = await this.plugin.currentFile
} }
if (this.plugin.nodeIndex.nodesPerFile && this.plugin.nodeIndex.nodesPerFile[fileName] && this.plugin.nodeIndex.nodesPerFile[fileName].contracts) { if (this.plugin.nodeIndex.nodesPerFile && this.plugin.nodeIndex.nodesPerFile[fileName] && this.plugin.nodeIndex.nodesPerFile[fileName].contracts) {
const estimates: any = [] const estimates: any = []
for (const contract in this.plugin.nodeIndex.nodesPerFile[fileName].contracts) { for (const contract in this.plugin.nodeIndex.nodesPerFile[fileName].contracts) {
if (this.plugin.nodeIndex.nodesPerFile[fileName].contracts[contract].contractNodes) { if (this.plugin.nodeIndex.nodesPerFile[fileName].contracts[contract].contractNodes) {
const nodes = this.plugin.nodeIndex.nodesPerFile[fileName].contracts[contract].contractNodes const nodes = this.plugin.nodeIndex.nodesPerFile[fileName].contracts[contract].contractNodes
for (const node of Object.values(nodes) as any[]) { for (const node of Object.values(nodes) as any[]) {
if (node.gasEstimate) { if (node.gasEstimate) {
estimates.push({ estimates.push({
node, node,
range: await this.plugin.getLineColumnOfNode(node) range: await this.plugin.getLineColumnOfNode(node)
}) })
}
}
}
} }
return estimates }
} }
}
return estimates
} }
}
async showGasEstimates() {
const showGasConfig = await this.plugin.call('config', 'getAppParameter', 'show-gas')
if(!showGasConfig) {
await this.plugin.call('editor', 'discardLineTexts')
return
}
this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
// cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository
this.plugin.nodeIndex.nodesPerFile[this.plugin.currentFile] = await this.plugin._extractFileNodes(this.plugin.currentFile, this.plugin.compilerAbstract as unknown as lastCompilationResult)
const gasEstimates = await this.getGasEstimates(this.plugin.currentFile) async showGasEstimates() {
const showGasConfig = await this.plugin.call('config', 'getAppParameter', 'show-gas')
if(!showGasConfig) {
await this.plugin.call('editor', 'discardLineTexts')
return
}
this.plugin.currentFile = await this.plugin.call('fileManager', 'file')
// cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository
this.plugin.nodeIndex.nodesPerFile[this.plugin.currentFile] = await this.plugin._extractFileNodes(this.plugin.currentFile, this.plugin.compilerAbstract as unknown as lastCompilationResult)
const gasEstimates = await this.getGasEstimates(this.plugin.currentFile)
const friendlyNames = { const friendlyNames = {
'executionCost': 'Estimated execution cost', 'executionCost': 'Estimated execution cost',
'codeDepositCost': 'Estimated code deposit cost', 'codeDepositCost': 'Estimated code deposit cost',
'creationCost': 'Estimated creation cost', 'creationCost': 'Estimated creation cost',
}
await this.plugin.call('editor', 'discardLineTexts')
if (gasEstimates) {
for (const estimate of gasEstimates) {
const linetext: lineText = {
content: Object.entries(estimate.node.gasEstimate).map(([, value]) => `${value} gas`).join(' '),
position: estimate.range,
hide: false,
className: 'text-muted small',
afterContentClassName: 'text-muted small fas fa-gas-pump pl-4',
from: 'codeParser',
hoverMessage: [{
value: `${Object.entries(estimate.node.gasEstimate).map(([key, value]) => `${friendlyNames[key]}: ${value} gas`).join(' ')}`,
},
],
} }
await this.plugin.call('editor', 'discardLineTexts')
if (gasEstimates) {
for (const estimate of gasEstimates) {
const linetext: lineText = {
content: Object.entries(estimate.node.gasEstimate).map(([, value]) => `${value} gas`).join(' '),
position: estimate.range,
hide: false,
className: 'text-muted small',
afterContentClassName: 'text-muted small fas fa-gas-pump pl-4',
from: 'codeParser',
hoverMessage: [{
value: `${Object.entries(estimate.node.gasEstimate).map(([key, value]) => `${friendlyNames[key]}: ${value} gas`).join(' ')}`,
},
],
}
this.plugin.call('editor', 'addLineText', linetext, estimate.range.fileName) this.plugin.call('editor', 'addLineText', linetext, estimate.range.fileName)
} }
}
} }
}
} }

@ -8,81 +8,81 @@ export type CodeParserImportsData= {
} }
export default class CodeParserImports { export default class CodeParserImports {
plugin: CodeParser plugin: CodeParser
data: CodeParserImportsData = {} data: CodeParserImportsData = {}
constructor(plugin: CodeParser) { constructor(plugin: CodeParser) {
this.plugin = plugin this.plugin = plugin
this.init() this.init()
} }
async getImports(){ async getImports(){
return this.data return this.data
} }
async init() { async init() {
// @ts-ignore // @ts-ignore
const txt = await import('raw-loader!libs/remix-ui/editor/src/lib/providers/completion/contracts/contracts.txt') const txt = await import('raw-loader!libs/remix-ui/editor/src/lib/providers/completion/contracts/contracts.txt')
this.data.modules = txt.default.split('\n') this.data.modules = txt.default.split('\n')
.filter(x => x !== '') .filter(x => x !== '')
.map(x => x.replace('./node_modules/', '')) .map(x => x.replace('./node_modules/', ''))
.filter(x => { .filter(x => {
if(x.includes('@openzeppelin')) { if(x.includes('@openzeppelin')) {
return !x.includes('mock') return !x.includes('mock')
}else{ }else{
return true return true
} }
}) })
// get unique first words of the values in the array // get unique first words of the values in the array
this.data.packages = [...new Set(this.data.modules.map(x => x.split('/')[0]))] this.data.packages = [...new Set(this.data.modules.map(x => x.split('/')[0]))]
} }
setFileTree = async () => { setFileTree = async () => {
this.data.files = await this.getDirectory('/') this.data.files = await this.getDirectory('/')
this.data.files = this.data.files.filter(x => x.endsWith('.sol') && !x.startsWith('.deps') && !x.startsWith('.git')) this.data.files = this.data.files.filter(x => x.endsWith('.sol') && !x.startsWith('.deps') && !x.startsWith('.git'))
} }
getDirectory = async (dir: string) => { getDirectory = async (dir: string) => {
let result = [] let result = []
let files = {} let files = {}
try { try {
if (await this.plugin.call('fileManager', 'exists', dir)) { if (await this.plugin.call('fileManager', 'exists', dir)) {
files = await this.plugin.call('fileManager', 'readdir', dir) files = await this.plugin.call('fileManager', 'readdir', dir)
} }
} catch (e) {} } catch (e) {}
const fileArray = this.normalize(files) const fileArray = this.normalize(files)
for (const fi of fileArray) { for (const fi of fileArray) {
if (fi) { if (fi) {
const type = fi.data.isDirectory const type = fi.data.isDirectory
if (type === true) { if (type === true) {
result = [...result, ...(await this.getDirectory(`${fi.filename}`))] result = [...result, ...(await this.getDirectory(`${fi.filename}`))]
} else { } else {
result = [...result, fi.filename] result = [...result, fi.filename]
}
}
} }
return result }
} }
return result
}
normalize = filesList => { normalize = filesList => {
const folders = [] const folders = []
const files = [] const files = []
Object.keys(filesList || {}).forEach(key => { Object.keys(filesList || {}).forEach(key => {
if (filesList[key].isDirectory) { if (filesList[key].isDirectory) {
folders.push({ folders.push({
filename: key, filename: key,
data: filesList[key] data: filesList[key]
})
} else {
files.push({
filename: key,
data: filesList[key]
})
}
}) })
return [...folders, ...files] } else {
} files.push({
filename: key,
data: filesList[key]
})
}
})
return [...folders, ...files]
}
} }

@ -596,132 +596,132 @@ type ASTTypeMap = ASTMap<ASTNode>
export const astNodeTypes = [ export const astNodeTypes = [
'SourceUnit', 'SourceUnit',
'PragmaDirective', 'PragmaDirective',
'ImportDirective', 'ImportDirective',
'ContractDefinition', 'ContractDefinition',
'InheritanceSpecifier', 'InheritanceSpecifier',
'StateVariableDeclaration', 'StateVariableDeclaration',
'UsingForDeclaration', 'UsingForDeclaration',
'StructDefinition', 'StructDefinition',
'ModifierDefinition', 'ModifierDefinition',
'ModifierInvocation', 'ModifierInvocation',
'FunctionDefinition', 'FunctionDefinition',
'EventDefinition', 'EventDefinition',
'CustomErrorDefinition', 'CustomErrorDefinition',
'RevertStatement', 'RevertStatement',
'EnumValue', 'EnumValue',
'EnumDefinition', 'EnumDefinition',
'VariableDeclaration', 'VariableDeclaration',
'UserDefinedTypeName', 'UserDefinedTypeName',
'Mapping', 'Mapping',
'ArrayTypeName', 'ArrayTypeName',
'FunctionTypeName', 'FunctionTypeName',
'Block', 'Block',
'ExpressionStatement', 'ExpressionStatement',
'IfStatement', 'IfStatement',
'WhileStatement', 'WhileStatement',
'ForStatement', 'ForStatement',
'InlineAssemblyStatement', 'InlineAssemblyStatement',
'DoWhileStatement', 'DoWhileStatement',
'ContinueStatement', 'ContinueStatement',
'Break', 'Break',
'Continue', 'Continue',
'BreakStatement', 'BreakStatement',
'ReturnStatement', 'ReturnStatement',
'EmitStatement', 'EmitStatement',
'ThrowStatement', 'ThrowStatement',
'VariableDeclarationStatement', 'VariableDeclarationStatement',
'ElementaryTypeName', 'ElementaryTypeName',
'FunctionCall', 'FunctionCall',
'AssemblyBlock', 'AssemblyBlock',
'AssemblyCall', 'AssemblyCall',
'AssemblyLocalDefinition', 'AssemblyLocalDefinition',
'AssemblyAssignment', 'AssemblyAssignment',
'AssemblyStackAssignment', 'AssemblyStackAssignment',
'LabelDefinition', 'LabelDefinition',
'AssemblySwitch', 'AssemblySwitch',
'AssemblyCase', 'AssemblyCase',
'AssemblyFunctionDefinition', 'AssemblyFunctionDefinition',
'AssemblyFunctionReturns', 'AssemblyFunctionReturns',
'AssemblyFor', 'AssemblyFor',
'AssemblyIf', 'AssemblyIf',
'SubAssembly', 'SubAssembly',
'TupleExpression', 'TupleExpression',
'NameValueExpression', 'NameValueExpression',
'BooleanLiteral', 'BooleanLiteral',
'NumberLiteral', 'NumberLiteral',
'Identifier', 'Identifier',
'BinaryOperation', 'BinaryOperation',
'UnaryOperation', 'UnaryOperation',
'NewExpression', 'NewExpression',
'Conditional', 'Conditional',
'StringLiteral', 'StringLiteral',
'HexLiteral', 'HexLiteral',
'HexNumber', 'HexNumber',
'DecimalNumber', 'DecimalNumber',
'MemberAccess', 'MemberAccess',
'IndexAccess', 'IndexAccess',
'IndexRangeAccess', 'IndexRangeAccess',
'NameValueList', 'NameValueList',
'UncheckedStatement', 'UncheckedStatement',
'TryStatement', 'TryStatement',
'CatchClause', 'CatchClause',
'FileLevelConstant', 'FileLevelConstant',
'AssemblyMemberAccess', 'AssemblyMemberAccess',
'TypeDefinition', 'TypeDefinition',
'InvalidNode' 'InvalidNode'
] as const ] as const
export const binaryOpValues = [ export const binaryOpValues = [
'+', '+',
'-', '-',
'*', '*',
'/', '/',
'**', '**',
'%', '%',
'<<', '<<',
'>>', '>>',
'&&', '&&',
'||', '||',
',,', ',,',
'&', '&',
',', ',',
'^', '^',
'<', '<',
'>', '>',
'<=', '<=',
'>=', '>=',
'==', '==',
'!=', '!=',
'=', '=',
',=', ',=',
'^=', '^=',
'&=', '&=',
'<<=', '<<=',
'>>=', '>>=',
'+=', '+=',
'-=', '-=',
'*=', '*=',
'/=', '/=',
'%=', '%=',
'|', '|',
'|=', '|=',
] as const ] as const
export type BinOp = typeof binaryOpValues[number] export type BinOp = typeof binaryOpValues[number]
export const unaryOpValues = [ export const unaryOpValues = [
'-', '-',
'+', '+',
'++', '++',
'--', '--',
'~', '~',
'after', 'after',
'delete', 'delete',
'!', '!',
] as const ] as const
export type UnaryOp = typeof unaryOpValues[number] export type UnaryOp = typeof unaryOpValues[number]

@ -6,73 +6,73 @@ import { PermissionHandlerDialog, PermissionHandlerValue } from '@remix-ui/permi
import { Profile } from '@remixproject/plugin-utils' import { Profile } from '@remixproject/plugin-utils'
const profile = { const profile = {
name: 'permissionhandler', name: 'permissionhandler',
displayName: 'permissionhandler', displayName: 'permissionhandler',
description: 'Plugin to handle permissions', description: 'Plugin to handle permissions',
methods: ['askPermission'] methods: ['askPermission']
} }
export class PermissionHandlerPlugin extends Plugin { export class PermissionHandlerPlugin extends Plugin {
permissions: any permissions: any
sessionPermissions: any sessionPermissions: any
currentVersion: number currentVersion: number
fallbackMemory: boolean fallbackMemory: boolean
constructor() { constructor() {
super(profile) super(profile)
this.fallbackMemory = false this.fallbackMemory = false
this.permissions = this._getFromLocal() this.permissions = this._getFromLocal()
this.sessionPermissions = {} this.sessionPermissions = {}
this.currentVersion = 1 this.currentVersion = 1
// here we remove the old permissions saved before adding 'permissionVersion' // here we remove the old permissions saved before adding 'permissionVersion'
// since with v1 the structure has been changed because of new engine ^0.2.0-alpha.6 changes // since with v1 the structure has been changed because of new engine ^0.2.0-alpha.6 changes
if (!localStorage.getItem('permissionVersion')) { if (!localStorage.getItem('permissionVersion')) {
localStorage.setItem('plugins/permissions', '') localStorage.setItem('plugins/permissions', '')
localStorage.setItem('permissionVersion', this.currentVersion.toString()) localStorage.setItem('permissionVersion', this.currentVersion.toString())
}
} }
}
_getFromLocal() { _getFromLocal() {
if (this.fallbackMemory) return this.permissions if (this.fallbackMemory) return this.permissions
const permission = localStorage.getItem('plugins/permissions') const permission = localStorage.getItem('plugins/permissions')
return permission ? JSON.parse(permission) : {} return permission ? JSON.parse(permission) : {}
} }
persistPermissions() { persistPermissions() {
const permissions = JSON.stringify(this.permissions) const permissions = JSON.stringify(this.permissions)
try { try {
localStorage.setItem('plugins/permissions', permissions) localStorage.setItem('plugins/permissions', permissions)
} catch (e) { } catch (e) {
this.fallbackMemory = true this.fallbackMemory = true
console.log(e) console.log(e)
}
} }
}
switchMode (from: Profile, to: Profile, method: string, set: boolean, sensitiveCall: boolean) { switchMode (from: Profile, to: Profile, method: string, set: boolean, sensitiveCall: boolean) {
if (sensitiveCall) { if (sensitiveCall) {
set set
? this.sessionPermissions[to.name][method][from.name] = {} ? this.sessionPermissions[to.name][method][from.name] = {}
: delete this.sessionPermissions[to.name][method][from.name] : delete this.sessionPermissions[to.name][method][from.name]
} else { } else {
set set
? this.permissions[to.name][method][from.name] = {} ? this.permissions[to.name][method][from.name] = {}
: delete this.permissions[to.name][method][from.name] : delete this.permissions[to.name][method][from.name]
} }
} }
clear() { clear() {
localStorage.removeItem('plugins/permissions') localStorage.removeItem('plugins/permissions')
this.permissions = this._getFromLocal() this.permissions = this._getFromLocal()
this.sessionPermissions = {} this.sessionPermissions = {}
} }
notAllowWarning(from: Profile, to: Profile, method: string) { notAllowWarning(from: Profile, to: Profile, method: string) {
return `${from.displayName || from.name} is not allowed to call ${method} method of ${to.displayName || to.name}.` return `${from.displayName || from.name} is not allowed to call ${method} method of ${to.displayName || to.name}.`
} }
async getTheme() { async getTheme() {
return (await this.call('theme', 'currentTheme')).quality return (await this.call('theme', 'currentTheme')).quality
} }
/** /**
* Check if a plugin has the permission to call another plugin and askPermission if needed * Check if a plugin has the permission to call another plugin and askPermission if needed
* @param {PluginProfile} from the profile of the plugin that make the call * @param {PluginProfile} from the profile of the plugin that make the call
* @param {ModuleProfile} to The profile of the module that receive the call * @param {ModuleProfile} to The profile of the module that receive the call
@ -80,95 +80,95 @@ export class PermissionHandlerPlugin extends Plugin {
* @param {string} message from the caller plugin to add more details if needed * @param {string} message from the caller plugin to add more details if needed
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
async askPermission(from: Profile, to: Profile, method: string, message: string, sensitiveCall: boolean) { async askPermission(from: Profile, to: Profile, method: string, message: string, sensitiveCall: boolean) {
try { try {
if (sensitiveCall) { if (sensitiveCall) {
if (!this.sessionPermissions[to.name]) this.sessionPermissions[to.name] = {} if (!this.sessionPermissions[to.name]) this.sessionPermissions[to.name] = {}
if (!this.sessionPermissions[to.name][method]) this.sessionPermissions[to.name][method] = {} if (!this.sessionPermissions[to.name][method]) this.sessionPermissions[to.name][method] = {}
if (!this.sessionPermissions[to.name][method][from.name]) return this.openPermission(from, to, method, message, sensitiveCall) if (!this.sessionPermissions[to.name][method][from.name]) return this.openPermission(from, to, method, message, sensitiveCall)
} else { } else {
this.permissions = this._getFromLocal() this.permissions = this._getFromLocal()
if (!this.permissions[to.name]) this.permissions[to.name] = {} if (!this.permissions[to.name]) this.permissions[to.name] = {}
if (!this.permissions[to.name][method]) this.permissions[to.name][method] = {} if (!this.permissions[to.name][method]) this.permissions[to.name][method] = {}
if (!this.permissions[to.name][method][from.name]) return this.openPermission(from, to, method, message, sensitiveCall) if (!this.permissions[to.name][method][from.name]) return this.openPermission(from, to, method, message, sensitiveCall)
} }
const { allow, hash } = sensitiveCall ? this.sessionPermissions[to.name][method][from.name] : this.permissions[to.name][method][from.name] const { allow, hash } = sensitiveCall ? this.sessionPermissions[to.name][method][from.name] : this.permissions[to.name][method][from.name]
if (!allow) { if (!allow) {
const warning = this.notAllowWarning(from, to, method) const warning = this.notAllowWarning(from, to, method)
this.call('notification', 'toast', warning) this.call('notification', 'toast', warning)
return false return false
} }
return hash === from.hash return hash === from.hash
? true // Allow ? true // Allow
: await this.openPermission(from, to, method, message, sensitiveCall) : await this.openPermission(from, to, method, message, sensitiveCall)
} catch (err) { } catch (err) {
throw new Error(err) throw new Error(err)
}
} }
}
async openPermission(from: Profile, to: Profile, method: string, message: string, sensitiveCall: boolean) { async openPermission(from: Profile, to: Profile, method: string, message: string, sensitiveCall: boolean) {
let remember let remember
if (sensitiveCall) {
remember = this.sessionPermissions[to.name][method][from.name]
} else {
remember = this.permissions[to.name][method][from.name]
}
const value: PermissionHandlerValue = {
from,
to,
method,
message,
remember,
sensitiveCall
}
const modal: AppModal = {
id: 'PermissionHandler',
title: <FormattedMessage id='permissionHandler.permissionNeededFor' values={{ to: to.displayName || to.name }} />,
message: <PermissionHandlerDialog plugin={this} theme={await this.getTheme()} value={value}></PermissionHandlerDialog>,
okLabel: <FormattedMessage id='permissionHandler.accept' />,
cancelLabel: <FormattedMessage id='permissionHandler.decline' />
}
const result = await this.call('notification', 'modal', modal)
return new Promise((resolve, reject) => {
if (result) {
if (sensitiveCall) { if (sensitiveCall) {
remember = this.sessionPermissions[to.name][method][from.name] if (this.sessionPermissions[to.name][method][from.name]) {
this.sessionPermissions[to.name][method][from.name] = {
allow: true,
hash: from.hash
}
}
} else { } else {
remember = this.permissions[to.name][method][from.name] if (this.permissions[to.name][method][from.name]) {
} this.permissions[to.name][method][from.name] = {
const value: PermissionHandlerValue = { allow: true,
from, hash: from.hash
to, }
method, this.persistPermissions()
message, }
remember,
sensitiveCall
}
const modal: AppModal = {
id: 'PermissionHandler',
title: <FormattedMessage id='permissionHandler.permissionNeededFor' values={{ to: to.displayName || to.name }} />,
message: <PermissionHandlerDialog plugin={this} theme={await this.getTheme()} value={value}></PermissionHandlerDialog>,
okLabel: <FormattedMessage id='permissionHandler.accept' />,
cancelLabel: <FormattedMessage id='permissionHandler.decline' />
} }
resolve(true)
const result = await this.call('notification', 'modal', modal) } else {
return new Promise((resolve, reject) => { if (sensitiveCall) {
if (result) { if (this.sessionPermissions[to.name][method][from.name]) {
if (sensitiveCall) { this.sessionPermissions[to.name][method][from.name] = {
if (this.sessionPermissions[to.name][method][from.name]) { allow: false,
this.sessionPermissions[to.name][method][from.name] = { hash: from.hash
allow: true,
hash: from.hash
}
}
} else {
if (this.permissions[to.name][method][from.name]) {
this.permissions[to.name][method][from.name] = {
allow: true,
hash: from.hash
}
this.persistPermissions()
}
}
resolve(true)
} else {
if (sensitiveCall) {
if (this.sessionPermissions[to.name][method][from.name]) {
this.sessionPermissions[to.name][method][from.name] = {
allow: false,
hash: from.hash
}
}
} else {
if (this.permissions[to.name][method][from.name]) {
this.permissions[to.name][method][from.name] = {
allow: false,
hash: from.hash
}
this.persistPermissions()
}
}
reject(this.notAllowWarning(from, to, method))
} }
}) }
} } else {
if (this.permissions[to.name][method][from.name]) {
this.permissions[to.name][method][from.name] = {
allow: false,
hash: from.hash
}
this.persistPermissions()
}
}
reject(this.notAllowWarning(from, to, method))
}
})
}
} }

@ -11,179 +11,179 @@ import { AppModal, AlertModal } from '@remix-ui/app'
const LOCALHOST = ' - connect to localhost - ' const LOCALHOST = ' - connect to localhost - '
const profile = { const profile = {
name: 'remixd', name: 'remixd',
displayName: 'RemixD', displayName: 'RemixD',
url: 'ws://127.0.0.1:65520', url: 'ws://127.0.0.1:65520',
methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'rename', 'remove', 'isDirectory', 'list', 'createDir'], methods: ['folderIsReadOnly', 'resolveDirectory', 'get', 'exists', 'isFile', 'set', 'rename', 'remove', 'isDirectory', 'list', 'createDir'],
events: [], events: [],
description: 'Using Remixd daemon, allow to access file system', description: 'Using Remixd daemon, allow to access file system',
kind: 'other', kind: 'other',
version: packageJson.version, version: packageJson.version,
repo: "https://github.com/ethereum/remix-project/tree/master/libs/remixd", repo: "https://github.com/ethereum/remix-project/tree/master/libs/remixd",
maintainedBy: "Remix", maintainedBy: "Remix",
documentation: "https://remix-ide.readthedocs.io/en/latest/remixd.html", documentation: "https://remix-ide.readthedocs.io/en/latest/remixd.html",
authorContact: "" authorContact: ""
} }
export class RemixdHandle extends WebsocketPlugin { export class RemixdHandle extends WebsocketPlugin {
localhostProvider: any localhostProvider: any
appManager: PluginManager appManager: PluginManager
dependentPlugins: Array<string> dependentPlugins: Array<string>
constructor(localhostProvider, appManager) { constructor(localhostProvider, appManager) {
super(profile) super(profile)
this.localhostProvider = localhostProvider this.localhostProvider = localhostProvider
this.appManager = appManager this.appManager = appManager
this.dependentPlugins = ['hardhat', 'truffle', 'slither', 'foundry'] this.dependentPlugins = ['hardhat', 'truffle', 'slither', 'foundry']
} }
async deactivate() { async deactivate() {
for (const plugin of this.dependentPlugins) { for (const plugin of this.dependentPlugins) {
if (await this.appManager.isActive(plugin)) { if (await this.appManager.isActive(plugin)) {
await this.appManager.deactivatePlugin(plugin) await this.appManager.deactivatePlugin(plugin)
} }
}
if (super.socket) super.deactivate()
// this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342
this.localhostProvider.close((error) => {
if (error) console.log(error)
})
} }
if (super.socket) super.deactivate()
// this.appManager.deactivatePlugin('git') // plugin call doesn't work.. see issue https://github.com/ethereum/remix-plugin/issues/342
this.localhostProvider.close((error) => {
if (error) console.log(error)
})
}
async activate() {
this.connectToLocalhost()
return true
}
async activate() { async canceled() {
this.connectToLocalhost() for (const plugin of this.dependentPlugins) {
return true if (await this.appManager.isActive(plugin)) {
await this.appManager.deactivatePlugin(plugin)
}
} }
async canceled() { await this.appManager.deactivatePlugin('remixd')
for (const plugin of this.dependentPlugins) {
if (await this.appManager.isActive(plugin)) {
await this.appManager.deactivatePlugin(plugin)
}
}
await this.appManager.deactivatePlugin('remixd') }
async callPluginMethod(key: string, payload?: any[]) {
if (this.socket.readyState !== this.socket.OPEN) {
console.log(`${this.profile.name} connection is not opened.`)
return false
} }
return super.callPluginMethod(key, payload)
}
async callPluginMethod(key: string, payload?: any[]) { /**
if (this.socket.readyState !== this.socket.OPEN) {
console.log(`${this.profile.name} connection is not opened.`)
return false
}
return super.callPluginMethod(key, payload)
}
/**
* connect to localhost if no connection and render the explorer * connect to localhost if no connection and render the explorer
* disconnect from localhost if connected and remove the explorer * disconnect from localhost if connected and remove the explorer
* *
* @param {String} txHash - hash of the transaction * @param {String} txHash - hash of the transaction
*/ */
async connectToLocalhost() { async connectToLocalhost() {
const connection = async (error?: any) => { const connection = async (error?: any) => {
if (error) { if (error) {
console.log(error) console.log(error)
const alert: AlertModal = { const alert: AlertModal = {
id: 'connectionAlert', id: 'connectionAlert',
message: 'Cannot connect to the remixd daemon. Please make sure you have the remixd running in the background.' message: 'Cannot connect to the remixd daemon. Please make sure you have the remixd running in the background.'
}
this.call('notification', 'alert', alert)
this.canceled()
} else {
const intervalId = setInterval(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
clearInterval(intervalId)
const alert: AlertModal = {
id: 'connectionAlert',
message: 'Connection to remixd terminated. Please make sure remixd is still running in the background.'
}
this.call('notification', 'alert', alert)
this.canceled()
}
}, 3000)
this.localhostProvider.init(() => {
this.call('filePanel', 'setWorkspace', { name: LOCALHOST, isLocalhost: true }, true)
});
for (const plugin of this.dependentPlugins) {
await this.appManager.activatePlugin(plugin)
}
}
} }
if (this.localhostProvider.isConnected()) { this.call('notification', 'alert', alert)
this.deactivate() this.canceled()
} else if (!isElectron()) { } else {
// warn the user only if he/she is in the browser context const intervalId = setInterval(() => {
const mod: AppModal = { if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
id: 'remixdConnect', clearInterval(intervalId)
title: 'Access file system using remixd', const alert: AlertModal = {
message: remixdDialog(), id: 'connectionAlert',
okLabel: 'Connect', message: 'Connection to remixd terminated. Please make sure remixd is still running in the background.'
cancelLabel: 'Cancel',
}
const result = await this.call('notification', 'modal', mod)
if (result) {
try {
this.localhostProvider.preInit()
super.activate()
setTimeout(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
connection(new Error('Connection with daemon failed.'))
} else {
connection()
}
}, 3000)
} catch (error) {
connection(error)
}
}
else {
await this.canceled()
} }
} else { this.call('notification', 'alert', alert)
try { this.canceled()
super.activate() }
setTimeout(() => { connection() }, 2000) }, 3000)
} catch (error) { this.localhostProvider.init(() => {
connection(error) this.call('filePanel', 'setWorkspace', { name: LOCALHOST, isLocalhost: true }, true)
});
for (const plugin of this.dependentPlugins) {
await this.appManager.activatePlugin(plugin)
}
}
}
if (this.localhostProvider.isConnected()) {
this.deactivate()
} else if (!isElectron()) {
// warn the user only if he/she is in the browser context
const mod: AppModal = {
id: 'remixdConnect',
title: 'Access file system using remixd',
message: remixdDialog(),
okLabel: 'Connect',
cancelLabel: 'Cancel',
}
const result = await this.call('notification', 'modal', mod)
if (result) {
try {
this.localhostProvider.preInit()
super.activate()
setTimeout(() => {
if (!this.socket || (this.socket && this.socket.readyState === 3)) { // 3 means connection closed
connection(new Error('Connection with daemon failed.'))
} else {
connection()
} }
}, 3000)
} catch (error) {
connection(error)
} }
}
else {
await this.canceled()
}
} else {
try {
super.activate()
setTimeout(() => { connection() }, 2000)
} catch (error) {
connection(error)
}
} }
}
} }
function remixdDialog() { function remixdDialog() {
const commandText = 'remixd' const commandText = 'remixd'
const fullCommandText = 'remixd -s <path-to-the-shared-folder> -u <remix-ide-instance-URL>' const fullCommandText = 'remixd -s <path-to-the-shared-folder> -u <remix-ide-instance-URL>'
return (<> return (<>
<div className=''> <div className=''>
<div className='mb-2 text-break'> <div className='mb-2 text-break'>
Access your local file system from Remix IDE using <a target="_blank" href="https://www.npmjs.com/package/@remix-project/remixd">Remixd NPM package</a>. Access your local file system from Remix IDE using <a target="_blank" href="https://www.npmjs.com/package/@remix-project/remixd">Remixd NPM package</a>.
</div> </div>
<div className='mb-2 text-break'> <div className='mb-2 text-break'>
Remixd <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html">documentation</a>. Remixd <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html">documentation</a>.
</div> </div>
<div className='mb-2 text-break'> <div className='mb-2 text-break'>
The remixd command is: The remixd command is:
<br /><b>{commandText}</b> <br /><b>{commandText}</b>
</div> </div>
<div className='mb-2 text-break'> <div className='mb-2 text-break'>
The remixd command without options uses the terminal's current directory as the shared directory and the shared Remix domain can only be https://remix.ethereum.org, https://remix-alpha.ethereum.org, or https://remix-beta.ethereum.org The remixd command without options uses the terminal's current directory as the shared directory and the shared Remix domain can only be https://remix.ethereum.org, https://remix-alpha.ethereum.org, or https://remix-beta.ethereum.org
</div> </div>
<div className='mb-2 text-break'> <div className='mb-2 text-break'>
Example command with flags: <br /> Example command with flags: <br />
<b>{fullCommandText}</b> <b>{fullCommandText}</b>
</div> </div>
<div className='mb-2 text-break'> <div className='mb-2 text-break'>
For info about ports, see <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#ports-usage">Remixd ports usage</a> For info about ports, see <a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#ports-usage">Remixd ports usage</a>
</div> </div>
<div className='mb-2 text-break'> <div className='mb-2 text-break'>
This feature is still in Alpha. We recommend to keep a backup of the shared folder. This feature is still in Alpha. We recommend to keep a backup of the shared folder.
</div> </div>
<div className='mb-2 text-break'> <div className='mb-2 text-break'>
<h6 className="text-danger"> <h6 className="text-danger">
Before using, make sure remixd version is latest i.e. <b>v{remixdVersion}</b> Before using, make sure remixd version is latest i.e. <b>v{remixdVersion}</b>
<br></br><a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#update-to-the-latest-remixd">Read here how to update it</a> <br></br><a target="_blank" href="https://remix-ide.readthedocs.io/en/latest/remixd.html#update-to-the-latest-remixd">Read here how to update it</a>
</h6> </h6>
</div> </div>
</div> </div>
</>) </>)
} }

@ -6,25 +6,25 @@ import { TransactionConfig } from 'web3-core'
const _paq = window._paq = window._paq || [] //eslint-disable-line const _paq = window._paq = window._paq || [] //eslint-disable-line
const profile = { const profile = {
name: 'solidity-script', name: 'solidity-script',
displayName: 'solidity-script', displayName: 'solidity-script',
description: 'solidity-script', description: 'solidity-script',
methods: ['execute'] methods: ['execute']
} }
export class SolidityScript extends Plugin { export class SolidityScript extends Plugin {
constructor () { constructor () {
super(profile) super(profile)
} }
async execute (path: string, functionName: string = 'run') { async execute (path: string, functionName: string = 'run') {
_paq.push(['trackEvent', 'SolidityScript', 'execute', 'script']) _paq.push(['trackEvent', 'SolidityScript', 'execute', 'script'])
this.call('terminal', 'log', `Running free function '${functionName}' from ${path}...`) this.call('terminal', 'log', `Running free function '${functionName}' from ${path}...`)
let content = await this.call('fileManager', 'readFile', path) let content = await this.call('fileManager', 'readFile', path)
const params = await this.call('solidity', 'getCompilerParameters') const params = await this.call('solidity', 'getCompilerParameters')
content = ` content = `
// SPDX-License-Identifier: GPL-3.0 // SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1; pragma solidity >=0.7.1;
@ -38,69 +38,69 @@ export class SolidityScript extends Plugin {
${functionName}(); ${functionName}();
} }
}` }`
const targets = { 'script.sol': { content } } const targets = { 'script.sol': { content } }
// compile // compile
const compilation = await compile(targets, params, async (url, cb) => { const compilation = await compile(targets, params, async (url, cb) => {
await this.call('contentImport', 'resolveAndSave', url).then((result) => cb(null, result)).catch((error) => cb(error.message)) await this.call('contentImport', 'resolveAndSave', url).then((result) => cb(null, result)).catch((error) => cb(error.message))
}) })
if (compilation.data.error) { if (compilation.data.error) {
this.call('terminal', 'log', compilation.data.error.formattedMessage) this.call('terminal', 'log', compilation.data.error.formattedMessage)
} }
if (compilation.data.errors && compilation.data.errors.length > 0) { if (compilation.data.errors && compilation.data.errors.length > 0) {
compilation.data.errors.map((error) => { compilation.data.errors.map((error) => {
this.call('terminal', 'log', error.formattedMessage) this.call('terminal', 'log', error.formattedMessage)
}) })
} }
// get the contract // get the contract
const contract = compilation.getContract('SolidityScript') const contract = compilation.getContract('SolidityScript')
if (!contract) { if (!contract) {
console.log('compilation failed') console.log('compilation failed')
return return
} }
const bytecode = '0x' + contract.object.evm.bytecode.object const bytecode = '0x' + contract.object.evm.bytecode.object
const web3 = await this.call('blockchain', 'web3VM') const web3 = await this.call('blockchain', 'web3VM')
const accounts = await this.call('blockchain', 'getAccounts') const accounts = await this.call('blockchain', 'getAccounts')
if (!accounts || accounts.length === 0) { if (!accounts || accounts.length === 0) {
throw new Error('no account available') throw new Error('no account available')
} }
// deploy the contract // deploy the contract
let tx: TransactionConfig = { let tx: TransactionConfig = {
from: accounts[0], from: accounts[0],
data: bytecode data: bytecode
} }
const receipt = await web3.eth.sendTransaction(tx) const receipt = await web3.eth.sendTransaction(tx)
tx = { tx = {
from: accounts[0], from: accounts[0],
to: receipt.contractAddress, to: receipt.contractAddress,
data: '0x69d4394b' // function remixRun() public data: '0x69d4394b' // function remixRun() public
} }
const receiptCall = await web3.eth.sendTransaction(tx) const receiptCall = await web3.eth.sendTransaction(tx)
const hhlogs = await web3.eth.getHHLogsForTx(receiptCall.transactionHash) const hhlogs = await web3.eth.getHHLogsForTx(receiptCall.transactionHash)
if (hhlogs && hhlogs.length) { if (hhlogs && hhlogs.length) {
const finalLogs = <div><div><b>console.log:</b></div> const finalLogs = <div><div><b>console.log:</b></div>
{ {
hhlogs.map((log) => { hhlogs.map((log) => {
let formattedLog let formattedLog
// Hardhat implements the same formatting options that can be found in Node.js' console.log, // Hardhat implements the same formatting options that can be found in Node.js' console.log,
// which in turn uses util.format: https://nodejs.org/dist/latest-v12.x/docs/api/util.html#util_util_format_format_args // which in turn uses util.format: https://nodejs.org/dist/latest-v12.x/docs/api/util.html#util_util_format_format_args
// For example: console.log("Name: %s, Age: %d", remix, 6) will log 'Name: remix, Age: 6' // For example: console.log("Name: %s, Age: %d", remix, 6) will log 'Name: remix, Age: 6'
// We check first arg to determine if 'util.format' is needed // We check first arg to determine if 'util.format' is needed
if (typeof log[0] === 'string' && (log[0].includes('%s') || log[0].includes('%d'))) { if (typeof log[0] === 'string' && (log[0].includes('%s') || log[0].includes('%d'))) {
formattedLog = format(log[0], ...log.slice(1)) formattedLog = format(log[0], ...log.slice(1))
} else { } else {
formattedLog = log.join(' ') formattedLog = log.join(' ')
} }
return <div>{formattedLog}</div> return <div>{formattedLog}</div>
})} })}
</div> </div>
_paq.push(['trackEvent', 'udapp', 'hardhat', 'console.log']) _paq.push(['trackEvent', 'udapp', 'hardhat', 'console.log'])
this.call('terminal', 'logHtml', finalLogs) this.call('terminal', 'logHtml', finalLogs)
} }
} }
} }

@ -16,39 +16,39 @@ const parser = (window as any).SolidityParser
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const profile = { const profile = {
name: 'solidityumlgen', name: 'solidityumlgen',
displayName: 'Solidity UML Generator', displayName: 'Solidity UML Generator',
description: 'Generates UML diagram in svg format from last compiled contract', description: 'Generates UML diagram in svg format from last compiled contract',
location: 'mainPanel', location: 'mainPanel',
methods: ['showUmlDiagram', 'generateUml', 'generateCustomAction'], methods: ['showUmlDiagram', 'generateUml', 'generateCustomAction'],
events: [], events: [],
} }
const themeCollection = [ const themeCollection = [
{ themeName: 'HackerOwl', backgroundColor: '#011628', textColor: '#babbcc', { themeName: 'HackerOwl', backgroundColor: '#011628', textColor: '#babbcc',
shapeColor: '#8694a1',fillColor: '#011C32'}, shapeColor: '#8694a1',fillColor: '#011C32'},
{ themeName: 'Cerulean', backgroundColor: '#ffffff', textColor: '#343a40', { themeName: 'Cerulean', backgroundColor: '#ffffff', textColor: '#343a40',
shapeColor: '#343a40',fillColor: '#f8f9fa'}, shapeColor: '#343a40',fillColor: '#f8f9fa'},
{ themeName: 'Cyborg', backgroundColor: '#060606', textColor: '#adafae', { themeName: 'Cyborg', backgroundColor: '#060606', textColor: '#adafae',
shapeColor: '#adafae', fillColor: '#222222'}, shapeColor: '#adafae', fillColor: '#222222'},
{ themeName: 'Dark', backgroundColor: '#222336', textColor: '#babbcc', { themeName: 'Dark', backgroundColor: '#222336', textColor: '#babbcc',
shapeColor: '#babbcc',fillColor: '#2a2c3f'}, shapeColor: '#babbcc',fillColor: '#2a2c3f'},
{ themeName: 'Flatly', backgroundColor: '#ffffff', textColor: '#343a40', { themeName: 'Flatly', backgroundColor: '#ffffff', textColor: '#343a40',
shapeColor: '#7b8a8b',fillColor: '#ffffff'}, shapeColor: '#7b8a8b',fillColor: '#ffffff'},
{ themeName: 'Black', backgroundColor: '#1a1a1a', textColor: '#babbcc', { themeName: 'Black', backgroundColor: '#1a1a1a', textColor: '#babbcc',
shapeColor: '#b5b4bc',fillColor: '#1f2020'}, shapeColor: '#b5b4bc',fillColor: '#1f2020'},
{ themeName: 'Light', backgroundColor: '#eef1f6', textColor: '#3b445e', { themeName: 'Light', backgroundColor: '#eef1f6', textColor: '#3b445e',
shapeColor: '#343a40',fillColor: '#ffffff'}, shapeColor: '#343a40',fillColor: '#ffffff'},
{ themeName: 'Midcentury', backgroundColor: '#DBE2E0', textColor: '#11556c', { themeName: 'Midcentury', backgroundColor: '#DBE2E0', textColor: '#11556c',
shapeColor: '#343a40',fillColor: '#eeede9'}, shapeColor: '#343a40',fillColor: '#eeede9'},
{ themeName: 'Spacelab', backgroundColor: '#ffffff', textColor: '#343a40', { themeName: 'Spacelab', backgroundColor: '#ffffff', textColor: '#343a40',
shapeColor: '#333333', fillColor: '#eeeeee'}, shapeColor: '#333333', fillColor: '#eeeeee'},
{ themeName: 'Candy', backgroundColor: '#d5efff', textColor: '#11556c', { themeName: 'Candy', backgroundColor: '#d5efff', textColor: '#11556c',
shapeColor: '#343a40',fillColor: '#fbe7f8' }, shapeColor: '#343a40',fillColor: '#fbe7f8' },
{ themeName: 'Violet', backgroundColor: '#f1eef6', textColor: '#3b445e', { themeName: 'Violet', backgroundColor: '#f1eef6', textColor: '#3b445e',
shapeColor: '#343a40',fillColor: '#f8fafe' }, shapeColor: '#343a40',fillColor: '#f8fafe' },
{ themeName: 'Unicorn', backgroundColor: '#f1eef6', textColor: '#343a40', { themeName: 'Unicorn', backgroundColor: '#f1eef6', textColor: '#343a40',
shapeColor: '#343a40',fillColor: '#f8fafe' }, shapeColor: '#343a40',fillColor: '#f8fafe' },
] ]
/** /**
@ -57,182 +57,182 @@ const themeCollection = [
*/ */
export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen { export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
element: HTMLDivElement element: HTMLDivElement
currentFile: string currentFile: string
svgPayload: string svgPayload: string
updatedSvg: string updatedSvg: string
currentlySelectedTheme: string currentlySelectedTheme: string
themeName: string themeName: string
themeDark: string themeDark: string
loading: boolean loading: boolean
themeCollection: ThemeSummary[] themeCollection: ThemeSummary[]
activeTheme: ThemeSummary activeTheme: ThemeSummary
triggerGenerateUml: boolean triggerGenerateUml: boolean
umlClasses: UmlClass[] = [] umlClasses: UmlClass[] = []
appManager: RemixAppManager appManager: RemixAppManager
dispatch: React.Dispatch<any> = () => {} dispatch: React.Dispatch<any> = () => {}
constructor(appManager: RemixAppManager) { constructor(appManager: RemixAppManager) {
super(profile) super(profile)
this.currentFile = '' this.currentFile = ''
this.svgPayload = '' this.svgPayload = ''
this.updatedSvg = '' this.updatedSvg = ''
this.loading = false this.loading = false
this.currentlySelectedTheme = '' this.currentlySelectedTheme = ''
this.themeName = '' this.themeName = ''
this.themeCollection = themeCollection this.themeCollection = themeCollection
this.activeTheme = themeCollection.find(t => t.themeName === 'Dark') this.activeTheme = themeCollection.find(t => t.themeName === 'Dark')
this.appManager = appManager this.appManager = appManager
this.element = document.createElement('div') this.element = document.createElement('div')
this.element.setAttribute('id', 'sol-uml-gen') this.element.setAttribute('id', 'sol-uml-gen')
} }
onActivation(): void { onActivation(): void {
this.handleThemeChange() this.handleThemeChange()
this.on('solidity', 'compilationFinished', async (file: string, source, languageVersion, data, input, version) => { this.on('solidity', 'compilationFinished', async (file: string, source, languageVersion, data, input, version) => {
if(!this.triggerGenerateUml) return if(!this.triggerGenerateUml) return
this.triggerGenerateUml = false this.triggerGenerateUml = false
const currentTheme: ThemeQualityType = await this.call('theme', 'currentTheme') const currentTheme: ThemeQualityType = await this.call('theme', 'currentTheme')
this.currentlySelectedTheme = currentTheme.quality this.currentlySelectedTheme = currentTheme.quality
this.themeName = currentTheme.name this.themeName = currentTheme.name
let result = '' let result = ''
const normalized = normalizeContractPath(file) const normalized = normalizeContractPath(file)
this.currentFile = normalized[normalized.length - 1] this.currentFile = normalized[normalized.length - 1]
try { try {
if (data.sources && Object.keys(data.sources).length > 1) { // we should flatten first as there are multiple asts if (data.sources && Object.keys(data.sources).length > 1) { // we should flatten first as there are multiple asts
result = await this.flattenContract(source, file, data) result = await this.flattenContract(source, file, data)
} }
const ast = result.length > 1 ? parser.parse(result) : parser.parse(source.sources[file].content) const ast = result.length > 1 ? parser.parse(result) : parser.parse(source.sources[file].content)
this.umlClasses = convertAST2UmlClasses(ast, this.currentFile) this.umlClasses = convertAST2UmlClasses(ast, this.currentFile)
let umlDot = '' let umlDot = ''
this.activeTheme = themeCollection.find(theme => theme.themeName === currentTheme.name) this.activeTheme = themeCollection.find(theme => theme.themeName === currentTheme.name)
umlDot = convertUmlClasses2Dot(this.umlClasses, false, { backColor: this.activeTheme.backgroundColor, textColor: this.activeTheme.textColor, shapeColor: this.activeTheme.shapeColor, fillColor: this.activeTheme.fillColor }) umlDot = convertUmlClasses2Dot(this.umlClasses, false, { backColor: this.activeTheme.backgroundColor, textColor: this.activeTheme.textColor, shapeColor: this.activeTheme.shapeColor, fillColor: this.activeTheme.fillColor })
const payload = vizRenderStringSync(umlDot) const payload = vizRenderStringSync(umlDot)
this.updatedSvg = payload this.updatedSvg = payload
_paq.push(['trackEvent', 'solidityumlgen', 'umlgenerated']) _paq.push(['trackEvent', 'solidityumlgen', 'umlgenerated'])
this.renderComponent()
await this.call('tabs', 'focus', 'solidityumlgen')
} catch (error) {
console.log('error', error)
}
})
}
getThemeCssVariables(cssVars: string) {
return window.getComputedStyle(document.documentElement)
.getPropertyValue(cssVars)
}
private handleThemeChange() {
this.on('theme', 'themeChanged', async (theme) => {
this.currentlySelectedTheme = theme.quality
const themeQuality: ThemeQualityType = await this.call('theme', 'currentTheme')
themeCollection.forEach((theme) => {
if (theme.themeName === themeQuality.name) {
this.themeDark = theme.backgroundColor
this.activeTheme = theme
const umlDot = convertUmlClasses2Dot(this.umlClasses, false, { backColor: this.activeTheme.backgroundColor, textColor: this.activeTheme.textColor, shapeColor: this.activeTheme.shapeColor, fillColor: this.activeTheme.fillColor })
this.updatedSvg = vizRenderStringSync(umlDot)
this.renderComponent()
}
})
await this.call('tabs', 'focus', 'solidityumlgen')
})
}
async mangleSvgPayload(svgPayload: string) : Promise<string> {
const parser = new DOMParser()
const themeQuality: ThemeQualityType = await this.call('theme', 'currentTheme')
const parsedDocument = parser.parseFromString(svgPayload, 'image/svg+xml')
const element = parsedDocument.getElementsByTagName('svg')
themeCollection.forEach((theme) => {
if (theme.themeName === themeQuality.name) {
parsedDocument.documentElement.setAttribute('style', `background-color: var(${this.getThemeCssVariables('--body-bg')})`)
element[0].setAttribute('fill', theme.backgroundColor)
}
})
const stringifiedSvg = new XMLSerializer().serializeToString(parsedDocument)
return stringifiedSvg
}
onDeactivation(): void {
this.off('solidity', 'compilationFinished')
}
generateCustomAction = async (action: customAction) => {
this.triggerGenerateUml = true
this.updatedSvg = this.updatedSvg.startsWith('<?xml') ? '' : this.updatedSvg
_paq.push(['trackEvent', 'solidityumlgen', 'activated'])
await this.generateUml(action.path[0])
}
async generateUml(currentFile: string) {
await this.call('solidity', 'compile', currentFile)
await this.call('tabs', 'focus', 'solidityumlgen')
this.loading = true
this.renderComponent() this.renderComponent()
} await this.call('tabs', 'focus', 'solidityumlgen')
} catch (error) {
/** console.log('error', error)
}
})
}
getThemeCssVariables(cssVars: string) {
return window.getComputedStyle(document.documentElement)
.getPropertyValue(cssVars)
}
private handleThemeChange() {
this.on('theme', 'themeChanged', async (theme) => {
this.currentlySelectedTheme = theme.quality
const themeQuality: ThemeQualityType = await this.call('theme', 'currentTheme')
themeCollection.forEach((theme) => {
if (theme.themeName === themeQuality.name) {
this.themeDark = theme.backgroundColor
this.activeTheme = theme
const umlDot = convertUmlClasses2Dot(this.umlClasses, false, { backColor: this.activeTheme.backgroundColor, textColor: this.activeTheme.textColor, shapeColor: this.activeTheme.shapeColor, fillColor: this.activeTheme.fillColor })
this.updatedSvg = vizRenderStringSync(umlDot)
this.renderComponent()
}
})
await this.call('tabs', 'focus', 'solidityumlgen')
})
}
async mangleSvgPayload(svgPayload: string) : Promise<string> {
const parser = new DOMParser()
const themeQuality: ThemeQualityType = await this.call('theme', 'currentTheme')
const parsedDocument = parser.parseFromString(svgPayload, 'image/svg+xml')
const element = parsedDocument.getElementsByTagName('svg')
themeCollection.forEach((theme) => {
if (theme.themeName === themeQuality.name) {
parsedDocument.documentElement.setAttribute('style', `background-color: var(${this.getThemeCssVariables('--body-bg')})`)
element[0].setAttribute('fill', theme.backgroundColor)
}
})
const stringifiedSvg = new XMLSerializer().serializeToString(parsedDocument)
return stringifiedSvg
}
onDeactivation(): void {
this.off('solidity', 'compilationFinished')
}
generateCustomAction = async (action: customAction) => {
this.triggerGenerateUml = true
this.updatedSvg = this.updatedSvg.startsWith('<?xml') ? '' : this.updatedSvg
_paq.push(['trackEvent', 'solidityumlgen', 'activated'])
await this.generateUml(action.path[0])
}
async generateUml(currentFile: string) {
await this.call('solidity', 'compile', currentFile)
await this.call('tabs', 'focus', 'solidityumlgen')
this.loading = true
this.renderComponent()
}
/**
* Takes currently compiled contract that has a bunch of imports at the top * Takes currently compiled contract that has a bunch of imports at the top
* and flattens them ready for UML creation. Takes the flattened result * and flattens them ready for UML creation. Takes the flattened result
* and assigns to a local property * and assigns to a local property
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
async flattenContract (source: any, filePath: string, data: any) { async flattenContract (source: any, filePath: string, data: any) {
const result = await this.call('contractflattener', 'flattenContract', source, filePath, data) const result = await this.call('contractflattener', 'flattenContract', source, filePath, data)
return result return result
} }
async showUmlDiagram(svgPayload: string) { async showUmlDiagram(svgPayload: string) {
this.updatedSvg = svgPayload this.updatedSvg = svgPayload
this.renderComponent() this.renderComponent()
} }
hideSpinner() { hideSpinner() {
this.loading = false this.loading = false
this.renderComponent() this.renderComponent()
} }
setDispatch (dispatch: React.Dispatch<any>) { setDispatch (dispatch: React.Dispatch<any>) {
this.dispatch = dispatch this.dispatch = dispatch
this.renderComponent() this.renderComponent()
} }
render() { render() {
return <div id='sol-uml-gen'> return <div id='sol-uml-gen'>
<PluginViewWrapper plugin={this} /> <PluginViewWrapper plugin={this} />
</div> </div>
} }
renderComponent () { renderComponent () {
this.dispatch({ this.dispatch({
...this, ...this,
updatedSvg: this.updatedSvg, updatedSvg: this.updatedSvg,
loading: this.loading, loading: this.loading,
themeSelected: this.currentlySelectedTheme, themeSelected: this.currentlySelectedTheme,
themeName: this.themeName, themeName: this.themeName,
themeDark: this.themeDark, themeDark: this.themeDark,
fileName: this.currentFile, fileName: this.currentFile,
themeCollection: this.themeCollection, themeCollection: this.themeCollection,
activeTheme: this.activeTheme, activeTheme: this.activeTheme,
}) })
} }
updateComponent(state: any) { updateComponent(state: any) {
return <RemixUiSolidityUmlGen return <RemixUiSolidityUmlGen
updatedSvg={state.updatedSvg} updatedSvg={state.updatedSvg}
loading={state.loading} loading={state.loading}
themeSelected={state.currentlySelectedTheme} themeSelected={state.currentlySelectedTheme}
themeName={state.themeName} themeName={state.themeName}
fileName={state.fileName} fileName={state.fileName}
themeCollection={state.themeCollection} themeCollection={state.themeCollection}
themeDark={state.themeDark} themeDark={state.themeDark}
/> />
} }
} }
@ -246,10 +246,10 @@ interface Sol2umlClassOptions extends ClassOptions {
import { dirname } from 'path' import { dirname } from 'path'
import { convertClass2Dot } from 'sol2uml/lib/converterClass2Dot' import { convertClass2Dot } from 'sol2uml/lib/converterClass2Dot'
import { import {
Association, Association,
ClassStereotype, ClassStereotype,
ReferenceType, ReferenceType,
UmlClass, UmlClass,
} from 'sol2uml/lib/umlClass' } from 'sol2uml/lib/umlClass'
import { findAssociatedClass } from 'sol2uml/lib/associations' import { findAssociatedClass } from 'sol2uml/lib/associations'
@ -264,11 +264,11 @@ import { findAssociatedClass } from 'sol2uml/lib/associations'
* @return dotString Graphviz's DOT format for defining nodes, edges and clusters. * @return dotString Graphviz's DOT format for defining nodes, edges and clusters.
*/ */
export function convertUmlClasses2Dot( export function convertUmlClasses2Dot(
umlClasses: UmlClass[], umlClasses: UmlClass[],
clusterFolders: boolean = false, clusterFolders: boolean = false,
classOptions: Sol2umlClassOptions = {} classOptions: Sol2umlClassOptions = {}
): string { ): string {
let dotString: string = ` let dotString: string = `
digraph UmlClassDiagram { digraph UmlClassDiagram {
rankdir=BT rankdir=BT
arrowhead=open arrowhead=open
@ -276,124 +276,124 @@ bgcolor="${classOptions.backColor}"
edge [color="${classOptions.shapeColor}"] edge [color="${classOptions.shapeColor}"]
node [shape=record, style=filled, color="${classOptions.shapeColor}", fillcolor="${classOptions.fillColor}", fontcolor="${classOptions.textColor}"]` node [shape=record, style=filled, color="${classOptions.shapeColor}", fillcolor="${classOptions.fillColor}", fontcolor="${classOptions.textColor}"]`
// Sort UML Classes by folder of source file // Sort UML Classes by folder of source file
const umlClassesSortedByCodePath = sortUmlClassesByCodePath(umlClasses) const umlClassesSortedByCodePath = sortUmlClassesByCodePath(umlClasses)
let currentCodeFolder = '' let currentCodeFolder = ''
for (const umlClass of umlClassesSortedByCodePath) { for (const umlClass of umlClassesSortedByCodePath) {
const codeFolder = dirname(umlClass.relativePath) const codeFolder = dirname(umlClass.relativePath)
if (currentCodeFolder !== codeFolder) { if (currentCodeFolder !== codeFolder) {
// Need to close off the last subgraph if not the first // Need to close off the last subgraph if not the first
if (currentCodeFolder != '') { if (currentCodeFolder != '') {
dotString += '\n}' dotString += '\n}'
} }
dotString += ` dotString += `
subgraph ${getSubGraphName(clusterFolders)} { subgraph ${getSubGraphName(clusterFolders)} {
label="${codeFolder}"` label="${codeFolder}"`
currentCodeFolder = codeFolder currentCodeFolder = codeFolder
}
dotString += convertClass2Dot(umlClass, classOptions)
} }
dotString += convertClass2Dot(umlClass, classOptions)
}
// Need to close off the last subgraph if not the first // Need to close off the last subgraph if not the first
if (currentCodeFolder != '') { if (currentCodeFolder != '') {
dotString += '\n}' dotString += '\n}'
} }
dotString += addAssociationsToDot(umlClasses, classOptions) dotString += addAssociationsToDot(umlClasses, classOptions)
// Need to close off the last the digraph // Need to close off the last the digraph
dotString += '\n}' dotString += '\n}'
// debug(dotString) // debug(dotString)
return dotString return dotString
} }
let subGraphCount = 0 let subGraphCount = 0
function getSubGraphName(clusterFolders: boolean = false) { function getSubGraphName(clusterFolders: boolean = false) {
if (clusterFolders) { if (clusterFolders) {
return ` cluster_${subGraphCount++}` return ` cluster_${subGraphCount++}`
} }
return ` graph_${subGraphCount++}` return ` graph_${subGraphCount++}`
} }
function sortUmlClassesByCodePath(umlClasses: UmlClass[]): UmlClass[] { function sortUmlClassesByCodePath(umlClasses: UmlClass[]): UmlClass[] {
return umlClasses.sort((a, b) => { return umlClasses.sort((a, b) => {
if (a.relativePath < b.relativePath) { if (a.relativePath < b.relativePath) {
return -1 return -1
} }
if (a.relativePath > b.relativePath) { if (a.relativePath > b.relativePath) {
return 1 return 1
} }
return 0 return 0
}) })
} }
export function addAssociationsToDot( export function addAssociationsToDot(
umlClasses: UmlClass[], umlClasses: UmlClass[],
classOptions: ClassOptions = {} classOptions: ClassOptions = {}
): string { ): string {
let dotString: string = '' let dotString: string = ''
// for each class // for each class
for (const sourceUmlClass of umlClasses) { for (const sourceUmlClass of umlClasses) {
if (!classOptions.hideEnums) { if (!classOptions.hideEnums) {
// for each enum in the class // for each enum in the class
sourceUmlClass.enums.forEach((enumId) => { sourceUmlClass.enums.forEach((enumId) => {
// Has the enum been filtered out? eg depth limited // Has the enum been filtered out? eg depth limited
const targetUmlClass = umlClasses.find((c) => c.id === enumId) const targetUmlClass = umlClasses.find((c) => c.id === enumId)
if (targetUmlClass) { if (targetUmlClass) {
// Draw aggregated link from contract to contract level Enum // Draw aggregated link from contract to contract level Enum
dotString += `\n${enumId} -> ${sourceUmlClass.id} [arrowhead=diamond, weight=2]` dotString += `\n${enumId} -> ${sourceUmlClass.id} [arrowhead=diamond, weight=2]`
}
})
} }
if (!classOptions.hideStructs) { })
// for each struct in the class }
sourceUmlClass.structs.forEach((structId) => { if (!classOptions.hideStructs) {
// Has the struct been filtered out? eg depth limited // for each struct in the class
const targetUmlClass = umlClasses.find((c) => c.id === structId) sourceUmlClass.structs.forEach((structId) => {
if (targetUmlClass) { // Has the struct been filtered out? eg depth limited
// Draw aggregated link from contract to contract level Struct const targetUmlClass = umlClasses.find((c) => c.id === structId)
dotString += `\n${structId} -> ${sourceUmlClass.id} [arrowhead=diamond, weight=2]` if (targetUmlClass) {
} // Draw aggregated link from contract to contract level Struct
}) dotString += `\n${structId} -> ${sourceUmlClass.id} [arrowhead=diamond, weight=2]`
} }
})
}
// for each association in that class // for each association in that class
for (const association of Object.values(sourceUmlClass.associations)) { for (const association of Object.values(sourceUmlClass.associations)) {
const targetUmlClass = findAssociatedClass( const targetUmlClass = findAssociatedClass(
association, association,
sourceUmlClass, sourceUmlClass,
umlClasses umlClasses
) )
if (targetUmlClass) { if (targetUmlClass) {
dotString += addAssociationToDot( dotString += addAssociationToDot(
sourceUmlClass, sourceUmlClass,
targetUmlClass, targetUmlClass,
association, association,
classOptions classOptions
) )
} }
}
} }
}
return dotString return dotString
} }
function addAssociationToDot( function addAssociationToDot(
sourceUmlClass: UmlClass, sourceUmlClass: UmlClass,
targetUmlClass: UmlClass, targetUmlClass: UmlClass,
association: Association, association: Association,
classOptions: ClassOptions = {} classOptions: ClassOptions = {}
): string { ): string {
// do not include library or interface associations if hidden // do not include library or interface associations if hidden
// Or associations to Structs, Enums or Constants if they are hidden // Or associations to Structs, Enums or Constants if they are hidden
if ( if (
(classOptions.hideLibraries && (classOptions.hideLibraries &&
(sourceUmlClass.stereotype === ClassStereotype.Library || (sourceUmlClass.stereotype === ClassStereotype.Library ||
targetUmlClass.stereotype === ClassStereotype.Library)) || targetUmlClass.stereotype === ClassStereotype.Library)) ||
(classOptions.hideInterfaces && (classOptions.hideInterfaces &&
@ -408,28 +408,28 @@ function addAssociationToDot(
targetUmlClass.stereotype === ClassStereotype.Enum) || targetUmlClass.stereotype === ClassStereotype.Enum) ||
(classOptions.hideConstants && (classOptions.hideConstants &&
targetUmlClass.stereotype === ClassStereotype.Constant) targetUmlClass.stereotype === ClassStereotype.Constant)
) { ) {
return '' return ''
} }
let dotString = `\n${sourceUmlClass.id} -> ${targetUmlClass.id} [` let dotString = `\n${sourceUmlClass.id} -> ${targetUmlClass.id} [`
if ( if (
association.referenceType == ReferenceType.Memory || association.referenceType == ReferenceType.Memory ||
(association.realization && (association.realization &&
targetUmlClass.stereotype === ClassStereotype.Interface) targetUmlClass.stereotype === ClassStereotype.Interface)
) { ) {
dotString += 'style=dashed, ' dotString += 'style=dashed, '
} }
if (association.realization) { if (association.realization) {
dotString += 'arrowhead=empty, arrowsize=3, ' dotString += 'arrowhead=empty, arrowsize=3, '
if (!targetUmlClass.stereotype) { if (!targetUmlClass.stereotype) {
dotString += 'weight=4, ' dotString += 'weight=4, '
} else { } else {
dotString += 'weight=3, ' dotString += 'weight=3, '
}
} }
}
return dotString + ']' return dotString + ']'
} }

@ -1,59 +1,59 @@
import { Plugin } from '@remixproject/engine'; import { Plugin } from '@remixproject/engine';
const profile = { const profile = {
name: 'storage', name: 'storage',
displayName: 'Storage', displayName: 'Storage',
description: 'Storage', description: 'Storage',
methods: ['getStorage', 'formatString'] methods: ['getStorage', 'formatString']
}; };
export class StoragePlugin extends Plugin { export class StoragePlugin extends Plugin {
constructor() { constructor() {
super(profile); super(profile);
}
async getStorage() {
let storage = null
if ('storage' in navigator && 'estimate' in navigator.storage && (window as any).remixFileSystem.name !== 'localstorage') {
storage = await navigator.storage.estimate()
} else {
storage ={
usage: parseFloat(this.calculateLocalStorage()) * 1000,
quota: 5000000,
}
} }
const _paq = (window as any)._paq = (window as any)._paq || []
async getStorage() { _paq.push(['trackEvent', 'Storage', 'used', this.formatString(storage)]);
let storage = null return storage
if ('storage' in navigator && 'estimate' in navigator.storage && (window as any).remixFileSystem.name !== 'localstorage') { }
storage = await navigator.storage.estimate()
} else { formatString(storage) {
storage ={ return `${this.formatBytes(storage.usage)} / ${this.formatBytes(storage.quota)}`;
usage: parseFloat(this.calculateLocalStorage()) * 1000, }
quota: 5000000,
} calculateLocalStorage() {
} let _lsTotal = 0
const _paq = (window as any)._paq = (window as any)._paq || [] let _xLen; let _x
_paq.push(['trackEvent', 'Storage', 'used', this.formatString(storage)]); for (_x in localStorage) {
return storage // eslint-disable-next-line no-prototype-builtins
} if (!localStorage.hasOwnProperty(_x)) {
continue
formatString(storage) { }
return `${this.formatBytes(storage.usage)} / ${this.formatBytes(storage.quota)}`; _xLen = ((localStorage[_x].length + _x.length))
_lsTotal += _xLen
} }
return (_lsTotal / 1024).toFixed(2)
}
calculateLocalStorage() { formatBytes(bytes: number, decimals = 2) {
let _lsTotal = 0 if (bytes === 0) return '0 Bytes';
let _xLen; let _x
for (_x in localStorage) {
// eslint-disable-next-line no-prototype-builtins
if (!localStorage.hasOwnProperty(_x)) {
continue
}
_xLen = ((localStorage[_x].length + _x.length))
_lsTotal += _xLen
}
return (_lsTotal / 1024).toFixed(2)
}
formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024; const k = 1024;
const dm = decimals < 0 ? 0 : decimals; const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
} }
} }

@ -28,110 +28,110 @@ export interface IProvider {
} }
export abstract class AbstractProvider extends Plugin implements IProvider { export abstract class AbstractProvider extends Plugin implements IProvider {
provider: ethers.providers.JsonRpcProvider provider: ethers.providers.JsonRpcProvider
blockchain: Blockchain blockchain: Blockchain
defaultUrl: string defaultUrl: string
connected: boolean connected: boolean
nodeUrl: string nodeUrl: string
options: { [id: string] : any } = {} options: { [id: string] : any } = {}
constructor (profile, blockchain, defaultUrl) { constructor (profile, blockchain, defaultUrl) {
super(profile) super(profile)
this.defaultUrl = defaultUrl this.defaultUrl = defaultUrl
this.provider = null this.provider = null
this.connected = false this.connected = false
this.blockchain = blockchain this.blockchain = blockchain
this.nodeUrl = 'http://localhost:8545' this.nodeUrl = 'http://localhost:8545'
} }
abstract body(): JSX.Element abstract body(): JSX.Element
onDeactivation () { onDeactivation () {
this.provider = null this.provider = null
} }
async init () { async init () {
this.nodeUrl = await ((): Promise<string> => { this.nodeUrl = await ((): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const modalContent: AppModal = { const modalContent: AppModal = {
id: this.profile.name, id: this.profile.name,
title: this.profile.displayName, title: this.profile.displayName,
message: this.body(), message: this.body(),
modalType: ModalTypes.prompt, modalType: ModalTypes.prompt,
okLabel: 'OK', okLabel: 'OK',
cancelLabel: 'Cancel', cancelLabel: 'Cancel',
validationFn: (value) => { validationFn: (value) => {
if (!value) return { valid: false, message: "value is empty" } if (!value) return { valid: false, message: "value is empty" }
if (value.startsWith('https://') || value.startsWith('http://')) { if (value.startsWith('https://') || value.startsWith('http://')) {
return { return {
valid: true, valid: true,
message: '' message: ''
}
} else {
return {
valid: false,
message: 'the provided value should contain the protocol ( e.g starts with http:// or https:// )'
}
}
},
okFn: (value: string) => {
setTimeout(() => resolve(value), 0)
},
cancelFn: () => {
setTimeout(() => reject(new Error('Canceled')), 0)
},
hideFn: () => {
setTimeout(() => reject(new Error('Hide')), 0)
},
defaultValue: this.defaultUrl
} }
this.call('notification', 'modal', modalContent) } else {
}) return {
})() valid: false,
this.provider = new ethers.providers.JsonRpcProvider(this.nodeUrl) message: 'the provided value should contain the protocol ( e.g starts with http:// or https:// )'
return { }
nodeUrl: this.nodeUrl }
} },
okFn: (value: string) => {
setTimeout(() => resolve(value), 0)
},
cancelFn: () => {
setTimeout(() => reject(new Error('Canceled')), 0)
},
hideFn: () => {
setTimeout(() => reject(new Error('Hide')), 0)
},
defaultValue: this.defaultUrl
}
this.call('notification', 'modal', modalContent)
})
})()
this.provider = new ethers.providers.JsonRpcProvider(this.nodeUrl)
return {
nodeUrl: this.nodeUrl
}
} }
sendAsync (data: JsonDataRequest): Promise<JsonDataResult> { sendAsync (data: JsonDataRequest): Promise<JsonDataResult> {
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
if (!this.provider) return reject(new Error('provider node set')) if (!this.provider) return reject(new Error('provider node set'))
this.sendAsyncInternal(data, resolve, reject) this.sendAsyncInternal(data, resolve, reject)
}) })
} }
private async switchAway (showError) { private async switchAway (showError) {
if (!this.provider) return if (!this.provider) return
this.provider = null this.provider = null
this.connected = false this.connected = false
if (showError) { if (showError) {
const modalContent: AlertModal = { const modalContent: AlertModal = {
id: this.profile.name, id: this.profile.name,
title: this.profile.displayName, title: this.profile.displayName,
message: `Error while connecting to the provider, provider not connected`, message: `Error while connecting to the provider, provider not connected`,
}
this.call('notification', 'alert', modalContent)
} }
await this.call('udapp', 'setEnvironmentMode', { context: 'vm-merge'}) this.call('notification', 'alert', modalContent)
return }
await this.call('udapp', 'setEnvironmentMode', { context: 'vm-merge'})
return
} }
private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> { private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> {
if (this.provider) { if (this.provider) {
try { try {
const result = await this.provider.send(data.method, data.params) const result = await this.provider.send(data.method, data.params)
resolve({ jsonrpc: '2.0', result, id: data.id }) resolve({ jsonrpc: '2.0', result, id: data.id })
} catch (error) { } catch (error) {
if (error && error.message && error.message.includes('net_version') && error.message.includes('SERVER_ERROR')) { if (error && error.message && error.message.includes('net_version') && error.message.includes('SERVER_ERROR')) {
this.switchAway(true) this.switchAway(true)
} }
reject(error) reject(error)
}
} else {
const result = data.method === 'net_listening' ? 'canceled' : []
resolve({ jsonrpc: '2.0', result: result, id: data.id })
} }
} else {
const result = data.method === 'net_listening' ? 'canceled' : []
resolve({ jsonrpc: '2.0', result: result, id: data.id })
}
} }
} }

@ -5,102 +5,102 @@ import { BasicVMProvider } from './vm-provider'
import { Hardfork } from '@ethereumjs/common' import { Hardfork } from '@ethereumjs/common'
export class CustomForkVMProvider extends BasicVMProvider { export class CustomForkVMProvider extends BasicVMProvider {
nodeUrl: string nodeUrl: string
blockNumber: number | 'latest' blockNumber: number | 'latest'
inputs: any inputs: any
constructor (blockchain) { constructor (blockchain) {
super({ super({
name: 'vm-custom-fork', name: 'vm-custom-fork',
displayName: 'Custom fork - Remix VM', displayName: 'Custom fork - Remix VM',
kind: 'provider', kind: 'provider',
description: 'Custom fork - Remix VM', description: 'Custom fork - Remix VM',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
}, blockchain) }, blockchain)
this.blockchain = blockchain this.blockchain = blockchain
this.fork = '' this.fork = ''
this.nodeUrl = '' this.nodeUrl = ''
this.blockNumber = 'latest' this.blockNumber = 'latest'
this.inputs = {} this.inputs = {}
} }
async init () { async init () {
const body = () => { const body = () => {
return <div> return <div>
<span>Please provide information about the custom fork. If the node URL is not provided, the VM will start with an empty state.</span> <span>Please provide information about the custom fork. If the node URL is not provided, the VM will start with an empty state.</span>
<div> <div>
<label className="mt-3 mb-1">Node URL</label> <label className="mt-3 mb-1">Node URL</label>
<input data-id="CustomForkNodeUrl" name="nodeUrl" type="text" className="border form-control border-right-0" /> <input data-id="CustomForkNodeUrl" name="nodeUrl" type="text" className="border form-control border-right-0" />
</div> </div>
<div> <div>
<label className="mt-3 mb-1">Block number (or "latest")</label> <label className="mt-3 mb-1">Block number (or "latest")</label>
<input data-id="CustomForkBlockNumber" name="blockNumber" type="text" defaultValue="latest" placeholder='block number or "latest"' className="border form-control border-right-0" /> <input data-id="CustomForkBlockNumber" name="blockNumber" type="text" defaultValue="latest" placeholder='block number or "latest"' className="border form-control border-right-0" />
</div> </div>
<div> <div>
<label className="mt-3 mb-1">EVM</label> <label className="mt-3 mb-1">EVM</label>
<select data-id="CustomForkEvmType" name="evmType" defaultValue="merge" className="border form-control border-right-0"> <select data-id="CustomForkEvmType" name="evmType" defaultValue="merge" className="border form-control border-right-0">
{Object.keys(Hardfork).map((value, index) => { {Object.keys(Hardfork).map((value, index) => {
return <option value={Hardfork[value]} key={index}>{value}</option> return <option value={Hardfork[value]} key={index}>{value}</option>
})} })}
</select> </select>
</div> </div>
</div> </div>
} }
const result = await ((): Promise<any> => { const result = await ((): Promise<any> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const modalContent: AppModal = { const modalContent: AppModal = {
id: this.profile.name, id: this.profile.name,
title: this.profile.displayName, title: this.profile.displayName,
message: body(), message: body(),
validationFn: (data: any) => { validationFn: (data: any) => {
if(data.nodeUrl !== '' && !data.nodeUrl.startsWith("http")) { if(data.nodeUrl !== '' && !data.nodeUrl.startsWith("http")) {
return { return {
valid: false, valid: false,
message: 'node URL should be a valid URL' message: 'node URL should be a valid URL'
} }
} }
if (data.blockNumber !== 'latest' && isNaN(data.blockNumber)) { if (data.blockNumber !== 'latest' && isNaN(data.blockNumber)) {
return { return {
valid: false, valid: false,
message: 'blockNumber should be a number or "latest"' message: 'blockNumber should be a number or "latest"'
} }
} }
return { return {
valid: true, valid: true,
message: '' message: ''
} }
}, },
modalType: ModalTypes.form, modalType: ModalTypes.form,
okLabel: 'Connect', okLabel: 'Connect',
cancelLabel: 'Cancel', cancelLabel: 'Cancel',
okFn: (value: string) => { okFn: (value: string) => {
setTimeout(() => resolve(value), 0) setTimeout(() => resolve(value), 0)
}, },
cancelFn: () => { cancelFn: () => {
setTimeout(() => reject(new Error('Canceled')), 0) setTimeout(() => reject(new Error('Canceled')), 0)
}, },
hideFn: () => { hideFn: () => {
setTimeout(() => reject(new Error('Hide')), 0) setTimeout(() => reject(new Error('Hide')), 0)
} }
}
return this.call('notification', 'modal', modalContent)
})
})()
this.fork = result.evmType
this.nodeUrl = result.nodeUrl
if (this.nodeUrl) {
const block = result.blockNumber
this.blockNumber = block === 'latest' ? 'latest' : parseInt(block)
} else {
this.nodeUrl = undefined
this.blockNumber = undefined
} }
return this.call('notification', 'modal', modalContent)
})
})()
this.fork = result.evmType
this.nodeUrl = result.nodeUrl
if (this.nodeUrl) {
const block = result.blockNumber
this.blockNumber = block === 'latest' ? 'latest' : parseInt(block)
} else {
this.nodeUrl = undefined
this.blockNumber = undefined
}
return { return {
'fork': this.fork, 'fork': this.fork,
'nodeUrl': this.nodeUrl, 'nodeUrl': this.nodeUrl,
'blockNumber': this.blockNumber 'blockNumber': this.blockNumber
}
} }
}
} }

@ -3,39 +3,39 @@ import React from 'react' // eslint-disable-line
import { AbstractProvider } from './abstract-provider' import { AbstractProvider } from './abstract-provider'
const profile = { const profile = {
name: 'basic-http-provider', name: 'basic-http-provider',
displayName: 'External Http Provider', displayName: 'External Http Provider',
kind: 'provider', kind: 'provider',
description: 'External Http Provider', description: 'External Http Provider',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
} }
export class ExternalHttpProvider extends AbstractProvider { export class ExternalHttpProvider extends AbstractProvider {
constructor (blockchain) { constructor (blockchain) {
super(profile, blockchain, 'http://127.0.0.1:8545') super(profile, blockchain, 'http://127.0.0.1:8545')
} }
body (): JSX.Element { body (): JSX.Element {
const thePath = '<path/to/local/folder/for/test/chain>' const thePath = '<path/to/local/folder/for/test/chain>'
return ( return (
<> <>
<div className=""> <div className="">
Note: To use Geth & https://remix.ethereum.org, configure it to allow requests from Remix:(see <a href="https://geth.ethereum.org/docs/rpc/server" target="_blank" rel="noreferrer">Geth Docs on rpc server</a>) Note: To use Geth & https://remix.ethereum.org, configure it to allow requests from Remix:(see <a href="https://geth.ethereum.org/docs/rpc/server" target="_blank" rel="noreferrer">Geth Docs on rpc server</a>)
<div className="border p-1">geth --http --http.corsdomain https://remix.ethereum.org</div> <div className="border p-1">geth --http --http.corsdomain https://remix.ethereum.org</div>
<br /> <br />
To run Remix & a local Geth test node, use this command: (see <a href="https://geth.ethereum.org/getting-started/dev-mode" target="_blank" rel="noreferrer">Geth Docs on Dev mode</a>) To run Remix & a local Geth test node, use this command: (see <a href="https://geth.ethereum.org/getting-started/dev-mode" target="_blank" rel="noreferrer">Geth Docs on Dev mode</a>)
<div className="border p-1">geth --http --http.corsdomain="{window.origin}" --http.api web3,eth,debug,personal,net --vmdebug --datadir {thePath} --dev console</div> <div className="border p-1">geth --http --http.corsdomain="{window.origin}" --http.api web3,eth,debug,personal,net --vmdebug --datadir {thePath} --dev console</div>
<br /> <br />
<br /> <br />
<b>WARNING:</b> It is not safe to use the --http.corsdomain flag with a wildcard: <b>--http.corsdomain *</b> <b>WARNING:</b> It is not safe to use the --http.corsdomain flag with a wildcard: <b>--http.corsdomain *</b>
<br /> <br />
<br />For more info: <a href="https://remix-ide.readthedocs.io/en/latest/run.html#more-about-web3-provider" target="_blank" rel="noreferrer">Remix Docs on External HTTP Provider</a> <br />For more info: <a href="https://remix-ide.readthedocs.io/en/latest/run.html#more-about-web3-provider" target="_blank" rel="noreferrer">Remix Docs on External HTTP Provider</a>
<br /> <br />
<br /> <br />
External HTTP Provider Endpoint External HTTP Provider Endpoint
</div> </div>
</> </>
) )
} }
} }

@ -3,29 +3,29 @@ import React from 'react' // eslint-disable-line
import { AbstractProvider } from './abstract-provider' import { AbstractProvider } from './abstract-provider'
const profile = { const profile = {
name: 'foundry-provider', name: 'foundry-provider',
displayName: 'Foundry Provider', displayName: 'Foundry Provider',
kind: 'provider', kind: 'provider',
description: 'Foundry Anvil provider', description: 'Foundry Anvil provider',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
} }
export class FoundryProvider extends AbstractProvider { export class FoundryProvider extends AbstractProvider {
constructor (blockchain) { constructor (blockchain) {
super(profile, blockchain, 'http://127.0.0.1:8545') super(profile, blockchain, 'http://127.0.0.1:8545')
} }
body (): JSX.Element { body (): JSX.Element {
return ( return (
<div> Note: To run Anvil on your system, run: <div> Note: To run Anvil on your system, run:
<div className="p-1 pl-3"><b>curl -L https://foundry.paradigm.xyz | bash</b></div> <div className="p-1 pl-3"><b>curl -L https://foundry.paradigm.xyz | bash</b></div>
<div className="p-1 pl-3"><b>anvil</b></div> <div className="p-1 pl-3"><b>anvil</b></div>
<div className="pt-2 pb-4"> <div className="pt-2 pb-4">
For more info, visit: <a href="https://github.com/foundry-rs/foundry" target="_blank">Foundry Documentation</a> For more info, visit: <a href="https://github.com/foundry-rs/foundry" target="_blank">Foundry Documentation</a>
</div> </div>
<div>Anvil JSON-RPC Endpoint:</div> <div>Anvil JSON-RPC Endpoint:</div>
</div> </div>
) )
} }
} }

@ -3,29 +3,29 @@ import React from 'react' // eslint-disable-line
import { AbstractProvider } from './abstract-provider' import { AbstractProvider } from './abstract-provider'
const profile = { const profile = {
name: 'ganache-provider', name: 'ganache-provider',
displayName: 'Ganache Provider', displayName: 'Ganache Provider',
kind: 'provider', kind: 'provider',
description: 'Truffle Ganache provider', description: 'Truffle Ganache provider',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
} }
export class GanacheProvider extends AbstractProvider { export class GanacheProvider extends AbstractProvider {
constructor (blockchain) { constructor (blockchain) {
super(profile, blockchain, 'http://127.0.0.1:8545') super(profile, blockchain, 'http://127.0.0.1:8545')
} }
body (): JSX.Element { body (): JSX.Element {
return ( return (
<div> Note: To run Ganache on your system, run: <div> Note: To run Ganache on your system, run:
<div className="p-1 pl-3"><b>yarn global add ganache</b></div> <div className="p-1 pl-3"><b>yarn global add ganache</b></div>
<div className="p-1 pl-3"><b>ganache</b></div> <div className="p-1 pl-3"><b>ganache</b></div>
<div className="pt-2 pb-4"> <div className="pt-2 pb-4">
For more info, visit: <a href="https://github.com/trufflesuite/ganache" target="_blank">Ganache Documentation</a> For more info, visit: <a href="https://github.com/trufflesuite/ganache" target="_blank">Ganache Documentation</a>
</div> </div>
<div>Ganache JSON-RPC Endpoint:</div> <div>Ganache JSON-RPC Endpoint:</div>
</div> </div>
) )
} }
} }

@ -2,28 +2,28 @@ import * as packageJson from '../../../../../package.json'
import { BasicVMProvider } from './vm-provider' import { BasicVMProvider } from './vm-provider'
export class GoerliForkVMProvider extends BasicVMProvider { export class GoerliForkVMProvider extends BasicVMProvider {
nodeUrl: string nodeUrl: string
blockNumber: number | 'latest' blockNumber: number | 'latest'
constructor (blockchain) { constructor (blockchain) {
super({ super({
name: 'vm-goerli-fork', name: 'vm-goerli-fork',
displayName: 'Goerli fork - Remix VM (London)', displayName: 'Goerli fork - Remix VM (London)',
kind: 'provider', kind: 'provider',
description: 'Remix VM (London)', description: 'Remix VM (London)',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
}, blockchain) }, blockchain)
this.blockchain = blockchain this.blockchain = blockchain
this.fork = 'shanghai' this.fork = 'shanghai'
this.nodeUrl = 'https://remix-sepolia.ethdevops.io' this.nodeUrl = 'https://remix-sepolia.ethdevops.io'
this.blockNumber = 'latest' this.blockNumber = 'latest'
} }
async init () { async init () {
return { return {
'fork': this.fork, 'fork': this.fork,
'nodeUrl': this.nodeUrl, 'nodeUrl': this.nodeUrl,
'blockNumber': this.blockNumber 'blockNumber': this.blockNumber
}
} }
}
} }

@ -3,28 +3,28 @@ import React from 'react' // eslint-disable-line
import { AbstractProvider } from './abstract-provider' import { AbstractProvider } from './abstract-provider'
const profile = { const profile = {
name: 'hardhat-provider', name: 'hardhat-provider',
displayName: 'Hardhat Provider', displayName: 'Hardhat Provider',
kind: 'provider', kind: 'provider',
description: 'Hardhat provider', description: 'Hardhat provider',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
} }
export class HardhatProvider extends AbstractProvider { export class HardhatProvider extends AbstractProvider {
constructor (blockchain) { constructor (blockchain) {
super(profile, blockchain, 'http://127.0.0.1:8545') super(profile, blockchain, 'http://127.0.0.1:8545')
} }
body (): JSX.Element { body (): JSX.Element {
return ( return (
<div> Note: To run Hardhat network node on your system, go to hardhat project folder and run command: <div> Note: To run Hardhat network node on your system, go to hardhat project folder and run command:
<div className="p-1 pl-3"><b>npx hardhat node</b></div> <div className="p-1 pl-3"><b>npx hardhat node</b></div>
<div className="pt-2 pb-4"> <div className="pt-2 pb-4">
For more info, visit: <a href="https://hardhat.org/getting-started/#connecting-a-wallet-or-dapp-to-hardhat-network" target="_blank">Hardhat Documentation</a> 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> </div>
<div>Hardhat JSON-RPC Endpoint:</div> <div>Hardhat JSON-RPC Endpoint:</div>
</div> </div>
) )
} }
} }

@ -1,55 +1,55 @@
import { InjectedProviderDefaultBase } from './injected-provider-default' import { InjectedProviderDefaultBase } from './injected-provider-default'
export class InjectedL2Provider extends InjectedProviderDefaultBase { export class InjectedL2Provider extends InjectedProviderDefaultBase {
chainName: string chainName: string
chainId: string chainId: string
rpcUrls: Array<string> rpcUrls: Array<string>
constructor (profile: any, chainName: string, chainId: string, rpcUrls: Array<string>) { constructor (profile: any, chainName: string, chainId: string, rpcUrls: Array<string>) {
super(profile) super(profile)
this.chainName = chainName this.chainName = chainName
this.chainId = chainId this.chainId = chainId
this.rpcUrls = rpcUrls this.rpcUrls = rpcUrls
} }
async init () { async init () {
await super.init() await super.init()
if (this.chainName && this.rpcUrls && this.rpcUrls.length > 0) await addL2Network(this.chainName, this.chainId, this.rpcUrls) if (this.chainName && this.rpcUrls && this.rpcUrls.length > 0) await addL2Network(this.chainName, this.chainId, this.rpcUrls)
else else
throw new Error('Cannot add the L2 network to main injected provider') throw new Error('Cannot add the L2 network to main injected provider')
return {} return {}
} }
} }
export const addL2Network = async (chainName: string, chainId: string, rpcUrls: Array<string>) => { export const addL2Network = async (chainName: string, chainId: string, rpcUrls: Array<string>) => {
try { try {
await (window as any).ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainId }],
});
} catch (switchError) {
// This error code indicates that the chain has not been added to MetaMask.
if (switchError.code === 4902) {
try {
await (window as any).ethereum.request({ await (window as any).ethereum.request({
method: 'wallet_switchEthereumChain', method: 'wallet_addEthereumChain',
params: [{ chainId: chainId }], params: [
{
chainId: chainId,
chainName: chainName,
rpcUrls: rpcUrls,
},
],
}); });
} catch (switchError) {
// This error code indicates that the chain has not been added to MetaMask.
if (switchError.code === 4902) {
try {
await (window as any).ethereum.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: chainId,
chainName: chainName,
rpcUrls: rpcUrls,
},
],
});
await (window as any).ethereum.request({ await (window as any).ethereum.request({
method: 'wallet_switchEthereumChain', method: 'wallet_switchEthereumChain',
params: [{ chainId: chainId }], params: [{ chainId: chainId }],
}); });
} catch (addError) { } catch (addError) {
// handle "add" error // handle "add" error
} }
}
// handle other "switch" errors
} }
// handle other "switch" errors
}
} }

@ -2,17 +2,17 @@ import * as packageJson from '../../../../../package.json'
import { InjectedL2Provider } from './injected-L2-provider' import { InjectedL2Provider } from './injected-L2-provider'
const profile = { const profile = {
name: 'injected-arbitrum-one-provider', name: 'injected-arbitrum-one-provider',
displayName: 'Injected Arbitrum One Provider', displayName: 'Injected Arbitrum One Provider',
kind: 'provider', kind: 'provider',
description: 'injected Arbitrum One Provider', description: 'injected Arbitrum One Provider',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
} }
export class InjectedArbitrumOneProvider extends InjectedL2Provider { export class InjectedArbitrumOneProvider extends InjectedL2Provider {
constructor () { constructor () {
super(profile, 'Arbitrum One', '0xa4b1', ['https://arb1.arbitrum.io/rpc']) super(profile, 'Arbitrum One', '0xa4b1', ['https://arb1.arbitrum.io/rpc'])
} }
} }

@ -2,17 +2,17 @@ import * as packageJson from '../../../../../package.json'
import { InjectedL2Provider } from './injected-L2-provider' import { InjectedL2Provider } from './injected-L2-provider'
const profile = { const profile = {
name: 'injected-optimism-provider', name: 'injected-optimism-provider',
displayName: 'Injected Optimism Provider', displayName: 'Injected Optimism Provider',
kind: 'provider', kind: 'provider',
description: 'injected Optimism Provider', description: 'injected Optimism Provider',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
} }
export class Injected0ptimismProvider extends InjectedL2Provider { export class Injected0ptimismProvider extends InjectedL2Provider {
constructor () { constructor () {
super(profile, 'Optimism', '0xa', ['https://mainnet.optimism.io']) super(profile, 'Optimism', '0xa', ['https://mainnet.optimism.io'])
} }
} }

@ -3,38 +3,38 @@ import * as packageJson from '../../../../../package.json'
import { InjectedProvider } from './injected-provider' import { InjectedProvider } from './injected-provider'
export class InjectedProviderDefaultBase extends InjectedProvider { export class InjectedProviderDefaultBase extends InjectedProvider {
constructor (profile) { constructor (profile) {
super(profile) super(profile)
} }
async init () { async init () {
const injectedProvider = this.getInjectedProvider() const injectedProvider = this.getInjectedProvider()
if (injectedProvider && injectedProvider._metamask && injectedProvider._metamask.isUnlocked) { if (injectedProvider && injectedProvider._metamask && injectedProvider._metamask.isUnlocked) {
if (!await injectedProvider._metamask.isUnlocked()) this.call('notification', 'toast', 'Please make sure the injected provider is unlocked (e.g Metamask).') if (!await injectedProvider._metamask.isUnlocked()) this.call('notification', 'toast', 'Please make sure the injected provider is unlocked (e.g Metamask).')
}
return super.init()
} }
return super.init()
}
getInjectedProvider () { getInjectedProvider () {
return (window as any).ethereum return (window as any).ethereum
} }
notFound () { notFound () {
return 'No injected provider found. Make sure your provider (e.g. MetaMask, ...) is active and running (when recently activated you may have to reload the page).' return 'No injected provider found. Make sure your provider (e.g. MetaMask, ...) is active and running (when recently activated you may have to reload the page).'
} }
} }
const profile = { const profile = {
name: 'injected', name: 'injected',
displayName: 'Injected Provider', displayName: 'Injected Provider',
kind: 'provider', kind: 'provider',
description: 'injected Provider', description: 'injected Provider',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
} }
export class InjectedProviderDefault extends InjectedProviderDefaultBase { export class InjectedProviderDefault extends InjectedProviderDefaultBase {
constructor () { constructor () {
super(profile) super(profile)
} }
} }

@ -3,24 +3,24 @@ import * as packageJson from '../../../../../package.json'
import { InjectedProvider } from './injected-provider' import { InjectedProvider } from './injected-provider'
const profile = { const profile = {
name: 'injected-trustwallet', name: 'injected-trustwallet',
displayName: 'Trust wallet', displayName: 'Trust wallet',
kind: 'provider', kind: 'provider',
description: 'Trust wallet', description: 'Trust wallet',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
} }
export class InjectedProviderTrustWallet extends InjectedProvider { export class InjectedProviderTrustWallet extends InjectedProvider {
constructor () { constructor () {
super(profile) super(profile)
} }
getInjectedProvider () { getInjectedProvider () {
return (window as any).trustwallet return (window as any).trustwallet
} }
notFound () { notFound () {
return 'Could not find Trust Wallet provider. Please make sure the Trust Wallet extension is active. Download the latest version from https://trustwallet.com/browser-extension' return 'Could not find Trust Wallet provider. Please make sure the Trust Wallet extension is active. Download the latest version from https://trustwallet.com/browser-extension'
} }
} }

@ -5,101 +5,101 @@ import { JsonDataRequest, RejectRequest, SuccessRequest } from '../providers/abs
import { IProvider } from './abstract-provider' import { IProvider } from './abstract-provider'
export abstract class InjectedProvider extends Plugin implements IProvider { export abstract class InjectedProvider extends Plugin implements IProvider {
options: { [id: string] : any } = {} options: { [id: string] : any } = {}
listenerAccountsChanged: (accounts: Array<string>) => void listenerAccountsChanged: (accounts: Array<string>) => void
listenerChainChanged: (chainId: number) => void listenerChainChanged: (chainId: number) => void
constructor (profile) { constructor (profile) {
super(profile) super(profile)
this.listenerAccountsChanged = (accounts: Array<string>) => { this.listenerAccountsChanged = (accounts: Array<string>) => {
this.emit('accountsChanged', accounts) this.emit('accountsChanged', accounts)
} }
this.listenerChainChanged = (chainId: number) => { this.listenerChainChanged = (chainId: number) => {
this.emit('chainChanged', chainId) this.emit('chainChanged', chainId)
}
} }
}
abstract getInjectedProvider(): any abstract getInjectedProvider(): any
abstract notFound(): string abstract notFound(): string
onActivation(): void { onActivation(): void {
try { try {
const web3Provider = this.getInjectedProvider() const web3Provider = this.getInjectedProvider()
web3Provider.on('accountsChanged', this.listenerAccountsChanged); web3Provider.on('accountsChanged', this.listenerAccountsChanged);
web3Provider.on('chainChanged', this.listenerChainChanged); web3Provider.on('chainChanged', this.listenerChainChanged);
} catch (error) { } catch (error) {
console.log('unable to listen on context changed') console.log('unable to listen on context changed')
} }
} }
onDeactivation(): void { onDeactivation(): void {
try { try {
const web3Provider = this.getInjectedProvider() const web3Provider = this.getInjectedProvider()
web3Provider.removeListener('accountsChanged', this.listenerAccountsChanged) web3Provider.removeListener('accountsChanged', this.listenerAccountsChanged)
web3Provider.removeListener('chainChanged', this.listenerChainChanged) web3Provider.removeListener('chainChanged', this.listenerChainChanged)
} catch (error) { } catch (error) {
console.log('unable to remove listener on context changed') console.log('unable to remove listener on context changed')
} }
} }
askPermission (throwIfNoInjectedProvider) { askPermission (throwIfNoInjectedProvider) {
const web3Provider = this.getInjectedProvider() const web3Provider = this.getInjectedProvider()
if (typeof web3Provider !== "undefined" && typeof web3Provider.request === "function") { if (typeof web3Provider !== "undefined" && typeof web3Provider.request === "function") {
web3Provider.request({ method: "eth_requestAccounts" }) web3Provider.request({ method: "eth_requestAccounts" })
} else if (throwIfNoInjectedProvider) { } else if (throwIfNoInjectedProvider) {
throw new Error(this.notFound()) throw new Error(this.notFound())
} }
} }
body (): JSX.Element { body (): JSX.Element {
return ( return (
<div></div> <div></div>
) )
} }
async init () { async init () {
const injectedProvider = this.getInjectedProvider() const injectedProvider = this.getInjectedProvider()
if (injectedProvider === undefined) { if (injectedProvider === undefined) {
this.call('notification', 'toast', this.notFound()) this.call('notification', 'toast', this.notFound())
throw new Error(this.notFound()) throw new Error(this.notFound())
} else { } else {
this.askPermission(true) this.askPermission(true)
} }
return {} return {}
} }
sendAsync (data: JsonDataRequest): Promise<any> { sendAsync (data: JsonDataRequest): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.sendAsyncInternal(data, resolve, reject) this.sendAsyncInternal(data, resolve, reject)
}) })
} }
private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> { private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> {
// Check the case where current environment is VM on UI and it still sends RPC requests // Check the case where current environment is VM on UI and it still sends RPC requests
// This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!' // This will be displayed on UI tooltip as 'cannot get account list: Environment Updated !!'
const web3Provider = this.getInjectedProvider() const web3Provider = this.getInjectedProvider()
if (!web3Provider) { if (!web3Provider) {
this.call('notification', 'toast', 'No injected provider (e.g Metamask) has been found.') this.call('notification', 'toast', 'No injected provider (e.g Metamask) has been found.')
return resolve({ jsonrpc: '2.0', error: 'no injected provider found', id: data.id }) return resolve({ jsonrpc: '2.0', error: 'no injected provider found', id: data.id })
}
try {
let resultData
if (web3Provider.send) resultData = await web3Provider.send(data.method, data.params)
else if (web3Provider.request) resultData = await web3Provider.request({ method: data.method, params: data.params})
else {
resolve({ jsonrpc: '2.0', error: 'provider not valid', id: data.id })
return
} }
try { if (resultData) {
let resultData if (resultData.jsonrpc && resultData.jsonrpc === '2.0') {
if (web3Provider.send) resultData = await web3Provider.send(data.method, data.params) resultData = resultData.result
else if (web3Provider.request) resultData = await web3Provider.request({ method: data.method, params: data.params}) }
else { resolve({ jsonrpc: '2.0', result: resultData, id: data.id })
resolve({ jsonrpc: '2.0', error: 'provider not valid', id: data.id }) } else {
return resolve({ jsonrpc: '2.0', error: 'no return data provided', id: data.id })
}
if (resultData) {
if (resultData.jsonrpc && resultData.jsonrpc === '2.0') {
resultData = resultData.result
}
resolve({ jsonrpc: '2.0', result: resultData, id: data.id })
} else {
resolve({ jsonrpc: '2.0', error: 'no return data provided', id: data.id })
}
} catch (error) {
resolve({ jsonrpc: '2.0', error: error.data && error.data.message ? error.data.message : error.message, id: data.id })
} }
} catch (error) {
resolve({ jsonrpc: '2.0', error: error.data && error.data.message ? error.data.message : error.message, id: data.id })
}
} }
} }

@ -2,28 +2,28 @@ import * as packageJson from '../../../../../package.json'
import { BasicVMProvider } from './vm-provider' import { BasicVMProvider } from './vm-provider'
export class MainnetForkVMProvider extends BasicVMProvider { export class MainnetForkVMProvider extends BasicVMProvider {
nodeUrl: string nodeUrl: string
blockNumber: number | 'latest' blockNumber: number | 'latest'
constructor (blockchain) { constructor (blockchain) {
super({ super({
name: 'vm-mainnet-fork', name: 'vm-mainnet-fork',
displayName: 'Mainet fork -Remix VM (London)', displayName: 'Mainet fork -Remix VM (London)',
kind: 'provider', kind: 'provider',
description: 'Remix VM (London)', description: 'Remix VM (London)',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
}, blockchain) }, blockchain)
this.blockchain = blockchain this.blockchain = blockchain
this.fork = 'shanghai' this.fork = 'shanghai'
this.nodeUrl = 'https://mainnet.infura.io/v3/08b2a484451e4635a28b3d8234f24332' this.nodeUrl = 'https://mainnet.infura.io/v3/08b2a484451e4635a28b3d8234f24332'
this.blockNumber = 'latest' this.blockNumber = 'latest'
} }
async init () { async init () {
return { return {
'fork': this.fork, 'fork': this.fork,
'nodeUrl': this.nodeUrl, 'nodeUrl': this.nodeUrl,
'blockNumber': this.blockNumber 'blockNumber': this.blockNumber
}
} }
}
} }

@ -2,28 +2,28 @@ import * as packageJson from '../../../../../package.json'
import { BasicVMProvider } from './vm-provider' import { BasicVMProvider } from './vm-provider'
export class SepoliaForkVMProvider extends BasicVMProvider { export class SepoliaForkVMProvider extends BasicVMProvider {
nodeUrl: string nodeUrl: string
blockNumber: number | 'latest' blockNumber: number | 'latest'
constructor (blockchain) { constructor (blockchain) {
super({ super({
name: 'vm-sepolia-fork', name: 'vm-sepolia-fork',
displayName: 'Sepolia fork - Remix VM (London)', displayName: 'Sepolia fork - Remix VM (London)',
kind: 'provider', kind: 'provider',
description: 'Remix VM (London)', description: 'Remix VM (London)',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
}, blockchain) }, blockchain)
this.blockchain = blockchain this.blockchain = blockchain
this.fork = 'shanghai' this.fork = 'shanghai'
this.nodeUrl = 'https://remix-sepolia.ethdevops.io' this.nodeUrl = 'https://remix-sepolia.ethdevops.io'
this.blockNumber = 'latest' this.blockNumber = 'latest'
} }
async init () { async init () {
return { return {
'fork': this.fork, 'fork': this.fork,
'nodeUrl': this.nodeUrl, 'nodeUrl': this.nodeUrl,
'blockNumber': this.blockNumber 'blockNumber': this.blockNumber
}
} }
}
} }

@ -5,99 +5,99 @@ import { Plugin } from '@remixproject/engine'
import { IProvider } from './abstract-provider' import { IProvider } from './abstract-provider'
export class BasicVMProvider extends Plugin implements IProvider { export class BasicVMProvider extends Plugin implements IProvider {
blockchain blockchain
fork: string fork: string
options: { [id: string] : any } = {} options: { [id: string] : any } = {}
constructor (profile, blockchain) { constructor (profile, blockchain) {
super(profile) super(profile)
this.blockchain = blockchain this.blockchain = blockchain
this.fork = '' this.fork = ''
} }
async init (): Promise<{ [id: string] : any }> { return {} } async init (): Promise<{ [id: string] : any }> { return {} }
body (): JSX.Element { body (): JSX.Element {
return ( return (
<div></div> <div></div>
) )
} }
sendAsync (data: JsonDataRequest): Promise<any> { sendAsync (data: JsonDataRequest): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.sendAsyncInternal(data, resolve, reject) this.sendAsyncInternal(data, resolve, reject)
}) })
} }
private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> { private async sendAsyncInternal (data: JsonDataRequest, resolve: SuccessRequest, reject: RejectRequest): Promise<void> {
try { try {
await this.blockchain.providers.vm.provider.sendAsync(data, (error, result) => { await this.blockchain.providers.vm.provider.sendAsync(data, (error, result) => {
if (error) return reject(error) if (error) return reject(error)
else { else {
resolve({ jsonrpc: '2.0', result, id: data.id }) resolve({ jsonrpc: '2.0', result, id: data.id })
}
})
} catch (error) {
reject(error)
} }
})
} catch (error) {
reject(error)
} }
}
} }
export class MergeVMProvider extends BasicVMProvider { export class MergeVMProvider extends BasicVMProvider {
constructor (blockchain) { constructor (blockchain) {
super({ super({
name: 'vm-merge', name: 'vm-merge',
displayName: 'Remix VM (Merge)', displayName: 'Remix VM (Merge)',
kind: 'provider', kind: 'provider',
description: 'Remix VM (Merge)', description: 'Remix VM (Merge)',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
}, blockchain) }, blockchain)
this.blockchain = blockchain this.blockchain = blockchain
this.fork = 'merge' this.fork = 'merge'
} }
} }
export class LondonVMProvider extends BasicVMProvider { export class LondonVMProvider extends BasicVMProvider {
constructor (blockchain) { constructor (blockchain) {
super({ super({
name: 'vm-london', name: 'vm-london',
displayName: 'Remix VM (London)', displayName: 'Remix VM (London)',
kind: 'provider', kind: 'provider',
description: 'Remix VM (London)', description: 'Remix VM (London)',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
}, blockchain) }, blockchain)
this.blockchain = blockchain this.blockchain = blockchain
this.fork = 'london' this.fork = 'london'
} }
} }
export class BerlinVMProvider extends BasicVMProvider { export class BerlinVMProvider extends BasicVMProvider {
constructor (blockchain) { constructor (blockchain) {
super({ super({
name: 'vm-berlin', name: 'vm-berlin',
displayName: 'Remix VM (Berlin)', displayName: 'Remix VM (Berlin)',
kind: 'provider', kind: 'provider',
description: 'Remix VM (Berlin)', description: 'Remix VM (Berlin)',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
}, blockchain) }, blockchain)
this.blockchain = blockchain this.blockchain = blockchain
this.fork = 'berlin' this.fork = 'berlin'
} }
} }
export class ShanghaiVMProvider extends BasicVMProvider { export class ShanghaiVMProvider extends BasicVMProvider {
constructor (blockchain) { constructor (blockchain) {
super({ super({
name: 'vm-shanghai', name: 'vm-shanghai',
displayName: 'Remix VM (Shanghai)', displayName: 'Remix VM (Shanghai)',
kind: 'provider', kind: 'provider',
description: 'Remix VM (Shanghai)', description: 'Remix VM (Shanghai)',
methods: ['sendAsync', 'init'], methods: ['sendAsync', 'init'],
version: packageJson.version version: packageJson.version
}, blockchain) }, blockchain)
this.blockchain = blockchain this.blockchain = blockchain
this.fork = 'shanghai' this.fork = 'shanghai'
} }
} }

@ -4,35 +4,35 @@ type registryEntry = {
} }
export default class Registry { export default class Registry {
private static instance: Registry; private static instance: Registry;
private state: any private state: any
private constructor () { private constructor () {
this.state = {} this.state = {}
} }
public static getInstance (): Registry {
if (!Registry.instance) {
Registry.instance = new Registry()
}
return Registry.instance public static getInstance (): Registry {
if (!Registry.instance) {
Registry.instance = new Registry()
} }
public put (entry: registryEntry) { return Registry.instance
if (this.state[entry.name]) return this.state[entry.name] }
const server = {
// uid: serveruid,
api: entry.api
}
this.state[entry.name] = { server }
return server
}
public get (name: string) { public put (entry: registryEntry) {
const state = this.state[name] if (this.state[entry.name]) return this.state[entry.name]
if (!state) return const server = {
const server = state.server // uid: serveruid,
return server api: entry.api
} }
this.state[entry.name] = { server }
return server
}
public get (name: string) {
const state = this.state[name]
if (!state) return
const server = state.server
return server
}
} }

@ -9,91 +9,91 @@ import { PluginViewWrapper } from '@remix-ui/helper'
var EventManager = require('../../lib/events') var EventManager = require('../../lib/events')
const profile = { const profile = {
name: 'solidityStaticAnalysis', name: 'solidityStaticAnalysis',
displayName: 'Solidity Analyzers', displayName: 'Solidity Analyzers',
methods: [], methods: [],
events: [], events: [],
icon: 'assets/img/staticAnalysis.webp', icon: 'assets/img/staticAnalysis.webp',
description: 'Analyze your code using Remix, Solhint and Slither.', description: 'Analyze your code using Remix, Solhint and Slither.',
kind: 'analysis', kind: 'analysis',
location: 'sidePanel', location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/static_analysis.html', documentation: 'https://remix-ide.readthedocs.io/en/latest/static_analysis.html',
version: packageJson.version, version: packageJson.version,
maintainedBy: 'Remix' maintainedBy: 'Remix'
} }
class AnalysisTab extends ViewPlugin { class AnalysisTab extends ViewPlugin {
constructor () { constructor () {
super(profile) super(profile)
this.event = new EventManager() this.event = new EventManager()
this.events = new EventEmitter() this.events = new EventEmitter()
this.registry = Registry.getInstance() this.registry = Registry.getInstance()
this.element = document.createElement('div') this.element = document.createElement('div')
this.element.setAttribute('id', 'staticAnalyserView') this.element.setAttribute('id', 'staticAnalyserView')
this._components = {} this._components = {}
this._components.registry = this.registry this._components.registry = this.registry
this._deps = { this._deps = {
offsetToLineColumnConverter: this.registry.get( offsetToLineColumnConverter: this.registry.get(
'offsettolinecolumnconverter').api 'offsettolinecolumnconverter').api
}
this.dispatch = null
this.hints = []
this.basicEnabled = false
this.solhintEnabled = false
this.slitherEnabled = false
} }
this.dispatch = null
this.hints = []
this.basicEnabled = false
this.solhintEnabled = false
this.slitherEnabled = false
}
async onActivation () { async onActivation () {
this.renderComponent() this.renderComponent()
const isSolidityActive = await this.call('manager', 'isActive', 'solidity') const isSolidityActive = await this.call('manager', 'isActive', 'solidity')
if (!isSolidityActive) { if (!isSolidityActive) {
await this.call('manager', 'activatePlugin', 'solidity') await this.call('manager', 'activatePlugin', 'solidity')
} }
this.event.register('staticAnaysisWarning', (count) => { this.event.register('staticAnaysisWarning', (count) => {
let payloadType = '' let payloadType = ''
const error = this.hints?.find(hint => hint.type === 'error') const error = this.hints?.find(hint => hint.type === 'error')
if (error && this.solhintEnabled) { if (error && this.solhintEnabled) {
payloadType = 'error' payloadType = 'error'
} else { } else {
payloadType = 'warning' payloadType = 'warning'
} }
if (count > 0) { if (count > 0) {
this.emit('statusChanged', { key: count, title: payloadType === 'error' ? `You have ${count} problem${count === 1 ? '' : 's'}` : `You have ${count} warnings`, type: payloadType }) this.emit('statusChanged', { key: count, title: payloadType === 'error' ? `You have ${count} problem${count === 1 ? '' : 's'}` : `You have ${count} warnings`, type: payloadType })
} else if (count === 0) { } else if (count === 0) {
this.emit('statusChanged', { key: 'succeed', title: 'no warnings or errors', type: 'success' }) this.emit('statusChanged', { key: 'succeed', title: 'no warnings or errors', type: 'success' })
} else { } else {
// count ==-1 no compilation result // count ==-1 no compilation result
this.emit('statusChanged', { key: 'none' }) this.emit('statusChanged', { key: 'none' })
} }
}) })
} }
setDispatch (dispatch) { setDispatch (dispatch) {
this.dispatch = dispatch this.dispatch = dispatch
this.renderComponent() this.renderComponent()
} }
render () { render () {
return <div id='staticAnalyserView'><PluginViewWrapper plugin={this} /></div> return <div id='staticAnalyserView'><PluginViewWrapper plugin={this} /></div>
} }
updateComponent(state) { updateComponent(state) {
return <RemixUiStaticAnalyser return <RemixUiStaticAnalyser
registry={state.registry} registry={state.registry}
analysisModule={state.analysisModule} analysisModule={state.analysisModule}
event={state.event} event={state.event}
/> />
} }
renderComponent () { renderComponent () {
this.dispatch && this.dispatch({ this.dispatch && this.dispatch({
registry: this.registry, registry: this.registry,
analysisModule: this, analysisModule: this,
event: this.event event: this.event
}) })
} }
} }
module.exports = AnalysisTab module.exports = AnalysisTab

@ -8,89 +8,89 @@ declare global {
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
export const profile = { export const profile = {
name: 'compileAndRun', name: 'compileAndRun',
displayName: 'Compile and Run', displayName: 'Compile and Run',
description: 'After each compilation, run the script defined in Natspec.', description: 'After each compilation, run the script defined in Natspec.',
methods: ['runScriptAfterCompilation'], methods: ['runScriptAfterCompilation'],
version: packageJson.version, version: packageJson.version,
kind: 'none' kind: 'none'
} }
type listener = (event: KeyboardEvent) => void type listener = (event: KeyboardEvent) => void
export class CompileAndRun extends Plugin { export class CompileAndRun extends Plugin {
executionListener: listener executionListener: listener
targetFileName: string targetFileName: string
constructor () { constructor () {
super(profile) super(profile)
this.executionListener = async (e) => { this.executionListener = async (e) => {
// ctrl+e or command+e // ctrl+e or command+e
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.keyCode === 83) { if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.keyCode === 83) {
const file = await this.call('fileManager', 'file') const file = await this.call('fileManager', 'file')
if (file) { if (file) {
if (file.endsWith('.sol')) { if (file.endsWith('.sol')) {
e.preventDefault() e.preventDefault()
this.targetFileName = file this.targetFileName = file
await this.call('solidity', 'compile', file) await this.call('solidity', 'compile', file)
_paq.push(['trackEvent', 'ScriptExecutor', 'CompileAndRun', 'compile_solidity']) _paq.push(['trackEvent', 'ScriptExecutor', 'CompileAndRun', 'compile_solidity'])
} else if (file.endsWith('.js') || file.endsWith('.ts')) { } else if (file.endsWith('.js') || file.endsWith('.ts')) {
e.preventDefault() e.preventDefault()
this.runScript(file, false) this.runScript(file, false)
_paq.push(['trackEvent', 'ScriptExecutor', 'CompileAndRun', 'run_script']) _paq.push(['trackEvent', 'ScriptExecutor', 'CompileAndRun', 'run_script'])
} }
}
}
} }
}
} }
}
runScriptAfterCompilation (fileName: string) { runScriptAfterCompilation (fileName: string) {
this.targetFileName = fileName this.targetFileName = fileName
_paq.push(['trackEvent', 'ScriptExecutor', 'CompileAndRun', 'request_run_script']) _paq.push(['trackEvent', 'ScriptExecutor', 'CompileAndRun', 'request_run_script'])
} }
async runScript (fileName, clearAllInstances) { async runScript (fileName, clearAllInstances) {
await this.call('terminal', 'log', { value: `running ${fileName} ...`, type: 'info' }) await this.call('terminal', 'log', { value: `running ${fileName} ...`, type: 'info' })
try { try {
const exists = await this.call('fileManager', 'exists', fileName) const exists = await this.call('fileManager', 'exists', fileName)
if (!exists) { if (!exists) {
await this.call('terminal', 'log', { value: `${fileName} does not exist.`, type: 'info' } ) await this.call('terminal', 'log', { value: `${fileName} does not exist.`, type: 'info' } )
return return
} }
const content = await this.call('fileManager', 'readFile', fileName) const content = await this.call('fileManager', 'readFile', fileName)
if (clearAllInstances) { if (clearAllInstances) {
await this.call('udapp', 'clearAllInstances') await this.call('udapp', 'clearAllInstances')
} }
await this.call('scriptRunner', 'execute', content, fileName) await this.call('scriptRunner', 'execute', content, fileName)
} catch (e) { } catch (e) {
this.call('notification', 'toast', e.message || e) this.call('notification', 'toast', e.message || e)
} }
} }
onActivation () { onActivation () {
window.document.addEventListener('keydown', this.executionListener) window.document.addEventListener('keydown', this.executionListener)
this.on('compilerMetadata', 'artefactsUpdated', async (fileName, contract) => { this.on('compilerMetadata', 'artefactsUpdated', async (fileName, contract) => {
if (this.targetFileName === contract.file) { if (this.targetFileName === contract.file) {
this.targetFileName = null this.targetFileName = null
if (contract.object && contract.object.devdoc['custom:dev-run-script']) { if (contract.object && contract.object.devdoc['custom:dev-run-script']) {
const file = contract.object.devdoc['custom:dev-run-script'] const file = contract.object.devdoc['custom:dev-run-script']
if (file) { if (file) {
this.runScript(file, true) this.runScript(file, true)
_paq.push(['trackEvent', 'ScriptExecutor', 'CompileAndRun', 'run_script_after_compile']) _paq.push(['trackEvent', 'ScriptExecutor', 'CompileAndRun', 'run_script_after_compile'])
} else { } else {
this.call('notification', 'toast', 'You have not set a script to run. Set it with @custom:dev-run-script NatSpec tag.') this.call('notification', 'toast', 'You have not set a script to run. Set it with @custom:dev-run-script NatSpec tag.')
} }
} else { } else {
this.call('notification', 'toast', 'You have not set a script to run. Set it with @custom:dev-run-script NatSpec tag.') this.call('notification', 'toast', 'You have not set a script to run. Set it with @custom:dev-run-script NatSpec tag.')
} }
} }
}) })
} }
onDeactivation () { onDeactivation () {
window.document.removeEventListener('keydown', this.executionListener) window.document.removeEventListener('keydown', this.executionListener)
this.off('compilerMetadata', 'artefactsUpdated') this.off('compilerMetadata', 'artefactsUpdated')
} }
} }

@ -11,17 +11,17 @@ import { compilerConfigChangedToastMsg, compileToastMsg } from '@remix-ui/helper
import { isNative } from '../../remixAppManager' import { isNative } from '../../remixAppManager'
const profile = { const profile = {
name: 'solidity', name: 'solidity',
displayName: 'Solidity compiler', displayName: 'Solidity compiler',
icon: 'assets/img/solidity.webp', icon: 'assets/img/solidity.webp',
description: 'Compile solidity contracts', description: 'Compile solidity contracts',
kind: 'compiler', kind: 'compiler',
permission: true, permission: true,
location: 'sidePanel', location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/compile.html', documentation: 'https://remix-ide.readthedocs.io/en/latest/compile.html',
version: packageJson.version, version: packageJson.version,
maintainedBy: 'Remix', maintainedBy: 'Remix',
methods: ['getCompilationResult', 'compile', 'compileWithParameters', 'setCompilerConfig', 'compileFile', 'getCompilerState', 'getCompilerParameters', 'getCompiler'] methods: ['getCompilationResult', 'compile', 'compileWithParameters', 'setCompilerConfig', 'compileFile', 'getCompilerState', 'getCompilerParameters', 'getCompiler']
} }
// EditorApi: // EditorApi:
@ -29,134 +29,134 @@ const profile = {
// - methods: ['getCompilationResult'] // - methods: ['getCompilationResult']
class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerApi class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerApi
constructor (config, fileManager) { constructor (config, fileManager) {
super(profile) super(profile)
this.fileManager = fileManager this.fileManager = fileManager
this.config = config this.config = config
this.queryParams = new QueryParams() this.queryParams = new QueryParams()
this.compileTabLogic = new CompileTabLogic(this, this.contentImport) this.compileTabLogic = new CompileTabLogic(this, this.contentImport)
this.compiler = this.compileTabLogic.compiler this.compiler = this.compileTabLogic.compiler
this.compileTabLogic.init() this.compileTabLogic.init()
this.initCompilerApi() this.initCompilerApi()
this.el = document.createElement('div') this.el = document.createElement('div')
this.el.setAttribute('id', 'compileTabView') this.el.setAttribute('id', 'compileTabView')
} }
renderComponent () { renderComponent () {
// empty method, is a state update needed? // empty method, is a state update needed?
} }
onCurrentFileChanged () { onCurrentFileChanged () {
this.renderComponent() this.renderComponent()
} }
// onResetResults () { // onResetResults () {
// this.renderComponent() // this.renderComponent()
// } // }
onSetWorkspace () { onSetWorkspace () {
this.renderComponent() this.renderComponent()
} }
onFileRemoved () { onFileRemoved () {
this.renderComponent() this.renderComponent()
} }
onNoFileSelected () { onNoFileSelected () {
this.renderComponent() this.renderComponent()
} }
onFileClosed () { onFileClosed () {
this.renderComponent() this.renderComponent()
} }
onCompilationFinished () { onCompilationFinished () {
this.renderComponent() this.renderComponent()
} }
render () { render () {
return <div id='compileTabView'><SolidityCompiler api={this}/></div> return <div id='compileTabView'><SolidityCompiler api={this}/></div>
} }
async compileWithParameters (compilationTargets, settings) { async compileWithParameters (compilationTargets, settings) {
return await super.compileWithParameters(compilationTargets, settings) return await super.compileWithParameters(compilationTargets, settings)
} }
getCompilationResult () { getCompilationResult () {
return super.getCompilationResult() return super.getCompilationResult()
} }
getFileManagerMode () { getFileManagerMode () {
return this.fileManager.mode return this.fileManager.mode
} }
/** /**
* set the compiler configuration * set the compiler configuration
* This function is used by remix-plugin compiler API. * This function is used by remix-plugin compiler API.
* @param {object} settings {evmVersion, optimize, runs, version, language} * @param {object} settings {evmVersion, optimize, runs, version, language}
*/ */
setCompilerConfig (settings) { setCompilerConfig (settings) {
super.setCompilerConfig(settings) super.setCompilerConfig(settings)
this.renderComponent() this.renderComponent()
// @todo(#2875) should use loading compiler return value to check whether the compiler is loaded instead of "setInterval" // @todo(#2875) should use loading compiler return value to check whether the compiler is loaded instead of "setInterval"
const value = JSON.stringify(settings, null, '\t') const value = JSON.stringify(settings, null, '\t')
this.call('notification', 'toast', compilerConfigChangedToastMsg(this.currentRequest.from, value)) this.call('notification', 'toast', compilerConfigChangedToastMsg(this.currentRequest.from, value))
} }
compile (fileName) { compile (fileName) {
if (!isNative(this.currentRequest.from)) this.call('notification', 'toast', compileToastMsg(this.currentRequest.from, fileName)) if (!isNative(this.currentRequest.from)) this.call('notification', 'toast', compileToastMsg(this.currentRequest.from, fileName))
super.compile(fileName) super.compile(fileName)
} }
compileFile (event) { compileFile (event) {
return super.compileFile(event) return super.compileFile(event)
} }
async onActivation () { async onActivation () {
super.onActivation() super.onActivation()
this.on('filePanel', 'workspaceInitializationCompleted', () => { this.on('filePanel', 'workspaceInitializationCompleted', () => {
this.call('filePanel', 'registerContextMenuItem', { this.call('filePanel', 'registerContextMenuItem', {
id: 'solidity', id: 'solidity',
name: 'compileFile', name: 'compileFile',
label: 'Compile', label: 'Compile',
type: [], type: [],
extension: ['.sol'], extension: ['.sol'],
path: [], path: [],
pattern: [], pattern: [],
group: 6 group: 6
}) })
}) })
try { try {
this.currentFile = await this.call('fileManager', 'file') this.currentFile = await this.call('fileManager', 'file')
} catch (error) { } catch (error) {
if (error.message !== 'Error: No such file or directory No file selected') throw error if (error.message !== 'Error: No such file or directory No file selected') throw error
} }
} }
getCompiler () { getCompiler () {
return this.compileTabLogic.compiler return this.compileTabLogic.compiler
} }
getCompilerParameters () { getCompilerParameters () {
const params = this.queryParams.get() const params = this.queryParams.get()
params.evmVersion = params.evmVersion === 'null' || params.evmVersion === 'undefined' ? null : params.evmVersion params.evmVersion = params.evmVersion === 'null' || params.evmVersion === 'undefined' ? null : params.evmVersion
params.optimize = (params.optimize === 'false' || params.optimize === null || params.optimize === undefined) ? false : params.optimize params.optimize = (params.optimize === 'false' || params.optimize === null || params.optimize === undefined) ? false : params.optimize
params.optimize = params.optimize === 'true' ? true : params.optimize params.optimize = params.optimize === 'true' ? true : params.optimize
return params return params
} }
setCompilerParameters (params) { setCompilerParameters (params) {
this.queryParams.update(params) this.queryParams.update(params)
} }
async getAppParameter (name) { async getAppParameter (name) {
return await this.call('config', 'getAppParameter', name) return await this.call('config', 'getAppParameter', name)
} }
async setAppParameter (name, value) { async setAppParameter (name, value) {
await this.call('config', 'setAppParameter', name, value) await this.call('config', 'setAppParameter', name, value)
} }
} }
module.exports = CompileTab module.exports = CompileTab

@ -9,106 +9,106 @@ import { compilationFinishedToastMsg, compilingToastMsg, localCompilationToastMs
const css = require('./styles/debugger-tab-styles') const css = require('./styles/debugger-tab-styles')
const profile = { const profile = {
name: 'debugger', name: 'debugger',
displayName: 'Debugger', displayName: 'Debugger',
methods: ['debug', 'getTrace', 'decodeLocalVariable', 'decodeStateVariable', 'globalContext'], methods: ['debug', 'getTrace', 'decodeLocalVariable', 'decodeStateVariable', 'globalContext'],
events: [], events: [],
icon: 'assets/img/debuggerLogo.webp', icon: 'assets/img/debuggerLogo.webp',
description: 'Debug transactions', description: 'Debug transactions',
kind: 'debugging', kind: 'debugging',
location: 'sidePanel', location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/debugger.html', documentation: 'https://remix-ide.readthedocs.io/en/latest/debugger.html',
version: packageJson.version, version: packageJson.version,
maintainedBy: 'Remix' maintainedBy: 'Remix'
} }
export class DebuggerTab extends DebuggerApiMixin(ViewPlugin) { export class DebuggerTab extends DebuggerApiMixin(ViewPlugin) {
constructor () { constructor () {
super(profile) super(profile)
this.el = document.createElement('div') this.el = document.createElement('div')
this.el.setAttribute('id', 'debugView') this.el.setAttribute('id', 'debugView')
this.el.classList.add(css.debuggerTabView) this.el.classList.add(css.debuggerTabView)
this.initDebuggerApi() this.initDebuggerApi()
} }
render () { render () {
this.on('fetchAndCompile', 'compiling', (settings) => { this.on('fetchAndCompile', 'compiling', (settings) => {
settings = JSON.stringify(settings, null, '\t') settings = JSON.stringify(settings, null, '\t')
this.call('notification', 'toast', compilingToastMsg(settings)) this.call('notification', 'toast', compilingToastMsg(settings))
}) })
this.on('fetchAndCompile', 'compilationFailed', (data) => { this.on('fetchAndCompile', 'compilationFailed', (data) => {
this.call('notification', 'toast', compilationFinishedToastMsg()) this.call('notification', 'toast', compilationFinishedToastMsg())
}) })
this.on('fetchAndCompile', 'notFound', (contractAddress) => { this.on('fetchAndCompile', 'notFound', (contractAddress) => {
this.call('notification', 'toast', notFoundToastMsg(contractAddress)) this.call('notification', 'toast', notFoundToastMsg(contractAddress))
}) })
this.on('fetchAndCompile', 'sourceVerificationNotAvailable', () => { this.on('fetchAndCompile', 'sourceVerificationNotAvailable', () => {
this.call('notification', 'toast', sourceVerificationNotAvailableToastMsg()) this.call('notification', 'toast', sourceVerificationNotAvailableToastMsg())
}) })
const onReady = (api) => { this.api = api } const onReady = (api) => { this.api = api }
return <div className="overflow-hidden px-1" id='debugView'><DebuggerUI debuggerAPI={this} onReady={onReady} /></div> return <div className="overflow-hidden px-1" id='debugView'><DebuggerUI debuggerAPI={this} onReady={onReady} /></div>
} }
showMessage (title, message) { showMessage (title, message) {
try { try {
this.call('notification', 'alert', { this.call('notification', 'alert', {
id: 'debuggerTabShowMessage', id: 'debuggerTabShowMessage',
title, title,
message: bleach.sanitize(message) message: bleach.sanitize(message)
}) })
} catch (e) { } catch (e) {
console.log(e) console.log(e)
}
} }
}
async decodeLocalVariable (variableId) { async decodeLocalVariable (variableId) {
if (!this.debuggerBackend) return null if (!this.debuggerBackend) return null
return await this.debuggerBackend.debugger.decodeLocalVariableByIdAtCurrentStep(this.debuggerBackend.step_manager.currentStepIndex, variableId) return await this.debuggerBackend.debugger.decodeLocalVariableByIdAtCurrentStep(this.debuggerBackend.step_manager.currentStepIndex, variableId)
} }
async decodeStateVariable (variableId) { async decodeStateVariable (variableId) {
if (!this.debuggerBackend) return null if (!this.debuggerBackend) return null
return await this.debuggerBackend.debugger.decodeStateVariableByIdAtCurrentStep(this.debuggerBackend.step_manager.currentStepIndex, variableId) return await this.debuggerBackend.debugger.decodeStateVariableByIdAtCurrentStep(this.debuggerBackend.step_manager.currentStepIndex, variableId)
} }
async globalContext () { async globalContext () {
if (this.api?.globalContext) { if (this.api?.globalContext) {
const { tx, block } = await this.api.globalContext() const { tx, block } = await this.api.globalContext()
const blockContext = { const blockContext = {
'chainid': tx.chainId, 'chainid': tx.chainId,
'coinbase': block.miner, 'coinbase': block.miner,
'difficulty': block.difficulty, 'difficulty': block.difficulty,
'gaslimit': block.gasLimit, 'gaslimit': block.gasLimit,
'number': block.number, 'number': block.number,
'timestamp': block.timestamp, 'timestamp': block.timestamp,
} }
if (block.baseFeePerGas) { if (block.baseFeePerGas) {
blockContext['basefee'] = Web3.utils.toBN(block.baseFeePerGas).toString(10) + ` Wei (${block.baseFeePerGas})` blockContext['basefee'] = Web3.utils.toBN(block.baseFeePerGas).toString(10) + ` Wei (${block.baseFeePerGas})`
} }
const msg = { const msg = {
'sender': tx.from, 'sender': tx.from,
'sig': tx.input.substring(0, 10), 'sig': tx.input.substring(0, 10),
'value': tx.value + ' Wei' 'value': tx.value + ' Wei'
} }
const txOrigin = { const txOrigin = {
'origin': tx.from 'origin': tx.from
} }
return { return {
block: blockContext, block: blockContext,
msg, msg,
tx: txOrigin tx: txOrigin
} }
} else { } else {
return { return {
block: null, block: null,
msg: null, msg: null,
tx: null tx: null
} }
}
} }
}
} }

@ -8,71 +8,71 @@ import zhJson from './locales/zh'
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const locales = [ const locales = [
{ code: 'en', name: 'English', localeName: 'English', messages: enJson }, { code: 'en', name: 'English', localeName: 'English', messages: enJson },
{ code: 'zh', name: 'Chinese Simplified', localeName: '简体中文', messages: zhJson }, { code: 'zh', name: 'Chinese Simplified', localeName: '简体中文', messages: zhJson },
] ]
const profile = { const profile = {
name: 'locale', name: 'locale',
events: ['localeChanged'], events: ['localeChanged'],
methods: ['switchLocale', 'getLocales', 'currentLocale'], methods: ['switchLocale', 'getLocales', 'currentLocale'],
version: packageJson.version, version: packageJson.version,
kind: 'locale' kind: 'locale'
} }
export class LocaleModule extends Plugin { export class LocaleModule extends Plugin {
constructor () { constructor () {
super(profile) super(profile)
this.events = new EventEmitter() this.events = new EventEmitter()
this._deps = { this._deps = {
config: Registry.getInstance().get('config') && Registry.getInstance().get('config').api config: Registry.getInstance().get('config') && Registry.getInstance().get('config').api
}
this.locales = {}
locales.map((locale) => {
this.locales[locale.code.toLocaleLowerCase()] = locale
})
this._paq = _paq
this.queryParams = new QueryParams()
let queryLocale = this.queryParams.get().lang
queryLocale = queryLocale && queryLocale.toLocaleLowerCase()
queryLocale = this.locales[queryLocale] ? queryLocale : null
let currentLocale = (this._deps.config && this._deps.config.get('settings/locale')) || null
currentLocale = currentLocale && currentLocale.toLocaleLowerCase()
currentLocale = this.locales[currentLocale] ? currentLocale : null
this.currentLocaleState = { queryLocale, currentLocale }
this.active = queryLocale || currentLocale || 'en'
this.forced = !!queryLocale
this.queryParams.update({ lang: this.active })
} }
this.locales = {}
locales.map((locale) => {
this.locales[locale.code.toLocaleLowerCase()] = locale
})
this._paq = _paq
this.queryParams = new QueryParams()
let queryLocale = this.queryParams.get().lang
queryLocale = queryLocale && queryLocale.toLocaleLowerCase()
queryLocale = this.locales[queryLocale] ? queryLocale : null
let currentLocale = (this._deps.config && this._deps.config.get('settings/locale')) || null
currentLocale = currentLocale && currentLocale.toLocaleLowerCase()
currentLocale = this.locales[currentLocale] ? currentLocale : null
this.currentLocaleState = { queryLocale, currentLocale }
this.active = queryLocale || currentLocale || 'en'
this.forced = !!queryLocale
this.queryParams.update({ lang: this.active })
}
/** Return the active locale */ /** Return the active locale */
currentLocale () { currentLocale () {
return this.locales[this.active] return this.locales[this.active]
} }
/** Returns all locales as an array */ /** Returns all locales as an array */
getLocales () { getLocales () {
return Object.keys(this.locales).map(key => this.locales[key]) return Object.keys(this.locales).map(key => this.locales[key])
} }
/** /**
* Change the current locale * Change the current locale
* @param {string} [localeCode] - The code of the locale * @param {string} [localeCode] - The code of the locale
*/ */
switchLocale (localeCode) { switchLocale (localeCode) {
localeCode = localeCode && localeCode.toLocaleLowerCase() localeCode = localeCode && localeCode.toLocaleLowerCase()
if (localeCode && !Object.keys(this.locales).includes(localeCode)) { if (localeCode && !Object.keys(this.locales).includes(localeCode)) {
throw new Error(`Locale ${localeCode} doesn't exist`) throw new Error(`Locale ${localeCode} doesn't exist`)
}
const next = localeCode || this.active // Name
if (next === this.active) return // --> exit out of this method
_paq.push(['trackEvent', 'localeModule', 'switchTo', next])
const nextLocale = this.locales[next] // Locale
if (!this.forced) this._deps.config.set('settings/locale', next)
if (localeCode) this.active = localeCode
this.queryParams.update({ lang: localeCode })
this.emit('localeChanged', nextLocale)
this.events.emit('localeChanged', nextLocale)
} }
const next = localeCode || this.active // Name
if (next === this.active) return // --> exit out of this method
_paq.push(['trackEvent', 'localeModule', 'switchTo', next])
const nextLocale = this.locales[next] // Locale
if (!this.forced) this._deps.config.set('settings/locale', next)
if (localeCode) this.active = localeCode
this.queryParams.update({ lang: localeCode })
this.emit('localeChanged', nextLocale)
this.events.emit('localeChanged', nextLocale)
}
} }

@ -12,16 +12,16 @@ import solidityUnitTestingJson from './solidityUnitTesting.json';
import permissionHandlerJson from './permissionHandler.json'; import permissionHandlerJson from './permissionHandler.json';
export default { export default {
...debuggerJson, ...debuggerJson,
...filePanelJson, ...filePanelJson,
...homeJson, ...homeJson,
...panelJson, ...panelJson,
...pluginManagerJson, ...pluginManagerJson,
...searchJson, ...searchJson,
...settingsJson, ...settingsJson,
...solidityJson, ...solidityJson,
...terminalJson, ...terminalJson,
...udappJson, ...udappJson,
...solidityUnitTestingJson, ...solidityUnitTestingJson,
...permissionHandlerJson, ...permissionHandlerJson,
} }

@ -15,16 +15,16 @@ import enJson from '../en';
// There may have some un-translated content. Always fill in the gaps with EN JSON. // There may have some un-translated content. Always fill in the gaps with EN JSON.
// No need for a defaultMessage prop when render a FormattedMessage component. // No need for a defaultMessage prop when render a FormattedMessage component.
export default Object.assign({}, enJson, { export default Object.assign({}, enJson, {
...debuggerJson, ...debuggerJson,
...filePanelJson, ...filePanelJson,
...homeJson, ...homeJson,
...panelJson, ...panelJson,
...pluginManagerJson, ...pluginManagerJson,
...searchJson, ...searchJson,
...settingsJson, ...settingsJson,
...solidityJson, ...solidityJson,
...terminalJson, ...terminalJson,
...udappJson, ...udappJson,
...solidityUnitTestingJson, ...solidityUnitTestingJson,
...permissionHandlerJson, ...permissionHandlerJson,
}) })

@ -3,11 +3,11 @@ import * as packageJson from '../../../../../package.json'
import { Web3 } from 'web3' import { Web3 } from 'web3'
export const profile = { export const profile = {
name: 'network', name: 'network',
description: 'Manage the network (mainnet, ropsten, goerli...) and the provider (web3, vm, injected)', description: 'Manage the network (mainnet, ropsten, goerli...) and the provider (web3, vm, injected)',
methods: ['getNetworkProvider', 'getEndpoint', 'detectNetwork', 'addNetwork', 'removeNetwork'], methods: ['getNetworkProvider', 'getEndpoint', 'detectNetwork', 'addNetwork', 'removeNetwork'],
version: packageJson.version, version: packageJson.version,
kind: 'network' kind: 'network'
} }
// Network API has : // Network API has :
@ -15,46 +15,46 @@ export const profile = {
// - methods: ['getNetworkProvider', 'getEndpoint', 'detectNetwork', 'addNetwork', 'removeNetwork'] // - methods: ['getNetworkProvider', 'getEndpoint', 'detectNetwork', 'addNetwork', 'removeNetwork']
export class NetworkModule extends Plugin { export class NetworkModule extends Plugin {
constructor (blockchain) { constructor (blockchain) {
super(profile) super(profile)
this.blockchain = blockchain this.blockchain = blockchain
// TODO: See with remix-lib to make sementic coherent // TODO: See with remix-lib to make sementic coherent
this.blockchain.event.register('contextChanged', (provider) => { this.blockchain.event.register('contextChanged', (provider) => {
this.emit('providerChanged', provider) this.emit('providerChanged', provider)
}) })
} }
/** Return the current network provider (web3, vm, injected) */ /** Return the current network provider (web3, vm, injected) */
getNetworkProvider () { getNetworkProvider () {
return this.blockchain.getProvider() return this.blockchain.getProvider()
} }
/** Return the current network */ /** Return the current network */
detectNetwork () { detectNetwork () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.blockchain.detectNetwork((error, network) => { this.blockchain.detectNetwork((error, network) => {
error ? reject(error) : resolve(network) error ? reject(error) : resolve(network)
}) })
}) })
} }
/** Return the url only if network provider is 'web3' */ /** Return the url only if network provider is 'web3' */
getEndpoint () { getEndpoint () {
const provider = this.blockchain.getProvider() const provider = this.blockchain.getProvider()
if (provider !== 'web3') { if (provider !== 'web3') {
throw new Error('no endpoint: current provider is either injected or vm') throw new Error('no endpoint: current provider is either injected or vm')
}
return this.blockchain.web3().currentProvider.host
}
/** Add a custom network to the list of available networks */
addNetwork (network) { // { name, url }
const provider = network.url === 'ipc' ? new Web3.providers.IpcProvider() : new Web3.providers.HttpProvider(network.url)
this.blockchain.addProvider({ name: network.name, provider })
}
/** Remove a network to the list of availble networks */
removeNetwork (name) {
this.blockchain.removeProvider(name)
} }
return this.blockchain.web3().currentProvider.host
}
/** Add a custom network to the list of available networks */
addNetwork (network) { // { name, url }
const provider = network.url === 'ipc' ? new Web3.providers.IpcProvider() : new Web3.providers.HttpProvider(network.url)
this.blockchain.addProvider({ name: network.name, provider })
}
/** Remove a network to the list of availble networks */
removeNetwork (name) {
this.blockchain.removeProvider(name)
}
} }

@ -12,110 +12,110 @@ import { addressToString } from '@remix-ui/helper'
const _paq = window._paq = window._paq || [] //eslint-disable-line const _paq = window._paq = window._paq || [] //eslint-disable-line
const profile = { const profile = {
name: 'recorder', name: 'recorder',
displayName: 'Recorder', displayName: 'Recorder',
description: 'Records transactions to save and run', description: 'Records transactions to save and run',
version: packageJson.version, version: packageJson.version,
methods: [ ] methods: [ ]
} }
/** /**
* Record transaction as long as the user create them. * Record transaction as long as the user create them.
*/ */
class Recorder extends Plugin { class Recorder extends Plugin {
constructor (blockchain) { constructor (blockchain) {
super(profile) super(profile)
this.event = new EventManager() this.event = new EventManager()
this.blockchain = blockchain this.blockchain = blockchain
this.data = { _listen: true, _replay: false, journal: [], _createdContracts: {}, _createdContractsReverse: {}, _usedAccounts: {}, _abis: {}, _contractABIReferences: {}, _linkReferences: {} } this.data = { _listen: true, _replay: false, journal: [], _createdContracts: {}, _createdContractsReverse: {}, _usedAccounts: {}, _abis: {}, _contractABIReferences: {}, _linkReferences: {} }
this.blockchain.event.register('initiatingTransaction', (timestamp, tx, payLoad) => { this.blockchain.event.register('initiatingTransaction', (timestamp, tx, payLoad) => {
if (tx.useCall) return if (tx.useCall) return
var { from, to, value } = tx var { from, to, value } = tx
// convert to and from to tokens // convert to and from to tokens
if (this.data._listen) { if (this.data._listen) {
var record = { var record = {
value, value,
inputs: txHelper.serializeInputs(payLoad.funAbi), inputs: txHelper.serializeInputs(payLoad.funAbi),
parameters: payLoad.funArgs, parameters: payLoad.funArgs,
name: payLoad.funAbi.name, name: payLoad.funAbi.name,
type: payLoad.funAbi.type type: payLoad.funAbi.type
} }
if (!to) { if (!to) {
var abi = payLoad.contractABI var abi = payLoad.contractABI
var keccak = bufferToHex(hash.keccakFromString(JSON.stringify(abi))) var keccak = bufferToHex(hash.keccakFromString(JSON.stringify(abi)))
record.abi = keccak record.abi = keccak
record.contractName = payLoad.contractName record.contractName = payLoad.contractName
record.bytecode = payLoad.contractBytecode record.bytecode = payLoad.contractBytecode
record.linkReferences = payLoad.linkReferences record.linkReferences = payLoad.linkReferences
if (record.linkReferences && Object.keys(record.linkReferences).length) { if (record.linkReferences && Object.keys(record.linkReferences).length) {
for (var file in record.linkReferences) { for (var file in record.linkReferences) {
for (var lib in record.linkReferences[file]) { for (var lib in record.linkReferences[file]) {
this.data._linkReferences[lib] = '<address>' this.data._linkReferences[lib] = '<address>'
} }
} }
} }
this.data._abis[keccak] = abi this.data._abis[keccak] = abi
this.data._contractABIReferences[timestamp] = keccak this.data._contractABIReferences[timestamp] = keccak
} else { } else {
var creationTimestamp = this.data._createdContracts[to] var creationTimestamp = this.data._createdContracts[to]
record.to = `created{${creationTimestamp}}` record.to = `created{${creationTimestamp}}`
record.abi = this.data._contractABIReferences[creationTimestamp] record.abi = this.data._contractABIReferences[creationTimestamp]
} }
for (var p in record.parameters) { for (var p in record.parameters) {
var thisarg = record.parameters[p] var thisarg = record.parameters[p]
var thistimestamp = this.data._createdContracts[thisarg] var thistimestamp = this.data._createdContracts[thisarg]
if (thistimestamp) record.parameters[p] = `created{${thistimestamp}}` if (thistimestamp) record.parameters[p] = `created{${thistimestamp}}`
} }
this.blockchain.getAccounts((error, accounts) => { this.blockchain.getAccounts((error, accounts) => {
if (error) return console.log(error) if (error) return console.log(error)
record.from = `account{${accounts.indexOf(from)}}` record.from = `account{${accounts.indexOf(from)}}`
this.data._usedAccounts[record.from] = from this.data._usedAccounts[record.from] = from
this.append(timestamp, record) this.append(timestamp, record)
})
}
}) })
}
})
this.blockchain.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp, _payload) => { this.blockchain.event.register('transactionExecuted', (error, from, to, data, call, txResult, timestamp, _payload) => {
if (error) return console.log(error) if (error) return console.log(error)
if (call) return if (call) return
const rawAddress = txResult.receipt.contractAddress const rawAddress = txResult.receipt.contractAddress
if (!rawAddress) return // not a contract creation if (!rawAddress) return // not a contract creation
const address = addressToString(rawAddress) const address = addressToString(rawAddress)
// save back created addresses for the convertion from tokens to real adresses // save back created addresses for the convertion from tokens to real adresses
this.data._createdContracts[address] = timestamp this.data._createdContracts[address] = timestamp
this.data._createdContractsReverse[timestamp] = address this.data._createdContractsReverse[timestamp] = address
}) })
this.blockchain.event.register('contextChanged', this.clearAll.bind(this)) this.blockchain.event.register('contextChanged', this.clearAll.bind(this))
this.event.register('newTxRecorded', (count) => { this.event.register('newTxRecorded', (count) => {
this.event.trigger('recorderCountChange', [count]) this.event.trigger('recorderCountChange', [count])
}) })
this.event.register('cleared', () => { this.event.register('cleared', () => {
this.event.trigger('recorderCountChange', [0]) this.event.trigger('recorderCountChange', [0])
}) })
} }
/** /**
* stop/start saving txs. If not listenning, is basically in replay mode * stop/start saving txs. If not listenning, is basically in replay mode
* *
* @param {Bool} listen * @param {Bool} listen
*/ */
setListen (listen) { setListen (listen) {
this.data._listen = listen this.data._listen = listen
this.data._replay = !listen this.data._replay = !listen
} }
extractTimestamp (value) { extractTimestamp (value) {
var stamp = /created{(.*)}/g.exec(value) var stamp = /created{(.*)}/g.exec(value)
if (stamp) { if (stamp) {
return stamp[1] return stamp[1]
}
return null
} }
return null
}
/** /**
* convert back from/to from tokens to real addresses * convert back from/to from tokens to real addresses
* *
* @param {Object} record * @param {Object} record
@ -123,66 +123,66 @@ class Recorder extends Plugin {
* @param {Object} options * @param {Object} options
* *
*/ */
resolveAddress (record, accounts, options) { resolveAddress (record, accounts, options) {
if (record.to) { if (record.to) {
var stamp = this.extractTimestamp(record.to) var stamp = this.extractTimestamp(record.to)
if (stamp) { if (stamp) {
record.to = this.data._createdContractsReverse[stamp] record.to = this.data._createdContractsReverse[stamp]
} }
}
record.from = accounts[record.from]
// @TODO: writing browser test
return record
} }
record.from = accounts[record.from]
// @TODO: writing browser test
return record
}
/** /**
* save the given @arg record * save the given @arg record
* *
* @param {Number/String} timestamp * @param {Number/String} timestamp
* @param {Object} record * @param {Object} record
* *
*/ */
append (timestamp, record) { append (timestamp, record) {
this.data.journal.push({ timestamp, record }) this.data.journal.push({ timestamp, record })
this.event.trigger('newTxRecorded', [this.data.journal.length]) this.event.trigger('newTxRecorded', [this.data.journal.length])
} }
/** /**
* basically return the records + associate values (like abis / accounts) * basically return the records + associate values (like abis / accounts)
* *
*/ */
getAll () { getAll () {
var records = [].concat(this.data.journal) var records = [].concat(this.data.journal)
return { return {
accounts: this.data._usedAccounts, accounts: this.data._usedAccounts,
linkReferences: this.data._linkReferences, linkReferences: this.data._linkReferences,
transactions: records.sort((A, B) => { transactions: records.sort((A, B) => {
var stampA = A.timestamp var stampA = A.timestamp
var stampB = B.timestamp var stampB = B.timestamp
return stampA - stampB return stampA - stampB
}), }),
abis: this.data._abis abis: this.data._abis
}
} }
}
/** /**
* delete the seen transactions * delete the seen transactions
* *
*/ */
clearAll () { clearAll () {
this.data._listen = true this.data._listen = true
this.data._replay = false this.data._replay = false
this.data.journal = [] this.data.journal = []
this.data._createdContracts = {} this.data._createdContracts = {}
this.data._createdContractsReverse = {} this.data._createdContractsReverse = {}
this.data._usedAccounts = {} this.data._usedAccounts = {}
this.data._abis = {} this.data._abis = {}
this.data._contractABIReferences = {} this.data._contractABIReferences = {}
this.data._linkReferences = {} this.data._linkReferences = {}
this.event.trigger('cleared', []) this.event.trigger('cleared', [])
} }
/** /**
* run the list of records * run the list of records
* *
* @param {Object} records * @param {Object} records
@ -199,134 +199,134 @@ class Recorder extends Plugin {
* @param {Function} newContractFn * @param {Function} newContractFn
* *
*/ */
run (records, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, liveMode, newContractFn) { run (records, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, liveMode, newContractFn) {
this.setListen(false) this.setListen(false)
const liveMsg = liveMode ? ' with updated contracts' : '' const liveMsg = liveMode ? ' with updated contracts' : ''
logCallBack(`Running ${records.length} transaction(s)${liveMsg} ...`) logCallBack(`Running ${records.length} transaction(s)${liveMsg} ...`)
async.eachOfSeries(records, async (tx, index, cb) => { async.eachOfSeries(records, async (tx, index, cb) => {
if (liveMode && tx.record.type === 'constructor') { if (liveMode && tx.record.type === 'constructor') {
// resolve the bytecode and ABI using the contract name, this ensure getting the last compiled one. // resolve the bytecode and ABI using the contract name, this ensure getting the last compiled one.
const data = await this.call('compilerArtefacts', 'getArtefactsByContractName', tx.record.contractName) const data = await this.call('compilerArtefacts', 'getArtefactsByContractName', tx.record.contractName)
tx.record.bytecode = data.artefact.evm.bytecode.object tx.record.bytecode = data.artefact.evm.bytecode.object
const updatedABIKeccak = bufferToHex(hash.keccakFromString(JSON.stringify(data.artefact.abi))) const updatedABIKeccak = bufferToHex(hash.keccakFromString(JSON.stringify(data.artefact.abi)))
abis[updatedABIKeccak] = data.artefact.abi abis[updatedABIKeccak] = data.artefact.abi
tx.record.abi = updatedABIKeccak tx.record.abi = updatedABIKeccak
} }
var record = this.resolveAddress(tx.record, accounts, options) var record = this.resolveAddress(tx.record, accounts, options)
var abi = abis[tx.record.abi] var abi = abis[tx.record.abi]
if (!abi) { if (!abi) {
return alertCb('cannot find ABI for ' + tx.record.abi + '. Execution stopped at ' + index) return alertCb('cannot find ABI for ' + tx.record.abi + '. Execution stopped at ' + index)
} }
/* Resolve Library */ /* Resolve Library */
if (record.linkReferences && Object.keys(record.linkReferences).length) { if (record.linkReferences && Object.keys(record.linkReferences).length) {
for (var k in linkReferences) { for (var k in linkReferences) {
var link = linkReferences[k] var link = linkReferences[k]
var timestamp = this.extractTimestamp(link) var timestamp = this.extractTimestamp(link)
if (timestamp && this.data._createdContractsReverse[timestamp]) { if (timestamp && this.data._createdContractsReverse[timestamp]) {
link = this.data._createdContractsReverse[timestamp] link = this.data._createdContractsReverse[timestamp]
} }
tx.record.bytecode = format.linkLibraryStandardFromlinkReferences(k, link.replace('0x', ''), tx.record.bytecode, tx.record.linkReferences) tx.record.bytecode = format.linkLibraryStandardFromlinkReferences(k, link.replace('0x', ''), tx.record.bytecode, tx.record.linkReferences)
}
}
/* Encode params */
var fnABI
if (tx.record.type === 'constructor') {
fnABI = txHelper.getConstructorInterface(abi)
} else if (tx.record.type === 'fallback') {
fnABI = txHelper.getFallbackInterface(abi)
} else if (tx.record.type === 'receive') {
fnABI = txHelper.getReceiveInterface(abi)
} else {
fnABI = txHelper.getFunction(abi, record.name + record.inputs)
}
if (!fnABI) {
alertCb('cannot resolve abi of ' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index)
return cb('cannot resolve abi')
}
if (tx.record.parameters) {
/* check if we have some params to resolve */
try {
tx.record.parameters.forEach((value, index) => {
var isString = true
if (typeof value !== 'string') {
isString = false
value = JSON.stringify(value)
}
for (var timestamp in this.data._createdContractsReverse) {
value = value.replace(new RegExp('created\\{' + timestamp + '\\}', 'g'), this.data._createdContractsReverse[timestamp])
}
if (!isString) value = JSON.parse(value)
tx.record.parameters[index] = value
})
} catch (e) {
return alertCb('cannot resolve input parameters ' + JSON.stringify(tx.record.parameters) + '. Execution stopped at ' + index)
}
}
var data = format.encodeData(fnABI, tx.record.parameters, tx.record.bytecode)
if (data.error) {
alertCb(data.error + '. Record:' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index)
return cb(data.error)
}
logCallBack(`(${index}) ${JSON.stringify(record, null, '\t')}`)
logCallBack(`(${index}) data: ${data.data}`)
record.data = { dataHex: data.data, funArgs: tx.record.parameters, funAbi: fnABI, contractBytecode: tx.record.bytecode, contractName: tx.record.contractName, timestamp: tx.timestamp }
this.blockchain.runTx(record, confirmationCb, continueCb, promptCb,
(err, txResult, rawAddress) => {
if (err) {
console.error(err)
return logCallBack(err + '. Execution failed at ' + index)
}
if (rawAddress) {
const address = addressToString(rawAddress)
// save back created addresses for the convertion from tokens to real adresses
this.data._createdContracts[address] = tx.timestamp
this.data._createdContractsReverse[tx.timestamp] = address
newContractFn(abi, address, record.contractName)
}
cb(err)
}
)
}, () => { this.setListen(true) })
}
runScenario (liveMode, json, continueCb, promptCb, alertCb, confirmationCb, logCallBack, cb) {
_paq.push(['trackEvent', 'run', 'recorder', 'start'])
if (!json) {
_paq.push(['trackEvent', 'run', 'recorder', 'wrong-json'])
return cb('a json content must be provided')
}
if (typeof json === 'string') {
try {
json = JSON.parse(json)
} catch (e) {
return cb('A scenario file is required. It must be json formatted')
}
} }
}
let txArray /* Encode params */
let accounts var fnABI
let options if (tx.record.type === 'constructor') {
let abis fnABI = txHelper.getConstructorInterface(abi)
let linkReferences } else if (tx.record.type === 'fallback') {
fnABI = txHelper.getFallbackInterface(abi)
} else if (tx.record.type === 'receive') {
fnABI = txHelper.getReceiveInterface(abi)
} else {
fnABI = txHelper.getFunction(abi, record.name + record.inputs)
}
if (!fnABI) {
alertCb('cannot resolve abi of ' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index)
return cb('cannot resolve abi')
}
if (tx.record.parameters) {
/* check if we have some params to resolve */
try { try {
txArray = json.transactions || [] tx.record.parameters.forEach((value, index) => {
accounts = json.accounts || [] var isString = true
options = json.options || {} if (typeof value !== 'string') {
abis = json.abis || {} isString = false
linkReferences = json.linkReferences || {} value = JSON.stringify(value)
}
for (var timestamp in this.data._createdContractsReverse) {
value = value.replace(new RegExp('created\\{' + timestamp + '\\}', 'g'), this.data._createdContractsReverse[timestamp])
}
if (!isString) value = JSON.parse(value)
tx.record.parameters[index] = value
})
} catch (e) { } catch (e) {
return cb('Invalid scenario file. Please try again') return alertCb('cannot resolve input parameters ' + JSON.stringify(tx.record.parameters) + '. Execution stopped at ' + index)
} }
}
var data = format.encodeData(fnABI, tx.record.parameters, tx.record.bytecode)
if (data.error) {
alertCb(data.error + '. Record:' + JSON.stringify(record, null, '\t') + '. Execution stopped at ' + index)
return cb(data.error)
}
logCallBack(`(${index}) ${JSON.stringify(record, null, '\t')}`)
logCallBack(`(${index}) data: ${data.data}`)
record.data = { dataHex: data.data, funArgs: tx.record.parameters, funAbi: fnABI, contractBytecode: tx.record.bytecode, contractName: tx.record.contractName, timestamp: tx.timestamp }
if (!txArray.length) { this.blockchain.runTx(record, confirmationCb, continueCb, promptCb,
return cb('No transactions found in scenario file') (err, txResult, rawAddress) => {
if (err) {
console.error(err)
return logCallBack(err + '. Execution failed at ' + index)
}
if (rawAddress) {
const address = addressToString(rawAddress)
// save back created addresses for the convertion from tokens to real adresses
this.data._createdContracts[address] = tx.timestamp
this.data._createdContractsReverse[tx.timestamp] = address
newContractFn(abi, address, record.contractName)
}
cb(err)
} }
)
}, () => { this.setListen(true) })
}
this.run(txArray, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, liveMode, (abi, address, contractName) => { runScenario (liveMode, json, continueCb, promptCb, alertCb, confirmationCb, logCallBack, cb) {
cb(null, abi, address, contractName) _paq.push(['trackEvent', 'run', 'recorder', 'start'])
}) if (!json) {
_paq.push(['trackEvent', 'run', 'recorder', 'wrong-json'])
return cb('a json content must be provided')
}
if (typeof json === 'string') {
try {
json = JSON.parse(json)
} catch (e) {
return cb('A scenario file is required. It must be json formatted')
}
} }
let txArray
let accounts
let options
let abis
let linkReferences
try {
txArray = json.transactions || []
accounts = json.accounts || []
options = json.options || {}
abis = json.abis || {}
linkReferences = json.linkReferences || {}
} catch (e) {
return cb('Invalid scenario file. Please try again')
}
if (!txArray.length) {
return cb('No transactions found in scenario file')
}
this.run(txArray, accounts, options, abis, linkReferences, confirmationCb, continueCb, promptCb, alertCb, logCallBack, liveMode, (abi, address, contractName) => {
cb(null, abi, address, contractName)
})
}
} }
module.exports = Recorder module.exports = Recorder

@ -3,31 +3,31 @@ import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line import React from 'react' // eslint-disable-line
import { SearchTab } from '@remix-ui/search' import { SearchTab } from '@remix-ui/search'
const profile = { const profile = {
name: 'search', name: 'search',
displayName: 'Search in files', displayName: 'Search in files',
methods: [''], methods: [''],
events: [], events: [],
icon: 'assets/img/search_icon.webp', icon: 'assets/img/search_icon.webp',
description: 'Find and replace in file explorer', description: 'Find and replace in file explorer',
kind: '', kind: '',
location: 'sidePanel', location: 'sidePanel',
documentation: '', documentation: '',
version: packageJson.version, version: packageJson.version,
maintainedBy: 'Remix' maintainedBy: 'Remix'
} }
export class SearchPlugin extends ViewPlugin { export class SearchPlugin extends ViewPlugin {
constructor () { constructor () {
super(profile) super(profile)
} }
render() { render() {
return ( return (
<div id='searchTab'> <div id='searchTab'>
<SearchTab plugin={this}></SearchTab> <SearchTab plugin={this}></SearchTab>
</div> </div>
); );
} }
} }

@ -7,81 +7,81 @@ import Registry from '../state/registry'
import { PluginViewWrapper } from '@remix-ui/helper' import { PluginViewWrapper } from '@remix-ui/helper'
const profile = { const profile = {
name: 'settings', name: 'settings',
displayName: 'Settings', displayName: 'Settings',
methods: ['get'], methods: ['get'],
events: [], events: [],
icon: 'assets/img/settings.webp', icon: 'assets/img/settings.webp',
description: 'Remix-IDE settings', description: 'Remix-IDE settings',
kind: 'settings', kind: 'settings',
location: 'sidePanel', location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/settings.html', documentation: 'https://remix-ide.readthedocs.io/en/latest/settings.html',
version: packageJson.version, version: packageJson.version,
permission: true, permission: true,
maintainedBy: "Remix" maintainedBy: "Remix"
} }
module.exports = class SettingsTab extends ViewPlugin { module.exports = class SettingsTab extends ViewPlugin {
config: any = {} config: any = {}
editor: any editor: any
private _deps: { private _deps: {
themeModule: any themeModule: any
localeModule: any localeModule: any
} }
element: HTMLDivElement element: HTMLDivElement
public useMatomoAnalytics: any public useMatomoAnalytics: any
dispatch: React.Dispatch<any> = () => {} dispatch: React.Dispatch<any> = () => {}
constructor (config, editor) { constructor (config, editor) {
super(profile) super(profile)
this.config = config this.config = config
this.config.events.on('configChanged', (changedConfig) => { this.config.events.on('configChanged', (changedConfig) => {
this.emit('configChanged', changedConfig) this.emit('configChanged', changedConfig)
}) })
this.editor = editor this.editor = editor
this._deps = { this._deps = {
themeModule: Registry.getInstance().get('themeModule').api, themeModule: Registry.getInstance().get('themeModule').api,
localeModule: Registry.getInstance().get('localeModule').api localeModule: Registry.getInstance().get('localeModule').api
}
this.element = document.createElement('div')
this.element.setAttribute('id', 'settingsTab')
this.useMatomoAnalytics = null
} }
this.element = document.createElement('div')
this.element.setAttribute('id', 'settingsTab')
this.useMatomoAnalytics = null
}
setDispatch (dispatch: React.Dispatch<any>) { setDispatch (dispatch: React.Dispatch<any>) {
this.dispatch = dispatch this.dispatch = dispatch
this.renderComponent() this.renderComponent()
} }
render() { render() {
return <div id='settingsTab'> return <div id='settingsTab'>
<PluginViewWrapper plugin={this} /> <PluginViewWrapper plugin={this} />
</div> </div>
} }
updateComponent(state: any){ updateComponent(state: any){
return <RemixUiSettings return <RemixUiSettings
config={state.config} config={state.config}
editor={state.editor} editor={state.editor}
_deps={state._deps} _deps={state._deps}
useMatomoAnalytics={state.useMatomoAnalytics} useMatomoAnalytics={state.useMatomoAnalytics}
themeModule = {state._deps.themeModule} themeModule = {state._deps.themeModule}
localeModule={state._deps.localeModule} localeModule={state._deps.localeModule}
/> />
} }
renderComponent () { renderComponent () {
this.dispatch(this) this.dispatch(this)
} }
get (key) { get (key) {
return this.config.get(key) return this.config.get(key)
} }
updateMatomoAnalyticsChoice (isChecked) { updateMatomoAnalyticsChoice (isChecked) {
this.config.set('settings/matomo-analytics', isChecked) this.config.set('settings/matomo-analytics', isChecked)
this.useMatomoAnalytics = isChecked this.useMatomoAnalytics = isChecked
this.dispatch({ this.dispatch({
...this ...this
}) })
} }
} }

@ -11,147 +11,147 @@ import { PluginViewWrapper } from '@remix-ui/helper'
var { UnitTestRunner, assertLibCode } = require('@remix-project/remix-tests') var { UnitTestRunner, assertLibCode } = require('@remix-project/remix-tests')
const profile = { const profile = {
name: 'solidityUnitTesting', name: 'solidityUnitTesting',
displayName: 'Solidity unit testing', displayName: 'Solidity unit testing',
methods: ['testFromPath', 'testFromSource', 'setTestFolderPath', 'getTestlibs', 'createTestLibs'], methods: ['testFromPath', 'testFromSource', 'setTestFolderPath', 'getTestlibs', 'createTestLibs'],
events: [], events: [],
icon: 'assets/img/unitTesting.webp', icon: 'assets/img/unitTesting.webp',
description: 'Write and run unit tests for your contracts in Solidity', description: 'Write and run unit tests for your contracts in Solidity',
location: 'sidePanel', location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/unittesting.html', documentation: 'https://remix-ide.readthedocs.io/en/latest/unittesting.html',
maintainedBy: 'Remix' maintainedBy: 'Remix'
} }
module.exports = class TestTab extends ViewPlugin { module.exports = class TestTab extends ViewPlugin {
constructor (fileManager, offsetToLineColumnConverter, filePanel, compileTab, appManager, contentImport) { constructor (fileManager, offsetToLineColumnConverter, filePanel, compileTab, appManager, contentImport) {
super(profile) super(profile)
this.compileTab = compileTab this.compileTab = compileTab
this.contentImport = contentImport this.contentImport = contentImport
this.fileManager = fileManager this.fileManager = fileManager
this.filePanel = filePanel this.filePanel = filePanel
this.appManager = appManager this.appManager = appManager
this.testRunner = new UnitTestRunner() this.testRunner = new UnitTestRunner()
this.testTabLogic = new TestTabLogic(this.fileManager, helper) this.testTabLogic = new TestTabLogic(this.fileManager, helper)
this.offsetToLineColumnConverter = offsetToLineColumnConverter this.offsetToLineColumnConverter = offsetToLineColumnConverter
this.allFilesInvolved = ['.deps/remix-tests/remix_tests.sol', '.deps/remix-tests/remix_accounts.sol'] this.allFilesInvolved = ['.deps/remix-tests/remix_tests.sol', '.deps/remix-tests/remix_accounts.sol']
this.element = document.createElement('div') this.element = document.createElement('div')
this.dispatch = null this.dispatch = null
}
onActivationInternal () {
this.listenToEvents()
this.call('filePanel', 'registerContextMenuItem', {
id: 'solidityUnitTesting',
name: 'setTestFolderPath',
label: 'Set path for Unit Testing',
type: ['folder'],
extension: [],
path: [],
pattern: []
})
}
async setTestFolderPath (event) {
if (event.path.length > 0) {
this.renderComponent(event.path[0])
} }
}
onActivationInternal () { getTestlibs () {
this.listenToEvents() return { assertLibCode, accountsLibCode: this.testRunner.accountsLibCode }
this.call('filePanel', 'registerContextMenuItem', { }
id: 'solidityUnitTesting',
name: 'setTestFolderPath',
label: 'Set path for Unit Testing',
type: ['folder'],
extension: [],
path: [],
pattern: []
})
}
async setTestFolderPath (event) {
if (event.path.length > 0) {
this.renderComponent(event.path[0])
}
}
getTestlibs () {
return { assertLibCode, accountsLibCode: this.testRunner.accountsLibCode }
}
async createTestLibs () {
const provider = await this.fileManager.currentFileProvider()
if (provider) {
await provider.addExternal('.deps/remix-tests/remix_tests.sol', assertLibCode, 'remix_tests.sol')
await provider.addExternal('.deps/remix-tests/remix_accounts.sol', this.testRunner.accountsLibCode, 'remix_accounts.sol')
}
}
async onActivation () {
const isSolidityActive = await this.call('manager', 'isActive', 'solidity')
if (!isSolidityActive) {
await this.call('manager', 'activatePlugin', 'solidity')
}
await this.testRunner.init(await this.call('blockchain', 'web3VM'))
await this.createTestLibs()
}
onDeactivation () {
this.off('filePanel', 'newTestFileCreated')
this.off('filePanel', 'setWorkspace')
// 'currentFileChanged' event is added more than once
this.fileManager.events.removeAllListeners('currentFileChanged')
}
listenToEvents () { async createTestLibs () {
this.on('filePanel', 'workspaceCreated', async () => { const provider = await this.fileManager.currentFileProvider()
this.createTestLibs() if (provider) {
}) await provider.addExternal('.deps/remix-tests/remix_tests.sol', assertLibCode, 'remix_tests.sol')
await provider.addExternal('.deps/remix-tests/remix_accounts.sol', this.testRunner.accountsLibCode, 'remix_accounts.sol')
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, input, version)
}
})
} }
}
async testFromPath (path) { async onActivation () {
const fileContent = await this.fileManager.readFile(path) const isSolidityActive = await this.call('manager', 'isActive', 'solidity')
return this.testFromSource(fileContent, path) if (!isSolidityActive) {
await this.call('manager', 'activatePlugin', 'solidity')
} }
await this.testRunner.init(await this.call('blockchain', 'web3VM'))
/* await this.createTestLibs()
}
onDeactivation () {
this.off('filePanel', 'newTestFileCreated')
this.off('filePanel', 'setWorkspace')
// 'currentFileChanged' event is added more than once
this.fileManager.events.removeAllListeners('currentFileChanged')
}
listenToEvents () {
this.on('filePanel', 'workspaceCreated', async () => {
this.createTestLibs()
})
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, input, version)
}
})
}
async testFromPath (path) {
const fileContent = await this.fileManager.readFile(path)
return this.testFromSource(fileContent, path)
}
/*
Test is not associated with the UI Test is not associated with the UI
*/ */
async testFromSource (content, path = 'browser/unit_test.sol') { async testFromSource (content, path = 'browser/unit_test.sol') {
const web3 = await this.call('blockchain', 'web3VM') const web3 = await this.call('blockchain', 'web3VM')
await this.testRunner.init(web3) await this.testRunner.init(web3)
await this.createTestLibs() await this.createTestLibs()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const runningTest = {} const runningTest = {}
runningTest[path] = { content } runningTest[path] = { content }
const { currentVersion, evmVersion, optimize, runs } = this.compileTab.getCurrentCompilerConfig() const { currentVersion, evmVersion, optimize, runs } = this.compileTab.getCurrentCompilerConfig()
const currentCompilerUrl = urlFromVersion(currentVersion) const currentCompilerUrl = urlFromVersion(currentVersion)
const compilerConfig = { const compilerConfig = {
currentCompilerUrl, currentCompilerUrl,
evmVersion, evmVersion,
optimize, optimize,
usingWorker: canUseWorker(currentVersion), usingWorker: canUseWorker(currentVersion),
runs runs
} }
this.testRunner.runTestSources(runningTest, compilerConfig, () => { /* Do nothing. */ }, () => { /* Do nothing. */ }, null, (error, result) => { this.testRunner.runTestSources(runningTest, compilerConfig, () => { /* Do nothing. */ }, () => { /* Do nothing. */ }, null, (error, result) => {
if (error) return reject(error) if (error) return reject(error)
resolve(result) resolve(result)
}, (url, cb) => { }, (url, cb) => {
return this.contentImport.resolveAndSave(url).then((result) => cb(null, result)).catch((error) => cb(error.message)) return this.contentImport.resolveAndSave(url).then((result) => cb(null, result)).catch((error) => cb(error.message))
}, {}) }, {})
}) })
} }
setDispatch (dispatch) { setDispatch (dispatch) {
this.dispatch = dispatch this.dispatch = dispatch
this.renderComponent('tests') this.renderComponent('tests')
} }
render () { render () {
this.onActivationInternal() this.onActivationInternal()
return <div><PluginViewWrapper plugin={this} /></div> return <div><PluginViewWrapper plugin={this} /></div>
} }
updateComponent(state) { updateComponent(state) {
return <SolidityUnitTesting testTab={state.testTab} helper={state.helper} initialPath={state.testDirPath} /> return <SolidityUnitTesting testTab={state.testTab} helper={state.helper} initialPath={state.testDirPath} />
} }
renderComponent (testDirPath) { renderComponent (testDirPath) {
this.dispatch({ this.dispatch({
testTab: this, testTab: this,
helper: helper, helper: helper,
testDirPath: testDirPath testDirPath: testDirPath
}) })
} }
} }

@ -6,128 +6,128 @@ import Registry from '../state/registry'
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const themes = [ const themes = [
{ name: 'Dark', quality: 'dark', url: 'assets/css/themes/remix-dark_tvx1s2.css' }, { name: 'Dark', quality: 'dark', url: 'assets/css/themes/remix-dark_tvx1s2.css' },
{ name: 'Light', quality: 'light', url: 'assets/css/themes/remix-light_powaqg.css' }, { name: 'Light', quality: 'light', url: 'assets/css/themes/remix-light_powaqg.css' },
{ name: 'Violet', quality: 'light', url: 'assets/css/themes/remix-violet.css' }, { name: 'Violet', quality: 'light', url: 'assets/css/themes/remix-violet.css' },
{ name: 'Unicorn', quality: 'light', url: 'assets/css/themes/remix-unicorn.css' }, { name: 'Unicorn', quality: 'light', url: 'assets/css/themes/remix-unicorn.css' },
{ name: 'Midcentury', quality: 'light', url: 'assets/css/themes/remix-midcentury_hrzph3.css' }, { name: 'Midcentury', quality: 'light', url: 'assets/css/themes/remix-midcentury_hrzph3.css' },
{ name: 'Black', quality: 'dark', url: 'assets/css/themes/remix-black_undtds.css' }, { name: 'Black', quality: 'dark', url: 'assets/css/themes/remix-black_undtds.css' },
{ name: 'Candy', quality: 'light', url: 'assets/css/themes/remix-candy_ikhg4m.css' }, { name: 'Candy', quality: 'light', url: 'assets/css/themes/remix-candy_ikhg4m.css' },
{ name: 'HackerOwl', quality: 'dark', url: 'assets/css/themes/remix-hacker_owl.css' }, { name: 'HackerOwl', quality: 'dark', url: 'assets/css/themes/remix-hacker_owl.css' },
{ name: 'Cerulean', quality: 'light', url: 'assets/css/themes/bootstrap-cerulean.min.css' }, { name: 'Cerulean', quality: 'light', url: 'assets/css/themes/bootstrap-cerulean.min.css' },
{ name: 'Flatly', quality: 'light', url: 'assets/css/themes/bootstrap-flatly.min.css' }, { name: 'Flatly', quality: 'light', url: 'assets/css/themes/bootstrap-flatly.min.css' },
{ name: 'Spacelab', quality: 'light', url: 'assets/css/themes/bootstrap-spacelab.min.css' }, { name: 'Spacelab', quality: 'light', url: 'assets/css/themes/bootstrap-spacelab.min.css' },
{ name: 'Cyborg', quality: 'dark', url: 'assets/css/themes/bootstrap-cyborg.min.css' } { name: 'Cyborg', quality: 'dark', url: 'assets/css/themes/bootstrap-cyborg.min.css' }
] ]
const profile = { const profile = {
name: 'theme', name: 'theme',
events: ['themeChanged'], events: ['themeChanged'],
methods: ['switchTheme', 'getThemes', 'currentTheme', 'fixInvert'], methods: ['switchTheme', 'getThemes', 'currentTheme', 'fixInvert'],
version: packageJson.version, version: packageJson.version,
kind: 'theme' kind: 'theme'
} }
export class ThemeModule extends Plugin { export class ThemeModule extends Plugin {
constructor () { constructor () {
super(profile) super(profile)
this.events = new EventEmitter() this.events = new EventEmitter()
this._deps = { this._deps = {
config: Registry.getInstance().get('config') && Registry.getInstance().get('config').api config: Registry.getInstance().get('config') && Registry.getInstance().get('config').api
}
this.themes = {}
themes.map((theme) => {
this.themes[theme.name.toLocaleLowerCase()] = {
...theme,
url: window.location.origin + ( window.location.pathname.startsWith('/address/') || window.location.pathname.endsWith('.sol') ? '/' : 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 && this._deps.config.get('settings/theme')) || null
currentTheme = currentTheme && currentTheme.toLocaleLowerCase()
currentTheme = this.themes[currentTheme] ? currentTheme : null
this.currentThemeState = { queryTheme, currentTheme }
this.active = queryTheme || currentTheme || 'dark'
this.forced = !!queryTheme
} }
this.themes = {}
themes.map((theme) => {
this.themes[theme.name.toLocaleLowerCase()] = {
...theme,
url: window.location.origin + ( window.location.pathname.startsWith('/address/') || window.location.pathname.endsWith('.sol') ? '/' : 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 && this._deps.config.get('settings/theme')) || null
currentTheme = currentTheme && currentTheme.toLocaleLowerCase()
currentTheme = this.themes[currentTheme] ? currentTheme : null
this.currentThemeState = { queryTheme, currentTheme }
this.active = queryTheme || currentTheme || 'dark'
this.forced = !!queryTheme
}
/** Return the active theme /** Return the active theme
* @return {{ name: string, quality: string, url: string }} - The active theme * @return {{ name: string, quality: string, url: string }} - The active theme
*/ */
currentTheme () { currentTheme () {
return this.themes[this.active] return this.themes[this.active]
} }
/** Returns all themes as an array */ /** Returns all themes as an array */
getThemes () { getThemes () {
return Object.keys(this.themes).map(key => this.themes[key]) return Object.keys(this.themes).map(key => this.themes[key])
} }
/** /**
* Init the theme * Init the theme
*/ */
initTheme (callback) { // callback is setTimeOut in app.js which is always passed initTheme (callback) { // callback is setTimeOut in app.js which is always passed
if (callback) this.initCallback = callback if (callback) this.initCallback = callback
if (this.active) { if (this.active) {
document.getElementById('theme-link') ? document.getElementById('theme-link').remove():null document.getElementById('theme-link') ? document.getElementById('theme-link').remove():null
const nextTheme = this.themes[this.active] // Theme const nextTheme = this.themes[this.active] // Theme
document.documentElement.style.setProperty('--theme', nextTheme.quality) document.documentElement.style.setProperty('--theme', nextTheme.quality)
const theme = document.createElement('link') const theme = document.createElement('link')
theme.setAttribute('rel', 'stylesheet') theme.setAttribute('rel', 'stylesheet')
theme.setAttribute('href', nextTheme.url) theme.setAttribute('href', nextTheme.url)
theme.setAttribute('id', 'theme-link') theme.setAttribute('id', 'theme-link')
theme.addEventListener('load', () => { theme.addEventListener('load', () => {
if (callback) callback() if (callback) callback()
}) })
document.head.insertBefore(theme, document.head.firstChild) document.head.insertBefore(theme, document.head.firstChild)
}
} }
}
/** /**
* Change the current theme * Change the current theme
* @param {string} [themeName] - The name of the theme * @param {string} [themeName] - The name of the theme
*/ */
switchTheme (themeName) { switchTheme (themeName) {
themeName = themeName && themeName.toLocaleLowerCase() themeName = themeName && themeName.toLocaleLowerCase()
if (themeName && !Object.keys(this.themes).includes(themeName)) { if (themeName && !Object.keys(this.themes).includes(themeName)) {
throw new Error(`Theme ${themeName} doesn't exist`) throw new Error(`Theme ${themeName} doesn't exist`)
}
const next = themeName || this.active // Name
if (next === this.active) return // --> exit out of this method
_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') ? 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')
theme.addEventListener('load', () => {
this.emit('themeLoaded', nextTheme)
this.events.emit('themeLoaded', nextTheme)
})
document.head.insertBefore(theme, document.head.firstChild)
document.documentElement.style.setProperty('--theme', nextTheme.quality)
if (themeName) this.active = themeName
// TODO: Only keep `this.emit` (issue#2210)
this.emit('themeChanged', nextTheme)
this.events.emit('themeChanged', nextTheme)
} }
const next = themeName || this.active // Name
if (next === this.active) return // --> exit out of this method
_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') ? 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')
theme.addEventListener('load', () => {
this.emit('themeLoaded', nextTheme)
this.events.emit('themeLoaded', nextTheme)
})
document.head.insertBefore(theme, document.head.firstChild)
document.documentElement.style.setProperty('--theme', nextTheme.quality)
if (themeName) this.active = themeName
// TODO: Only keep `this.emit` (issue#2210)
this.emit('themeChanged', nextTheme)
this.events.emit('themeChanged', nextTheme)
}
/** /**
* fixes the invertion for images since this should be adjusted when we switch between dark/light qualified themes * fixes the invertion for images since this should be adjusted when we switch between dark/light qualified themes
* @param {element} [image] - the dom element which invert should be fixed to increase visibility * @param {element} [image] - the dom element which invert should be fixed to increase visibility
*/ */
fixInvert (image) { fixInvert (image) {
const invert = this.currentTheme().quality === 'dark' ? 1 : 0 const invert = this.currentTheme().quality === 'dark' ? 1 : 0
if (image) { if (image) {
image.style.filter = `invert(${invert})` image.style.filter = `invert(${invert})`
}
} }
}
} }

@ -2,83 +2,83 @@ import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
export const profile = { export const profile = {
name: 'web3Provider', name: 'web3Provider',
displayName: 'Global Web3 Provider', displayName: 'Global Web3 Provider',
description: 'Represent the current web3 provider used by the app at global scope', description: 'Represent the current web3 provider used by the app at global scope',
methods: ['sendAsync'], methods: ['sendAsync'],
version: packageJson.version, version: packageJson.version,
kind: 'provider' kind: 'provider'
} }
export class Web3ProviderModule extends Plugin { export class Web3ProviderModule extends Plugin {
constructor(blockchain) { constructor(blockchain) {
super(profile) super(profile)
this.blockchain = blockchain this.blockchain = blockchain
} }
/* /*
that is used by plugins to call the current ethereum provider. that is used by plugins to call the current ethereum provider.
Should be taken carefully and probably not be release as it is now. Should be taken carefully and probably not be release as it is now.
*/ */
sendAsync(payload) { sendAsync(payload) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.askUserPermission('sendAsync', `Calling ${payload.method} with parameters ${JSON.stringify(payload.params, null, '\t')}`).then( this.askUserPermission('sendAsync', `Calling ${payload.method} with parameters ${JSON.stringify(payload.params, null, '\t')}`).then(
async (result) => { async (result) => {
if (result) { if (result) {
const provider = this.blockchain.web3().currentProvider const provider = this.blockchain.web3().currentProvider
// see https://github.com/ethereum/web3.js/pull/1018/files#diff-d25786686c1053b786cc2626dc6e048675050593c0ebaafbf0814e1996f22022R129 // see https://github.com/ethereum/web3.js/pull/1018/files#diff-d25786686c1053b786cc2626dc6e048675050593c0ebaafbf0814e1996f22022R129
provider[provider.sendAsync ? 'sendAsync' : 'send'](payload, async (error, message) => { provider[provider.sendAsync ? 'sendAsync' : 'send'](payload, async (error, message) => {
if (error) { if (error) {
// Handle 'The method "debug_traceTransaction" does not exist / is not available.' error // Handle 'The method "debug_traceTransaction" does not exist / is not available.' error
if(error.message && error.code && error.code === -32601) { if(error.message && error.code && error.code === -32601) {
this.call('terminal', 'log', { value: error.message, type: 'error' } ) this.call('terminal', 'log', { value: error.message, type: 'error' } )
return reject(error.message) return reject(error.message)
} else { } else {
const errorData = error.data || error.message || error const errorData = error.data || error.message || error
// See: https://github.com/ethers-io/ethers.js/issues/901 // See: https://github.com/ethers-io/ethers.js/issues/901
if (!(typeof errorData === 'string' && errorData.includes("unknown method eth_chainId"))) this.call('terminal', 'log', { value: error.data || error.message, type: 'error' } ) if (!(typeof errorData === 'string' && errorData.includes("unknown method eth_chainId"))) this.call('terminal', 'log', { value: error.data || error.message, type: 'error' } )
return reject(errorData) return reject(errorData)
} }
} }
if (payload.method === 'eth_sendTransaction') { if (payload.method === 'eth_sendTransaction') {
if (payload.params.length && !payload.params[0].to && message.result) { if (payload.params.length && !payload.params[0].to && message.result) {
setTimeout(async () => { setTimeout(async () => {
const receipt = await this.tryTillReceiptAvailable(message.result) const receipt = await this.tryTillReceiptAvailable(message.result)
if (!receipt.contractAddress) { if (!receipt.contractAddress) {
console.log('receipt available but contract address not present', receipt) console.log('receipt available but contract address not present', receipt)
return return
}
const contractData = await this.call('compilerArtefacts', 'getContractDataFromAddress', receipt.contractAddress)
if (contractData) this.call('udapp', 'addInstance', receipt.contractAddress, contractData.contract.abi, contractData.name)
}, 50)
}
}
resolve(message)
})
} else {
reject(new Error('User denied permission'))
} }
}).catch((e) => { const contractData = await this.call('compilerArtefacts', 'getContractDataFromAddress', receipt.contractAddress)
reject(e) if (contractData) this.call('udapp', 'addInstance', receipt.contractAddress, contractData.contract.abi, contractData.name)
}, 50)
}
}
resolve(message)
}) })
}) } else {
} reject(new Error('User denied permission'))
}
}).catch((e) => {
reject(e)
})
})
}
async tryTillReceiptAvailable(txhash) { async tryTillReceiptAvailable(txhash) {
try { try {
const receipt = await this.call('blockchain', 'getTransactionReceipt', txhash) const receipt = await this.call('blockchain', 'getTransactionReceipt', txhash)
if (receipt) return receipt if (receipt) return receipt
} catch (e) { } catch (e) {
// do nothing // do nothing
}
await this.pause()
return await this.tryTillReceiptAvailable(txhash)
} }
await this.pause()
return await this.tryTillReceiptAvailable(txhash)
}
async pause() { async pause() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(resolve, 500) setTimeout(resolve, 500)
}) })
} }
} }

@ -4,37 +4,37 @@ var remixLib = require('@remix-project/remix-lib')
var EventsDecoder = remixLib.execution.EventsDecoder var EventsDecoder = remixLib.execution.EventsDecoder
export function makeUdapp (blockchain, compilersArtefacts, logHtmlCallback) { export function makeUdapp (blockchain, compilersArtefacts, logHtmlCallback) {
// ----------------- Tx listener ----------------- // ----------------- Tx listener -----------------
const _transactionReceipts = {} const _transactionReceipts = {}
const transactionReceiptResolver = (tx, cb) => { const transactionReceiptResolver = (tx, cb) => {
if (_transactionReceipts[tx.hash]) { if (_transactionReceipts[tx.hash]) {
return cb(null, _transactionReceipts[tx.hash]) return cb(null, _transactionReceipts[tx.hash])
}
blockchain.web3().eth.getTransactionReceipt(tx.hash, (error, receipt) => {
if (error) {
return cb(error)
}
_transactionReceipts[tx.hash] = receipt
cb(null, receipt)
})
} }
blockchain.web3().eth.getTransactionReceipt(tx.hash, (error, receipt) => {
const txlistener = blockchain.getTxListener({ if (error) {
api: { return cb(error)
contracts: function () { }
if (compilersArtefacts.__last) return compilersArtefacts.getAllContractDatas() _transactionReceipts[tx.hash] = receipt
return null cb(null, receipt)
},
resolveReceipt: transactionReceiptResolver
}
}) })
}
const txlistener = blockchain.getTxListener({
api: {
contracts: function () {
if (compilersArtefacts.__last) return compilersArtefacts.getAllContractDatas()
return null
},
resolveReceipt: transactionReceiptResolver
}
})
Registry.getInstance().put({ api: txlistener, name: 'txlistener' }) Registry.getInstance().put({ api: txlistener, name: 'txlistener' })
blockchain.startListening(txlistener) blockchain.startListening(txlistener)
const eventsDecoder = new EventsDecoder({ const eventsDecoder = new EventsDecoder({
resolveReceipt: transactionReceiptResolver resolveReceipt: transactionReceiptResolver
}) })
txlistener.startListening() txlistener.startListening()
Registry.getInstance().put({ api: eventsDecoder, name: 'eventsDecoder' }) Registry.getInstance().put({ api: eventsDecoder, name: 'eventsDecoder' })
} }

@ -9,183 +9,183 @@ const Recorder = require('../tabs/runTab/model/recorder.js')
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const profile = { const profile = {
name: 'udapp', name: 'udapp',
displayName: 'Deploy & run transactions', displayName: 'Deploy & run transactions',
icon: 'assets/img/deployAndRun.webp', icon: 'assets/img/deployAndRun.webp',
description: 'Execute, save and replay transactions', description: 'Execute, save and replay transactions',
kind: 'udapp', kind: 'udapp',
location: 'sidePanel', location: 'sidePanel',
documentation: 'https://remix-ide.readthedocs.io/en/latest/run.html', documentation: 'https://remix-ide.readthedocs.io/en/latest/run.html',
version: packageJson.version, version: packageJson.version,
maintainedBy: 'Remix', maintainedBy: 'Remix',
permission: true, permission: true,
events: ['newTransaction'], events: ['newTransaction'],
methods: ['createVMAccount', 'sendTransaction', 'getAccounts', 'pendingTransactionsCount', 'getSettings', 'setEnvironmentMode', 'clearAllInstances', 'addInstance', 'resolveContractAndAddInstance'] methods: ['createVMAccount', 'sendTransaction', 'getAccounts', 'pendingTransactionsCount', 'getSettings', 'setEnvironmentMode', 'clearAllInstances', 'addInstance', 'resolveContractAndAddInstance']
} }
export class RunTab extends ViewPlugin { export class RunTab extends ViewPlugin {
constructor (blockchain, config, fileManager, editor, filePanel, compilersArtefacts, networkModule, fileProvider) { constructor (blockchain, config, fileManager, editor, filePanel, compilersArtefacts, networkModule, fileProvider) {
super(profile) super(profile)
this.event = new EventManager() this.event = new EventManager()
this.config = config this.config = config
this.blockchain = blockchain this.blockchain = blockchain
this.fileManager = fileManager this.fileManager = fileManager
this.editor = editor this.editor = editor
this.filePanel = filePanel this.filePanel = filePanel
this.compilersArtefacts = compilersArtefacts this.compilersArtefacts = compilersArtefacts
this.networkModule = networkModule this.networkModule = networkModule
this.fileProvider = fileProvider this.fileProvider = fileProvider
this.recorder = new Recorder(blockchain) this.recorder = new Recorder(blockchain)
this.REACT_API = {} this.REACT_API = {}
this.setupEvents() this.setupEvents()
this.el = document.createElement('div') this.el = document.createElement('div')
} }
setupEvents () { setupEvents () {
this.blockchain.events.on('newTransaction', (tx, receipt) => { this.blockchain.events.on('newTransaction', (tx, receipt) => {
this.emit('newTransaction', tx, receipt) this.emit('newTransaction', tx, receipt)
}) })
} }
getSettings () { getSettings () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
resolve({ resolve({
selectedAccount: this.REACT_API.accounts.selectedAccount, selectedAccount: this.REACT_API.accounts.selectedAccount,
selectedEnvMode: this.REACT_API.selectExEnv, selectedEnvMode: this.REACT_API.selectExEnv,
networkEnvironment: this.REACT_API.networkName networkEnvironment: this.REACT_API.networkName
}) })
}) })
} }
async setEnvironmentMode (env) {
const canCall = await this.askUserPermission('setEnvironmentMode', 'change the environment used')
if (canCall) {
env = typeof env === 'string' ? { context: env } : env
this.emit('setEnvironmentModeReducer', env, this.currentRequest.from)
}
}
clearAllInstances () {
this.emit('clearAllInstancesReducer')
}
addInstance (address, abi, name) {
this.emit('addInstanceReducer', address, abi, name)
}
createVMAccount (newAccount) {
return this.blockchain.createVMAccount(newAccount)
}
sendTransaction (tx) {
_paq.push(['trackEvent', 'udapp', 'sendTx', 'udappTransaction'])
return this.blockchain.sendTransaction(tx)
}
getAccounts (cb) {
return this.blockchain.getAccounts(cb)
}
pendingTransactionsCount () {
return this.blockchain.pendingTransactionsCount()
}
render () {
return <div><RunTabUI plugin={this} /></div>
}
onReady (api) {
this.REACT_API = api
}
async onInitDone () {
const udapp = this // eslint-disable-line
async setEnvironmentMode (env) { const addProvider = async (name, displayName, isInjected, isVM, fork = '', dataId = '', title = '') => {
const canCall = await this.askUserPermission('setEnvironmentMode', 'change the environment used') await this.call('blockchain', 'addProvider', {
if (canCall) { options: {},
env = typeof env === 'string' ? { context: env } : env dataId,
this.emit('setEnvironmentModeReducer', env, this.currentRequest.from) name,
displayName,
fork,
isInjected,
isVM,
title,
init: async function () {
const options = await udapp.call(name, 'init')
if (options) {
this.options = options
if (options['fork']) this.fork = options['fork']
}
},
provider: {
async sendAsync (payload, callback) {
try {
const result = await udapp.call(name, 'sendAsync', payload)
callback(null, result)
} catch (e) {
callback(e)
}
}
} }
})
} }
clearAllInstances () { // basic injected
this.emit('clearAllInstancesReducer') // if it's the trust wallet provider, we have a specific provider for that, see below
} if (window && window.ethereum && !(window.ethereum.isTrustWallet || window.ethereum.selectedProvider?.isTrustWallet)) {
const displayNameInjected = `Injected Provider${(window && window.ethereum && !(window.ethereum.providers && !window.ethereum.selectedProvider)) ?
addInstance (address, abi, name) { window.ethereum.isCoinbaseWallet || window.ethereum.selectedProvider?.isCoinbaseWallet ? ' - Coinbase' :
this.emit('addInstanceReducer', address, abi, name) window.ethereum.isBraveWallet || window.ethereum.selectedProvider?.isBraveWallet ? ' - Brave' :
} window.ethereum.isMetaMask || window.ethereum.selectedProvider?.isMetaMask ? ' - MetaMask' : '' : ''}`
await addProvider('injected', displayNameInjected, true, false)
createVMAccount (newAccount) { } else if (window && !window.ethereum) {
return this.blockchain.createVMAccount(newAccount) // we still add "injected" if there's no provider (just so it's visible to the user).
} await addProvider('injected', 'Injected Provider', true, false)
sendTransaction (tx) {
_paq.push(['trackEvent', 'udapp', 'sendTx', 'udappTransaction'])
return this.blockchain.sendTransaction(tx)
}
getAccounts (cb) {
return this.blockchain.getAccounts(cb)
}
pendingTransactionsCount () {
return this.blockchain.pendingTransactionsCount()
}
render () {
return <div><RunTabUI plugin={this} /></div>
} }
onReady (api) { if (window && window.trustwallet) {
this.REACT_API = api const displayNameInjected = `Injected Provider - TrustWallet`
await addProvider('injected-trustwallet', displayNameInjected, true, false)
} }
async onInitDone () {
const udapp = this // eslint-disable-line
const addProvider = async (name, displayName, isInjected, isVM, fork = '', dataId = '', title = '') => {
await this.call('blockchain', 'addProvider', {
options: {},
dataId,
name,
displayName,
fork,
isInjected,
isVM,
title,
init: async function () {
const options = await udapp.call(name, 'init')
if (options) {
this.options = options
if (options['fork']) this.fork = options['fork']
}
},
provider: {
async sendAsync (payload, callback) {
try {
const result = await udapp.call(name, 'sendAsync', payload)
callback(null, result)
} catch (e) {
callback(e)
}
}
}
})
}
// basic injected
// if it's the trust wallet provider, we have a specific provider for that, see below
if (window && window.ethereum && !(window.ethereum.isTrustWallet || window.ethereum.selectedProvider?.isTrustWallet)) {
const displayNameInjected = `Injected Provider${(window && window.ethereum && !(window.ethereum.providers && !window.ethereum.selectedProvider)) ?
window.ethereum.isCoinbaseWallet || window.ethereum.selectedProvider?.isCoinbaseWallet ? ' - Coinbase' :
window.ethereum.isBraveWallet || window.ethereum.selectedProvider?.isBraveWallet ? ' - Brave' :
window.ethereum.isMetaMask || window.ethereum.selectedProvider?.isMetaMask ? ' - MetaMask' : '' : ''}`
await addProvider('injected', displayNameInjected, true, false)
} else if (window && !window.ethereum) {
// we still add "injected" if there's no provider (just so it's visible to the user).
await addProvider('injected', 'Injected Provider', true, false)
}
if (window && window.trustwallet) {
const displayNameInjected = `Injected Provider - TrustWallet`
await addProvider('injected-trustwallet', displayNameInjected, true, false)
}
// VM // VM
const titleVM = 'Execution environment is local to Remix. Data is only saved to browser memory and will vanish upon reload.' const titleVM = 'Execution environment is local to Remix. Data is only saved to browser memory and will vanish upon reload.'
await addProvider('vm-shanghai', 'Remix VM (Shanghai)', false, true, 'shanghai', 'settingsVMShanghaiMode', titleVM) await addProvider('vm-shanghai', 'Remix VM (Shanghai)', false, true, 'shanghai', 'settingsVMShanghaiMode', titleVM)
await addProvider('vm-merge', 'Remix VM (Merge)', false, true, 'merge', 'settingsVMMergeMode', titleVM) await addProvider('vm-merge', 'Remix VM (Merge)', false, true, 'merge', 'settingsVMMergeMode', titleVM)
await addProvider('vm-london', 'Remix VM (London)', false, true, 'london', 'settingsVMLondonMode', titleVM) await addProvider('vm-london', 'Remix VM (London)', false, true, 'london', 'settingsVMLondonMode', titleVM)
await addProvider('vm-berlin', 'Remix VM (Berlin)', false, true, 'berlin', 'settingsVMBerlinMode', titleVM) await addProvider('vm-berlin', 'Remix VM (Berlin)', false, true, 'berlin', 'settingsVMBerlinMode', titleVM)
await addProvider('vm-mainnet-fork', 'Remix VM - Mainnet fork', false, true, 'merge', 'settingsVMMainnetMode', titleVM) await addProvider('vm-mainnet-fork', 'Remix VM - Mainnet fork', false, true, 'merge', 'settingsVMMainnetMode', titleVM)
await addProvider('vm-sepolia-fork', 'Remix VM - Sepolia fork', false, true, 'merge', 'settingsVMSepoliaMode', titleVM) await addProvider('vm-sepolia-fork', 'Remix VM - Sepolia fork', false, true, 'merge', 'settingsVMSepoliaMode', titleVM)
await addProvider('vm-goerli-fork', 'Remix VM - Goerli fork', false, true, 'merge', 'settingsVMGoerliMode', titleVM) await addProvider('vm-goerli-fork', 'Remix VM - Goerli fork', false, true, 'merge', 'settingsVMGoerliMode', titleVM)
await addProvider('vm-custom-fork', 'Remix VM - Custom fork', false, true, '', 'settingsVMCustomMode', titleVM) await addProvider('vm-custom-fork', 'Remix VM - Custom fork', false, true, '', 'settingsVMCustomMode', titleVM)
// wallet connect // wallet connect
await addProvider('walletconnect', 'WalletConnect', false, false) await addProvider('walletconnect', 'WalletConnect', false, false)
// external provider // external provider
await addProvider('basic-http-provider', 'Custom - External Http Provider', false, false) await addProvider('basic-http-provider', 'Custom - External Http Provider', false, false)
await addProvider('hardhat-provider', 'Dev - Hardhat Provider', false, false) await addProvider('hardhat-provider', 'Dev - Hardhat Provider', false, false)
await addProvider('ganache-provider', 'Dev - Ganache Provider', false, false) await addProvider('ganache-provider', 'Dev - Ganache Provider', false, false)
await addProvider('foundry-provider', 'Dev - Foundry Provider', false, false) await addProvider('foundry-provider', 'Dev - Foundry Provider', false, false)
// injected provider // injected provider
await addProvider('injected-optimism-provider', 'L2 - Optimism Provider', true, false) await addProvider('injected-optimism-provider', 'L2 - Optimism Provider', true, false)
await addProvider('injected-arbitrum-one-provider', 'L2 - Arbitrum One Provider', true, false) await addProvider('injected-arbitrum-one-provider', 'L2 - Arbitrum One Provider', true, false)
} }
writeFile (fileName, content) { writeFile (fileName, content) {
return this.call('fileManager', 'writeFile', fileName, content) return this.call('fileManager', 'writeFile', fileName, content)
} }
readFile (fileName) { readFile (fileName) {
return this.call('fileManager', 'readFile', fileName) return this.call('fileManager', 'readFile', fileName)
} }
resolveContractAndAddInstance (contractObject, address) { resolveContractAndAddInstance (contractObject, address) {
const data = this.compilersArtefacts.getCompilerAbstract(contractObject.contract.file) const data = this.compilersArtefacts.getCompilerAbstract(contractObject.contract.file)
this.compilersArtefacts.addResolvedContract(addressToString(address), data) this.compilersArtefacts.addResolvedContract(addressToString(address), data)
this.addInstance(address, contractObject.abi, contractObject.name) this.addInstance(address, contractObject.abi, contractObject.name)
} }
} }

@ -5,33 +5,33 @@ import { ViewPlugin } from '@remixproject/engine-web'
import { RemixUiHomeTab } from '@remix-ui/home-tab' // eslint-disable-line import { RemixUiHomeTab } from '@remix-ui/home-tab' // eslint-disable-line
const profile = { const profile = {
name: 'home', name: 'home',
displayName: 'Home', displayName: 'Home',
methods: [], methods: [],
events: [], events: [],
description: 'Remix Home', description: 'Remix Home',
icon: 'assets/img/home.webp', icon: 'assets/img/home.webp',
location: 'mainPanel', location: 'mainPanel',
version: packageJson.version version: packageJson.version
} }
export class LandingPage extends ViewPlugin { export class LandingPage extends ViewPlugin {
constructor (appManager, verticalIcons, fileManager, filePanel, contentImport) { constructor (appManager, verticalIcons, fileManager, filePanel, contentImport) {
super(profile) super(profile)
this.profile = profile this.profile = profile
this.fileManager = fileManager this.fileManager = fileManager
this.filePanel = filePanel this.filePanel = filePanel
this.contentImport = contentImport this.contentImport = contentImport
this.appManager = appManager this.appManager = appManager
this.verticalIcons = verticalIcons this.verticalIcons = verticalIcons
this.el = document.createElement('div') this.el = document.createElement('div')
this.el.setAttribute('id', 'landingPageHomeContainer') this.el.setAttribute('id', 'landingPageHomeContainer')
this.el.setAttribute('class', 'remixui_homeContainer justify-content-between bg-light d-flex') this.el.setAttribute('class', 'remixui_homeContainer justify-content-between bg-light d-flex')
this.el.setAttribute('data-id', 'landingPageHomeContainer') this.el.setAttribute('data-id', 'landingPageHomeContainer')
} }
render () { render () {
return <div id='landingPageHomeContainer' className='remixui_homeContainer justify-content-between bg-light d-flex' data-id='landingPageHomeContainer'> return <div id='landingPageHomeContainer' className='remixui_homeContainer justify-content-between bg-light d-flex' data-id='landingPageHomeContainer'>
<RemixUiHomeTab plugin={this} /> <RemixUiHomeTab plugin={this} />
</div> </div>
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

@ -8,188 +8,188 @@ const _paq = window._paq = window._paq || []
let web3 let web3
if (typeof window !== 'undefined' && typeof window.ethereum !== 'undefined') { if (typeof window !== 'undefined' && typeof window.ethereum !== 'undefined') {
var injectedProvider = window.ethereum var injectedProvider = window.ethereum
web3 = new Web3(injectedProvider) web3 = new Web3(injectedProvider)
} else { } else {
web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'))
} }
/* /*
trigger contextChanged, web3EndpointChanged trigger contextChanged, web3EndpointChanged
*/ */
export class ExecutionContext { export class ExecutionContext {
constructor () { constructor () {
this.event = new EventManager() this.event = new EventManager()
this.executionContext = 'vm-shanghai' this.executionContext = 'vm-shanghai'
this.lastBlock = null this.lastBlock = null
this.blockGasLimitDefault = 4300000 this.blockGasLimitDefault = 4300000
this.blockGasLimit = this.blockGasLimitDefault this.blockGasLimit = this.blockGasLimitDefault
this.currentFork = 'shanghai' this.currentFork = 'shanghai'
this.mainNetGenesisHash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3' this.mainNetGenesisHash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3'
this.customNetWorks = {} this.customNetWorks = {}
this.blocks = {} this.blocks = {}
this.latestBlockNumber = 0 this.latestBlockNumber = 0
this.txs = {} this.txs = {}
this.customWeb3 = {} // mapping between a context name and a web3.js instance this.customWeb3 = {} // mapping between a context name and a web3.js instance
} }
init (config) { init (config) {
this.executionContext = 'vm-shanghai' this.executionContext = 'vm-shanghai'
this.event.trigger('contextChanged', [this.executionContext]) this.event.trigger('contextChanged', [this.executionContext])
} }
getProvider () { getProvider () {
return this.executionContext return this.executionContext
} }
getProviderObject () { getProviderObject () {
return this.customNetWorks[this.executionContext] return this.customNetWorks[this.executionContext]
} }
getSelectedAddress () { getSelectedAddress () {
return injectedProvider ? injectedProvider.selectedAddress : null return injectedProvider ? injectedProvider.selectedAddress : null
} }
getCurrentFork () { getCurrentFork () {
return this.currentFork return this.currentFork
} }
isVM () { isVM () {
return this.executionContext.startsWith('vm') return this.executionContext.startsWith('vm')
} }
setWeb3 (context, web3) { setWeb3 (context, web3) {
this.customWeb3[context] = web3 this.customWeb3[context] = web3
} }
web3 () { web3 () {
if (this.customWeb3[this.executionContext]) return this.customWeb3[this.executionContext] if (this.customWeb3[this.executionContext]) return this.customWeb3[this.executionContext]
return web3 return web3
} }
detectNetwork (callback) { detectNetwork (callback) {
if (this.isVM()) { if (this.isVM()) {
callback(null, { id: '-', name: 'VM' }) callback(null, { id: '-', name: 'VM' })
} else {
if (!web3.currentProvider) {
return callback('No provider set')
}
web3.eth.net.getId((err, id) => {
let name = null
if (err) name = 'Unknown'
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
else if (id === 1) name = 'Main'
else if (id === 3) name = 'Ropsten'
else if (id === 4) name = 'Rinkeby'
else if (id === 5) name = 'Goerli'
else if (id === 42) name = 'Kovan'
else if (id === 11155111) name = 'Sepolia'
else name = 'Custom'
if (id === '1') {
web3.eth.getBlock(0, (error, block) => {
if (error) console.log('cant query first block')
if (block && block.hash !== this.mainNetGenesisHash) name = 'Custom'
callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
})
} else { } else {
if (!web3.currentProvider) { callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
return callback('No provider set')
}
web3.eth.net.getId((err, id) => {
let name = null
if (err) name = 'Unknown'
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
else if (id === 1) name = 'Main'
else if (id === 3) name = 'Ropsten'
else if (id === 4) name = 'Rinkeby'
else if (id === 5) name = 'Goerli'
else if (id === 42) name = 'Kovan'
else if (id === 11155111) name = 'Sepolia'
else name = 'Custom'
if (id === '1') {
web3.eth.getBlock(0, (error, block) => {
if (error) console.log('cant query first block')
if (block && block.hash !== this.mainNetGenesisHash) name = 'Custom'
callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
})
} else {
callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
}
})
} }
})
} }
}
removeProvider (name) { removeProvider (name) {
if (name && this.customNetWorks[name]) { if (name && this.customNetWorks[name]) {
if (this.executionContext === name) this.setContext('vm-merge', null, null, null) if (this.executionContext === name) this.setContext('vm-merge', null, null, null)
delete this.customNetWorks[name] delete this.customNetWorks[name]
this.event.trigger('removeProvider', [name]) this.event.trigger('removeProvider', [name])
}
} }
}
addProvider (network) { addProvider (network) {
if (network && network.name && !this.customNetWorks[network.name]) { if (network && network.name && !this.customNetWorks[network.name]) {
this.customNetWorks[network.name] = network this.customNetWorks[network.name] = network
this.event.trigger('addProvider', [network]) this.event.trigger('addProvider', [network])
}
} }
}
internalWeb3 () { internalWeb3 () {
return web3 return web3
} }
setContext (context, endPointUrl, confirmCb, infoCb) { setContext (context, endPointUrl, confirmCb, infoCb) {
this.executionContext = context this.executionContext = context
this.executionContextChange(context, endPointUrl, confirmCb, infoCb, null) this.executionContextChange(context, endPointUrl, confirmCb, infoCb, null)
} }
async executionContextChange (value, endPointUrl, confirmCb, infoCb, cb) { async executionContextChange (value, endPointUrl, confirmCb, infoCb, cb) {
_paq.push(['trackEvent', 'udapp', 'providerChanged', value.context]) _paq.push(['trackEvent', 'udapp', 'providerChanged', value.context])
const context = value.context const context = value.context
if (!cb) cb = () => { /* Do nothing. */ } if (!cb) cb = () => { /* Do nothing. */ }
if (!confirmCb) confirmCb = () => { /* Do nothing. */ } if (!confirmCb) confirmCb = () => { /* Do nothing. */ }
if (!infoCb) infoCb = () => { /* Do nothing. */ } if (!infoCb) infoCb = () => { /* Do nothing. */ }
if (this.customNetWorks[context]) { if (this.customNetWorks[context]) {
var network = this.customNetWorks[context] var network = this.customNetWorks[context]
await network.init() await network.init()
this.currentFork = network.fork this.currentFork = network.fork
this.executionContext = context this.executionContext = context
// injected // injected
web3.setProvider(network.provider) web3.setProvider(network.provider)
await this._updateChainContext() await this._updateChainContext()
this.event.trigger('contextChanged', [context]) this.event.trigger('contextChanged', [context])
cb() cb()
}
}
currentblockGasLimit () {
return this.blockGasLimit
}
stopListenOnLastBlock () {
if (this.listenOnLastBlockId) clearInterval(this.listenOnLastBlockId)
this.listenOnLastBlockId = null
}
async _updateChainContext () {
if (!this.isVM()) {
try {
const block = await web3.eth.getBlock('latest')
// we can't use the blockGasLimit cause the next blocks could have a lower limit : https://github.com/ethereum/remix/issues/506
this.blockGasLimit = (block && block.gasLimit) ? Math.floor(block.gasLimit - (5 * block.gasLimit) / 1024) : this.blockGasLimitDefault
this.lastBlock = block
try {
this.currentFork = execution.forkAt(await web3.eth.net.getId(), block.number)
} catch (e) {
this.currentFork = 'merge'
console.log(`unable to detect fork, defaulting to ${this.currentFork}..`)
console.error(e)
} }
} catch (e) {
console.error(e)
this.blockGasLimit = this.blockGasLimitDefault
}
} }
}
currentblockGasLimit () { listenOnLastBlock () {
return this.blockGasLimit this.listenOnLastBlockId = setInterval(() => {
} this._updateChainContext()
}, 15000)
stopListenOnLastBlock () { }
if (this.listenOnLastBlockId) clearInterval(this.listenOnLastBlockId)
this.listenOnLastBlockId = null
}
async _updateChainContext () {
if (!this.isVM()) {
try {
const block = await web3.eth.getBlock('latest')
// we can't use the blockGasLimit cause the next blocks could have a lower limit : https://github.com/ethereum/remix/issues/506
this.blockGasLimit = (block && block.gasLimit) ? Math.floor(block.gasLimit - (5 * block.gasLimit) / 1024) : this.blockGasLimitDefault
this.lastBlock = block
try {
this.currentFork = execution.forkAt(await web3.eth.net.getId(), block.number)
} catch (e) {
this.currentFork = 'merge'
console.log(`unable to detect fork, defaulting to ${this.currentFork}..`)
console.error(e)
}
} catch (e) {
console.error(e)
this.blockGasLimit = this.blockGasLimitDefault
}
}
}
listenOnLastBlock () { txDetailsLink (network, hash) {
this.listenOnLastBlockId = setInterval(() => { const transactionDetailsLinks = {
this._updateChainContext() Main: 'https://www.etherscan.io/tx/',
}, 15000) Rinkeby: 'https://rinkeby.etherscan.io/tx/',
Ropsten: 'https://ropsten.etherscan.io/tx/',
Sepolia: 'https://sepolia.etherscan.io/tx/',
Kovan: 'https://kovan.etherscan.io/tx/',
Goerli: 'https://goerli.etherscan.io/tx/'
} }
txDetailsLink (network, hash) { if (transactionDetailsLinks[network]) {
const transactionDetailsLinks = { return transactionDetailsLinks[network] + hash
Main: 'https://www.etherscan.io/tx/',
Rinkeby: 'https://rinkeby.etherscan.io/tx/',
Ropsten: 'https://ropsten.etherscan.io/tx/',
Sepolia: 'https://sepolia.etherscan.io/tx/',
Kovan: 'https://kovan.etherscan.io/tx/',
Goerli: 'https://goerli.etherscan.io/tx/'
}
if (transactionDetailsLinks[network]) {
return transactionDetailsLinks[network] + hash
}
} }
}
} }

@ -1,14 +1,14 @@
const transactionDetailsLinks = { const transactionDetailsLinks = {
Main: 'https://www.etherscan.io/tx/', Main: 'https://www.etherscan.io/tx/',
Rinkeby: 'https://rinkeby.etherscan.io/tx/', Rinkeby: 'https://rinkeby.etherscan.io/tx/',
Ropsten: 'https://ropsten.etherscan.io/tx/', Ropsten: 'https://ropsten.etherscan.io/tx/',
Kovan: 'https://kovan.etherscan.io/tx/', Kovan: 'https://kovan.etherscan.io/tx/',
Goerli: 'https://goerli.etherscan.io/tx/', Goerli: 'https://goerli.etherscan.io/tx/',
Sepolia: 'https://sepolia.etherscan.io/tx/' Sepolia: 'https://sepolia.etherscan.io/tx/'
} }
export function etherScanLink (network: string, hash: string): string { export function etherScanLink (network: string, hash: string): string {
if (transactionDetailsLinks[network]) { if (transactionDetailsLinks[network]) {
return transactionDetailsLinks[network] + hash return transactionDetailsLinks[network] + hash
} }
} }

@ -3,47 +3,47 @@ import { hashPersonalMessage } from '@ethereumjs/util'
import { ExecutionContext } from '../execution-context' import { ExecutionContext } from '../execution-context'
export class InjectedProvider { export class InjectedProvider {
executionContext: ExecutionContext executionContext: ExecutionContext
constructor (executionContext) { constructor (executionContext) {
this.executionContext = executionContext this.executionContext = executionContext
} }
getAccounts (cb) { getAccounts (cb) {
return this.executionContext.web3().eth.getAccounts(cb) return this.executionContext.web3().eth.getAccounts(cb)
} }
newAccount (passwordPromptCb, cb) { newAccount (passwordPromptCb, cb) {
passwordPromptCb((passphrase) => { passwordPromptCb((passphrase) => {
this.executionContext.web3().eth.personal.newAccount(passphrase, cb) this.executionContext.web3().eth.personal.newAccount(passphrase, cb)
}) })
} }
async resetEnvironment () { async resetEnvironment () {
/* Do nothing. */ /* Do nothing. */
} }
async getBalanceInEther (address) { async getBalanceInEther (address) {
const balance = await this.executionContext.web3().eth.getBalance(address) const balance = await this.executionContext.web3().eth.getBalance(address)
return Web3.utils.fromWei(balance.toString(10), 'ether') return Web3.utils.fromWei(balance.toString(10), 'ether')
} }
getGasPrice (cb) { getGasPrice (cb) {
this.executionContext.web3().eth.getGasPrice(cb) this.executionContext.web3().eth.getGasPrice(cb)
} }
signMessage (message, account, _passphrase, cb) { signMessage (message, account, _passphrase, cb) {
const messageHash = hashPersonalMessage(Buffer.from(message)) const messageHash = hashPersonalMessage(Buffer.from(message))
try { try {
this.executionContext.web3().eth.personal.sign(message, account, (error, signedData) => { this.executionContext.web3().eth.personal.sign(message, account, (error, signedData) => {
cb(error, '0x' + messageHash.toString('hex'), signedData) cb(error, '0x' + messageHash.toString('hex'), signedData)
}) })
} catch (e) { } catch (e) {
cb(e.message) cb(e.message)
} }
} }
getProvider () { getProvider () {
return 'injected' return 'injected'
} }
} }

@ -5,56 +5,56 @@ import { ExecutionContext } from '../execution-context'
import Config from '../../config' import Config from '../../config'
export class NodeProvider { export class NodeProvider {
executionContext: ExecutionContext executionContext: ExecutionContext
config: Config config: Config
constructor (executionContext: ExecutionContext, config: Config) { constructor (executionContext: ExecutionContext, config: Config) {
this.executionContext = executionContext this.executionContext = executionContext
this.config = config this.config = config
} }
getAccounts (cb) { getAccounts (cb) {
if (this.config.get('settings/personal-mode')) { if (this.config.get('settings/personal-mode')) {
return this.executionContext.web3().eth.personal.getAccounts(cb) return this.executionContext.web3().eth.personal.getAccounts(cb)
} }
return this.executionContext.web3().eth.getAccounts(cb) return this.executionContext.web3().eth.getAccounts(cb)
} }
newAccount (passwordPromptCb, cb) { newAccount (passwordPromptCb, cb) {
if (!this.config.get('settings/personal-mode')) { if (!this.config.get('settings/personal-mode')) {
return cb('Not running in personal mode') return cb('Not running in personal mode')
} }
passwordPromptCb((passphrase) => { passwordPromptCb((passphrase) => {
this.executionContext.web3().eth.personal.newAccount(passphrase, cb) this.executionContext.web3().eth.personal.newAccount(passphrase, cb)
}) })
} }
async resetEnvironment () { async resetEnvironment () {
/* Do nothing. */ /* Do nothing. */
} }
async getBalanceInEther (address) { async getBalanceInEther (address) {
const balance = await this.executionContext.web3().eth.getBalance(address) const balance = await this.executionContext.web3().eth.getBalance(address)
return Web3.utils.fromWei(balance.toString(10), 'ether') return Web3.utils.fromWei(balance.toString(10), 'ether')
} }
getGasPrice (cb) { getGasPrice (cb) {
this.executionContext.web3().eth.getGasPrice(cb) this.executionContext.web3().eth.getGasPrice(cb)
} }
signMessage (message, account, passphrase, cb) { signMessage (message, account, passphrase, cb) {
const messageHash = hashPersonalMessage(Buffer.from(message)) const messageHash = hashPersonalMessage(Buffer.from(message))
try { try {
const personal = new Personal(this.executionContext.web3().currentProvider) const personal = new Personal(this.executionContext.web3().currentProvider)
personal.sign(message, account, passphrase, (error, signedData) => { personal.sign(message, account, passphrase, (error, signedData) => {
cb(error, '0x' + messageHash.toString('hex'), signedData) cb(error, '0x' + messageHash.toString('hex'), signedData)
}) })
} catch (e) { } catch (e) {
cb(e.message) cb(e.message)
} }
} }
getProvider () { getProvider () {
return this.executionContext.getProvider() return this.executionContext.getProvider()
} }
} }

@ -5,105 +5,105 @@ import { extend, JSONRPCRequestPayload, JSONRPCResponseCallback } from '@remix-p
import { ExecutionContext } from '../execution-context' import { ExecutionContext } from '../execution-context'
export class VMProvider { export class VMProvider {
executionContext: ExecutionContext executionContext: ExecutionContext
web3: Web3 web3: Web3
worker: Worker worker: Worker
provider: { provider: {
sendAsync: (query: JSONRPCRequestPayload, callback: JSONRPCResponseCallback) => void sendAsync: (query: JSONRPCRequestPayload, callback: JSONRPCResponseCallback) => void
} }
newAccountCallback: {[stamp: number]: (error: Error, address: string) => void} newAccountCallback: {[stamp: number]: (error: Error, address: string) => void}
constructor (executionContext: ExecutionContext) { constructor (executionContext: ExecutionContext) {
this.executionContext = executionContext this.executionContext = executionContext
this.worker = null this.worker = null
this.provider = null this.provider = null
this.newAccountCallback = {} this.newAccountCallback = {}
} }
getAccounts (cb) { getAccounts (cb) {
this.web3.eth.getAccounts((err, accounts) => { this.web3.eth.getAccounts((err, accounts) => {
if (err) { if (err) {
return cb('No accounts?') return cb('No accounts?')
} }
return cb(null, accounts) return cb(null, accounts)
}) })
} }
async resetEnvironment () { async resetEnvironment () {
if (this.worker) this.worker.terminate() if (this.worker) this.worker.terminate()
this.worker = new Worker(new URL('./worker-vm', import.meta.url)) this.worker = new Worker(new URL('./worker-vm', import.meta.url))
const provider = this.executionContext.getProviderObject() const provider = this.executionContext.getProviderObject()
let incr = 0 let incr = 0
const stamps = {} const stamps = {}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.worker.addEventListener('message', (msg) => { this.worker.addEventListener('message', (msg) => {
if (msg.data.cmd === 'sendAsyncResult' && stamps[msg.data.stamp]) { if (msg.data.cmd === 'sendAsyncResult' && stamps[msg.data.stamp]) {
stamps[msg.data.stamp](msg.data.error, msg.data.result) stamps[msg.data.stamp](msg.data.error, msg.data.result)
} else if (msg.data.cmd === 'initiateResult') { } else if (msg.data.cmd === 'initiateResult') {
if (!msg.data.error) { if (!msg.data.error) {
this.provider = { this.provider = {
sendAsync: (query, callback) => { sendAsync: (query, callback) => {
const stamp = Date.now() + incr const stamp = Date.now() + incr
incr++ incr++
stamps[stamp] = callback stamps[stamp] = callback
this.worker.postMessage({ cmd: 'sendAsync', query, stamp }) this.worker.postMessage({ cmd: 'sendAsync', query, stamp })
} }
} }
this.web3 = new Web3(this.provider) this.web3 = new Web3(this.provider)
extend(this.web3) extend(this.web3)
this.executionContext.setWeb3(this.executionContext.getProvider(), this.web3) this.executionContext.setWeb3(this.executionContext.getProvider(), this.web3)
resolve({}) resolve({})
} else { } else {
reject(new Error(msg.data.error)) reject(new Error(msg.data.error))
} }
} else if (msg.data.cmd === 'newAccountResult') { } else if (msg.data.cmd === 'newAccountResult') {
if (this.newAccountCallback[msg.data.stamp]) { if (this.newAccountCallback[msg.data.stamp]) {
this.newAccountCallback[msg.data.stamp](msg.data.error, msg.data.result) this.newAccountCallback[msg.data.stamp](msg.data.error, msg.data.result)
delete this.newAccountCallback[msg.data.stamp] delete this.newAccountCallback[msg.data.stamp]
} }
} }
}) })
this.worker.postMessage({ cmd: 'init', fork: this.executionContext.getCurrentFork(), nodeUrl: provider?.options['nodeUrl'], blockNumber: provider?.options['blockNumber']}) this.worker.postMessage({ cmd: 'init', fork: this.executionContext.getCurrentFork(), nodeUrl: provider?.options['nodeUrl'], blockNumber: provider?.options['blockNumber']})
}) })
} }
// TODO: is still here because of the plugin API // TODO: is still here because of the plugin API
// can be removed later when we update the API // can be removed later when we update the API
createVMAccount (newAccount) { createVMAccount (newAccount) {
const { privateKey, balance } = newAccount const { privateKey, balance } = newAccount
this.worker.postMessage({ cmd: 'addAccount', privateKey: privateKey, balance }) this.worker.postMessage({ cmd: 'addAccount', privateKey: privateKey, balance })
const privKey = Buffer.from(privateKey, 'hex') const privKey = Buffer.from(privateKey, 'hex')
return '0x' + privateToAddress(privKey).toString('hex') return '0x' + privateToAddress(privKey).toString('hex')
} }
newAccount (_passwordPromptCb, cb) { newAccount (_passwordPromptCb, cb) {
const stamp = Date.now() const stamp = Date.now()
this.newAccountCallback[stamp] = cb this.newAccountCallback[stamp] = cb
this.worker.postMessage({ cmd: 'newAccount', stamp }) this.worker.postMessage({ cmd: 'newAccount', stamp })
} }
async getBalanceInEther (address) { async getBalanceInEther (address) {
const balance = await this.web3.eth.getBalance(address) const balance = await this.web3.eth.getBalance(address)
return Web3.utils.fromWei(new BN(balance).toString(10), 'ether') return Web3.utils.fromWei(new BN(balance).toString(10), 'ether')
} }
getGasPrice (cb) { getGasPrice (cb) {
this.web3.eth.getGasPrice(cb) this.web3.eth.getGasPrice(cb)
} }
signMessage (message, account, _passphrase, cb) { signMessage (message, account, _passphrase, cb) {
const messageHash = hashPersonalMessage(Buffer.from(message)) const messageHash = hashPersonalMessage(Buffer.from(message))
this.web3.eth.sign(message, account, (error, signedData) => { this.web3.eth.sign(message, account, (error, signedData) => {
if (error) { if (error) {
return cb(error) return cb(error)
} }
cb(null, '0x' + messageHash.toString('hex'), signedData) cb(null, '0x' + messageHash.toString('hex'), signedData)
}) })
} }
getProvider () { getProvider () {
return this.executionContext.getProvider() return this.executionContext.getProvider()
} }
} }

@ -2,76 +2,76 @@ import { Provider } from '@remix-project/remix-simulator'
let provider: Provider = null let provider: Provider = null
self.onmessage = (e: MessageEvent) => { self.onmessage = (e: MessageEvent) => {
const data = e.data const data = e.data
switch (data.cmd) { switch (data.cmd) {
case 'init': case 'init':
{ {
provider = new Provider({ fork: data.fork, nodeUrl: data.nodeUrl, blockNumber: data.blockNumber }) provider = new Provider({ fork: data.fork, nodeUrl: data.nodeUrl, blockNumber: data.blockNumber })
provider.init().then(() => { provider.init().then(() => {
self.postMessage({ self.postMessage({
cmd: 'initiateResult', cmd: 'initiateResult',
stamp: data.stamp stamp: data.stamp
}) })
}).catch((error) => { }).catch((error) => {
self.postMessage({ self.postMessage({
cmd: 'initiateResult', cmd: 'initiateResult',
error, error,
stamp: data.stamp stamp: data.stamp
}) })
})
break
}
case 'sendAsync':
{
if (provider) {
provider.sendAsync(data.query, (error, result) => {
self.postMessage({
cmd: 'sendAsyncResult',
error,
result,
stamp: data.stamp
}) })
break })
} else {
self.postMessage({
cmd: 'sendAsyncResult',
error: 'Provider not instantiated',
result: null,
stamp: data.stamp
})
} }
case 'sendAsync':
{
if (provider) {
provider.sendAsync(data.query, (error, result) => {
self.postMessage({
cmd: 'sendAsyncResult',
error,
result,
stamp: data.stamp
})
})
} else {
self.postMessage({
cmd: 'sendAsyncResult',
error: 'Provider not instantiated',
result: null,
stamp: data.stamp
})
}
break break
}
case 'addAccount':
{
if (provider) {
provider.Accounts._addAccount(data.privateKey, data.balance)
} }
case 'addAccount':
{
if (provider) {
provider.Accounts._addAccount(data.privateKey, data.balance)
}
break break
}
case 'newAccount':
{
if (provider) {
provider.Accounts.newAccount((error, address: string) => {
if (error) {
self.postMessage({
cmd: 'newAccountResult',
error,
stamp: data.stamp
})
} else {
self.postMessage({
cmd: 'newAccountResult',
result: address,
stamp: data.stamp
})
}
})
} }
case 'newAccount':
{
if (provider) {
provider.Accounts.newAccount((error, address: string) => {
if (error) {
self.postMessage({
cmd: 'newAccountResult',
error,
stamp: data.stamp
})
} else {
self.postMessage({
cmd: 'newAccountResult',
result: address,
stamp: data.stamp
})
}
})
}
break break
} }
} }
} }

@ -4,53 +4,53 @@ var CONFIG_FILE = '.remix.config'
const EventEmitter = require('events') const EventEmitter = require('events')
function Config(storage) { function Config(storage) {
this.items = {} this.items = {}
this.unpersistedItems = {} this.unpersistedItems = {}
this.events = new EventEmitter() this.events = new EventEmitter()
// load on instantiation // load on instantiation
try { try {
var config = storage.get(CONFIG_FILE) var config = storage.get(CONFIG_FILE)
if (config) { if (config) {
this.items = JSON.parse(config) this.items = JSON.parse(config)
}
} catch (exception) {
/* Do nothing. */
}
this.exists = function (key) {
return this.items[key] !== undefined
}
this.get = function (key) {
return this.items[key]
} }
} catch (exception) {
/* Do nothing. */
}
this.set = function (key, content) { this.exists = function (key) {
this.items[key] = content return this.items[key] !== undefined
try { }
storage.set(CONFIG_FILE, JSON.stringify(this.items))
this.events.emit('configChanged', { key, content })
this.events.emit(key + '_changed', content)
} catch (exception) {
/* Do nothing. */
}
}
this.clear = function () { this.get = function (key) {
this.items = {} return this.items[key]
storage.remove(CONFIG_FILE) }
}
this.getUnpersistedProperty = function (key) { this.set = function (key, content) {
return this.unpersistedItems[key] this.items[key] = content
try {
storage.set(CONFIG_FILE, JSON.stringify(this.items))
this.events.emit('configChanged', { key, content })
this.events.emit(key + '_changed', content)
} catch (exception) {
/* Do nothing. */
} }
}
// TODO: this only used for *one* property "doNotShowTransactionConfirmationAgain" this.clear = function () {
// and can be removed once it's refactored away in txRunner this.items = {}
this.setUnpersistedProperty = function (key, value) { storage.remove(CONFIG_FILE)
this.unpersistedItems[key] = value }
}
this.getUnpersistedProperty = function (key) {
return this.unpersistedItems[key]
}
// TODO: this only used for *one* property "doNotShowTransactionConfirmationAgain"
// and can be removed once it's refactored away in txRunner
this.setUnpersistedProperty = function (key, value) {
this.unpersistedItems[key] = value
}
} }
module.exports = Config module.exports = Config

@ -9,20 +9,20 @@ import Registry from './app/state/registry'
import { Storage } from '@remix-project/remix-lib' import { Storage } from '@remix-project/remix-lib'
(async function () { (async function () {
try { try {
const configStorage = new Storage('config-v0.8:') const configStorage = new Storage('config-v0.8:')
const config = new Config(configStorage); const config = new Config(configStorage);
Registry.getInstance().put({ api: config, name: 'config' }) Registry.getInstance().put({ api: config, name: 'config' })
} catch (e) { } } catch (e) { }
const theme = new ThemeModule() const theme = new ThemeModule()
theme.initTheme() theme.initTheme()
render( render(
<React.StrictMode> <React.StrictMode>
<Preload></Preload> <Preload></Preload>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
) )
})() })()

@ -2,153 +2,153 @@ var async = require('async')
import { toChecksumAddress } from '@ethereumjs/util' import { toChecksumAddress } from '@ethereumjs/util'
export default { export default {
shortenAddress: function (address, etherBalance) { shortenAddress: function (address, etherBalance) {
var len = address.length var len = address.length
return address.slice(0, 5) + '...' + address.slice(len - 5, len) + (etherBalance ? ' (' + etherBalance.toString() + ' ether)' : '') return address.slice(0, 5) + '...' + address.slice(len - 5, len) + (etherBalance ? ' (' + etherBalance.toString() + ' ether)' : '')
}, },
addressToString: function (address) { addressToString: function (address) {
if (!address) return null if (!address) return null
if (typeof address !== 'string') { if (typeof address !== 'string') {
address = address.toString('hex') address = address.toString('hex')
} }
if (address.indexOf('0x') === -1) { if (address.indexOf('0x') === -1) {
address = '0x' + address address = '0x' + address
} }
return toChecksumAddress(address) return toChecksumAddress(address)
}, },
shortenHexData: function (data) { shortenHexData: function (data) {
if (!data) return '' if (!data) return ''
if (data.length < 5) return data if (data.length < 5) return data
var len = data.length var len = data.length
return data.slice(0, 5) + '...' + data.slice(len - 5, len) return data.slice(0, 5) + '...' + data.slice(len - 5, len)
}, },
createNonClashingNameWithPrefix (name, fileProvider, prefix, cb) { createNonClashingNameWithPrefix (name, fileProvider, prefix, cb) {
if (!name) name = 'Undefined' if (!name) name = 'Undefined'
var counter = '' var counter = ''
var ext = 'sol' var ext = 'sol'
var reg = /(.*)\.([^.]+)/g var reg = /(.*)\.([^.]+)/g
var split = reg.exec(name) var split = reg.exec(name)
if (split) { if (split) {
name = split[1] name = split[1]
ext = split[2] ext = split[2]
} }
var exist = true var exist = true
async.whilst( async.whilst(
() => { return exist }, () => { return exist },
(callback) => { (callback) => {
fileProvider.exists(name + counter + prefix + '.' + ext).then(currentExist => { fileProvider.exists(name + counter + prefix + '.' + ext).then(currentExist => {
exist = currentExist exist = currentExist
if (exist) counter = (counter | 0) + 1 if (exist) counter = (counter | 0) + 1
callback() callback()
}).catch(error => { }).catch(error => {
if (error) console.log(error) if (error) console.log(error)
}) })
}, },
(error) => { cb(error, name + counter + prefix + '.' + ext) } (error) => { cb(error, name + counter + prefix + '.' + ext) }
) )
}, },
createNonClashingName (name, fileProvider, cb) { createNonClashingName (name, fileProvider, cb) {
this.createNonClashingNameWithPrefix(name, fileProvider, '', cb) this.createNonClashingNameWithPrefix(name, fileProvider, '', cb)
}, },
async createNonClashingNameAsync (name, fileManager, prefix = '') { async createNonClashingNameAsync (name, fileManager, prefix = '') {
if (!name) name = 'Undefined' if (!name) name = 'Undefined'
let counter = '' let counter = ''
let ext = 'sol' let ext = 'sol'
const reg = /(.*)\.([^.]+)/g const reg = /(.*)\.([^.]+)/g
const split = reg.exec(name) const split = reg.exec(name)
if (split) { if (split) {
name = split[1] name = split[1]
ext = split[2] ext = split[2]
} }
let exist = true let exist = true
do { do {
const isDuplicate = await fileManager.exists(name + counter + prefix + '.' + ext) const isDuplicate = await fileManager.exists(name + counter + prefix + '.' + ext)
if (isDuplicate) counter = (counter | 0) + 1 if (isDuplicate) counter = (counter | 0) + 1
else exist = false else exist = false
} while (exist) } while (exist)
return name + counter + prefix + '.' + ext return name + counter + prefix + '.' + ext
}, },
async createNonClashingDirNameAsync (name, fileManager) { async createNonClashingDirNameAsync (name, fileManager) {
if (!name) name = 'Undefined' if (!name) name = 'Undefined'
let counter = '' let counter = ''
let exist = true let exist = true
do { do {
const isDuplicate = await fileManager.exists(name + counter) const isDuplicate = await fileManager.exists(name + counter)
if (isDuplicate) counter = (counter | 0) + 1 if (isDuplicate) counter = (counter | 0) + 1
else exist = false else exist = false
} while (exist) } while (exist)
return name + counter return name + counter
}, },
checkSpecialChars (name) { checkSpecialChars (name) {
return name.match(/[:*?"<>\\'|]/) != null return name.match(/[:*?"<>\\'|]/) != null
}, },
checkSlash (name) { checkSlash (name) {
return name.match(/\//) != null return name.match(/\//) != null
}, },
isHexadecimal (value) { isHexadecimal (value) {
return /^[0-9a-fA-F]+$/.test(value) && (value.length % 2 === 0) return /^[0-9a-fA-F]+$/.test(value) && (value.length % 2 === 0)
}, },
is0XPrefixed (value) { is0XPrefixed (value) {
return value.substr(0, 2) === '0x' return value.substr(0, 2) === '0x'
}, },
isNumeric (value) { isNumeric (value) {
return /^\+?(0|[1-9]\d*)$/.test(value) return /^\+?(0|[1-9]\d*)$/.test(value)
}, },
isValidHash (hash) { // 0x prefixed, hexadecimal, 64digit isValidHash (hash) { // 0x prefixed, hexadecimal, 64digit
const hexValue = hash.slice(2, hash.length) const hexValue = hash.slice(2, hash.length)
return this.is0XPrefixed(hash) && /^[0-9a-fA-F]{64}$/.test(hexValue) return this.is0XPrefixed(hash) && /^[0-9a-fA-F]{64}$/.test(hexValue)
}, },
removeTrailingSlashes (text) { removeTrailingSlashes (text) {
// Remove single or consecutive trailing slashes // Remove single or consecutive trailing slashes
return text.replace(/\/+$/g, '') return text.replace(/\/+$/g, '')
}, },
removeMultipleSlashes (text) { removeMultipleSlashes (text) {
// Replace consecutive slashes with '/' // Replace consecutive slashes with '/'
return text.replace(/\/+/g, '/') return text.replace(/\/+/g, '/')
}, },
find: find, find: find,
joinPath (...paths) { joinPath (...paths) {
paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash) paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash)
if (paths.length === 1) return paths[0] if (paths.length === 1) return paths[0]
return paths.join('/') return paths.join('/')
}, },
extractNameFromKey (key) { extractNameFromKey (key) {
const keyPath = key.split('/') const keyPath = key.split('/')
return keyPath[keyPath.length - 1] return keyPath[keyPath.length - 1]
} }
} }
function findDeep (object, fn, found = { break: false, value: undefined }) { function findDeep (object, fn, found = { break: false, value: undefined }) {
if (typeof object !== 'object' || object === null) return if (typeof object !== 'object' || object === null) return
for (var i in object) { for (var i in object) {
if (found.break) break if (found.break) break
var el = object[i] var el = object[i]
if (el && el.innerText !== undefined && el.innerText !== null) el = el.innerText if (el && el.innerText !== undefined && el.innerText !== null) el = el.innerText
if (fn(el, i, object)) { if (fn(el, i, object)) {
found.value = el found.value = el
found.break = true found.break = true
break break
} else { } else {
findDeep(el, fn, found) findDeep(el, fn, found)
}
} }
return found.value }
return found.value
} }
function find (args, query) { function find (args, query) {
query = query.trim() query = query.trim()
var isMatch = !!findDeep(args, function check (value, key) { var isMatch = !!findDeep(args, function check (value, key) {
if (value === undefined || value === null) return false if (value === undefined || value === null) return false
if (typeof value === 'function') return false if (typeof value === 'function') return false
if (typeof value === 'object') return false if (typeof value === 'object') return false
var contains = String(value).indexOf(query.trim()) !== -1 var contains = String(value).indexOf(query.trim()) !== -1
return contains return contains
}) })
return isMatch return isMatch
} }

@ -1,26 +1,26 @@
// const moduleID = require('./module-id.js') // const moduleID = require('./module-id.js')
module.exports = class registry { module.exports = class registry {
constructor () { constructor () {
this.state = {} this.state = {}
} }
put ({ api, name }) { put ({ api, name }) {
// const serveruid = moduleID() + '.' + (name || '') // const serveruid = moduleID() + '.' + (name || '')
if (this.state[name]) return this.state[name] if (this.state[name]) return this.state[name]
const server = { const server = {
// uid: serveruid, // uid: serveruid,
api api
}
this.state[name] = { server }
return server
} }
this.state[name] = { server }
return server
}
get (name) { get (name) {
// const clientuid = moduleID() // const clientuid = moduleID()
const state = this.state[name] const state = this.state[name]
if (!state) return if (!state) return
const server = state.server const server = state.server
return server return server
} }
} }

@ -6,13 +6,13 @@ const _paq = window._paq = window._paq || []
// requiredModule removes the plugin from the plugin manager list on UI // requiredModule removes the plugin from the plugin manager list on UI
const requiredModules = [ // services + layout views + system views const requiredModules = [ // services + layout views + system views
'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme', 'locale', 'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme', 'locale',
'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons', 'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons',
'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity', 'solidity-logic', 'gistHandler', 'layout', 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity', 'solidity-logic', 'gistHandler', 'layout',
'notification', 'permissionhandler', 'walkthrough', 'storage', 'restorebackupzip', 'link-libraries', 'deploy-libraries', 'openzeppelin-proxy', 'notification', 'permissionhandler', 'walkthrough', 'storage', 'restorebackupzip', 'link-libraries', 'deploy-libraries', 'openzeppelin-proxy',
'hardhat-provider', 'ganache-provider', 'foundry-provider', 'basic-http-provider', 'injected', 'injected-trustwallet', 'injected-optimism-provider', 'injected-arbitrum-one-provider', 'vm-custom-fork', 'vm-goerli-fork', 'vm-mainnet-fork', 'vm-sepolia-fork', 'vm-merge', 'vm-london', 'vm-berlin', 'hardhat-provider', 'ganache-provider', 'foundry-provider', 'basic-http-provider', 'injected', 'injected-trustwallet', 'injected-optimism-provider', 'injected-arbitrum-one-provider', 'vm-custom-fork', 'vm-goerli-fork', 'vm-mainnet-fork', 'vm-sepolia-fork', 'vm-merge', 'vm-london', 'vm-berlin',
'vm-shanghai', 'vm-shanghai',
'compileAndRun', 'search', 'recorder', 'fileDecorator', 'codeParser', 'codeFormatter', 'solidityumlgen', 'contractflattener', 'solidity-script'] 'compileAndRun', 'search', 'recorder', 'fileDecorator', 'codeParser', 'codeFormatter', 'solidityumlgen', 'contractflattener', 'solidity-script']
// dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd) // dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd)
const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither'] const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither']
@ -20,17 +20,17 @@ const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither']
const loadLocalPlugins = ["doc-gen", "doc-viewer", "etherscan", "vyper", 'solhint', 'walletconnect'] const loadLocalPlugins = ["doc-gen", "doc-viewer", "etherscan", "vyper", 'solhint', 'walletconnect']
const sensitiveCalls = { const sensitiveCalls = {
'fileManager': ['writeFile', 'copyFile', 'rename', 'copyDir'], 'fileManager': ['writeFile', 'copyFile', 'rename', 'copyDir'],
'contentImport': ['resolveAndSave'], 'contentImport': ['resolveAndSave'],
'web3Provider': ['sendAsync'], 'web3Provider': ['sendAsync'],
} }
export function isNative(name) { export function isNative(name) {
// nativePlugin allows to bypass the permission request // nativePlugin allows to bypass the permission request
const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'solidity-logic', 'solidityStaticAnalysis', 'solidityUnitTesting', const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'solidity-logic', 'solidityStaticAnalysis', 'solidityUnitTesting',
'layout', 'notification', 'hardhat-provider', 'ganache-provider', 'foundry-provider', 'basic-http-provider', 'injected-optimism-provider', 'layout', 'notification', 'hardhat-provider', 'ganache-provider', 'foundry-provider', 'basic-http-provider', 'injected-optimism-provider',
'tabs', 'injected-arbitrum-one-provider', 'injected', 'doc-gen', 'doc-viewer'] 'tabs', 'injected-arbitrum-one-provider', 'injected', 'doc-gen', 'doc-viewer']
return nativePlugins.includes(name) || requiredModules.includes(name) return nativePlugins.includes(name) || requiredModules.includes(name)
} }
/** /**
@ -44,191 +44,191 @@ export function isNative(name) {
* @returns {boolean} * @returns {boolean}
*/ */
export function canActivate(from, to) { export function canActivate(from, to) {
return ['ethdoc'].includes(from.name) || return ['ethdoc'].includes(from.name) ||
isNative(from.name) || isNative(from.name) ||
(to && from && from.canActivate && from.canActivate.includes(to.name)) (to && from && from.canActivate && from.canActivate.includes(to.name))
} }
export class RemixAppManager extends PluginManager { export class RemixAppManager extends PluginManager {
constructor() { constructor() {
super() super()
this.event = new EventEmitter() this.event = new EventEmitter()
this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json' this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json'
this.pluginLoader = new PluginLoader() this.pluginLoader = new PluginLoader()
} }
async canActivatePlugin(from, to) { async canActivatePlugin(from, to) {
return canActivate(from, to) return canActivate(from, to)
} }
async canDeactivatePlugin(from, to) { async canDeactivatePlugin(from, to) {
if (requiredModules.includes(to.name)) return false if (requiredModules.includes(to.name)) return false
return isNative(from.name) return isNative(from.name)
} }
async canDeactivate(from, to) { async canDeactivate(from, to) {
return this.canDeactivatePlugin(from, to) return this.canDeactivatePlugin(from, to)
} }
async deactivatePlugin(name) { async deactivatePlugin(name) {
const profile = await this.getProfile(name) const profile = await this.getProfile(name)
const [to, from] = [ const [to, from] = [
profile, profile,
await this.getProfile(this.requestFrom) await this.getProfile(this.requestFrom)
] ]
if (this.canDeactivatePlugin(from, to)) { if (this.canDeactivatePlugin(from, to)) {
if (profile.methods.includes('deactivate')) { if (profile.methods.includes('deactivate')) {
try { try {
await this.call(name, 'deactivate') await this.call(name, 'deactivate')
} catch (e) { } catch (e) {
console.log(e) console.log(e)
}
}
await this.toggleActive(name)
} }
}
await this.toggleActive(name)
} }
}
async canCall(from, to, method, message) { async canCall(from, to, method, message) {
const isSensitiveCall = sensitiveCalls[to] && sensitiveCalls[to].includes(method) const isSensitiveCall = sensitiveCalls[to] && sensitiveCalls[to].includes(method)
// Make sure the caller of this methods is the target plugin // Make sure the caller of this methods is the target plugin
if (to !== this.currentRequest.from) { if (to !== this.currentRequest.from) {
return false return false
}
// skipping native plugins' requests
if (isNative(from)) {
return true
}
// ask the user for permission
return await this.call('permissionhandler', 'askPermission', this.profiles[from], this.profiles[to], method, message, isSensitiveCall)
} }
// skipping native plugins' requests
onPluginActivated(plugin) { if (isNative(from)) {
this.pluginLoader.set(plugin, this.actives.filter((plugin) => !this.isDependent(plugin))) return true
this.event.emit('activate', plugin)
this.emit('activate', plugin)
if (!requiredModules.includes(plugin.name)) _paq.push(['trackEvent', 'pluginManager', 'activate', plugin.name])
} }
getAll() { // ask the user for permission
return Object.keys(this.profiles).map((p) => { return await this.call('permissionhandler', 'askPermission', this.profiles[from], this.profiles[to], method, message, isSensitiveCall)
return this.profiles[p] }
})
}
getIds() { onPluginActivated(plugin) {
return Object.keys(this.profiles) this.pluginLoader.set(plugin, this.actives.filter((plugin) => !this.isDependent(plugin)))
} this.event.emit('activate', plugin)
this.emit('activate', plugin)
if (!requiredModules.includes(plugin.name)) _paq.push(['trackEvent', 'pluginManager', 'activate', plugin.name])
}
onPluginDeactivated(plugin) { getAll() {
this.pluginLoader.set(plugin, this.actives.filter((plugin) => !this.isDependent(plugin))) return Object.keys(this.profiles).map((p) => {
this.event.emit('deactivate', plugin) return this.profiles[p]
_paq.push(['trackEvent', 'pluginManager', 'deactivate', plugin.name]) })
} }
isDependent(name) { getIds() {
return dependentModules.includes(name) return Object.keys(this.profiles)
} }
isRequired(name) { onPluginDeactivated(plugin) {
// excluding internal use plugins this.pluginLoader.set(plugin, this.actives.filter((plugin) => !this.isDependent(plugin)))
return requiredModules.includes(name) this.event.emit('deactivate', plugin)
} _paq.push(['trackEvent', 'pluginManager', 'deactivate', plugin.name])
}
isDependent(name) {
return dependentModules.includes(name)
}
async registeredPlugins() { isRequired(name) {
let plugins // excluding internal use plugins
return requiredModules.includes(name)
}
async registeredPlugins() {
let plugins
try {
const res = await fetch(this.pluginsDirectory)
plugins = await res.json()
plugins = plugins.filter((plugin) => {
if (plugin.targets && Array.isArray(plugin.targets) && plugin.targets.length > 0) {
return (plugin.targets.includes('remix'))
}
return true
})
localStorage.setItem('plugins-directory', JSON.stringify(plugins))
} catch (e) {
console.log('getting plugins list from localstorage...')
const savedPlugins = localStorage.getItem('plugins-directory')
if (savedPlugins) {
try { try {
const res = await fetch(this.pluginsDirectory) plugins = JSON.parse(savedPlugins)
plugins = await res.json()
plugins = plugins.filter((plugin) => {
if (plugin.targets && Array.isArray(plugin.targets) && plugin.targets.length > 0) {
return (plugin.targets.includes('remix'))
}
return true
})
localStorage.setItem('plugins-directory', JSON.stringify(plugins))
} catch (e) { } catch (e) {
console.log('getting plugins list from localstorage...') console.error(e)
const savedPlugins = localStorage.getItem('plugins-directory')
if (savedPlugins) {
try {
plugins = JSON.parse(savedPlugins)
} catch (e) {
console.error(e)
}
}
}
const testPluginName = localStorage.getItem('test-plugin-name')
const testPluginUrl = localStorage.getItem('test-plugin-url')
for (let plugin of loadLocalPlugins) {
// fetch the profile from the local plugin
try {
const profile = await fetch(`plugins/${plugin}/profile.json`)
const profileJson = await profile.json()
// remove duplicates
plugins = plugins.filter((p) => p.name !== profileJson.name && p.displayName !== profileJson.displayName)
// change url
profileJson.url = `plugins/${plugin}/index.html`
// add the local plugin
plugins.push(profileJson)
} catch (e) {
console.log(e)
}
} }
}
return plugins.map(plugin => {
if (plugin.name === testPluginName) plugin.url = testPluginUrl
return new IframePlugin(plugin)
})
} }
const testPluginName = localStorage.getItem('test-plugin-name')
const testPluginUrl = localStorage.getItem('test-plugin-url')
async registerContextMenuItems() { for (let plugin of loadLocalPlugins) {
await this.call('filePanel', 'registerContextMenuItem', { // fetch the profile from the local plugin
id: 'contractflattener', try {
name: 'flattenAContract', const profile = await fetch(`plugins/${plugin}/profile.json`)
label: 'Flatten', const profileJson = await profile.json()
type: [], // remove duplicates
extension: ['.sol'], plugins = plugins.filter((p) => p.name !== profileJson.name && p.displayName !== profileJson.displayName)
path: [], // change url
pattern: [], profileJson.url = `plugins/${plugin}/index.html`
sticky: true, // add the local plugin
group: 5 plugins.push(profileJson)
}) } catch (e) {
await this.call('filePanel', 'registerContextMenuItem', { console.log(e)
id: 'nahmii-compiler', }
name: 'compileCustomAction',
label: 'Compile for Nahmii',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 6
})
await this.call('filePanel', 'registerContextMenuItem', {
id: 'solidityumlgen',
name: 'generateCustomAction',
label: 'Generate UML',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 7
})
await this.call('filePanel', 'registerContextMenuItem', {
id: 'doc-gen',
name: 'generateDocsCustomAction',
label: 'Generate Docs',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 7
})
} }
return plugins.map(plugin => {
if (plugin.name === testPluginName) plugin.url = testPluginUrl
return new IframePlugin(plugin)
})
}
async registerContextMenuItems() {
await this.call('filePanel', 'registerContextMenuItem', {
id: 'contractflattener',
name: 'flattenAContract',
label: 'Flatten',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 5
})
await this.call('filePanel', 'registerContextMenuItem', {
id: 'nahmii-compiler',
name: 'compileCustomAction',
label: 'Compile for Nahmii',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 6
})
await this.call('filePanel', 'registerContextMenuItem', {
id: 'solidityumlgen',
name: 'generateCustomAction',
label: 'Generate UML',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 7
})
await this.call('filePanel', 'registerContextMenuItem', {
id: 'doc-gen',
name: 'generateDocsCustomAction',
label: 'Generate Docs',
type: [],
extension: ['.sol'],
path: [],
pattern: [],
sticky: true,
group: 7
})
}
} }
/** @class Reference loaders. /** @class Reference loaders.
@ -236,39 +236,39 @@ export class RemixAppManager extends PluginManager {
* (localStorage, queryParams) * (localStorage, queryParams)
**/ **/
class PluginLoader { class PluginLoader {
get currentLoader() { get currentLoader() {
return this.loaders[this.current] return this.loaders[this.current]
} }
constructor() { constructor() {
const queryParams = new QueryParams() const queryParams = new QueryParams()
this.donotAutoReload = ['remixd'] // that would be a bad practice to force loading some plugins at page load. this.donotAutoReload = ['remixd'] // that would be a bad practice to force loading some plugins at page load.
this.loaders = {} this.loaders = {}
this.loaders.localStorage = { this.loaders.localStorage = {
set: (plugin, actives) => { set: (plugin, actives) => {
const saved = actives.filter((name) => !this.donotAutoReload.includes(name)) const saved = actives.filter((name) => !this.donotAutoReload.includes(name))
localStorage.setItem('workspace', JSON.stringify(saved)) localStorage.setItem('workspace', JSON.stringify(saved))
}, },
get: () => { return JSON.parse(localStorage.getItem('workspace')) } get: () => { return JSON.parse(localStorage.getItem('workspace')) }
}
this.loaders.queryParams = {
set: () => { /* Do nothing. */ },
get: () => {
const { activate } = queryParams.get()
if (!activate) return []
return activate.split(',')
}
}
this.current = queryParams.get().activate ? 'queryParams' : 'localStorage'
} }
set(plugin, actives) { this.loaders.queryParams = {
this.currentLoader.set(plugin, actives) set: () => { /* Do nothing. */ },
get: () => {
const { activate } = queryParams.get()
if (!activate) return []
return activate.split(',')
}
} }
get() { this.current = queryParams.get().activate ? 'queryParams' : 'localStorage'
return this.currentLoader.get() }
}
set(plugin, actives) {
this.currentLoader.set(plugin, actives)
}
get() {
return this.currentLoader.get()
}
} }

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

Loading…
Cancel
Save