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. 11
      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. 14
      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. 32
      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. 524
      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. 12
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  33. 271
      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. 2
      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. 3
      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. 414
      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. 6101
      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
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

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

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

@ -3,7 +3,7 @@ import { NightwatchBrowser } from 'nightwatch'
class checkAnnotations extends EventEmitter {
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
}
}

@ -3,7 +3,7 @@ import { NightwatchBrowser } from 'nightwatch'
class checkAnnotationsNotPresent extends EventEmitter {
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
}
}

@ -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 {
this.api.perform((client, done) => {
this.api.execute(function () {
const elem: any = document.getElementById('input')
const elem: any = document.getElementById('editorView')
return elem.editor.getValue()
return elem.currentContent()
}, [], (result) => {
done()
const value = typeof result.value === 'string' ? result.value : null

@ -39,8 +39,8 @@ function removeFile (browser: NightwatchBrowser, path: string, workspace: string
.pause(2000)
.perform(() => {
console.log(path, 'to remove')
browser.waitForElementVisible('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok')
.click('*[data-id="' + workspace + 'ModalDialogContainer-react"] .modal-ok')
browser.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementNotPresent('[data-path="' + path + '"]')
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 {
this.api.perform((client, done) => {
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], () => {
done()
if (callback) {

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

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

@ -62,9 +62,13 @@ module.exports = {
},
'Should jump through breakpoints': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]')
.click('.ace_gutter-cell:nth-of-type(10)')
.click('.ace_gutter-cell:nth-of-type(20)')
browser.waitForElementVisible('#editorView')
.execute(() => {
(window as any).addRemixBreakpoint(11)
}, [], () => {})
.execute(() => {
(window as any).addRemixBreakpoint(21)
}, [], () => {})
.waitForElementVisible('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
.click('*[data-id="buttonNavigatorJumpPreviousBreakpoint"]')
.pause(2000)
@ -127,7 +131,7 @@ module.exports = {
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
*/
.waitForElementPresent('.highlightLine7')
.waitForElementPresent('.highlightLine8')
.goToVMTraceStep(266)
.pause(1000)
.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"]')
.openFile('contracts')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('*[data-id="editorInput"]')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '12px')
.waitForElementVisible('#editorView')
.checkElementStyle('.view-lines', 'font-size', '14px')
.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) {
browser.waitForElementVisible('*[data-id="editorInput"]')
.checkElementStyle('*[data-id="editorInput"]', 'font-size', '14px')
browser.waitForElementVisible('#editorView')
.checkElementStyle('.view-lines', 'font-size', '16px')
.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) {
browser.waitForElementVisible('*[data-id="editorInput"]')
.waitForElementVisible('*[class="ace_content"]')
.click('*[class="ace_content"]')
.editorScroll('down', 27) // scroll down to line 27 and add the error word
.sendKeys('*[class="ace_text-input"]', 'error')
.waitForElementVisible('.ace_error', 120000)
.checkAnnotations('error', 28)
browser.waitForElementVisible('#editorView')
.setEditorValue(storageContractWithError + 'error')
.pause(2000)
.waitForElementVisible('.margin-view-overlays .fa-exclamation-square', 120000)
.checkAnnotations('fa-exclamation-square', 29) // error
.clickLaunchIcon('udapp')
.checkAnnotationsNotPresent('error')
.checkAnnotationsNotPresent('fa-exclamation-square') // error
.clickLaunchIcon('solidity')
.checkAnnotations('error', 28)
.checkAnnotations('fa-exclamation-square', 29) // error
},
'Should minimize and maximize codeblock in editor': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]')
'Should minimize and maximize codeblock in editor': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.waitForElementVisible('.ace_open')
.click('.ace_start:nth-of-type(1)')
.waitForElementVisible('.ace_closed')
@ -54,27 +52,29 @@ module.exports = {
},
'Should add breakpoint to editor': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]')
.waitForElementNotPresent('.ace_breakpoint')
.click('.ace_gutter-cell:nth-of-type(1)')
.waitForElementVisible('.ace_breakpoint')
browser.waitForElementVisible('#editorView')
.waitForElementNotPresent('.margin-view-overlays .fa-circle')
.execute(() => {
(window as any).addRemixBreakpoint(1)
}, [], () => {})
.waitForElementVisible('.margin-view-overlays .fa-circle')
},
'Should load syntax highlighter for ace light theme': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="editorInput"]')
'Should load syntax highlighter for ace light theme': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment)
.checkElementStyle('.ace_function', 'color', aceThemes.light.function)
.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"]')
.click('*[data-id="verticalIconsKindsettings"]')
.waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]')
.click('*[data-id="settingsTabThemeLabelDark"]')
.pause(2000)
.waitForElementVisible('*[data-id="editorInput"]')
.waitForElementVisible('#editorView')
/* @todo(#2863) ch for class and not colors
.checkElementStyle('.ace_keyword', 'color', aceThemes.dark.keyword)
.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
browser
.addFile('sourcehighlight.js', sourcehighlightScript)
.addFile('removeSourcehighlightScript.js', removeSourcehighlightScript)
.addFile('removeAllSourcehighlightScript.js', removeAllSourcehighlightScript)
.openFile('sourcehighlight.js')
.executeScript('remix.exeCurrent()')
.editorScroll('down', 60)
.pause(1000)
.waitForElementPresent('.highlightLine32', 60000)
.pause(1000)
.checkElementStyle('.highlightLine32', 'background-color', 'rgb(8, 108, 181)')
.pause(1000)
.waitForElementPresent('.highlightLine40', 60000)
.pause(1000)
.checkElementStyle('.highlightLine40', 'background-color', 'rgb(8, 108, 181)')
.waitForElementPresent('.highlightLine50', 60000)
.checkElementStyle('.highlightLine50', 'background-color', 'rgb(8, 108, 181)')
.scrollToLine(32)
.waitForElementPresent('.highlightLine33', 60000)
.checkElementStyle('.highlightLine33', 'background-color', 'rgb(52, 152, 219)')
.scrollToLine(40)
.waitForElementPresent('.highlightLine41', 60000)
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.scrollToLine(50)
.waitForElementPresent('.highlightLine51', 60000)
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
},
'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"]')
.click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.pause(2000)
@ -113,9 +110,9 @@ module.exports = {
.click('li[data-id="treeViewLitreeViewItemcontracts"]')
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.waitForElementNotPresent('.highlightLine32', 60000)
.checkElementStyle('.highlightLine40', 'background-color', 'rgb(8, 108, 181)')
.checkElementStyle('.highlightLine50', 'background-color', 'rgb(8, 108, 181)')
.waitForElementNotPresent('.highlightLine33', 60000)
.checkElementStyle('.highlightLine41', 'background-color', 'rgb(52, 152, 219)')
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
},
'Should remove all highlights from source code': function (browser: NightwatchBrowser) {
@ -126,9 +123,9 @@ module.exports = {
.waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.click('li[data-id="treeViewLitreeViewItemcontracts/3_Ballot.sol"]')
.pause(2000)
.waitForElementNotPresent('.highlightLine32', 60000)
.waitForElementNotPresent('.highlightLine40', 60000)
.waitForElementNotPresent('.highlightLine50', 60000)
.waitForElementNotPresent('.highlightLine33', 60000)
.waitForElementNotPresent('.highlightLine41', 60000)
.waitForElementNotPresent('.highlightLine51', 60000)
.end()
}
}
@ -152,6 +149,7 @@ const sourcehighlightScript = {
content: `
(async () => {
try {
await remix.call('fileManager', 'open', 'contracts/3_Ballot.sol')
const pos = {
start: {
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 = {
content: `
(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"]')
.rightClick('[data-path="Browser_E2E_Tests"]')
.click('*[id="menuitemdelete"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.waitForElementNotPresent('*[data-id="treeViewLitreeViewItemBrowser_E2E_Tests"]')
},
@ -81,13 +81,13 @@ module.exports = {
.pause(10000)
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000)
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]', 60000)
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok', 60000)
.pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.click('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.pause(2000)
.perform((done) => {
if (runtimeBrowser === 'chrome') {
@ -101,6 +101,7 @@ module.exports = {
'Should open local filesystem explorer': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="filePanelFileExplorerTree"]')
.click('[data-id="remixUIWorkspaceExplorer"]')
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile1)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile2)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile3)

@ -36,7 +36,9 @@ module.exports = {
.executeScript('remix.exeCurrent()')
.pause(2000)
.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) {

@ -103,14 +103,18 @@ module.exports = {
.clickLaunchIcon('filePanel')
.waitForElementVisible('*[data-id="fileExplorerNewFilepublishToGist"]')
.click('*[data-id="fileExplorerNewFilepublishToGist"]')
.waitForElementVisible('*[data-id="default_workspaceModalDialogContainer-react"]')
.pause(2000)
.click('*[data-id="default_workspaceModalDialogContainer-react"] .modal-ok')
.waitForElementVisible('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok')
.execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() })
.pause(10000)
.getText('[data-id="default_workspaceModalDialogModalBody-react"]', (result) => {
.perform((done) => {
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) {

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

@ -85,7 +85,7 @@ module.exports = {
.waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () {
browser
.click('[data-id="staticAnalysisModuleMiscellaneous1"')
.waitForElementPresent('.highlightLine15', 60000)
.waitForElementPresent('.highlightLine16', 60000)
.getEditorValue((content) => {
browser.assert.ok(content.indexOf(
'function _sendLogPayload(bytes memory payload) private view {') !== -1,
@ -153,7 +153,6 @@ function runTests (browser: NightwatchBrowser) {
.clickLaunchIcon('filePanel')
.waitForElementVisible('[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/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

@ -75,7 +75,9 @@ module.exports = {
.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
.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) {

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

@ -53,7 +53,7 @@ module.exports = {
'Call web3.eth.getAccounts() using JavaScript VM': function (browser: NightwatchBrowser) {
browser
.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) {
@ -111,6 +111,7 @@ module.exports = {
.addFile('deployWithEthersJs.js', { content: deployWithEthersJs })
.openFile('deployWithEthersJs.js')
.pause(1000)
.click('[data-id="treeViewDivtreeViewItemcontracts"]')
.openFile('contracts/2_Owner.sol')
.clickLaunchIcon('solidity')
.click('*[data-id="compilerContainerCompileBtn"]') // compile Owner

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

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

@ -162,8 +162,7 @@ class App {
}
init () {
var self = this
run.apply(self)
this.run().catch(console.error)
}
render () {
@ -202,11 +201,8 @@ class App {
`
return self._view.el
}
}
module.exports = App
async function run () {
async run () {
var self = this
// check the origin and warn message
@ -220,8 +216,8 @@ async function run () {
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`)
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.')
@ -254,7 +250,7 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
}, 1500)
})
// ----------------- editor service ----------------------------
const editor = new Editor({}, themeModule) // wrapper around ace editor
const editor = new Editor() // wrapper around ace editor
registry.put({ api: editor, name: 'editor' })
editor.event.register('requiringToSaveCurrentfile', () => fileManager.saveCurrentFile())
@ -299,10 +295,8 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
newpos = (newpos < height - limitDown) ? newpos : height - limitDown
return height - newpos
}
},
registry
}
)
const contextualListener = new ContextualListener({ editor })
engine.register([
@ -470,14 +464,20 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
console.log('couldn\'t register iframe plugins', e.message)
}
await appManager.activatePlugin(['theme', 'editor', 'fileManager', 'compilerMetadata', 'compilerArtefacts', 'network', 'web3Provider', 'offsetToLineColumnConverter'])
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', 'filePanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport'])
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 {
@ -509,10 +509,14 @@ Please make a backup of your contracts and start using http://remix.ethereum.org
// activate solidity plugin
appManager.activatePlugin(['solidity', 'udapp'])
}
})
// Load and start the service who manager layout and frame
const framingService = new FramingService(sidePanel, menuicons, mainview, this._components.resizeFeature)
if (params.embed) framingService.embed()
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'
const { AstWalker } = require('@remix-project/remix-astwalker')
const csjs = require('csjs-inject')
const EventManager = require('../../lib/events')
const globalRegistry = require('../../global/registry')
@ -127,14 +126,6 @@ class ContextualListener extends Plugin {
const lastCompilationResult = this._deps.compilersArtefacts.__last
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0) {
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 has children, highlight the entire line. if not, just highlight the current source position of the node.
lineColumn = {
@ -150,7 +141,7 @@ class ContextualListener extends Plugin {
}
const fileName = lastCompilationResult.getSourceName(position.file)
if (fileName) {
return this.editor.addMarker(lineColumn, fileName, css.highlightref_fullLine)
return this.call('editor', 'highlight', lineColumn, fileName, '', { focus: false })
}
}
return null
@ -178,10 +169,7 @@ class ContextualListener extends Plugin {
}
_stopHighlighting () {
for (const eventKey in this._activeHighlights) {
const event = this._activeHighlights[eventKey]
this.editor.removeMarker(event.eventId, event.fileTarget)
}
this.call('editor', 'discardHighlight')
this.event.trigger('stopHighlighting', [])
this._activeHighlights = []
}

@ -1,200 +1,95 @@
'use strict'
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { EditorUI } from '@remix-ui/editor' // eslint-disable-line
import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json'
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 = {
displayName: 'Editor',
name: 'editor',
description: 'service - editor',
version: packageJson.version,
methods: ['highlight', 'discardHighlight', 'discardHighlightAt', 'clearAnnotations', 'addAnnotation', 'gotoLine']
methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine']
}
class Editor extends Plugin {
constructor (opts = {}, themeModule) {
constructor () {
super(profile)
// Dependancies
this._components = {}
this._components.registry = globalRegistry
this._deps = {
config: this._components.registry.get('config').api
}
this._themes = {
light: 'chrome',
dark: 'chaos',
remixDark: 'remixDark'
light: 'light',
dark: 'vs-dark',
remixDark: 'remix-dark'
}
themeModule.events.on('themeChanged', (theme) => {
this.setTheme(theme.name === 'Dark' ? 'remixDark' : theme.quality)
})
// Init
this.event = new EventManager()
this.sessions = {}
this.sourceAnnotationsPerFile = []
this.sourceAnnotationsPerFile = {}
this.markerPerFile = {}
this.readOnlySessions = {}
this.previousInput = ''
this.saveTimeout = null
this.sourceHighlighters = new SourceHighlighters()
this.emptySession = this._createSession('')
this.emptySession = null
this.modes = {
sol: 'ace/mode/solidity',
yul: 'ace/mode/solidity',
mvir: 'ace/mode/move',
js: 'ace/mode/javascript',
py: 'ace/mode/python',
vy: 'ace/mode/python',
zok: 'ace/mode/zokrates',
lex: 'ace/mode/lexon',
txt: 'ace/mode/text',
json: 'ace/mode/json',
abi: 'ace/mode/json',
rs: 'ace/mode/rust'
}
// Editor Setup
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
})
sol: 'sol',
yul: 'sol',
mvir: 'move',
js: 'javascript',
py: 'python',
vy: 'python',
zok: 'zokrates',
lex: 'lexon',
txt: 'text',
json: 'json',
abi: 'json',
rs: 'rust'
}
el.className += ' ' + css['ace-editor']
el.editor = this.editor // required to access the editor during tests
this.render = () => el
this.activated = false
// Completer for editor
const flowCompleter = {
getCompletions: (editor, session, pos, prefix, callback) => {
// @TODO add here other propositions
}
this.events = {
onBreakPointAdded: (file, line) => this.triggerEvent('breakpointAdded', [file, line]),
onBreakPointCleared: (file, line) => this.triggerEvent('breakpointCleared', [file, line]),
onDidChangeContent: (file) => this._onChange(file),
onEditorMounted: () => this.triggerEvent('editorMounted', [])
}
langTools.addCompleter(flowCompleter)
// zoom with Ctrl+wheel
window.addEventListener('wheel', (e) => {
if (e.ctrlKey && Math.abs(e.wheelY) > 5) {
this.editorFontSize(e.wheelY > 0 ? 1 : -1)
// to be implemented by the react component
this.api = {}
}
})
// EVENTS LISTENERS
render () {
if (this.el) return this.el
// Gutter Mouse down
this.editor.on('guttermousedown', e => {
const target = e.domEvent.target
if (target.className.indexOf('ace_gutter-cell') === -1) {
return
this.el = document.createElement('div')
this.el.setAttribute('id', 'editorView')
this.el.currentContent = () => this.currentContent() // used by e2e test
this.el.setCurrentContent = (value) => {
if (this.sessions[this.currentFile]) {
this.sessions[this.currentFile].setValue(value)
this._onChange(this.currentFile)
}
const row = e.getDocumentPosition().row
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.el.gotoLine = (line) => this.gotoLine(line, 0)
return this.el
}
this.setBreakpoint(row)
this.triggerEvent('breakpointAdded', [this.currentSession, row])
e.stop()
})
// Do setup on initialisation here
this.editor.on('changeSession', () => {
this._onChange()
this.triggerEvent('sessionSwitched', [])
this.editor.getSession().on('change', () => {
this._onChange()
this.sourceHighlighters.discardAllHighlights()
this.triggerEvent('contentChanged', [])
})
})
renderComponent () {
ReactDOM.render(
<EditorUI
editorAPI={this.api}
theme={this.currentTheme}
currentFile={this.currentFile}
sourceAnnotationsPerFile={this.sourceAnnotationsPerFile}
markerPerFile={this.markerPerFile}
events={this.events}
plugin={this}
/>
, this.el)
}
triggerEvent (name, params) {
@ -203,14 +98,25 @@ class Editor extends Plugin {
}
onActivation () {
this.activated = true
this.on('sidePanel', 'focusChanged', (name) => {
this.sourceHighlighters.hideHighlightsExcept(name)
this.keepAnnotationsFor(name)
this.keepDecorationsFor(name, 'sourceAnnotationsPerFile')
this.keepDecorationsFor(name, 'markerPerFile')
})
this.on('sidePanel', 'pluginDisabled', (name) => {
this.sourceHighlighters.discardHighlight(name)
this.clearAllAnnotationsFor(name)
this.clearAllDecorationsFor(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 () {
@ -218,30 +124,14 @@ class Editor extends Plugin {
this.off('sidePanel', 'pluginDisabled')
}
highlight (position, filePath, hexColor) {
const { from } = this.currentRequest
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')
async _onChange (file) {
const currentFile = await this.call('fileManager', 'file')
if (!currentFile) {
return
}
if (currentFile !== file) {
return
}
const input = this.get(currentFile)
if (!input) {
return
@ -257,16 +147,17 @@ class Editor extends Plugin {
if (this.saveTimeout) {
window.clearTimeout(this.saveTimeout)
}
this.triggerEvent('contentChanged', [])
this.saveTimeout = window.setTimeout(() => {
this.triggerEvent('requiringToSaveCurrentfile', [])
}, 5000)
}
_switchSession (path) {
this.currentSession = path
this.editor.setSession(this.sessions[this.currentSession])
this.editor.setReadOnly(this.readOnlySessions[this.currentSession])
this.editor.focus()
this.triggerEvent('sessionSwitched', [])
this.currentFile = path
this.renderComponent()
}
/**
@ -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} mode Ace Mode for this file [Default is `text`]
* @param {string} mode Mode for this file [Default is `text`]
*/
_createSession (content, mode) {
const s = new ace.EditSession(content)
s.setMode(mode || 'ace/mode/text')
s.setUndoManager(new ace.UndoManager())
s.setTabSize(4)
s.setUseSoftTabs(true)
return s
_createSession (path, content, mode) {
if (!this.activated) return
this.emit('addModel', content, mode, path, false)
return {
path,
language: mode,
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
*/
find (string) {
return this.editor.find(string)
return this.api.findMatches(this.currentFile, string)
}
/**
* Display an Empty read-only session
*/
displayEmptyReadOnlySession () {
this.currentSession = null
this.editor.setSession(this.emptySession)
this.editor.setReadOnly(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)
}
if (!this.activated) return
this.currentFile = null
this.emit('addModel', '', 'text', '_blank', true)
}
/**
@ -338,8 +219,8 @@ class Editor extends Plugin {
* @param {string} text New text to be place.
*/
setText (text) {
if (this.currentSession && this.sessions[this.currentSession]) {
this.sessions[this.currentSession].setValue(text)
if (this.currentFile && this.sessions[this.currentFile]) {
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
*/
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.readOnlySessions[path] = false
} else if (this.sessions[path].getValue() !== content) {
@ -372,7 +253,7 @@ class Editor extends Plugin {
*/
openReadOnly (path, content) {
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.readOnlySessions[path] = true
}
@ -394,8 +275,8 @@ class Editor extends Plugin {
* @return {String} content of the file referenced by @arg path
*/
get (path) {
if (!path || this.currentSession === path) {
return this.editor.getValue()
if (!path || this.currentFile === path) {
return this.api.getValue(path)
} else if (this.sessions[path]) {
return this.sessions[path].getValue()
}
@ -407,29 +288,23 @@ class Editor extends Plugin {
* @return {String} path of the current session
*/
current () {
if (this.editor.getSession() === this.emptySession) {
return
}
return this.currentSession
return this.currentFile
}
/**
* The position of the cursor
*/
getCursorPosition () {
return this.editor.session.doc.positionToIndex(
this.editor.getCursorPosition(),
0
)
return this.api.getCursorPosition()
}
/**
* Remove the current session from the list of sessions.
*/
discardCurrentSession () {
if (this.sessions[this.currentSession]) {
delete this.sessions[this.currentSession]
this.currentSession = null
if (this.sessions[this.currentFile]) {
delete this.sessions[this.currentFile]
this.currentFile = null
}
}
@ -438,73 +313,56 @@ class Editor extends Plugin {
* @param {string} path
*/
discard (path) {
if (this.sessions[path]) delete this.sessions[path]
if (this.currentSession === path) this.currentSession = null
if (this.sessions[path]) {
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.
* @param {boolean} useWrapMode Enable (or disable) wrap mode
* Increment the font size (in pixels) for the editor text.
* @param {number} incr The amount of pixels to add to the font.
*/
resize (useWrapMode) {
this.editor.resize()
const session = this.editor.getSession()
session.setUseWrapMode(useWrapMode)
if (session.getUseWrapMode()) {
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))
}
editorFontSize (incr) {
if (!this.activated) return
const newSize = this.api.getFontSize() + incr
if (newSize >= 6) {
this.emit('setFontSize', newSize)
}
}
/**
* Adds a new marker to the given `Range`.
* @param {*} lineColumnPos
* @param {string} source Path of the session to add the mark on.
* @param {string} cssClass css to apply to the mark.
* Resize the editor, and sets whether or not line wrapping is enabled.
* @param {boolean} useWrapMode Enable (or disable) wrap mode
*/
addMarker (lineColumnPos, source, cssClass) {
const currentRange = new Range(
lineColumnPos.start.line,
lineColumnPos.start.column,
lineColumnPos.end.line,
lineColumnPos.end.column
)
if (this.sessions[source]) {
return this.sessions[source].addMarker(currentRange, cssClass)
}
return null
resize (useWrapMode) {
if (!this.activated) return
this.emit('setWordWrap', useWrapMode)
}
/**
* Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to).
* @param {number} line The line to scroll to
* @param {boolean} center If true
* @param {boolean} animate If true animates scrolling
* @param {Function} callback Function to be called when the animation has finished
* Moves the cursor and focus to the specified line and column number
* @param {number} line
* @param {number} col
*/
scrollToLine (line, center, animate, callback) {
this.editor.scrollToLine(line, center, animate, callback)
gotoLine (line, col) {
if (!this.activated) return
this.emit('focus')
this.emit('revealLine', line + 1, col)
}
/**
* Remove a marker from the session
* @param {string} markerId Id of the marker
* @param {string} source Path of the session
* Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to).
* @param {number} line The line to scroll to
*/
removeMarker (markerId, source) {
if (this.sessions[source]) {
this.sessions[source].removeMarker(markerId)
}
scrollToLine (line) {
if (!this.activated) return
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:
column: -1
row: -1
@ -512,64 +370,79 @@ class Editor extends Plugin {
type: "warning"
* @param {String} filePath
* @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)
const session = this.sessions[filePath] || this.editor.getSession()
const path = filePath || this.currentSession
const path = filePath || this.currentFile
const currentAnnotations = this.sourceAnnotationsPerFile[path]
const currentAnnotations = this[typeOfDecoration][path]
if (!currentAnnotations) return
const newAnnotations = []
for (const annotation of currentAnnotations) {
if (annotation.from !== plugin) newAnnotations.push(annotation)
}
this.sourceAnnotationsPerFile[path] = newAnnotations
this._setAnnotations(session, path)
this[typeOfDecoration][path] = newAnnotations
this.renderComponent()
}
keepAnnotationsFor (name) {
if (!this.currentSession) return
if (!this.sourceAnnotationsPerFile[this.currentSession]) return
keepDecorationsFor (name, typeOfDecoration) {
if (!this.currentFile) return
if (!this[typeOfDecoration][this.currentFile]) return
const annotations = this.sourceAnnotationsPerFile[this.currentSession]
const annotations = this[typeOfDecoration][this.currentFile]
for (const annotation of annotations) {
annotation.hide = annotation.from !== name
}
this._setAnnotations(this.editor.getSession(), this.currentSession)
this.renderComponent()
}
/**
* 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:
column: -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.↵"
type: "warning"
* @param {String} filePath
* @param {String} plugin
*/
clearAnnotations (filePath) {
const { from } = this.currentRequest
this.clearAnnotationsByPlugin(filePath, from)
clearAllDecorationsFor (plugin) {
for (const session in this.sessions) {
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:
column: -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.↵"
type: "warning"
* @param {String} filePath
* @param {String} plugin
*/
clearAllAnnotationsFor (plugin) {
for (const session in this.sessions) {
this.clearAnnotationsByPlugin(session, plugin)
clearAnnotations (filePath) {
filePath = filePath || this.currentFile
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 {String} filePath
*/
addAnnotation (annotation, 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.currentSession
const { from } = this.currentRequest
if (!this.sourceAnnotationsPerFile[path]) this.sourceAnnotationsPerFile[path] = []
annotation.from = from
this.sourceAnnotationsPerFile[path].push(annotation)
this._setAnnotations(session, path)
async addAnnotation (annotation, filePath) {
filePath = filePath || this.currentFile
await this.addDecoration(annotation, filePath, 'sourceAnnotationsPerFile')
}
_setAnnotations (session, path) {
const annotations = this.sourceAnnotationsPerFile[path]
session.setAnnotations(annotations.filter((element) => !element.hide))
async highlight (position, filePath, highlightColor, opt = { focus: true }) {
filePath = filePath || this.currentFile
if (opt.focus) {
await this.call('fileManager', 'open', filePath)
this.scrollToLine(position.start.line)
}
await this.addDecoration({ position }, filePath, 'markerPerFile')
}
/**
* Moves the cursor and focus to the specified line and column number
* @param {number} line
* @param {number} col
*/
gotoLine (line, col) {
this.editor.focus()
this.editor.gotoLine(line + 1, col - 1, true)
discardHighlight () {
const { from } = this.currentRequest
for (const session in this.sessions) {
this.clearDecorationsByPlugin(session, from, 'markerPerFile')
}
}
}

@ -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')
}
closeAllFiles () {
async closeAllFiles () {
// TODO: Only keep `this.emit` (issue#2210)
this.emit('filesAllClosed')
this.events.emit('filesAllClosed')
@ -465,7 +465,7 @@ class FileManager extends Plugin {
}
}
closeFile (name) {
async closeFile (name) {
delete this.openedFiles[name]
if (!Object.keys(this.openedFiles).length) {
this._deps.config.set('currentFile', '')
@ -506,7 +506,8 @@ class FileManager extends Plugin {
async setFileContent (path, content) {
if (this.currentRequest) {
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
toaster(yo`
<div>
@ -611,6 +612,7 @@ class FileManager extends Plugin {
}
async openFile (file) {
file = this.normalize(file)
if (!file) {
this.emit('noFileSelected')
this.events.emit('noFileSelected')

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

@ -13,6 +13,7 @@ class WorkspaceFileProvider extends FileProvider {
}
setWorkspace (workspace) {
if (!workspace) return
workspace = workspace.replace(/^\/|\/$/g, '') // remove first and last slash
this.workspace = workspace
}
@ -30,7 +31,6 @@ class WorkspaceFileProvider extends FileProvider {
}
removePrefix (path) {
if (!this.workspace) this.createWorkspace()
path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path
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) {
if (!this.workspace) this.createWorkspace()
super.resolveDirectory(path, (error, files) => {
if (error) return callback(error)
const unscoped = {}
@ -76,13 +75,18 @@ class WorkspaceFileProvider extends FileProvider {
}
_normalizePath (path) {
if (!this.workspace) this.createWorkspace()
return path.replace(this.workspacesPath + '/' + this.workspace + '/', '')
}
createWorkspace (name) {
async createWorkspace (name) {
try {
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 React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import { Workspace } from '@remix-ui/workspace' // eslint-disable-line
import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import { checkSpecialChars, checkSlash } from '../../lib/helper'
import { FileSystemProvider } from '@remix-ui/workspace' // eslint-disable-line
const { RemixdHandle } = require('../files/remixd-handle.js')
const { GitHandle } = require('../files/git-handle.js')
const { HardhatHandle } = require('../files/hardhat-handle.js')
const { SlitherHandle } = require('../files/slither-handle.js')
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:
* fileManager: @args fileProviders (browser, shared-folder, swarm, github, etc ...) & config & editor
@ -35,8 +29,8 @@ const modalDialogCustom = require('../ui/modal-dialog-custom')
const profile = {
name: 'filePanel',
displayName: 'File explorers',
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace'],
events: ['setWorkspace', 'renameWorkspace', 'deleteWorkspace', 'createWorkspace'],
methods: ['createNewFile', 'uploadFile', 'getCurrentWorkspace', 'getWorkspaces', 'createWorkspace', 'setWorkspace', 'registerContextMenuItem', 'renameWorkspace', 'deleteWorkspace'],
events: ['setWorkspace', 'workspaceRenamed', 'workspaceDeleted', 'workspaceCreated'],
icon: 'assets/img/fileManager.webp',
description: ' - ',
kind: 'fileexplorer',
@ -47,54 +41,33 @@ const profile = {
module.exports = class Filepanel extends ViewPlugin {
constructor (appManager) {
super(profile)
this._components = {}
this._components.registry = globalRegistry
this._deps = {
fileProviders: this._components.registry.get('fileproviders').api,
fileManager: this._components.registry.get('filemanager').api
}
this.registry = globalRegistry
this.fileProviders = this.registry.get('fileproviders').api
this.fileManager = this.registry.get('filemanager').api
this.el = document.createElement('div')
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.hardhatHandle = new HardhatHandle()
this.slitherHandle = new SlitherHandle()
this.registeredMenuItems = []
this.removedMenuItems = []
this.request = {}
this.workspaces = []
this.initialWorkspace = null
this.appManager = appManager
this.currentWorkspaceMetadata = {}
}
onActivation () {
this.renderComponent()
}
render () {
this.initWorkspace().then(() => this.getWorkspaces()).catch(console.error)
return this.el
}
renderComponent () {
ReactDOM.render(
<Workspace
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}
/>
<FileSystemProvider plugin={this} />
, this.el)
}
@ -103,203 +76,103 @@ module.exports = class Filepanel extends ViewPlugin {
* @param callback (...args) => void
*/
registerContextMenuItem (item) {
if (!item) throw new Error('Invalid register context menu argument')
if (!item.name || !item.id) throw new Error('Item name and id is mandatory')
if (!item.type && !item.path && !item.extension && !item.pattern) throw new Error('Invalid file matching criteria provided')
if (this.registeredMenuItems.filter((o) => {
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()
return new Promise((resolve, reject) => {
this.emit('registerContextMenuItemReducerEvent', item, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
removePluginActions (plugin) {
this.registeredMenuItems = this.registeredMenuItems.filter((item) => {
if (item.id !== plugin.name || item.sticky === true) return true
else {
this.removedMenuItems.push(item)
return false
}
return new Promise((resolve, reject) => {
this.emit('removePluginActionsReducerEvent', plugin, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
this.renderComponent()
}
async getCurrentWorkspace () {
return await this.request.getCurrentWorkspace()
getCurrentWorkspace () {
return this.currentWorkspaceMetadata
}
async 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()
getWorkspaces () {
return this.workspaces
}
async initWorkspace () {
this.renderComponent()
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
setWorkspaces (workspaces) {
this.workspaces = workspaces
}
const self = this
this.appManager.on('manager', 'pluginDeactivated', self.removePluginActions.bind(this))
// insert example contracts if there are no files to show
createNewFile () {
return new Promise((resolve, reject) => {
this._deps.fileProviders.browser.resolveDirectory('/', async (error, filesList) => {
if (error) return reject(error)
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]
const provider = this.fileManager.currentFileProvider()
const dir = provider.workspace || '/'
this._deps.fileProviders.workspace.setWorkspace(workspaceName)
return resolve(workspaceName)
}
return reject(new Error('Can\'t find available workspace.'))
})
}
this.emit('createNewFileInputReducerEvent', dir, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
async createNewFile () {
return await this.request.createNewFile()
}
uploadFile (target) {
return new Promise((resolve, reject) => {
const provider = this.fileManager.currentFileProvider()
const dir = provider.workspace || '/'
async uploadFile (event) {
return await this.request.uploadFile(event)
return this.emit('uploadFileReducerEvent', dir, target, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}
async processCreateWorkspace (name) {
const workspaceProvider = this._deps.fileProviders.workspace
const browserProvider = this._deps.fileProviders.browser
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name
const workspaceRootPath = 'browser/' + workspaceProvider.workspacesPath
const workspaceRootPathExists = await browserProvider.exists(workspaceRootPath)
const workspacePathExists = await browserProvider.exists(workspacePath)
if (!workspaceRootPathExists) browserProvider.createDir(workspaceRootPath)
if (!workspacePathExists) browserProvider.createDir(workspacePath)
createWorkspace (workspaceName, isEmpty) {
return new Promise((resolve, reject) => {
this.emit('createWorkspaceReducerEvent', workspaceName, isEmpty, (err, data) => {
if (err) reject(err)
else resolve(data || true)
})
})
}
async workspaceExists (name) {
const workspaceProvider = this._deps.fileProviders.workspace
const browserProvider = this._deps.fileProviders.browser
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name
return browserProvider.exists(workspacePath)
renameWorkspace (oldName, workspaceName) {
return new Promise((resolve, reject) => {
this.emit('renameWorkspaceReducerEvent', oldName, workspaceName, (err, data) => {
if (err) reject(err)
else resolve(data || true)
})
})
}
async createWorkspace (workspaceName, isEmpty = false) {
if (!workspaceName) throw new Error('name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
if (await this.workspaceExists(workspaceName)) throw new Error('workspace already exists')
else {
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)
}
}
}
}
deleteWorkspace (workspaceName) {
return new Promise((resolve, reject) => {
this.emit('deleteWorkspaceReducerEvent', workspaceName, (err, data) => {
if (err) reject(err)
else resolve(data || true)
})
})
}
async renameWorkspace (oldName, workspaceName) {
if (!workspaceName) throw new Error('name cannot be empty')
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)
}
setWorkspace (workspace) {
const workspaceProvider = this.fileProviders.workspace
/** these are called by the react component, action is already finished whent it's called */
async setWorkspace (workspace, setEvent = true) {
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.currentWorkspaceMetadata = { name: workspace.name, isLocalhost: workspace.isLocalhost, absolutePath: `${workspaceProvider.workspacesPath}/${workspace.name}` }
this.emit('setWorkspace', workspace)
}
}
workspaceRenamed (workspace) {
this.emit('renameWorkspace', workspace)
workspaceRenamed (oldName, workspaceName) {
this.emit('workspaceRenamed', oldName, workspaceName)
}
workspaceDeleted (workspace) {
this.emit('deleteWorkspace', workspace)
this.emit('workspaceDeleted', workspace)
}
workspaceCreated (workspace) {
this.emit('createWorkspace', workspace)
this.emit('workspaceCreated', workspace)
}
/** end section */
}

@ -44,19 +44,32 @@ export class TabProxy extends Plugin {
fileManager.events.on('fileRemoved', (name) => {
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) => {
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) => {
const workspace = this.fileManager.currentWorkspace()
if (workspace) {
if (this.fileManager.mode === 'browser') {
const workspacePath = workspace + '/' + file
if (this._handlers[workspacePath]) {
@ -72,7 +85,7 @@ export class TabProxy extends Plugin {
this.event.emit('closeFile', file)
})
} else {
const path = this.fileManager.mode + '/' + file
const path = file.startsWith(this.fileManager.mode + '/') ? file : this.fileManager.mode + '/' + file
if (this._handlers[path]) {
this._view.filetabs.activateTab(path)
@ -92,7 +105,7 @@ export class TabProxy extends Plugin {
fileManager.events.on('fileRenamed', (oldName, newName, isFolder) => {
const workspace = this.fileManager.currentWorkspace()
if (workspace) {
if (this.fileManager.mode === 'browser') {
if (isFolder) {
for (const tab of this.loadedTabs) {
if (tab.name.indexOf(workspace + '/' + oldName + '/') === 0) {
@ -115,7 +128,7 @@ export class TabProxy extends Plugin {
return
}
// 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
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="tabProxyZoomIn" class="btn btn-sm px-1 fas fa-search-plus text-dark" onclick=${() => this.onZoomIn()}></span>
</div>
@ -307,6 +320,7 @@ export class TabProxy extends Plugin {
this._view.tabs = yo`
<div style="display: -webkit-box; max-height: 32px">
${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}
</div>
`

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

@ -115,6 +115,7 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
async onActivation () {
super.onActivation()
this.on('filePanel', 'workspaceInitializationCompleted', () => {
this.call('filePanel', 'registerContextMenuItem', {
id: 'solidity',
name: 'compileFile',
@ -124,6 +125,7 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
path: [],
pattern: []
})
})
try {
this.currentFile = await this.call('fileManager', 'file')
} catch (error) {

@ -48,7 +48,6 @@ module.exports = class TestTab extends ViewPlugin {
appManager.event.on('activate', (name) => {
if (name === 'solidity') this.updateRunAction()
console.log('solidity is activated')
})
appManager.event.on('deactivate', (name) => {
if (name === 'solidity') this.updateRunAction()
@ -224,7 +223,7 @@ module.exports = class TestTab extends ViewPlugin {
runningTests[fileName].content
)
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.RemixSimulatorProvider = new Provider({ fork: this.executionContext.getCurrentFork() })
this.RemixSimulatorProvider.init()
this.RemixSimulatorProvider.Accounts.resetAccounts()
this.web3 = new Web3(this.RemixSimulatorProvider)
extend(this.web3)
this.accounts = {}

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

@ -81,7 +81,7 @@ commander
const compVersion = commander.compiler
const baseURL = 'https://binaries.soliditylang.org/wasm/'
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
if (!compString) {
log.error(`No compiler found in releases with version ${compVersion}`)

@ -15,5 +15,6 @@
"rules": {
"no-unused-vars": "off",
"@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,
modalClass?: string,
showCancelIcon?: boolean,
hide: boolean,
hide?: boolean,
handleHide: (hideState?: boolean) => void,
children?: React.ReactNode
}

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

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

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

@ -6,7 +6,8 @@ import './toaster.css'
/* eslint-disable-next-line */
export interface ToasterProps {
message: string
timeOut?: number
timeOut?: number,
handleHide?: () => void
}
export const Toaster = (props: ToasterProps) => {
@ -59,6 +60,7 @@ export const Toaster = (props: ToasterProps) => {
if (state.timeOutId) {
clearTimeout(state.timeOutId)
}
props.handleHide && props.handleHide()
setState(prevState => {
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 { 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'
declare global {

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react' //eslint-disable-line
import { FileExplorerMenuProps } from './types'
import { FileExplorerMenuProps } from '../types'
export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
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 { FileExplorer } from '@remix-ui/file-explorer' // eslint-disable-line
import './remix-ui-workspace.css'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster'// eslint-disable-line
import { MenuItems } from 'libs/remix-ui/file-explorer/src/lib/types'
import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line
import { FileExplorer } from './components/file-explorer' // eslint-disable-line
import './css/remix-ui-workspace.css'
import { FileSystemContext } from './contexts'
/* eslint-disable-next-line */
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
}
const canUpload = window.File || window.FileReader || window.FileList || window.Blob
var canUpload = window.File || window.FileReader || window.FileList || window.Blob
export const Workspace = (props: WorkspaceProps) => {
export function Workspace () {
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
/* extends the parent 'plugin' with some function needed by the file explorer */
props.plugin.resetFocus = (reset) => {
setState(prevState => {
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}` }
}
const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE)
const global = useContext(FileSystemContext)
const workspaceRenameInput = useRef()
const workspaceCreateInput = useRef()
useEffect(() => {
let getWorkspaces = async () => {
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')
}
}
resetFocus()
}, [])
useEffect(() => {
props.localhost.event.off('disconnected', localhostDisconnect)
props.localhost.event.on('disconnected', localhostDisconnect)
props.localhost.event.on('connected', () => {
remixdExplorer.show()
setWorkspace(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 }
})
if (global.fs.mode === 'browser') {
if (global.fs.browser.currentWorkspace) setCurrentWorkspace(global.fs.browser.currentWorkspace)
else setCurrentWorkspace(NO_WORKSPACE)
global.dispatchFetchWorkspaceDirectory(global.fs.browser.currentWorkspace)
} else if (global.fs.mode === 'localhost') {
// global.dispatchFetchWorkspaceDirectory('/')
setCurrentWorkspace(LOCALHOST)
}
}, [])
}, [global.fs.browser.currentWorkspace, global.fs.localhost.sharedFolder, global.fs.mode])
const createNewWorkspace = async (workspaceName) => {
try {
await props.fileManager.closeAllFiles()
await props.createWorkspace(workspaceName)
await setWorkspace(workspaceName)
toast('New default workspace has been created.')
} catch (e) {
modalMessage('Create Default Workspace', e.message)
console.error(e)
useEffect(() => {
if (global.fs.browser.currentWorkspace && !global.fs.browser.workspaces.includes(global.fs.browser.currentWorkspace)) {
if (global.fs.browser.workspaces.length > 0) {
switchWorkspace(global.fs.browser.workspaces[global.fs.browser.workspaces.length - 1])
} else {
switchWorkspace(NO_WORKSPACE)
}
}
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 */
}, [global.fs.browser.workspaces])
const renameCurrentWorkspace = () => {
modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '', () => {})
global.modal('Rename Current Workspace', renameModalMessage(), 'OK', onFinishRenameWorkspace, '')
}
const createWorkspace = () => {
modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '', () => {})
global.modal('Create Workspace', createModalMessage(), 'OK', onFinishCreateWorkspace, '')
}
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 () => {
if (workspaceRenameInput.current === undefined) return
// @ts-ignore: Object is possibly 'null'.
const workspaceName = workspaceRenameInput.current.value
try {
await props.renameWorkspace(state.currentWorkspace, workspaceName)
setWorkspace(workspaceName)
props.workspaceRenamed({ name: workspaceName })
await global.dispatchRenameWorkspace(currentWorkspace, workspaceName)
} catch (e) {
modalMessage('Rename Workspace', e.message)
global.modal('Rename Workspace', e.message, 'OK', () => {}, '')
console.error(e)
}
}
@ -217,105 +69,40 @@ export const Workspace = (props: WorkspaceProps) => {
const workspaceName = workspaceCreateInput.current.value
try {
await props.fileManager.closeAllFiles()
await props.createWorkspace(workspaceName)
await setWorkspace(workspaceName)
await global.dispatchCreateWorkspace(workspaceName)
} catch (e) {
modalMessage('Create Workspace', e.message)
global.modal('Create Workspace', e.message, 'OK', () => {}, '')
console.error(e)
}
}
const onFinishDeleteWorkspace = async () => {
await props.fileManager.closeAllFiles()
const workspacesPath = props.workspace.workspacesPath
props.browser.remove(workspacesPath + '/' + state.currentWorkspace)
const name = state.currentWorkspace
setWorkspace(NO_WORKSPACE)
props.workspaceDeleted({ name })
}
/** ** ****/
const resetFocus = (reset) => {
setState(prevState => {
return { ...prevState, reset }
})
}
const setWorkspace = async (name) => {
await props.fileManager.closeAllFiles()
if (name === LOCALHOST) {
props.workspace.clearWorkspace()
} else if (name === NO_WORKSPACE) {
props.workspace.clearWorkspace()
} else {
await props.workspace.setWorkspace(name)
try {
await global.dispatchDeleteWorkspace(global.fs.browser.currentWorkspace)
} catch (e) {
global.modal('Delete Workspace', e.message, 'OK', () => {}, '')
console.error(e)
}
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 resetFocus = () => {
global.dispatchSetFocusElement([{ key: '', type: 'folder' }])
}
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 switchWorkspace = async (name: string) => {
try {
await global.dispatchSwitchToWorkspace(name)
global.dispatchHandleExpandPath([])
} catch (e) {
global.modal('Switch To Workspace', e.message, 'OK', () => {}, '')
console.error(e)
}
})
}
const createModalMessage = () => {
return (
<>
<span>{ state.modal.message }</span>
<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 = () => {
return (
<>
<span>{ state.modal.message }</span>
<input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ state.currentWorkspace } ref={workspaceRenameInput} className="form-control" />
<input type="text" data-id="modalDialogCustomPromptTextRename" defaultValue={ currentWorkspace } ref={workspaceRenameInput} className="form-control" />
</>
)
}
return (
<div className='remixui_container'>
{ state.modal.message && <ModalDialog
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 className='remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div>
<header>
<div className="mb-2">
@ -365,7 +137,7 @@ export const Workspace = (props: WorkspaceProps) => {
title='Create'>
</span>
<span
hidden={state.currentWorkspace === LOCALHOST || state.currentWorkspace === NO_WORKSPACE}
hidden={currentWorkspace === LOCALHOST || currentWorkspace === NO_WORKSPACE}
id='workspaceRename'
data-id='workspaceRename'
onClick={(e) => {
@ -376,7 +148,7 @@ export const Workspace = (props: WorkspaceProps) => {
title='Rename'>
</span>
<span
hidden={state.currentWorkspace === LOCALHOST || state.currentWorkspace === NO_WORKSPACE}
hidden={currentWorkspace === LOCALHOST || currentWorkspace === NO_WORKSPACE}
id='workspaceDelete'
data-id='workspaceDelete'
onClick={(e) => {
@ -387,15 +159,15 @@ export const Workspace = (props: WorkspaceProps) => {
title='Delete'>
</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) => {
return <option key={index} value={folder}>{folder}</option>
})
}
<option value={LOCALHOST}>{state.currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option>
{ state.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> }
<option value={LOCALHOST}>{currentWorkspace === LOCALHOST ? 'localhost' : LOCALHOST}</option>
{ global.fs.browser.workspaces.length <= 0 && <option value={NO_WORKSPACE}>{NO_WORKSPACE}</option> }
</select>
</div>
</header>
@ -403,34 +175,70 @@ export const Workspace = (props: WorkspaceProps) => {
<div className='remixui_fileExplorerTree'>
<div>
<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
name={state.currentWorkspace}
registry={props.registry}
filesProvider={props.workspace}
name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
plugin={props.plugin}
focusRoot={state.reset}
contextMenuItems={props.registeredMenuItems}
removedContextMenuItems={props.removedMenuItems}
displayInput={state.displayNewFile}
externalUploads={state.uploadFileEvent}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files}
expandPath={global.fs.browser.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>
{
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'>
{ !state.hideRemixdExplorer &&
{ global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost &&
<FileExplorer
name='localhost'
registry={props.registry}
filesProvider={props.localhost}
menuItems={['createNewFile', 'createNewFolder']}
plugin={props.plugin}
focusRoot={state.reset}
contextMenuItems={props.registeredMenuItems}
removedContextMenuItems={props.removedMenuItems}
contextMenuItems={global.fs.localhost.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.localhost.contextMenu.removedMenuItems}
files={global.fs.localhost.files}
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>

@ -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'
try {
const { data } = await Axios.get(gistUrl)
const { data } = (await Axios.get(gistUrl)) as { data: any }
try {
await writeJSON(path.resolve(path.join(__dirname, '..', 'origins.json')), { data })

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

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

6101
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -45,7 +45,7 @@
"workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph",
"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",
"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",
@ -141,6 +141,7 @@
"@ethereumjs/common": "^2.5.0",
"@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.3",
"@monaco-editor/react": "^4.3.1",
"@remixproject/engine": "next",
"@remixproject/engine-web": "next",
"@remixproject/plugin": "next",
@ -224,6 +225,7 @@
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.3.0",
"@types/request": "^2.48.7",
"@types/tape": "^4.13.0",
"@types/ws": "^7.2.4",
"@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-url-resolver": ["dist/libs/remix-url-resolver/src/index.js"],
"@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-ui/tree-view": ["libs/remix-ui/tree-view/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/file-explorer": ["libs/remix-ui/file-explorer/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/settings": ["libs/remix-ui/settings/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/publish-to-storage": ["libs/remix-ui/publish-to-storage/src/index.ts"],
"@remix-ui/solidity-compiler": [
"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/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"]

@ -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": {
"root": "apps/debugger",
"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": {

Loading…
Cancel
Save