diff --git a/apps/remix-ide-e2e/src/tests/editor.spec.ts b/apps/remix-ide-e2e/src/tests/editor.test.ts similarity index 71% rename from apps/remix-ide-e2e/src/tests/editor.spec.ts rename to apps/remix-ide-e2e/src/tests/editor.test.ts index f0e88fe5cc..7b714a432b 100644 --- a/apps/remix-ide-e2e/src/tests/editor.spec.ts +++ b/apps/remix-ide-e2e/src/tests/editor.test.ts @@ -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() } } diff --git a/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts b/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts index 267daa9e1c..663c1bb791 100644 --- a/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts +++ b/apps/remix-ide-e2e/src/tests/fileExplorer.test.ts @@ -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() diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index e569cc5e15..d2b8a690ae 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -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, diff --git a/apps/remix-ide/src/app/editor/contextView.js b/apps/remix-ide/src/app/editor/contextView.js deleted file mode 100644 index da01321762..0000000000 --- a/apps/remix-ide/src/app/editor/contextView.js +++ /dev/null @@ -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` -
-
- ${this._renderTarget()} -
-
` - 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`
` - 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` -
- - ${estimatedGas} -
- ` - } - } - - return yo` -
${showGasEstimation()} -
${type}
-
${node.name}
- - ${references} - - -
- ` - } -} - -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 diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 295d83fd59..013b1b9853 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -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 { @@ -75,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 } diff --git a/apps/remix-ide/src/app/panels/main-view.js b/apps/remix-ide/src/app/panels/main-view.js index 537b296796..cad19a58f3 100644 --- a/apps/remix-ide/src/app/panels/main-view.js +++ b/apps/remix-ide/src/app/panels/main-view.js @@ -1,11 +1,14 @@ + +import React from 'react' // eslint-disable-line +import ReactDOM from 'react-dom' +import { RemixUiEditorContextView } from '@remix-ui/editor-context-view' + var yo = require('yo-yo') 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` @@ -15,6 +18,10 @@ var css = csjs` height : 100%; width : 100%; } + .contextview { + opacity : 1; + position : relative; + } ` // @todo(#650) Extract this into two classes: MainPanel (TabsProxy + Iframe/Editor) & BottomPanel (Terminal) @@ -25,12 +32,13 @@ export class MainView { self._view = {} self._components = {} self._components.registry = globalRegistry + self.contextualListener = contextualListener + self.hideContextView = false 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 +47,8 @@ export class MainView { this.fileManager.unselectCurrentFile() this.mainPanel.showContent(name) this._view.editor.style.display = 'none' - this._components.contextView.hide() + this.hideContextView = true + this.renderContextView() this._view.mainPanel.style.display = 'block' } @@ -63,19 +72,22 @@ export class MainView { // we check upstream for "fileChanged" self._view.editor.style.display = 'block' self._view.mainPanel.style.display = 'none' - self._components.contextView.show() + this.hideContextView = false + this.renderContextView() }) self.tabProxy.event.on('openFile', (file) => { self._view.editor.style.display = 'block' self._view.mainPanel.style.display = 'none' - self._components.contextView.show() + this.hideContextView = false + this.renderContextView() }) 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() + this.hideContextView = false + this.renderContextView() self._view.mainPanel.style.display = 'none' }) self.tabProxy.event.on('tabCountChanged', (count) => { @@ -90,10 +102,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 +189,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`
${self.tabProxy.renderTabsbar()} ${self._view.editor} ${self._view.mainPanel} - ${self._components.contextView.render()} +
${self._view.terminal}
` + // INIT self._adjustLayout('top', self.data._layout.top.offset) @@ -200,6 +210,22 @@ export class MainView { return self._view.mainview } + renderContextView () { + if (!this.contextualListener.activated) return + + ReactDOM.render( + this.contextualListener.call('editor', 'gotoLine', line, column)} + openFile={(file) => this.contextualListener.call('editor', 'openFile', file)} + getLastCompilationResult={_ => { return this.contextualListener.call('compilerArtefacts', 'getLastCompilationResult') } } + offsetToLineColumn={(position, file, sources, asts) => { return this.contextualListener.call('offsetToLineColumnConverter', 'offsetToLineColumn', position, file, sources, asts) } } + getCurrentFileName={() => { return this.contextualListener.call('fileManager', 'file') } } + /> + , this._view.mainview.querySelector('.contextview')) + } + registerCommand (name, command, opts) { var self = this return self._components.terminal.registerCommand(name, command, opts) diff --git a/libs/remix-core-plugin/src/index.ts b/libs/remix-core-plugin/src/index.ts index 73c98a6181..fe8a5c661e 100644 --- a/libs/remix-core-plugin/src/index.ts +++ b/libs/remix-core-plugin/src/index.ts @@ -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' diff --git a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts index 148aa292c7..715bd2e9c4 100644 --- a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts +++ b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts @@ -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) => { diff --git a/apps/remix-ide/src/app/editor/contextualListener.js b/libs/remix-core-plugin/src/lib/editor-context-listener.ts similarity index 64% rename from apps/remix-ide/src/app/editor/contextualListener.js rename to libs/remix-core-plugin/src/lib/editor-context-listener.ts index 9eea6aeaed..72ccee7457 100644 --- a/apps/remix-ide/src/app/editor/contextualListener.js +++ b/libs/remix-core-plugin/src/lib/editor-context-listener.ts @@ -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: [], 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 + astWalker: any + currentPosition: any + currentFile: String + nodes: Array + 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 diff --git a/libs/remix-ui/editor-context-view/.babelrc b/libs/remix-ui/editor-context-view/.babelrc new file mode 100644 index 0000000000..09d67939cc --- /dev/null +++ b/libs/remix-ui/editor-context-view/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@nrwl/react/babel"], + "plugins": [] +} diff --git a/libs/remix-ui/editor-context-view/.eslintrc b/libs/remix-ui/editor-context-view/.eslintrc new file mode 100644 index 0000000000..9d709f91d0 --- /dev/null +++ b/libs/remix-ui/editor-context-view/.eslintrc @@ -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" + } +} diff --git a/libs/remix-ui/editor-context-view/README.md b/libs/remix-ui/editor-context-view/README.md new file mode 100644 index 0000000000..0b9719fedb --- /dev/null +++ b/libs/remix-ui/editor-context-view/README.md @@ -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). diff --git a/libs/remix-ui/editor-context-view/src/index.ts b/libs/remix-ui/editor-context-view/src/index.ts new file mode 100644 index 0000000000..b595accc41 --- /dev/null +++ b/libs/remix-ui/editor-context-view/src/index.ts @@ -0,0 +1 @@ +export * from './lib/remix-ui-editor-context-view'; diff --git a/apps/remix-ide/src/app/editor/styles/contextView-styles.js b/libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.css similarity index 70% rename from apps/remix-ide/src/app/editor/styles/contextView-styles.js rename to libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.css index 5303a356fa..f57c88f78f 100644 --- a/apps/remix-ide/src/app/editor/styles/contextView-styles.js +++ b/libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.css @@ -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 + } \ No newline at end of file diff --git a/libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.tsx b/libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.tsx new file mode 100644 index 0000000000..51b6f6bf9d --- /dev/null +++ b/libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.tsx @@ -0,0 +1,159 @@ +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 interface RemixUiEditorContextViewProps { + hide: boolean, + contextualListener: any, + gotoLine: (line: number, column: number) => void, + openFile: (fileName: string) => void, + getLastCompilationResult: () => any, + offsetToLineColumn: (position: any, file: any, sources: any, asts: any) => any, + getCurrentFileName: () => String +} + +function isDefinition (node: any) { + return node.nodeType === 'ContractDefinition' || + node.nodeType === 'FunctionDefinition' || + node.nodeType === 'ModifierDefinition' || + node.nodeType === 'VariableDeclaration' || + node.nodeType === 'StructDefinition' || + node.nodeType === 'EventDefinition' +} + +type astNode = { + name: string, + id: number, + children: Array, + typeDescriptions: any, + nodeType: String, + src: any, + nodeId: any, + position: any +} + +type nullableAstNode = astNode | null + +export function RemixUiEditorContextView(props: RemixUiEditorContextViewProps) { + const nodesRef = useRef>([]) + /* + 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 [nodesState, setNode] = useState>([]) + const contextualListener = props.contextualListener + + useEffect(() => { + contextualListener.on('contextualListener', 'contextChanged', (nodes: Array) => { + if (gotoLineDisableRef.current) { + gotoLineDisableRef.current = false + return + } + nodesRef.current = nodes + setNode(nodes) + }) + }, []) + + const _render = (node: nullableAstNode) => { + if (!node) return (
) + let references = 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: Array = contextualListener.getActiveHighlights() + + /* + * show gas estimation + */ + const gasEstimation = () => { + if (node.nodeType === 'FunctionDefinition') { + const result = 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 ( +
+ + {estimatedGas} +
+ ) + } else { + return (
) + } + } + + /* + * 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 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 ( +
{gasEstimation()} +
{type}
+
{node.name}
+ + {references} + + +
+ ) + } + + let last: nullableAstNode = null + if (!props.hide && nodesRef.current && nodesRef.current.length) { + last = nodesRef.current[nodesRef.current.length - 1] + if (!isDefinition(last)) { + last = contextualListener.declarationOf(last) + } + } + + return ( + !props.hide &&
+ {_render(last)} +
+ ); +} + +export default RemixUiEditorContextView diff --git a/libs/remix-ui/editor-context-view/tsconfig.json b/libs/remix-ui/editor-context-view/tsconfig.json new file mode 100644 index 0000000000..d52e31ad74 --- /dev/null +++ b/libs/remix-ui/editor-context-view/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/remix-ui/editor-context-view/tsconfig.lib.json b/libs/remix-ui/editor-context-view/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/editor-context-view/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"] +} diff --git a/nx.json b/nx.json index 8bc14e4e9d..a1a7131ff6 100644 --- a/nx.json +++ b/nx.json @@ -147,6 +147,9 @@ }, "remix-ui-theme-module": { "tags": [] + }, + "remix-ui-editor-context-view": { + "tags": [] } }, "targetDependencies": { diff --git a/tsconfig.base.json b/tsconfig.base.json index 3633434dce..68e1ae5175 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -69,10 +69,9 @@ "@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"] diff --git a/workspace.json b/workspace.json index 8ce5f9f9ce..8850066499 100644 --- a/workspace.json +++ b/workspace.json @@ -1116,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/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 }, - "@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" } }, @@ -1199,4 +1167,6 @@ } }, "defaultProject": "remix-ide" -} \ No newline at end of file + } +} +