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

pull/5370/head
Joseph Izang 3 years ago
commit b9c5eea1e2
  1. 1
      README.md
  2. 70
      apps/remix-ide-e2e/src/tests/editor.test.ts
  3. 4
      apps/remix-ide-e2e/src/tests/fileExplorer.test.ts
  4. 66
      apps/remix-ide-e2e/src/tests/importFromGithub.test.ts
  5. 5
      apps/remix-ide/src/app.js
  6. 194
      apps/remix-ide/src/app/editor/contextView.js
  7. 8
      apps/remix-ide/src/app/editor/editor.js
  8. 18
      apps/remix-ide/src/app/panels/main-view.js
  9. 571
      apps/remix-ide/src/app/ui/landing-page/landing-page.js
  10. BIN
      apps/remix-ide/src/assets/img/solhintLogo.png
  11. BIN
      apps/remix-ide/src/assets/img/solhintLogo.webp
  12. 1
      libs/remix-core-plugin/src/index.ts
  13. 6
      libs/remix-core-plugin/src/lib/compiler-artefacts.ts
  14. 100
      libs/remix-core-plugin/src/lib/editor-context-listener.ts
  15. 4
      libs/remix-ui/editor-context-view/.babelrc
  16. 19
      libs/remix-ui/editor-context-view/.eslintrc
  17. 7
      libs/remix-ui/editor-context-view/README.md
  18. 1
      libs/remix-ui/editor-context-view/src/index.ts
  19. 20
      libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.css
  20. 191
      libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.tsx
  21. 16
      libs/remix-ui/editor-context-view/tsconfig.json
  22. 13
      libs/remix-ui/editor-context-view/tsconfig.lib.json
  23. 133
      libs/remix-ui/editor/src/lib/cairoSyntax.ts
  24. 5
      libs/remix-ui/editor/src/lib/remix-ui-editor.css
  25. 57
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  26. 3
      libs/remix-ui/helper/src/lib/remix-ui-helper.ts
  27. 4
      libs/remix-ui/home-tab/.babelrc
  28. 248
      libs/remix-ui/home-tab/.eslintrc
  29. 7
      libs/remix-ui/home-tab/README.md
  30. 1
      libs/remix-ui/home-tab/src/index.ts
  31. 27
      libs/remix-ui/home-tab/src/lib/components/pluginButton.tsx
  32. 82
      libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.css
  33. 370
      libs/remix-ui/home-tab/src/lib/remix-ui-home-tab.tsx
  34. 16
      libs/remix-ui/home-tab/src/lib/themeContext.tsx
  35. 16
      libs/remix-ui/home-tab/tsconfig.json
  36. 13
      libs/remix-ui/home-tab/tsconfig.lib.json
  37. 4
      libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx
  38. 2
      libs/remix-ui/modal-dialog/src/lib/types/index.ts
  39. 1
      libs/remix-ui/plugin-manager/src/lib/components/permissionsSettings.tsx
  40. 77
      libs/remix-ui/terminal/src/lib/custom-hooks/useDragTerminal.tsx
  41. 5
      libs/remix-ui/terminal/src/lib/remix-ui-terminal.css
  42. 93
      libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx
  43. 1
      libs/remix-ui/toaster/src/lib/toaster.tsx
  44. 6
      nx.json
  45. 4242
      package-lock.json
  46. 1
      package.json
  47. 8
      tsconfig.base.json
  48. 117
      workspace.json

@ -51,6 +51,7 @@ git clone https://github.com/ethereum/remix-project.git
```bash
cd remix-project
npm install
npm run build:libs // Build remix libs
nx build
nx serve
```

@ -6,10 +6,10 @@ import init from '../helpers/init'
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done)
init(browser, done, 'http://127.0.0.1:8080', true)
},
'Should zoom in editor ': function (browser: NightwatchBrowser) {
'Should zoom in editor #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('div[data-id="mainPanelPluginsContainer"]')
.clickLaunchIcon('filePanel')
.waitForElementVisible('div[data-id="filePanelFileExplorerTree"]')
@ -22,7 +22,7 @@ module.exports = {
.checkElementStyle('.view-lines', 'font-size', '16px')
},
'Should zoom out editor ': function (browser: NightwatchBrowser) {
'Should zoom out editor #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.checkElementStyle('.view-lines', 'font-size', '16px')
.click('*[data-id="tabProxyZoomOut"]')
@ -30,7 +30,7 @@ module.exports = {
.checkElementStyle('.view-lines', 'font-size', '14px')
},
'Should display compile error in editor ': function (browser: NightwatchBrowser) {
'Should display compile error in editor #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.setEditorValue(storageContractWithError + 'error')
.pause(2000)
@ -42,7 +42,7 @@ module.exports = {
.checkAnnotations('fa-exclamation-square', 29) // error
},
'Should minimize and maximize codeblock in editor ': '' + function (browser: NightwatchBrowser) {
'Should minimize and maximize codeblock in editor #group1': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.waitForElementVisible('.ace_open')
.click('.ace_start:nth-of-type(1)')
@ -51,7 +51,7 @@ module.exports = {
.waitForElementVisible('.ace_open')
},
'Should add breakpoint to editor ': function (browser: NightwatchBrowser) {
'Should add breakpoint to editor #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.waitForElementNotPresent('.margin-view-overlays .fa-circle')
.execute(() => {
@ -60,7 +60,7 @@ module.exports = {
.waitForElementVisible('.margin-view-overlays .fa-circle')
},
'Should load syntax highlighter for ace light theme': '' + function (browser: NightwatchBrowser) {
'Should load syntax highlighter for ace light theme #group1': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('#editorView')
.checkElementStyle('.ace_keyword', 'color', aceThemes.light.keyword)
.checkElementStyle('.ace_comment.ace_doc', 'color', aceThemes.light.comment)
@ -68,7 +68,7 @@ module.exports = {
.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 #group1': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('*[data-id="verticalIconsKindsettings"]')
.click('*[data-id="verticalIconsKindsettings"]')
.waitForElementVisible('*[data-id="settingsTabThemeLabelDark"]')
@ -83,7 +83,7 @@ module.exports = {
*/
},
'Should highlight source code ': function (browser: NightwatchBrowser) {
'Should highlight source code #group1': function (browser: NightwatchBrowser) {
// include all files here because switching between plugins in side-panel removes highlight
browser
.addFile('sourcehighlight.js', sourcehighlightScript)
@ -101,7 +101,7 @@ module.exports = {
.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 #group1': '' + function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItemremoveSourcehighlightScript.js"]')
.pause(2000)
@ -115,7 +115,7 @@ module.exports = {
.checkElementStyle('.highlightLine51', 'background-color', 'rgb(52, 152, 219)')
},
'Should remove all highlights from source code ': function (browser: NightwatchBrowser) {
'Should remove all highlights from source code #group1': function (browser: NightwatchBrowser) {
browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]')
.click('li[data-id="treeViewLitreeViewItemremoveAllSourcehighlightScript.js"]')
.pause(2000)
@ -126,6 +126,54 @@ module.exports = {
.waitForElementNotPresent('.highlightLine33', 60000)
.waitForElementNotPresent('.highlightLine41', 60000)
.waitForElementNotPresent('.highlightLine51', 60000)
},
'Should display the context view #group2': function (browser: NightwatchBrowser) {
browser
.openFile('contracts')
.openFile('contracts/1_Storage.sol')
.waitForElementVisible('#editorView')
.setEditorValue(storageContractWithError)
.pause(2000)
.execute(() => {
(document.getElementById('editorView') as any).gotoLine(17, 16)
}, [], () => {})
.waitForElementVisible('.contextview')
.waitForElementContainsText('.contextview .type', 'FunctionDefinition')
.waitForElementContainsText('.contextview .name', 'store')
.execute(() => {
(document.getElementById('editorView') as any).gotoLine(18, 12)
}, [], () => {})
.waitForElementContainsText('.contextview .type', 'uint256')
.waitForElementContainsText('.contextview .name', 'number')
.click('.contextview [data-action="previous"]') // declaration
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '180')
})
.click('.contextview [data-action="next"]') // back to the initial state
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '323')
})
.click('.contextview [data-action="next"]') // next reference
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '489')
})
.click('.contextview [data-action="gotoref"]') // back to the declaration
.execute(() => {
return (document.getElementById('editorView') as any).getCursorPosition()
}, [], (result) => {
console.log('result', result)
browser.assert.equal(result.value, '180')
})
.end()
}
}

@ -4,7 +4,7 @@ import init from '../helpers/init'
import * as path from 'path'
const testData = {
testFile1: path.resolve(__dirname + '/editor.spec.js'), // eslint-disable-line
testFile1: path.resolve(__dirname + '/editor.test.js'), // eslint-disable-line
testFile2: path.resolve(__dirname + '/fileExplorer.test.js'), // eslint-disable-line
testFile3: path.resolve(__dirname + '/generalSettings.test.js') // eslint-disable-line
}
@ -105,7 +105,7 @@ module.exports = {
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile1)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile2)
.setValue('*[data-id="fileExplorerFileUpload"]', testData.testFile3)
.waitForElementVisible('[data-id="treeViewLitreeViewItemeditor.spec.js"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemeditor.test.js"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemfileExplorer.test.js"]')
.waitForElementVisible('[data-id="treeViewLitreeViewItemgeneralSettings.test.js"]')
.end()

@ -0,0 +1,66 @@
'use strict'
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'
const testData = {
validURL: 'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol',
invalidURL: 'https://github.com/Oppelin/Roles.sol'
}
module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done)
},
'Import from GitHub Modal': function (browser: NightwatchBrowser) {
browser.clickLaunchIcon('home')
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('filePanel')
.click('div[title="home"]')
.waitForElementVisible('button[data-id="landingPageImportFromGitHubButton"]')
.pause(1000)
.scrollAndClick('button[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('*[data-id="homeTabModalDialogModalTitle-react"]')
.assert.containsText('*[data-id="homeTabModalDialogModalTitle-react"]', 'Import from Github')
.waitForElementVisible('*[data-id="homeTabModalDialogModalBody-react"]')
.assert.containsText('*[data-id="homeTabModalDialogModalBody-react"]', 'Enter the github URL you would like to load.')
.waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]')
.refresh()
},
'Display Error Message For Invalid GitHub URL Modal': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('settings')
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]')
.execute(() => {
(document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus()
}, [], () => {})
.setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.invalidURL)
.waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]')
.scrollAndClick('[data-id="homeTab-modal-footer-ok-react"]') // submitted
.waitForElementVisible('*[data-shared="tooltipPopup"]')
.assert.containsText('*[data-shared="tooltipPopup"] span', 'not found ' + testData.invalidURL)
},
'Import From Github For Valid URL': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000)
.clickLaunchIcon('settings')
.clickLaunchIcon('filePanel')
.scrollAndClick('*[data-id="landingPageImportFromGitHubButton"]')
.waitForElementVisible('*[data-id="homeTabModalDialogCustomPromptText"]')
.clearValue('*[data-id="homeTabModalDialogCustomPromptText"]')
.execute(() => {
(document.querySelector('input[data-id="homeTabModalDialogCustomPromptText"]') as any).focus()
}, [], () => {})
.setValue('input[data-id="homeTabModalDialogCustomPromptText"]', testData.validURL)
.waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]')
.scrollAndClick('[data-id="homeTab-modal-footer-ok-react"]')
.openFile('github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol')
.waitForElementVisible("div[title='default_workspace/github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol'")
.end()
}
}

