diff --git a/apps/remix-ide-e2e/src/tests/url.spec.ts b/apps/remix-ide-e2e/src/tests/url.spec.ts index 13227238f7..56a94e34a9 100644 --- a/apps/remix-ide-e2e/src/tests/url.spec.ts +++ b/apps/remix-ide-e2e/src/tests/url.spec.ts @@ -104,6 +104,21 @@ module.exports = { .verify.elementPresent('#runs:disabled') .click('[for="optimize"') .verify.attributeEquals('#runs', 'value', '200') + }, + + 'Should load json files from link passed in remix URL': function (browser: NightwatchBrowser) { + browser + .url('http://localhost:8080/#optimize=false&runs=200&evmVersion=null&version=soljson-v0.6.12+commit.27d51765.js&url=https://raw.githubusercontent.com/EthVM/evm-source-verification/main/contracts/1/0x011e5846975c6463a8c6337eecf3cbf64e328884/input.json') + .refresh() + .pause(5000) + .waitForElementPresent('*[data-id="workspacesSelect"] option[value="code-sample"]') + .openFile('@openzeppelin') + .openFile('@openzeppelin/contracts') + .openFile('@openzeppelin/contracts/access') + .openFile('@openzeppelin/contracts/access/AccessControl.sol') + .openFile('contracts') + .openFile('contracts/governance') + .openFile('contracts/governance/UnionGovernor.sol') .end() } } diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index e7887df7e2..dcbe03bcc7 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -28,8 +28,6 @@ class Editor extends Plugin { // Init this.event = new EventManager() this.sessions = {} - this.sourceAnnotationsPerFile = {} - this.markerPerFile = {} this.readOnlySessions = {} this.previousInput = '' this.saveTimeout = null @@ -74,8 +72,6 @@ class Editor extends Plugin { editorAPI={state.api} themeType={state.currentThemeType} currentFile={state.currentFile} - sourceAnnotationsPerFile={state.sourceAnnotationsPerFile} - markerPerFile={state.markerPerFile} events={state.events} plugin={state.plugin} /> @@ -108,6 +104,10 @@ class Editor extends Plugin { } this.ref.gotoLine = (line, column) => this.gotoLine(line, column || 0) this.ref.getCursorPosition = () => this.getCursorPosition() + this.ref.addMarkerPerFile = (marker, filePath) => this.addMarkerPerFile(marker, filePath) + this.ref.addSourceAnnotationsPerFile = (annotation, filePath) => this.addSourceAnnotationsPerFile(annotation, filePath) + this.ref.clearDecorationsByPlugin = (filePath, plugin, typeOfDecoration) => this.clearDecorationsByPlugin(filePath, plugin, typeOfDecoration) + this.ref.keepDecorationsFor = (name, typeOfDecoration) => this.keepDecorationsFor(name, typeOfDecoration) }} id='editorView'> @@ -118,8 +118,6 @@ class Editor extends Plugin { api: this.api, currentThemeType: this.currentThemeType, currentFile: this.currentFile, - sourceAnnotationsPerFile: this.sourceAnnotationsPerFile, - markerPerFile: this.markerPerFile, events: this.events, plugin: this }) @@ -410,27 +408,12 @@ class Editor extends Plugin { if (filePath && !this.sessions[filePath]) throw new Error('file not found' + filePath) const path = filePath || this.currentFile - const currentAnnotations = this[typeOfDecoration][path] - if (!currentAnnotations) return - - const newAnnotations = [] - for (const annotation of currentAnnotations) { - if (annotation.from !== plugin) newAnnotations.push(annotation) - } - - this[typeOfDecoration][path] = newAnnotations - this.renderComponent() + this.api.clearDecorationsByPlugin(path, plugin, typeOfDecoration) } - keepDecorationsFor (name, typeOfDecoration) { + keepDecorationsFor (plugin, typeOfDecoration) { if (!this.currentFile) return - if (!this[typeOfDecoration][this.currentFile]) return - - const annotations = this[typeOfDecoration][this.currentFile] - for (const annotation of annotations) { - annotation.hide = annotation.from !== name - } - this.renderComponent() + this.api.keepDecorationsFor(this.currentFile, plugin, typeOfDecoration) } /** @@ -473,10 +456,16 @@ class Editor extends Plugin { const path = filePath || this.currentFile const { from } = this.currentRequest - if (!this[typeOfDecoration][path]) this[typeOfDecoration][path] = [] decoration.from = from - this[typeOfDecoration][path].push(decoration) - this.renderComponent() + + if (typeOfDecoration === 'markerPerFile') { + this.api.addMarkerPerFile(decoration, path) + return + } + if (typeOfDecoration === 'sourceAnnotationsPerFile') { + this.api.addSourceAnnotationsPerFile(decoration, path) + return + } } /** diff --git a/apps/remix-ide/src/app/files/fileManager.ts b/apps/remix-ide/src/app/files/fileManager.ts index 5ac6f8dd30..9c11b5863d 100644 --- a/apps/remix-ide/src/app/files/fileManager.ts +++ b/apps/remix-ide/src/app/files/fileManager.ts @@ -752,7 +752,7 @@ class FileManager extends Plugin { if (provider) { try{ const content = await provider.get(currentFile) - this.editor.setText(content) + if(content) this.editor.setText(content) }catch(error){ console.log(error) } diff --git a/apps/remix-ide/src/app/files/remixDProvider.js b/apps/remix-ide/src/app/files/remixDProvider.js index a36b6d8ec9..c52055e04f 100644 --- a/apps/remix-ide/src/app/files/remixDProvider.js +++ b/apps/remix-ide/src/app/files/remixDProvider.js @@ -98,20 +98,19 @@ module.exports = class RemixDProvider extends FileProvider { }) } - get (path, cb) { + async get (path, cb) { if (!this._isReady) return cb && cb('provider not ready') var unprefixedpath = this.removePrefix(path) - this._appManager.call('remixd', 'get', { path: unprefixedpath }) - .then((file) => { - this.filesContent[path] = file.content - if (file.readonly) { this._readOnlyFiles[path] = 1 } - cb(null, file.content) - }).catch((error) => { - if (error) console.log(error) - // display the last known content. - // TODO should perhaps better warn the user that the file is not synced. - return cb(null, this.filesContent[path]) - }) + try{ + const file = await this._appManager.call('remixd', 'get', { path: unprefixedpath }) + this.filesContent[path] = file.content + if (file.readonly) { this._readOnlyFiles[path] = 1 } + if(cb) cb(null, file.content) + return file.content + } catch(error) { + if (error) console.log(error) + if(cb) return cb(null, this.filesContent[path]) + } } async set (path, content, cb) { diff --git a/apps/remix-ide/src/app/tabs/settings-tab.tsx b/apps/remix-ide/src/app/tabs/settings-tab.tsx index 25ea5fb934..8440ffbfa7 100644 --- a/apps/remix-ide/src/app/tabs/settings-tab.tsx +++ b/apps/remix-ide/src/app/tabs/settings-tab.tsx @@ -47,11 +47,9 @@ module.exports = class SettingsTab extends ViewPlugin { } render() { - return ( -
+ return
- ); } updateComponent(state: any){ diff --git a/apps/remix-ide/src/app/tabs/test-tab.js b/apps/remix-ide/src/app/tabs/test-tab.js index c2af5ce7a7..7f634bd9d1 100644 --- a/apps/remix-ide/src/app/tabs/test-tab.js +++ b/apps/remix-ide/src/app/tabs/test-tab.js @@ -89,12 +89,12 @@ module.exports = class TestTab extends ViewPlugin { this.createTestLibs() }) - this.testRunner.event.on('compilationFinished', (success, data, source) => { + this.testRunner.event.on('compilationFinished', (success, data, source, input, version) => { if (success) { this.allFilesInvolved.push(...Object.keys(data.sources)) // forwarding the event to the appManager infra // This is listened by compilerArtefacts to show data while debugging - this.emit('compilationFinished', source.target, source, 'soljson', data) + this.emit('compilationFinished', source.target, source, 'soljson', data, input, version) } }) } diff --git a/apps/solidity-compiler/src/app/compiler-api.ts b/apps/solidity-compiler/src/app/compiler-api.ts index abfe0b377b..174d40f910 100644 --- a/apps/solidity-compiler/src/app/compiler-api.ts +++ b/apps/solidity-compiler/src/app/compiler-api.ts @@ -261,11 +261,11 @@ export const CompilerApiMixin = (Base) => class extends Base { this.on('fileManager', 'fileClosed', this.data.eventHandlers.onFileClosed) - this.data.eventHandlers.onCompilationFinished = (success, data, source) => { + this.data.eventHandlers.onCompilationFinished = (success, data, source, input, version) => { this.compileErrors = data if (success) { // forwarding the event to the appManager infra - this.emit('compilationFinished', source.target, source, 'soljson', data) + this.emit('compilationFinished', source.target, source, 'soljson', data, input, version) if (data.errors && data.errors.length > 0) { this.statusChanged({ key: data.errors.length, diff --git a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts index d35fa36141..96fe7358e5 100644 --- a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts +++ b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts @@ -24,12 +24,12 @@ export class CompilerArtefacts extends Plugin { } onActivation () { - const saveCompilationPerFileResult = (file, source, languageVersion, data) => { - this.compilersArtefactsPerFile[file] = new CompilerAbstract(languageVersion, data, source) + const saveCompilationPerFileResult = (file, source, languageVersion, data, input?) => { + this.compilersArtefactsPerFile[file] = new CompilerAbstract(languageVersion, data, source, input) } - this.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => { - this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source) + this.on('solidity', 'compilationFinished', (file, source, languageVersion, data, input, version) => { + this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source, input) saveCompilationPerFileResult(file, source, languageVersion, data) }) @@ -48,16 +48,24 @@ export class CompilerArtefacts extends Plugin { saveCompilationPerFileResult(file, source, languageVersion, data) }) - this.on('solidityUnitTesting', 'compilationFinished', (file, source, languageVersion, data) => { - this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source) - saveCompilationPerFileResult(file, source, languageVersion, data) + this.on('solidityUnitTesting', 'compilationFinished', (file, source, languageVersion, data, input, version) => { + this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source, input) + saveCompilationPerFileResult(file, source, languageVersion, data, input) }) } - + + /** + * Get artefacts for last compiled contract + * * @returns last compiled contract compiler abstract + */ getLastCompilationResult () { return this.compilersArtefacts.__last } + /** + * Get compilation output for contracts compiled during a session of Remix IDE + * @returns compilatin output + */ getAllContractDatas () { const contractsData = {} Object.keys(this.compilersArtefactsPerFile).map((targetFile) => { @@ -72,51 +80,85 @@ export class CompilerArtefacts extends Plugin { return contractsData } - async getArtefactsFromFE (path, contractName) { + /** + * Get a particular contract output/artefacts from a compiler output of a Solidity file compilation + * @param compilerOutput compiler output + * @param contractName contract name + * @returns arefacts object, with fully qualified name (e.g; contracts/1_Storage.sol:Storage) as key + */ + _getAllContractArtefactsfromOutput (compilerOutput, contractName) { + const contractArtefacts = {} + for (const filename in compilerOutput) { + if(Object.keys(compilerOutput[filename]).includes(contractName)) contractArtefacts[filename + ':' + contractName] = compilerOutput[filename][contractName] + } + return contractArtefacts + } + + /** + * Populate resultant object with a particular contract output/artefacts by processing all the artifacts stored in file explorer + * @param path path to start looking from + * @param contractName contract to be looked for + * @param contractArtefacts populated resultant artefacts object, with fully qualified name (e.g: contracts/1_Storage.sol:Storage) as key + * Once method execution completes, contractArtefacts object will hold all possible artefacts for contract + */ + async _populateAllContractArtefactsFromFE (path, contractName, contractArtefacts) { const dirList = await this.call('fileManager', 'dirList', path) if(dirList && dirList.length) { - if(dirList.includes(path + '/artifacts')) { - const fileList = await this.call('fileManager', 'fileList', path + '/artifacts') - const artefactsFilePaths = fileList.filter(filePath => { - const filenameArr = filePath.split('/') - const filename = filenameArr[filenameArr.length - 1] - if (filename === `${contractName}.json` || filename === `${contractName}_metadata.json`) return true - }) - if (artefactsFilePaths && artefactsFilePaths.length) { - const content = await this.call('fileManager', 'readFile', artefactsFilePaths[1]) - const artifacts = JSON.parse(content) - return { abi: artifacts.abi, bytecode: artifacts.data.bytecode.object } - } else { - for (const dirPath of dirList) { - const result = await this.getArtefactsFromFE (dirPath, contractName) - if (result) return result + for (const dirPath of dirList) { + // check if directory contains an 'artifacts' folder and a 'build-info' folder inside 'artifacts' + if(dirPath === path + '/artifacts' && await this.call('fileManager', 'exists', dirPath + '/build-info')) { + const buildFileList = await this.call('fileManager', 'fileList', dirPath + '/build-info') + // process each build-info file to populate the artefacts for contractName + for (const buildFile of buildFileList) { + let content = await this.call('fileManager', 'readFile', buildFile) + if (content) content = JSON.parse(content) + const compilerOutput = content.output.contracts + const artefacts = this._getAllContractArtefactsfromOutput(compilerOutput, contractName) + // populate the resultant object with artefacts + Object.assign(contractArtefacts, artefacts) } - } - } else { - for (const dirPath of dirList) { - const result = await this.getArtefactsFromFE (dirPath, contractName) - if (result) return result - } - } + } else await this._populateAllContractArtefactsFromFE (dirPath, contractName, contractArtefacts) + } } else return } - async getArtefactsByContractName (contractName) { + /** + * Get artefacts for a contract (called by script-runner) + * @param name contract name or fully qualified name i.e. : e.g: contracts/1_Storage.sol:Storage + * @returns artefacts for the contract + */ + async getArtefactsByContractName (name) { const contractsDataByFilename = this.getAllContractDatas() - const contractsData = Object.values(contractsDataByFilename) - if (contractsData && contractsData.length) { - const index = contractsData.findIndex((contractsObj) => Object.keys(contractsObj).includes(contractName)) - if (index !== -1) return { abi: contractsData[index][contractName].abi, bytecode: contractsData[index][contractName].evm.bytecode.object } + // check if name is a fully qualified name + if (name.includes(':')) { + const fullyQualifiedName = name + const nameArr = fullyQualifiedName.split(':') + const filename = nameArr[0] + const contract = nameArr[1] + if(Object.keys(contractsDataByFilename).includes(filename) && contractsDataByFilename[filename][contract]) + return contractsDataByFilename[filename][contract] else { - const result = await this.getArtefactsFromFE ('contracts', contractName) - if (result) return result - else throw new Error(`Could not find artifacts for ${contractName}. Compile contract to generate artifacts.`) + const allContractsData = {} + await this._populateAllContractArtefactsFromFE ('contracts', contract, allContractsData) + if(allContractsData[fullyQualifiedName]) return allContractsData[fullyQualifiedName] + else throw new Error(`Could not find artifacts for ${fullyQualifiedName}. Compile contract to generate artifacts.`) } } else { - const result = await this.getArtefactsFromFE ('contracts', contractName) - if (result) return result - else throw new Error(`Could not find artifacts for ${contractName}. Compile contract to generate artifacts.`) - } + const contractName = name + const contractArtefacts = this._getAllContractArtefactsfromOutput(contractsDataByFilename, contractName) + let keys = Object.keys(contractArtefacts) + if (!keys.length) { + await this._populateAllContractArtefactsFromFE ('contracts', contractName, contractArtefacts) + keys = Object.keys(contractArtefacts) + } + if (keys.length === 1) return contractArtefacts[keys[0]] + else if (keys.length > 1) { + throw new Error(`There are multiple artifacts for contract "${contractName}", please use a fully qualified name.\n + Please replace ${contractName} for one of these options wherever you are trying to read its artifact: \n + ${keys.join()}\n + OR just compile the required contract again`) + } else throw new Error(`Could not find artifacts for ${contractName}. Compile contract to generate artifacts.`) + } } getCompilerAbstract (file) { diff --git a/libs/remix-core-plugin/src/lib/compiler-metadata.ts b/libs/remix-core-plugin/src/lib/compiler-metadata.ts index dfd4ade1dd..e7c5f3eec6 100644 --- a/libs/remix-core-plugin/src/lib/compiler-metadata.ts +++ b/libs/remix-core-plugin/src/lib/compiler-metadata.ts @@ -1,6 +1,7 @@ 'use strict' import { Plugin } from '@remixproject/engine' import { CompilerAbstract } from '@remix-project/remix-solidity' +import { createHash } from 'crypto' const profile = { name: 'compilerMetadata', @@ -28,10 +29,11 @@ export class CompilerMetadata extends Plugin { onActivation () { const self = this - this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data) => { + this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data, input, version) => { if (!await this.call('settings', 'get', 'settings/generate-contract-metadata')) return - const compiler = new CompilerAbstract(languageVersion, data, source) + const compiler = new CompilerAbstract(languageVersion, data, source, input) const path = self._extractPathOf(source.target) + await this.setBuildInfo(version, input, data, path) compiler.visitContracts((contract) => { if (contract.file !== source.target) return (async () => { @@ -43,6 +45,23 @@ export class CompilerMetadata extends Plugin { }) } + async setBuildInfo (version, input, output, path) { + input = JSON.parse(input) + const solcLongVersion = version.replace('.Emscripten.clang', '') + const solcVersion = solcLongVersion.substring(0, solcLongVersion.indexOf('+commit')) + const format = 'hh-sol-build-info-1' + const json = JSON.stringify({ + _format: format, + solcVersion, + solcLongVersion, + input + }) + const id = createHash('md5').update(Buffer.from(json)).digest().toString('hex') + const buildFilename = this.joinPath(path, this.innerPath, 'build-info/' + id + '.json') + const buildData = {id, _format: format, solcVersion, solcLongVersion, input, output} + await this.call('fileManager', 'writeFile', buildFilename, JSON.stringify(buildData, null, '\t')) + } + _extractPathOf (file) { const reg = /(.*)(\/).*/ const path = reg.exec(file) @@ -51,14 +70,15 @@ export class CompilerMetadata extends Plugin { async _setArtefacts (content, contract, path) { content = content || '{}' + const fileName = this._JSONFileName(path, contract.name) + const metadataFileName = this._MetadataFileName(path, contract.name) + let metadata try { metadata = JSON.parse(content) } catch (e) { console.log(e) } - const fileName = this._JSONFileName(path, contract.name) - const metadataFileName = this._MetadataFileName(path, contract.name) const deploy = metadata.deploy || {} this.networks.forEach((network) => { diff --git a/libs/remix-core-plugin/src/lib/editor-context-listener.ts b/libs/remix-core-plugin/src/lib/editor-context-listener.ts index b20eb6e855..39b33e17b4 100644 --- a/libs/remix-core-plugin/src/lib/editor-context-listener.ts +++ b/libs/remix-core-plugin/src/lib/editor-context-listener.ts @@ -42,7 +42,7 @@ export class EditorContextListener extends Plugin { onActivation () { this.on('editor', 'contentChanged', () => { this._stopHighlighting() }) - this.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => { + this.on('solidity', 'compilationFinished', (file, source, languageVersion, data, input, version) => { if (languageVersion.indexOf('soljson') !== 0) return this._stopHighlighting() this._index = { diff --git a/libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts b/libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts index ccb201aa47..b6e02bbcdd 100644 --- a/libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts +++ b/libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts @@ -70,7 +70,7 @@ export class OffsetToLineColumnConverter extends Plugin { * called by plugin API */ activate () { - this.on('solidity', 'compilationFinished', () => { + this.on('solidity', 'compilationFinished', (success, data, source, input, version) => { this.clear() }) } diff --git a/libs/remix-debug/src/source/offsetToLineColumnConverter.ts b/libs/remix-debug/src/source/offsetToLineColumnConverter.ts index 298feaa1c6..6605b660b9 100644 --- a/libs/remix-debug/src/source/offsetToLineColumnConverter.ts +++ b/libs/remix-debug/src/source/offsetToLineColumnConverter.ts @@ -8,7 +8,7 @@ export class OffsetToColumnConverter { constructor (compilerEvent) { this.lineBreakPositionsByContent = {} if (compilerEvent) { - compilerEvent.register('compilationFinished', (success, data, source) => { + compilerEvent.register('compilationFinished', (success, data, source, input, version) => { this.clear() }) } diff --git a/libs/remix-solidity/src/compiler/compiler-abstract.ts b/libs/remix-solidity/src/compiler/compiler-abstract.ts index 81cd33db28..8a7c25f72f 100644 --- a/libs/remix-solidity/src/compiler/compiler-abstract.ts +++ b/libs/remix-solidity/src/compiler/compiler-abstract.ts @@ -5,10 +5,12 @@ export class CompilerAbstract { languageversion: any data: any source: any - constructor (languageversion, data, source) { + input: any + constructor (languageversion, data, source, input?) { this.languageversion = languageversion this.data = data this.source = source // source code + this.input = input // source code } getContracts () { @@ -27,6 +29,10 @@ export class CompilerAbstract { return this.data } + getInput () { + return this.input + } + getAsts () { return this.data.sources // ast } diff --git a/libs/remix-solidity/src/compiler/compiler-helpers.ts b/libs/remix-solidity/src/compiler/compiler-helpers.ts index 8a70e6be4f..ae7af0e0f1 100644 --- a/libs/remix-solidity/src/compiler/compiler-helpers.ts +++ b/libs/remix-solidity/src/compiler/compiler-helpers.ts @@ -12,8 +12,8 @@ export const compile = async (compilationTargets, settings, contentResolverCallb compiler.set('language', settings.language) compiler.set('runs', settings.runs) compiler.loadVersion(canUseWorker(settings.version), urlFromVersion(settings.version)) - compiler.event.register('compilationFinished', (success, compilationData, source) => { - resolve(new CompilerAbstract(settings.version, compilationData, source)) + compiler.event.register('compilationFinished', (success, compilationData, source, input, version) => { + resolve(new CompilerAbstract(settings.version, compilationData, source, input)) }) compiler.event.register('compilerLoaded', _ => compiler.compile(compilationTargets, '')) }) diff --git a/libs/remix-solidity/src/compiler/compiler-worker.ts b/libs/remix-solidity/src/compiler/compiler-worker.ts index 7685a766f3..662a03ec06 100644 --- a/libs/remix-solidity/src/compiler/compiler-worker.ts +++ b/libs/remix-solidity/src/compiler/compiler-worker.ts @@ -45,6 +45,7 @@ export default function (self) { // eslint-disable-line @typescript-eslint/expli cmd: 'compiled', job: data.job, data: compileJSON(data.input), + input: data.input, missingInputs: missingInputs }) } diff --git a/libs/remix-solidity/src/compiler/compiler.ts b/libs/remix-solidity/src/compiler/compiler.ts index 4a7903f5f5..dc2421ff9c 100644 --- a/libs/remix-solidity/src/compiler/compiler.ts +++ b/libs/remix-solidity/src/compiler/compiler.ts @@ -37,7 +37,7 @@ export class Compiler { } } - this.event.register('compilationFinished', (success: boolean, data: CompilationResult, source: SourceWithTarget) => { + this.event.register('compilationFinished', (success: boolean, data: CompilationResult, source: SourceWithTarget, input: string, version: string) => { if (success && this.state.compilationStartTime) { this.event.trigger('compilationDuration', [(new Date().getTime()) - this.state.compilationStartTime]) } @@ -70,7 +70,7 @@ export class Compiler { this.gatherImports(files, missingInputs, (error, input) => { if (error) { this.state.lastCompilationResult = null - this.event.trigger('compilationFinished', [false, { error: { formattedMessage: error, severity: 'error' } }, files]) + this.event.trigger('compilationFinished', [false, { error: { formattedMessage: error, severity: 'error' } }, files, input, this.state.currentVersion]) } else if (this.state.compileJSON && input) { this.state.compileJSON(input) } }) } @@ -111,16 +111,17 @@ export class Compiler { return { error: 'Deferred import' } } let result: CompilationResult = {} + let input try { if (source && source.sources) { const { optimize, runs, evmVersion, language } = this.state - const input = compilerInput(source.sources, { optimize, runs, evmVersion, language }) + input = compilerInput(source.sources, { optimize, runs, evmVersion, language }) result = JSON.parse(compiler.compile(input, { import: missingInputsCallback })) } } catch (exception) { result = { error: { formattedMessage: 'Uncaught JavaScript exception:\n' + exception, severity: 'error', mode: 'panic' } } } - this.onCompilationFinished(result, missingInputs, source) + this.onCompilationFinished(result, missingInputs, source, input, this.state.currentVersion) } this.onCompilerLoaded(compiler.version()) } @@ -133,7 +134,7 @@ export class Compiler { * @param source Source */ - onCompilationFinished (data: CompilationResult, missingInputs?: string[], source?: SourceWithTarget): void { + onCompilationFinished (data: CompilationResult, missingInputs?: string[], source?: SourceWithTarget, input?: string, version?: string): void { let noFatalErrors = true // ie warnings are ok const checkIfFatalError = (error: CompilationError) => { @@ -146,7 +147,7 @@ export class Compiler { if (!noFatalErrors) { // There are fatal errors, abort here this.state.lastCompilationResult = null - this.event.trigger('compilationFinished', [false, data, source]) + this.event.trigger('compilationFinished', [false, data, source, input, version]) } else if (missingInputs !== undefined && missingInputs.length > 0 && source && source.sources) { // try compiling again with the new set of inputs this.internalCompile(source.sources, missingInputs) @@ -159,7 +160,7 @@ export class Compiler { source: source } } - this.event.trigger('compilationFinished', [true, data, source]) + this.event.trigger('compilationFinished', [true, data, source, input, version]) } } @@ -182,16 +183,17 @@ export class Compiler { return { error: 'Deferred import' } } let result: CompilationResult = {} + let input: string try { if (source && source.sources) { const { optimize, runs, evmVersion, language } = this.state - const input = compilerInput(source.sources, { optimize, runs, evmVersion, language }) + input = compilerInput(source.sources, { optimize, runs, evmVersion, language }) result = JSON.parse(remoteCompiler.compile(input, { import: missingInputsCallback })) } } catch (exception) { result = { error: { formattedMessage: 'Uncaught JavaScript exception:\n' + exception, severity: 'error', mode: 'panic' } } } - this.onCompilationFinished(result, missingInputs, source) + this.onCompilationFinished(result, missingInputs, source, input, version) } this.onCompilerLoaded(version) } @@ -273,7 +275,7 @@ export class Compiler { sources = jobs[data.job].sources delete jobs[data.job] } - this.onCompilationFinished(result, data.missingInputs, sources) + this.onCompilationFinished(result, data.missingInputs, sources, data.input, this.state.currentVersion) } break } diff --git a/libs/remix-solidity/src/compiler/types.ts b/libs/remix-solidity/src/compiler/types.ts index 14751cfabc..709c0c97df 100644 --- a/libs/remix-solidity/src/compiler/types.ts +++ b/libs/remix-solidity/src/compiler/types.ts @@ -186,6 +186,7 @@ export interface MessageFromWorker { cmd: string, job?: number, missingInputs?: string[], + input?: any, data?: string } diff --git a/libs/remix-tests/src/compiler.ts b/libs/remix-tests/src/compiler.ts index 70487c97f3..697483d593 100644 --- a/libs/remix-tests/src/compiler.ts +++ b/libs/remix-tests/src/compiler.ts @@ -142,7 +142,7 @@ export function compileFileOrFiles (filename: string, isDirectory: boolean, opts }, function doCompilation (next) { // @ts-ignore - compiler.event.register('compilationFinished', this, (success, data, source) => { + compiler.event.register('compilationFinished', this, (success, data, source, input, version) => { next(null, data) }) compiler.compile(sources, filepath) @@ -201,10 +201,10 @@ export function compileContractSources (sources: SrcIfc, newCompConfig: any, imp } }, (next) => { - const compilationFinishedCb = (success, data, source) => { + const compilationFinishedCb = (success, data, source, input, version) => { // data.error usually exists for exceptions like worker error etc. if (!data.error) UTRunner.compiler = compiler - if (opts && opts.event) opts.event.emit('compilationFinished', success, data, source) + if (opts && opts.event) opts.event.emit('compilationFinished', success, data, source, input, version) next(null, data) } compiler.event.unregister('compilationFinished', compilationFinishedCb) 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 f268b16a71..8452fa2124 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -38,14 +38,6 @@ type sourceMarker = { hide: boolean } -type sourceAnnotationMap = { - [key: string]: [sourceAnnotation]; -} - -type sourceMarkerMap = { - [key: string]: [sourceMarker]; -} - loader.config({ paths: { vs: 'assets/js/monaco-editor/dev/vs' } }) /* eslint-disable-next-line */ @@ -54,8 +46,6 @@ export interface EditorUIProps { activated: boolean themeType: string currentFile: string - sourceAnnotationsPerFile: sourceAnnotationMap - markerPerFile: sourceMarkerMap events: { onBreakPointAdded: (file: string, line: number) => void onBreakPointCleared: (file: string, line: number) => void @@ -71,17 +61,21 @@ export interface EditorUIProps { getFontSize: () => number, getValue: (uri: string) => string getCursorPosition: () => cursorPosition + addMarkerPerFile: (marker: sourceMarker, filePath: string) => void + addSourceAnnotationsPerFile: (annotations: sourceAnnotation, filePath: string) => void + clearDecorationsByPlugin: (filePath: string, plugin: string, typeOfDecoration: string) => void + keepDecorationsFor: (filePath: string, plugin: string, typeOfDecoration: string) => void } } export const EditorUI = (props: EditorUIProps) => { const [, setCurrentBreakpoints] = useState({}) - const [currentAnnotations, setCurrentAnnotations] = useState({}) - const [currentMarkers, setCurrentMarkers] = useState({}) const editorRef = useRef(null) const monacoRef = useRef(null) const currentFileRef = useRef('') - + const currentDecorations = useRef({ sourceAnnotationsPerFile: {}, markerPerFile: {} }) // decorations that are currently in use by the editor + const registeredDecorations = useRef({}) // registered decorations + const [editorModelsState, dispatch] = useReducer(reducerActions, initialState) const formatColor = (name) => { @@ -226,55 +220,6 @@ export const EditorUI = (props: EditorUIProps) => { defineAndSetTheme(monacoRef.current) }) - const setAnnotationsbyFile = (uri) => { - if (props.sourceAnnotationsPerFile[uri]) { - const model = editorModelsState[uri]?.model - const newAnnotations = [] - for (const annotation of props.sourceAnnotationsPerFile[uri]) { - if (!annotation.hide) { - newAnnotations.push({ - range: new monacoRef.current.Range(annotation.row + 1, 1, annotation.row + 1, 1), - options: { - isWholeLine: false, - glyphMarginHoverMessage: { value: (annotation.from ? `from ${annotation.from}:\n` : '') + annotation.text }, - glyphMarginClassName: `fal fa-exclamation-square text-${annotation.type === 'error' ? 'danger' : (annotation.type === 'warning' ? 'warning' : 'info')}` - } - }) - } - } - setCurrentAnnotations(prevState => { - prevState[uri] = model.deltaDecorations(currentAnnotations[uri] || [], newAnnotations) - return prevState - }) - } - } - - const setMarkerbyFile = (uri) => { - if (props.markerPerFile[uri]) { - const model = editorModelsState[uri]?.model - const newMarkers = [] - for (const marker of props.markerPerFile[uri]) { - if (!marker.hide) { - let isWholeLine = false - if (marker.position.start.line === marker.position.end.line && marker.position.end.column - marker.position.start.column < 3) { - // in this case we force highlighting the whole line (doesn't make sense to highlight 2 chars) - isWholeLine = true - } - newMarkers.push({ - range: new monacoRef.current.Range(marker.position.start.line + 1, marker.position.start.column + 1, marker.position.end.line + 1, marker.position.end.column + 1), - options: { - isWholeLine, - inlineClassName: `alert-info border-0 highlightLine${marker.position.start.line + 1}` - } - }) - } - } - setCurrentMarkers(prevState => { - prevState[uri] = model.deltaDecorations(currentMarkers[uri] || [], newMarkers) - return prevState - }) - } - } useEffect(() => { if (!editorRef.current) return @@ -286,18 +231,91 @@ export const EditorUI = (props: EditorUIProps) => { 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]) - useEffect(() => { - setAnnotationsbyFile(props.currentFile) - }, [JSON.stringify(props.sourceAnnotationsPerFile)]) + const convertToMonacoDecoration = (decoration: sourceAnnotation | sourceMarker, typeOfDecoration: string) => { + if (typeOfDecoration === 'sourceAnnotationsPerFile') { + decoration = decoration as sourceAnnotation + return { + type: typeOfDecoration, + range: new monacoRef.current.Range(decoration.row + 1, 1, decoration.row + 1, 1), + options: { + isWholeLine: false, + glyphMarginHoverMessage: { value: (decoration.from ? `from ${decoration.from}:\n` : '') + decoration.text }, + glyphMarginClassName: `fal fa-exclamation-square text-${decoration.type === 'error' ? 'danger' : (decoration.type === 'warning' ? 'warning' : 'info')}` + } + } + } + if (typeOfDecoration === 'markerPerFile') { + decoration = decoration as sourceMarker + let isWholeLine = false + if (decoration.position.start.line === decoration.position.end.line && decoration.position.end.column - decoration.position.start.column < 3) { + // in this case we force highlighting the whole line (doesn't make sense to highlight 2 chars) + isWholeLine = true + } + return { + type: typeOfDecoration, + range: new monacoRef.current.Range(decoration.position.start.line + 1, decoration.position.start.column + 1, decoration.position.end.line + 1, decoration.position.end.column + 1), + options: { + isWholeLine, + inlineClassName: `alert-info border-0 highlightLine${decoration.position.start.line + 1}` + } + } + } + } + + props.editorAPI.clearDecorationsByPlugin = (filePath: string, plugin: string, typeOfDecoration: string) => { + const model = editorModelsState[filePath]?.model + if (!model) return + const decorations = [] + const newRegisteredDecorations = [] + if (registeredDecorations.current[filePath]) { + for (const decoration of registeredDecorations.current[filePath]) { + if (decoration.type === typeOfDecoration && decoration.value.from !== plugin) { + decorations.push(convertToMonacoDecoration(decoration.value, typeOfDecoration)) + newRegisteredDecorations.push(decoration) + } + } + } + currentDecorations.current[typeOfDecoration][filePath] = model.deltaDecorations(currentDecorations.current[typeOfDecoration][filePath], decorations) + registeredDecorations.current[filePath] = newRegisteredDecorations + } + + props.editorAPI.keepDecorationsFor = (filePath: string, plugin: string, typeOfDecoration: string) => { + const model = editorModelsState[filePath]?.model + if (!model) return + const decorations = [] + if (registeredDecorations.current[filePath]) { + for (const decoration of registeredDecorations.current[filePath]) { + if (decoration.type === typeOfDecoration && decoration.value.from === plugin) { + decorations.push(convertToMonacoDecoration(decoration.value, typeOfDecoration)) + } + } + } + currentDecorations.current[typeOfDecoration][filePath] = model.deltaDecorations(currentDecorations.current[typeOfDecoration][filePath], decorations) + } + + const addDecoration = (decoration: sourceAnnotation | sourceMarker, filePath: string, typeOfDecoration: string) => { + const model = editorModelsState[filePath]?.model + if (!model) return + const monacoDecoration = convertToMonacoDecoration(decoration, typeOfDecoration) + + if (!registeredDecorations.current[filePath]) registeredDecorations.current[filePath] = [] + registeredDecorations.current[filePath].push({ value: decoration, type: typeOfDecoration }) + if (!currentDecorations.current[typeOfDecoration][filePath]) currentDecorations.current[typeOfDecoration][filePath] = [] + + currentDecorations.current[typeOfDecoration][filePath].push(...model.deltaDecorations([], [monacoDecoration])) + } + + props.editorAPI.addSourceAnnotationsPerFile = (annotation: sourceAnnotation, filePath: string) => { + addDecoration(annotation, filePath, 'sourceAnnotationsPerFile') + } + + props.editorAPI.addMarkerPerFile = (marker: sourceMarker, filePath: string) => { + addDecoration(marker, filePath, 'markerPerFile') + } - useEffect(() => { - setMarkerbyFile(props.currentFile) - }, [JSON.stringify(props.markerPerFile)]) props.editorAPI.findMatches = (uri: string, value: string) => { if (!editorRef.current) return diff --git a/libs/remix-ui/run-tab/src/lib/actions/index.ts b/libs/remix-ui/run-tab/src/lib/actions/index.ts index 18635bb1d7..464a68eed0 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/index.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/index.ts @@ -66,7 +66,7 @@ const setupEvents = () => { plugin.on('manager', 'pluginDeactivated', removePluginProvider.bind(plugin)) - plugin.on('solidity', 'compilationFinished', (file, source, languageVersion, data) => broadcastCompilationResult(file, source, languageVersion, data)) + plugin.on('solidity', 'compilationFinished', (file, source, languageVersion, data, input, version) => broadcastCompilationResult(file, source, languageVersion, data, input)) plugin.on('vyper', 'compilationFinished', (file, source, languageVersion, data) => broadcastCompilationResult(file, source, languageVersion, data)) @@ -301,9 +301,9 @@ export const signMessageWithAddress = (account: string, message: string, modalCo }) } -const broadcastCompilationResult = (file, source, languageVersion, data) => { +const broadcastCompilationResult = (file, source, languageVersion, data, input?) => { // TODO check whether the tab is configured - const compiler = new CompilerAbstract(languageVersion, data, source) + const compiler = new CompilerAbstract(languageVersion, data, source, input) plugin.compilersArtefacts[languageVersion] = compiler plugin.compilersArtefacts.__last = compiler diff --git a/libs/remix-ui/solidity-compiler/src/lib/actions/compiler.ts b/libs/remix-ui/solidity-compiler/src/lib/actions/compiler.ts index bc43ed73b1..c67aee6ddb 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/actions/compiler.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/actions/compiler.ts @@ -50,7 +50,7 @@ export const listenToEvents = (compileTabLogic: CompileTabLogic, api) => (dispat dispatch(setCompilerMode('compilerLoaded')) }) - compileTabLogic.compiler.event.register('compilationFinished', (success, data, source) => { - dispatch(setCompilerMode('compilationFinished', success, data, source)) + compileTabLogic.compiler.event.register('compilationFinished', (success, data, source, input, version) => { + dispatch(setCompilerMode('compilationFinished', success, data, source, input, version)) }) } diff --git a/libs/remix-ui/static-analyser/src/lib/actions/staticAnalysisActions.ts b/libs/remix-ui/static-analyser/src/lib/actions/staticAnalysisActions.ts index 4f55437cb6..c8ce131cee 100644 --- a/libs/remix-ui/static-analyser/src/lib/actions/staticAnalysisActions.ts +++ b/libs/remix-ui/static-analyser/src/lib/actions/staticAnalysisActions.ts @@ -5,9 +5,9 @@ export const compilation = (analysisModule, dispatch) => { analysisModule.on( 'solidity', 'compilationFinished', - (file, source, languageVersion, data) => { + (file, source, languageVersion, data, input, version) => { if (languageVersion.indexOf('soljson') !== 0) return - dispatch({ type: 'compilationFinished', payload: { file, source, languageVersion, data } }) + dispatch({ type: 'compilationFinished', payload: { file, source, languageVersion, data, input, version } }) } ) } diff --git a/libs/remix-ui/static-analyser/src/lib/reducers/staticAnalysisReducer.ts b/libs/remix-ui/static-analyser/src/lib/reducers/staticAnalysisReducer.ts index eb59ca7872..24c9a25868 100644 --- a/libs/remix-ui/static-analyser/src/lib/reducers/staticAnalysisReducer.ts +++ b/libs/remix-ui/static-analyser/src/lib/reducers/staticAnalysisReducer.ts @@ -13,7 +13,9 @@ export const analysisReducer = (state, action) => { file: action.payload.file, source: action.payload.source, languageVersion: action.payload.languageVersion, - data: action.payload.data + data: action.payload.data, + input: action.payload.input, + version: action.payload.version } default: return initialState diff --git a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx index 73b5b3e424..904685f7a5 100644 --- a/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx +++ b/libs/remix-ui/terminal/src/lib/remix-ui-terminal.tsx @@ -544,7 +544,7 @@ export const RemixUiTerminal = (props: RemixUiTerminalProps) => { ) } else { return ( -
{ msg ? msg.toString().replace(/,/g, '') : msg }
+
{msg? msg.toString() : null}
) } })