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..1cb3b1ffa5 100644 --- a/apps/remix-ide/src/app/panels/main-view.js +++ b/apps/remix-ide/src/app/panels/main-view.js @@ -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`
${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) 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..7bde42de11 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: [], + 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 + 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..e0c0898868 --- /dev/null +++ b/libs/remix-ui/editor-context-view/src/lib/remix-ui-editor-context-view.tsx @@ -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, + typeDescriptions: any, + nodeType: String, + src: any, + nodeId: any, + position: any +} + +export type onContextListenerChangedListener = (nodes: Array) => 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 + getActiveHighlights: () => Array + 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, + references: Array, + activeHighlights: Array + currentNode: nullableAstNode, + gasEstimation: gasEstimationType + }>({ + nodes: [], + references: [], + activeHighlights: [], + currentNode: null, + gasEstimation: { executionCost: '', codeDepositCost: '' } + }) + + useEffect(() => { + props.onContextListenerChanged(async (nodes: Array) => { + 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 ( +
+ + {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 _render = (node: nullableAstNode) => { + if (!node) return (
) + 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 = 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 ( +
{gasEstimation(node)} +
{type}
+
{node.name}
+ + {referencesCount} + + +
+ ) + } + + return ( + !props.hide &&
+ {_render(state.currentNode)} +
+ ) +} + +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/libs/remix-ui/editor/src/lib/remix-ui-editor.css b/libs/remix-ui/editor/src/lib/remix-ui-editor.css index a487d143f8..af7cd06bfd 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.css +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.css @@ -8,4 +8,9 @@ border-radius : 10px; height: auto; width: auto; +} + +.contextview { + opacity: 1; + position: absolute; } \ No newline at end of file diff --git a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx index 480b548be9..67df7cd443 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -1,4 +1,5 @@ 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' @@ -49,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 @@ -62,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 @@ -207,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) @@ -256,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}` } }) } @@ -377,15 +385,31 @@ export const EditorUI = (props: EditorUIProps) => { } return ( - +
+ +
+ 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) }} + /> +
+
) } diff --git a/nx.json b/nx.json index 4904941579..5a3da940bf 100644 --- a/nx.json +++ b/nx.json @@ -150,6 +150,9 @@ }, "remix-ui-theme-module": { "tags": [] + }, + "remix-ui-editor-context-view": { + "tags": [] } }, "targetDependencies": { diff --git a/tsconfig.base.json b/tsconfig.base.json index 1fd4cfdac0..bc3d4dd009 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -73,7 +73,8 @@ "libs/remix-ui/vertical-icons-panel/src/index.ts" ], "@remix-ui/theme-module": ["libs/remix-ui/theme-module/src/index.ts"], - "@remix-ui/panel": ["libs/remix-ui/panel/src/index.ts"] + "@remix-ui/panel": ["libs/remix-ui/panel/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 7873b49153..d4aca08282 100644 --- a/workspace.json +++ b/workspace.json @@ -1131,80 +1131,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/editor-context-view/tsconfig.lib.json"], + "exclude": ["**/node_modules/**", "!libs/remix-ui/editor-context-view/**/*"] + } + } + } } }, - "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" } }, @@ -1212,6 +1180,8 @@ "plugin": { "linter": "eslint" } - }, - "defaultProject": "remix-ide" -} \ No newline at end of file + } + }, + "defaultProject": "remix-ide" +} +