@ -15,7 +15,7 @@ import { FramingService } from './framingService'
import { WalkthroughService } from './walkthroughService'
import { OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports } from '@remix-project/core-plugin'
import { OffsetToLineColumnConverter, CompilerMetadata, CompilerArtefacts, FetchAndCompile, CompilerImports, EditorContextListener } from '@remix-project/core-plugin'
import migrateFileSystem from './migrateFileSystem'
@ -48,7 +48,6 @@ const TestTab = require('./app/tabs/test-tab')
const FilePanel = require('./app/panels/file-panel')
const Editor = require('./app/editor/editor')
const Terminal = require('./app/panels/terminal')
const ContextualListener = require('./app/editor/contextualListener')
class AppComponent {
constructor (api = {}, events = {}, opts = {}) {
@ -156,7 +155,7 @@ class AppComponent {
}
}
)
const contextualListener = new ContextualListener({ editor })
const contextualListener = new EditorContextListener()
self.engine.register([
blockchain,

@ -1,194 +0,0 @@
'use strict'
import { sourceMappingDecoder } from '@remix-project/remix-debug'
const yo = require('yo-yo')
const globalRegistry = require('../../global/registry')
const css = require('./styles/contextView-styles')
/*
Display information about the current focused code:
- if it's a reference, display information about the declaration
- jump to the declaration
- number of references
- rename declaration/references
*/
class ContextView {
constructor (opts, localRegistry) {
this._components = {}
this._components.registry = localRegistry || globalRegistry
this.contextualListener = opts.contextualListener
this.editor = opts.editor
this._deps = {
compilersArtefacts: this._components.registry.get('compilersartefacts').api,
offsetToLineColumnConverter: this._components.registry.get('offsettolinecolumnconverter').api,
config: this._components.registry.get('config').api,
fileManager: this._components.registry.get('filemanager').api
}
this._view = null
this._nodes = null
this._current = null
this.sourceMappingDecoder = sourceMappingDecoder
this.previousElement = null
this.contextualListener.event.register('contextChanged', nodes => {
this.show()
this._nodes = nodes
this.update()
})
this.contextualListener.event.register('stopHighlighting', () => {
})
}
render () {
const view = yo`
<div class="${css.contextview} ${css.contextviewcontainer} bg-light text-dark border-0">
<div class=${css.container}>
${this._renderTarget()}
</div>
</div>`
if (!this._view) {
this._view = view
}
return view
}
hide () {
if (this._view) {
this._view.style.display = 'none'
}
}
show () {
if (this._view) {
this._view.style.display = 'block'
}
}
update () {
if (this._view) {
yo.update(this._view, this.render())
}
}
_renderTarget () {
let last
const previous = this._current
if (this._nodes && this._nodes.length) {
last = this._nodes[this._nodes.length - 1]
if (isDefinition(last)) {
this._current = last
} else {
const target = this.contextualListener.declarationOf(last)
if (target) {
this._current = target
} else {
this._current = null
}
}
}
if (!this._current || !previous || previous.id !== this._current.id || (this.previousElement && !this.previousElement.children.length)) {
this.previousElement = this._render(this._current, last)
}
return this.previousElement
}
_jumpToInternal (position) {
const jumpToLine = (lineColumn) => {
if (lineColumn.start && lineColumn.start.line && lineColumn.start.column) {
this.editor.gotoLine(lineColumn.start.line, lineColumn.end.column + 1)
}
}
const lastCompilationResult = this._deps.compilersArtefacts.__last
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0 && lastCompilationResult.data) {
const lineColumn = this._deps.offsetToLineColumnConverter.offsetToLineColumn(
position,
position.file,
lastCompilationResult.getSourceCode().sources,
lastCompilationResult.getAsts())
const filename = lastCompilationResult.getSourceName(position.file)
// TODO: refactor with rendererAPI.errorClick
if (filename !== this._deps.config.get('currentFile')) {
const provider = this._deps.fileManager.fileProviderOf(filename)
if (provider) {
provider.exists(filename).then(exist => {
this._deps.fileManager.open(filename)
jumpToLine(lineColumn)
}).catch(error => {
if (error) return console.log(error)
})
}
} else {
jumpToLine(lineColumn)
}
}
}
_render (node, nodeAtCursorPosition) {
if (!node) return yo`<div></div>`
let references = this.contextualListener.referencesOf(node)
const type = node.typeDescriptions && node.typeDescriptions.typeString ? node.typeDescriptions.typeString : node.nodeType
references = `${references ? references.length : '0'} reference(s)`
let ref = 0
const nodes = this.contextualListener.getActiveHighlights()
for (const k in nodes) {
if (nodeAtCursorPosition.id === nodes[k].nodeId) {
ref = k
break
}
}
// JUMP BETWEEN REFERENCES
const jump = (e) => {
e.target.dataset.action === 'next' ? ref++ : ref--
if (ref < 0) ref = nodes.length - 1
if (ref >= nodes.length) ref = 0
this._jumpToInternal(nodes[ref].position)
}
const jumpTo = () => {
if (node && node.src) {
const position = this.sourceMappingDecoder.decode(node.src)
if (position) {
this._jumpToInternal(position)
}
}
}
const showGasEstimation = () => {
if (node.nodeType === 'FunctionDefinition') {
const result = this.contextualListener.gasEstimation(node)
const executionCost = ' Execution cost: ' + result.executionCost + ' gas'
const codeDepositCost = 'Code deposit cost: ' + result.codeDepositCost + ' gas'
const estimatedGas = result.codeDepositCost ? `${codeDepositCost}, ${executionCost}` : `${executionCost}`
return yo`
<div class=${css.gasEstimation}>
<i class="fas fa-gas-pump ${css.gasStationIcon}" title='Gas estimation'></i>
<span>${estimatedGas}</span>
</div>
`
}
}
return yo`
<div class=${css.line}>${showGasEstimation()}
<div title=${type} class=${css.type}>${type}</div>
<div title=${node.name} class=${css.name}>${node.name}</div>
<i class="fas fa-share ${css.jump}" aria-hidden="true" onclick=${jumpTo}></i>
<span class=${css.referencesnb}>${references}</span>
<i data-action='previous' class="fas fa-chevron-up ${css.jump}" aria-hidden="true" onclick=${jump}></i>
<i data-action='next' class="fas fa-chevron-down ${css.jump}" aria-hidden="true" onclick=${jump}></i>
</div>
`
}
}
function isDefinition (node) {
return node.nodeType === 'ContractDefinition' ||
node.nodeType === 'FunctionDefinition' ||
node.nodeType === 'ModifierDefinition' ||
node.nodeType === 'VariableDeclaration' ||
node.nodeType === 'StructDefinition' ||
node.nodeType === 'EventDefinition'
}
module.exports = ContextView

@ -12,7 +12,7 @@ const profile = {
name: 'editor',
description: 'service - editor',
version: packageJson.version,
methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine']
methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine', 'getCursorPosition']
}
class Editor extends Plugin {
@ -46,7 +46,8 @@ class Editor extends Plugin {
txt: 'text',
json: 'json',
abi: 'json',
rs: 'rust'
rs: 'rust',
cairo: 'cairo'
}
this.activated = false
@ -74,7 +75,8 @@ class Editor extends Plugin {
this._onChange(this.currentFile)
}
}
this.el.gotoLine = (line) => this.gotoLine(line, 0)
this.el.gotoLine = (line, column) => this.gotoLine(line, column || 0)
this.el.getCursorPosition = () => this.getCursorPosition()
return this.el
}

