Merge branch 'master' of https://github.com/ethereum/remix-project into e2epluginapi2

pull/5370/head
bunsenstraat 3 years ago
commit 8f8061f4d8
  1. 4
      README.md
  2. 2
      apps/debugger/src/app/debugger-api.ts
  3. 29
      apps/remix-ide-e2e/src/commands/addFile.ts
  4. 2
      apps/remix-ide-e2e/src/commands/checkAnnotations.ts
  5. 2
      apps/remix-ide-e2e/src/commands/checkAnnotationsNotPresent.ts
  6. 22
      apps/remix-ide-e2e/src/commands/editorScroll.ts
  7. 4
      apps/remix-ide-e2e/src/commands/getEditorValue.ts
  8. 4
      apps/remix-ide-e2e/src/commands/removeFile.ts
  9. 17
      apps/remix-ide-e2e/src/commands/scrollToLine.ts
  10. 4
      apps/remix-ide-e2e/src/commands/setEditorValue.ts
  11. 7
      apps/remix-ide-e2e/src/tests/ballot.test.ts
  12. 5
      apps/remix-ide-e2e/src/tests/ballot_0_4_11.spec.ts
  13. 12
      apps/remix-ide-e2e/src/tests/debugger.spec.ts
  14. 128
      apps/remix-ide-e2e/src/tests/editor.spec.ts
  15. 13
      apps/remix-ide-e2e/src/tests/fileExplorer.test.ts
  16. 4
      apps/remix-ide-e2e/src/tests/fileManager_api.spec.ts
  17. 16
      apps/remix-ide-e2e/src/tests/gist.spec.ts
  18. 14
      apps/remix-ide-e2e/src/tests/recorder.spec.ts
  19. 3
      apps/remix-ide-e2e/src/tests/remixd.test.ts
  20. 4
      apps/remix-ide-e2e/src/tests/solidityImport.spec.ts
  21. 21
      apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts
  22. 3
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  23. 30
      apps/remix-ide-e2e/src/tests/workspace.test.ts
  24. 2
      apps/remix-ide-e2e/src/types/index.d.ts
  25. 596
      apps/remix-ide/src/app.js
  26. 84
      apps/remix-ide/src/app/editor/SourceHighlighters.js
  27. 16
      apps/remix-ide/src/app/editor/contextualListener.js
  28. 530
      apps/remix-ide/src/app/editor/editor.js
  29. 86
      apps/remix-ide/src/app/editor/sourceHighlighter.js
  30. 8
      apps/remix-ide/src/app/files/fileManager.js
  31. 25
      apps/remix-ide/src/app/files/remixDProvider.js
  32. 16
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  33. 275
      apps/remix-ide/src/app/panels/file-panel.js
  34. 28
      apps/remix-ide/src/app/panels/tab-proxy.js
  35. 15
      apps/remix-ide/src/app/panels/terminal.js
  36. 18
      apps/remix-ide/src/app/tabs/compile-tab.js
  37. 3
      apps/remix-ide/src/app/tabs/test-tab.js
  38. 1
      apps/remix-ide/src/blockchain/providers/vm.js
  39. 2
      apps/remix-ide/src/lib/cmdInterpreterAPI.js
  40. 2
      libs/remix-tests/src/run.ts
  41. 0
      libs/remix-ui/editor/.babelrc
  42. 5
      libs/remix-ui/editor/.eslintrc
  43. 8
      libs/remix-ui/editor/README.md
  44. 1
      libs/remix-ui/editor/src/index.ts
  45. 135
      libs/remix-ui/editor/src/lib/actions/editor.ts
  46. 11
      libs/remix-ui/editor/src/lib/remix-ui-editor.css
  47. 256
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  48. 7709
      libs/remix-ui/editor/src/types/monaco.d.ts
  49. 0
      libs/remix-ui/editor/tsconfig.json
  50. 0
      libs/remix-ui/editor/tsconfig.lib.json
  51. 7
      libs/remix-ui/file-explorer/README.md
  52. 1
      libs/remix-ui/file-explorer/src/index.ts
  53. 384
      libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts
  54. 1105
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  55. 348
      libs/remix-ui/file-explorer/src/lib/reducers/fileSystem.ts
  56. 59
      libs/remix-ui/file-explorer/src/lib/types/index.ts
  57. 13
      libs/remix-ui/file-explorer/src/lib/utils/index.ts
  58. 1
      libs/remix-ui/helper/.eslintrc
  59. 3
      libs/remix-ui/helper/README.md
  60. 1
      libs/remix-ui/helper/src/index.ts
  61. 63
      libs/remix-ui/helper/src/lib/remix-ui-helper.ts
  62. 10
      libs/remix-ui/helper/tsconfig.json
  63. 12
      libs/remix-ui/helper/tsconfig.lib.json
  64. 2
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  65. 10
      libs/remix-ui/plugin-manager/src/lib/components/LocalPluginForm.tsx
  66. 1
      libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx
  67. 6
      libs/remix-ui/static-analyser/src/lib/ErrorRenderer.tsx
  68. 4
      libs/remix-ui/toaster/src/lib/toaster.tsx
  69. 3
      libs/remix-ui/workspace/src/index.ts
  70. 187
      libs/remix-ui/workspace/src/lib/actions/events.ts
  71. 332
      libs/remix-ui/workspace/src/lib/actions/index.ts
  72. 234
      libs/remix-ui/workspace/src/lib/actions/payload.ts
  73. 299
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  74. 4
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  75. 2
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  76. 473
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  77. 67
      libs/remix-ui/workspace/src/lib/components/file-label.tsx
  78. 124
      libs/remix-ui/workspace/src/lib/components/file-render.tsx
  79. 32
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  80. 0
      libs/remix-ui/workspace/src/lib/css/file-explorer-context-menu.css
  81. 0
      libs/remix-ui/workspace/src/lib/css/file-explorer.css
  82. 0
      libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css
  83. 230
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  84. 815
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  85. 416
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  86. 155
      libs/remix-ui/workspace/src/lib/types/index.ts
  87. 63
      libs/remix-ui/workspace/src/lib/utils/index.ts
  88. 2
      libs/remixd/src/bin/remixd.ts
  89. 2
      libs/remixd/src/services/remixdClient.ts
  90. 9
      nx.json
  91. 6867
      package-lock.json
  92. 4
      package.json
  93. 20
      tsconfig.base.json
  94. 51
      workspace.json

@ -23,9 +23,9 @@ For desktop version, see releases: [https://github.com/ethereum/remix-desktop/re
## Offline Usage ## Offline Usage
The `master` branch has always the latest stable build of Remix. It also contains a ZIP file with the entire build. Download it to use offline. The `gh-pages` branch of [remix-live](https://github.com/ethereum/remix-live) always has the latest stable build of Remix. It contains a ZIP file with the entire build. Download it to use offline.
Note: It contains the latest release of Solidity available at the time of the packaging. No other compiler versions are supported. Note: It contains the latest supported version of Solidity available at the time of the packaging. Other compiler versions can be used online only.
## Setup ## Setup

@ -44,7 +44,7 @@ export const DebuggerApiMixin = (Base) => class extends Base {
} }
async highlight (lineColumnPos, path) { async highlight (lineColumnPos, path) {
await this.call('editor', 'highlight', lineColumnPos, path) await this.call('editor', 'highlight', lineColumnPos, path, '', { focus: true })
} }
async getFile (path) { async getFile (path) {

@ -17,16 +17,25 @@ function addFile (browser: NightwatchBrowser, name: string, content: NightwatchC
browser.clickLaunchIcon('udapp') browser.clickLaunchIcon('udapp')
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory
.click('[data-id="fileExplorerNewFilecreateNewFile"]') .elements('css selector', `li[data-id="treeViewLitreeViewItem${name}"]`, (res) => {
.waitForElementContainsText('*[data-id$="/blank"]', '', 60000) if (res.value && (res.value as any).length > 0) {
.sendKeys('*[data-id$="/blank"] .remixui_items', name) browser.openFile(name)
.sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER) .perform(function () {
.pause(2000) done()
.waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000) })
.setEditorValue(content.content) } else {
.pause(1000) browser.click('[data-id="fileExplorerNewFilecreateNewFile"]')
.perform(function () { .waitForElementContainsText('*[data-id$="/blank"]', '', 60000)
done() .sendKeys('*[data-id$="/blank"] .remixui_items', name)
.sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.pause(2000)
.waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000)
.setEditorValue(content.content)
.pause(1000)
.perform(function () {
done()
})
}
}) })
} }

@ -3,7 +3,7 @@ import { NightwatchBrowser } from 'nightwatch'
class checkAnnotations extends EventEmitter { class checkAnnotations extends EventEmitter {
command (this: NightwatchBrowser, type: string, line: number): NightwatchBrowser { command (this: NightwatchBrowser, type: string, line: number): NightwatchBrowser {
this.api.assert.containsText(`.ace_${type}`, line.toString()).perform(() => this.emit('complete')) this.api.assert.containsText(`.margin-view-overlays .${type} + div`, line.toString()).perform(() => this.emit('complete'))
return this return this
} }
} }

@ -3,7 +3,7 @@ import { NightwatchBrowser } from 'nightwatch'
class checkAnnotationsNotPresent extends EventEmitter { class checkAnnotationsNotPresent extends EventEmitter {
command (this: NightwatchBrowser, type: string): NightwatchBrowser { command (this: NightwatchBrowser, type: string): NightwatchBrowser {
this.api.waitForElementNotPresent(`.ace_${type}`).perform(() => this.emit('complete')) this.api.waitForElementNotPresent(`.margin-view-overlays .${type}`).perform(() => this.emit('complete'))
return this return this
} }
} }

@ -1,22 +0,0 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
// fix for editor scroll
class ScrollEditor extends EventEmitter {
command (this: NightwatchBrowser, direction: 'up' | 'down', numberOfTimes: number): NightwatchBrowser {
const browser = this.api
browser.waitForElementPresent('.ace_text-input')
for (let i = 0; i < numberOfTimes; i++) {
if (direction.toLowerCase() === 'up') browser.sendKeys('.ace_text-input', browser.Keys.ARROW_UP)
if (direction.toLowerCase() === 'down') browser.sendKeys('.ace_text-input', browser.Keys.ARROW_DOWN)
}
browser.perform((done) => {
done()
this.emit('complete')
})
return this
}
}
module.exports = ScrollEditor

@ -5,9 +5,9 @@ class GetEditorValue extends EventEmitter {
command (this: NightwatchBrowser, callback: (content: string) => void): NightwatchBrowser { command (this: NightwatchBrowser, callback: (content: string) => void): NightwatchBrowser {
this.api.perform((client, done) => { this.api.perform((client, done) => {
this.api.execute(function () { this.api.execute(function () {
const elem: any = document.getElementById('input') const elem: any = document.getElementById('editorView')
return elem.editor.getValue() return elem.currentContent()
}, [], (result) => { }, [], (result) => {
done() done()
const value = typeof result.value === 'string' ? result.value : null const value = typeof result.value === 'string' ? result.value : null

@ -39,8 +39,8 @@ function removeFile (browser: NightwatchBrowser, path: string, workspace: string
.pause(2000) .pause(2000)
.perform(() => { .perform(() => {
console.log(path, 'to remove') console.log(path, 'to remove')
browser.waitForElementVisible('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok') browser.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementNotPresent('[data-path="' + path + '"]') .waitForElementNotPresent('[data-path="' + path + '"]')
done() done()
}) })

@ -0,0 +1,17 @@
import { NightwatchBrowser } from 'nightwatch'
import EventEmitter from 'events'
class ScrollToLine extends EventEmitter {
command (this: NightwatchBrowser, line: number): NightwatchBrowser {
this.api.execute(function (line) {
const elem: any = document.getElementById('editorView')
elem.gotoLine(line)
}, [line], () => {
this.emit('complete')
})
return this
}
}
module.exports = ScrollToLine

@ -5,9 +5,9 @@ class SetEditorValue extends EventEmitter {
command (this: NightwatchBrowser, value: string, callback?: VoidFunction): NightwatchBrowser { command (this: NightwatchBrowser, value: string, callback?: VoidFunction): NightwatchBrowser {
this.api.perform((client, done) => { this.api.perform((client, done) => {
this.api.execute(function (value) { this.api.execute(function (value) {
const elem: any = document.getElementById('input') const elem: any = document.getElementById('editorView')
elem.editor.session.setValue(value) elem.setCurrentContent(value)
}, [value], () => { }, [value], () => {
done() done()
if (callback) { if (callback) {

@ -81,8 +81,9 @@ module.exports = {
'Deploy and use Ballot using external web3': function (browser: NightwatchBrowser) { 'Deploy and use Ballot using external web3': function (browser: NightwatchBrowser) {
browser browser
.click('option[value="web3"]') .openFile('Untitled.sol')
.pause(5000) .clickLaunchIcon('udapp')
.click('*[data-id="settingsWeb3Mode"]')
.modalFooterOKClick() .modalFooterOKClick()
.execute(function () { .execute(function () {
const env: any = document.getElementById('selectExEnvOptions') const env: any = document.getElementById('selectExEnvOptions')
@ -92,8 +93,8 @@ module.exports = {
browser.assert.ok(result.value === 'web3', 'Web3 Provider not selected') browser.assert.ok(result.value === 'web3', 'Web3 Provider not selected')
}) })
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['Ballot'])
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.pause(2000)
.setValue('input[placeholder="bytes32[] proposalNames"]', '["0x48656c6c6f20576f726c64210000000000000000000000000000000000000000"]') .setValue('input[placeholder="bytes32[] proposalNames"]', '["0x48656c6c6f20576f726c64210000000000000000000000000000000000000000"]')
.click('*[data-id="Deploy - transact (not payable)"]') .click('*[data-id="Deploy - transact (not payable)"]')
.clickInstance(0) .clickInstance(0)

@ -28,7 +28,6 @@ module.exports = {
'Deploy Ballot': function (browser: NightwatchBrowser) { 'Deploy Ballot': function (browser: NightwatchBrowser) {
browser.pause(500) browser.pause(500)
.testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['Ballot'])
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c') .selectAccount('0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c')
.setValue('input[placeholder="uint8 _numProposals"]', '2') .setValue('input[placeholder="uint8 _numProposals"]', '2')
@ -77,11 +76,13 @@ module.exports = {
'Deploy and use Ballot using external web3': function (browser: NightwatchBrowser) { 'Deploy and use Ballot using external web3': function (browser: NightwatchBrowser) {
browser browser
.openFile('Untitled.sol')
.clickLaunchIcon('udapp')
.click('*[data-id="settingsWeb3Mode"]') .click('*[data-id="settingsWeb3Mode"]')
.modalFooterOKClick() .modalFooterOKClick()
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.testContracts('Untitled.sol', sources[0]['Untitled.sol'], ['Ballot'])
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.pause(2000)
.setValue('input[placeholder="uint8 _numProposals"]', '2') .setValue('input[placeholder="uint8 _numProposals"]', '2')
.click('*[data-id="Deploy - transact (not payable)"]') .click('*[data-id="Deploy - transact (not payable)"]')
.clickInstance(0) .clickInstance(0)

@ -62,9 +62,13 @@ module.exports = {
}, },
'Should jump through breakpoints': function (browser: NightwatchBrowser) { 'Should jump through breakpoints': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.click('.ace_gutter-cell:nth-of-type(10)') .execute(() => {
.click('.ace_gutter-cell:nth-of-type(20)') (window as any).addRemixBreakpoint(11)
}, [], () => {})
.execute(() => {
(window as any).addRemixBreakpoint(21)
}, [], () => {})
.waitForElementVisible('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]') .waitForElementVisible('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
.click('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]') .click('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
.pause(2000) .pause(2000)
@ -127,7 +131,7 @@ module.exports = {
But the debugger uses now validSourcelocation, which means file is not -1. But the debugger uses now validSourcelocation, which means file is not -1.
In that case the source highlight at 261 should be the same as for step 262 In that case the source highlight at 261 should be the same as for step 262
*/ */
.waitForElementPresent('.highlightLine7') .waitForElementPresent('.highlightLine8')
.goToVMTraceStep(266) .goToVMTraceStep(266)
.pause(1000) .pause(1000)
.checkVariableDebug('soliditylocals', localVariable_step266_ABIEncoder) // locals should not be initiated at this point, only idAsk should .checkVariableDebug('soliditylocals', localVariable_step266_ABIEncoder) // locals should not be initiated at this point, only idAsk should

@ -15,37 +15,35 @@ module.exports = {
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]') .waitForElementVisible('div[data-id="filePanelFileExplorerTree"]')
.openFile('contracts') .openFile('contracts')
.openFile('contracts/1_Storage.sol') .openFile('contracts/1_Storage.sol')
.waitForElementVisible('*[data-id="editorInput"]') .waitForElementVisible('#editorView')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '12px') .checkElementStyle('.view-lines', 'font-size', '14px')
.click('*[data-id="tabProxyZoomIn"]') .click('*[data-id="tabProxyZoomIn"]')
.click('*[data-id="tabProxyZoomIn"]') .click('*[data-id="tabProxyZoomIn"]')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '14px') .checkElementStyle('.view-lines', 'font-size', '16px')
}, },
'Should zoom out editor': function (browser: NightwatchBrowser) { 'Should zoom out editor': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '14px') .checkElementStyle('.view-lines', 'font-size', '16px')
.click('*[data-id="tabProxyZoomOut"]') .click('*[data-id="tabProxyZoomOut"]')
.click('*[data-id="tabProxyZoomOut"]') .click('*[data-id="tabProxyZoomOut"]')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '12px') .checkElementStyle('.view-lines', 'font-size', '14px')
}, },
'Should display compile error in editor': function (browser: NightwatchBrowser) { 'Should display compile error in editor': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.waitForElementVisible('*[class="ace_content"]') .setEditorValue(storageContractWithError + 'error')
.click('*[class="ace_content"]') .pause(2000)
.editorScroll('down', 27) // scroll down to line 27 and add the error word .waitForElementVisible('.margin-view-overlays .fa-exclamation-square', 120000)
.sendKeys('*[class="ace_text-input"]', 'error') .checkAnnotations('fa-exclamation-square', 29) // error
.waitForElementVisible('.ace_error', 120000)
.checkAnnotations('error', 28)
.clickLaunchIcon('udapp') .clickLaunchIcon('udapp')
.checkAnnotationsNotPresent('error') .checkAnnotationsNotPresent('fa-exclamation-square') // error
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.checkAnnotations('error', 28) .checkAnnotations('fa-exclamation-square', 29) // error
}, },
'Should minimize and maximize codeblock in editor': function (browser: NightwatchBrowser) { 'Should minimize and maximize codeblock in editor': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.waitForElementVisible('.ace_open') .waitForElementVisible('.ace_open')
.click('.ace_start:nth-of-type(1)') .click('.ace_start:nth-of-type(1)')
.waitForElementVisible('.ace_closed') .waitForElementVisible('.ace_closed')
@ -54,27 +52,29 @@ module.exports = {
}, },
'Should add breakpoint to editor': function (browser: NightwatchBrowser) { 'Should add breakpoint to editor': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.waitForElementNotPresent('.ace_breakpoint') .waitForElementNotPresent('.margin-view-overlays .fa-circle')
.click('.ace_gutter-cell:nth-of-type(1)') .execute(() => {
.waitForElementVisible('.ace_breakpoint') (window as any).addRemixBreakpoint(1)
}, [], () => {})
.waitForElementVisible('.margin-view-overlays .fa-circle')
}, },
'Should load syntax highlighter for ace light theme': function (browser: NightwatchBrowser) { 'Should load syntax highlighter for ace light theme': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]') browser.waitForElementVisible('#editorView')
.checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword) .checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment) .checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment)
.checkElementStyle('.ace_function', 'color', aceThemes.light.function) .checkElementStyle('.ace_function', 'color', aceThemes.light.function)
.checkElementStyle('.ace_variable', 'color', aceThemes.light.variable) .checkElementStyle('.ace_variable', 'color', aceThemes.light.variable)
}, },
'Should load syntax highlighter for ace dark theme': function (browser: NightwatchBrowser) { 'Should load syntax highlighter for ace dark theme': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]') browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]')
.click('*[data-id="verticalIconsKindsettings"]') .click('*[data-id="verticalIconsKindsettings"]')
.waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]') .waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]')
.click('*[data-id="settingsTabThemeLabelDark"]') .click('*[data-id="settingsTabThemeLabelDark"]')
.pause(2000) .pause(2000)
.waitForElementVisible('*[data-id="editorInput"]') .waitForElementVisible('#editorView')
/* @todo(#2863) ch for class and not colors /* @todo(#2863) ch for class and not colors
.checkElementStyle('.ace_keyword', 'color', aceThemes.dark.keyword) .checkElementStyle('.ace_keyword', 'color', aceThemes.dark.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.dark.comment) .checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.dark.comment)
@ -87,24 +87,21 @@ module.exports = {
// include all files here because switching between plugins in side-panel removes highlight // include all files here because switching between plugins in side-panel removes highlight
browser browser
.addFile('sourcehighlight.js', sourcehighlightScript) .addFile('sourcehighlight.js', sourcehighlightScript)
.addFile('removeSourcehighlightScript.js', removeSourcehighlightScript)
.addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript) .addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript)
.openFile('sourcehighlight.js') .openFile('sourcehighlight.js')
.executeScript('remix.exeCurrent()') .executeScript('remix.exeCurrent()')
.editorScroll('down', 60) .scrollToLine(32)
.pause(1000) .waitForElementPresent('.highlightLine33', 60000)
.waitForElementPresent('.highlightLine32', 60000) .checkElementStyle('.highlightLine33', 'background-color', 'rgb(52, 152, 219)')
.pause(1000) .scrollToLine(40)
.checkElementStyle('.highlightLine32', 'background-color', 'rgb(8, 108, 181)') .waitForElementPresent('.highlightLine41', 60000)
.pause(1000) .checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.waitForElementPresent('.highlightLine40', 60000) .scrollToLine(50)
.pause(1000) .waitForElementPresent('.highlightLine51', 60000)
.checkElementStyle('.highlightLine40', 'background-color', 'rgb(8, 108, 181)') .checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
.waitForElementPresent('.highlightLine50', 60000)
.checkElementStyle('.highlightLine50', 'background-color', 'rgb(8, 108, 181)')
}, },
'Should remove 1 highlight from source code': function (browser: NightwatchBrowser) { 'Should remove 1 highlight from source code': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]') browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]') .click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.pause(2000) .pause(2000)
@ -113,9 +110,9 @@ module.exports = {
.click('li[data-id="treeViewLitreeViewItemcontracts"]') .click('li[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.waitForElementNotPresent('.highlightLine32', 60000) .waitForElementNotPresent('.highlightLine33', 60000)
.checkElementStyle('.highlightLine40', 'background-color', 'rgb(8, 108, 181)') .checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.checkElementStyle('.highlightLine50', 'background-color', 'rgb(8, 108, 181)') .checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
}, },
'Should remove all highlights from source code': function (browser: NightwatchBrowser) { 'Should remove all highlights from source code': function (browser: NightwatchBrowser) {
@ -126,9 +123,9 @@ module.exports = {
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]') .click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.pause(2000) .pause(2000)
.waitForElementNotPresent('.highlightLine32', 60000) .waitForElementNotPresent('.highlightLine33', 60000)
.waitForElementNotPresent('.highlightLine40', 60000) .waitForElementNotPresent('.highlightLine41', 60000)
.waitForElementNotPresent('.highlightLine50', 60000) .waitForElementNotPresent('.highlightLine51', 60000)
.end() .end()
} }
} }
@ -152,6 +149,7 @@ const sourcehighlightScript = {
content: ` content: `
(async () => { (async () => {
try { try {
await remix.call('fileManager', 'open', 'contracts/3_Ballot.sol')
const pos = { const pos = {
start: { start: {
line: 32, line: 32,
@ -194,18 +192,6 @@ const sourcehighlightScript = {
` `
} }
const removeSourcehighlightScript = {
content: `
(async () => {
try {
await remix.call('editor', 'discardHighlightAt', 32, 'contracts/3_Ballot.sol')
} catch (e) {
console.log(e.message)
}
})()
`
}
const removeAllSourcehighlightScript = { const removeAllSourcehighlightScript = {
content: ` content: `
(async () => { (async () => {
@ -217,3 +203,33 @@ const removeAllSourcehighlightScript = {
})() })()
` `
} }
const storageContractWithError = `
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}`

@ -68,9 +68,9 @@ module.exports = {
.waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]')
.rightClick('[data-path="Browser_E2E_Tests"]') .rightClick('[data-path="Browser_E2E_Tests"]')
.click('*[id="menuitemdelete"]') .click('*[id="menuitemdelete"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000) .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000) .pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]') .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]')
}, },
@ -81,13 +81,13 @@ module.exports = {
.pause(10000) .pause(10000)
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]') .waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]') .click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000) .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000) .pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000) .pause(2000)
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000) .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000) .pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok') .click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000) .pause(2000)
.perform((done) => { .perform((done) => {
if (runtimeBrowser === 'chrome') { if (runtimeBrowser === 'chrome') {
@ -101,6 +101,7 @@ module.exports = {
'Should open local filesystem explorer': function (browser: NightwatchBrowser) { 'Should open local filesystem explorer': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="filePanelFileExplorerTree"]') browser.waitForElementVisible('*[data-id="filePanelFileExplorerTree"]')
.click('[data-id="remixUIWorkspaceExplorer"]')
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile1) .setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile1)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile2) .setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile2)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile3) .setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile3)

@ -36,7 +36,9 @@ module.exports = {
.executeScript('remix.exeCurrent()') .executeScript('remix.exeCurrent()')
.pause(2000) .pause(2000)
.openFile('new_contract.sol') .openFile('new_contract.sol')
.assert.containsText('[data-id="editorInput"]', 'pragma solidity ^0.6.0') .getEditorValue((content) => {
browser.assert.ok(content.indexOf('pragma solidity ^0.6.0') !== -1, 'content does not contain "pragma solidity ^0.6.0"')
})
}, },
'Should execute `readFile` api from file manager external api': function (browser: NightwatchBrowser) { 'Should execute `readFile` api from file manager external api': function (browser: NightwatchBrowser) {

@ -103,14 +103,18 @@ module.exports = {
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]') .waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]') .click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]') .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000) .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.pause(10000) .pause(10000)
.getText('[data-id="default_workspaceModalDialogModalBody-react"]', (result) => { .perform((done) => {
browser.assert.ok(result.value === 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Assert failed. Gist token error message not displayed.') browser.getText('[data-id="fileSystemModalDialogModalBody-react"]', (result) => {
console.log('result.value: ', result.value)
browser.assert.ok(result.value === 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Assert failed. Gist token error message not displayed.')
done()
})
}) })
.click('[data-id="default_workspace-modal-footer-ok-react"]') .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
}, },
'Import From Gist For Valid Gist ID': function (browser: NightwatchBrowser) { 'Import From Gist For Valid Gist ID': function (browser: NightwatchBrowser) {

@ -129,7 +129,7 @@ contract t2est {
const records = `{ const records = `{
"accounts": { "accounts": {
"account{2}": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c" "account{10}": "0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c"
}, },
"linkReferences": { "linkReferences": {
"testLib": "created{1512830014773}" "testLib": "created{1512830014773}"
@ -146,7 +146,7 @@ const records = `{
"linkReferences": {}, "linkReferences": {},
"inputs": "()", "inputs": "()",
"type": "constructor", "type": "constructor",
"from": "account{2}" "from": "account{10}"
} }
}, },
{ {
@ -172,7 +172,7 @@ const records = `{
"name": "", "name": "",
"type": "constructor", "type": "constructor",
"inputs": "(uint256)", "inputs": "(uint256)",
"from": "account{2}" "from": "account{10}"
} }
}, },
{ {
@ -188,7 +188,7 @@ const records = `{
"name": "set", "name": "set",
"inputs": "(uint256,address)", "inputs": "(uint256,address)",
"type": "function", "type": "function",
"from": "account{2}" "from": "account{10}"
} }
} }
], ],
@ -287,7 +287,7 @@ const records = `{
const scenario = { const scenario = {
accounts: { accounts: {
'account{2}': '0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c' 'account{10}': '0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c'
}, },
linkReferences: {}, linkReferences: {},
transactions: [ transactions: [
@ -305,7 +305,7 @@ const scenario = {
name: '', name: '',
type: 'constructor', type: 'constructor',
inputs: '(uint256)', inputs: '(uint256)',
from: 'account{2}' from: 'account{10}'
} }
}, },
{ {
@ -320,7 +320,7 @@ const scenario = {
name: 'set', name: 'set',
inputs: '(uint256)', inputs: '(uint256)',
type: 'function', type: 'function',
from: 'account{2}' from: 'account{10}'
} }
} }
], ],

@ -85,7 +85,7 @@ module.exports = {
.waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () { .waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () {
browser browser
.click('[data-id="staticAnalysisModuleMiscellaneous1"') .click('[data-id="staticAnalysisModuleMiscellaneous1"')
.waitForElementPresent('.highlightLine15', 60000) .waitForElementPresent('.highlightLine16', 60000)
.getEditorValue((content) => { .getEditorValue((content) => {
browser.assert.ok(content.indexOf( browser.assert.ok(content.indexOf(
'function _sendLogPayload(bytes memory payload) private view {') !== -1, 'function _sendLogPayload(bytes memory payload) private view {') !== -1,
@ -153,7 +153,6 @@ function runTests (browser: NightwatchBrowser) {
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.waitForElementVisible('[data-path="folder1"]') .waitForElementVisible('[data-path="folder1"]')
.click('[data-path="folder1"]') .click('[data-path="folder1"]')
.click('[data-path="folder1"]') // click twice because remixd does not return nested folder details after update
.waitForElementVisible('[data-path="folder1/contract1.sol"]') .waitForElementVisible('[data-path="folder1/contract1.sol"]')
.waitForElementVisible('[data-path="folder1/renamed_contract_' + browserName + '.sol"]') // check if renamed file is preset .waitForElementVisible('[data-path="folder1/renamed_contract_' + browserName + '.sol"]') // check if renamed file is preset
.waitForElementNotPresent('[data-path="folder1/contract_' + browserName + '.sol"]') // check if renamed (old) file is not present .waitForElementNotPresent('[data-path="folder1/contract_' + browserName + '.sol"]') // check if renamed (old) file is not present

@ -75,7 +75,9 @@ module.exports = {
.waitForElementVisible('[data-id="https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol"]', 120000) .waitForElementVisible('[data-id="https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol"]', 120000)
.scrollAndClick('[data-id="https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol"]') // click on error which point to ERC20 code .scrollAndClick('[data-id="https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol"]') // click on error which point to ERC20 code
.pause(5000) .pause(5000)
.waitForElementContainsText('#input', 'contract ERC20 is Context, IERC20', 60000) .getEditorValue((content) => {
browser.assert.ok(content.indexOf('contract ERC20 is Context, IERC20') !== -1, 'content does not contain "contract ERC20 is Context, IERC20"')
})
}, },
'Test NPM Import (with unpkg.com)': function (browser: NightwatchBrowser) { 'Test NPM Import (with unpkg.com)': function (browser: NightwatchBrowser) {

@ -32,9 +32,10 @@ module.exports = {
.click('*[data-id="verticalIconsKindsolidityUnitTesting"]') .click('*[data-id="verticalIconsKindsolidityUnitTesting"]')
.waitForElementPresent('*[data-id="testTabGenerateTestFile"]') .waitForElementPresent('*[data-id="testTabGenerateTestFile"]')
.click('*[data-id="testTabGenerateTestFile"]') .click('*[data-id="testTabGenerateTestFile"]')
.waitForElementPresent('*[title="tests/simple_storage_test.sol"]') .waitForElementPresent('*[title="default_workspace/tests/simple_storage_test.sol"]')
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.pause(10000) .waitForElementPresent('[data-id="treeViewDivtreeViewItemtests"]')
.click('[data-id="treeViewDivtreeViewItemtests"]')
.openFile('tests/simple_storage_test.sol') .openFile('tests/simple_storage_test.sol')
.removeFile('tests/simple_storage_test.sol', 'default_workspace') .removeFile('tests/simple_storage_test.sol', 'default_workspace')
}, },
@ -164,10 +165,9 @@ module.exports = {
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_new' })
.pause(5000) .waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]')
.waitForElementPresent('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .execute(function () { (document.querySelector('[data-id="fileSystem-modal-footer-ok-react"]') as HTMLElement).click() })
.click('*[data-id="workspacesModalDialogModalDialogModalFooter-react"] .modal-ok') .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_new"]')
.click('*[data-id="workspacesSelect"] option[value="workspace_new"]')
// end of creating // end of creating
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.pause(2000) .pause(2000)
@ -193,8 +193,17 @@ module.exports = {
}, },
'Solidity Unit tests with hardhat console log': function (browser: NightwatchBrowser) { 'Solidity Unit tests with hardhat console log': function (browser: NightwatchBrowser) {
const runtimeBrowser = browser.options.desiredCapabilities.browserName
browser browser
.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') .waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]')
.perform((done) => {
if (runtimeBrowser !== 'chrome') {
browser.clickLaunchIcon('filePanel')
.waitForElementVisible('[data-id="treeViewLitreeViewItemtests"]')
}
done()
})
.addFile('tests/hhLogs_test.sol', sources[0]['tests/hhLogs_test.sol']) .addFile('tests/hhLogs_test.sol', sources[0]['tests/hhLogs_test.sol'])
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.waitForElementVisible('*[id="singleTesttests/4_Ballot_test.sol"]', 60000) .waitForElementVisible('*[id="singleTesttests/4_Ballot_test.sol"]', 60000)

@ -53,7 +53,7 @@ module.exports = {
'Call web3.eth.getAccounts() using JavaScript VM': function (browser: NightwatchBrowser) { 'Call web3.eth.getAccounts() using JavaScript VM': function (browser: NightwatchBrowser) {
browser browser
.executeScript('web3.eth.getAccounts()') .executeScript('web3.eth.getAccounts()')
.waitForElementContainsText('*[data-id="terminalJournal"]', '["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db","0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB","0x617F2E2fD72FD9D5503197092aC168c91465E7f2","0x17F6AD8Ef982297579C203069C1DbfFE4348c372","0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C","0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678","0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7","0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C","0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC","0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB","0x583031D1113aD414F02576BD6afaBfb302140225","0xdD870fA1b7C4700F2BD7f44238821C26f7392148"]', 80000) .waitForElementContainsText('*[data-id="terminalJournal"]', '["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db","0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB","0x617F2E2fD72FD9D5503197092aC168c91465E7f2","0x17F6AD8Ef982297579C203069C1DbfFE4348c372","0x5c6B0f7Bf3E7ce046039Bd8FABdfD3f9F5021678","0x03C6FcED478cBbC9a4FAB34eF9f40767739D1Ff7","0x1aE0EA34a72D944a8C7603FfB3eC30a6669E454C","0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC","0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c","0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C","0x4B0897b0513fdC7C541B6d9D7E929C4e5364D2dB","0x583031D1113aD414F02576BD6afaBfb302140225","0xdD870fA1b7C4700F2BD7f44238821C26f7392148"]')
}, },
'Call web3.eth.getAccounts() using Web3 Provider': function (browser: NightwatchBrowser) { 'Call web3.eth.getAccounts() using Web3 Provider': function (browser: NightwatchBrowser) {
@ -111,6 +111,7 @@ module.exports = {
.addFile('deployWithEthersJs.js', { content: deployWithEthersJs }) .addFile('deployWithEthersJs.js', { content: deployWithEthersJs })
.openFile('deployWithEthersJs.js') .openFile('deployWithEthersJs.js')
.pause(1000) .pause(1000)
.click('[data-id="treeViewDivtreeViewItemcontracts"]')
.openFile('contracts/2_Owner.sol') .openFile('contracts/2_Owner.sol')
.clickLaunchIcon('solidity') .clickLaunchIcon('solidity')
.click('*[data-id="compilerContainerCompileBtn"]') // compile Owner .click('*[data-id="compilerContainerCompileBtn"]') // compile Owner

@ -19,7 +19,7 @@ module.exports = {
browser browser
.pause(5000) .pause(5000)
.refresh() .refresh()
.pause(5000) .pause(10000)
.getEditorValue((content) => { .getEditorValue((content) => {
browser.assert.ok(content.indexOf('contract Ballot {') !== -1, 'content doesn\'t include Ballot contract') browser.assert.ok(content.indexOf('contract Ballot {') !== -1, 'content doesn\'t include Ballot contract')
}) })
@ -35,24 +35,22 @@ module.exports = {
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.click('*[data-id="workspaceCreate"]') // create workspace_name .click('*[data-id="workspaceCreate"]') // create workspace_name
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="workspacesModalDialogModalDialogModalFooter-react"] > span') .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name' })
.pause(1000) .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.pause(1000) .pause(1000)
.addFile('test.sol', { content: 'test' }) .addFile('test.sol', { content: 'test' })
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]')
.click('*[data-id="workspaceCreate"]') // create workspace_name_1 .click('*[data-id="workspaceCreate"]') // create workspace_name_1
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextCreate"]')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] > span')
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name_1' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextCreate"]')['value'] = 'workspace_name_1' })
.waitForElementVisible('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
// eslint-disable-next-line dot-notation .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.execute(function () { document.querySelector('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') })
.pause(2000)
.click('span[data-id="workspacesModalDialog-modal-footer-ok-react"]')
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtests"]')
.pause(2000) .pause(2000)
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
@ -68,13 +66,13 @@ module.exports = {
.waitForElementVisible('*[data-id="modalDialogCustomPromptTextRename"]') .waitForElementVisible('*[data-id="modalDialogCustomPromptTextRename"]')
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
.execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextRename"]')['value'] = 'workspace_name_renamed' }) .execute(function () { document.querySelector('*[data-id="modalDialogCustomPromptTextRename"]')['value'] = 'workspace_name_renamed' })
.pause(2000) .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementPresent('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.click('span[data-id="workspacesModalDialog-modal-footer-ok-react"]') .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.pause(2000)
.click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.pause(2000) .pause(2000)
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementNotPresent('*[data-id="treeViewLitreeViewItemtest.sol"]')
.waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]')
.click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]') .click('*[data-id="workspacesSelect"] option[value="workspace_name_renamed"]')
.pause(2000) .pause(2000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemtest.sol"]')
@ -84,10 +82,8 @@ module.exports = {
browser browser
.click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .click('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.click('*[data-id="workspaceDelete"]') // delete workspace_name_1 .click('*[data-id="workspaceDelete"]') // delete workspace_name_1
.waitForElementVisible('[data-id="workspacesModalDialogModalDialogModalFooter-react"] > span') .waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000) .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.click('[data-id="workspacesModalDialogModalDialogModalFooter-react"] > span')
.pause(2000)
.waitForElementNotPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]') .waitForElementNotPresent('*[data-id="workspacesSelect"] option[value="workspace_name_1"]')
.end() .end()
}, },

@ -27,9 +27,9 @@ declare module 'nightwatch' {
debugTransaction(index: number): NightwatchBrowser, debugTransaction(index: number): NightwatchBrowser,
checkElementStyle(cssSelector: string, styleProperty: string, expectedResult: string): NightwatchBrowser, checkElementStyle(cssSelector: string, styleProperty: string, expectedResult: string): NightwatchBrowser,
openFile(name: string): NightwatchBrowser, openFile(name: string): NightwatchBrowser,
editorScroll(direction: 'up' | 'down', numberOfTimes: number): NightwatchBrowser,
renamePath(path: string, newFileName: string, renamedPath: string): NightwatchBrowser, renamePath(path: string, newFileName: string, renamedPath: string): NightwatchBrowser,
rightClick(cssSelector: string): NightwatchBrowser, rightClick(cssSelector: string): NightwatchBrowser,
scrollToLine(line: number): NightwatchBrowser,
waitForElementContainsText(id: string, value: string, timeout?: number): NightwatchBrowser, waitForElementContainsText(id: string, value: string, timeout?: number): NightwatchBrowser,
getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser, getModalBody(callback: (value: string, cb: VoidFunction) => void): NightwatchBrowser,
modalFooterCancelClick(): NightwatchBrowser, modalFooterCancelClick(): NightwatchBrowser,

@ -162,8 +162,7 @@ class App {
} }
init () { init () {
var self = this this.run().catch(console.error)
run.apply(self)
} }
render () { render () {
@ -202,317 +201,322 @@ class App {
` `
return self._view.el return self._view.el
} }
}
module.exports = App
async function run () { async run () {
var self = this var self = this
// check the origin and warn message
if (window.location.hostname === 'yann300.github.io') {
modalDialogCustom.alert('This UNSTABLE ALPHA branch of Remix has been moved to http://ethereum.github.io/remix-live-alpha.')
} else if (window.location.hostname === 'remix-alpha.ethereum.org' ||
(window.location.hostname === 'ethereum.github.io' && window.location.pathname.indexOf('/remix-live-alpha') === 0)) {
modalDialogCustom.alert('Welcome to the Remix alpha instance. Please use it to try out latest features. But use preferably https://remix.ethereum.org for any production work.')
} else if (window.location.protocol.indexOf('http') === 0 &&
window.location.hostname !== 'remix.ethereum.org' &&
window.location.hostname !== 'localhost' &&
window.location.hostname !== '127.0.0.1') {
modalDialogCustom.alert(`The Remix IDE has moved to http://remix.ethereum.org.\n
This instance of Remix you are visiting WILL NOT BE UPDATED.\n
Please make a backup of your contracts and start using http://remix.ethereum.org`)
}
if (window.location.protocol.indexOf('https') === 0) {
toolTip('You are using an `https` connection. Please switch to `http` if you are using Remix against an `http Web3 provider` or allow Mixed Content in your browser.')
}
const hosts = ['127.0.0.1:8080', '192.168.0.101:8080', 'localhost:8080'] // check the origin and warn message
// workaround for Electron support if (window.location.hostname === 'yann300.github.io') {
if (!isElectron() && !hosts.includes(window.location.host)) { modalDialogCustom.alert('This UNSTABLE ALPHA branch of Remix has been moved to http://ethereum.github.io/remix-live-alpha.')
// Oops! Accidentally trigger refresh or bookmark. } else if (window.location.hostname === 'remix-alpha.ethereum.org' ||
window.onbeforeunload = function () { (window.location.hostname === 'ethereum.github.io' && window.location.pathname.indexOf('/remix-live-alpha') === 0)) {
return 'Are you sure you want to leave?' modalDialogCustom.alert('Welcome to the Remix alpha instance. Please use it to try out latest features. But use preferably https://remix.ethereum.org for any production work.')
} else if (window.location.protocol.indexOf('http') === 0 &&
window.location.hostname !== 'remix.ethereum.org' &&
window.location.hostname !== 'localhost' &&
window.location.hostname !== '127.0.0.1') {
modalDialogCustom.alert(`The Remix IDE has moved to http://remix.ethereum.org.\n
This instance of Remix you are visiting WILL NOT BE UPDATED.\n
Please make a backup of your contracts and start using http://remix.ethereum.org`)
}
if (window.location.protocol.indexOf('https') === 0) {
toolTip('You are using an `https` connection. Please switch to `http` if you are using Remix against an `http Web3 provider` or allow Mixed Content in your browser.')
} }
}
// APP_MANAGER const hosts = ['127.0.0.1:8080', '192.168.0.101:8080', 'localhost:8080']
const appManager = self.appManager // workaround for Electron support
const pluginLoader = appManager.pluginLoader if (!isElectron() && !hosts.includes(window.location.host)) {
const workspace = pluginLoader.get() // Oops! Accidentally trigger refresh or bookmark.
const engine = new RemixEngine() window.onbeforeunload = function () {
engine.register(appManager) return 'Are you sure you want to leave?'
// SERVICES
// ----------------- theme service ---------------------------------
const themeModule = new ThemeModule(registry)
registry.put({ api: themeModule, name: 'themeModule' })
themeModule.initTheme(() => {
setTimeout(() => {
document.body.removeChild(self._view.splashScreen)
self._view.el.style.visibility = 'visible'
}, 1500)
})
// ----------------- editor service ----------------------------
const editor = new Editor({}, themeModule) // wrapper around ace editor
registry.put({ api: editor, name: 'editor' })
editor.event.register('requiringToSaveCurrentfile', () => fileManager.saveCurrentFile())
// ----------------- fileManager service ----------------------------
const fileManager = new FileManager(editor, appManager)
registry.put({ api: fileManager, name: 'filemanager' })
// ----------------- dGit provider ---------------------------------
const dGitProvider = new DGitProvider()
// ----------------- import content service ------------------------
const contentImport = new CompilerImports()
const blockchain = new Blockchain(registry.get('config').api)
// ----------------- compilation metadata generation service ---------
const compilerMetadataGenerator = new CompilerMetadata()
// ----------------- compilation result service (can keep track of compilation results) ----------------------------
const compilersArtefacts = new CompilerArtefacts() // store all the compilation results (key represent a compiler name)
registry.put({ api: compilersArtefacts, name: 'compilersartefacts' })
// service which fetch contract artifacts from sourve-verify, put artifacts in remix and compile it
const fetchAndCompile = new FetchAndCompile()
// ----------------- network service (resolve network id / name) -----
const networkModule = new NetworkModule(blockchain)
// ----------------- represent the current selected web3 provider ----
const web3Provider = new Web3ProviderModule(blockchain)
const hardhatProvider = new HardhatProvider(blockchain)
// ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new OffsetToLineColumnConverter()
registry.put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' })
// -------------------Terminal----------------------------------------
makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl))
const terminal = new Terminal(
{ appManager, blockchain },
{
getPosition: (event) => {
var limitUp = 36
var limitDown = 20
var height = window.innerHeight
var newpos = (event.pageY < limitUp) ? limitUp : event.pageY
newpos = (newpos < height - limitDown) ? newpos : height - limitDown
return height - newpos
} }
},
registry
)
const contextualListener = new ContextualListener({ editor })
engine.register([
blockchain,
contentImport,
themeModule,
editor,
fileManager,
compilerMetadataGenerator,
compilersArtefacts,
networkModule,
offsetToLineColumnConverter,
contextualListener,
terminal,
web3Provider,
fetchAndCompile,
dGitProvider,
hardhatProvider
])
// LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel()
const mainview = new MainView(contextualListener, editor, appPanel, fileManager, appManager, terminal)
registry.put({ api: mainview, name: 'mainview' })
engine.register([
appPanel,
mainview.tabProxy
])
// those views depend on app_manager
const menuicons = new VerticalIcons(appManager)
const sidePanel = new SidePanel(appManager, menuicons)
const hiddenPanel = new HiddenPanel()
const pluginManagerComponent = new PluginManagerComponent(appManager, engine)
const filePanel = new FilePanel(appManager)
const landingPage = new LandingPage(appManager, menuicons, fileManager, filePanel, contentImport)
const settings = new SettingsTab(
registry.get('config').api,
editor,
appManager
)
// adding Views to the DOM
self._view.mainpanel.appendChild(mainview.render())
self._view.iconpanel.appendChild(menuicons.render())
self._view.sidepanel.appendChild(sidePanel.render())
document.body.appendChild(hiddenPanel.render()) // Hidden Panel is display none, it can be directly on body
engine.register([
menuicons,
landingPage,
hiddenPanel,
sidePanel,
filePanel,
pluginManagerComponent,
settings
])
const queryParams = new QueryParams()
const params = queryParams.get()
const onAcceptMatomo = () => {
_paq.push(['forgetUserOptOut'])
// @TODO remove next line when https://github.com/matomo-org/matomo/commit/9e10a150585522ca30ecdd275007a882a70c6df5 is used
document.cookie = 'mtm_consent_removed=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
settings.updateMatomoAnalyticsChoice(true)
const el = document.getElementById('modal-dialog')
el.parentElement.removeChild(el)
startWalkthroughService()
}
const onDeclineMatomo = () => {
settings.updateMatomoAnalyticsChoice(false)
_paq.push(['optUserOut'])
const el = document.getElementById('modal-dialog')
el.parentElement.removeChild(el)
startWalkthroughService()
}
const startWalkthroughService = () => {
const walkthroughService = new WalkthroughService(localStorage)
if (!params.code && !params.url && !params.minimizeterminal && !params.gist && !params.minimizesidepanel) {
walkthroughService.start()
} }
}
// Ask to opt in to Matomo for remix, remix-alpha and remix-beta // APP_MANAGER
const matomoDomains = { const appManager = self.appManager
'remix-alpha.ethereum.org': 27, const pluginLoader = appManager.pluginLoader
'remix-beta.ethereum.org': 25, const workspace = pluginLoader.get()
'remix.ethereum.org': 23 const engine = new RemixEngine()
} engine.register(appManager)
if (matomoDomains[window.location.hostname] && !registry.get('config').api.exists('settings/matomo-analytics')) {
modalDialog( // SERVICES
'Help us to improve Remix IDE', // ----------------- theme service ---------------------------------
yo` const themeModule = new ThemeModule(registry)
<div> registry.put({ api: themeModule, name: 'themeModule' })
<p>An Opt-in version of <a href="https://matomo.org" target="_blank">Matomo</a>, an open source data analytics platform is being used to improve Remix IDE.</p> themeModule.initTheme(() => {
<p>We realize that our users have sensitive information in their code and that their privacy - your privacy - must be protected.</p> setTimeout(() => {
<p>All data collected through Matomo is stored on our own server - no data is ever given to third parties. Our analytics reports are public: <a href="https://matomo.ethereum.org/index.php?module=MultiSites&action=index&idSite=23&period=day&date=yesterday" target="_blank">take a look</a>.</p> document.body.removeChild(self._view.splashScreen)
<p>We do not collect nor store any personally identifiable information (PII).</p> self._view.el.style.visibility = 'visible'
<p>For more info, see: <a href="https://medium.com/p/66ef69e14931/" target="_blank">Matomo Analyitcs on Remix iDE</a>.</p> }, 1500)
<p>You can change your choice in the Settings panel anytime.</p> })
<div class="d-flex justify-content-around pt-3 border-top"> // ----------------- editor service ----------------------------
<button class="btn btn-primary ${css.matomoBtn}" onclick=${() => onAcceptMatomo()}>Sure</button> const editor = new Editor() // wrapper around ace editor
<button class="btn btn-secondary ${css.matomoBtn}" onclick=${() => onDeclineMatomo()}>Decline</button> registry.put({ api: editor, name: 'editor' })
</div> editor.event.register('requiringToSaveCurrentfile', () => fileManager.saveCurrentFile())
</div>`,
{ // ----------------- fileManager service ----------------------------
label: '', const fileManager = new FileManager(editor, appManager)
fn: null registry.put({ api: fileManager, name: 'filemanager' })
}, // ----------------- dGit provider ---------------------------------
const dGitProvider = new DGitProvider()
// ----------------- import content service ------------------------
const contentImport = new CompilerImports()
const blockchain = new Blockchain(registry.get('config').api)
// ----------------- compilation metadata generation service ---------
const compilerMetadataGenerator = new CompilerMetadata()
// ----------------- compilation result service (can keep track of compilation results) ----------------------------
const compilersArtefacts = new CompilerArtefacts() // store all the compilation results (key represent a compiler name)
registry.put({ api: compilersArtefacts, name: 'compilersartefacts' })
// service which fetch contract artifacts from sourve-verify, put artifacts in remix and compile it
const fetchAndCompile = new FetchAndCompile()
// ----------------- network service (resolve network id / name) -----
const networkModule = new NetworkModule(blockchain)
// ----------------- represent the current selected web3 provider ----
const web3Provider = new Web3ProviderModule(blockchain)
const hardhatProvider = new HardhatProvider(blockchain)
// ----------------- convert offset to line/column service -----------
const offsetToLineColumnConverter = new OffsetToLineColumnConverter()
registry.put({ api: offsetToLineColumnConverter, name: 'offsettolinecolumnconverter' })
// -------------------Terminal----------------------------------------
makeUdapp(blockchain, compilersArtefacts, (domEl) => terminal.logHtml(domEl))
const terminal = new Terminal(
{ appManager, blockchain },
{ {
label: '', getPosition: (event) => {
fn: null var limitUp = 36
var limitDown = 20
var height = window.innerHeight
var newpos = (event.pageY < limitUp) ? limitUp : event.pageY
newpos = (newpos < height - limitDown) ? newpos : height - limitDown
return height - newpos
}
} }
) )
} else { const contextualListener = new ContextualListener({ editor })
startWalkthroughService()
} engine.register([
blockchain,
contentImport,
themeModule,
editor,
fileManager,
compilerMetadataGenerator,
compilersArtefacts,
networkModule,
offsetToLineColumnConverter,
contextualListener,
terminal,
web3Provider,
fetchAndCompile,
dGitProvider,
hardhatProvider
])
// LAYOUT & SYSTEM VIEWS
const appPanel = new MainPanel()
const mainview = new MainView(contextualListener, editor, appPanel, fileManager, appManager, terminal)
registry.put({ api: mainview, name: 'mainview' })
engine.register([
appPanel,
mainview.tabProxy
])
// those views depend on app_manager
const menuicons = new VerticalIcons(appManager)
const sidePanel = new SidePanel(appManager, menuicons)
const hiddenPanel = new HiddenPanel()
const pluginManagerComponent = new PluginManagerComponent(appManager, engine)
const filePanel = new FilePanel(appManager)
const landingPage = new LandingPage(appManager, menuicons, fileManager, filePanel, contentImport)
const settings = new SettingsTab(
registry.get('config').api,
editor,
appManager
)
// CONTENT VIEWS & DEFAULT PLUGINS // adding Views to the DOM
const compileTab = new CompileTab(registry.get('config').api, registry.get('filemanager').api) self._view.mainpanel.appendChild(mainview.render())
const run = new RunTab( self._view.iconpanel.appendChild(menuicons.render())
blockchain, self._view.sidepanel.appendChild(sidePanel.render())
registry.get('config').api, document.body.appendChild(hiddenPanel.render()) // Hidden Panel is display none, it can be directly on body
registry.get('filemanager').api,
registry.get('editor').api, engine.register([
filePanel, menuicons,
registry.get('compilersartefacts').api, landingPage,
networkModule, hiddenPanel,
mainview, sidePanel,
registry.get('fileproviders/browser').api filePanel,
) pluginManagerComponent,
const analysis = new AnalysisTab(registry) settings
const debug = new DebuggerTab() ])
const test = new TestTab(
registry.get('filemanager').api, const queryParams = new QueryParams()
registry.get('offsettolinecolumnconverter').api, const params = queryParams.get()
filePanel,
compileTab, const onAcceptMatomo = () => {
appManager, _paq.push(['forgetUserOptOut'])
contentImport // @TODO remove next line when https://github.com/matomo-org/matomo/commit/9e10a150585522ca30ecdd275007a882a70c6df5 is used
) document.cookie = 'mtm_consent_removed=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
settings.updateMatomoAnalyticsChoice(true)
engine.register([ const el = document.getElementById('modal-dialog')
compileTab, el.parentElement.removeChild(el)
run, startWalkthroughService()
debug, }
analysis, const onDeclineMatomo = () => {
test, settings.updateMatomoAnalyticsChoice(false)
filePanel.remixdHandle, _paq.push(['optUserOut'])
filePanel.gitHandle, const el = document.getElementById('modal-dialog')
filePanel.hardhatHandle, el.parentElement.removeChild(el)
filePanel.slitherHandle startWalkthroughService()
]) }
if (isElectron()) {
appManager.activatePlugin('remixd')
}
try { const startWalkthroughService = () => {
engine.register(await appManager.registeredPlugins()) const walkthroughService = new WalkthroughService(localStorage)
} catch (e) { if (!params.code && !params.url && !params.minimizeterminal && !params.gist && !params.minimizesidepanel) {
console.log('couldn\'t register iframe plugins', e.message) walkthroughService.start()
} }
}
await appManager.activatePlugin(['theme', 'editor', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter']) // Ask to opt in to Matomo for remix, remix-alpha and remix-beta
await appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs']) const matomoDomains = {
await appManager.activatePlugin(['sidePanel']) // activating host plugin separately 'remix-alpha.ethereum.org': 27,
await appManager.activatePlugin(['home']) 'remix-beta.ethereum.org': 25,
await appManager.activatePlugin(['settings']) 'remix.ethereum.org': 23
await appManager.activatePlugin(['hiddenPanel', 'filePanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport']) }
await appManager.registerContextMenuItems() if (matomoDomains[window.location.hostname] && !registry.get('config').api.exists('settings/matomo-analytics')) {
// Set workspace after initial activation modalDialog(
if (Array.isArray(workspace)) { 'Help us to improve Remix IDE',
appManager.activatePlugin(workspace).then(async () => { yo`
try { <div>
if (params.deactivate) { <p>An Opt-in version of <a href="https://matomo.org" target="_blank">Matomo</a>, an open source data analytics platform is being used to improve Remix IDE.</p>
await appManager.deactivatePlugin(params.deactivate.split(',')) <p>We realize that our users have sensitive information in their code and that their privacy - your privacy - must be protected.</p>
<p>All data collected through Matomo is stored on our own server - no data is ever given to third parties. Our analytics reports are public: <a href="https://matomo.ethereum.org/index.php?module=MultiSites&action=index&idSite=23&period=day&date=yesterday" target="_blank">take a look</a>.</p>
<p>We do not collect nor store any personally identifiable information (PII).</p>
<p>For more info, see: <a href="https://medium.com/p/66ef69e14931/" target="_blank">Matomo Analyitcs on Remix iDE</a>.</p>
<p>You can change your choice in the Settings panel anytime.</p>
<div class="d-flex justify-content-around pt-3 border-top">
<button class="btn btn-primary ${css.matomoBtn}" onclick=${() => onAcceptMatomo()}>Sure</button>
<button class="btn btn-secondary ${css.matomoBtn}" onclick=${() => onDeclineMatomo()}>Decline</button>
</div>
</div>`,
{
label: '',
fn: null
},
{
label: '',
fn: null
} }
} catch (e) { )
console.log(e) } else {
} startWalkthroughService()
}
if (params.code) { // CONTENT VIEWS & DEFAULT PLUGINS
// if code is given in url we focus on solidity plugin const compileTab = new CompileTab(registry.get('config').api, registry.get('filemanager').api)
menuicons.select('solidity') const run = new RunTab(
} else { blockchain,
// If plugins are loaded from the URL params, we focus on the last one. registry.get('config').api,
if (pluginLoader.current === 'queryParams' && workspace.length > 0) menuicons.select(workspace[workspace.length - 1]) registry.get('filemanager').api,
} registry.get('editor').api,
filePanel,
registry.get('compilersartefacts').api,
networkModule,
mainview,
registry.get('fileproviders/browser').api
)
const analysis = new AnalysisTab(registry)
const debug = new DebuggerTab()
const test = new TestTab(
registry.get('filemanager').api,
registry.get('offsettolinecolumnconverter').api,
filePanel,
compileTab,
appManager,
contentImport
)
if (params.call) { engine.register([
const callDetails = params.call.split('//') compileTab,
if (callDetails.length > 1) { run,
toolTip(`initiating ${callDetails[0]} ...`) debug,
// @todo(remove the timeout when activatePlugin is on 0.3.0) analysis,
appManager.call(...callDetails).catch(console.error) test,
} filePanel.remixdHandle,
filePanel.gitHandle,
filePanel.hardhatHandle,
filePanel.slitherHandle
])
if (isElectron()) {
appManager.activatePlugin('remixd')
}
try {
engine.register(await appManager.registeredPlugins())
} catch (e) {
console.log('couldn\'t register iframe plugins', e.message)
}
await appManager.activatePlugin(['editor'])
await appManager.activatePlugin(['theme', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
await appManager.activatePlugin(['mainPanel', 'menuicons', 'tabs'])
await appManager.activatePlugin(['sidePanel']) // activating host plugin separately
await appManager.activatePlugin(['home'])
await appManager.activatePlugin(['settings'])
await appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport'])
appManager.on('filePanel', 'workspaceInitializationCompleted', async () => {
await appManager.registerContextMenuItems()
})
await appManager.activatePlugin(['filePanel'])
// Set workspace after initial activation
appManager.on('editor', 'editorMounted', () => {
if (Array.isArray(workspace)) {
appManager.activatePlugin(workspace).then(async () => {
try {
if (params.deactivate) {
await appManager.deactivatePlugin(params.deactivate.split(','))
}
} catch (e) {
console.log(e)
}
if (params.code) {
// if code is given in url we focus on solidity plugin
menuicons.select('solidity')
} else {
// If plugins are loaded from the URL params, we focus on the last one.
if (pluginLoader.current === 'queryParams' && workspace.length > 0) menuicons.select(workspace[workspace.length - 1])
}
if (params.call) {
const callDetails = params.call.split('//')
if (callDetails.length > 1) {
toolTip(`initiating ${callDetails[0]} ...`)
// @todo(remove the timeout when activatePlugin is on 0.3.0)
appManager.call(...callDetails).catch(console.error)
}
}
}).catch(console.error)
} else {
// activate solidity plugin
appManager.activatePlugin(['solidity', 'udapp'])
} }
}).catch(console.error) })
} else {
// activate solidity plugin
appManager.activatePlugin(['solidity', 'udapp'])
}
// Load and start the service who manager layout and frame // Load and start the service who manager layout and frame
const framingService = new FramingService(sidePanel, menuicons, mainview, this._components.resizeFeature) const framingService = new FramingService(sidePanel, menuicons, mainview, this._components.resizeFeature)
if (params.embed) framingService.embed() if (params.embed) framingService.embed()
framingService.start(params) framingService.start(params)
}
} }
module.exports = App

@ -1,84 +0,0 @@
'use strict'
const SourceHighlighter = require('./sourceHighlighter')
class SourceHighlighters {
constructor () {
this.highlighters = {}
}
highlight (position, filePath, hexColor, from) {
// eslint-disable-next-line
try {
if (!this.highlighters[from]) this.highlighters[from] = []
const sourceHighlight = new SourceHighlighter()
if (
!this.highlighters[from].length ||
(this.highlighters[from].length && !this.highlighters[from].find((el) => {
return el.source === filePath && el.position === position
}))
) {
sourceHighlight.currentSourceLocationFromfileName(position, filePath, hexColor)
this.highlighters[from].push(sourceHighlight)
}
} catch (e) {
throw e
}
}
// highlights all locations for @from plugin
highlightAllFrom (from) {
// eslint-disable-next-line
try {
if (!this.highlighters[from]) return
let sourceHighlight
for (const index in this.highlighters[from]) {
sourceHighlight = new SourceHighlighter()
sourceHighlight.currentSourceLocationFromfileName(
this.highlighters[from][index].position,
this.highlighters[from][index].source,
this.highlighters[from][index].style
)
this.highlighters[from][index] = sourceHighlight
}
} catch (e) {
throw e
}
}
discardHighlight (from) {
if (this.highlighters[from]) {
for (const index in this.highlighters[from]) this.highlighters[from][index].currentSourceLocation(null)
}
this.highlighters[from] = []
}
discardAllHighlights () {
for (const from in this.highlighters) {
this.discardHighlight(from)
}
}
hideHighlightsExcept (toStay) {
for (const highlighter in this.highlighters) {
for (const index in this.highlighters[highlighter]) {
this.highlighters[highlighter][index].currentSourceLocation(null)
}
}
this.highlightAllFrom(toStay)
}
discardHighlightAt (line, filePath, from) {
if (this.highlighters[from]) {
for (const index in this.highlighters[from]) {
const highlight = this.highlighters[from][index]
if (highlight.source === filePath &&
(highlight.position.start.line === line || highlight.position.end.line === line)) {
highlight.currentSourceLocation(null)
this.highlighters[from].splice(index, 1)
}
}
}
}
}
module.exports = SourceHighlighters

@ -4,7 +4,6 @@ import * as packageJson from '../../../../../package.json'
import { sourceMappingDecoder } from '@remix-project/remix-debug' import { sourceMappingDecoder } from '@remix-project/remix-debug'
const { AstWalker } = require('@remix-project/remix-astwalker') const { AstWalker } = require('@remix-project/remix-astwalker')
const csjs = require('csjs-inject')
const EventManager = require('../../lib/events') const EventManager = require('../../lib/events')
const globalRegistry = require('../../global/registry') const globalRegistry = require('../../global/registry')
@ -127,14 +126,6 @@ class ContextualListener extends Plugin {
const lastCompilationResult = this._deps.compilersArtefacts.__last const lastCompilationResult = this._deps.compilersArtefacts.__last
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0) { if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0) {
let lineColumn = this._deps.offsetToLineColumnConverter.offsetToLineColumn(position, position.file, lastCompilationResult.getSourceCode().sources, lastCompilationResult.getAsts()) let lineColumn = this._deps.offsetToLineColumnConverter.offsetToLineColumn(position, position.file, lastCompilationResult.getSourceCode().sources, lastCompilationResult.getAsts())
const css = csjs`
.highlightref_fullLine {
position: absolute;
z-index: 2;
opacity: 0.1;
background-color: var(--info);
}
`
if (node.nodes && node.nodes.length) { if (node.nodes && node.nodes.length) {
// If node has children, highlight the entire line. if not, just highlight the current source position of the node. // If node has children, highlight the entire line. if not, just highlight the current source position of the node.
lineColumn = { lineColumn = {
@ -150,7 +141,7 @@ class ContextualListener extends Plugin {
} }
const fileName = lastCompilationResult.getSourceName(position.file) const fileName = lastCompilationResult.getSourceName(position.file)
if (fileName) { if (fileName) {
return this.editor.addMarker(lineColumn, fileName, css.highlightref_fullLine) return this.call('editor', 'highlight', lineColumn, fileName, '', { focus: false })
} }
} }
return null return null
@ -178,10 +169,7 @@ class ContextualListener extends Plugin {
} }
_stopHighlighting () { _stopHighlighting () {
for (const eventKey in this._activeHighlights) { this.call('editor', 'discardHighlight')
const event = this._activeHighlights[eventKey]
this.editor.removeMarker(event.eventId, event.fileTarget)
}
this.event.trigger('stopHighlighting', []) this.event.trigger('stopHighlighting', [])
this._activeHighlights = [] this._activeHighlights = []
} }

@ -1,200 +1,95 @@
'use strict' 'use strict'
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { EditorUI } from '@remix-ui/editor' // eslint-disable-line
import { Plugin } from '@remixproject/engine' import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
const EventManager = require('../../lib/events') const EventManager = require('../../lib/events')
const yo = require('yo-yo')
const csjs = require('csjs-inject')
const ace = require('brace')
const globalRegistry = require('../../global/registry')
const SourceHighlighters = require('./SourceHighlighters')
const Range = ace.acequire('ace/range').Range
require('brace/ext/language_tools')
require('brace/ext/searchbox')
const langTools = ace.acequire('ace/ext/language_tools')
require('ace-mode-solidity/build/remix-ide/mode-solidity')
require('ace-mode-move/build/remix-ide/mode-move')
require('ace-mode-zokrates')
require('ace-mode-lexon')
require('brace/mode/javascript')
require('brace/mode/python')
require('brace/mode/json')
require('brace/mode/rust')
require('brace/theme/chrome') // for all light themes
require('brace/theme/chaos') // for all dark themes
require('../../assets/js/editor/darkTheme') // a custom one for remix 'Dark' theme
const css = csjs`
.ace-editor {
width : 100%;
}
`
document.head.appendChild(yo`
<style>
.ace-tm .ace_gutter,
.ace-tm .ace_gutter-active-line,
.ace-tm .ace_marker-layer .ace_active-line {
background-color: var(--secondary);
}
.ace_gutter-cell.ace_breakpoint{
background-color: var(--secondary);
}
</style>
`)
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', 'discardHighlightAt', 'clearAnnotations', 'addAnnotation', 'gotoLine'] methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine']
} }
class Editor extends Plugin { class Editor extends Plugin {
constructor (opts = {}, themeModule) { constructor () {
super(profile) super(profile)
// Dependancies
this._components = {}
this._components.registry = globalRegistry
this._deps = {
config: this._components.registry.get('config').api
}
this._themes = { this._themes = {
light: 'chrome', light: 'light',
dark: 'chaos', dark: 'vs-dark',
remixDark: 'remixDark' remixDark: 'remix-dark'
} }
themeModule.events.on('themeChanged', (theme) => {
this.setTheme(theme.name === 'Dark' ? 'remixDark' : theme.quality)
})
// Init // Init
this.event = new EventManager() this.event = new EventManager()
this.sessions = {} this.sessions = {}
this.sourceAnnotationsPerFile = [] this.sourceAnnotationsPerFile = {}
this.markerPerFile = {}
this.readOnlySessions = {} this.readOnlySessions = {}
this.previousInput = '' this.previousInput = ''
this.saveTimeout = null this.saveTimeout = null
this.sourceHighlighters = new SourceHighlighters() this.emptySession = null
this.emptySession = this._createSession('')
this.modes = { this.modes = {
sol: 'ace/mode/solidity', sol: 'sol',
yul: 'ace/mode/solidity', yul: 'sol',
mvir: 'ace/mode/move', mvir: 'move',
js: 'ace/mode/javascript', js: 'javascript',
py: 'ace/mode/python', py: 'python',
vy: 'ace/mode/python', vy: 'python',
zok: 'ace/mode/zokrates', zok: 'zokrates',
lex: 'ace/mode/lexon', lex: 'lexon',
txt: 'ace/mode/text', txt: 'text',
json: 'ace/mode/json', json: 'json',
abi: 'ace/mode/json', abi: 'json',
rs: 'ace/mode/rust' rs: 'rust'
} }
// Editor Setup this.activated = false
const el = yo`<div id="input" data-id="editorInput"></div>`
this.editor = ace.edit(el)
ace.acequire('ace/ext/language_tools')
// Unmap ctrl-l & cmd-l
this.editor.commands.bindKeys({
'ctrl-L': null,
'Command-L': null
})
// shortcuts for "Ctrl-"" and "Ctrl+"" to increase/decrease font size of the editor
this.editor.commands.addCommand({
name: 'increasefontsizeEqual',
bindKey: { win: 'Ctrl-=', mac: 'Command-=' },
exec: (editor) => {
this.editorFontSize(1)
},
readOnly: true
})
this.editor.commands.addCommand({
name: 'increasefontsizePlus',
bindKey: { win: 'Ctrl-+', mac: 'Command-+' },
exec: (editor) => {
this.editorFontSize(1)
},
readOnly: true
})
this.editor.commands.addCommand({
name: 'decreasefontsize',
bindKey: { win: 'Ctrl--', mac: 'Command--' },
exec: (editor) => {
this.editorFontSize(-1)
},
readOnly: true
})
this.editor.setShowPrintMargin(false)
this.editor.resize(true)
this.editor.setOptions({
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
})
el.className += ' ' + css['ace-editor']
el.editor = this.editor // required to access the editor during tests
this.render = () => el
// Completer for editor this.events = {
const flowCompleter = { onBreakPointAdded: (file, line) => this.triggerEvent('breakpointAdded', [file, line]),
getCompletions: (editor, session, pos, prefix, callback) => { onBreakPointCleared: (file, line) => this.triggerEvent('breakpointCleared', [file, line]),
// @TODO add here other propositions onDidChangeContent: (file) => this._onChange(file),
} onEditorMounted: () => this.triggerEvent('editorMounted', [])
} }
langTools.addCompleter(flowCompleter)
// zoom with Ctrl+wheel // to be implemented by the react component
window.addEventListener('wheel', (e) => { this.api = {}
if (e.ctrlKey && Math.abs(e.wheelY) > 5) { }
this.editorFontSize(e.wheelY > 0 ? 1 : -1)
}
})
// EVENTS LISTENERS render () {
if (this.el) return this.el
// Gutter Mouse down this.el = document.createElement('div')
this.editor.on('guttermousedown', e => { this.el.setAttribute('id', 'editorView')
const target = e.domEvent.target this.el.currentContent = () => this.currentContent() // used by e2e test
if (target.className.indexOf('ace_gutter-cell') === -1) { this.el.setCurrentContent = (value) => {
return if (this.sessions[this.currentFile]) {
} this.sessions[this.currentFile].setValue(value)
const row = e.getDocumentPosition().row this._onChange(this.currentFile)
const breakpoints = e.editor.session.getBreakpoints()
for (const k in breakpoints) {
if (k === row.toString()) {
this.triggerEvent('breakpointCleared', [this.currentSession, row])
e.editor.session.clearBreakpoint(row)
e.stop()
return
}
} }
this.setBreakpoint(row) }
this.triggerEvent('breakpointAdded', [this.currentSession, row]) this.el.gotoLine = (line) => this.gotoLine(line, 0)
e.stop() return this.el
}) }
// Do setup on initialisation here renderComponent () {
this.editor.on('changeSession', () => { ReactDOM.render(
this._onChange() <EditorUI
this.triggerEvent('sessionSwitched', []) editorAPI={this.api}
this.editor.getSession().on('change', () => { theme={this.currentTheme}
this._onChange() currentFile={this.currentFile}
this.sourceHighlighters.discardAllHighlights() sourceAnnotationsPerFile={this.sourceAnnotationsPerFile}
this.triggerEvent('contentChanged', []) markerPerFile={this.markerPerFile}
}) events={this.events}
}) plugin={this}
/>
, this.el)
} }
triggerEvent (name, params) { triggerEvent (name, params) {
@ -203,14 +98,25 @@ class Editor extends Plugin {
} }
onActivation () { onActivation () {
this.activated = true
this.on('sidePanel', 'focusChanged', (name) => { this.on('sidePanel', 'focusChanged', (name) => {
this.sourceHighlighters.hideHighlightsExcept(name) this.keepDecorationsFor(name, 'sourceAnnotationsPerFile')
this.keepAnnotationsFor(name) this.keepDecorationsFor(name, 'markerPerFile')
}) })
this.on('sidePanel', 'pluginDisabled', (name) => { this.on('sidePanel', 'pluginDisabled', (name) => {
this.sourceHighlighters.discardHighlight(name) this.clearAllDecorationsFor(name)
this.clearAllAnnotationsFor(name) })
const translateTheme = (theme) => this._themes[theme.name === 'Dark' ? 'remixDark' : theme.quality]
this.on('theme', 'themeChanged', (theme) => {
this.currentTheme = translateTheme(theme)
this.renderComponent()
})
this.call('theme', 'currentTheme', (theme) => {
this.currentTheme = translateTheme(theme)
this.renderComponent()
}) })
this.renderComponent()
} }
onDeactivation () { onDeactivation () {
@ -218,30 +124,14 @@ class Editor extends Plugin {
this.off('sidePanel', 'pluginDisabled') this.off('sidePanel', 'pluginDisabled')
} }
highlight (position, filePath, hexColor) { async _onChange (file) {
const { from } = this.currentRequest const currentFile = await this.call('fileManager', 'file')
this.sourceHighlighters.highlight(position, filePath, hexColor, from)
}
discardHighlight () {
const { from } = this.currentRequest
this.sourceHighlighters.discardHighlight(from)
}
discardHighlightAt (line, filePath) {
const { from } = this.currentRequest
this.sourceHighlighters.discardHighlightAt(line, filePath, from)
}
setTheme (type) {
this.editor.setTheme('ace/theme/' + this._themes[type])
}
_onChange () {
const currentFile = this._deps.config.get('currentFile')
if (!currentFile) { if (!currentFile) {
return return
} }
if (currentFile !== file) {
return
}
const input = this.get(currentFile) const input = this.get(currentFile)
if (!input) { if (!input) {
return return
@ -257,16 +147,17 @@ class Editor extends Plugin {
if (this.saveTimeout) { if (this.saveTimeout) {
window.clearTimeout(this.saveTimeout) window.clearTimeout(this.saveTimeout)
} }
this.triggerEvent('contentChanged', [])
this.saveTimeout = window.setTimeout(() => { this.saveTimeout = window.setTimeout(() => {
this.triggerEvent('requiringToSaveCurrentfile', []) this.triggerEvent('requiringToSaveCurrentfile', [])
}, 5000) }, 5000)
} }
_switchSession (path) { _switchSession (path) {
this.currentSession = path this.triggerEvent('sessionSwitched', [])
this.editor.setSession(this.sessions[this.currentSession]) this.currentFile = path
this.editor.setReadOnly(this.readOnlySessions[this.currentSession]) this.renderComponent()
this.editor.focus()
} }
/** /**
@ -283,17 +174,27 @@ class Editor extends Plugin {
} }
/** /**
* Create an Ace session * Create an editor session
* @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 Ace Mode for this file [Default is `text`] * @param {string} mode Mode for this file [Default is `text`]
*/ */
_createSession (content, mode) { _createSession (path, content, mode) {
const s = new ace.EditSession(content) if (!this.activated) return
s.setMode(mode || 'ace/mode/text') this.emit('addModel', content, mode, path, false)
s.setUndoManager(new ace.UndoManager()) return {
s.setTabSize(4) path,
s.setUseSoftTabs(true) language: mode,
return s setValue: (content) => {
this.emit('setValue', path, content)
},
getValue: () => {
return this.api.getValue(path, content)
},
dispose: () => {
this.emit('disposeModel', path)
}
}
} }
/** /**
@ -301,36 +202,16 @@ class Editor extends Plugin {
* @param {string} string * @param {string} string
*/ */
find (string) { find (string) {
return this.editor.find(string) return this.api.findMatches(this.currentFile, string)
} }
/** /**
* Display an Empty read-only session * Display an Empty read-only session
*/ */
displayEmptyReadOnlySession () { displayEmptyReadOnlySession () {
this.currentSession = null if (!this.activated) return
this.editor.setSession(this.emptySession) this.currentFile = null
this.editor.setReadOnly(true) this.emit('addModel', '', 'text', '_blank', true)
}
/**
* Sets a breakpoint on the row number
* @param {number} row Line index of the breakpoint
* @param {string} className Class of the breakpoint
*/
setBreakpoint (row, className) {
this.editor.session.setBreakpoint(row, className)
}
/**
* Increment the font size (in pixels) for the editor text.
* @param {number} incr The amount of pixels to add to the font.
*/
editorFontSize (incr) {
const newSize = this.editor.getFontSize() + incr
if (newSize >= 6) {
this.editor.setFontSize(newSize)
}
} }
/** /**
@ -338,8 +219,8 @@ class Editor extends Plugin {
* @param {string} text New text to be place. * @param {string} text New text to be place.
*/ */
setText (text) { setText (text) {
if (this.currentSession && this.sessions[this.currentSession]) { if (this.currentFile && this.sessions[this.currentFile]) {
this.sessions[this.currentSession].setValue(text) this.sessions[this.currentFile].setValue(text)
} }
} }
@ -356,7 +237,7 @@ class Editor extends Plugin {
- 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]) {
const session = this._createSession(content, this._getMode(path)) const session = this._createSession(path, content, this._getMode(path))
this.sessions[path] = session this.sessions[path] = session
this.readOnlySessions[path] = false this.readOnlySessions[path] = false
} else if (this.sessions[path].getValue() !== content) { } else if (this.sessions[path].getValue() !== content) {
@ -372,7 +253,7 @@ class Editor extends Plugin {
*/ */
openReadOnly (path, content) { openReadOnly (path, content) {
if (!this.sessions[path]) { if (!this.sessions[path]) {
const session = this._createSession(content, this._getMode(path)) const session = this._createSession(path, content, this._getMode(path))
this.sessions[path] = session this.sessions[path] = session
this.readOnlySessions[path] = true this.readOnlySessions[path] = true
} }
@ -394,8 +275,8 @@ class Editor extends Plugin {
* @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.currentSession === path) { if (!path || this.currentFile === path) {
return this.editor.getValue() 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()
} }
@ -407,29 +288,23 @@ class Editor extends Plugin {
* @return {String} path of the current session * @return {String} path of the current session
*/ */
current () { current () {
if (this.editor.getSession() === this.emptySession) { return this.currentFile
return
}
return this.currentSession
} }
/** /**
* The position of the cursor * The position of the cursor
*/ */
getCursorPosition () { getCursorPosition () {
return this.editor.session.doc.positionToIndex( return this.api.getCursorPosition()
this.editor.getCursorPosition(),
0
)
} }
/** /**
* Remove the current session from the list of sessions. * Remove the current session from the list of sessions.
*/ */
discardCurrentSession () { discardCurrentSession () {
if (this.sessions[this.currentSession]) { if (this.sessions[this.currentFile]) {
delete this.sessions[this.currentSession] delete this.sessions[this.currentFile]
this.currentSession = null this.currentFile = null
} }
} }
@ -438,73 +313,56 @@ class Editor extends Plugin {
* @param {string} path * @param {string} path
*/ */
discard (path) { discard (path) {
if (this.sessions[path]) delete this.sessions[path] if (this.sessions[path]) {
if (this.currentSession === path) this.currentSession = null this.sessions[path].dispose()
delete this.sessions[path]
}
if (this.currentFile === path) this.currentFile = null
} }
/** /**
* Resize the editor, and sets whether or not line wrapping is enabled. * Increment the font size (in pixels) for the editor text.
* @param {boolean} useWrapMode Enable (or disable) wrap mode * @param {number} incr The amount of pixels to add to the font.
*/ */
resize (useWrapMode) { editorFontSize (incr) {
this.editor.resize() if (!this.activated) return
const session = this.editor.getSession() const newSize = this.api.getFontSize() + incr
session.setUseWrapMode(useWrapMode) if (newSize >= 6) {
if (session.getUseWrapMode()) { this.emit('setFontSize', newSize)
const characterWidth = this.editor.renderer.characterWidth
const contentWidth = this.editor.container.ownerDocument.getElementsByClassName(
'ace_scroller'
)[0].clientWidth
if (contentWidth > 0) {
session.setWrapLimit(parseInt(contentWidth / characterWidth, 10))
}
} }
} }
/** /**
* Adds a new marker to the given `Range`. * Resize the editor, and sets whether or not line wrapping is enabled.
* @param {*} lineColumnPos * @param {boolean} useWrapMode Enable (or disable) wrap mode
* @param {string} source Path of the session to add the mark on.
* @param {string} cssClass css to apply to the mark.
*/ */
addMarker (lineColumnPos, source, cssClass) { resize (useWrapMode) {
const currentRange = new Range( if (!this.activated) return
lineColumnPos.start.line, this.emit('setWordWrap', useWrapMode)
lineColumnPos.start.column,
lineColumnPos.end.line,
lineColumnPos.end.column
)
if (this.sessions[source]) {
return this.sessions[source].addMarker(currentRange, cssClass)
}
return null
} }
/** /**
* Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to). * Moves the cursor and focus to the specified line and column number
* @param {number} line The line to scroll to * @param {number} line
* @param {boolean} center If true * @param {number} col
* @param {boolean} animate If true animates scrolling
* @param {Function} callback Function to be called when the animation has finished
*/ */
scrollToLine (line, center, animate, callback) { gotoLine (line, col) {
this.editor.scrollToLine(line, center, animate, callback) if (!this.activated) return
this.emit('focus')
this.emit('revealLine', line + 1, col)
} }
/** /**
* Remove a marker from the session * Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to).
* @param {string} markerId Id of the marker * @param {number} line The line to scroll to
* @param {string} source Path of the session
*/ */
removeMarker (markerId, source) { scrollToLine (line) {
if (this.sessions[source]) { if (!this.activated) return
this.sessions[source].removeMarker(markerId) this.emit('revealLine', line + 1, 0)
}
} }
/** /**
* Clears all the annotations 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
row: -1 row: -1
@ -512,64 +370,79 @@ class Editor extends Plugin {
type: "warning" type: "warning"
* @param {String} filePath * @param {String} filePath
* @param {String} plugin * @param {String} plugin
* @param {String} typeOfDecoration
*/ */
clearAnnotationsByPlugin (filePath, plugin) { 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 session = this.sessions[filePath] || this.editor.getSession() const path = filePath || this.currentFile
const path = filePath || this.currentSession
const currentAnnotations = this.sourceAnnotationsPerFile[path] const currentAnnotations = this[typeOfDecoration][path]
if (!currentAnnotations) return if (!currentAnnotations) return
const newAnnotations = [] const newAnnotations = []
for (const annotation of currentAnnotations) { for (const annotation of currentAnnotations) {
if (annotation.from !== plugin) newAnnotations.push(annotation) if (annotation.from !== plugin) newAnnotations.push(annotation)
} }
this.sourceAnnotationsPerFile[path] = newAnnotations
this._setAnnotations(session, path) this[typeOfDecoration][path] = newAnnotations
this.renderComponent()
} }
keepAnnotationsFor (name) { keepDecorationsFor (name, typeOfDecoration) {
if (!this.currentSession) return if (!this.currentFile) return
if (!this.sourceAnnotationsPerFile[this.currentSession]) return if (!this[typeOfDecoration][this.currentFile]) return
const annotations = this.sourceAnnotationsPerFile[this.currentSession] const annotations = this[typeOfDecoration][this.currentFile]
for (const annotation of annotations) { for (const annotation of annotations) {
annotation.hide = annotation.from !== name annotation.hide = annotation.from !== name
} }
this.renderComponent()
this._setAnnotations(this.editor.getSession(), this.currentSession)
} }
/** /**
* 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 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
row: -1 row: -1
text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵" text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵"
type: "warning" type: "warning"
* @param {String} filePath * @param {String} filePath
* @param {String} plugin
*/ */
clearAnnotations (filePath) { clearAllDecorationsFor (plugin) {
const { from } = this.currentRequest for (const session in this.sessions) {
this.clearAnnotationsByPlugin(filePath, from) this.clearDecorationsByPlugin(session, plugin, 'sourceAnnotationsPerFile')
this.clearDecorationsByPlugin(session, plugin, 'markerPerFile')
}
} }
/** /**
* Clears all the annotations and for all the sessions for the given @arg plugin * 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
row: -1 row: -1
text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵" text: "browser/Untitled1.sol: Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.↵"
type: "warning" type: "warning"
* @param {String} filePath * @param {String} filePath
* @param {String} plugin
*/ */
clearAllAnnotationsFor (plugin) { clearAnnotations (filePath) {
for (const session in this.sessions) { filePath = filePath || this.currentFile
this.clearAnnotationsByPlugin(session, plugin) const { from } = this.currentRequest
} this.clearDecorationsByPlugin(filePath, from, 'sourceAnnotationsPerFile')
}
async addDecoration (decoration, filePath, typeOfDecoration) {
if (!filePath) return
filePath = await this.call('fileManager', 'getPathFromUrl', filePath)
filePath = filePath.file
if (!this.sessions[filePath]) throw new Error('file not found' + filePath)
const path = filePath || this.currentFile
const { from } = this.currentRequest
if (!this[typeOfDecoration][path]) this[typeOfDecoration][path] = []
decoration.from = from
this[typeOfDecoration][path].push(decoration)
this.renderComponent()
} }
/** /**
@ -582,32 +455,25 @@ class Editor extends Plugin {
* @param {Object} annotation * @param {Object} annotation
* @param {String} filePath * @param {String} filePath
*/ */
addAnnotation (annotation, filePath) { async addAnnotation (annotation, filePath) {
if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath) filePath = filePath || this.currentFile
const session = this.sessions[filePath] || this.editor.getSession() await this.addDecoration(annotation, filePath, 'sourceAnnotationsPerFile')
const path = filePath || this.currentSession
const { from } = this.currentRequest
if (!this.sourceAnnotationsPerFile[path]) this.sourceAnnotationsPerFile[path] = []
annotation.from = from
this.sourceAnnotationsPerFile[path].push(annotation)
this._setAnnotations(session, path)
} }
_setAnnotations (session, path) { async highlight (position, filePath, highlightColor, opt = { focus: true }) {
const annotations = this.sourceAnnotationsPerFile[path] filePath = filePath || this.currentFile
session.setAnnotations(annotations.filter((element) => !element.hide)) if (opt.focus) {
await this.call('fileManager', 'open', filePath)
this.scrollToLine(position.start.line)
}
await this.addDecoration({ position }, filePath, 'markerPerFile')
} }
/** discardHighlight () {
* Moves the cursor and focus to the specified line and column number const { from } = this.currentRequest
* @param {number} line for (const session in this.sessions) {
* @param {number} col this.clearDecorationsByPlugin(session, from, 'markerPerFile')
*/ }
gotoLine (line, col) {
this.editor.focus()
this.editor.gotoLine(line + 1, col - 1, true)
} }
} }

@ -1,86 +0,0 @@
'use strict'
const csjs = require('csjs-inject')
const globlalRegistry = require('../../global/registry')
class SourceHighlighter {
constructor (localRegistry) {
this._components = {}
this._components.registry = localRegistry || globlalRegistry
// dependencies
this._deps = {
editor: this._components.registry.get('editor').api,
config: this._components.registry.get('config').api,
fileManager: this._components.registry.get('filemanager').api,
compilerArtefacts: this._components.registry.get('compilersartefacts').api
}
this.position = null
this.statementMarker = null
this.fullLineMarker = null
this.source = null
}
currentSourceLocation (lineColumnPos, location) {
if (this.statementMarker) this._deps.editor.removeMarker(this.statementMarker, this.source)
if (this.fullLineMarker) this._deps.editor.removeMarker(this.fullLineMarker, this.source)
const lastCompilationResult = this._deps.compilerArtefacts.__last
if (location && location.file !== undefined && lastCompilationResult) {
const path = lastCompilationResult.getSourceName(location.file)
if (path) {
this.currentSourceLocationFromfileName(lineColumnPos, path)
}
}
}
async currentSourceLocationFromfileName (lineColumnPos, filePath, style) {
if (this.statementMarker) this._deps.editor.removeMarker(this.statementMarker, this.source)
if (this.fullLineMarker) this._deps.editor.removeMarker(this.fullLineMarker, this.source)
this.statementMarker = null
this.fullLineMarker = null
this.source = null
if (lineColumnPos && lineColumnPos.start && lineColumnPos.end) {
this.source = filePath
this.style = style || 'var(--info)'
// if (!this.source) this.source = this._deps.fileManager.currentFile()
if (this._deps.fileManager.currentFile() !== this.source) {
await this._deps.fileManager.open(this.source)
this.source = this._deps.fileManager.currentFile()
}
const css = csjs`
.highlightcode {
position:absolute;
z-index:20;
opacity: 0.3;
background-color: ${this.style};
}
.highlightcode_fullLine {
position:absolute;
z-index:20;
opacity: 0.5;
background-color: ${this.style};
}
.customBackgroundColor {
background-color: ${this.style};
}
`
this.statementMarker = this._deps.editor.addMarker(lineColumnPos, this.source, css.highlightcode.className + ' ' + css.customBackgroundColor.className + ' ' + `highlightLine${lineColumnPos.start.line}`)
this._deps.editor.scrollToLine(lineColumnPos.start.line, true, true, function () {})
this.position = lineColumnPos
if (lineColumnPos.start.line === lineColumnPos.end.line) {
this.fullLineMarker = this._deps.editor.addMarker({
start: {
line: lineColumnPos.start.line,
column: 0
},
end: {
line: lineColumnPos.start.line + 1,
column: 0
}
}, this.source, css.highlightcode_fullLine.className)
}
}
}
}
module.exports = SourceHighlighter

@ -456,7 +456,7 @@ class FileManager extends Plugin {
return this._deps.config.get('currentFile') return this._deps.config.get('currentFile')
} }
closeAllFiles () { async closeAllFiles () {
// TODO: Only keep `this.emit` (issue#2210) // TODO: Only keep `this.emit` (issue#2210)
this.emit('filesAllClosed') this.emit('filesAllClosed')
this.events.emit('filesAllClosed') this.events.emit('filesAllClosed')
@ -465,7 +465,7 @@ class FileManager extends Plugin {
} }
} }
closeFile (name) { async closeFile (name) {
delete this.openedFiles[name] delete this.openedFiles[name]
if (!Object.keys(this.openedFiles).length) { if (!Object.keys(this.openedFiles).length) {
this._deps.config.set('currentFile', '') this._deps.config.set('currentFile', '')
@ -506,7 +506,8 @@ class FileManager extends Plugin {
async setFileContent (path, content) { async setFileContent (path, content) {
if (this.currentRequest) { if (this.currentRequest) {
const canCall = await this.askUserPermission('writeFile', '') const canCall = await this.askUserPermission('writeFile', '')
if (canCall) { const required = this.appManager.isRequired(this.currentRequest.from)
if (canCall && !required) {
// inform the user about modification after permission is granted and even if permission was saved before // inform the user about modification after permission is granted and even if permission was saved before
toaster(yo` toaster(yo`
<div> <div>
@ -611,6 +612,7 @@ class FileManager extends Plugin {
} }
async openFile (file) { async openFile (file) {
file = this.normalize(file)
if (!file) { if (!file) {
this.emit('noFileSelected') this.emit('noFileSelected')
this.events.emit('noFileSelected') this.events.emit('noFileSelected')

@ -15,9 +15,9 @@ module.exports = class RemixDProvider extends FileProvider {
_registerEvent () { _registerEvent () {
var remixdEvents = ['connecting', 'connected', 'errored', 'closed'] var remixdEvents = ['connecting', 'connected', 'errored', 'closed']
remixdEvents.forEach((value) => { remixdEvents.forEach((event) => {
this._appManager.on('remixd', value, (event) => { this._appManager.on('remixd', event, (value) => {
this.event.emit(value, event) this.event.emit(event, value)
}) })
}) })
@ -41,8 +41,18 @@ module.exports = class RemixDProvider extends FileProvider {
this.event.emit('fileRenamed', oldPath, newPath) this.event.emit('fileRenamed', oldPath, newPath)
}) })
this._appManager.on('remixd', 'rootFolderChanged', () => { this._appManager.on('remixd', 'rootFolderChanged', (path) => {
this.event.emit('rootFolderChanged') 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)
})
}) })
} }
@ -57,7 +67,7 @@ module.exports = class RemixDProvider extends FileProvider {
} }
preInit () { preInit () {
this.event.emit('loading') this.event.emit('loadingLocalhost')
} }
init (cb) { init (cb) {
@ -66,6 +76,7 @@ module.exports = class RemixDProvider extends FileProvider {
.then((result) => { .then((result) => {
this._isReady = true this._isReady = true
this._readOnlyMode = result this._readOnlyMode = result
this.event.emit('readOnlyModeChanged', result)
this._registerEvent() this._registerEvent()
this.event.emit('connected') this.event.emit('connected')
cb && cb() cb && cb()
@ -177,9 +188,7 @@ module.exports = class RemixDProvider extends FileProvider {
} }
resolveDirectory (path, callback) { resolveDirectory (path, callback) {
var self = this
if (path[0] === '/') path = path.substring(1) if (path[0] === '/') path = path.substring(1)
if (!path) return callback(null, { [self.type]: { } })
const unprefixedpath = this.removePrefix(path) const unprefixedpath = this.removePrefix(path)
if (!this._isReady) return callback && callback('provider not ready') if (!this._isReady) return callback && callback('provider not ready')

@ -13,6 +13,7 @@ class WorkspaceFileProvider extends FileProvider {
} }
setWorkspace (workspace) { setWorkspace (workspace) {
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
} }
@ -30,7 +31,6 @@ class WorkspaceFileProvider extends FileProvider {
} }
removePrefix (path) { removePrefix (path) {
if (!this.workspace) this.createWorkspace()
path = path.replace(/^\/|\/$/g, '') // remove first and last slash path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path
if (path.startsWith(this.workspace)) return path.replace(this.workspace, this.workspacesPath + '/' + this.workspace) if (path.startsWith(this.workspace)) return path.replace(this.workspace, this.workspacesPath + '/' + this.workspace)
@ -51,7 +51,6 @@ class WorkspaceFileProvider extends FileProvider {
} }
resolveDirectory (path, callback) { resolveDirectory (path, callback) {
if (!this.workspace) this.createWorkspace()
super.resolveDirectory(path, (error, files) => { super.resolveDirectory(path, (error, files) => {
if (error) return callback(error) if (error) return callback(error)
const unscoped = {} const unscoped = {}
@ -76,13 +75,18 @@ class WorkspaceFileProvider extends FileProvider {
} }
_normalizePath (path) { _normalizePath (path) {
if (!this.workspace) this.createWorkspace()
return path.replace(this.workspacesPath + '/' + this.workspace + '/', '') return path.replace(this.workspacesPath + '/' + this.workspace + '/', '')
} }
createWorkspace (name) { async createWorkspace (name) {
if (!name) name = 'default_workspace' try {
this.event.emit('createWorkspace', name) if (!name) name = 'default_workspace'
this.setWorkspace(name)
await super.createDir(name)
this.event.emit('createWorkspace', name)
} catch (e) {
throw new Error(e)
}
} }
} }

@ -3,18 +3,12 @@ import { ViewPlugin } from '@remixproject/engine-web'
import * as packageJson from '../../../../../package.json' import * as packageJson from '../../../../../package.json'
import React from 'react' // eslint-disable-line import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { Workspace } from '@remix-ui/workspace' // eslint-disable-line import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line
import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import { checkSpecialChars, checkSlash } from '../../lib/helper'
const { RemixdHandle } = require('../files/remixd-handle.js') const { RemixdHandle } = require('../files/remixd-handle.js')
const { GitHandle } = require('../files/git-handle.js') const { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js') const { HardhatHandle } = require('../files/hardhat-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js') const { SlitherHandle } = require('../files/slither-handle.js')
const globalRegistry = require('../../global/registry') const globalRegistry = require('../../global/registry')
const examples = require('../editor/examples')
const GistHandler = require('../../lib/gist-handler')
const QueryParams = require('../../lib/query-params')
const modalDialogCustom = require('../ui/modal-dialog-custom')
/* /*
Overview of APIs: Overview of APIs:
* fileManager: @args fileProviders (browser, shared-folder, swarm, github, etc ...) & config & editor * fileManager: @args fileProviders (browser, shared-folder, swarm, github, etc ...) & config & editor
@ -35,8 +29,8 @@ const modalDialogCustom = require('../ui/modal-dialog-custom')
const profile = { const profile = {
name: 'filePanel', name: 'filePanel',
displayName: 'File explorers', displayName: 'File explorers',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace'], methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'],
events: ['setWorkspace', 'renameWorkspace', 'deleteWorkspace', 'createWorkspace'], events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'],
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
description: ' - ', description: ' - ',
kind: 'fileexplorer', kind: 'fileexplorer',
@ -47,54 +41,33 @@ const profile = {
module.exports = class Filepanel extends ViewPlugin { module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) { constructor (appManager) {
super(profile) super(profile)
this._components = {} this.registry = globalRegistry
this._components.registry = globalRegistry this.fileProviders = this.registry.get('fileproviders').api
this._deps = { this.fileManager = this.registry.get('filemanager').api
fileProviders: this._components.registry.get('fileproviders').api,
fileManager: this._components.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._deps.fileProviders.localhost, appManager) this.remixdHandle = new RemixdHandle(this.fileProviders.localhost, appManager)
this.gitHandle = new GitHandle() this.gitHandle = new GitHandle()
this.hardhatHandle = new HardhatHandle() this.hardhatHandle = new HardhatHandle()
this.slitherHandle = new SlitherHandle() this.slitherHandle = new SlitherHandle()
this.registeredMenuItems = []
this.removedMenuItems = []
this.request = {}
this.workspaces = [] this.workspaces = []
this.initialWorkspace = null
this.appManager = appManager this.appManager = appManager
this.currentWorkspaceMetadata = {}
}
onActivation () {
this.renderComponent()
} }
render () { render () {
this.initWorkspace().then(() => this.getWorkspaces()).catch(console.error)
return this.el return this.el
} }
renderComponent () { renderComponent () {
ReactDOM.render( ReactDOM.render(
<Workspace <FileSystemProvider plugin={this} />
createWorkspace={this.createWorkspace.bind(this)}
renameWorkspace={this.renameWorkspace.bind(this)}
setWorkspace={this.setWorkspace.bind(this)}
workspaceRenamed={this.workspaceRenamed.bind(this)}
workspaceDeleted={this.workspaceDeleted.bind(this)}
workspaceCreated={this.workspaceCreated.bind(this)}
workspace={this._deps.fileProviders.workspace}
browser={this._deps.fileProviders.browser}
localhost={this._deps.fileProviders.localhost}
fileManager={this._deps.fileManager}
registry={this._components.registry}
plugin={this}
request={this.request}
workspaces={this.workspaces}
registeredMenuItems={this.registeredMenuItems}
removedMenuItems={this.removedMenuItems}
initialWorkspace={this.initialWorkspace}
/>
, this.el) , this.el)
} }
@ -103,203 +76,103 @@ module.exports = class Filepanel extends ViewPlugin {
* @param callback (...args) => void * @param callback (...args) => void
*/ */
registerContextMenuItem (item) { registerContextMenuItem (item) {
if (!item) throw new Error('Invalid register context menu argument') return new Promise((resolve, reject) => {
if (!item.name || !item.id) throw new Error('Item name and id is mandatory') this.emit('registerContextMenuItemReducerEvent', item, (err, data) => {
if (!item.type && !item.path && !item.extension && !item.pattern) throw new Error('Invalid file matching criteria provided') if (err) reject(err)
if (this.registeredMenuItems.filter((o) => { else resolve(data)
return o.id === item.id && o.name === item.name })
}).length) throw new Error(`Action ${item.name} already exists on ${item.id}`) })
this.registeredMenuItems = [...this.registeredMenuItems, item]
this.removedMenuItems = this.removedMenuItems.filter(menuItem => item.id !== menuItem.id)
this.renderComponent()
} }
removePluginActions (plugin) { removePluginActions (plugin) {
this.registeredMenuItems = this.registeredMenuItems.filter((item) => { return new Promise((resolve, reject) => {
if (item.id !== plugin.name || item.sticky === true) return true this.emit('removePluginActionsReducerEvent', plugin, (err, data) => {
else { if (err) reject(err)
this.removedMenuItems.push(item) else resolve(data)
return false })
}
}) })
this.renderComponent()
} }
async getCurrentWorkspace () { getCurrentWorkspace () {
return await this.request.getCurrentWorkspace() return this.currentWorkspaceMetadata
} }
async getWorkspaces () { getWorkspaces () {
const result = new Promise((resolve, reject) => {
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
this._deps.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => {
if (error) {
console.error(error)
return reject(error)
}
resolve(Object.keys(items)
.filter((item) => items[item].isDirectory)
.map((folder) => folder.replace(workspacesPath + '/', '')))
})
})
try {
this.workspaces = await result
} catch (e) {
modalDialogCustom.alert('Workspaces have not been created on your system. Please use "Migrate old filesystem to workspace" on the home page to transfer your files or start by creating a new workspace in the File Explorers.')
console.log(e)
}
this.renderComponent()
return this.workspaces return this.workspaces
} }
async initWorkspace () { setWorkspaces (workspaces) {
this.renderComponent() this.workspaces = workspaces
const queryParams = new QueryParams() }
const gistHandler = new GistHandler()
const params = queryParams.get()
// get the file from gist
let loadedFromGist = false
if (params.gist) {
await this.processCreateWorkspace('gist-sample')
this._deps.fileProviders.workspace.setWorkspace('gist-sample')
this.initialWorkspace = 'gist-sample'
loadedFromGist = gistHandler.loadFromGist(params, this._deps.fileManager)
}
if (loadedFromGist) return
if (params.code || params.url) {
try {
await this.processCreateWorkspace('code-sample')
this._deps.fileProviders.workspace.setWorkspace('code-sample')
let path = ''
let content = ''
if (params.code) {
var hash = bufferToHex(keccakFromString(params.code))
path = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol'
content = atob(params.code)
await this._deps.fileProviders.workspace.set(path, content)
}
if (params.url) {
const data = await this.call('contentImport', 'resolve', params.url)
path = data.cleanUrl
content = data.content
await this._deps.fileProviders.workspace.set(path, content)
}
this.initialWorkspace = 'code-sample'
await this._deps.fileManager.openFile(path)
} catch (e) {
console.error(e)
}
return
}
const self = this createNewFile () {
this.appManager.on('manager', 'pluginDeactivated', self.removePluginActions.bind(this))
// insert example contracts if there are no files to show
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._deps.fileProviders.browser.resolveDirectory('/', async (error, filesList) => { const provider = this.fileManager.currentFileProvider()
if (error) return reject(error) const dir = provider.workspace || '/'
if (Object.keys(filesList).length === 0) {
await this.createWorkspace('default_workspace')
resolve('default_workspace')
} else {
this._deps.fileProviders.browser.resolveDirectory('.workspaces', async (error, filesList) => {
if (error) return reject(error)
if (Object.keys(filesList).length > 0) {
const workspacePath = Object.keys(filesList)[0].split('/').filter(val => val)
const workspaceName = workspacePath[workspacePath.length - 1]
this._deps.fileProviders.workspace.setWorkspace(workspaceName) this.emit('createNewFileInputReducerEvent', dir, (err, data) => {
return resolve(workspaceName) if (err) reject(err)
} else resolve(data)
return reject(new Error('Can\'t find available workspace.'))
})
}
}) })
}) })
} }
async createNewFile () { uploadFile (target) {
return await this.request.createNewFile() return new Promise((resolve, reject) => {
} const provider = this.fileManager.currentFileProvider()
const dir = provider.workspace || '/'
async uploadFile (event) { return this.emit('uploadFileReducerEvent', dir, target, (err, data) => {
return await this.request.uploadFile(event) if (err) reject(err)
else resolve(data)
})
})
} }
async processCreateWorkspace (name) { createWorkspace (workspaceName, isEmpty) {
const workspaceProvider = this._deps.fileProviders.workspace return new Promise((resolve, reject) => {
const browserProvider = this._deps.fileProviders.browser this.emit('createWorkspaceReducerEvent', workspaceName, isEmpty, (err, data) => {
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name if (err) reject(err)
const workspaceRootPath = 'browser/' + workspaceProvider.workspacesPath else resolve(data || true)
const workspaceRootPathExists = await browserProvider.exists(workspaceRootPath) })
const workspacePathExists = await browserProvider.exists(workspacePath) })
if (!workspaceRootPathExists) browserProvider.createDir(workspaceRootPath)
if (!workspacePathExists) browserProvider.createDir(workspacePath)
} }
async workspaceExists (name) { renameWorkspace (oldName, workspaceName) {
const workspaceProvider = this._deps.fileProviders.workspace return new Promise((resolve, reject) => {
const browserProvider = this._deps.fileProviders.browser this.emit('renameWorkspaceReducerEvent', oldName, workspaceName, (err, data) => {
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name if (err) reject(err)
return browserProvider.exists(workspacePath) else resolve(data || true)
})
})
} }
async createWorkspace (workspaceName, isEmpty = false) { deleteWorkspace (workspaceName) {
if (!workspaceName) throw new Error('name cannot be empty') return new Promise((resolve, reject) => {
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed') this.emit('deleteWorkspaceReducerEvent', workspaceName, (err, data) => {
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists') if (err) reject(err)
else { else resolve(data || true)
await this.call('fileManager', 'closeAllFiles') })
const workspaceProvider = this._deps.fileProviders.workspace })
await this.processCreateWorkspace(workspaceName)
workspaceProvider.setWorkspace(workspaceName)
await this.request.setWorkspace(workspaceName) // tells the react component to switch to that workspace
if (!isEmpty) {
for (const file in examples) {
try {
await workspaceProvider.set(examples[file].name, examples[file].content)
} catch (error) {
console.error(error)
}
}
}
}
} }
async renameWorkspace (oldName, workspaceName) { setWorkspace (workspace) {
if (!workspaceName) throw new Error('name cannot be empty') const workspaceProvider = this.fileProviders.workspace
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists')
const browserProvider = this._deps.fileProviders.browser
const workspacesPath = this._deps.fileProviders.workspace.workspacesPath
browserProvider.rename('browser/' + workspacesPath + '/' + oldName, 'browser/' + workspacesPath + '/' + workspaceName, true)
}
/** these are called by the react component, action is already finished whent it's called */ this.currentWorkspaceMetadata = { name: workspace.name, isLocalhost: workspace.isLocalhost, absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}` }
async setWorkspace (workspace, setEvent = true) { this.emit('setWorkspace', workspace)
if (workspace.isLocalhost) {
this.call('manager', 'activatePlugin', 'remixd')
} else if (await this.call('manager', 'isActive', 'remixd')) {
this.call('manager', 'deactivatePlugin', 'remixd')
}
if (setEvent) {
this._deps.fileManager.setMode(workspace.isLocalhost ? 'localhost' : 'browser')
this.emit('setWorkspace', workspace)
}
} }
workspaceRenamed (workspace) { workspaceRenamed (oldName, workspaceName) {
this.emit('renameWorkspace', workspace) this.emit('workspaceRenamed', oldName, workspaceName)
} }
workspaceDeleted (workspace) { workspaceDeleted (workspace) {
this.emit('deleteWorkspace', workspace) this.emit('workspaceDeleted', workspace)
} }
workspaceCreated (workspace) { workspaceCreated (workspace) {
this.emit('createWorkspace', workspace) this.emit('workspaceCreated', workspace)
} }
/** end section */ /** end section */
} }

@ -44,19 +44,32 @@ export class TabProxy extends Plugin {
fileManager.events.on('fileRemoved', (name) => { fileManager.events.on('fileRemoved', (name) => {
const workspace = this.fileManager.currentWorkspace() const workspace = this.fileManager.currentWorkspace()
workspace ? this.removeTab(workspace + '/' + name) : this.removeTab(this.fileManager.mode + '/' + name)
if (this.fileManager.mode === 'browser') {
name = name.startsWith(workspace + '/') ? name : workspace + '/' + name
this.removeTab(name)
} else {
name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name
this.removeTab(name)
}
}) })
fileManager.events.on('fileClosed', (name) => { fileManager.events.on('fileClosed', (name) => {
const workspace = this.fileManager.currentWorkspace() const workspace = this.fileManager.currentWorkspace()
workspace ? this.removeTab(workspace + '/' + name) : this.removeTab(this.fileManager.mode + '/' + name) if (this.fileManager.mode === 'browser') {
name = name.startsWith(workspace + '/') ? name : workspace + '/' + name
this.removeTab(name)
} else {
name = name.startsWith(this.fileManager.mode + '/') ? name : this.fileManager.mode + '/' + name
this.removeTab(name)
}
}) })
fileManager.events.on('currentFileChanged', (file) => { fileManager.events.on('currentFileChanged', (file) => {
const workspace = this.fileManager.currentWorkspace() const workspace = this.fileManager.currentWorkspace()
if (workspace) { if (this.fileManager.mode === 'browser') {
const workspacePath = workspace + '/' + file const workspacePath = workspace + '/' + file
if (this._handlers[workspacePath]) { if (this._handlers[workspacePath]) {
@ -72,7 +85,7 @@ export class TabProxy extends Plugin {
this.event.emit('closeFile', file) this.event.emit('closeFile', file)
}) })
} else { } else {
const path = this.fileManager.mode + '/' + file const path = file.startsWith(this.fileManager.mode + '/') ? file : this.fileManager.mode + '/' + file
if (this._handlers[path]) { if (this._handlers[path]) {
this._view.filetabs.activateTab(path) this._view.filetabs.activateTab(path)
@ -92,7 +105,7 @@ export class TabProxy extends Plugin {
fileManager.events.on('fileRenamed', (oldName, newName, isFolder) => { fileManager.events.on('fileRenamed', (oldName, newName, isFolder) => {
const workspace = this.fileManager.currentWorkspace() const workspace = this.fileManager.currentWorkspace()
if (workspace) { if (this.fileManager.mode === 'browser') {
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(workspace + '/' + oldName + '/') === 0) {
@ -115,7 +128,7 @@ export class TabProxy extends Plugin {
return return
} }
// should change the tab title too // should change the tab title too
this.renameTab(this.fileManager.mode + '/' + oldName, workspace + '/' + newName) this.renameTab(this.fileManager.mode + '/' + oldName, this.fileManager.mode + '/' + newName)
} }
}) })
@ -297,7 +310,7 @@ export class TabProxy extends Plugin {
this._view.filetabs.canAdd = false this._view.filetabs.canAdd = false
const zoomBtns = yo` const zoomBtns = yo`
<div class="d-flex flex-row justify-content-center align-items-center"> <div class="d-flex flex-row justify-content-center align-items-center" title="Zoom in/out">
<span data-id="tabProxyZoomOut" class="btn btn-sm px-1 fas fa-search-minus text-dark" onclick=${() => this.onZoomOut()}></span> <span data-id="tabProxyZoomOut" class="btn btn-sm px-1 fas fa-search-minus text-dark" onclick=${() => this.onZoomOut()}></span>
<span data-id="tabProxyZoomIn" class="btn btn-sm px-1 fas fa-search-plus text-dark" onclick=${() => this.onZoomIn()}></span> <span data-id="tabProxyZoomIn" class="btn btn-sm px-1 fas fa-search-plus text-dark" onclick=${() => this.onZoomIn()}></span>
</div> </div>
@ -307,6 +320,7 @@ export class TabProxy extends Plugin {
this._view.tabs = yo` this._view.tabs = yo`
<div style="display: -webkit-box; max-height: 32px"> <div style="display: -webkit-box; max-height: 32px">
${zoomBtns} ${zoomBtns}
<i class="d-flex flex-row justify-content-center align-items-center far fa-sliders-v px-1" title="press F1 when focusing the editor to show advanced configuration settings"></i>
${this._view.filetabs} ${this._view.filetabs}
</div> </div>
` `

@ -12,7 +12,6 @@ const AutoCompletePopup = require('../ui/auto-complete-popup')
import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line import { CompilerImports } from '@remix-project/core-plugin' // eslint-disable-line
const globalRegistry = require('../../global/registry') const globalRegistry = require('../../global/registry')
const SourceHighlighter = require('../../app/editor/sourceHighlighter')
const GistHandler = require('../../lib/gist-handler') const GistHandler = require('../../lib/gist-handler')
const KONSOLES = [] const KONSOLES = []
@ -29,12 +28,11 @@ const profile = {
} }
class Terminal extends Plugin { class Terminal extends Plugin {
constructor (opts, api, registry) { constructor (opts, api) {
super(profile) super(profile)
this.fileImport = new CompilerImports() this.fileImport = new CompilerImports()
this.gistHandler = new GistHandler() this.gistHandler = new GistHandler()
this.event = new EventManager() this.event = new EventManager()
this.registry = registry
this.globalRegistry = globalRegistry this.globalRegistry = globalRegistry
this.element = document.createElement('div') this.element = document.createElement('div')
this.element.setAttribute('class', 'panel') this.element.setAttribute('class', 'panel')
@ -42,12 +40,11 @@ class Terminal extends Plugin {
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.sourceHighlighter = new SourceHighlighter()
this._deps = { this._deps = {
fileManager: this.registry.get('filemanager').api, fileManager: this.globalRegistry.get('filemanager').api,
editor: this.registry.get('editor').api, editor: this.globalRegistry.get('editor').api,
compilersArtefacts: this.registry.get('compilersartefacts').api, compilersArtefacts: this.globalRegistry.get('compilersartefacts').api,
offsetToLineColumnConverter: this.registry.get('offsettolinecolumnconverter').api offsetToLineColumnConverter: this.globalRegistry.get('offsettolinecolumnconverter').api
} }
this.commandHelp = { this.commandHelp = {
'remix.loadgist(id)': 'Load a gist in the file explorer.', 'remix.loadgist(id)': 'Load a gist in the file explorer.',
@ -60,7 +57,7 @@ class Terminal extends Plugin {
this.vm = vm this.vm = vm
this._api = api this._api = api
this._opts = opts this._opts = opts
this.config = registry.get('config').api this.config = this.globalRegistry.get('config').api
this.version = packageJson.version this.version = packageJson.version
this.data = { this.data = {
lineLength: opts.lineLength || 80, // ???? lineLength: opts.lineLength || 80, // ????

@ -115,14 +115,16 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
async onActivation () { async onActivation () {
super.onActivation() super.onActivation()
this.call('filePanel', 'registerContextMenuItem', { this.on('filePanel', 'workspaceInitializationCompleted', () => {
id: 'solidity', this.call('filePanel', 'registerContextMenuItem', {
name: 'compileFile', id: 'solidity',
label: 'Compile', name: 'compileFile',
type: [], label: 'Compile',
extension: ['.sol'], type: [],
path: [], extension: ['.sol'],
pattern: [] path: [],
pattern: []
})
}) })
try { try {
this.currentFile = await this.call('fileManager', 'file') this.currentFile = await this.call('fileManager', 'file')

@ -48,7 +48,6 @@ module.exports = class TestTab extends ViewPlugin {
appManager.event.on('activate', (name) => { appManager.event.on('activate', (name) => {
if (name === 'solidity') this.updateRunAction() if (name === 'solidity') this.updateRunAction()
console.log('solidity is activated')
}) })
appManager.event.on('deactivate', (name) => { appManager.event.on('deactivate', (name) => {
if (name === 'solidity') this.updateRunAction() if (name === 'solidity') this.updateRunAction()
@ -224,7 +223,7 @@ module.exports = class TestTab extends ViewPlugin {
runningTests[fileName].content runningTests[fileName].content
) )
await this.call('editor', 'discardHighlight') await this.call('editor', 'discardHighlight')
await this.call('editor', 'highlight', location, fileName) await this.call('editor', 'highlight', location, fileName, '', { focus: true })
} }
} }

@ -20,7 +20,6 @@ class VMProvider {
this.accounts = {} this.accounts = {}
this.RemixSimulatorProvider = new Provider({ fork: this.executionContext.getCurrentFork() }) this.RemixSimulatorProvider = new Provider({ fork: this.executionContext.getCurrentFork() })
this.RemixSimulatorProvider.init() this.RemixSimulatorProvider.init()
this.RemixSimulatorProvider.Accounts.resetAccounts()
this.web3 = new Web3(this.RemixSimulatorProvider) this.web3 = new Web3(this.RemixSimulatorProvider)
extend(this.web3) extend(this.web3)
this.accounts = {} this.accounts = {}

@ -6,7 +6,6 @@ var EventManager = require('../lib/events')
var toolTip = require('../app/ui/tooltip') var toolTip = require('../app/ui/tooltip')
var globalRegistry = require('../global/registry') var globalRegistry = require('../global/registry')
var SourceHighlighter = require('../app/editor/sourceHighlighter')
var GistHandler = require('./gist-handler') var GistHandler = require('./gist-handler')
class CmdInterpreterAPI { class CmdInterpreterAPI {
@ -17,7 +16,6 @@ class CmdInterpreterAPI {
self._components = {} self._components = {}
self._components.registry = localRegistry || globalRegistry self._components.registry = localRegistry || globalRegistry
self._components.terminal = terminal self._components.terminal = terminal
self._components.sourceHighlighter = new SourceHighlighter()
self._components.fileImport = new CompilerImports() self._components.fileImport = new CompilerImports()
self._components.gistHandler = new GistHandler() self._components.gistHandler = new GistHandler()
self._deps = { self._deps = {

@ -81,7 +81,7 @@ commander
const compVersion = commander.compiler const compVersion = commander.compiler
const baseURL = 'https://binaries.soliditylang.org/wasm/' const baseURL = 'https://binaries.soliditylang.org/wasm/'
const response: AxiosResponse = await axios.get(baseURL + 'list.json') const response: AxiosResponse = await axios.get(baseURL + 'list.json')
const { releases, latestRelease } = response.data const { releases, latestRelease } = response.data as { releases: string[], latestRelease: string }
const compString = releases ? releases[compVersion] : null const compString = releases ? releases[compVersion] : null
if (!compString) { if (!compString) {
log.error(`No compiler found in releases with version ${compVersion}`) log.error(`No compiler found in releases with version ${compVersion}`)

@ -15,5 +15,6 @@
"rules": { "rules": {
"no-unused-vars": "off", "no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error" "@typescript-eslint/no-unused-vars": "error"
} },
} "ignorePatterns": ["monaco.d.ts"]
}

@ -0,0 +1,8 @@
# remix-ui-editor
This library was generated with [Nx](https://nx.dev).
Its purpose is to integrate Monaco editor as a react component inside Remix IDE.
## Running unit tests
Run `nx test remix-ui-editor` to execute the unit tests via [Jest](https://jestjs.io).

@ -0,0 +1 @@
export * from './lib/remix-ui-editor'

@ -0,0 +1,135 @@
export interface Action {
type: string;
payload: Record<string, any>
monaco: any,
editor: any
}
export const initialState = {}
export const reducerActions = (models = initialState, action: Action) => {
const monaco = action.monaco
const editor = action.editor
switch (action.type) {
case 'ADD_MODEL': {
if (!editor) return models
const uri = action.payload.uri
const value = action.payload.value
const language = action.payload.language
const readOnly = action.payload.readOnly
if (models[uri]) return models // already existing
models[uri] = { language, uri, readOnly }
const model = monaco.editor.createModel(value, language, monaco.Uri.parse(uri))
models[uri].model = model
model.onDidChangeContent(() => action.payload.events.onDidChangeContent(uri))
return models
}
case 'DISPOSE_MODEL': {
const uri = action.payload.uri
const model = models[uri]?.model
if (model) model.dispose()
delete models[uri]
return models
}
case 'SET_VALUE': {
if (!editor) return models
const uri = action.payload.uri
const value = action.payload.value
const model = models[uri]?.model
if (model) {
model.setValue(value)
}
return models
}
case 'REVEAL_LINE': {
if (!editor) return models
const line = action.payload.line
const column = action.payload.column
editor.revealLine(line)
editor.setPosition({ column, lineNumber: line })
return models
}
case 'FOCUS': {
if (!editor) return models
editor.focus()
return models
}
case 'SET_FONTSIZE': {
if (!editor) return models
const size = action.payload.size
editor.updateOptions({ fontSize: size })
return models
}
case 'SET_WORDWRAP': {
if (!editor) return models
const wrap = action.payload.wrap
editor.updateOptions({ wordWrap: wrap ? 'on' : 'off' })
return models
}
}
}
export const reducerListener = (plugin, dispatch, monaco, editor, events) => {
plugin.on('editor', 'addModel', (value, language, uri, readOnly) => {
dispatch({
type: 'ADD_MODEL',
payload: { uri, value, language, readOnly, events },
monaco,
editor
})
})
plugin.on('editor', 'disposeModel', (uri) => {
dispatch({
type: 'DISPOSE_MODEL',
payload: { uri },
monaco,
editor
})
})
plugin.on('editor', 'setValue', (uri, value) => {
dispatch({
type: 'SET_VALUE',
payload: { uri, value },
monaco,
editor
})
})
plugin.on('editor', 'revealLine', (line, column) => {
dispatch({
type: 'REVEAL_LINE',
payload: { line, column },
monaco,
editor
})
})
plugin.on('editor', 'focus', () => {
dispatch({
type: 'FOCUS',
payload: {},
monaco,
editor
})
})
plugin.on('editor', 'setFontSize', (size) => {
dispatch({
type: 'SET_FONTSIZE',
payload: { size },
monaco,
editor
})
})
plugin.on('editor', 'setWordWrap', (wrap) => {
dispatch({
type: 'SET_WORDWRAP',
payload: { wrap },
monaco,
editor
})
})
}

@ -0,0 +1,11 @@
.hover-row {
white-space: pre;
margin-left : 10px;
background : var(--light);
font-weight : bold;
font-family : monospace;
padding : 10px;
border-radius : 10px;
height: auto;
width: auto;
}

@ -0,0 +1,256 @@
import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line
import Editor from '@monaco-editor/react'
import { reducerActions, reducerListener, initialState } from './actions/editor'
import './remix-ui-editor.css'
type cursorPosition = {
startLineNumber: number,
startColumn: number,
endLineNumber: number,
endColumn: number
}
type sourceAnnotation = {
row: number,
column: number,
text: string,
type: 'error' | 'warning' | 'info'
hide: boolean
from: string // plugin name
}
type sourceMarker = {
position: {
start: {
line: number
column: number
},
end: {
line: number
column: number
}
},
from: string // plugin name
hide: boolean
}
type sourceAnnotationMap = {
[key: string]: [sourceAnnotation];
}
type sourceMarkerMap = {
[key: string]: [sourceMarker];
}
/* eslint-disable-next-line */
export interface EditorUIProps {
activated: boolean
theme: string
currentFile: string
sourceAnnotationsPerFile: sourceAnnotationMap
markerPerFile: sourceMarkerMap
events: {
onBreakPointAdded: (file: string, line: number) => void
onBreakPointCleared: (file: string, line: number) => void
onDidChangeContent: (file: string) => void
onEditorMounted: () => void
}
plugin: {
on: (plugin: string, event: string, listener: any) => void
}
editorAPI:{
findMatches: (uri: string, value: string) => any
getFontSize: () => number,
getValue: (uri: string) => string
getCursorPosition: () => cursorPosition
}
}
export const EditorUI = (props: EditorUIProps) => {
const [, setCurrentBreakpoints] = useState({})
const [currentAnnotations, setCurrentAnnotations] = useState({})
const [currentMarkers, setCurrentMarkers] = useState({})
const editorRef = useRef(null)
const monacoRef = useRef(null)
const currentFileRef = useRef('')
const [editorModelsState, dispatch] = useReducer(reducerActions, initialState)
useEffect(() => {
if (!monacoRef.current) return
monacoRef.current.editor.setTheme(props.theme)
}, [props.theme])
if (monacoRef.current) monacoRef.current.editor.setTheme(props.theme)
const setAnnotationsbyFile = (uri) => {
if (props.sourceAnnotationsPerFile[uri]) {
const model = editorModelsState[uri]?.model
const newAnnotations = []
for (const annotation of props.sourceAnnotationsPerFile[uri]) {
if (!annotation.hide) {
newAnnotations.push({
range: new monacoRef.current.Range(annotation.row + 1, 1, annotation.row + 1, 1),
options: {
isWholeLine: false,
glyphMarginHoverMessage: { value: (annotation.from ? `from ${annotation.from}:\n` : '') + annotation.text },
glyphMarginClassName: `fal fa-exclamation-square text-${annotation.type === 'error' ? 'danger' : (annotation.type === 'warning' ? 'warning' : 'info')}`
}
})
}
}
setCurrentAnnotations(prevState => {
prevState[uri] = model.deltaDecorations(currentAnnotations[uri] || [], newAnnotations)
return prevState
})
}
}
const setMarkerbyFile = (uri) => {
if (props.markerPerFile[uri]) {
const model = editorModelsState[uri]?.model
const newMarkers = []
for (const marker of props.markerPerFile[uri]) {
if (!marker.hide) {
let isWholeLine = false
if (marker.position.start.line === marker.position.end.line && marker.position.end.column - marker.position.start.column < 3) {
// in this case we force highlighting the whole line (doesn't make sense to highlight 2 chars)
isWholeLine = true
}
newMarkers.push({
range: new monacoRef.current.Range(marker.position.start.line + 1, marker.position.start.column + 1, marker.position.end.line + 1, marker.position.end.column + 1),
options: {
isWholeLine,
inlineClassName: `bg-info highlightLine${marker.position.start.line + 1}`
}
})
}
}
setCurrentMarkers(prevState => {
prevState[uri] = model.deltaDecorations(currentMarkers[uri] || [], newMarkers)
return prevState
})
}
}
useEffect(() => {
if (!editorRef.current) return
currentFileRef.current = props.currentFile
editorRef.current.setModel(editorModelsState[props.currentFile].model)
editorRef.current.updateOptions({ readOnly: editorModelsState[props.currentFile].readOnly })
setAnnotationsbyFile(props.currentFile)
setMarkerbyFile(props.currentFile)
}, [props.currentFile])
useEffect(() => {
setAnnotationsbyFile(props.currentFile)
}, [JSON.stringify(props.sourceAnnotationsPerFile)])
useEffect(() => {
setMarkerbyFile(props.currentFile)
}, [JSON.stringify(props.markerPerFile)])
props.editorAPI.findMatches = (uri: string, value: string) => {
if (!editorRef.current) return
const model = editorModelsState[uri]?.model
if (model) return model.findMatches(value)
}
props.editorAPI.getValue = (uri: string) => {
if (!editorRef.current) return
const model = editorModelsState[uri]?.model
if (model) {
return model.getValue()
}
}
props.editorAPI.getCursorPosition = () => {
if (!monacoRef.current) return
const model = editorModelsState[currentFileRef.current]?.model
if (model) {
return model.getOffsetAt(editorRef.current.getPosition())
}
}
props.editorAPI.getFontSize = () => {
if (!editorRef.current) return
return editorRef.current.getOption(42).fontSize
}
(window as any).addRemixBreakpoint = (position) => { // make it available from e2e testing...
const model = editorRef.current.getModel()
if (model) {
setCurrentBreakpoints(prevState => {
const currentFile = currentFileRef.current
if (!prevState[currentFile]) prevState[currentFile] = {}
const decoration = Object.keys(prevState[currentFile]).filter((line) => parseInt(line) === position.lineNumber)
if (decoration.length) {
props.events.onBreakPointCleared(currentFile, position.lineNumber)
model.deltaDecorations([prevState[currentFile][position.lineNumber]], [])
delete prevState[currentFile][position.lineNumber]
} else {
props.events.onBreakPointAdded(currentFile, position.lineNumber)
const decorationIds = model.deltaDecorations([], [{
range: new monacoRef.current.Range(position.lineNumber, 1, position.lineNumber, 1),
options: {
isWholeLine: false,
glyphMarginClassName: 'fas fa-circle text-info'
}
}])
prevState[currentFile][position.lineNumber] = decorationIds[0]
}
return prevState
})
}
}
function handleEditorDidMount (editor) {
editorRef.current = editor
monacoRef.current.editor.setTheme(props.theme)
reducerListener(props.plugin, dispatch, monacoRef.current, editorRef.current, props.events)
props.events.onEditorMounted()
editor.onMouseUp((e) => {
if (e && e.target && e.target.toString().startsWith('GUTTER')) {
(window as any).addRemixBreakpoint(e.target.position)
}
})
}
function handleEditorWillMount (monaco) {
monacoRef.current = monaco
// see https://microsoft.github.io/monaco-editor/playground.html#customizing-the-appearence-exposed-colors
const lightColor = window.getComputedStyle(document.documentElement).getPropertyValue('--light').trim()
const infoColor = window.getComputedStyle(document.documentElement).getPropertyValue('--info').trim()
const darkColor = window.getComputedStyle(document.documentElement).getPropertyValue('--dark').trim()
const grayColor = window.getComputedStyle(document.documentElement).getPropertyValue('--gray-dark').trim()
monaco.editor.defineTheme('remix-dark', {
base: 'vs-dark',
inherit: true, // can also be false to completely replace the builtin rules
rules: [{ background: darkColor.replace('#', '') }],
colors: {
'editor.background': darkColor,
'editorSuggestWidget.background': lightColor,
'editorSuggestWidget.selectedBackground': lightColor,
'editorSuggestWidget.highlightForeground': infoColor,
'editor.lineHighlightBorder': lightColor,
'editor.lineHighlightBackground': grayColor,
'editorGutter.background': lightColor
}
})
}
return (
<Editor
width="100%"
height="100%"
path={props.currentFile}
language={editorModelsState[props.currentFile] ? editorModelsState[props.currentFile].language : 'text'}
onMount={handleEditorDidMount}
beforeMount={handleEditorWillMount}
options= { { glyphMargin: true } }
/>
)
}
export default EditorUI

File diff suppressed because it is too large Load Diff

@ -1,7 +0,0 @@
# remix-ui-file-explorer
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remix-ui-file-explorer` to execute the unit tests via [Jest](https://jestjs.io).

@ -1 +0,0 @@
export * from './lib/file-explorer'

@ -1,384 +0,0 @@
import React from 'react'
import { File } from '../types'
import { extractNameFromKey, extractParentFromKey } from '../utils'
const queuedEvents = []
const pendingEvents = {}
let provider = null
let plugin = null
let dispatch: React.Dispatch<any> = null
export const fetchDirectoryError = (error: any) => {
return {
type: 'FETCH_DIRECTORY_ERROR',
payload: error
}
}
export const fetchDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_DIRECTORY_REQUEST',
payload: promise
}
}
export const fetchDirectorySuccess = (path: string, files: File[]) => {
return {
type: 'FETCH_DIRECTORY_SUCCESS',
payload: { path, files }
}
}
export const fileSystemReset = () => {
return {
type: 'FILESYSTEM_RESET'
}
}
const normalize = (parent, filesList, newInputType?: string): any => {
const folders = {}
const files = {}
Object.keys(filesList || {}).forEach(key => {
key = key.replace(/^\/|\/$/g, '') // remove first and last slash
let path = key
path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (filesList[key].isDirectory) {
folders[extractNameFromKey(key)] = {
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
isDirectory: filesList[key].isDirectory,
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder'
}
} else {
files[extractNameFromKey(key)] = {
path,
name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory,
type: 'file'
}
}
})
if (newInputType === 'folder') {
const path = parent + '/blank'
folders[path] = {
path: path,
name: '',
isDirectory: true,
type: 'folder'
}
} else if (newInputType === 'file') {
const path = parent + '/blank'
files[path] = {
path: path,
name: '',
isDirectory: false,
type: 'file'
}
}
return Object.assign({}, folders, files)
}
const fetchDirectoryContent = async (provider, folderPath: string, newInputType?: string): Promise<any> => {
return new Promise((resolve) => {
provider.resolveDirectory(folderPath, (error, fileTree) => {
if (error) console.error(error)
const files = normalize(folderPath, fileTree, newInputType)
resolve({ [extractNameFromKey(folderPath)]: files })
})
})
}
export const fetchDirectory = (provider, path: string) => (dispatch: React.Dispatch<any>) => {
const promise = fetchDirectoryContent(provider, path)
dispatch(fetchDirectoryRequest(promise))
promise.then((files) => {
dispatch(fetchDirectorySuccess(path, files))
}).catch((error) => {
dispatch(fetchDirectoryError({ error }))
})
return promise
}
export const resolveDirectoryError = (error: any) => {
return {
type: 'RESOLVE_DIRECTORY_ERROR',
payload: error
}
}
export const resolveDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'RESOLVE_DIRECTORY_REQUEST',
payload: promise
}
}
export const resolveDirectorySuccess = (path: string, files: File[]) => {
return {
type: 'RESOLVE_DIRECTORY_SUCCESS',
payload: { path, files }
}
}
export const resolveDirectory = (provider, path: string) => (dispatch: React.Dispatch<any>) => {
const promise = fetchDirectoryContent(provider, path)
dispatch(resolveDirectoryRequest(promise))
promise.then((files) => {
dispatch(resolveDirectorySuccess(path, files))
}).catch((error) => {
dispatch(resolveDirectoryError({ error }))
})
return promise
}
export const fetchProviderError = (error: any) => {
return {
type: 'FETCH_PROVIDER_ERROR',
payload: error
}
}
export const fetchProviderRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_PROVIDER_REQUEST',
payload: promise
}
}
export const fetchProviderSuccess = (provider: any) => {
return {
type: 'FETCH_PROVIDER_SUCCESS',
payload: provider
}
}
export const fileAddedSuccess = (path: string, files) => {
return {
type: 'FILE_ADDED',
payload: { path, files }
}
}
export const folderAddedSuccess = (path: string, files) => {
return {
type: 'FOLDER_ADDED',
payload: { path, files }
}
}
export const fileRemovedSuccess = (path: string, removePath: string) => {
return {
type: 'FILE_REMOVED',
payload: { path, removePath }
}
}
export const fileRenamedSuccess = (path: string, removePath: string, files) => {
return {
type: 'FILE_RENAMED',
payload: { path, removePath, files }
}
}
export const init = (fileProvider, filePanel, registry) => (reducerDispatch: React.Dispatch<any>) => {
provider = fileProvider
plugin = filePanel
dispatch = reducerDispatch
if (provider) {
provider.event.on('fileAdded', async (filePath) => {
await executeEvent('fileAdded', filePath)
})
provider.event.on('folderAdded', async (folderPath) => {
await executeEvent('folderAdded', folderPath)
})
provider.event.on('fileRemoved', async (removePath) => {
await executeEvent('fileRemoved', removePath)
})
provider.event.on('fileRenamed', async (oldPath) => {
await executeEvent('fileRenamed', oldPath)
})
provider.event.on('rootFolderChanged', async () => {
await executeEvent('rootFolderChanged')
})
provider.event.on('fileExternallyChanged', async (path: string, file: { content: string }) => {
const config = registry.get('config').api
const editor = registry.get('editor').api
if (config.get('currentFile') === path && editor.currentContent() !== file.content) {
if (provider.isReadOnly(path)) return editor.setText(file.content)
dispatch(displayNotification(
path + ' changed',
'This file has been changed outside of Remix IDE.',
'Replace by the new content', 'Keep the content displayed in Remix',
() => {
editor.setText(file.content)
}
))
}
})
provider.event.on('fileRenamedError', async () => {
dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel'))
})
dispatch(fetchProviderSuccess(provider))
} else {
dispatch(fetchProviderError('No provider available'))
}
}
export const setCurrentWorkspace = (name: string) => {
return {
type: 'SET_CURRENT_WORKSPACE',
payload: name
}
}
export const addInputFieldSuccess = (path: string, files: File[]) => {
return {
type: 'ADD_INPUT_FIELD',
payload: { path, files }
}
}
export const addInputField = (provider, type: string, path: string) => (dispatch: React.Dispatch<any>) => {
const promise = fetchDirectoryContent(provider, path, type)
promise.then((files) => {
dispatch(addInputFieldSuccess(path, files))
}).catch((error) => {
console.error(error)
})
return promise
}
export const removeInputFieldSuccess = (path: string) => {
return {
type: 'REMOVE_INPUT_FIELD',
payload: { path }
}
}
export const removeInputField = (path: string) => (dispatch: React.Dispatch<any>) => {
return dispatch(removeInputFieldSuccess(path))
}
export const displayNotification = (title: string, message: string, labelOk: string, labelCancel: string, actionOk?: (...args) => void, actionCancel?: (...args) => void) => {
return {
type: 'DISPLAY_NOTIFICATION',
payload: { title, message, labelOk, labelCancel, actionOk, actionCancel }
}
}
export const hideNotification = () => {
return {
type: 'DISPLAY_NOTIFICATION'
}
}
export const closeNotificationModal = () => (dispatch: React.Dispatch<any>) => {
dispatch(hideNotification())
}
const fileAdded = async (filePath: string) => {
if (extractParentFromKey(filePath) === '/.workspaces') return
const path = extractParentFromKey(filePath) || provider.workspace || provider.type || ''
const data = await fetchDirectoryContent(provider, path)
await dispatch(fileAddedSuccess(path, data))
if (filePath.includes('_test.sol')) {
plugin.emit('newTestFileCreated', filePath)
}
}
const folderAdded = async (folderPath: string) => {
if (extractParentFromKey(folderPath) === '/.workspaces') return
const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || ''
const data = await fetchDirectoryContent(provider, path)
await dispatch(folderAddedSuccess(path, data))
}
const fileRemoved = async (removePath: string) => {
const path = extractParentFromKey(removePath) || provider.workspace || provider.type || ''
await dispatch(fileRemovedSuccess(path, removePath))
}
const fileRenamed = async (oldPath: string) => {
const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || ''
const data = await fetchDirectoryContent(provider, path)
await dispatch(fileRenamedSuccess(path, oldPath, data))
}
const rootFolderChanged = async () => {
const workspaceName = provider.workspace || provider.type || ''
await fetchDirectory(provider, workspaceName)(dispatch)
}
const executeEvent = async (eventName: 'fileAdded' | 'folderAdded' | 'fileRemoved' | 'fileRenamed' | 'rootFolderChanged', path?: string) => {
if (Object.keys(pendingEvents).length) {
return queuedEvents.push({ eventName, path })
}
pendingEvents[eventName + path] = { eventName, path }
switch (eventName) {
case 'fileAdded':
await fileAdded(path)
delete pendingEvents[eventName + path]
if (queuedEvents.length) {
const next = queuedEvents.pop()
await executeEvent(next.eventName, next.path)
}
break
case 'folderAdded':
await folderAdded(path)
delete pendingEvents[eventName + path]
if (queuedEvents.length) {
const next = queuedEvents.pop()
await executeEvent(next.eventName, next.path)
}
break
case 'fileRemoved':
await fileRemoved(path)
delete pendingEvents[eventName + path]
if (queuedEvents.length) {
const next = queuedEvents.pop()
await executeEvent(next.eventName, next.path)
}
break
case 'fileRenamed':
await fileRenamed(path)
delete pendingEvents[eventName + path]
if (queuedEvents.length) {
const next = queuedEvents.pop()
await executeEvent(next.eventName, next.path)
}
break
case 'rootFolderChanged':
await rootFolderChanged()
delete pendingEvents[eventName + path]
if (queuedEvents.length) {
const next = queuedEvents.pop()
await executeEvent(next.eventName, next.path)
}
break
}
}

File diff suppressed because it is too large Load Diff

@ -1,348 +0,0 @@
import * as _ from 'lodash'
import { extractNameFromKey } from '../utils'
interface Action {
type: string;
payload: Record<string, any>;
}
export const fileSystemInitialState = {
files: {
files: [],
expandPath: [],
blankPath: null,
isRequesting: false,
isSuccessful: false,
error: null
},
provider: {
provider: null,
isRequesting: false,
isSuccessful: false,
error: null
},
notification: {
title: null,
message: null,
actionOk: () => {},
actionCancel: () => {},
labelOk: null,
labelCancel: null
}
}
export const fileSystemReducer = (state = fileSystemInitialState, action: Action) => {
switch (action.type) {
case 'FETCH_DIRECTORY_REQUEST': {
return {
...state,
files: {
...state.files,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'FETCH_DIRECTORY_SUCCESS': {
return {
...state,
files: {
...state.files,
files: action.payload.files,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FETCH_DIRECTORY_ERROR': {
return {
...state,
files: {
...state.files,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
case 'RESOLVE_DIRECTORY_REQUEST': {
return {
...state,
files: {
...state.files,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'RESOLVE_DIRECTORY_SUCCESS': {
return {
...state,
files: {
...state.files,
files: resolveDirectory(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'RESOLVE_DIRECTORY_ERROR': {
return {
...state,
files: {
...state.files,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
case 'FETCH_PROVIDER_REQUEST': {
return {
...state,
provider: {
...state.provider,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'FETCH_PROVIDER_SUCCESS': {
return {
...state,
provider: {
...state.provider,
provider: action.payload,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FETCH_PROVIDER_ERROR': {
return {
...state,
provider: {
...state.provider,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
case 'ADD_INPUT_FIELD': {
return {
...state,
files: {
...state.files,
files: addInputField(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
blankPath: action.payload.path,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'REMOVE_INPUT_FIELD': {
return {
...state,
files: {
...state.files,
files: removeInputField(state.provider.provider, state.files.blankPath, state.files.files),
blankPath: null,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FILE_ADDED': {
return {
...state,
files: {
...state.files,
files: fileAdded(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
expandPath: [...new Set([...state.files.expandPath, action.payload.path])],
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FOLDER_ADDED': {
return {
...state,
files: {
...state.files,
files: folderAdded(state.provider.provider, action.payload.path, state.files.files, action.payload.files),
expandPath: [...new Set([...state.files.expandPath, action.payload.path])],
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FILE_REMOVED': {
return {
...state,
files: {
...state.files,
files: fileRemoved(state.provider.provider, action.payload.path, action.payload.removePath, state.files.files),
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FILE_RENAMED': {
return {
...state,
files: {
...state.files,
files: fileRenamed(state.provider.provider, action.payload.path, action.payload.removePath, state.files.files, action.payload.files),
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'DISPLAY_NOTIFICATION': {
return {
...state,
notification: {
title: action.payload.title,
message: action.payload.message,
actionOk: action.payload.actionOk || fileSystemInitialState.notification.actionOk,
actionCancel: action.payload.actionCancel || fileSystemInitialState.notification.actionCancel,
labelOk: action.payload.labelOk,
labelCancel: action.payload.labelCancel
}
}
}
case 'HIDE_NOTIFICATION': {
return {
...state,
notification: fileSystemInitialState.notification
}
}
default:
throw new Error()
}
}
const resolveDirectory = (provider, path: string, files, content) => {
const root = provider.workspace || provider.type
if (path === root) return { [root]: { ...content[root], ...files[root] } }
const pathArr: string[] = path.split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
const prevFiles = _.get(files, _path)
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: { ...content[pathArr[pathArr.length - 1]], ...(prevFiles ? prevFiles.child : {}) }
})
return files
}
const removePath = (root, path: string, pathName, files) => {
const pathArr: string[] = path.split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
const prevFiles = _.get(files, _path)
if (prevFiles) {
prevFiles.child && prevFiles.child[pathName] && delete prevFiles.child[pathName]
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: prevFiles ? prevFiles.child : {}
})
}
return files
}
const addInputField = (provider, path: string, files, content) => {
const root = provider.workspace || provider.type || ''
if (path === root) return { [root]: { ...content[root], ...files[root] } }
const result = resolveDirectory(provider, path, files, content)
return result
}
const removeInputField = (provider, path: string, files) => {
const root = provider.workspace || provider.type || ''
if (path === root) {
delete files[root][path + '/' + 'blank']
return files
}
return removePath(root, path, path + '/' + 'blank', files)
}
const fileAdded = (provider, path: string, files, content) => {
return resolveDirectory(provider, path, files, content)
}
const folderAdded = (provider, path: string, files, content) => {
return resolveDirectory(provider, path, files, content)
}
const fileRemoved = (provider, path: string, removedPath: string, files) => {
const root = provider.workspace || provider.type || ''
if (path === root) {
delete files[root][removedPath]
return files
}
return removePath(root, path, extractNameFromKey(removedPath), files)
}
const fileRenamed = (provider, path: string, removePath: string, files, content) => {
const root = provider.workspace || provider.type || ''
if (path === root) {
const allFiles = { [root]: { ...content[root], ...files[root] } }
delete allFiles[root][extractNameFromKey(removePath) || removePath]
return allFiles
}
const pathArr: string[] = path.split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
const prevFiles = _.get(files, _path)
delete prevFiles.child[extractNameFromKey(removePath)]
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: { ...content[pathArr[pathArr.length - 1]], ...prevFiles.child }
})
return files
}

@ -1,59 +0,0 @@
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
export type MenuItems = action[] // eslint-disable-line no-use-before-define
/* eslint-disable-next-line */
export interface FileExplorerProps {
name: string,
registry: any,
filesProvider: any,
menuItems?: string[],
plugin: any,
focusRoot: boolean,
contextMenuItems: MenuItems,
removedContextMenuItems: MenuItems,
displayInput?: boolean,
externalUploads?: EventTarget & HTMLInputElement,
}
export interface File {
path: string,
name: string,
isDirectory: boolean,
type: string,
child?: File[]
}
export interface FileExplorerMenuProps {
title: string,
menuItems: string[],
fileManager: any,
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
publishToGist: (path?: string) => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void
}
export type action = { name: string, type: string[], path: string[], extension: string[], pattern: string[], id: string, multiselect: boolean, label: string }
export interface FileExplorerContextMenuProps {
actions: action[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
deletePath: (path: string | string[]) => void,
renamePath: (path: string, type: string) => void,
hideContextMenu: () => void,
publishToGist?: (path?: string, type?: string) => void,
pushChangesToGist?: (path?: string, type?: string) => void,
publishFolderToGist?: (path?: string, type?: string) => void,
publishFileToGist?: (path?: string, type?: string) => void,
runScript?: (path: string) => void,
emit?: (cmd: customAction) => void,
pageX: number,
pageY: number,
path: string,
type: string,
focus: {key:string, type:string}[],
onMouseOver?: (...args) => void,
copy?: (path: string, type: string) => void,
paste?: (destination: string, type: string) => void
}

@ -1,13 +0,0 @@
export const extractNameFromKey = (key: string): string => {
const keyPath = key.split('/')
return keyPath[keyPath.length - 1]
}
export const extractParentFromKey = (key: string):string => {
if (!key) return
const keyPath = key.split('/')
keyPath.pop()
return keyPath.join('/')
}

@ -0,0 +1 @@
{ "extends": "../../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] }

@ -0,0 +1,3 @@
# remix-ui-helper
This library was generated with [Nx](https://nx.dev).

@ -0,0 +1 @@
export * from './lib/remix-ui-helper'

@ -0,0 +1,63 @@
export const extractNameFromKey = (key: string): string => {
if (!key) return
const keyPath = key.split('/')
return keyPath[keyPath.length - 1]
}
export const extractParentFromKey = (key: string):string => {
if (!key) return
const keyPath = key.split('/')
keyPath.pop()
return keyPath.join('/')
}
export const checkSpecialChars = (name: string) => {
return name.match(/[:*?"<>\\'|]/) != null
}
export const checkSlash = (name: string) => {
return name.match(/\//) != null
}
export const createNonClashingNameAsync = async (name: string, fileManager, prefix = '') => {
if (!name) name = 'Undefined'
let _counter
let ext = 'sol'
const reg = /(.*)\.([^.]+)/g
const split = reg.exec(name)
if (split) {
name = split[1]
ext = split[2]
}
let exist = true
do {
const isDuplicate = await fileManager.exists(name + _counter + prefix + '.' + ext)
if (isDuplicate) _counter = (_counter | 0) + 1
else exist = false
} while (exist)
const counter = _counter || ''
return name + counter + prefix + '.' + ext
}
export const joinPath = (...paths) => {
paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash)
if (paths.length === 1) return paths[0]
return paths.join('/')
}
export const getPathIcon = (path: string) => {
return path.endsWith('.txt')
? 'far fa-file-alt' : path.endsWith('.md')
? 'far fa-file-alt' : path.endsWith('.sol')
? 'fak fa-solidity-mono' : path.endsWith('.js')
? 'fab fa-js' : path.endsWith('.json')
? 'fas fa-brackets-curly' : path.endsWith('.vy')
? 'fak fa-vyper-mono' : path.endsWith('.lex')
? 'fak fa-lexon' : path.endsWith('.contract')
? 'fab fa-ethereum' : 'far fa-file'
}

@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../dist/out-tsc",
"declaration": true,
"rootDir": "./src",
"types": ["node"]
},
"exclude": ["**/*.spec.ts"],
"include": ["**/*.ts"]
}

@ -9,7 +9,7 @@ export interface ModalDialogProps {
cancelFn?: () => void, cancelFn?: () => void,
modalClass?: string, modalClass?: string,
showCancelIcon?: boolean, showCancelIcon?: boolean,
hide: boolean, hide?: boolean,
handleHide: (hideState?: boolean) => void, handleHide: (hideState?: boolean) => void,
children?: React.ReactNode children?: React.ReactNode
} }

@ -110,7 +110,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input <input
className="form-control" className="form-control"
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
value={ name} value={ name || '' }
id="plugin-name" id="plugin-name"
data-id="localPluginName" data-id="localPluginName"
placeholder="Should be camelCase" /> placeholder="Should be camelCase" />
@ -120,7 +120,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input <input
className="form-control" className="form-control"
onChange={e => setDisplayName(e.target.value)} onChange={e => setDisplayName(e.target.value)}
value={ displayName } value={ displayName || '' }
id="plugin-displayname" id="plugin-displayname"
data-id="localPluginDisplayName" data-id="localPluginDisplayName"
placeholder="Name in the header" /> placeholder="Name in the header" />
@ -130,7 +130,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input <input
className="form-control" className="form-control"
onChange={e => setMethods(e.target.value)} onChange={e => setMethods(e.target.value)}
value={ methods } value={ methods || '' }
id="plugin-methods" id="plugin-methods"
data-id="localPluginMethods" data-id="localPluginMethods"
placeholder="Methods" /> placeholder="Methods" />
@ -140,7 +140,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input <input
className="form-control" className="form-control"
onChange={e => setCanactivate(e.target.value)} onChange={e => setCanactivate(e.target.value)}
value={ canactivate } value={ canactivate || '' }
id="plugin-canactivate" id="plugin-canactivate"
data-id="localPluginCanActivate" data-id="localPluginCanActivate"
placeholder="Plugin names" /> placeholder="Plugin names" />
@ -151,7 +151,7 @@ function LocalPluginForm ({ closeModal, visible, pluginManager }: LocalPluginFor
<input <input
className="form-control" className="form-control"
onChange={e => setUrl(e.target.value)} onChange={e => setUrl(e.target.value)}
value={ url } value={ url || '' }
id="plugin-url" id="plugin-url"
data-id="localPluginUrl" data-id="localPluginUrl"
placeholder="ex: https://localhost:8000" /> placeholder="ex: https://localhost:8000" />

@ -204,6 +204,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
const _setCompilerVersionFromPragma = (filename: string) => { const _setCompilerVersionFromPragma = (filename: string) => {
if (!state.allversions) return if (!state.allversions) return
api.readFile(filename).then(data => { api.readFile(filename).then(data => {
if (!data) return
const pragmaArr = data.match(/(pragma solidity (.+?);)/g) const pragmaArr = data.match(/(pragma solidity (.+?);)/g)
if (pragmaArr && pragmaArr.length === 1) { if (pragmaArr && pragmaArr.length === 1) {
const pragmaStr = pragmaArr[0].replace('pragma solidity', '').trim() const pragmaStr = pragmaArr[0].replace('pragma solidity', '').trim()

@ -25,9 +25,9 @@ const ErrorRenderer = ({ message, opt, editor }: ErrorRendererProps) => {
return result return result
} }
const handlePointToErrorOnClick = (location, fileName) => { const handlePointToErrorOnClick = async (location, fileName) => {
editor.call('editor', 'discardHighlight') await editor.call('editor', 'discardHighlight')
editor.call('editor', 'highlight', location, fileName) await editor.call('editor', 'highlight', location, fileName, '', { focus: true })
} }
if (!message) return if (!message) return

@ -6,7 +6,8 @@ import './toaster.css'
/* eslint-disable-next-line */ /* eslint-disable-next-line */
export interface ToasterProps { export interface ToasterProps {
message: string message: string
timeOut?: number timeOut?: number,
handleHide?: () => void
} }
export const Toaster = (props: ToasterProps) => { export const Toaster = (props: ToasterProps) => {
@ -59,6 +60,7 @@ export const Toaster = (props: ToasterProps) => {
if (state.timeOutId) { if (state.timeOutId) {
clearTimeout(state.timeOutId) clearTimeout(state.timeOutId)
} }
props.handleHide && props.handleHide()
setState(prevState => { setState(prevState => {
return { ...prevState, message: '', hide: true, hiding: false, timeOutId: null, showModal: false } return { ...prevState, message: '', hide: true, hiding: false, timeOutId: null, showModal: false }
}) })

@ -1 +1,2 @@
export * from './lib/remix-ui-workspace' export * from './lib/providers/FileSystemProvider'
export * from './lib/contexts'

@ -0,0 +1,187 @@
import { extractParentFromKey } from '@remix-ui/helper'
import React from 'react'
import { action } from '../types'
import { displayNotification, displayPopUp, fileAddedSuccess, fileRemovedSuccess, fileRenamedSuccess, folderAddedSuccess, loadLocalhostError, loadLocalhostRequest, loadLocalhostSuccess, removeContextMenuItem, rootFolderChangedSuccess, setContextMenuItem, setMode, setReadOnlyMode } from './payload'
import { addInputField, createWorkspace, deleteWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from './workspace'
const LOCALHOST = ' - connect to localhost - '
let plugin, dispatch: React.Dispatch<any>
export const listenOnPluginEvents = (filePanelPlugin) => {
plugin = filePanelPlugin
plugin.on('filePanel', 'createWorkspaceReducerEvent', (name: string, isEmpty = false, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
createWorkspace(name, isEmpty, cb)
})
plugin.on('filePanel', 'renameWorkspaceReducerEvent', (oldName: string, workspaceName: string, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
renameWorkspace(oldName, workspaceName, cb)
})
plugin.on('filePanel', 'deleteWorkspaceReducerEvent', (workspaceName: string, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
deleteWorkspace(workspaceName, cb)
})
plugin.on('filePanel', 'registerContextMenuItemReducerEvent', (item: action, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
registerContextMenuItem(item, cb)
})
plugin.on('filePanel', 'removePluginActionsReducerEvent', (plugin, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
removePluginActions(plugin, cb)
})
plugin.on('filePanel', 'createNewFileInputReducerEvent', (path, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
addInputField('file', path, cb)
})
plugin.on('filePanel', 'uploadFileReducerEvent', (dir: string, target, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
uploadFile(target, dir, cb)
})
plugin.on('remixd', 'rootFolderChanged', async (path: string) => {
rootFolderChanged(path)
})
}
export const listenOnProviderEvents = (provider) => (reducerDispatch: React.Dispatch<any>) => {
dispatch = reducerDispatch
provider.event.on('fileAdded', (filePath: string) => {
fileAdded(filePath)
})
provider.event.on('folderAdded', (folderPath: string) => {
if (folderPath.indexOf('/.workspaces') === 0) return
folderAdded(folderPath)
})
provider.event.on('fileRemoved', (removePath: string) => {
fileRemoved(removePath)
})
provider.event.on('fileRenamed', (oldPath: string) => {
fileRenamed(oldPath)
})
provider.event.on('disconnected', async () => {
plugin.fileManager.setMode('browser')
dispatch(setMode('browser'))
dispatch(loadLocalhostError('Remixd disconnected!'))
const workspaceProvider = plugin.fileProviders.workspace
await switchToWorkspace(workspaceProvider.workspace)
})
provider.event.on('connected', () => {
plugin.fileManager.setMode('localhost')
dispatch(setMode('localhost'))
fetchWorkspaceDirectory('/')
dispatch(loadLocalhostSuccess())
})
provider.event.on('loadingLocalhost', async () => {
await switchToWorkspace(LOCALHOST)
dispatch(loadLocalhostRequest())
})
provider.event.on('fileExternallyChanged', (path: string, content: string) => {
const config = plugin.registry.get('config').api
const editor = plugin.registry.get('editor').api
if (config.get('currentFile') === path && editor.currentContent() !== content) {
if (provider.isReadOnly(path)) return editor.setText(content)
dispatch(displayNotification(
path + ' changed',
'This file has been changed outside of Remix IDE.',
'Replace by the new content', 'Keep the content displayed in Remix',
() => {
editor.setText(content)
}
))
}
})
provider.event.on('fileRenamedError', () => {
dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel'))
})
provider.event.on('readOnlyModeChanged', (mode: boolean) => {
dispatch(setReadOnlyMode(mode))
})
}
const registerContextMenuItem = (item: action, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
if (!item) {
cb && cb(new Error('Invalid register context menu argument'))
return dispatch(displayPopUp('Invalid register context menu argument'))
}
if (!item.name || !item.id) {
cb && cb(new Error('Item name and id is mandatory'))
return dispatch(displayPopUp('Item name and id is mandatory'))
}
if (!item.type && !item.path && !item.extension && !item.pattern) {
cb && cb(new Error('Invalid file matching criteria provided'))
return dispatch(displayPopUp('Invalid file matching criteria provided'))
}
dispatch(setContextMenuItem(item))
cb && cb(null, item)
}
const removePluginActions = (plugin, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
dispatch(removeContextMenuItem(plugin))
cb && cb(null, true)
}
const fileAdded = async (filePath: string) => {
await dispatch(fileAddedSuccess(filePath))
if (filePath.includes('_test.sol')) {
plugin.emit('newTestFileCreated', filePath)
}
}
const folderAdded = async (folderPath: string) => {
const provider = plugin.fileManager.currentFileProvider()
const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || ''
const promise = new Promise((resolve) => {
provider.resolveDirectory(path, (error, fileTree) => {
if (error) console.error(error)
resolve(fileTree)
})
})
promise.then((files) => {
folderPath = folderPath.replace(/^\/+/, '')
dispatch(folderAddedSuccess(path, folderPath, files))
}).catch((error) => {
console.error(error)
})
return promise
}
const fileRemoved = async (removePath: string) => {
await dispatch(fileRemovedSuccess(removePath))
}
const fileRenamed = async (oldPath: string) => {
const provider = plugin.fileManager.currentFileProvider()
const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || ''
const promise = new Promise((resolve) => {
provider.resolveDirectory(path, (error, fileTree) => {
if (error) console.error(error)
resolve(fileTree)
})
})
promise.then((files) => {
dispatch(fileRenamedSuccess(path, oldPath, files))
}).catch((error) => {
console.error(error)
})
}
const rootFolderChanged = async (path) => {
await dispatch(rootFolderChangedSuccess(path))
}

@ -0,0 +1,332 @@
import React from 'react'
import { extractNameFromKey, createNonClashingNameAsync } from '@remix-ui/helper'
import Gists from 'gists'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type'
import { displayNotification, displayPopUp, fetchDirectoryError, fetchDirectoryRequest, fetchDirectorySuccess, focusElement, fsInitializationCompleted, hidePopUp, removeInputFieldSuccess, setCurrentWorkspace, setExpandPath, setMode, setWorkspaces } from './payload'
import { listenOnPluginEvents, listenOnProviderEvents } from './events'
import { createWorkspaceTemplate, getWorkspaces, loadWorkspacePreset, setPlugin } from './workspace'
export * from './events'
export * from './workspace'
const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params')
const queryParams = new QueryParams()
let plugin, dispatch: React.Dispatch<any>
export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.Dispatch<any>) => {
if (filePanelPlugin) {
plugin = filePanelPlugin
dispatch = reducerDispatch
setPlugin(plugin, dispatch)
const workspaceProvider = filePanelPlugin.fileProviders.workspace
const localhostProvider = filePanelPlugin.fileProviders.localhost
const params = queryParams.get()
const workspaces = await getWorkspaces() || []
dispatch(setWorkspaces(workspaces))
if (params.gist) {
await createWorkspaceTemplate('gist-sample', 'gist-template')
plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('gist-sample'))
await loadWorkspacePreset('gist-template')
} else if (params.code || params.url) {
await createWorkspaceTemplate('code-sample', 'code-template')
plugin.setWorkspace({ name: 'code-sample', isLocalhost: false })
dispatch(setCurrentWorkspace('code-sample'))
const filePath = await loadWorkspacePreset('code-template')
plugin.on('editor', 'editorMounted', () => plugin.fileManager.openFile(filePath))
} else {
if (workspaces.length === 0) {
await createWorkspaceTemplate('default_workspace', 'default-template')
plugin.setWorkspace({ name: 'default_workspace', isLocalhost: false })
dispatch(setCurrentWorkspace('default_workspace'))
await loadWorkspacePreset('default-template')
} else {
if (workspaces.length > 0) {
workspaceProvider.setWorkspace(workspaces[workspaces.length - 1])
plugin.setWorkspace({ name: workspaces[workspaces.length - 1], isLocalhost: false })
dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1]))
}
}
}
listenOnPluginEvents(plugin)
listenOnProviderEvents(workspaceProvider)(dispatch)
listenOnProviderEvents(localhostProvider)(dispatch)
dispatch(setMode('browser'))
plugin.setWorkspaces(await getWorkspaces())
dispatch(fsInitializationCompleted())
plugin.emit('workspaceInitializationCompleted')
}
}
export const fetchDirectory = async (path: string) => {
const provider = plugin.fileManager.currentFileProvider()
const promise = new Promise((resolve) => {
provider.resolveDirectory(path, (error, fileTree) => {
if (error) console.error(error)
resolve(fileTree)
})
})
dispatch(fetchDirectoryRequest(promise))
promise.then((fileTree) => {
dispatch(fetchDirectorySuccess(path, fileTree))
}).catch((error) => {
dispatch(fetchDirectoryError({ error }))
})
return promise
}
export const removeInputField = async (path: string) => {
dispatch(removeInputFieldSuccess(path))
}
export const publishToGist = async (path?: string, type?: string) => {
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
const folder = path || '/'
const id = type === 'gist' ? extractNameFromKey(path).split('-')[1] : null
try {
const packaged = await packageGistFiles(folder)
// check for token
const config = plugin.registry.get('config').api
const accessToken = config.get('settings/gist-access-token')
if (!accessToken) {
dispatch(displayNotification('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', null, () => {}))
} else {
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' +
queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist='
const gists = new Gists({ token: accessToken })
if (id) {
const originalFileList = await getOriginalFiles(id)
// Telling the GIST API to remove files
const updatedFileList = Object.keys(packaged)
const allItems = Object.keys(originalFileList)
.filter(fileName => updatedFileList.indexOf(fileName) === -1)
.reduce((acc, deleteFileName) => ({
...acc,
[deleteFileName]: null
}), originalFileList)
// adding new files
updatedFileList.forEach((file) => {
const _items = file.split('/')
const _fileName = _items[_items.length - 1]
allItems[_fileName] = packaged[file]
})
dispatch(displayPopUp('Saving gist (' + id + ') ...'))
gists.edit({
description: description,
public: true,
files: allItems,
id: id
}, (error, result) => {
handleGistResponse(error, result)
if (!error) {
for (const key in allItems) {
if (allItems[key] === null) delete allItems[key]
}
}
})
} else {
// id is not existing, need to create a new gist
dispatch(displayPopUp('Creating a new gist ...'))
gists.create({
description: description,
public: true,
files: packaged
}, (error, result) => {
handleGistResponse(error, result)
})
}
}
} catch (error) {
console.log(error)
dispatch(displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => {}))
}
}
export const clearPopUp = async () => {
dispatch(hidePopUp())
}
export const createNewFile = async (path: string, rootDir: string) => {
const fileManager = plugin.fileManager
const newName = await createNonClashingNameAsync(path, fileManager)
const createFile = await fileManager.writeFile(newName, '')
if (!createFile) {
return dispatch(displayPopUp('Failed to create file ' + newName))
} else {
const path = newName.indexOf(rootDir + '/') === 0 ? newName.replace(rootDir + '/', '') : newName
await fileManager.open(path)
setFocusElement([{ key: path, type: 'file' }])
}
}
export const setFocusElement = async (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => {
dispatch(focusElement(elements))
}
export const createNewFolder = async (path: string, rootDir: string) => {
const fileManager = plugin.fileManager
const dirName = path + '/'
const exists = await fileManager.exists(dirName)
if (exists) {
return dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(path)} already exists at this location. Please choose a different name.`, 'Close', null, () => {}))
}
await fileManager.mkdir(dirName)
path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path
dispatch(focusElement([{ key: path, type: 'folder' }]))
}
export const deletePath = async (path: string[]) => {
const fileManager = plugin.fileManager
for (const p of path) {
try {
await fileManager.remove(p)
} catch (e) {
const isDir = await fileManager.isDirectory(p)
dispatch(displayPopUp(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`))
}
}
}
export const renamePath = async (oldPath: string, newPath: string) => {
const fileManager = plugin.fileManager
const exists = await fileManager.exists(newPath)
if (exists) {
dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', null, () => {}))
} else {
await fileManager.rename(oldPath, newPath)
}
}
export const copyFile = async (src: string, dest: string) => {
const fileManager = plugin.fileManager
try {
fileManager.copyFile(src, dest)
} catch (error) {
dispatch(displayPopUp('Oops! An error ocurred while performing copyFile operation.' + error))
}
}
export const copyFolder = async (src: string, dest: string) => {
const fileManager = plugin.fileManager
try {
fileManager.copyDir(src, dest)
} catch (error) {
dispatch(displayPopUp('Oops! An error ocurred while performing copyDir operation.' + error))
}
}
export const runScript = async (path: string) => {
const provider = plugin.fileManager.currentFileProvider()
provider.get(path, (error, content: string) => {
if (error) {
return dispatch(displayPopUp(error))
}
plugin.call('scriptRunner', 'execute', content)
})
}
export const emitContextMenuEvent = async (cmd: customAction) => {
plugin.call(cmd.id, cmd.name, cmd)
}
export const handleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => {
plugin.fileManager.open(path)
dispatch(focusElement([{ key: path, type }]))
}
export const handleExpandPath = (paths: string[]) => {
dispatch(setExpandPath(paths))
}
const packageGistFiles = (directory) => {
return new Promise((resolve, reject) => {
const workspaceProvider = plugin.fileProviders.workspace
const isFile = workspaceProvider.isFile(directory)
const ret = {}
if (isFile) {
try {
workspaceProvider.get(directory, (error, content) => {
if (error) throw new Error('An error ocurred while getting file content. ' + directory)
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
directory = directory.replace(/\//g, '...')
ret[directory] = { content }
return resolve(ret)
})
} catch (e) {
return reject(e)
}
} else {
try {
(async () => {
await workspaceProvider.copyFolderToJson(directory, ({ path, content }) => {
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
if (path.indexOf('gist-') === 0) {
path = path.split('/')
path.shift()
path = path.join('/')
}
path = path.replace(/\//g, '...')
ret[path] = { content }
})
resolve(ret)
})()
} catch (e) {
return reject(e)
}
}
})
}
const handleGistResponse = (error, data) => {
if (error) {
dispatch(displayNotification('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', null))
} else {
if (data.html_url) {
dispatch(displayNotification('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', 'Cancel', () => {
window.open(data.html_url, '_blank')
}, () => {}))
} else {
const error = JSON.stringify(data.errors, null, '\t') || ''
const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message
dispatch(displayNotification('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', null))
}
}
}
/**
* This function is to get the original content of given gist
* @params id is the gist id to fetch
*/
const getOriginalFiles = async (id) => {
if (!id) {
return []
}
const url = `https://api.github.com/gists/${id}`
const res = await fetch(url)
const data = await res.json()
return data.files || []
}

@ -0,0 +1,234 @@
import { action } from '../types'
export const setCurrentWorkspace = (workspace: string) => {
return {
type: 'SET_CURRENT_WORKSPACE',
payload: workspace
}
}
export const setWorkspaces = (workspaces: string[]) => {
return {
type: 'SET_WORKSPACES',
payload: workspaces
}
}
export const setMode = (mode: 'browser' | 'localhost') => {
return {
type: 'SET_MODE',
payload: mode
}
}
export const fetchDirectoryError = (error: any) => {
return {
type: 'FETCH_DIRECTORY_ERROR',
payload: error
}
}
export const fetchDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_DIRECTORY_REQUEST',
payload: promise
}
}
export const fetchDirectorySuccess = (path: string, fileTree) => {
return {
type: 'FETCH_DIRECTORY_SUCCESS',
payload: { path, fileTree }
}
}
export const displayNotification = (title: string, message: string, labelOk: string, labelCancel: string, actionOk?: (...args) => void, actionCancel?: (...args) => void) => {
return {
type: 'DISPLAY_NOTIFICATION',
payload: { title, message, labelOk, labelCancel, actionOk, actionCancel }
}
}
export const hideNotification = () => {
return {
type: 'HIDE_NOTIFICATION'
}
}
export const fileAddedSuccess = (filePath: string) => {
return {
type: 'FILE_ADDED_SUCCESS',
payload: filePath
}
}
export const folderAddedSuccess = (path: string, folderPath: string, fileTree) => {
return {
type: 'FOLDER_ADDED_SUCCESS',
payload: { path, folderPath, fileTree }
}
}
export const fileRemovedSuccess = (removePath: string) => {
return {
type: 'FILE_REMOVED_SUCCESS',
payload: removePath
}
}
export const fileRenamedSuccess = (path: string, oldPath: string, fileTree) => {
return {
type: 'FILE_RENAMED_SUCCESS',
payload: { path, oldPath, fileTree }
}
}
export const rootFolderChangedSuccess = (path: string) => {
return {
type: 'ROOT_FOLDER_CHANGED',
payload: path
}
}
export const addInputFieldSuccess = (path: string, fileTree, type: 'file' | 'folder' | 'gist') => {
return {
type: 'ADD_INPUT_FIELD',
payload: { path, fileTree, type }
}
}
export const removeInputFieldSuccess = (path: string) => {
return {
type: 'REMOVE_INPUT_FIELD',
payload: { path }
}
}
export const setReadOnlyMode = (mode: boolean) => {
return {
type: 'SET_READ_ONLY_MODE',
payload: mode
}
}
export const createWorkspaceError = (error: any) => {
return {
type: 'CREATE_WORKSPACE_ERROR',
payload: error
}
}
export const createWorkspaceRequest = (promise: Promise<any>) => {
return {
type: 'CREATE_WORKSPACE_REQUEST',
payload: promise
}
}
export const createWorkspaceSuccess = (workspaceName: string) => {
return {
type: 'CREATE_WORKSPACE_SUCCESS',
payload: workspaceName
}
}
export const fetchWorkspaceDirectoryError = (error: any) => {
return {
type: 'FETCH_WORKSPACE_DIRECTORY_ERROR',
payload: error
}
}
export const fetchWorkspaceDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_WORKSPACE_DIRECTORY_REQUEST',
payload: promise
}
}
export const fetchWorkspaceDirectorySuccess = (path: string, fileTree) => {
return {
type: 'FETCH_WORKSPACE_DIRECTORY_SUCCESS',
payload: { path, fileTree }
}
}
export const setRenameWorkspace = (oldName: string, workspaceName: string) => {
return {
type: 'RENAME_WORKSPACE',
payload: { oldName, workspaceName }
}
}
export const setDeleteWorkspace = (workspaceName: string) => {
return {
type: 'DELETE_WORKSPACE',
payload: workspaceName
}
}
export const displayPopUp = (message: string) => {
return {
type: 'DISPLAY_POPUP_MESSAGE',
payload: message
}
}
export const hidePopUp = () => {
return {
type: 'HIDE_POPUP_MESSAGE'
}
}
export const focusElement = (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => {
return {
type: 'SET_FOCUS_ELEMENT',
payload: elements
}
}
export const setContextMenuItem = (item: action) => {
return {
type: 'SET_CONTEXT_MENU_ITEM',
payload: item
}
}
export const removeContextMenuItem = (plugin) => {
return {
type: 'REMOVE_CONTEXT_MENU_ITEM',
payload: plugin
}
}
export const setExpandPath = (paths: string[]) => {
return {
type: 'SET_EXPAND_PATH',
payload: paths
}
}
export const loadLocalhostError = (error: any) => {
return {
type: 'LOAD_LOCALHOST_ERROR',
payload: error
}
}
export const loadLocalhostRequest = () => {
return {
type: 'LOAD_LOCALHOST_REQUEST'
}
}
export const loadLocalhostSuccess = () => {
return {
type: 'LOAD_LOCALHOST_SUCCESS'
}
}
export const fsInitializationCompleted = () => {
return {
type: 'FS_INITIALIZATION_COMPLETED'
}
}

@ -0,0 +1,299 @@
import React from 'react'
import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import axios, { AxiosResponse } from 'axios'
import { addInputFieldSuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload'
import { checkSlash, checkSpecialChars } from '@remix-ui/helper'
const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples')
const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params')
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
const queryParams = new QueryParams()
let plugin, dispatch: React.Dispatch<any>
export const setPlugin = (filePanelPlugin, reducerDispatch) => {
plugin = filePanelPlugin
dispatch = reducerDispatch
}
export const addInputField = async (type: 'file' | 'folder', path: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
const provider = plugin.fileManager.currentFileProvider()
const promise = new Promise((resolve, reject) => {
provider.resolveDirectory(path, (error, fileTree) => {
if (error) {
cb && cb(error)
return reject(error)
}
cb && cb(null, true)
resolve(fileTree)
})
})
promise.then((files) => {
dispatch(addInputFieldSuccess(path, files, type))
}).catch((error) => {
console.error(error)
})
return promise
}
export const createWorkspace = async (workspaceName: string, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
await plugin.fileManager.closeAllFiles()
const promise = createWorkspaceTemplate(workspaceName, 'default-template')
dispatch(createWorkspaceRequest(promise))
promise.then(async () => {
dispatch(createWorkspaceSuccess(workspaceName))
plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
plugin.setWorkspaces(await getWorkspaces())
plugin.workspaceCreated(workspaceName)
if (!isEmpty) await loadWorkspacePreset('default-template')
cb && cb(null, workspaceName)
}).catch((error) => {
dispatch(createWorkspaceError({ error }))
cb && cb(error)
})
return promise
}
export const createWorkspaceTemplate = async (workspaceName: string, template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => {
if (!workspaceName) throw new Error('workspace name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await workspaceExists(workspaceName) && template === 'default-template') throw new Error('workspace already exists')
else {
const workspaceProvider = plugin.fileProviders.workspace
await workspaceProvider.createWorkspace(workspaceName)
}
}
export const loadWorkspacePreset = async (template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => {
const workspaceProvider = plugin.fileProviders.workspace
const params = queryParams.get()
switch (template) {
case 'code-template':
// creates a new workspace code-sample and loads code from url params.
try {
let path = ''; let content = ''
if (params.code) {
const hash = bufferToHex(keccakFromString(params.code))
path = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol'
content = atob(params.code)
workspaceProvider.set(path, content)
}
if (params.url) {
const data = await plugin.call('contentImport', 'resolve', params.url)
path = data.cleanUrl
content = data.content
workspaceProvider.set(path, content)
}
return path
} catch (e) {
console.error(e)
}
break
case 'gist-template':
// creates a new workspace gist-sample and get the file from gist
try {
const gistId = params.gist
const response: AxiosResponse = await axios.get(`https://api.github.com/gists/${gistId}`)
const data = response.data as { files: any }
if (!data.files) {
return dispatch(displayNotification('Gist load error', 'No files found', 'OK', null, () => { dispatch(hideNotification()) }, null))
}
const obj = {}
Object.keys(data.files).forEach((element) => {
const path = element.replace(/\.\.\./g, '/')
obj['/' + 'gist-' + gistId + '/' + path] = data.files[element]
})
plugin.fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => {
if (!errorLoadingFile) {
const provider = plugin.fileManager.getProvider('workspace')
provider.lastLoadedGistId = gistId
} else {
dispatch(displayNotification('', errorLoadingFile.message || errorLoadingFile, 'OK', null, () => {}, null))
}
})
} catch (e) {
dispatch(displayNotification('Gist load error', e.message, 'OK', null, () => { dispatch(hideNotification()) }, null))
console.error(e)
}
break
case 'default-template':
// creates a new workspace and populates it with default project template.
// insert example contracts
for (const file in examples) {
try {
await workspaceProvider.set(examples[file].name, examples[file].content)
} catch (error) {
console.error(error)
}
}
break
}
}
export const workspaceExists = async (name: string) => {
const workspaceProvider = plugin.fileProviders.workspace
const browserProvider = plugin.fileProviders.browser
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name
return browserProvider.exists(workspacePath)
}
export const fetchWorkspaceDirectory = async (path: string) => {
if (!path) return
const provider = plugin.fileManager.currentFileProvider()
const promise = new Promise((resolve) => {
provider.resolveDirectory(path, (error, fileTree) => {
if (error) console.error(error)
resolve(fileTree)
})
})
dispatch(fetchWorkspaceDirectoryRequest(promise))
promise.then((fileTree) => {
dispatch(fetchWorkspaceDirectorySuccess(path, fileTree))
}).catch((error) => {
dispatch(fetchWorkspaceDirectoryError({ error }))
})
return promise
}
export const renameWorkspace = async (oldName: string, workspaceName: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
await renameWorkspaceFromProvider(oldName, workspaceName)
await dispatch(setRenameWorkspace(oldName, workspaceName))
plugin.setWorkspace({ name: workspaceName, isLocalhost: false })
plugin.workspaceRenamed(oldName, workspaceName)
cb && cb(null, workspaceName)
}
export const renameWorkspaceFromProvider = async (oldName: string, workspaceName: string) => {
if (!workspaceName) throw new Error('name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await workspaceExists(workspaceName)) throw new Error('workspace already exists')
const browserProvider = plugin.fileProviders.browser
const workspaceProvider = plugin.fileProviders.workspace
const workspacesPath = workspaceProvider.workspacesPath
browserProvider.rename('browser/' + workspacesPath + '/' + oldName, 'browser/' + workspacesPath + '/' + workspaceName, true)
workspaceProvider.setWorkspace(workspaceName)
plugin.setWorkspaces(await getWorkspaces())
}
export const deleteWorkspace = async (workspaceName: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
await deleteWorkspaceFromProvider(workspaceName)
await dispatch(setDeleteWorkspace(workspaceName))
plugin.workspaceDeleted(workspaceName)
cb && cb(null, workspaceName)
}
const deleteWorkspaceFromProvider = async (workspaceName: string) => {
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
await plugin.fileManager.closeAllFiles()
plugin.fileProviders.browser.remove(workspacesPath + '/' + workspaceName)
plugin.setWorkspaces(await getWorkspaces())
}
export const switchToWorkspace = async (name: string) => {
await plugin.fileManager.closeAllFiles()
if (name === LOCALHOST) {
const isActive = await plugin.call('manager', 'isActive', 'remixd')
if (!isActive) await plugin.call('manager', 'activatePlugin', 'remixd')
dispatch(setMode('localhost'))
plugin.emit('setWorkspace', { name: null, isLocalhost: true })
} else if (name === NO_WORKSPACE) {
plugin.fileProviders.workspace.clearWorkspace()
plugin.setWorkspace({ name: null, isLocalhost: false })
dispatch(setCurrentWorkspace(null))
} else {
const isActive = await plugin.call('manager', 'isActive', 'remixd')
if (isActive) plugin.call('manager', 'deactivatePlugin', 'remixd')
await plugin.fileProviders.workspace.setWorkspace(name)
plugin.setWorkspace({ name, isLocalhost: false })
dispatch(setMode('browser'))
dispatch(setCurrentWorkspace(name))
dispatch(setReadOnlyMode(false))
}
}
export const uploadFile = async (target, targetFolder: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => {
// TODO The file explorer is merely a view on the current state of
// the files module. Please ask the user here if they want to overwrite
// a file and then just use `files.add`. The file explorer will
// pick that up via the 'fileAdded' event from the files module.
[...target.files].forEach((file) => {
const workspaceProvider = plugin.fileProviders.workspace
const loadFile = (name: string): void => {
const fileReader = new FileReader()
fileReader.onload = async function (event) {
if (checkSpecialChars(file.name)) {
return dispatch(displayNotification('File Upload Failed', 'Special characters are not allowed', 'Close', null, async () => {}))
}
const success = await workspaceProvider.set(name, event.target.result)
if (!success) {
return dispatch(displayNotification('File Upload Failed', 'Failed to create file ' + name, 'Close', null, async () => {}))
}
const config = plugin.registry.get('config').api
const editor = plugin.registry.get('editor').api
if ((config.get('currentFile') === name) && (editor.currentContent() !== event.target.result)) {
editor.setText(event.target.result)
}
}
fileReader.readAsText(file)
cb && cb(null, true)
}
const name = `${targetFolder}/${file.name}`
workspaceProvider.exists(name).then(exist => {
if (!exist) {
loadFile(name)
} else {
dispatch(displayNotification('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', null, () => {
loadFile(name)
}, () => {}))
}
}).catch(error => {
cb && cb(error)
if (error) console.log(error)
})
})
}
export const getWorkspaces = async (): Promise<string[]> | undefined => {
try {
const workspaces: string[] = await new Promise((resolve, reject) => {
const workspacesPath = plugin.fileProviders.workspace.workspacesPath
plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => {
if (error) {
return reject(error)
}
resolve(Object.keys(items)
.filter((item) => items[item].isDirectory)
.map((folder) => folder.replace(workspacesPath + '/', '')))
})
})
plugin.setWorkspaces(workspaces)
return workspaces
} catch (e) {}
}

@ -1,7 +1,7 @@
import React, { useRef, useEffect } from 'react' // eslint-disable-line import React, { useRef, useEffect } from 'react' // eslint-disable-line
import { action, FileExplorerContextMenuProps } from './types' import { action, FileExplorerContextMenuProps } from '../types'
import './css/file-explorer-context-menu.css' import '../css/file-explorer-context-menu.css'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
declare global { declare global {

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react' //eslint-disable-line import React, { useState, useEffect } from 'react' //eslint-disable-line
import { FileExplorerMenuProps } from './types' import { FileExplorerMenuProps } from '../types'
export const FileExplorerMenu = (props: FileExplorerMenuProps) => { export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
const [state, setState] = useState({ const [state, setState] = useState({

@ -0,0 +1,473 @@
import React, { useEffect, useState, useContext, SyntheticEvent } from 'react' // eslint-disable-line
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerProps, MenuItems, FileExplorerState } from '../types'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
import { contextMenuActions } from '../utils'
import '../css/file-explorer.css'
import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } from '@remix-ui/helper'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileRender } from './file-render'
export const FileExplorer = (props: FileExplorerProps) => {
const { name, contextMenuItems, removedContextMenuItems, files } = props
const [state, setState] = useState<FileExplorerState>({
ctrlKey: false,
newFileName: '',
actions: contextMenuActions,
focusContext: {
element: null,
x: null,
y: null,
type: ''
},
focusEdit: {
element: null,
type: '',
isNew: false,
lastEdit: ''
},
mouseOverElement: null,
showContextMenu: false,
reservedKeywords: [name, 'gist-'],
copyElement: []
})
const [canPaste, setCanPaste] = useState(false)
useEffect(() => {
if (contextMenuItems) {
addMenuItems(contextMenuItems)
}
}, [contextMenuItems])
useEffect(() => {
if (removedContextMenuItems) {
removeMenuItems(removedContextMenuItems)
}
}, [contextMenuItems])
useEffect(() => {
if (props.focusEdit) {
setState(prevState => {
return { ...prevState, focusEdit: { element: props.focusEdit, type: 'file', isNew: true, lastEdit: null } }
})
}
}, [props.focusEdit])
useEffect(() => {
const keyPressHandler = (e: KeyboardEvent) => {
if (e.shiftKey) {
setState(prevState => {
return { ...prevState, ctrlKey: true }
})
}
}
const keyUpHandler = (e: KeyboardEvent) => {
if (!e.shiftKey) {
setState(prevState => {
return { ...prevState, ctrlKey: false }
})
}
}
document.addEventListener('keydown', keyPressHandler)
document.addEventListener('keyup', keyUpHandler)
return () => {
document.removeEventListener('keydown', keyPressHandler)
document.removeEventListener('keyup', keyUpHandler)
}
}, [])
useEffect(() => {
if (canPaste) {
addMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
} else {
removeMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
}
}, [canPaste])
const addMenuItems = (items: MenuItems) => {
setState(prevState => {
// filter duplicate items
const actions = items.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1)
return { ...prevState, actions: [...prevState.actions, ...actions] }
})
}
const removeMenuItems = (items: MenuItems) => {
setState(prevState => {
const actions = prevState.actions.filter(({ id, name }) => items.findIndex(item => id === item.id && name === item.name) === -1)
return { ...prevState, actions }
})
}
const hasReservedKeyword = (content: string): boolean => {
if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true
else return false
}
const getFocusedFolder = () => {
if (props.focusElement[0]) {
if (props.focusElement[0].type === 'folder' && props.focusElement[0].key) return props.focusElement[0].key
else if (props.focusElement[0].type === 'gist' && props.focusElement[0].key) return props.focusElement[0].key
else if (props.focusElement[0].type === 'file' && props.focusElement[0].key) return extractParentFromKey(props.focusElement[0].key) ? extractParentFromKey(props.focusElement[0].key) : name
else return name
}
}
const createNewFile = async (newFilePath: string) => {
try {
props.dispatchCreateNewFile(newFilePath, props.name)
} catch (error) {
return props.modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {})
}
}
const createNewFolder = async (newFolderPath: string) => {
try {
props.dispatchCreateNewFolder(newFolderPath, props.name)
} catch (e) {
return props.modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {})
}
}
const deletePath = async (path: string[]) => {
if (props.readonly) return props.toast('cannot delete file. ' + name + ' is a read only explorer')
if (!Array.isArray(path)) path = [path]
props.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', () => { props.dispatchDeletePath(path) }, 'Cancel', () => {})
}
const renamePath = async (oldPath: string, newPath: string) => {
try {
props.dispatchRenamePath(oldPath, newPath)
} catch (error) {
props.modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {})
}
}
const uploadFile = (target) => {
const parentFolder = getFocusedFolder()
const expandPath = [...new Set([...props.expandPath, parentFolder])]
props.dispatchHandleExpandPath(expandPath)
props.dispatchUploadFile(target, parentFolder)
}
const copyFile = (src: string, dest: string) => {
try {
props.dispatchCopyFile(src, dest)
} catch (error) {
props.modal('Copy File Failed', 'Unexpected error while copying file: ' + src, 'Close', async () => {})
}
}
const copyFolder = (src: string, dest: string) => {
try {
props.dispatchCopyFolder(src, dest)
} catch (error) {
props.modal('Copy Folder Failed', 'Unexpected error while copying folder: ' + src, 'Close', async () => {})
}
}
const publishToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const pushChangesToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFolderToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const publishFileToGist = (path?: string, type?: string) => {
props.modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
}
const toGist = (path?: string, type?: string) => {
props.dispatchPublishToGist(path, type)
}
const runScript = async (path: string) => {
try {
props.dispatchRunScript(path)
} catch (error) {
props.toast('Run script failed')
}
}
const emitContextMenuEvent = (cmd: customAction) => {
try {
props.dispatchEmitContextMenuEvent(cmd)
} catch (error) {
props.toast(error)
}
}
const handleClickFile = (path: string, type: 'folder' | 'file' | 'gist') => {
path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path
if (!state.ctrlKey) {
props.dispatchHandleClickFile(path, type)
} else {
if (props.focusElement.findIndex(item => item.key === path) !== -1) {
const focusElement = props.focusElement.filter(item => item.key !== path)
props.dispatchSetFocusElement(focusElement)
} else {
const nonRootFocus = props.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') })
nonRootFocus.push({ key: path, type })
props.dispatchSetFocusElement(nonRootFocus)
}
}
}
const handleClickFolder = async (path: string, type: 'folder' | 'file' | 'gist') => {
if (state.ctrlKey) {
if (props.focusElement.findIndex(item => item.key === path) !== -1) {
const focusElement = props.focusElement.filter(item => item.key !== path)
props.dispatchSetFocusElement(focusElement)
} else {
const nonRootFocus = props.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') })
nonRootFocus.push({ key: path, type })
props.dispatchSetFocusElement(nonRootFocus)
}
} else {
let expandPath = []
if (!props.expandPath.includes(path)) {
expandPath = [...new Set([...props.expandPath, path])]
props.dispatchFetchDirectory(path)
} else {
expandPath = [...new Set(props.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))]
}
props.dispatchSetFocusElement([{ key: path, type }])
props.dispatchHandleExpandPath(expandPath)
}
}
const handleContextMenu = (pageX: number, pageY: number, path: string, content: string, type: string) => {
if (!content) return
setState(prevState => {
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path }
})
}
const hideContextMenu = () => {
setState(prevState => {
return { ...prevState, focusContext: { element: null, x: 0, y: 0, type: '' }, showContextMenu: false }
})
}
const editModeOn = (path: string, type: string, isNew: boolean = false) => {
if (props.readonly) return props.toast('Cannot write/modify file system in read only mode.')
setState(prevState => {
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } }
})
}
const editModeOff = async (content: string) => {
if (typeof content === 'string') content = content.trim()
const parentFolder = extractParentFromKey(state.focusEdit.element)
if (!content || (content.trim() === '')) {
if (state.focusEdit.isNew) {
props.dispatchRemoveInputField(parentFolder)
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
} else {
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
} else {
if (state.focusEdit.lastEdit === content) {
return setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
if (checkSpecialChars(content)) {
props.modal('Validation Error', 'Special characters are not allowed', 'OK', () => {})
} else {
if (state.focusEdit.isNew) {
if (hasReservedKeyword(content)) {
props.dispatchRemoveInputField(parentFolder)
props.modal('Reserved Keyword', `File name contains Remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content))
props.dispatchRemoveInputField(parentFolder)
}
} else {
if (hasReservedKeyword(content)) {
props.modal('Reserved Keyword', `File name contains Remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
if (state.focusEdit.element) {
const oldPath: string = state.focusEdit.element
const oldName = extractNameFromKey(oldPath)
const newPath = oldPath.replace(oldName, content)
renamePath(oldPath, newPath)
}
}
}
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
}
}
const handleNewFileInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = getFocusedFolder()
const expandPath = [...new Set([...props.expandPath, parentFolder])]
await props.dispatchAddInputField(parentFolder, 'file')
props.dispatchHandleExpandPath(expandPath)
editModeOn(parentFolder + '/blank', 'file', true)
}
const handleNewFolderInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = getFocusedFolder()
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder)
const expandPath = [...new Set([...props.expandPath, parentFolder])]
await props.dispatchAddInputField(parentFolder, 'folder')
props.dispatchHandleExpandPath(expandPath)
editModeOn(parentFolder + '/blank', 'folder', true)
}
const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file') => {
setState(prevState => {
return { ...prevState, copyElement: [{ key: path, type }] }
})
setCanPaste(true)
props.toast(`Copied to clipboard ${path}`)
}
const handlePasteClick = (dest: string, destType: string) => {
dest = destType === 'file' ? extractParentFromKey(dest) || props.name : dest
state.copyElement.map(({ key, type }) => {
type === 'file' ? copyFile(key, dest) : copyFolder(key, dest)
})
}
const deleteMessage = (path: string[]) => {
return (
<div>
<div>Are you sure you want to delete {path.length > 1 ? 'these items' : 'this item'}?</div>
{
path.map((item, i) => (<li key={i}>{item}</li>))
}
</div>
)
}
const handleFileExplorerMenuClick = (e: SyntheticEvent) => {
e.stopPropagation()
if (e && (e.target as any).getAttribute('data-id') === 'fileExplorerUploadFileuploadFile') return // we don't want to let propagate the input of type file
if (e && (e.target as any).getAttribute('data-id') === 'fileExplorerFileUpload') return // we don't want to let propagate the input of type file
let expandPath = []
if (!props.expandPath.includes(props.name)) {
expandPath = [props.name, ...new Set([...props.expandPath])]
} else {
expandPath = [...new Set(props.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(props.name)))]
}
props.dispatchHandleExpandPath(expandPath)
}
return (
<div>
<TreeView id='treeView'>
<TreeViewItem id="treeViewItem"
controlBehaviour={true}
label={
<div onClick={handleFileExplorerMenuClick}>
<FileExplorerMenu
title={''}
menuItems={props.menuItems}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
publishToGist={publishToGist}
uploadFile={uploadFile}
/>
</div>
}
expand={true}>
<div className='pb-2'>
<TreeView id='treeViewMenu'>
{
files[props.name] && Object.keys(files[props.name]).map((key, index) => <FileRender
file={files[props.name][key]}
index={index}
focusContext={state.focusContext}
focusEdit={state.focusEdit}
focusElement={props.focusElement}
ctrlKey={state.ctrlKey}
expandPath={props.expandPath}
editModeOff={editModeOff}
handleClickFile={handleClickFile}
handleClickFolder={handleClickFolder}
handleContextMenu={handleContextMenu}
key={index}
/>)
}
</TreeView>
</div>
</TreeViewItem>
</TreeView>
{ state.showContextMenu &&
<FileExplorerContextMenu
actions={props.focusElement.length > 1 ? state.actions.filter(item => item.multiselect) : state.actions.filter(item => !item.multiselect)}
hideContextMenu={hideContextMenu}
createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput}
deletePath={deletePath}
renamePath={editModeOn}
runScript={runScript}
copy={handleCopyClick}
paste={handlePasteClick}
emit={emitContextMenuEvent}
pageX={state.focusContext.x}
pageY={state.focusContext.y}
path={state.focusContext.element}
type={state.focusContext.type}
focus={props.focusElement}
pushChangesToGist={pushChangesToGist}
publishFolderToGist={publishFolderToGist}
publishFileToGist={publishFileToGist}
/>
}
</div>
)
}
export default FileExplorer

@ -0,0 +1,67 @@
// eslint-disable-next-line no-use-before-define
import React, { useEffect, useRef, useState } from 'react'
import { FileType } from '../types'
export interface FileLabelProps {
file: FileType,
focusEdit: {
element: string
type: string
isNew: boolean
lastEdit: string
}
editModeOff: (content: string) => void
}
export const FileLabel = (props: FileLabelProps) => {
const { file, focusEdit, editModeOff } = props
const [isEditable, setIsEditable] = useState<boolean>(false)
const labelRef = useRef(null)
useEffect(() => {
if (focusEdit.element && file.path) {
setIsEditable(focusEdit.element === file.path)
}
}, [file.path, focusEdit])
useEffect(() => {
if (labelRef.current) {
setTimeout(() => {
labelRef.current.focus()
}, 0)
}
}, [isEditable])
const handleEditInput = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.which === 13) {
event.preventDefault()
editModeOff(labelRef.current.innerText)
labelRef.current.innerText = file.name
}
}
const handleEditBlur = (event: React.SyntheticEvent) => {
event.stopPropagation()
editModeOff(labelRef.current.innerText)
labelRef.current.innerText = file.name
}
return (
<div
className='remixui_items d-inline-block w-100'
ref={isEditable ? labelRef : null}
suppressContentEditableWarning={true}
contentEditable={isEditable}
onKeyDown={handleEditInput}
onBlur={handleEditBlur}
>
<span
title={file.path}
className={'remixui_label ' + (file.isDirectory ? 'folder' : 'remixui_leaf')}
data-path={file.path}
>
{ file.name }
</span>
</div>
)
}

@ -0,0 +1,124 @@
// eslint-disable-next-line no-use-before-define
import React, { SyntheticEvent, useEffect, useState } from 'react'
import { FileType } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { TreeView, TreeViewItem } from '@remix-ui/tree-view'
import { getPathIcon } from '@remix-ui/helper'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileLabel } from './file-label'
export interface RenderFileProps {
file: FileType,
index: number,
focusEdit: { element: string, type: string, isNew: boolean, lastEdit: string },
focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[],
focusContext: { element: string, x: number, y: number, type: string },
ctrlKey: boolean,
expandPath: string[],
editModeOff: (content: string) => void,
handleClickFolder: (path: string, type: string) => void,
handleClickFile: (path: string, type: string) => void,
handleContextMenu: (pageX: number, pageY: number, path: string, content: string, type: string) => void
}
export const FileRender = (props: RenderFileProps) => {
const [file, setFile] = useState<FileType>({} as FileType)
const [hover, setHover] = useState<boolean>(false)
const [icon, setIcon] = useState<string>('')
useEffect(() => {
if (props.file && props.file.path && props.file.type) {
setFile(props.file)
setIcon(getPathIcon(props.file.path))
}
}, [props.file])
const labelClass = props.focusEdit.element === file.path
? 'bg-light' : props.focusElement.findIndex(item => item.key === file.path) !== -1
? 'bg-secondary' : hover
? 'bg-light border' : (props.focusContext.element === file.path) && (props.focusEdit.element !== file.path)
? 'bg-light border' : ''
const spreadProps = {
onClick: (e) => e.stopPropagation()
}
const handleFolderClick = (event: SyntheticEvent) => {
event.stopPropagation()
if (props.focusEdit.element !== file.path) props.handleClickFolder(file.path, file.type)
}
const handleFileClick = (event: SyntheticEvent) => {
event.stopPropagation()
if (props.focusEdit.element !== file.path) props.handleClickFile(file.path, file.type)
}
const handleContextMenu = (event: PointerEvent) => {
event.preventDefault()
event.stopPropagation()
props.handleContextMenu(event.pageX, event.pageY, file.path, (event.target as HTMLElement).textContent, file.type)
}
const handleMouseOut = (event: SyntheticEvent) => {
event.stopPropagation()
setHover(false)
}
const handleMouseOver = (event: SyntheticEvent) => {
event.stopPropagation()
setHover(true)
}
if (file.isDirectory) {
return (
<TreeViewItem
id={`treeViewItem${file.path}`}
iconX='pr-3 fa fa-folder'
iconY='pr-3 fa fa-folder-open'
key={`${file.path + props.index}`}
label={<FileLabel file={file} focusEdit={props.focusEdit} editModeOff={props.editModeOff} />}
onClick={handleFolderClick}
onContextMenu={handleContextMenu}
labelClass={labelClass}
controlBehaviour={ props.ctrlKey }
expand={props.expandPath.includes(file.path)}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
>
{
file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{
Object.keys(file.child).map((key, index) => <FileRender
file={file.child[key]}
index={index}
focusContext={props.focusContext}
focusEdit={props.focusEdit}
focusElement={props.focusElement}
ctrlKey={props.ctrlKey}
editModeOff={props.editModeOff}
handleClickFile={props.handleClickFile}
handleClickFolder={props.handleClickFolder}
handleContextMenu={props.handleContextMenu}
expandPath={props.expandPath}
key={index}
/>)
}
</TreeView> : <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }/>
}
</TreeViewItem>
)
} else {
return (
<TreeViewItem
id={`treeViewItem${file.path}`}
key={`treeView${file.path}`}
label={<FileLabel file={file} focusEdit={props.focusEdit} editModeOff={props.editModeOff} />}
onClick={handleFileClick}
onContextMenu={handleContextMenu}
icon={icon}
labelClass={labelClass}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
/>
)
}
}

@ -0,0 +1,32 @@
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type'
import { createContext, SyntheticEvent } from 'react'
import { BrowserState } from '../reducers/workspace'
export const FileSystemContext = createContext<{
fs: BrowserState,
// eslint-disable-next-line no-undef
modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void,
dispatchInitWorkspace:() => Promise<void>,
dispatchFetchDirectory:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchRemoveInputField:(path: string) => Promise<void>,
dispatchCreateWorkspace: (workspaceName: string) => Promise<void>,
toast: (toasterMsg: string) => void,
dispatchFetchWorkspaceDirectory: (path: string) => Promise<void>,
dispatchSwitchToWorkspace: (name: string) => Promise<void>,
dispatchRenameWorkspace: (oldName: string, workspaceName: string) => Promise<void>,
dispatchDeleteWorkspace: (workspaceName: string) => Promise<void>,
dispatchPublishToGist: (path?: string, type?: string) => Promise<void>,
dispatchUploadFile: (target?: SyntheticEvent, targetFolder?: string) => Promise<void>,
dispatchCreateNewFile: (path: string, rootDir: string) => Promise<void>,
dispatchSetFocusElement: (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => Promise<void>,
dispatchCreateNewFolder: (path: string, rootDir: string) => Promise<void>,
dispatchDeletePath: (path: string[]) => Promise<void>,
dispatchRenamePath: (oldPath: string, newPath: string) => Promise<void>,
dispatchCopyFile: (src: string, dest: string) => Promise<void>,
dispatchCopyFolder: (src: string, dest: string) => Promise<void>,
dispatchRunScript: (path: string) => Promise<void>,
dispatchEmitContextMenuEvent: (cmd: customAction) => Promise<void>,
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>
dispatchHandleExpandPath: (paths: string[]) => Promise<void>
}>(null)

@ -0,0 +1,230 @@
// eslint-disable-next-line no-use-before-define
import React, { useReducer, useState, useEffect, SyntheticEvent } from 'react'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileSystemContext } from '../contexts'
import { browserReducer, browserInitialState } from '../reducers/workspace'
import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from '../actions'
import { Modal, WorkspaceProps } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Workspace } from '../remix-ui-workspace'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type'
export const FileSystemProvider = (props: WorkspaceProps) => {
const { plugin } = props
const [fs, fsDispatch] = useReducer(browserReducer, browserInitialState)
const [focusModal, setFocusModal] = useState<Modal>({
hide: true,
title: '',
message: '',
okLabel: '',
okFn: () => {},
cancelLabel: '',
cancelFn: () => {}
})
const [modals, setModals] = useState<Modal[]>([])
const [focusToaster, setFocusToaster] = useState<string>('')
const [toasters, setToasters] = useState<string[]>([])
const dispatchInitWorkspace = async () => {
await initWorkspace(plugin)(fsDispatch)
}
const dispatchFetchDirectory = async (path: string) => {
await fetchDirectory(path)
}
const dispatchAddInputField = async (path: string, type: 'file' | 'folder') => {
await addInputField(type, path)
}
const dispatchRemoveInputField = async (path: string) => {
await removeInputField(path)
}
const dispatchCreateWorkspace = async (workspaceName: string) => {
await createWorkspace(workspaceName)
}
const dispatchFetchWorkspaceDirectory = async (path: string) => {
await fetchWorkspaceDirectory(path)
}
const dispatchSwitchToWorkspace = async (name: string) => {
await switchToWorkspace(name)
}
const dispatchRenameWorkspace = async (oldName: string, workspaceName: string) => {
await renameWorkspace(oldName, workspaceName)
}
const dispatchDeleteWorkspace = async (workspaceName: string) => {
await deleteWorkspace(workspaceName)
}
const dispatchPublishToGist = async (path?: string, type?: string) => {
await publishToGist(path, type)
}
const dispatchUploadFile = async (target?: SyntheticEvent, targetFolder?: string) => {
await uploadFile(target, targetFolder)
}
const dispatchCreateNewFile = async (path: string, rootDir: string) => {
await createNewFile(path, rootDir)
}
const dispatchSetFocusElement = async (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => {
await setFocusElement(elements)
}
const dispatchCreateNewFolder = async (path: string, rootDir: string) => {
await createNewFolder(path, rootDir)
}
const dispatchDeletePath = async (path: string[]) => {
await deletePath(path)
}
const dispatchRenamePath = async (oldPath: string, newPath: string) => {
await renamePath(oldPath, newPath)
}
const dispatchCopyFile = async (src: string, dest: string) => {
await copyFile(src, dest)
}
const dispatchCopyFolder = async (src: string, dest: string) => {
await copyFolder(src, dest)
}
const dispatchRunScript = async (path: string) => {
await runScript(path)
}
const dispatchEmitContextMenuEvent = async (cmd: customAction) => {
await emitContextMenuEvent(cmd)
}
const dispatchHandleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => {
await handleClickFile(path, type)
}
const dispatchHandleExpandPath = async (paths: string[]) => {
await handleExpandPath(paths)
}
useEffect(() => {
dispatchInitWorkspace()
}, [])
useEffect(() => {
if (modals.length > 0) {
setFocusModal(() => {
const focusModal = {
hide: false,
title: modals[0].title,
message: modals[0].message,
okLabel: modals[0].okLabel,
okFn: modals[0].okFn,
cancelLabel: modals[0].cancelLabel,
cancelFn: modals[0].cancelFn
}
return focusModal
})
const modalList = modals.slice()
modalList.shift()
setModals(modalList)
}
}, [modals])
useEffect(() => {
if (toasters.length > 0) {
setFocusToaster(() => {
return toasters[0]
})
const toasterList = toasters.slice()
toasterList.shift()
setToasters(toasterList)
}
}, [toasters])
useEffect(() => {
if (fs.notification.title) {
modal(fs.notification.title, fs.notification.message, fs.notification.labelOk, fs.notification.actionOk, fs.notification.labelCancel, fs.notification.actionCancel)
}
}, [fs.notification])
useEffect(() => {
if (fs.popup) {
toast(fs.popup)
}
}, [fs.popup])
const handleHideModal = () => {
setFocusModal(modal => {
return { ...modal, hide: true, message: null }
})
}
// eslint-disable-next-line no-undef
const modal = (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => {
setModals(modals => {
modals.push({ message, title, okLabel, okFn, cancelLabel, cancelFn })
return [...modals]
})
}
const handleToaster = () => {
setFocusToaster('')
clearPopUp()
}
const toast = (toasterMsg: string) => {
setToasters(messages => {
messages.push(toasterMsg)
return [...messages]
})
}
const value = {
fs,
modal,
toast,
dispatchInitWorkspace,
dispatchFetchDirectory,
dispatchAddInputField,
dispatchRemoveInputField,
dispatchCreateWorkspace,
dispatchFetchWorkspaceDirectory,
dispatchSwitchToWorkspace,
dispatchRenameWorkspace,
dispatchDeleteWorkspace,
dispatchPublishToGist,
dispatchUploadFile,
dispatchCreateNewFile,
dispatchSetFocusElement,
dispatchCreateNewFolder,
dispatchDeletePath,
dispatchRenamePath,
dispatchCopyFile,
dispatchCopyFolder,
dispatchRunScript,
dispatchEmitContextMenuEvent,
dispatchHandleClickFile,
dispatchHandleExpandPath
}
return (
<FileSystemContext.Provider value={value}>
{ fs.initializingFS && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div> }
{ !fs.initializingFS && <Workspace /> }
<ModalDialog id='fileSystem' { ...focusModal } handleHide={ handleHideModal } />
<Toaster message={focusToaster} handleHide={handleToaster} />
</FileSystemContext.Provider>
)
}
export default FileSystemProvider

@ -0,0 +1,815 @@
import { extractNameFromKey } from '@remix-ui/helper'
import { action, FileType } from '../types'
import * as _ from 'lodash'
interface Action {
type: string
payload: any
}
export interface BrowserState {
browser: {
currentWorkspace: string,
workspaces: string[],
files: { [x: string]: Record<string, FileType> },
expandPath: string[]
isRequestingDirectory: boolean,
isSuccessfulDirectory: boolean,
isRequestingWorkspace: boolean,
isSuccessfulWorkspace: boolean,
error: string,
contextMenu: {
registeredMenuItems: action[],
removedMenuItems: action[],
error: string
}
},
localhost: {
sharedFolder: string,
files: { [x: string]: Record<string, FileType> },
expandPath: string[],
isRequestingDirectory: boolean,
isSuccessfulDirectory: boolean,
isRequestingLocalhost: boolean,
isSuccessfulLocalhost: boolean,
error: string,
contextMenu: {
registeredMenuItems: action[],
removedMenuItems: action[],
error: string
}
},
mode: 'browser' | 'localhost',
notification: {
title: string,
message: string,
actionOk: () => void,
actionCancel: (() => void) | null,
labelOk: string,
labelCancel: string
},
readonly: boolean,
popup: string,
focusEdit: string,
focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[],
initializingFS: boolean
}
export const browserInitialState: BrowserState = {
browser: {
currentWorkspace: '',
workspaces: [],
files: {},
expandPath: [],
isRequestingDirectory: false,
isSuccessfulDirectory: false,
isRequestingWorkspace: false,
isSuccessfulWorkspace: false,
error: null,
contextMenu: {
registeredMenuItems: [],
removedMenuItems: [],
error: null
}
},
localhost: {
sharedFolder: '',
files: {},
expandPath: [],
isRequestingDirectory: false,
isSuccessfulDirectory: false,
isRequestingLocalhost: false,
isSuccessfulLocalhost: false,
error: null,
contextMenu: {
registeredMenuItems: [],
removedMenuItems: [],
error: null
}
},
mode: 'browser',
notification: {
title: '',
message: '',
actionOk: () => {},
actionCancel: () => {},
labelOk: '',
labelCancel: ''
},
readonly: false,
popup: '',
focusEdit: '',
focusElement: [],
initializingFS: true
}
export const browserReducer = (state = browserInitialState, action: Action) => {
switch (action.type) {
case 'SET_CURRENT_WORKSPACE': {
const payload = action.payload as string
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload,
workspaces: workspaces.filter(workspace => workspace)
}
}
}
case 'SET_WORKSPACES': {
const payload = action.payload as string[]
return {
...state,
browser: {
...state.browser,
workspaces: payload.filter(workspace => workspace)
}
}
}
case 'SET_MODE': {
const payload = action.payload as 'browser' | 'localhost'
return {
...state,
mode: payload
}
}
case 'FETCH_DIRECTORY_REQUEST': {
return {
...state,
browser: {
...state.browser,
isRequestingDirectory: state.mode === 'browser',
isSuccessfulDirectory: false,
error: null
},
localhost: {
...state.localhost,
isRequestingDirectory: state.mode === 'localhost',
isSuccessfulDirectory: false,
error: null
}
}
}
case 'FETCH_DIRECTORY_SUCCESS': {
const payload = action.payload as { path: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files,
isRequestingDirectory: false,
isSuccessfulDirectory: true,
error: null
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload) : state.localhost.files,
isRequestingDirectory: false,
isSuccessfulDirectory: true,
error: null
}
}
}
case 'FETCH_DIRECTORY_ERROR': {
return {
...state,
browser: {
...state.browser,
isRequestingDirectory: false,
isSuccessfulDirectory: false,
error: state.mode === 'browser' ? action.payload : null
},
localhost: {
...state.localhost,
isRequestingDirectory: false,
isSuccessfulDirectory: false,
error: state.mode === 'localhost' ? action.payload : null
}
}
}
case 'FETCH_WORKSPACE_DIRECTORY_REQUEST': {
return {
...state,
browser: {
...state.browser,
isRequestingWorkspace: state.mode === 'browser',
isSuccessfulWorkspace: false,
error: null
},
localhost: {
...state.localhost,
isRequestingWorkspace: state.mode === 'localhost',
isSuccessfulWorkspace: false,
error: null
}
}
}
case 'FETCH_WORKSPACE_DIRECTORY_SUCCESS': {
const payload = action.payload as { path: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchWorkspaceDirectoryContent(state, payload) : state.browser.files,
isRequestingWorkspace: false,
isSuccessfulWorkspace: true,
error: null
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchWorkspaceDirectoryContent(state, payload) : state.localhost.files,
isRequestingWorkspace: false,
isSuccessfulWorkspace: true,
error: null
}
}
}
case 'FETCH_WORKSPACE_DIRECTORY_ERROR': {
return {
...state,
browser: {
...state.browser,
isRequestingWorkspace: false,
isSuccessfulWorkspace: false,
error: state.mode === 'browser' ? action.payload : null
},
localhost: {
...state.localhost,
isRequestingWorkspace: false,
isSuccessfulWorkspace: false,
error: state.mode === 'localhost' ? action.payload : null
}
}
}
case 'DISPLAY_NOTIFICATION': {
const payload = action.payload as { title: string, message: string, actionOk: () => void, actionCancel: () => void, labelOk: string, labelCancel: string }
return {
...state,
notification: {
title: payload.title,
message: payload.message,
actionOk: payload.actionOk || browserInitialState.notification.actionOk,
actionCancel: payload.actionCancel || browserInitialState.notification.actionCancel,
labelOk: payload.labelOk,
labelCancel: payload.labelCancel
}
}
}
case 'HIDE_NOTIFICATION': {
return {
...state,
notification: browserInitialState.notification
}
}
case 'FILE_ADDED_SUCCESS': {
const payload = action.payload as string
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fileAdded(state, payload) : state.browser.files,
expandPath: state.mode === 'browser' ? [...new Set([...state.browser.expandPath, payload])] : state.browser.expandPath
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fileAdded(state, payload) : state.localhost.files,
expandPath: state.mode === 'localhost' ? [...new Set([...state.localhost.expandPath, payload])] : state.localhost.expandPath
}
}
}
case 'FOLDER_ADDED_SUCCESS': {
const payload = action.payload as { path: string, folderPath: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files,
expandPath: state.mode === 'browser' ? [...new Set([...state.browser.expandPath, payload.folderPath])] : state.browser.expandPath
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload) : state.localhost.files,
expandPath: state.mode === 'localhost' ? [...new Set([...state.localhost.expandPath, payload.folderPath])] : state.localhost.expandPath
}
}
}
case 'FILE_REMOVED_SUCCESS': {
const payload = action.payload as string
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fileRemoved(state, payload) : state.browser.files,
expandPath: state.mode === 'browser' ? [...(state.browser.expandPath.filter(path => path !== payload))] : state.browser.expandPath
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fileRemoved(state, payload) : state.localhost.files,
expandPath: state.mode === 'localhost' ? [...(state.browser.expandPath.filter(path => path !== payload))] : state.localhost.expandPath
}
}
}
case 'ROOT_FOLDER_CHANGED': {
const payload = action.payload as string
return {
...state,
localhost: {
...state.localhost,
sharedFolder: payload
}
}
}
case 'ADD_INPUT_FIELD': {
const payload = action.payload as { path: string, fileTree, type: 'file' | 'folder' }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload) : state.localhost.files
},
focusEdit: payload.path + '/' + 'blank'
}
}
case 'REMOVE_INPUT_FIELD': {
const payload = action.payload as { path: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? removeInputField(state, payload.path) : state.browser.files
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? removeInputField(state, payload.path) : state.localhost.files
},
focusEdit: null
}
}
case 'SET_READ_ONLY_MODE': {
const payload = action.payload as boolean
return {
...state,
readonly: payload
}
}
case 'FILE_RENAMED_SUCCESS': {
const payload = action.payload as { path: string, oldPath: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload, payload.oldPath) : state.browser.files
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload, payload.oldPath) : state.localhost.files
}
}
}
case 'CREATE_WORKSPACE_REQUEST': {
return {
...state,
browser: {
...state.browser,
isRequestingWorkspace: true,
isSuccessfulWorkspace: false,
error: null
}
}
}
case 'CREATE_WORKSPACE_SUCCESS': {
const payload = action.payload as string
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload]
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload,
workspaces: workspaces.filter(workspace => workspace),
isRequestingWorkspace: false,
isSuccessfulWorkspace: true,
error: null
}
}
}
case 'CREATE_WORKSPACE_ERROR': {
return {
...state,
browser: {
...state.browser,
isRequestingWorkspace: false,
isSuccessfulWorkspace: false,
error: action.payload
}
}
}
case 'RENAME_WORKSPACE': {
const payload = action.payload as { oldName: string, workspaceName: string }
const workspaces = state.browser.workspaces.filter(name => name && (name !== payload.oldName))
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload.workspaceName,
workspaces: [...workspaces, payload.workspaceName],
expandPath: []
}
}
}
case 'DELETE_WORKSPACE': {
const payload = action.payload as string
const workspaces = state.browser.workspaces.filter(name => name && (name !== payload))
return {
...state,
browser: {
...state.browser,
workspaces: workspaces
}
}
}
case 'DISPLAY_POPUP_MESSAGE': {
const payload = action.payload as string
return {
...state,
popup: payload
}
}
case 'HIDE_POPUP_MESSAGE': {
return {
...state,
popup: ''
}
}
case 'SET_FOCUS_ELEMENT': {
const payload = action.payload as { key: string, type: 'file' | 'folder' | 'gist' }[]
return {
...state,
focusElement: payload
}
}
case 'SET_CONTEXT_MENU_ITEM': {
const payload = action.payload as action
return {
...state,
browser: {
...state.browser,
contextMenu: state.mode === 'browser' ? addContextMenuItem(state, payload) : state.browser.contextMenu
},
localhost: {
...state.localhost,
contextMenu: state.mode === 'localhost' ? addContextMenuItem(state, payload) : state.localhost.contextMenu
}
}
}
case 'REMOVE_CONTEXT_MENU_ITEM': {
const payload = action.payload
return {
...state,
browser: {
...state.browser,
contextMenu: state.mode === 'browser' ? removeContextMenuItem(state, payload) : state.browser.contextMenu
},
localhost: {
...state.localhost,
contextMenu: state.mode === 'localhost' ? removeContextMenuItem(state, payload) : state.localhost.contextMenu
}
}
}
case 'SET_EXPAND_PATH': {
const payload = action.payload as string[]
return {
...state,
browser: {
...state.browser,
expandPath: state.mode === 'browser' ? payload : state.browser.expandPath
},
localhost: {
...state.localhost,
expandPath: state.mode === 'localhost' ? payload : state.localhost.expandPath
}
}
}
case 'LOAD_LOCALHOST_REQUEST': {
return {
...state,
localhost: {
...state.localhost,
isRequestingLocalhost: true,
isSuccessfulLocalhost: false,
error: null
}
}
}
case 'LOAD_LOCALHOST_SUCCESS': {
return {
...state,
localhost: {
...state.localhost,
isRequestingLocalhost: false,
isSuccessfulLocalhost: true,
error: null
}
}
}
case 'LOAD_LOCALHOST_ERROR': {
const payload = action.payload as string
return {
...state,
localhost: {
...state.localhost,
isRequestingLocalhost: false,
isSuccessfulLocalhost: false,
error: payload
}
}
}
case 'FS_INITIALIZATION_COMPLETED': {
return {
...state,
initializingFS: false
}
}
default:
throw new Error()
}
}
const fileAdded = (state: BrowserState, path: string): { [x: string]: Record<string, FileType> } => {
let files = state.mode === 'browser' ? state.browser.files : state.localhost.files
const _path = splitPath(state, path)
files = _.set(files, _path, {
path: path,
name: extractNameFromKey(path),
isDirectory: false,
type: 'file'
})
return files
}
const fileRemoved = (state: BrowserState, path: string): { [x: string]: Record<string, FileType> } => {
const files = state.mode === 'browser' ? state.browser.files : state.localhost.files
const _path = splitPath(state, path)
_.unset(files, _path)
return files
}
const removeInputField = (state: BrowserState, path: string): { [x: string]: Record<string, FileType> } => {
let files = state.mode === 'browser' ? state.browser.files : state.localhost.files
const root = state.mode === 'browser' ? state.browser.currentWorkspace : state.mode
if (path === root) {
delete files[root][path + '/' + 'blank']
return files
}
const _path = splitPath(state, path)
const prevFiles = _.get(files, _path)
if (prevFiles) {
prevFiles.child && prevFiles.child[path + '/' + 'blank'] && delete prevFiles.child[path + '/' + 'blank']
files = _.set(files, _path, {
isDirectory: true,
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder',
child: prevFiles ? prevFiles.child : {}
})
}
return files
}
// IDEA: Modify function to remove blank input field without fetching content
const fetchDirectoryContent = (state: BrowserState, payload: { fileTree, path: string, type?: 'file' | 'folder' }, deletePath?: string): { [x: string]: Record<string, FileType> } => {
if (!payload.fileTree) return state.mode === 'browser' ? state.browser.files : state[state.mode].files
if (state.mode === 'browser') {
if (payload.path === state.browser.currentWorkspace) {
let files = normalize(payload.fileTree, payload.path, payload.type)
files = _.merge(files, state.browser.files[state.browser.currentWorkspace])
if (deletePath) delete files[deletePath]
return { [state.browser.currentWorkspace]: files }
} else {
let files = state.browser.files
const _path = splitPath(state, payload.path)
const prevFiles = _.get(files, _path)
if (prevFiles) {
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child)
if (deletePath) {
if (deletePath.endsWith('/blank')) delete prevFiles.child[deletePath]
else {
deletePath = extractNameFromKey(deletePath)
delete prevFiles.child[deletePath]
}
}
files = _.set(files, _path, prevFiles)
} else if (payload.fileTree && payload.path) {
files = { [payload.path]: normalize(payload.fileTree, payload.path, payload.type) }
}
return files
}
} else {
if (payload.path === state.mode || payload.path === '/') {
let files = normalize(payload.fileTree, payload.path, payload.type)
files = _.merge(files, state[state.mode].files[state.mode])
if (deletePath) delete files[deletePath]
return { [state.mode]: files }
} else {
let files = state.localhost.files
const _path = splitPath(state, payload.path)
const prevFiles = _.get(files, _path)
if (prevFiles) {
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child)
if (deletePath) {
if (deletePath.endsWith('/blank')) delete prevFiles.child[deletePath]
else {
deletePath = extractNameFromKey(deletePath)
delete prevFiles.child[deletePath]
}
}
files = _.set(files, _path, prevFiles)
} else {
files = { [payload.path]: normalize(payload.fileTree, payload.path, payload.type) }
}
return files
}
}
}
const fetchWorkspaceDirectoryContent = (state: BrowserState, payload: { fileTree, path: string }): { [x: string]: Record<string, FileType> } => {
if (state.mode === 'browser') {
const files = normalize(payload.fileTree, payload.path)
return { [payload.path]: files }
} else {
return fetchDirectoryContent(state, payload)
}
}
const normalize = (filesList, directory?: string, newInputType?: 'folder' | 'file'): Record<string, FileType> => {
const folders = {}
const files = {}
Object.keys(filesList || {}).forEach(key => {
key = key.replace(/^\/|\/$/g, '') // remove first and last slash
let path = key
path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (filesList[key].isDirectory) {
folders[extractNameFromKey(key)] = {
path,
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path),
isDirectory: filesList[key].isDirectory,
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder'
}
} else {
files[extractNameFromKey(key)] = {
path,
name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory,
type: 'file'
}
}
})
if (newInputType === 'folder') {
const path = directory + '/blank'
folders[path] = {
path: path,
name: '',
isDirectory: true,
type: 'folder'
}
} else if (newInputType === 'file') {
const path = directory + '/blank'
files[path] = {
path: path,
name: '',
isDirectory: false,
type: 'file'
}
}
return Object.assign({}, folders, files)
}
const splitPath = (state: BrowserState, path: string): string[] | string => {
const root = state.mode === 'browser' ? state.browser.currentWorkspace : 'localhost'
const pathArr: string[] = (path || '').split('/').filter(value => value)
if (pathArr[0] !== root) pathArr.unshift(root)
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => {
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]
}, [])
return _path
}
const addContextMenuItem = (state: BrowserState, item: action): { registeredMenuItems: action[], removedMenuItems: action[], error: string } => {
let registeredItems = state[state.mode].contextMenu.registeredMenuItems
let removedItems = state[state.mode].contextMenu.removedMenuItems
let error = null
if (registeredItems.filter((o) => {
return o.id === item.id && o.name === item.name
}).length) {
error = `Action ${item.name} already exists on ${item.id}`
return {
registeredMenuItems: registeredItems,
removedMenuItems: removedItems,
error
}
}
registeredItems = [...registeredItems, item]
removedItems = removedItems.filter(menuItem => item.id !== menuItem.id)
return {
registeredMenuItems: registeredItems,
removedMenuItems: removedItems,
error
}
}
const removeContextMenuItem = (state: BrowserState, plugin): { registeredMenuItems: action[], removedMenuItems: action[], error: string } => {
let registeredItems = state[state.mode].contextMenu.registeredMenuItems
const removedItems = state[state.mode].contextMenu.removedMenuItems
const error = null
registeredItems = registeredItems.filter((item) => {
if (item.id !== plugin.name || item.sticky === true) return true
else {
removedItems.push(item)
return false
}
})
return {
registeredMenuItems: registeredItems,
removedMenuItems: removedItems,
error
}
}

@ -1,212 +1,64 @@
import React, { useState, useEffect, useRef } from 'react' // eslint-disable-line import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line
import { FileExplorer } from '@remix-ui/file-explorer' // eslint-disable-line import { FileExplorer } from './components/file-explorer' // eslint-disable-line
import './remix-ui-workspace.css' import './css/remix-ui-workspace.css'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line import { FileSystemContext } from './contexts'
import { Toaster } from '@remix-ui/toaster'// eslint-disable-line
import { MenuItems } from 'libs/remix-ui/file-explorer/src/lib/types'
/* eslint-disable-next-line */ const canUpload = window.File || window.FileReader || window.FileList || window.Blob
export interface WorkspaceProps {
setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void,
createWorkspace: (name: string) => void,
renameWorkspace: (oldName: string, newName: string) => void
workspaceRenamed: ({ name: string }) => void,
workspaceCreated: ({ name: string }) => void,
workspaceDeleted: ({ name: string }) => void,
workspace: any // workspace provider,
browser: any // browser provider
localhost: any // localhost provider
fileManager : any
registry: any // registry
plugin: any // plugin call and resetFocus
request: any // api request,
workspaces: any,
registeredMenuItems: MenuItems // menu items
removedMenuItems: MenuItems
initialWorkspace: string
}
var canUpload = window.File || window.FileReader || window.FileList || window.Blob export function Workspace () {
export const Workspace = (props: WorkspaceProps) => {
const LOCALHOST = ' - connect to localhost - ' const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - ' const NO_WORKSPACE = ' - none - '
const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE)
/* extends the parent 'plugin' with some function needed by the file explorer */ const global = useContext(FileSystemContext)
props.plugin.resetFocus = (reset) => { const workspaceRenameInput = useRef()
setState(prevState => { const workspaceCreateInput = useRef()
return { ...prevState, reset }
})
}
props.plugin.resetNewFile = () => {
setState(prevState => {
return { ...prevState, displayNewFile: !state.displayNewFile }
})
}
props.plugin.resetUploadFile = () => {}
/* implement an external API, consumed by the parent */
props.request.createWorkspace = () => {
return createWorkspace()
}
props.request.setWorkspace = (workspaceName) => {
return setWorkspace(workspaceName)
}
props.request.createNewFile = async () => {
if (!state.workspaces.length) await createNewWorkspace('default_workspace')
props.plugin.resetNewFile()
}
props.request.uploadFile = async (target) => {
if (!state.workspaces.length) await createNewWorkspace('default_workspace')
setState(prevState => {
return { ...prevState, uploadFileEvent: target }
})
}
props.request.getCurrentWorkspace = () => {
return { name: state.currentWorkspace, isLocalhost: state.currentWorkspace === LOCALHOST, absolutePath: `${props.workspace.workspacesPath}/${state.currentWorkspace}` }
}
useEffect(() => { useEffect(() => {
let getWorkspaces = async () => { resetFocus()
if (props.workspaces && Array.isArray(props.workspaces)) { }, [])
if (props.workspaces.length > 0 && state.currentWorkspace === NO_WORKSPACE) {
const currentWorkspace = props.workspace.getWorkspace() ? props.workspace.getWorkspace() : props.workspaces[0]
await props.workspace.setWorkspace(currentWorkspace)
setState(prevState => {
return { ...prevState, workspaces: props.workspaces, currentWorkspace }
})
} else {
setState(prevState => {
return { ...prevState, workspaces: props.workspaces }
})
}
}
}
getWorkspaces()
return () => {
getWorkspaces = async () => {}
}
}, [props.workspaces])
const localhostDisconnect = () => {
if (state.currentWorkspace === LOCALHOST) setWorkspace(props.workspaces.length > 0 ? props.workspaces[0] : NO_WORKSPACE)
// This should be removed some time after refactoring: https://github.com/ethereum/remix-project/issues/1197
else {
setWorkspace(state.currentWorkspace) // Useful to switch to last selcted workspace when remixd is disconnected
props.fileManager.setMode('browser')
}
}
useEffect(() => { useEffect(() => {
props.localhost.event.off('disconnected', localhostDisconnect) if (global.fs.mode === 'browser') {
props.localhost.event.on('disconnected', localhostDisconnect) if (global.fs.browser.currentWorkspace) setCurrentWorkspace(global.fs.browser.currentWorkspace)
props.localhost.event.on('connected', () => { else setCurrentWorkspace(NO_WORKSPACE)
remixdExplorer.show() global.dispatchFetchWorkspaceDirectory(global.fs.browser.currentWorkspace)
setWorkspace(LOCALHOST) } else if (global.fs.mode === 'localhost') {
}) // global.dispatchFetchWorkspaceDirectory('/')
setCurrentWorkspace(LOCALHOST)
props.localhost.event.on('disconnected', () => {
remixdExplorer.hide()
})
props.localhost.event.on('loading', () => {
remixdExplorer.loading()
})
props.workspace.event.on('createWorkspace', (name) => {
createNewWorkspace(name)
})
if (props.initialWorkspace) {
props.workspace.setWorkspace(props.initialWorkspace)
setState(prevState => {
return { ...prevState, currentWorkspace: props.initialWorkspace }
})
} }
}, []) }, [global.fs.browser.currentWorkspace, global.fs.localhost.sharedFolder, global.fs.mode])
const createNewWorkspace = async (workspaceName) => { useEffect(() => {
try { if (global.fs.browser.currentWorkspace && !global.fs.browser.workspaces.includes(global.fs.browser.currentWorkspace)) {
await props.fileManager.closeAllFiles() if (global.fs.browser.workspaces.length > 0) {
await props.createWorkspace(workspaceName) switchWorkspace(global.fs.browser.workspaces[global.fs.browser.workspaces.length - 1])
await setWorkspace(workspaceName) } else {
toast('New default workspace has been created.') switchWorkspace(NO_WORKSPACE)
} catch (e) { }
modalMessage('Create Default Workspace', e.message)
console.error(e)
} }
} }, [global.fs.browser.workspaces])
const [state, setState] = useState({
workspaces: [],
reset: false,
currentWorkspace: NO_WORKSPACE,
hideRemixdExplorer: true,
displayNewFile: false,
externalUploads: null,
uploadFileEvent: null,
modal: {
hide: true,
title: '',
message: null,
okLabel: '',
okFn: () => {},
cancelLabel: '',
cancelFn: () => {},
handleHide: null
},
loadingLocalhost: false,
toasterMsg: ''
})
const toast = (message: string) => {
setState(prevState => {
return { ...prevState, toasterMsg: message }
})
}
/* workspace creation, renaming and deletion */
const renameCurrentWorkspace = () => { const renameCurrentWorkspace = () => {
modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '', () => {}) global.modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '')
} }
const createWorkspace = () => { const createWorkspace = () => {
modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '', () => {}) global.modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '')
} }
const deleteCurrentWorkspace = () => { const deleteCurrentWorkspace = () => {
modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '', () => {}) global.modal('Delete Current Workspace', 'Are you sure to delete the current workspace?', 'OK', onFinishDeleteWorkspace, '')
}
const modalMessage = (title: string, body: string) => {
setTimeout(() => { // wait for any previous modal a chance to close
modal(title, body, 'OK', () => {}, '', null)
}, 200)
} }
const workspaceRenameInput = useRef()
const workspaceCreateInput = useRef()
const onFinishRenameWorkspace = async () => { const onFinishRenameWorkspace = async () => {
if (workspaceRenameInput.current === undefined) return if (workspaceRenameInput.current === undefined) return
// @ts-ignore: Object is possibly 'null'. // @ts-ignore: Object is possibly 'null'.
const workspaceName = workspaceRenameInput.current.value const workspaceName = workspaceRenameInput.current.value
try { try {
await props.renameWorkspace(state.currentWorkspace, workspaceName) await global.dispatchRenameWorkspace(currentWorkspace, workspaceName)
setWorkspace(workspaceName)
props.workspaceRenamed({ name: workspaceName })
} catch (e) { } catch (e) {
modalMessage('Rename Workspace', e.message) global.modal('Rename Workspace', e.message, 'OK', () => {}, '')
console.error(e) console.error(e)
} }
} }
@ -217,105 +69,40 @@ export const Workspace = (props: WorkspaceProps) => {
const workspaceName = workspaceCreateInput.current.value const workspaceName = workspaceCreateInput.current.value
try { try {
await props.fileManager.closeAllFiles() await global.dispatchCreateWorkspace(workspaceName)
await props.createWorkspace(workspaceName)
await setWorkspace(workspaceName)
} catch (e) { } catch (e) {
modalMessage('Create Workspace', e.message) global.modal('Create Workspace', e.message, 'OK', () => {}, '')
console.error(e) console.error(e)
} }
} }
const onFinishDeleteWorkspace = async () => { const onFinishDeleteWorkspace = async () => {
await props.fileManager.closeAllFiles() try {
const workspacesPath = props.workspace.workspacesPath await global.dispatchDeleteWorkspace(global.fs.browser.currentWorkspace)
props.browser.remove(workspacesPath + '/' + state.currentWorkspace) } catch (e) {
const name = state.currentWorkspace global.modal('Delete Workspace', e.message, 'OK', () => {}, '')
setWorkspace(NO_WORKSPACE) console.error(e)
props.workspaceDeleted({ name }) }
} }
/** ** ****/ /** ** ****/
const resetFocus = (reset) => { const resetFocus = () => {
setState(prevState => { global.dispatchSetFocusElement([{ key: '', type: 'folder' }])
return { ...prevState, reset }
})
} }
const setWorkspace = async (name) => { const switchWorkspace = async (name: string) => {
await props.fileManager.closeAllFiles() try {
if (name === LOCALHOST) { await global.dispatchSwitchToWorkspace(name)
props.workspace.clearWorkspace() global.dispatchHandleExpandPath([])
} else if (name === NO_WORKSPACE) { } catch (e) {
props.workspace.clearWorkspace() global.modal('Switch To Workspace', e.message, 'OK', () => {}, '')
} else { console.error(e)
await props.workspace.setWorkspace(name)
}
await props.setWorkspace({ name, isLocalhost: name === LOCALHOST }, !(name === LOCALHOST || name === NO_WORKSPACE))
props.plugin.getWorkspaces()
setState(prevState => {
return { ...prevState, currentWorkspace: name }
})
}
const remixdExplorer = {
hide: async () => {
// If 'connect to localhost' is clicked from home tab, mode is not 'localhost'
// if (props.fileManager.mode === 'localhost') {
await setWorkspace(NO_WORKSPACE)
props.fileManager.setMode('browser')
setState(prevState => {
return { ...prevState, hideRemixdExplorer: true, loadingLocalhost: false }
})
// } else {
// // Hide spinner in file explorer
// setState(prevState => {
// return { ...prevState, loadingLocalhost: false }
// })
// }
},
show: () => {
props.fileManager.setMode('localhost')
setState(prevState => {
return { ...prevState, hideRemixdExplorer: false, loadingLocalhost: false }
})
},
loading: () => {
setState(prevState => {
return { ...prevState, loadingLocalhost: true }
})
} }
} }
const handleHideModal = () => {
setState(prevState => {
return { ...prevState, modal: { ...state.modal, hide: true, message: null } }
})
}
// eslint-disable-next-line no-undef
const modal = async (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel: string, cancelFn: () => void) => {
await setState(prevState => {
return {
...prevState,
modal: {
...prevState.modal,
hide: false,
message,
title,
okLabel,
okFn,
cancelLabel,
cancelFn,
handleHide: handleHideModal
}
}
})
}
const createModalMessage = () => { const createModalMessage = () => {
return ( return (
<> <>
<span>{ state.modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" /> <input type="text" data-id="modalDialogCustomPromptTextCreate" defaultValue={`workspace_${Date.now()}`} ref={workspaceCreateInput} className="form-control" />
</> </>
) )
@ -324,29 +111,14 @@ export const Workspace = (props: WorkspaceProps) => {
const renameModalMessage = () => { const renameModalMessage = () => {
return ( return (
<> <>
<span>{ state.modal.message }</span> <input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ currentWorkspace } ref={workspaceRenameInput} className="form-control" />
<input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ state.currentWorkspace } ref={workspaceRenameInput} className="form-control" />
</> </>
) )
} }
return ( return (
<div className='remixui_container'> <div className='remixui_container'>
{ state.modal.message && <ModalDialog <div className='remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
id='workspacesModalDialog'
title={ state.modal.title }
message={ state.modal.message }
hide={ state.modal.hide }
okLabel={ state.modal.okLabel }
okFn={ state.modal.okFn }
cancelLabel={ state.modal.cancelLabel }
cancelFn={ state.modal.cancelFn }
handleHide={ handleHideModal }>
{ (typeof state.modal.message !== 'string') && state.modal.message }
</ModalDialog>
}
<Toaster message={state.toasterMsg} />
<div className='remixui_fileexplorer' onClick={() => resetFocus(true)}>
<div> <div>
<header> <header>
<div className="mb-2"> <div className="mb-2">
@ -365,7 +137,7 @@ export const Workspace = (props: WorkspaceProps) => {
title='Create'> title='Create'>
</span> </span>
<span <span
hidden={state.currentWorkspace === LOCALHOST || state.currentWorkspace === NO_WORKSPACE} hidden={currentWorkspace === LOCALHOST || currentWorkspace === NO_WORKSPACE}
id='workspaceRename' id='workspaceRename'
data-id='workspaceRename' data-id='workspaceRename'
onClick={(e) => { onClick={(e) => {
@ -376,7 +148,7 @@ export const Workspace = (props: WorkspaceProps) => {
title='Rename'> title='Rename'>
</span> </span>
<span <span
hidden={state.currentWorkspace === LOCALHOST || state.currentWorkspace === NO_WORKSPACE} hidden={currentWorkspace === LOCALHOST || currentWorkspace === NO_WORKSPACE}
id='workspaceDelete' id='workspaceDelete'
data-id='workspaceDelete' data-id='workspaceDelete'
onClick={(e) => { onClick={(e) => {
@ -387,15 +159,15 @@ export const Workspace = (props: WorkspaceProps) => {
title='Delete'> title='Delete'>
</span> </span>
</span> </span>
<select id="workspacesSelect" value={state.currentWorkspace} data-id="workspacesSelect" onChange={(e) => setWorkspace(e.target.value)} className="form-control custom-select"> <select id="workspacesSelect" value={currentWorkspace} data-id="workspacesSelect" onChange={(e) => switchWorkspace(e.target.value)} className="form-control custom-select">
{ {
state.workspaces global.fs.browser.workspaces
.map((folder, index) => { .map((folder, index) => {
return <option key={index} value={folder}>{folder}</option> return <option key={index} value={folder}>{folder}</option>
}) })
} }
<option value={LOCALHOST}>{state.currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option> <option value={LOCALHOST}>{currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option>
{ state.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> } { global.fs.browser.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> }
</select> </select>
</div> </div>
</header> </header>
@ -403,34 +175,70 @@ export const Workspace = (props: WorkspaceProps) => {
<div className='remixui_fileExplorerTree'> <div className='remixui_fileExplorerTree'>
<div> <div>
<div className='pl-2 remixui_treeview' data-id='filePanelFileExplorerTree'> <div className='pl-2 remixui_treeview' data-id='filePanelFileExplorerTree'>
{ state.hideRemixdExplorer && state.currentWorkspace && state.currentWorkspace !== NO_WORKSPACE && state.currentWorkspace !== LOCALHOST && { (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<FileExplorer <FileExplorer
name={state.currentWorkspace} name={currentWorkspace}
registry={props.registry}
filesProvider={props.workspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']} menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
plugin={props.plugin} contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
focusRoot={state.reset} removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
contextMenuItems={props.registeredMenuItems} files={global.fs.browser.files}
removedContextMenuItems={props.removedMenuItems} expandPath={global.fs.browser.expandPath}
displayInput={state.displayNewFile} focusEdit={global.fs.focusEdit}
externalUploads={state.uploadFileEvent} focusElement={global.fs.focusElement}
dispatchCreateNewFile={global.dispatchCreateNewFile}
modal={global.modal}
dispatchCreateNewFolder={global.dispatchCreateNewFolder}
readonly={global.fs.readonly}
toast={global.toast}
dispatchDeletePath={global.dispatchDeletePath}
dispatchRenamePath={global.dispatchRenamePath}
dispatchUploadFile={global.dispatchUploadFile}
dispatchCopyFile={global.dispatchCopyFile}
dispatchCopyFolder={global.dispatchCopyFolder}
dispatchPublishToGist={global.dispatchPublishToGist}
dispatchRunScript={global.dispatchRunScript}
dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent}
dispatchHandleClickFile={global.dispatchHandleClickFile}
dispatchSetFocusElement={global.dispatchSetFocusElement}
dispatchFetchDirectory={global.dispatchFetchDirectory}
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
/> />
} }
</div> </div>
{ {
state.loadingLocalhost ? <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div> global.fs.localhost.isRequestingLocalhost ? <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>
: <div className='pl-2 filesystemexplorer remixui_treeview'> : <div className='pl-2 filesystemexplorer remixui_treeview'>
{ !state.hideRemixdExplorer && { global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost &&
<FileExplorer <FileExplorer
name='localhost' name='localhost'
registry={props.registry}
filesProvider={props.localhost}
menuItems={['createNewFile', 'createNewFolder']} menuItems={['createNewFile', 'createNewFolder']}
plugin={props.plugin} contextMenuItems={global.fs.localhost.contextMenu.registeredMenuItems}
focusRoot={state.reset} removedContextMenuItems={global.fs.localhost.contextMenu.removedMenuItems}
contextMenuItems={props.registeredMenuItems} files={global.fs.localhost.files}
removedContextMenuItems={props.removedMenuItems} expandPath={global.fs.localhost.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
dispatchCreateNewFile={global.dispatchCreateNewFile}
modal={global.modal}
dispatchCreateNewFolder={global.dispatchCreateNewFolder}
readonly={global.fs.readonly}
toast={global.toast}
dispatchDeletePath={global.dispatchDeletePath}
dispatchRenamePath={global.dispatchRenamePath}
dispatchUploadFile={global.dispatchUploadFile}
dispatchCopyFile={global.dispatchCopyFile}
dispatchCopyFolder={global.dispatchCopyFolder}
dispatchPublishToGist={global.dispatchPublishToGist}
dispatchRunScript={global.dispatchRunScript}
dispatchEmitContextMenuEvent={global.dispatchEmitContextMenuEvent}
dispatchHandleClickFile={global.dispatchHandleClickFile}
dispatchSetFocusElement={global.dispatchSetFocusElement}
dispatchFetchDirectory={global.dispatchFetchDirectory}
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
/> />
} }
</div> </div>

@ -0,0 +1,155 @@
import React from 'react'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
export type action = { name: string, type?: Array<'folder' | 'gist' | 'file'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean }
export type MenuItems = action[]
export interface WorkspaceProps {
plugin: {
setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void,
createWorkspace: (name: string) => void,
renameWorkspace: (oldName: string, newName: string) => void
workspaceRenamed: ({ name: string }) => void,
workspaceCreated: ({ name: string }) => void,
workspaceDeleted: ({ name: string }) => void,
workspace: any // workspace provider,
browser: any // browser provider
localhost: any // localhost provider
fileManager : any
registry: any // registry
request: {
createWorkspace: () => void,
setWorkspace: (workspaceName: string) => void,
createNewFile: () => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void,
getCurrentWorkspace: () => void
} // api request,
workspaces: any,
registeredMenuItems: MenuItems // menu items
removedMenuItems: MenuItems
initialWorkspace: string,
resetNewFile: () => void,
getWorkspaces: () => string[]
}
}
export interface WorkspaceState {
hideRemixdExplorer: boolean
displayNewFile: boolean
loadingLocalhost: boolean
}
export interface Modal {
hide?: boolean
title: string
// eslint-disable-next-line no-undef
message: string | JSX.Element
okLabel: string
okFn: () => void
cancelLabel: string
cancelFn: () => void
}
export interface FileType {
path: string,
name: string,
isDirectory: boolean,
type: 'folder' | 'file' | 'gist',
child?: File[]
}
/* eslint-disable-next-line */
export interface FileExplorerProps {
name: string,
menuItems?: string[],
contextMenuItems: MenuItems,
removedContextMenuItems: MenuItems,
files: { [x: string]: Record<string, FileType> },
expandPath: string[],
focusEdit: string,
focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[],
dispatchCreateNewFile: (path: string, rootDir: string) => Promise<void>,
// eslint-disable-next-line no-undef
modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void,
dispatchCreateNewFolder: (path: string, rootDir: string) => Promise<void>,
readonly: boolean,
toast: (toasterMsg: string) => void,
dispatchDeletePath: (path: string[]) => Promise<void>,
dispatchRenamePath: (oldPath: string, newPath: string) => Promise<void>,
dispatchUploadFile: (target?: React.SyntheticEvent, targetFolder?: string) => Promise<void>,
dispatchCopyFile: (src: string, dest: string) => Promise<void>,
dispatchCopyFolder: (src: string, dest: string) => Promise<void>,
dispatchRunScript: (path: string) => Promise<void>,
dispatchPublishToGist: (path?: string, type?: string) => Promise<void>,
dispatchEmitContextMenuEvent: (cmd: customAction) => Promise<void>,
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>,
dispatchSetFocusElement: (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => Promise<void>,
dispatchFetchDirectory:(path: string) => Promise<void>,
dispatchRemoveInputField:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchHandleExpandPath: (paths: string[]) => Promise<void>
}
export interface FileExplorerMenuProps {
title: string,
menuItems: string[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
publishToGist: (path?: string) => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void
}
export interface FileExplorerContextMenuProps {
actions: action[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
deletePath: (path: string | string[]) => void,
renamePath: (path: string, type: string) => void,
hideContextMenu: () => void,
publishToGist?: (path?: string, type?: string) => void,
pushChangesToGist?: (path?: string, type?: string) => void,
publishFolderToGist?: (path?: string, type?: string) => void,
publishFileToGist?: (path?: string, type?: string) => void,
runScript?: (path: string) => void,
emit?: (cmd: customAction) => void,
pageX: number,
pageY: number,
path: string,
type: string,
focus: {key:string, type:string}[],
onMouseOver?: (...args) => void,
copy?: (path: string, type: string) => void,
paste?: (destination: string, type: string) => void
}
export interface FileExplorerState {
ctrlKey: boolean
newFileName: string
actions: {
id: string
name: string
type?: Array<'folder' | 'gist' | 'file'>
path?: string[]
extension?: string[]
pattern?: string[]
multiselect: boolean
label: string
}[]
focusContext: {
element: string
x: number
y: number
type: string
}
focusEdit: {
element: string
type: string
isNew: boolean
lastEdit: string
}
mouseOverElement: string
showContextMenu: boolean
reservedKeywords: string[]
copyElement: {
key: string
type: 'folder' | 'gist' | 'file'
}[]
}

@ -0,0 +1,63 @@
import { MenuItems } from '../types'
export const contextMenuActions: MenuItems = [{
id: 'newFile',
name: 'New File',
type: ['folder', 'gist'],
multiselect: false,
label: ''
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder', 'gist'],
multiselect: false,
label: ''
}, {
id: 'rename',
name: 'Rename',
type: ['file', 'folder'],
multiselect: false,
label: ''
}, {
id: 'delete',
name: 'Delete',
type: ['file', 'folder', 'gist'],
multiselect: false,
label: ''
}, {
id: 'run',
name: 'Run',
extension: ['.js'],
multiselect: false,
label: ''
}, {
id: 'pushChangesToGist',
name: 'Push changes to gist',
type: ['gist'],
multiselect: false,
label: ''
}, {
id: 'publishFolderToGist',
name: 'Publish folder to gist',
type: ['folder'],
multiselect: false,
label: ''
}, {
id: 'publishFileToGist',
name: 'Publish file to gist',
type: ['file'],
multiselect: false,
label: ''
}, {
id: 'copy',
name: 'Copy',
type: ['folder', 'file'],
multiselect: false,
label: ''
}, {
id: 'deleteAll',
name: 'Delete All',
type: ['folder', 'file'],
multiselect: true,
label: ''
}]

@ -148,7 +148,7 @@ function errorHandler (error: any, service: string) {
const gistUrl = 'https://gist.githubusercontent.com/EthereumRemix/091ccc57986452bbb33f57abfb13d173/raw/3367e019335746b73288e3710af2922d4c8ef5a3/origins.json' const gistUrl = 'https://gist.githubusercontent.com/EthereumRemix/091ccc57986452bbb33f57abfb13d173/raw/3367e019335746b73288e3710af2922d4c8ef5a3/origins.json'
try { try {
const { data } = await Axios.get(gistUrl) const { data } = (await Axios.get(gistUrl)) as { data: any }
try { try {
await writeJSON(path.resolve(path.join(__dirname, '..', 'origins.json')), { data }) await writeJSON(path.resolve(path.join(__dirname, '..', 'origins.json')), { data })

@ -23,7 +23,7 @@ export class RemixdClient extends PluginClient {
sharedFolder (currentSharedFolder: string): void { sharedFolder (currentSharedFolder: string): void {
this.currentSharedFolder = currentSharedFolder this.currentSharedFolder = currentSharedFolder
if (this.isLoaded) this.emit('rootFolderChanged') if (this.isLoaded) this.emit('rootFolderChanged', this.currentSharedFolder)
} }
list (): Filelist { list (): Filelist {

@ -88,9 +88,6 @@
"remix-ui-toaster": { "remix-ui-toaster": {
"tags": [] "tags": []
}, },
"remix-ui-file-explorer": {
"tags": []
},
"debugger": { "debugger": {
"tags": [] "tags": []
}, },
@ -129,6 +126,12 @@
}, },
"remix-ide-e2e-src-local-plugin": { "remix-ide-e2e-src-local-plugin": {
"tags": [] "tags": []
},
"remix-ui-editor": {
"tags": []
},
"remix-ui-helper": {
"tags": []
} }
}, },
"targetDependencies": { "targetDependencies": {

6867
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -45,7 +45,7 @@
"workspace-schematic": "nx workspace-schematic", "workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph", "dep-graph": "nx dep-graph",
"help": "nx help", "help": "nx help",
"lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-file-explorer,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,remix-ui-plugin-manager,remix-ui-terminal", "lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-helper,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,remix-ui-plugin-manager,remix-ui-terminal,remix-ui-editor",
"build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd", "build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd", "test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"publish:libs": "npm run build:libs && lerna publish --skip-git && npm run bumpVersion:libs", "publish:libs": "npm run build:libs && lerna publish --skip-git && npm run bumpVersion:libs",
@ -141,6 +141,7 @@
"@ethereumjs/common": "^2.5.0", "@ethereumjs/common": "^2.5.0",
"@ethereumjs/tx": "^3.3.2", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.3", "@ethereumjs/vm": "^5.5.3",
"@monaco-editor/react": "^4.3.1",
"@remixproject/engine": "next", "@remixproject/engine": "next",
"@remixproject/engine-web": "next", "@remixproject/engine-web": "next",
"@remixproject/plugin": "next", "@remixproject/plugin": "next",
@ -224,6 +225,7 @@
"@types/react-beautiful-dnd": "^13.1.2", "@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.3.0", "@types/react-router-dom": "^5.3.0",
"@types/request": "^2.48.7",
"@types/tape": "^4.13.0", "@types/tape": "^4.13.0",
"@types/ws": "^7.2.4", "@types/ws": "^7.2.4",
"@typescript-eslint/eslint-plugin": "^4.32.0", "@typescript-eslint/eslint-plugin": "^4.32.0",

@ -25,7 +25,9 @@
"@remix-project/remix-tests": ["dist/libs/remix-tests/src/index.js"], "@remix-project/remix-tests": ["dist/libs/remix-tests/src/index.js"],
"@remix-project/remix-url-resolver": ["dist/libs/remix-url-resolver/src/index.js"], "@remix-project/remix-url-resolver": ["dist/libs/remix-url-resolver/src/index.js"],
"@remixproject/debugger-plugin": ["apps/debugger/src/index.ts"], "@remixproject/debugger-plugin": ["apps/debugger/src/index.ts"],
"@remixproject/solidity-compiler-plugin": ["apps/solidity-compiler/src/index.ts"], "@remixproject/solidity-compiler-plugin": [
"apps/solidity-compiler/src/index.ts"
],
"@remix-project/remixd": ["dist/libs/remixd/index.js"], "@remix-project/remixd": ["dist/libs/remixd/index.js"],
"@remix-ui/tree-view": ["libs/remix-ui/tree-view/src/index.ts"], "@remix-ui/tree-view": ["libs/remix-ui/tree-view/src/index.ts"],
"@remix-ui/debugger-ui": ["libs/remix-ui/debugger-ui/src/index.ts"], "@remix-ui/debugger-ui": ["libs/remix-ui/debugger-ui/src/index.ts"],
@ -37,15 +39,23 @@
"@remix-ui/toaster": ["libs/remix-ui/toaster/src/index.ts"], "@remix-ui/toaster": ["libs/remix-ui/toaster/src/index.ts"],
"@remix-ui/file-explorer": ["libs/remix-ui/file-explorer/src/index.ts"], "@remix-ui/file-explorer": ["libs/remix-ui/file-explorer/src/index.ts"],
"@remix-ui/workspace": ["libs/remix-ui/workspace/src/index.ts"], "@remix-ui/workspace": ["libs/remix-ui/workspace/src/index.ts"],
"@remix-ui/static-analyser": ["libs/remix-ui/static-analyser/src/index.ts"], "@remix-ui/static-analyser": [
"libs/remix-ui/static-analyser/src/index.ts"
],
"@remix-ui/checkbox": ["libs/remix-ui/checkbox/src/index.ts"], "@remix-ui/checkbox": ["libs/remix-ui/checkbox/src/index.ts"],
"@remix-ui/settings": ["libs/remix-ui/settings/src/index.ts"], "@remix-ui/settings": ["libs/remix-ui/settings/src/index.ts"],
"@remix-project/core-plugin": ["libs/remix-core-plugin/src/index.ts"], "@remix-project/core-plugin": ["libs/remix-core-plugin/src/index.ts"],
"@remix-ui/solidity-compiler": ["libs/remix-ui/solidity-compiler/src/index.ts"], "@remix-ui/solidity-compiler": [
"@remix-ui/publish-to-storage": ["libs/remix-ui/publish-to-storage/src/index.ts"], "libs/remix-ui/solidity-compiler/src/index.ts"
],
"@remix-ui/publish-to-storage": [
"libs/remix-ui/publish-to-storage/src/index.ts"
],
"@remix-ui/plugin-manager": ["libs/remix-ui/plugin-manager/src/index.ts"],
"@remix-ui/renderer": ["libs/remix-ui/renderer/src/index.ts"], "@remix-ui/renderer": ["libs/remix-ui/renderer/src/index.ts"],
"@remix-ui/terminal": ["libs/remix-ui/terminal/src/index.ts"], "@remix-ui/terminal": ["libs/remix-ui/terminal/src/index.ts"],
"@remix-ui/plugin-manager": ["libs/remix-ui/plugin-manager/src/index.ts"] "@remix-ui/editor": ["libs/remix-ui/editor/src/index.ts"],
"@remix-ui/helper": ["libs/remix-ui/helper/src/index.ts"]
} }
}, },
"exclude": ["node_modules", "tmp"] "exclude": ["node_modules", "tmp"]

@ -623,25 +623,6 @@
} }
} }
}, },
"remix-ui-file-explorer": {
"root": "libs/remix-ui/file-explorer",
"sourceRoot": "libs/remix-ui/file-explorer/src",
"projectType": "library",
"schematics": {},
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/file-explorer/tsconfig.lib.json"],
"exclude": [
"**/node_modules/**",
"!libs/remix-ui/file-explorer/**/*"
]
}
}
}
},
"debugger": { "debugger": {
"root": "apps/debugger", "root": "apps/debugger",
"sourceRoot": "apps/debugger/src", "sourceRoot": "apps/debugger/src",
@ -1035,6 +1016,38 @@
} }
} }
} }
},
"remix-ui-editor": {
"root": "libs/remix-ui/editor",
"sourceRoot": "libs/remix-ui/editor/src",
"projectType": "library",
"schematics": {},
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/editor/tsconfig.lib.json"],
"exclude": ["**/node_modules/**", "!libs/remix-ui/editor/**/*"]
}
}
}
},
"remix-ui-helper": {
"root": "libs/remix-ui/helper",
"sourceRoot": "libs/remix-ui/helper/src",
"projectType": "library",
"schematics": {},
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/helper/tsconfig.lib.json"],
"exclude": ["**/node_modules/**", "!libs/remix-ui/helper/**/*"]
}
}
}
} }
}, },
"cli": { "cli": {

Loading…
Cancel
Save