@ -4,8 +4,6 @@ var EventManager = require('../../lib/events')
var globalRegistry = require('../../global/registry')
var { TabProxy } = require('./tab-proxy.js')
var ContextView = require('../editor/contextView')
var csjs = require('csjs-inject')
var css = csjs`
@ -14,7 +12,7 @@ var css = csjs`
flex-direction : column;
height : 100%;
width : 100%;
}
}
`
// @todo(#650) Extract this into two classes: MainPanel (TabsProxy + Iframe/Editor) & BottomPanel (Terminal)
@ -25,12 +23,12 @@ export class MainView {
self._view = {}
self._components = {}
self._components.registry = globalRegistry
self.contextualListener = contextualListener
self.editor = editor
self.fileManager = fileManager
self.mainPanel = mainPanel
self.txListener = globalRegistry.get('txlistener').api
self._components.terminal = terminal
self._components.contextualListener = contextualListener
this.appManager = appManager
this.init()
}
@ -39,7 +37,6 @@ export class MainView {
this.fileManager.unselectCurrentFile()
this.mainPanel.showContent(name)
this._view.editor.style.display = 'none'
this._components.contextView.hide()
this._view.mainPanel.style.display = 'block'
}
@ -63,19 +60,16 @@ export class MainView {
// we check upstream for "fileChanged"
self._view.editor.style.display = 'block'
self._view.mainPanel.style.display = 'none'
self._components.contextView.show()
})
self.tabProxy.event.on('openFile', (file) => {
self._view.editor.style.display = 'block'
self._view.mainPanel.style.display = 'none'
self._components.contextView.show()
})
self.tabProxy.event.on('closeFile', (file) => {
})
self.tabProxy.event.on('switchApp', self.showApp.bind(self))
self.tabProxy.event.on('closeApp', (name) => {
self._view.editor.style.display = 'block'
self._components.contextView.show()
self._view.mainPanel.style.display = 'none'
})
self.tabProxy.event.on('tabCountChanged', (count) => {
@ -90,10 +84,6 @@ export class MainView {
}
}
const contextView = new ContextView({ contextualListener: self._components.contextualListener, editor: self.editor })
self._components.contextView = contextView
self._components.terminal.event.register('resize', delta => self._adjustLayout('top', delta))
if (self.txListener) {
self._components.terminal.event.register('listenOnNetWork', (listenOnNetWork) => {
@ -181,15 +171,17 @@ export class MainView {
self._view.editor.style.display = 'none'
self._view.mainPanel = self.mainPanel.render()
self._view.terminal = self._components.terminal.render()
self._view.mainview = yo`
<div class=${css.mainview}>
${self.tabProxy.renderTabsbar()}
${self._view.editor}
${self._view.mainPanel}
${self._components.contextView.render()}
<div class="${css.contextview} contextview"></div>
${self._view.terminal}
</div>
`
// INIT
self._adjustLayout('top', self.data._layout.top.offset)

@ -1,110 +1,11 @@
/* global */
import React from 'react' // eslint-disable-line
import ReactDOM from 'react-dom'
import * as packageJson from '../../../../../../package.json'
import { ViewPlugin } from '@remixproject/engine-web'
import { migrateToWorkspace } from '../../../migrateFileSystem'
import JSZip from 'jszip'
import { RemixUiHomeTab } from '@remix-ui/home-tab' // eslint-disable-line
const yo = require('yo-yo')
const csjs = require('csjs-inject')
const globalRegistry = require('../../../global/registry')
const modalDialogCustom = require('../modal-dialog-custom')
const modalDialog = require('../modaldialog')
const tooltip = require('../tooltip')
const GistHandler = require('../../../lib/gist-handler')
const QueryParams = require('../../../lib/query-params.js')
const _paq = window._paq = window._paq || []
const css = csjs`
.text {
cursor: pointer;
font-weight: normal;
max-width: 300px;
user-select: none;
}
.text:hover {
cursor: pointer;
text-decoration: underline;
}
.homeContainer {
user-select: none;
overflow-y: hidden;
}
.mainContent {
overflow-y: auto;
flex-grow: 3;
}
.hpLogoContainer {
margin: 30px;
padding-right: 90px;
}
.mediaBadge {
font-size: 2em;
height: 2em;
width: 2em;
}
.mediaBadge:focus {
outline: none;
}
.image {
height: 1em;
width: 1em;
text-align: center;
}
.logoImg {
height: 10em;
}
.hpSections {
}
.rightPanel {
right: 0;
position: absolute;
z-index: 3;
}
.remixHomeMedia {
overflow-y: auto;
overflow-x: hidden;
max-height: 720px;
}
.panels {
box-shadow: 0px 0px 13px -7px;
}
.labelIt {
margin-bottom: 0;
}
.bigLabelSize {
font-size: 13px;
}
.seeAll {
margin-top: 7px;
white-space: nowrap;
}
.importFrom p {
margin-right: 10px;
}
.logoContainer img{
height: 150px;
opacity: 0.7;
}
.envLogo {
height: 16px;
}
.cursorStyle {
cursor: pointer;
}
.envButton {
width: 120px;
height: 70px;
}
.media {
overflow: hidden;
width: 400px;
transition: .5s ease-out;
z-index: 1000;
}
.migrationBtn {
width: 100px;
}
}
`
const profile = {
name: 'home',
@ -116,7 +17,6 @@ const profile = {
location: 'mainPanel',
version: packageJson.version
}
export class LandingPage extends ViewPlugin {
constructor (appManager, verticalIcons, fileManager, filePanel, contentImport) {
super(profile)
@ -127,459 +27,22 @@ export class LandingPage extends ViewPlugin {
this.appManager = appManager
this.verticalIcons = verticalIcons
this.gistHandler = new GistHandler()
const themeQuality = globalRegistry.get('themeModule').api.currentTheme().quality
window.addEventListener('resize', () => this.adjustMediaPanel())
window.addEventListener('click', (e) => this.hideMediaPanel(e))
this.twitterFrame = yo`
<div class="px-2 ${css.media}">
<a class="twitter-timeline"
data-width="350"
data-theme="${themeQuality}"
data-chrome="nofooter noheader transparent"
data-tweet-limit="8"
href="https://twitter.com/EthereumRemix"
>
</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</div>
`
this.badgeTwitter = yo`<button
class="btn-info p-2 m-1 border rounded-circle ${css.mediaBadge} fab fa-twitter"
id="remixIDEHomeTwitterbtn"
onclick=${(e) => this.showMediaPanel(e)}
></button>`
this.badgeMedium = yo`<button
class="btn-danger p-2 m-1 border rounded-circle ${css.mediaBadge} fab fa-medium"
id="remixIDEHomeMediumbtn"
onclick=${(e) => this.showMediaPanel(e)}
></button>`
this.twitterPanel = yo`
<div id="remixIDE_TwitterBlock" class="p-2 mx-0 mb-0 d-none ${css.remixHomeMedia}">
${this.twitterFrame}
</div>
`
this.mediumPanel = yo`
<div id="remixIDE_MediumBlock" class="p-2 mx-0 mb-0 d-none ${css.remixHomeMedia}">
<div id="medium-widget" class="p-3 ${css.media}">
<div
id="retainable-rss-embed"
data-rss="https://medium.com/feed/remix-ide"
data-maxcols="1"
data-layout="grid"
data-poststyle="external"
data-readmore="More..."
data-buttonclass="btn mb-3"
data-offset="-100"
>
- </div>
</div>
</div>
`
this.adjustMediaPanel()
globalRegistry.get('themeModule').api.events.on('themeChanged', (theme) => {
this.onThemeChanged(theme.quality)
})
}
adjustMediaPanel () {
this.twitterPanel.style.maxHeight = Math.max(window.innerHeight - 150, 200) + 'px'
this.mediumPanel.style.maxHeight = Math.max(window.innerHeight - 150, 200) + 'px'
}
hideMediaPanel (e) {
const mediaPanelsTitle = document.getElementById('remixIDEMediaPanelsTitle')
const mediaPanels = document.getElementById('remixIDEMediaPanels')
if (!mediaPanelsTitle || !mediaPanels) return
if (!mediaPanelsTitle.contains(e.target) && !mediaPanels.contains(e.target)) {
this.mediumPanel.classList.remove('d-block')
this.mediumPanel.classList.add('d-none')
this.twitterPanel.classList.remove('d-block')
this.twitterPanel.classList.add('d-none')
}
}
onThemeChanged (themeQuality) {
const twitterFrame = yo`
<div class="px-2 ${css.media}">
<a class="twitter-timeline"
data-width="350"
data-theme="${themeQuality}"
data-chrome="nofooter noheader transparent"
data-tweet-limit="8"
href="https://twitter.com/EthereumRemix"
>
</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</div>
`
yo.update(this.twitterFrame, twitterFrame)
const invertNum = (themeQuality === 'dark') ? 1 : 0
if (this.solEnv.getElementsByTagName('img')[0]) this.solEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
if (this.optimismEnv.getElementsByTagName('img')[0]) this.optimismEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
if (this.solhintEnv.getElementsByTagName('img')[0]) this.solhintEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
if (this.learnEthEnv.getElementsByTagName('img')[0]) this.learnEthEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
if (this.sourcifyEnv.getElementsByTagName('img')[0]) this.sourcifyEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
if (this.moreEnv.getElementsByTagName('img')[0]) this.moreEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
if (this.websiteIcon) this.websiteIcon.style.filter = `invert(${invertNum})`
}
showMediaPanel (e) {
if (e.target.id === 'remixIDEHomeTwitterbtn') {
this.mediumPanel.classList.remove('d-block')
this.mediumPanel.classList.add('d-none')
this.twitterPanel.classList.toggle('d-block')
_paq.push(['trackEvent', 'pluginManager', 'media', 'twitter'])
} else {
this.twitterPanel.classList.remove('d-block')
this.twitterPanel.classList.add('d-none')
this.mediumPanel.classList.toggle('d-block')
_paq.push(['trackEvent', 'pluginManager', 'media', 'medium'])
}
this.el = document.createElement('div')
this.el.setAttribute('id', 'landingPageHomeContainer')
this.el.setAttribute('class', 'remixui_homeContainer justify-content-between bg-light d-flex')
this.el.setAttribute('data-id', 'landingPageHomeContainer')
}
render () {
const load = (service, item, examples, info) => {
const contentImport = this.contentImport
const fileProviders = globalRegistry.get('fileproviders').api
const msg = yo`
<div class="p-2">
<span>Enter the ${item} you would like to load.</span>
<div>${info}</div>
<div>e.g ${examples.map((url) => { return yo`<div class="p-1"><a>${url}</a></div>` })}</div>
</div>`
const title = `Import from ${service}`
modalDialogCustom.prompt(title, msg, null, (target) => {
if (target !== '') {
contentImport.import(
target,
(loadingMsg) => { tooltip(loadingMsg) },
(error, content, cleanUrl, type, url) => {
if (error) {
modalDialogCustom.alert(title, error.message || error)
} else {
try {
fileProviders.workspace.addExternal(type + '/' + cleanUrl, content, url)
this.verticalIcons.select('filePanel')
} catch (e) {
modalDialogCustom.alert(title, e.message)
}
}
}
)
}
})
}
const startSolidity = async () => {
await this.appManager.activatePlugin(['solidity', 'udapp', 'solidityStaticAnalysis', 'solidityUnitTesting'])
this.verticalIcons.select('solidity')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'solidity'])
}
const startOptimism = async () => {
await this.appManager.activatePlugin('optimism-compiler')
this.verticalIcons.select('optimism-compiler')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'optimism-compiler'])
}
const startSolhint = async () => {
await this.appManager.activatePlugin(['solidity', 'solhint'])
this.verticalIcons.select('solhint')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'solhint'])
}
const startLearnEth = async () => {
await this.appManager.activatePlugin(['solidity', 'LearnEth', 'solidityUnitTesting'])
this.verticalIcons.select('LearnEth')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'learnEth'])
}
const startSourceVerify = async () => {
await this.appManager.activatePlugin(['solidity', 'source-verification'])
this.verticalIcons.select('source-verification')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'source-verification'])
}
const startPluginManager = async () => {
await this.appManager.activatePlugin('pluginManager')
this.verticalIcons.select('pluginManager')
}
const startRestoreBackupZip = async () => {
await this.appManager.activatePlugin(['restorebackupzip'])
this.verticalIcons.select('restorebackupzip')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'restorebackupzip'])
}
const createNewFile = () => {
this.call('filePanel', 'createNewFile')
}
const saveAs = (blob, name) => {
const node = document.createElement('a')
node.download = name
node.rel = 'noopener'
node.href = URL.createObjectURL(blob)
setTimeout(function () { URL.revokeObjectURL(node.href) }, 4E4) // 40s
setTimeout(function () {
try {
node.dispatchEvent(new MouseEvent('click'))
} catch (e) {
var evt = document.createEvent('MouseEvents')
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
20, false, false, false, false, 0, null)
node.dispatchEvent(evt)
}
}, 0) // 40s
}
const downloadFiles = async () => {
try {
tooltip('preparing files for download, please wait..')
const fileProviders = globalRegistry.get('fileproviders').api
const zip = new JSZip()
await fileProviders.browser.copyFolderToJson('/', ({ path, content }) => {
zip.file(`remixbackup${path}`, content)
})
zip.generateAsync({ type: 'blob' }).then(function (blob) {
saveAs(blob, 'remixbackup.zip')
}).catch((e) => {
tooltip(e.message)
})
} catch (e) {
tooltip(e.message)
}
}
const uploadFile = (target) => {
this.call('filePanel', 'uploadFile', target)
}
const connectToLocalhost = () => {
this.appManager.activatePlugin('remixd')
}
const importFromGist = () => {
this.gistHandler.loadFromGist({ gist: '' }, globalRegistry.get('filemanager').api)
this.verticalIcons.select('filePanel')
}
globalRegistry.get('themeModule').api.events.on('themeChanged', (theme) => {
globalRegistry.get('themeModule').api.fixInvert(document.getElementById('remixLogo'))
globalRegistry.get('themeModule').api.fixInvert(document.getElementById('solidityLogo'))
globalRegistry.get('themeModule').api.fixInvert(document.getElementById('debuggerLogo'))
globalRegistry.get('themeModule').api.fixInvert(document.getElementById('learnEthLogo'))
globalRegistry.get('themeModule').api.fixInvert(document.getElementById('workshopLogo'))
globalRegistry.get('themeModule').api.fixInvert(document.getElementById('moreLogo'))
globalRegistry.get('themeModule').api.fixInvert(document.getElementById('solhintLogo'))
})
const createLargeButton = (imgPath, envID, envText, callback) => {
return yo`
<button
class="btn border-secondary d-flex mr-3 text-nowrap justify-content-center flex-column align-items-center ${css.envButton}"
data-id="landingPageStartSolidity"
onclick=${() => callback()}
>
<img class="m-2 align-self-center ${css.envLogo}" id=${envID} src="${imgPath}">
<label class="text-uppercase text-dark ${css.cursorStyle}">${envText}</label>
</button>
`
}
// main
this.solEnv = createLargeButton('assets/img/solidityLogo.webp', 'solidityLogo', 'Solidity', startSolidity)
// Featured
this.optimismEnv = createLargeButton('assets/img/optimismLogo.webp', 'optimismLogo', 'Optimism', startOptimism)
this.solhintEnv = createLargeButton('assets/img/solhintLogo.png', 'solhintLogo', 'Solhint linter', startSolhint)
this.learnEthEnv = createLargeButton('assets/img/learnEthLogo.webp', 'learnEthLogo', 'LearnEth', startLearnEth)
this.sourcifyEnv = createLargeButton('assets/img/sourcifyLogo.webp', 'sourcifyLogo', 'Sourcify', startSourceVerify)
this.moreEnv = createLargeButton('assets/img/moreLogo.webp', 'moreLogo', 'More', startPluginManager)
this.websiteIcon = yo`<img id='remixHhomeWebsite' class="mr-1 ${css.image}" src="${profile.icon}"></img>`
const themeQuality = globalRegistry.get('themeModule').api.currentTheme().quality
const invertNum = (themeQuality === 'dark') ? 1 : 0
this.solEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
this.optimismEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
this.solhintEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
this.learnEthEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
this.sourcifyEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
this.moreEnv.getElementsByTagName('img')[0].style.filter = `invert(${invertNum})`
this.websiteIcon.style.filter = `invert(${invertNum})`
const switchToPreviousVersion = () => {
const query = new QueryParams()
query.update({ appVersion: '0.7.7' })
_paq.push(['trackEvent', 'LoadingType', 'oldExperience_0.7.7'])
document.location.reload()
}
const migrate = async () => {
try {
setTimeout(() => {
tooltip('migrating workspace...')
}, 500)
const workspaceName = await migrateToWorkspace(this.fileManager, this.filePanel)
tooltip('done. ' + workspaceName + ' created.')
} catch (e) {
setTimeout(() => {
tooltip(e.message)
}, 1000)
}
}
const onAcceptDownloadn = async () => {
await downloadFiles()
const el = document.getElementById('modal-dialog')
el.parentElement.removeChild(el)
migrate()
}
const onDownload = () => {
const el = document.getElementById('modal-dialog')
el.parentElement.removeChild(el)
migrate()
}
const onCancel = () => {
const el = document.getElementById('modal-dialog')
el.parentElement.removeChild(el)
}
const migrateWorkspace = async () => {
modalDialog(
'File system Migration',
yo`
<span>Do you want to download your files to local device first?</span>
<div class="d-flex justify-content-around pt-3 mt-3 border-top">
<button class="btn btn-sm btn-primary" onclick=${async () => onAcceptDownloadn()}>Download and Migrate</button>
<button class="btn btn-sm btn-secondary ${css.migrationBtn}" onclick=${() => onDownload()}>Migrate</button>
<button class="btn btn-sm btn-secondary ${css.migrationBtn}" onclick=${() => onCancel()}>Cancel</button>
</div>
`,
{
label: '',
fn: null
},
{
label: '',
fn: null
}
)
}
const img = yo`<img class="m-4 ${css.logoImg}" src="assets/img/guitarRemiCroped.webp" onclick="${() => playRemi()}"></img>`
const playRemi = async () => { await document.getElementById('remiAudio').play() }
// to retrieve medium posts
document.body.appendChild(yo`<script src="https://www.twilik.com/assets/retainable/rss-embed/retainable-rss-embed.js"></script>`)
const container = yo`
<div class="${css.homeContainer} d-flex" data-id="landingPageHomeContainer">
<div class="${css.mainContent} bg-light">
<div class="d-flex justify-content-between">
<div class="d-flex flex-column">
<div class="border-bottom d-flex justify-content-between clearfix py-3 mb-4">
<div class="mx-4 w-100 d-flex">
${img}
<audio id="remiAudio" muted=false src="assets/audio/remiGuitar-single-power-chord-A-minor.wav"></audio>
<div class="w-80 pl-5 ml-5">
<h5 class="mb-1">Quicklinks</h5>
<a class="${css.text} mr-1" target="__blank" href="https://medium.com/remix-ide/migrating-files-to-workspaces-8e34737c751c?source=friends_link&sk=b75cfd9093aa23c78be13cce49e4a5e8">Guide </a>for migrating the old File System
<p class="font-weight-bold mb-0 py-1">Migration tools:</p>
<li class="pl-1">
<spam class="pl-0">
<u class="${css.text} pr-1" onclick=${() => migrateWorkspace()}>Basic migration</u>
</spam>
</li>
<li class="pl-1">
<u class="${css.text} pr-1" onclick=${() => downloadFiles()}>Download all Files</u>
as a backup zip
</li>
<li class="pl-1">
<u class="${css.text} pr-1" onclick=${() => startRestoreBackupZip()}>Restore files</u>from backup zip
</li>
<p class="font-weight-bold mb-0 mt-2">Help:</p>
<dir class="d-flex flex-column mt-1 pl-0">
<a class="${css.text} mx-1" target="__blank" href="https://gitter.im/ethereum/remix">Gitter channel</a>
<a class="${css.text} mx-1" target="__blank" href="https://github.com/ethereum/remix-project/issues">Report on Github</a>
</dir>
</div>
</div>
</div>
<div class="row ${css.hpSections} mx-4" data-id="landingPageHpSections">
<div class="ml-3">
<div class="plugins mb-5">
<h4>Featured Plugins</h4>
<div class="d-flex flex-row pt-2">
${this.solEnv}
${this.optimismEnv}
${this.learnEthEnv}
${this.solhintEnv}
${this.sourcifyEnv}
${this.moreEnv}
</div>
</div>
<div class="d-flex">
<div class="file">
<h4>File</h4>
<p class="mb-1">
<i class="mr-1 far fa-file"></i>
<span class="ml-1 mb-1 ${css.text}" onclick=${() => createNewFile()}>New File</span>
</p>
<p class="mb-1">
<i class="mr-1 far fa-file-alt"></i>
<label class="ml-1 ${css.labelIt} ${css.bigLabelSize} ${css.text}">
Open Files
<input title="open file" type="file" onchange="${(event) => {
event.stopPropagation()
uploadFile(event.target)
}
}" multiple />
</label>
</p>
<p class="mb-1">
<i class="far fa-hdd"></i>
<span class="ml-1 ${css.text}" onclick=${() => connectToLocalhost()}>Connect to Localhost</span>
</p>
<p class="mt-3 mb-0"><label>LOAD FROM:</label></p>
<div class="btn-group">
<button class="btn mr-1 btn-secondary" data-id="landingPageImportFromGistButton" onclick="${() => importFromGist()}">Gist</button>
<button class="btn mx-1 btn-secondary" onclick="${() => load('Github', 'github URL', ['https://github.com/0xcert/ethereum-erc721/src/contracts/tokens/nf-token-metadata.sol', 'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol'])}">GitHub</button>
<button class="btn mx-1 btn-secondary" onclick="${() => load('Ipfs', 'ipfs URL', ['ipfs://<ipfs-hash>'])}">Ipfs</button>
<button class="btn mx-1 btn-secondary" onclick="${() => load('Https', 'http/https raw content', ['https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol'])}">https</button>
</div><!-- end of btn-group -->
</div><!-- end of div.file -->
<div class="ml-4 pl-4">
<h4>Resources</h4>
<p class="mb-1">
<i class="mr-1 fas fa-book"></i>
<a class="${css.text}" target="__blank" href="https://remix-ide.readthedocs.io/en/latest/#">Documentation</a>
</p>
<p class="mb-1">
<i class="mr-1 fab fa-gitter"></i>
<a class="${css.text}" target="__blank" href="https://gitter.im/ethereum/remix">Gitter channel</a>
</p>
<p class="mb-1">
${this.websiteIcon}
<a class="${css.text}" target="__blank" href="https://remix-project.org">Featuring website</a>
</p>
<p class="mb-1">
<i class="fab fa-ethereum ${css.image}"></i>
<span class="${css.text}" onclick=${() => switchToPreviousVersion()}>Old experience</span>
</p>
</div>
</div>
</div>
</div><!-- end of hpSections -->
</div>
<div class="d-flex flex-column ${css.rightPanel}">
<div class="d-flex pr-3 py-2 align-self-end" id="remixIDEMediaPanelsTitle">
${this.badgeTwitter}
${this.badgeMedium}
</div>
<div class="mr-3 d-flex bg-light ${css.panels}" id="remixIDEMediaPanels">
${this.mediumPanel}
${this.twitterPanel}
</div>
</div>
</div>
</div>
</div>
`
this.renderComponent()
return this.el
}
return container
renderComponent () {
ReactDOM.render(
<RemixUiHomeTab
plugin={this}
/>
, this.el)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -3,3 +3,4 @@ export { CompilerMetadata } from './lib/compiler-metadata'
export { FetchAndCompile } from './lib/compiler-fetch-and-compile'
export { CompilerImports } from './lib/compiler-content-imports'
export { CompilerArtefacts } from './lib/compiler-artefacts'
export { EditorContextListener } from './lib/editor-context-listener'

@ -4,7 +4,7 @@ import { CompilerAbstract } from '@remix-project/remix-solidity'
const profile = {
name: 'compilerArtefacts',
methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas'],
methods: ['get', 'addResolvedContract', 'getCompilerAbstract', 'getAllContractDatas', 'getLastCompilationResult'],
events: [],
version: '0.0.1'
}
@ -59,6 +59,10 @@ export class CompilerArtefacts extends Plugin {
})
}
getLastCompilationResult () {
return this.compilersArtefacts.__last
}
getAllContractDatas () {
const contractsData = {}
Object.keys(this.compilersArtefactsPerFile).map((targetFile) => {

@ -1,47 +1,48 @@
'use strict'
import { Plugin } from '@remixproject/engine'
import * as packageJson from '../../../../../package.json'
import { sourceMappingDecoder } from '@remix-project/remix-debug'
const { AstWalker } = require('@remix-project/remix-astwalker')
const EventManager = require('../../lib/events')
const globalRegistry = require('../../global/registry')
const profile = {
name: 'contextualListener',
methods: [],
methods: ['referencesOf', 'getActiveHighlights', 'gasEstimation', 'declarationOf'],
events: [],
version: packageJson.version
version: '0.0.1'
}
/*
trigger contextChanged(nodes)
*/
class ContextualListener extends Plugin {
constructor (opts) {
export class EditorContextListener extends Plugin {
_index: any
_activeHighlights: Array<any>
astWalker: any
currentPosition: any
currentFile: String
nodes: Array<any>
results: any
estimationObj: any
creationCost: any
codeDepositCost: any
contract: any
activated: boolean
constructor () {
super(profile)
this.event = new EventManager()
this._components = {}
this._components.registry = globalRegistry
this.editor = opts.editor
this.pluginManager = opts.pluginManager
this._deps = {
compilersArtefacts: this._components.registry.get('compilersartefacts').api,
config: this._components.registry.get('config').api,
offsetToLineColumnConverter: this._components.registry.get('offsettolinecolumnconverter').api
}
this.activated = false
this._index = {
Declarations: {},
FlatReferences: {}
}
this._activeHighlights = []
this.editor.event.register('contentChanged', () => { this._stopHighlighting() })
this.sourceMappingDecoder = sourceMappingDecoder
this.astWalker = new AstWalker()
}
onActivation () {
this.on('editor', 'contentChanged', () => { this._stopHighlighting() })
this.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => {
if (languageVersion.indexOf('soljson') !== 0) return
this._stopHighlighting()
@ -52,11 +53,18 @@ class ContextualListener extends Plugin {
this._buildIndex(data, source)
})
setInterval(() => {
if (this._deps.compilersArtefacts.__last && this._deps.compilersArtefacts.__last.languageversion.indexOf('soljson') === 0) {
this._highlightItems(this.editor.getCursorPosition(), this._deps.compilersArtefacts.__last, this._deps.config.get('currentFile'))
setInterval(async () => {
const compilationResult = await this.call('compilerArtefacts', 'getLastCompilationResult')
if (compilationResult && compilationResult.languageversion.indexOf('soljson') === 0) {
this._highlightItems(
await this.call('editor', 'getCursorPosition'),
compilationResult,
await this.call('fileManager', 'file')
)
}
}, 1000)
this.activated = true
}
getActiveHighlights () {
@ -74,7 +82,7 @@ class ContextualListener extends Plugin {
return this._index.Declarations[node.id]
}
_highlightItems (cursorPosition, compilationResult, file) {
async _highlightItems (cursorPosition, compilationResult, file) {
if (this.currentPosition === cursorPosition) return
if (this.currentFile !== file) {
this.currentFile = file
@ -85,12 +93,12 @@ class ContextualListener extends Plugin {
this.currentPosition = cursorPosition
this.currentFile = file
if (compilationResult && compilationResult.data && compilationResult.data.sources[file]) {
const nodes = this.sourceMappingDecoder.nodesAtPosition(null, cursorPosition, compilationResult.data.sources[file])
const nodes = sourceMappingDecoder.nodesAtPosition(null, cursorPosition, compilationResult.data.sources[file])
this.nodes = nodes
if (nodes && nodes.length && nodes[nodes.length - 1]) {
this._highlightExpressions(nodes[nodes.length - 1], compilationResult)
await this._highlightExpressions(nodes[nodes.length - 1], compilationResult)
}
this.event.trigger('contextChanged', [nodes])
this.emit('contextChanged', nodes)
}
}
@ -111,21 +119,19 @@ class ContextualListener extends Plugin {
}
}
_highlight (node, compilationResult) {
async _highlight (node, compilationResult) {
if (!node) return
const position = this.sourceMappingDecoder.decode(node.src)
const eventId = this._highlightInternal(position, node)
const lastCompilationResult = this._deps.compilersArtefacts.__last
if (eventId && lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0) {
this._activeHighlights.push({ eventId, position, fileTarget: lastCompilationResult.getSourceName(position.file), nodeId: node.id })
const position = sourceMappingDecoder.decode(node.src)
await this._highlightInternal(position, node, compilationResult)
if (compilationResult && compilationResult.languageversion.indexOf('soljson') === 0) {
this._activeHighlights.push({ position, fileTarget: compilationResult.getSourceName(position.file), nodeId: node.id })
}
}
_highlightInternal (position, node) {
async _highlightInternal (position, node, compilationResult) {
if (node.nodeType === 'Block') return
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())
if (compilationResult && compilationResult.languageversion.indexOf('soljson') === 0) {
let lineColumn = await this.call('offsetToLineColumnConverter', 'offsetToLineColumn', position, position.file, compilationResult.getSourceCode().sources, compilationResult.getAsts())
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 = {
@ -139,38 +145,38 @@ class ContextualListener extends Plugin {
}
}
}
const fileName = lastCompilationResult.getSourceName(position.file)
const fileName = compilationResult.getSourceName(position.file)
if (fileName) {
return this.call('editor', 'highlight', lineColumn, fileName, '', { focus: false })
return await this.call('editor', 'highlight', lineColumn, fileName, '', { focus: false })
}
}
return null
}
_highlightExpressions (node, compilationResult) {
const highlights = (id) => {
async _highlightExpressions (node, compilationResult) {
const highlights = async (id) => {
if (this._index.Declarations && this._index.Declarations[id]) {
const refs = this._index.Declarations[id]
for (const ref in refs) {
const node = refs[ref]
this._highlight(node, compilationResult)
await this._highlight(node, compilationResult)
}
}
}
if (node && node.referencedDeclaration) {
highlights(node.referencedDeclaration)
await highlights(node.referencedDeclaration)
const current = this._index.FlatReferences[node.referencedDeclaration]
this._highlight(current, compilationResult)
await this._highlight(current, compilationResult)
} else {
highlights(node.id)
this._highlight(node, compilationResult)
await highlights(node.id)
await this._highlight(node, compilationResult)
}
this.results = compilationResult
}
_stopHighlighting () {
this.call('editor', 'discardHighlight')
this.event.trigger('stopHighlighting', [])
this.emit('stopHighlighting')
this._activeHighlights = []
}
@ -229,5 +235,3 @@ class ContextualListener extends Plugin {
return '(' + params.toString() + ')'
}
}
module.exports = ContextualListener

@ -0,0 +1,4 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

@ -0,0 +1,19 @@
{
"env": {
"browser": true,
"es6": true
},
"extends": "../../../.eslintrc",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error"
}
}

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

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

@ -1,17 +1,9 @@
var csjs = require('csjs-inject')
var css = csjs`
.contextview {
opacity : 1;
position : relative;
height : 25px;
}
.container {
.container-context-view {
padding : 1px 15px;
}
.line {
display : flex;
justify-content : flex-end;
align-items : center;
text-overflow : ellipsis;
overflow : hidden;
@ -48,12 +40,4 @@ var css = csjs`
z-index : 50;
border-radius : 1px;
border : 2px solid var(--secondary);
}
.contextviewcontainer{
z-index : 50;
border-radius : 1px;
border : 2px solid var(--secondary);
}
`
module.exports = css
}

@ -0,0 +1,191 @@
import React, { useEffect, useState, useRef } from 'react' // eslint-disable-line
import { sourceMappingDecoder } from '@remix-project/remix-debug'
import './remix-ui-editor-context-view.css'
/* eslint-disable-next-line */
export type astNode = {
name: string,
id: number,
children: Array<any>,
typeDescriptions: any,
nodeType: String,
src: any,
nodeId: any,
position: any
}
export type onContextListenerChangedListener = (nodes: Array<astNode>) => void
export type gasEstimationType = {
executionCost: string,
codeDepositCost: string
}
export interface RemixUiEditorContextViewProps {
hide: boolean,
gotoLine: (line: number, column: number) => void,
openFile: (fileName: string) => void,
getLastCompilationResult: () => any,
offsetToLineColumn: (position: any, file: any, sources: any, asts: any) => any,
getCurrentFileName: () => String
onContextListenerChanged: (listener: onContextListenerChangedListener) => void
referencesOf: (nodes: astNode) => Array<astNode>
getActiveHighlights: () => Array<astNode>
gasEstimation: (node: astNode) => gasEstimationType
declarationOf: (node: astNode) => astNode
}
function isDefinition (node: any) {
return node.nodeType === 'ContractDefinition' ||
node.nodeType === 'FunctionDefinition' ||
node.nodeType === 'ModifierDefinition' ||
node.nodeType === 'VariableDeclaration' ||
node.nodeType === 'StructDefinition' ||
node.nodeType === 'EventDefinition'
}
type nullableAstNode = astNode | null
export function RemixUiEditorContextView (props: RemixUiEditorContextViewProps) {
/*
gotoLineDisableRef is used to temporarily disable the update of the view.
e.g when the user ask the component to "gotoLine" we don't want to rerender the component (but just to put the mouse on the desired line)
*/
const gotoLineDisableRef = useRef(false)
const [state, setState] = useState<{
nodes: Array<astNode>,
references: Array<astNode>,
activeHighlights: Array<any>
currentNode: nullableAstNode,
gasEstimation: gasEstimationType
}>({
nodes: [],
references: [],
activeHighlights: [],
currentNode: null,
gasEstimation: { executionCost: '', codeDepositCost: '' }
})
useEffect(() => {
props.onContextListenerChanged(async (nodes: Array<astNode>) => {
if (gotoLineDisableRef.current) {
gotoLineDisableRef.current = false
return
}
let currentNode
if (!props.hide && nodes && nodes.length) {
currentNode = nodes[nodes.length - 1]
if (!isDefinition(currentNode)) {
currentNode = await props.declarationOf(currentNode)
}
}
let references
let gasEstimation
if (currentNode) {
references = await props.referencesOf(currentNode)
if (currentNode.nodeType === 'FunctionDefinition') {
gasEstimation = await props.gasEstimation(currentNode)
}
}
let activeHighlights = await props.getActiveHighlights()
setState(prevState => {
return { ...prevState, nodes, references, activeHighlights, currentNode, gasEstimation }
})
})
}, [])
/*
* show gas estimation
*/
const gasEstimation = (node) => {
if (node.nodeType === 'FunctionDefinition') {
const result: gasEstimationType = state.gasEstimation
const executionCost = ' Execution cost: ' + result.executionCost + ' gas'
const codeDepositCost = 'Code deposit cost: ' + result.codeDepositCost + ' gas'
const estimatedGas = result.codeDepositCost ? `${codeDepositCost}, ${executionCost}` : `${executionCost}`
return (
<div className="gasEstimation">
<i className="fas fa-gas-pump gasStationIcon" title='Gas estimation'></i>
<span>{estimatedGas}</span>
</div>
)
} else {
return (<div></div>)
}
}
/*
* onClick jump to ast node in the editor
*/
const _jumpToInternal = async (position: any) => {
const jumpToLine = async (fileName: string, lineColumn: any) => {
if (fileName !== await props.getCurrentFileName()) {
await props.openFile(fileName)
}
if (lineColumn.start && lineColumn.start.line && lineColumn.start.column) {
gotoLineDisableRef.current = true
props.gotoLine(lineColumn.start.line, lineColumn.end.column + 1)
}
}
const lastCompilationResult = await props.getLastCompilationResult()
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0 && lastCompilationResult.data) {
const lineColumn = await props.offsetToLineColumn(
position,
position.file,
lastCompilationResult.getSourceCode().sources,
lastCompilationResult.getAsts())
const filename = lastCompilationResult.getSourceName(position.file)
// TODO: refactor with rendererAPI.errorClick
jumpToLine(filename, lineColumn)
}
}
const _render = (node: nullableAstNode) => {
if (!node) return (<div></div>)
const references = state.references
const type = node.typeDescriptions && node.typeDescriptions.typeString ? node.typeDescriptions.typeString : node.nodeType
const referencesCount = `${references ? references.length : '0'} reference(s)`
let ref = 0
const nodes: Array<astNode> = state.activeHighlights
const jumpTo = () => {
if (node && node.src) {
const position = sourceMappingDecoder.decode(node.src)
if (position) {
_jumpToInternal(position)
}
}
}
// JUMP BETWEEN REFERENCES
const jump = (e: any) => {
e.target.dataset.action === 'next' ? ref++ : ref--
if (ref < 0) ref = nodes.length - 1
if (ref >= nodes.length) ref = 0
_jumpToInternal(nodes[ref].position)
}
return (
<div className="line">{gasEstimation(node)}
<div title={type} className="type">{type}</div>
<div title={node.name} className="name mr-2">{node.name}</div>
<i className="fas fa-share jump" data-action='gotoref' aria-hidden="true" onClick={jumpTo}></i>
<span className="referencesnb">{referencesCount}</span>
<i data-action='previous' className="fas fa-chevron-up jump" aria-hidden="true" onClick={jump}></i>
<i data-action='next' className="fas fa-chevron-down jump" aria-hidden="true" onClick={jump}></i>
</div>
)
}
return (
!props.hide && <div className="container-context-view contextviewcontainer bg-light text-dark border-0 py-1">
{_render(state.currentNode)}
</div>
)
}
export default RemixUiEditorContextView

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -0,0 +1,133 @@
/* eslint-disable */
export const cairoConf = {
comments: {
lineComment: '#'
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
['%{', '%}']
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '%{', close: '%}' },
{ open: "'", close: "'", notIn: ['string', 'comment'] }
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '%{', close: '%}' },
{ open: "'", close: "'" }
]
}
export const cairoLang = {
defaultToken: '',
tokenPostfix: '.cairo',
brackets: [
{ token: 'delimiter.curly', open: '{', close: '}' },
{ token: 'delimiter.parenthesis', open: '(', close: ')' },
{ token: 'delimiter.square', open: '[', close: ']' },
{ token: 'delimiter.curly', open: '%{', close: '%}' }
],
keywords: [
// control
'if',
'else',
'end',
// meta
'alloc_locals',
'as',
'assert',
'cast',
'const',
'dw',
'felt',
'from',
'func',
'import',
'let',
'local',
'member',
'nondet',
'return',
'static_assert',
'struct',
'tempvar',
'with_attr',
'with',
// register
'ap',
'fp',
// opcode
'call',
'jmp',
'ret',
'abs',
'rel'
],
operators: ['=', ':', '==', '++', '+', '-', '*', '**', '/', '&', '%', '_'],
// we include these common regular expressions
symbols: /[=><!~?:&|+\-*\/\^%]+/,
numberDecimal: /[+-]?[0-9]+/,
numberHex: /[+-]?0x[0-9a-fA-F]+/,
// The main tokenizer for our languages
tokenizer: {
root: [
// identifiers and keywords
[
/[a-zA-Z_]\w*/,
{
cases: {
'@keywords': { token: 'keyword.$0' },
'@default': 'identifier'
}
}
],
// whitespace
{ include: '@whitespace' },
// directives
[/^%[a-zA-Z]\w*/, 'tag'],
// delimiters and operators
[/[{}()\[\]]/, '@brackets'],
[/[<>](?!@symbols)/, '@brackets'],
[
/@symbols/,
{
cases: {
'@operators': 'delimiter',
'@default': ''
}
}
],
// numbers
[/(@numberHex)/, 'number.hex'],
[/(@numberDecimal)/, 'number'],
// strings
[/'[^']*'/, 'string']
],
whitespace: [
[/\s+/, 'white'],
[/(^#.*$)/, 'comment']
]
}
}

@ -8,4 +8,9 @@
border-radius : 10px;
height: auto;
width: auto;
}
.contextview {
opacity: 1;
position: absolute;
}

@ -1,7 +1,9 @@
import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line
import { RemixUiEditorContextView, astNode } from '@remix-ui/editor-context-view'
import Editor, { loader } from '@monaco-editor/react'
import { reducerActions, reducerListener, initialState } from './actions/editor'
import { language, conf } from './syntax'
import { cairoLang, cairoConf } from './cairoSyntax'
import './remix-ui-editor.css'
@ -48,6 +50,7 @@ loader.config({ paths: { vs: 'assets/js/monaco-editor/dev/vs' } })
/* eslint-disable-next-line */
export interface EditorUIProps {
contextualListener: any
activated: boolean
themeType: string
currentFile: string
@ -61,6 +64,7 @@ export interface EditorUIProps {
}
plugin: {
on: (plugin: string, event: string, listener: any) => void
call: (plugin: string, method: string, arg1?: any, arg2?: any, arg3?: any, arg4?: any) => any
}
editorAPI: {
findMatches: (uri: string, value: string) => any
@ -206,7 +210,12 @@ export const EditorUI = (props: EditorUIProps) => {
'editor.lineHighlightBorder': secondaryColor,
'editor.lineHighlightBackground': textbackground === darkColor ? lightColor : secondaryColor,
'editorGutter.background': lightColor,
'minimap.background': lightColor
'minimap.background': lightColor,
'menu.foreground': textColor,
'menu.background': textbackground,
'menu.selectionBackground': secondaryColor,
'menu.selectionForeground': textColor,
'menu.selectionBorder': secondaryColor
}
})
monacoRef.current.editor.setTheme(themeName)
@ -255,7 +264,7 @@ export const EditorUI = (props: EditorUIProps) => {
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}`
inlineClassName: `alert-info highlightLine${marker.position.start.line + 1}`
}
})
}
@ -273,7 +282,11 @@ export const EditorUI = (props: EditorUIProps) => {
const file = editorModelsState[props.currentFile]
editorRef.current.setModel(file.model)
editorRef.current.updateOptions({ readOnly: editorModelsState[props.currentFile].readOnly })
if (file.language === 'sol') monacoRef.current.editor.setModelLanguage(file.model, 'remix-solidity')
if (file.language === 'sol') {
monacoRef.current.editor.setModelLanguage(file.model, 'remix-solidity')
} else if (file.language === 'cairo') {
monacoRef.current.editor.setModelLanguage(file.model, 'remix-cairo')
}
setAnnotationsbyFile(props.currentFile)
setMarkerbyFile(props.currentFile)
}, [props.currentFile])
@ -362,21 +375,41 @@ export const EditorUI = (props: EditorUIProps) => {
monacoRef.current = monaco
// Register a new language
monacoRef.current.languages.register({ id: 'remix-solidity' })
monacoRef.current.languages.register({ id: 'remix-cairo' })
// Register a tokens provider for the language
monacoRef.current.languages.setMonarchTokensProvider('remix-solidity', language)
monacoRef.current.languages.setLanguageConfiguration('remix-solidity', conf)
monacoRef.current.languages.setMonarchTokensProvider('remix-cairo', cairoLang)
monacoRef.current.languages.setLanguageConfiguration('remix-cairo', cairoConf)
}
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 }}
/>
<div className="w-100 h-100 d-flex flex-column-reverse">
<Editor
width="100%"
path={props.currentFile}
language={editorModelsState[props.currentFile] ? editorModelsState[props.currentFile].language : 'text'}
onMount={handleEditorDidMount}
beforeMount={handleEditorWillMount}
options={{ glyphMargin: true }}
/>
<div className="contextview">
<RemixUiEditorContextView
hide={false}
gotoLine={(line, column) => props.plugin.call('editor', 'gotoLine', line, column)}
openFile={(file) => props.plugin.call('editor', 'openFile', file)}
getLastCompilationResult={() => { return props.plugin.call('compilerArtefacts', 'getLastCompilationResult') } }
offsetToLineColumn={(position, file, sources, asts) => { return props.plugin.call('offsetToLineColumnConverter', 'offsetToLineColumn', position, file, sources, asts) } }
getCurrentFileName={() => { return props.plugin.call('fileManager', 'file') } }
onContextListenerChanged={(listener) => { props.plugin.on('contextualListener', 'contextChanged', listener) }}
referencesOf={(node: astNode) => { return props.plugin.call('contextualListener', 'referencesOf', node) }}
getActiveHighlights={() => { return props.plugin.call('contextualListener', 'getActiveHighlights') }}
gasEstimation={(node: astNode) => { return props.plugin.call('contextualListener', 'gasEstimation', node) }}
declarationOf={(node: astNode) => { return props.plugin.call('contextualListener', 'declarationOf', node) }}
/>
</div>
</div>
)
}

@ -59,5 +59,6 @@ export const getPathIcon = (path: string) => {
? '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'
? 'fab fa-ethereum' : path.endsWith('.cairo')
? 'fab fa-ethereum' : 'far fa-file' // TODO: add cairo icon
}

@ -0,0 +1,4 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

@ -0,0 +1,248 @@
{
"rules": {
"array-callback-return": "warn",
"dot-location": ["warn", "property"],
"eqeqeq": ["warn", "smart"],
"new-parens": "warn",
"no-caller": "warn",
"no-cond-assign": ["warn", "except-parens"],
"no-const-assign": "warn",
"no-control-regex": "warn",
"no-delete-var": "warn",
"no-dupe-args": "warn",
"no-dupe-keys": "warn",
"no-duplicate-case": "warn",
"no-empty-character-class": "warn",
"no-empty-pattern": "warn",
"no-eval": "warn",
"no-ex-assign": "warn",
"no-extend-native": "warn",
"no-extra-bind": "warn",
"no-extra-label": "warn",
"no-fallthrough": "warn",
"no-func-assign": "warn",
"no-implied-eval": "warn",
"no-invalid-regexp": "warn",
"no-iterator": "warn",
"no-label-var": "warn",
"no-labels": ["warn", { "allowLoop": true, "allowSwitch": false }],
"no-lone-blocks": "warn",
"no-loop-func": "warn",
"no-mixed-operators": [
"warn",
{
"groups": [
["&", "|", "^", "~", "<<", ">>", ">>>"],
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
["&&", "||"],
["in", "instanceof"]
],
"allowSamePrecedence": false
}
],
"no-multi-str": "warn",
"no-native-reassign": "warn",
"no-negated-in-lhs": "warn",
"no-new-func": "warn",
"no-new-object": "warn",
"no-new-symbol": "warn",
"no-new-wrappers": "warn",
"no-obj-calls": "warn",
"no-octal": "warn",
"no-octal-escape": "warn",
"no-redeclare": "warn",
"no-regex-spaces": "warn",
"no-restricted-syntax": ["warn", "WithStatement"],
"no-script-url": "warn",
"no-self-assign": "warn",
"no-self-compare": "warn",
"no-sequences": "warn",
"no-shadow-restricted-names": "warn",
"no-sparse-arrays": "warn",
"no-template-curly-in-string": "warn",
"no-this-before-super": "warn",
"no-throw-literal": "warn",
"no-restricted-globals": [
"error",
"addEventListener",
"blur",
"close",
"closed",
"confirm",
"defaultStatus",
"defaultstatus",
"event",
"external",
"find",
"focus",
"frameElement",
"frames",
"history",
"innerHeight",
"innerWidth",
"length",
"location",
"locationbar",
"menubar",
"moveBy",
"moveTo",
"name",
"onblur",
"onerror",
"onfocus",
"onload",
"onresize",
"onunload",
"open",
"opener",
"opera",
"outerHeight",
"outerWidth",
"pageXOffset",
"pageYOffset",
"parent",
"print",
"removeEventListener",
"resizeBy",
"resizeTo",
"screen",
"screenLeft",
"screenTop",
"screenX",
"screenY",
"scroll",
"scrollbars",
"scrollBy",
"scrollTo",
"scrollX",
"scrollY",
"self",
"status",
"statusbar",
"stop",
"toolbar",
"top"
],
"no-unexpected-multiline": "warn",
"no-unreachable": "warn",
"no-unused-expressions": [
"error",
{
"allowShortCircuit": true,
"allowTernary": true,
"allowTaggedTemplates": true
}
],
"no-unused-labels": "warn",
"no-useless-computed-key": "warn",
"no-useless-concat": "warn",
"no-useless-escape": "warn",
"no-useless-rename": [
"warn",
{
"ignoreDestructuring": false,
"ignoreImport": false,
"ignoreExport": false
}
],
"no-with": "warn",
"no-whitespace-before-property": "warn",
"react-hooks/exhaustive-deps": "warn",
"require-yield": "warn",
"rest-spread-spacing": ["warn", "never"],
"strict": ["warn", "never"],
"unicode-bom": ["warn", "never"],
"use-isnan": "warn",
"valid-typeof": "warn",
"no-restricted-properties": [
"error",
{
"object": "require",
"property": "ensure",
"message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting"
},
{
"object": "System",
"property": "import",
"message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting"
}
],
"getter-return": "warn",
"import/first": "error",
"import/no-amd": "error",
"import/no-webpack-loader-syntax": "error",
"react/forbid-foreign-prop-types": ["warn", { "allowInPropTypes": true }],
"react/jsx-no-comment-textnodes": "warn",
"react/jsx-no-duplicate-props": "warn",
"react/jsx-no-target-blank": "warn",
"react/jsx-no-undef": "error",
"react/jsx-pascal-case": ["warn", { "allowAllCaps": true, "ignore": [] }],
"react/jsx-uses-react": "warn",
"react/jsx-uses-vars": "warn",
"react/no-danger-with-children": "warn",
"react/no-direct-mutation-state": "warn",
"react/no-is-mounted": "warn",
"react/no-typos": "error",
"react/react-in-jsx-scope": "error",
"react/require-render-return": "error",
"react/style-prop-object": "warn",
"react/jsx-no-useless-fragment": "warn",
"jsx-a11y/accessible-emoji": "warn",
"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-has-content": "warn",
"jsx-a11y/anchor-is-valid": [
"warn",
{ "aspects": ["noHref", "invalidHref"] }
],
"jsx-a11y/aria-activedescendant-has-tabindex": "warn",
"jsx-a11y/aria-props": "warn",
"jsx-a11y/aria-proptypes": "warn",
"jsx-a11y/aria-role": "warn",
"jsx-a11y/aria-unsupported-elements": "warn",
"jsx-a11y/heading-has-content": "warn",
"jsx-a11y/iframe-has-title": "warn",
"jsx-a11y/img-redundant-alt": "warn",
"jsx-a11y/no-access-key": "warn",
"jsx-a11y/no-distracting-elements": "warn",
"jsx-a11y/no-redundant-roles": "warn",
"jsx-a11y/role-has-required-aria-props": "warn",
"jsx-a11y/role-supports-aria-props": "warn",
"jsx-a11y/scope": "warn",
"react-hooks/rules-of-hooks": "error",
"default-case": "off",
"no-dupe-class-members": "off",
"no-undef": "off",
"@typescript-eslint/consistent-type-assertions": "warn",
"no-array-constructor": "off",
"@typescript-eslint/no-array-constructor": "warn",
"@typescript-eslint/no-namespace": "error",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": [
"warn",
{
"functions": false,
"classes": false,
"variables": false,
"typedefs": false
}
],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{ "args": "none", "ignoreRestSiblings": true }
],
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": "warn"
},
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"jest": true,
"node": true
},
"settings": { "react": { "version": "detect" } },
"plugins": ["import", "jsx-a11y", "react", "react-hooks"],
"extends": ["../../../.eslintrc"],
"ignorePatterns": ["!**/*"]
}

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

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

@ -0,0 +1,27 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React, { useContext } from 'react'
import { ThemeContext } from '../themeContext'
interface PluginButtonProps {
imgPath: string,
envID: string,
envText: string,
callback: any
}
function PluginButton ({ imgPath, envID, envText, callback }: PluginButtonProps) {
const themeFilter = useContext(ThemeContext)
return (
<button
className="btn border-secondary d-flex mr-3 text-nowrap justify-content-center flex-column align-items-center remixui_envButton"
data-id={'landingPageStart' + envText}
onClick={() => callback()}
>
<img className="m-2 align-self-center remixui_envLogo" id={envID} src={imgPath} alt="" style={ { filter: themeFilter.filter } } />
<label className="text-uppercase text-dark remixui_cursorStyle">{envText}</label>
</button>
)
}
export default PluginButton

@ -0,0 +1,82 @@
.remixui_text {
cursor: pointer;
font-weight: normal;
max-width: 300px;
}
.remixui_text:hover {
cursor: pointer;
text-decoration: underline;
}
.remixui_homeContainer {
overflow-y: hidden;
overflow-y: auto;
flex-grow: 3;
}
.remixui_hpLogoContainer {
margin: 30px;
padding-right: 90px;
}
.remixui_mediaBadge {
font-size: 2em;
height: 2em;
width: 2em;
}
.remixui_mediaBadge:focus {
outline: none;
}
.remixui_image {
height: 1em;
width: 1em;
text-align: center;
}
.remixui_logoImg {
height: 10em;
}
.remixui_rightPanel {
right: 0;
position: absolute;
z-index: 3;
}
.remixui_remixHomeMedia {
overflow-y: auto;
overflow-x: hidden;
}
.remixui_panels {
box-shadow: 0px 0px 13px -7px;
}
.remixui_labelIt {
margin-bottom: 0;
}
.remixui_bigLabelSize {
font-size: 13px;
}
.remixui_seeAll {
margin-top: 7px;
white-space: nowrap;
}
.remixui_importFrom p {
margin-right: 10px;
}
.remixui_logoContainer img{
height: 150px;
opacity: 0.7;
}
.remixui_envLogo {
height: 16px;
}
.remixui_cursorStyle {
cursor: pointer;
}
.remixui_envButton {
width: 120px;
height: 70px;
}
.remixui_media {
overflow: hidden;
max-width: 400px;
transition: .5s ease-out;
z-index: 1000;
}
.remixui_migrationBtn {
width: 100px;
}

@ -0,0 +1,370 @@
import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line
import './remix-ui-home-tab.css'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import PluginButton from './components/pluginButton' // eslint-disable-line
import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params'
import { ThemeContext, themes } from './themeContext'
declare global {
interface Window {
_paq: any
}
}
const _paq = window._paq = window._paq || [] //eslint-disable-line
/* eslint-disable-next-line */
export interface RemixUiHomeTabProps {
plugin: any
}
const loadingInitialState = {
tooltip: '',
showModalDialog: false,
importSource: ''
}
const loadingReducer = (state = loadingInitialState, action) => {
return { ...state, tooltip: action.tooltip, showModalDialog: false, importSource: '' }
}
export const RemixUiHomeTab = (props: RemixUiHomeTabProps) => {
const { plugin } = props
const fileManager = plugin.fileManager
const [state, setState] = useState<{
themeQuality: { filter: string, name: string },
showMediaPanel: 'none' | 'twitter' | 'medium',
showModalDialog: boolean,
modalInfo: { title: string, loadItem: string, examples: Array<string> },
importSource: string,
toasterMsg: string
}>({
themeQuality: themes.light,
showMediaPanel: 'none',
showModalDialog: false,
modalInfo: { title: '', loadItem: '', examples: [] },
importSource: '',
toasterMsg: ''
})
const processLoading = () => {
const contentImport = plugin.contentImport
const workspace = fileManager.getProvider('workspace')
contentImport.import(
state.importSource,
(loadingMsg) => dispatch({ tooltip: loadingMsg }),
(error, content, cleanUrl, type, url) => {
if (error) {
toast(error.message || error)
} else {
try {
workspace.addExternal(type + '/' + cleanUrl, content, url)
plugin.call('menuicons', 'select', 'filePanel')
} catch (e) {
toast(e.message)
}
}
}
)
setState(prevState => {
return { ...prevState, showModalDialog: false, importSource: '' }
})
}
const [, dispatch] = useReducer(loadingReducer, loadingInitialState)
const playRemi = async () => {
remiAudioEl.current.play()
}
const remiAudioEl = useRef(null)
const inputValue = useRef(null)
useEffect(() => {
plugin.call('theme', 'currentTheme').then((theme) => {
// update theme quality. To be used for for images
setState(prevState => {
return { ...prevState, themeQuality: theme.quality === 'dark' ? themes.dark : themes.light }
})
})
plugin.on('theme', 'themeChanged', (theme) => {
// update theme quality. To be used for for images
setState(prevState => {
return { ...prevState, themeQuality: theme.quality === 'dark' ? themes.dark : themes.light }
})
})
window.addEventListener('click', (event) => {
const target = event.target as Element
const id = target.id
if (id !== 'remixIDEHomeTwitterbtn' && id !== 'remixIDEHomeMediumbtn') {
// todo check event.target
setState(prevState => { return { ...prevState, showMediaPanel: 'none' } })
}
})
// to retrieve twitter feed
const scriptTwitter = document.createElement('script')
scriptTwitter.src = 'https://platform.twitter.com/widgets.js'
scriptTwitter.async = true
document.body.appendChild(scriptTwitter)
// to retrieve medium publications
const scriptMedium = document.createElement('script')
scriptMedium.src = 'https://www.twilik.com/assets/retainable/rss-embed/retainable-rss-embed.js'
scriptMedium.async = true
document.body.appendChild(scriptMedium)
return () => {
document.body.removeChild(scriptTwitter)
document.body.removeChild(scriptMedium)
}
}, [])
const toast = (message: string) => {
setState(prevState => {
return { ...prevState, toasterMsg: message }
})
}
const createNewFile = async () => {
await plugin.call('filePanel', 'createNewFile')
}
const uploadFile = async (target) => {
await plugin.call('filePanel', 'uploadFile', target)
}
const connectToLocalhost = () => {
plugin.appManager.activatePlugin('remixd')
}
const importFromGist = () => {
plugin.gistHandler.loadFromGist({ gist: '' }, fileManager)
plugin.verticalIcons.select('filePanel')
}
const switchToPreviousVersion = () => {
const query = new QueryParams()
query.update({ appVersion: '0.7.7' })
_paq.push(['trackEvent', 'LoadingType', 'oldExperience_0.7.7'])
document.location.reload()
}
const startSolidity = async () => {
await plugin.appManager.activatePlugin(['solidity', 'udapp', 'solidityStaticAnalysis', 'solidityUnitTesting'])
plugin.verticalIcons.select('solidity')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'solidity'])
}
const startOptimism = async () => {
await plugin.appManager.activatePlugin('optimism-compiler')
plugin.verticalIcons.select('optimism-compiler')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'optimism-compiler'])
}
const startSolhint = async () => {
await plugin.appManager.activatePlugin(['solidity', 'solhint'])
plugin.verticalIcons.select('solhint')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'solhint'])
}
const startLearnEth = async () => {
await plugin.appManager.activatePlugin(['solidity', 'LearnEth', 'solidityUnitTesting'])
plugin.verticalIcons.select('LearnEth')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'learnEth'])
}
const startSourceVerify = async () => {
await plugin.appManager.activatePlugin(['solidity', 'source-verification'])
plugin.verticalIcons.select('source-verification')
_paq.push(['trackEvent', 'pluginManager', 'userActivate', 'source-verification'])
}
const startPluginManager = async () => {
await plugin.appManager.activatePlugin('pluginManager')
plugin.verticalIcons.select('pluginManager')
}
const showFullMessage = (title: string, loadItem: string, examples: Array<string>) => {
setState(prevState => {
return { ...prevState, showModalDialog: true, modalInfo: { title: title, loadItem: loadItem, examples: examples } }
})
}
const hideFullMessage = () => { //eslint-disable-line
setState(prevState => {
return { ...prevState, showModalDialog: false, importSource: '' }
})
}
const maxHeight = Math.max(window.innerHeight - 150, 250) + 'px'
const examples = state.modalInfo.examples.map((urlEl, key) => (<div key={key} className="p-1 user-select-auto"><a>{urlEl}</a></div>))
const elHeight = '4000px'
return (
<>
<ModalDialog
id='homeTab'
title={ 'Import from ' + state.modalInfo.title }
okLabel='Import'
hide={ !state.showModalDialog }
handleHide={ () => hideFullMessage() }
okFn={ () => processLoading() }
>
<div className="p-2 user-select-auto">
{ state.modalInfo.loadItem !== '' && <span>Enter the { state.modalInfo.loadItem } you would like to load.</span> }
{ state.modalInfo.examples.length !== 0 &&
<>
<div>e.g</div>
<div>
{ examples }
</div>
</> }
<input
ref={inputValue}
type='text'
name='prompt_text'
id='inputPrompt_text'
className="w-100 mt-1 form-control"
data-id="homeTabModalDialogCustomPromptText"
value={state.importSource}
onInput={(e) => {
setState(prevState => {
return { ...prevState, importSource: inputValue.current.value }
})
}}
/>
</div>
</ModalDialog>
<Toaster message={state.toasterMsg} />
<div className="d-flex flex-column ml-4" id="remixUiRightPanel">
<div className="border-bottom d-flex justify-content-between mr-4 pb-3 mb-3">
<div className="mx-4 my-4 d-flex">
<label style={ { fontSize: 'xxx-large', height: 'auto', alignSelf: 'flex-end' } }>Remix IDE</label>
</div>
<div className="mr-4 d-flex">
<img className="mt-4 mb-2 remixui_logoImg" src="assets/img/guitarRemiCroped.webp" onClick={ () => playRemi() } alt=""></img>
<audio
id="remiAudio"
muted={false}
src="assets/audio/remiGuitar-single-power-chord-A-minor.wav"
ref={remiAudioEl}
></audio>
</div>
</div>
<div className="row remixui_hpSections mx-2 mr-4" data-id="landingPageHpSections">
<div className="ml-3">
<div className="mb-5">
<h4>Featured Plugins</h4>
<div className="d-flex flex-row pt-2">
<ThemeContext.Provider value={ state.themeQuality }>
<PluginButton imgPath="assets/img/solidityLogo.webp" envID="solidityLogo" envText="Solidity" callback={() => startSolidity()} />
<PluginButton imgPath="assets/img/optimismLogo.webp" envID="optimismLogo" envText="Optimism" callback={() => startOptimism()} />
<PluginButton imgPath="assets/img/solhintLogo.webp" envID="solhintLogo" envText="Solhint linter" callback={() => startSolhint()} />
<PluginButton imgPath="assets/img/learnEthLogo.webp" envID="learnEthLogo" envText="LearnEth" callback={() => startLearnEth()} />
<PluginButton imgPath="assets/img/sourcifyLogo.webp" envID="sourcifyLogo" envText="Sourcify" callback={() => startSourceVerify()} />
<PluginButton imgPath="assets/img/moreLogo.webp" envID="moreLogo" envText="More" callback={startPluginManager} />
</ThemeContext.Provider>
</div>
</div>
<div className="d-flex">
<div className="file">
<h4>File</h4>
<p className="mb-1">
<i className="mr-2 far fa-file"></i>
<span className="ml-1 mb-1 remixui_text" onClick={() => createNewFile()}>New File</span>
</p>
<p className="mb-1">
<i className="mr-2 far fa-file-alt"></i>
<span className="ml-1 remixui_labelIt remixui_bigLabelSize} remixui_text">
Open Files
<input title="open file" type="file" onChange={(event) => {
event.stopPropagation()
uploadFile(event.target)
}} multiple />
</span>
</p>
<p className="mb-1">
<i className="mr-1 far fa-hdd"></i>
<span className="ml-1 remixui_text" onClick={() => connectToLocalhost()}>Connect to Localhost</span>
</p>
<p className="mt-3 mb-0"><label>LOAD FROM:</label></p>
<div className="btn-group">
<button className="btn mr-1 btn-secondary" data-id="landingPageImportFromGistButton" onClick={() => importFromGist()}>Gist</button>
<button className="btn mx-1 btn-secondary" data-id="landingPageImportFromGitHubButton" onClick={() => showFullMessage('Github', 'github URL', ['https://github.com/0xcert/ethereum-erc721/src/contracts/tokens/nf-token-metadata.sol', 'https://github.com/OpenZeppelin/openzeppelin-solidity/blob/67bca857eedf99bf44a4b6a0fc5b5ed553135316/contracts/access/Roles.sol'])}>GitHub</button>
<button className="btn mx-1 btn-secondary" onClick={() => showFullMessage('Ipfs', 'ipfs URL', ['ipfs://<ipfs-hash>'])}>Ipfs</button>
<button className="btn mx-1 btn-secondary" onClick={() => showFullMessage('Https', 'http/https raw content', ['https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC20/ERC20.sol'])}>https</button>
</div>
</div>
<div className="ml-4 pl-4">
<h4>Resources</h4>
<p className="mb-1">
<i className="mr-2 fas fa-book"></i>
<a className="remixui_text" target="__blank" href="https://remix-ide.readthedocs.io/en/latest/#">Documentation</a>
</p>
<p className="mb-1">
<i className="mr-2 fab fa-gitter"></i>
<a className="remixui_text" target="__blank" href="https://gitter.im/ethereum/remix">Gitter channel</a>
</p>
<p className="mb-1">
<img id='remixHhomeWebsite' className="mr-2 remixui_image" src={ plugin.profile.icon } style={ { filter: state.themeQuality.filter } } alt=''></img>
<a className="remixui_text" target="__blank" href="https://remix-project.org">Featuring website</a>
</p>
<p className="mb-1">
<i className="mr-2 fab fa-ethereum remixui_image"></i>
<span className="remixui_text" onClick={() => switchToPreviousVersion()}>Old experience</span>
</p>
</div>
</div>
</div>
</div>
<div className="d-flex flex-column remixui_rightPanel">
<div className="d-flex pr-3 py-2 align-self-end" id="remixIDEMediaPanelsTitle">
<button
className="btn-info p-2 m-1 border rounded-circle remixui_mediaBadge fab fa-twitter"
id="remixIDEHomeTwitterbtn"
title="Twitter"
onClick={(e) => {
setState(prevState => {
return { ...prevState, showMediaPanel: state.showMediaPanel === 'twitter' ? 'none' : 'twitter' }
})
_paq.push(['trackEvent', 'pluginManager', 'media', 'twitter'])
}}
></button>
<button
className="btn-danger p-2 m-1 border rounded-circle remixui_mediaBadge fab fa-medium"
id="remixIDEHomeMediumbtn"
title="Medium blogs"
onClick={(e) => {
setState(prevState => {
return { ...prevState, showMediaPanel: state.showMediaPanel === 'medium' ? 'none' : 'medium' }
})
_paq.push(['trackEvent', 'pluginManager', 'media', 'medium'])
}}
></button>
</div>
<div className="mr-3 d-flex bg-light remixui_panels" style={ { visibility: state.showMediaPanel === 'none' ? 'hidden' : 'visible' } } id="remixIDEMediaPanels">
<div id="remixIDE_MediumBlock" className="p-2 mx-1 mt-3 mb-0 remixui_remixHomeMedia" style={ { maxHeight: maxHeight } }>
<div id="medium-widget" className="px-3 remixui_media" hidden={state.showMediaPanel !== 'medium'} style={ { maxHeight: elHeight } }>
<div
id="retainable-rss-embed"
data-rss="https://medium.com/feed/remix-ide"
data-maxcols="1"
data-layout="grid"
data-poststyle="external"
data-readmore="More..."
data-buttonclass="btn mb-3"
data-offset="-100"
>
</div>
</div>
</div>
<div id="remixIDE_TwitterBlock" className="p-2 mx-1 mt-3 mb-0 remixui_remixHomeMedia" hidden={state.showMediaPanel !== 'twitter'} style={ { maxHeight: maxHeight, marginRight: '28px' } } >
<div className="remixui_media" style={ { minHeight: elHeight } } >
<a className="twitter-timeline"
data-width="330"
data-theme={ state.themeQuality.name }
data-chrome="nofooter noheader transparent"
data-tweet-limit="18"
href="https://twitter.com/EthereumRemix"
>
</a>
</div>
</div>
</div>
</div>
</div>
</>
)
}
export default RemixUiHomeTab

@ -0,0 +1,16 @@
import React from 'react' // eslint-disable-line
export const themes = {
light: {
filter: 'invert(0)',
name: 'light'
},
dark: {
filter: 'invert(1)',
name: 'dark'
}
}
export const ThemeContext = React.createContext(
themes.dark // default value
)

@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

@ -34,7 +34,9 @@ export const ModalDialog = (props: ModalDialogProps) => {
modal.current.addEventListener('blur', handleBlur)
return () => {
modal.current.removeEventListener('blur', handleBlur)
if (modal.current) {
modal.current.removeEventListener('blur', handleBlur)
}
}
}
}, [modal.current])

@ -1,6 +1,6 @@
/* eslint-disable no-undef */
export interface ModalDialogProps {
id?: string
id: string
title?: string,
message?: string | JSX.Element,
okLabel?: string,

@ -106,6 +106,7 @@ function PermisssionsSettings ({ pluginSettings }: PermissionSettingsProps) {
return (
<Fragment>
<ModalDialog
id='permissionsSettings'
handleHide={closeModal}
cancelFn={cancel}
hide={modalVisibility}

@ -0,0 +1,77 @@
import React, { useEffect, useState } from 'react'
export const useDragTerminal = (minHeight: number, defaultPosition: number) => {
const [isOpen, setIsOpen] = useState(defaultPosition > minHeight)
const [lastYPosition, setLastYPosition] = useState(0)
const [terminalPosition, setTerminalPosition] = useState(defaultPosition)
// Used to save position of the terminal when it is closed
const [lastTerminalPosition, setLastTerminalPosition] = useState(defaultPosition)
const [isDragging, setIsDragging] = useState(false)
const handleDraggingStart = (event: React.MouseEvent) => {
setLastYPosition(event.clientY)
setIsDragging(true)
}
const handleDragging = (event: MouseEvent) => {
event.preventDefault()
if (isDragging) {
const mouseYPosition = event.clientY
const difference = lastYPosition - mouseYPosition
const newTerminalPosition = terminalPosition + difference
setTerminalPosition(newTerminalPosition)
setLastYPosition(mouseYPosition)
}
}
const handleDraggingEnd = () => {
if (!isDragging) return
setIsDragging(false)
// Check terminal position to determine if it should be open or closed
setIsOpen(terminalPosition > minHeight)
}
const handleToggleTerminal = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault()
event.stopPropagation()
if (isOpen) {
setLastTerminalPosition(terminalPosition)
setLastYPosition(0)
setTerminalPosition(minHeight)
} else {
setTerminalPosition(lastTerminalPosition <= minHeight ? 323 : lastTerminalPosition)
}
setIsOpen(!isOpen)
}
// Add event listeners for dragging
useEffect(() => {
document.addEventListener('mousemove', handleDragging)
document.addEventListener('mouseup', handleDraggingEnd)
return () => {
document.removeEventListener('mousemove', handleDragging)
document.removeEventListener('mouseup', handleDraggingEnd)
}
}, [handleDragging, handleDraggingEnd])
// Reset terminal position
useEffect(() => {
if (!terminalPosition) {
setTerminalPosition(defaultPosition)
}
}, [terminalPosition, setTerminalPosition])
return {
isOpen,
terminalPosition,
isDragging,
handleDraggingStart,
handleToggleTerminal
}
}

@ -151,6 +151,11 @@ element.style {
cursor : row-resize;
z-index : 999;
}
.dragbarDragging {
background-color: var(--primary);
border: 2px solid var(--primary);
}
.console {
cursor : pointer;

@ -17,6 +17,7 @@ import RenderKnownTransactions from './components/RenderKnownTransactions' // es
import parse from 'html-react-parser'
import { EMPTY_BLOCK, KNOWN_TRANSACTION, RemixUiTerminalProps, UNKNOWN_TRANSACTION } from './types/terminalTypes'
import { wrapScript } from './utils/wrapScript'
import { useDragTerminal } from './custom-hooks/useDragTerminal'
/* eslint-disable-next-line */
export interface ClipboardEvent<T = Element> extends SyntheticEvent<T, any> {
@ -25,13 +26,8 @@ export interface ClipboardEvent<T = Element> extends SyntheticEvent<T, any> {
export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
const { call, _deps, on, config, event, gistHandler, version } = props.plugin
const [toggleDownUp, setToggleDownUp] = useState('fa-angle-double-down')
const [_cmdIndex, setCmdIndex] = useState(-1)
const [_cmdTemp, setCmdTemp] = useState('')
// dragable state
const [leftHeight, setLeftHeight] = useState<undefined | number>(undefined)
const [separatorYPosition, setSeparatorYPosition] = useState<undefined | number>(undefined)
const [dragging, setDragging] = useState(false)
const [newstate, dispatch] = useReducer(registerCommandReducer, initialState)
const [cmdHistory, cmdHistoryDispatch] = useReducer(addCommandHistoryReducer, initialState)
@ -78,6 +74,28 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
// terminal dragable
const leftRef = useRef(null)
const panelRef = useRef(null)
const terminalMenu = useRef(null)
const terminalMenuOffsetHeight = (terminalMenu.current && terminalMenu.current.offsetHeight) || 35
const terminalDefaultPosition = config.get('terminal-top-offset')
const {
isOpen,
isDragging,
terminalPosition,
handleDraggingStart,
handleToggleTerminal
} = useDragTerminal(terminalMenuOffsetHeight, terminalDefaultPosition)
// Check open state
useEffect(() => {
const resizeValue = isOpen ? [config.get('terminal-top-offset')] : []
event.trigger('resize', resizeValue)
}, [isOpen])
useEffect(() => {
event.trigger('resize', [terminalPosition])
}, [terminalPosition])
const scrollToBottom = () => {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
@ -193,19 +211,6 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
}
}
const handleMinimizeTerminal = (e) => {
e.preventDefault()
e.stopPropagation()
if (toggleDownUp === 'fa-angle-double-down') {
setToggleDownUp('fa-angle-double-up')
event.trigger('resize', [])
} else {
const terminalTopOffset = config.get('terminal-top-offset')
event.trigger('resize', [terminalTopOffset])
setToggleDownUp('fa-angle-double-down')
}
}
const focusinput = () => {
inputEl.current.focus()
}
@ -277,51 +282,6 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
}
}
/* start of mouse events */
const mousedown = (event: MouseEvent) => {
setSeparatorYPosition(event.clientY)
setDragging(true)
}
const onMouseMove: any = (e: MouseEvent) => {
e.preventDefault()
if (dragging && leftHeight && separatorYPosition) {
const newLeftHeight = leftHeight + separatorYPosition - e.clientY
setSeparatorYPosition(e.clientY)
setLeftHeight(newLeftHeight)
event.trigger('resize', [newLeftHeight + 32])
}
}
const onMouseUp = () => {
leftRef.current.style.backgroundColor = ''
leftRef.current.style.border = ''
setDragging(false)
}
/* end of mouse event */
useEffect(() => {
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
return () => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
}, [onMouseMove, onMouseUp])
React.useEffect(() => {
if (panelRef) {
if (!leftHeight) {
setLeftHeight(panelRef.current.offsetHeight)
return
}
panelRef.current.style.height = `${leftHeight}px`
}
}, [leftHeight, setLeftHeight, panelRef])
/* block contents that gets rendered from scriptRunner */
const _blocksRenderer = (mode) => {
@ -463,9 +423,9 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
return (
<div style={{ height: '323px', flexGrow: 1 }} className='panel' ref={panelRef}>
<div className="bar">
<div className="dragbarHorizontal" onMouseDown={mousedown} ref={leftRef}></div>
<div className="menu border-top border-dark bg-light" data-id="terminalToggleMenu">
<i className={`mx-2 toggleTerminal fas ${toggleDownUp}`} data-id="terminalToggleIcon" onClick={ handleMinimizeTerminal }></i>
<div className={`dragbarHorizontal ${isDragging ? 'dragbarDragging' : ''}`} onMouseDown={handleDraggingStart} ref={leftRef}></div>
<div className="menu border-top border-dark bg-light" ref={terminalMenu} data-id="terminalToggleMenu">
<i className={`mx-2 toggleTerminal fas ${isOpen ? 'fa-angle-double-down' : 'fa-angle-double-up'}`} data-id="terminalToggleIcon" onClick={handleToggleTerminal}></i>
<div className="mx-2 console" id="clearConsole" data-id="terminalClearConsole" onClick={handleClearConsole} >
<i className="fas fa-ban" aria-hidden="true" title="Clear console"
></i>
@ -558,6 +518,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => {
</div>
</div>
<ModalDialog
id='terminal'
title={ modalState.title }
message={ modalState.message }
hide={ modalState.hide }

@ -92,6 +92,7 @@ export const Toaster = (props: ToasterProps) => {
return (
<>
<ModalDialog
id='toaster'
message={props.message}
cancelLabel='Close'
cancelFn={() => {}}

@ -139,11 +139,17 @@
"remix-ui-vertical-icons-panel": {
"tags": []
},
"remix-ui-home-tab": {
"tags": []
},
"remix-ui-tabs": {
"tags": []
},
"remix-ui-theme-module": {
"tags": []
},
"remix-ui-editor-context-view": {
"tags": []
}
},
"targetDependencies": {

4242
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -87,6 +87,7 @@
"nightwatch_local_fileExplorer": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/fileExplorer.test.js --env=chrome",
"nightwatch_local_debugger": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/debugger_*.test.js --env=chrome",
"nightwatch_local_editor": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/editor.test.js --env=chrome",
"nightwatch_local_importFromGithub": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/importFromGithub.test.js --env=chrome",
"nightwatch_local_compiler": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/compiler_api.test.js --env=chrome",
"nightwatch_local_txListener": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/txListener.test.js --env=chrome",
"nightwatch_local_fileManager": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/fileManager_api.test.js --env=chrome",

@ -64,14 +64,14 @@
"@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/home-tab": ["libs/remix-ui/home-tab/src/index.ts"],
"@remix-ui/editor": ["libs/remix-ui/editor/src/index.ts"],
"@remix-ui/tabs": ["libs/remix-ui/tabs/src/index.ts"],
"@remix-ui/helper": ["libs/remix-ui/helper/src/index.ts"],
"@remix-ui/app": ["libs/remix-ui/app/src/index.ts"],
"@remix-ui/vertical-icons-panel": [
"libs/remix-ui/vertical-icons-panel/src/index.ts"
],
"@remix-ui/theme-module": ["libs/remix-ui/theme-module/src/index.ts"]
"@remix-ui/vertical-icons-panel": ["libs/remix-ui/vertical-icons-panel/src/index.ts"],
"@remix-ui/theme-module": ["libs/remix-ui/theme-module/src/index.ts"],
"@remix-ui/editor-context-view": ["libs/remix-ui/editor-context-view/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]

@ -1007,6 +1007,23 @@
}
}
},
"remix-ui-home-tab": {
"root": "libs/remix-ui/home-tab",
"sourceRoot": "libs/remix-ui/home-tab/src",
"projectType": "library",
"schematics": {},
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/home-tab/tsconfig.lib.json"],
"exclude": ["**/node_modules/**", "!libs/remix-ui/home-tab/**/*"]
}
}
}
},
"remix-ui-editor": {
"root": "libs/remix-ui/editor",
"sourceRoot": "libs/remix-ui/editor/src",
@ -1099,80 +1116,48 @@
}
}
}
},
"remix-ui-editor-context-view": {
"root": "libs/remix-ui/editor-context-view",
"sourceRoot": "libs/remix-ui/editor-context-view/src",
"projectType": "library",
"architect": {
"lint": {
"builder": "@nrwl/linter:lint",
"options": {
"linter": "eslint",
"tsConfig": ["libs/remix-ui/vertical-icons-panel/tsconfig.lib.json"],
"exclude": ["**/node_modules/**", "!libs/remix-ui/vertical-icons-panel/**/*"]
}
}
}
}
},
"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",
"babel": true
},
"component": {
"style": "css"
},
"library": {
"style": "css",
"linter": "eslint"
}
},
"cli": {
"defaultCollection": "@nrwl/react"
},
"schematics": {
"@nrwl/workspace": {
"library": {
"linter": "eslint"
}
},
"@nrwl/nx-plugin": {
"plugin": {
"@nrwl/cypress": {
"cypress-project": {
"linter": "eslint"
}
},
"@nrwl/web": {
"application": {
"linter": "eslint"
}
"@nrwl/react": {
"application": {
"style": "css",
"linter": "eslint",
"babel": true
},
"@nrwl/node": {
"application": {
"linter": "eslint"
},
"library": {
"linter": "eslint"
}
}
},
"cli": {
"defaultCollection": "@nrwl/react"
},
"schematics": {
"@nrwl/workspace": {
"library": {
"linter": "eslint"
}
},
"@nrwl/cypress": {
"cypress-project": {
"linter": "eslint"
}
},
"@nrwl/react": {
"application": {
"style": "css",
"linter": "eslint",
"babel": true
},
"component": {
"style": "css"
},
"library": {
"style": "css",
"linter": "eslint"
}
"component": {
"style": "css"
},
"library": {
"style": "css",
"linter": "eslint"
}
},
@ -1180,6 +1165,8 @@
"plugin": {
"linter": "eslint"
}
},
"defaultProject": "remix-ide"
}
}
},
"defaultProject": "remix-ide"
}

Loading…
Cancel
Save