commit
bef4c60909
@ -1,702 +0,0 @@ |
||||
/* global FileReader */ |
||||
/* global fetch */ |
||||
const async = require('async') |
||||
const Gists = require('gists') |
||||
const modalDialogCustom = require('../ui/modal-dialog-custom') |
||||
const tooltip = require('../ui/tooltip') |
||||
const QueryParams = require('../../lib/query-params') |
||||
const helper = require('../../lib/helper') |
||||
const yo = require('yo-yo') |
||||
const Treeview = require('../ui/TreeView') |
||||
const modalDialog = require('../ui/modaldialog') |
||||
const EventManager = require('events') |
||||
const contextMenu = require('../ui/contextMenu') |
||||
const css = require('./styles/file-explorer-styles') |
||||
const globalRegistry = require('../../global/registry') |
||||
const queryParams = new QueryParams() |
||||
let MENU_HANDLE |
||||
|
||||
function fileExplorer (localRegistry, files, menuItems, plugin) { |
||||
var self = this |
||||
this.events = new EventManager() |
||||
// file provider backend
|
||||
this.files = files |
||||
// element currently focused on
|
||||
this.focusElement = null |
||||
// path currently focused on
|
||||
this.focusPath = null |
||||
const allItems = |
||||
[ |
||||
{ |
||||
action: 'createNewFile', |
||||
title: 'Create New File', |
||||
icon: 'fas fa-plus-circle' |
||||
}, |
||||
{ |
||||
action: 'publishToGist', |
||||
title: 'Publish all [browser] explorer files to a github gist', |
||||
icon: 'fab fa-github' |
||||
}, |
||||
{ |
||||
action: 'uploadFile', |
||||
title: 'Add Local file to the Browser Storage Explorer', |
||||
icon: 'far fa-folder-open' |
||||
}, |
||||
{ |
||||
action: 'updateGist', |
||||
title: 'Update the current [gist] explorer', |
||||
icon: 'fab fa-github' |
||||
} |
||||
] |
||||
// menu items
|
||||
this.menuItems = allItems.filter( |
||||
(item) => { |
||||
if (menuItems) return menuItems.find((name) => { return name === item.action }) |
||||
} |
||||
) |
||||
|
||||
self._components = {} |
||||
self._components.registry = localRegistry || globalRegistry |
||||
self._deps = { |
||||
config: self._components.registry.get('config').api, |
||||
editor: self._components.registry.get('editor').api, |
||||
fileManager: self._components.registry.get('filemanager').api |
||||
} |
||||
|
||||
self.events.register('focus', function (path) { |
||||
self._deps.fileManager.open(path) |
||||
}) |
||||
|
||||
self._components.registry.put({ api: self, name: `fileexplorer/${self.files.type}` }) |
||||
|
||||
// warn if file changed outside of Remix
|
||||
function remixdDialog () { |
||||
return yo`<div>This file has been changed outside of Remix IDE.</div>` |
||||
} |
||||
|
||||
this.files.event.register('fileExternallyChanged', (path, file) => { |
||||
if (self._deps.config.get('currentFile') === path && self._deps.editor.currentContent() && self._deps.editor.currentContent() !== file.content) { |
||||
if (this.files.isReadOnly(path)) return self._deps.editor.setText(file.content) |
||||
|
||||
modalDialog(path + ' changed', remixdDialog(), |
||||
{ |
||||
label: 'Replace by the new content', |
||||
fn: () => { |
||||
self._deps.editor.setText(file.content) |
||||
} |
||||
}, |
||||
{ |
||||
label: 'Keep the content displayed in Remix', |
||||
fn: () => {} |
||||
} |
||||
) |
||||
} |
||||
}) |
||||
|
||||
// register to event of the file provider
|
||||
files.event.on('fileRemoved', fileRemoved) |
||||
files.event.on('fileRenamed', fileRenamed) |
||||
files.event.on('fileRenamedError', fileRenamedError) |
||||
files.event.on('fileAdded', fileAdded) |
||||
files.event.on('folderAdded', folderAdded) |
||||
|
||||
function fileRenamedError (error) { |
||||
modalDialogCustom.alert(error) |
||||
} |
||||
|
||||
function fileAdded (filepath) { |
||||
self.ensureRoot(() => { |
||||
const folderpath = filepath.split('/').slice(0, -1).join('/') |
||||
const currentTree = self.treeView.nodeAt(folderpath) |
||||
if (!self.treeView.isExpanded(folderpath)) self.treeView.expand(folderpath) |
||||
if (currentTree) { |
||||
self.files.resolveDirectory(folderpath, (error, fileTree) => { |
||||
if (error) console.error(error) |
||||
if (!fileTree) return |
||||
fileTree = normalize(folderpath, fileTree) |
||||
self.treeView.updateNodeFromJSON(folderpath, fileTree, true) |
||||
self.focusElement = self.treeView.labelAt(self.focusPath) |
||||
// TODO: here we update the selected file (it applicable)
|
||||
// cause we are refreshing the interface of the whole directory when there's a new file.
|
||||
if (self.focusElement && !self.focusElement.classList.contains('bg-secondary')) { |
||||
self.focusElement.classList.add('bg-secondary') |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
function extractNameFromKey (key) { |
||||
const keyPath = key.split('/') |
||||
|
||||
return keyPath[keyPath.length - 1] |
||||
} |
||||
|
||||
function folderAdded (folderpath) { |
||||
self.ensureRoot(() => { |
||||
folderpath = folderpath.split('/').slice(0, -1).join('/') |
||||
self.files.resolveDirectory(folderpath, (error, fileTree) => { |
||||
if (error) console.error(error) |
||||
if (!fileTree) return |
||||
fileTree = normalize(folderpath, fileTree) |
||||
self.treeView.updateNodeFromJSON(folderpath, fileTree, true) |
||||
if (!self.treeView.isExpanded(folderpath)) self.treeView.expand(folderpath) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
function fileRemoved (filepath) { |
||||
const label = self.treeView.labelAt(filepath) |
||||
filepath = filepath.split('/').slice(0, -1).join('/') |
||||
|
||||
if (label && label.parentElement) { |
||||
label.parentElement.removeChild(label) |
||||
} |
||||
|
||||
self.updatePath(filepath) |
||||
} |
||||
|
||||
function fileRenamed (oldName, newName, isFolder) { |
||||
fileRemoved(oldName) |
||||
fileAdded(newName) |
||||
} |
||||
|
||||
// make interface and register to nodeClick, leafClick
|
||||
self.treeView = new Treeview({ |
||||
extractData: function extractData (value, tree, key) { |
||||
var newValue = {} |
||||
// var isReadOnly = false
|
||||
var isFile = false |
||||
Object.keys(value).filter(function keep (x) { |
||||
if (x === '/content') isFile = true |
||||
if (x[0] !== '/') return true |
||||
}).forEach(function (x) { newValue[x] = value[x] }) |
||||
return { |
||||
path: (tree || {}).path ? tree.path + '/' + key : key, |
||||
children: isFile ? undefined |
||||
: value instanceof Array ? value.map((item, index) => ({ |
||||
key: index, value: item |
||||
})) : value instanceof Object ? Object.keys(value).map(subkey => ({ |
||||
key: subkey, value: value[subkey] |
||||
})) : undefined |
||||
} |
||||
}, |
||||
formatSelf: function formatSelf (key, data, li) { |
||||
const isRoot = data.path === self.files.type |
||||
const isFolder = !!data.children |
||||
return yo` |
||||
<div class="${css.items}"> |
||||
<span |
||||
title="${data.path}" |
||||
class="${css.label} ${!isRoot ? !isFolder ? css.leaf : css.folder : ''}" |
||||
data-path="${data.path}" |
||||
style="${isRoot ? 'font-weight:bold;' : ''}" |
||||
onkeydown=${editModeOff} |
||||
onblur=${editModeOff} |
||||
> |
||||
${key.split('/').pop()} |
||||
</span> |
||||
${isRoot ? self.renderMenuItems() : ''} |
||||
</div> |
||||
` |
||||
} |
||||
}) |
||||
|
||||
/** |
||||
* Extracts first two folders as a subpath from the path. |
||||
**/ |
||||
function extractExternalFolder (path) { |
||||
const firstSlIndex = path.indexOf('/', 1) |
||||
if (firstSlIndex === -1) return '' |
||||
const secondSlIndex = path.indexOf('/', firstSlIndex + 1) |
||||
if (secondSlIndex === -1) return '' |
||||
return path.substring(0, secondSlIndex) |
||||
} |
||||
|
||||
self.treeView.event.register('nodeRightClick', function (key, data, label, event) { |
||||
if (self.files.readonly) return |
||||
if (key === self.files.type) return |
||||
MENU_HANDLE && MENU_HANDLE.hide(null, true) |
||||
const actions = {} |
||||
const provider = self._deps.fileManager.fileProviderOf(key) |
||||
actions['Create File'] = () => self.createNewFile(key) |
||||
actions['Create Folder'] = () => self.createNewFolder(key) |
||||
// @todo(#2386) not fully implemented. Readd later when fixed
|
||||
if (provider.isExternalFolder(key)) { |
||||
/* actions['Discard changes'] = () => { |
||||
modalDialogCustom.confirm( |
||||
'Discard changes', |
||||
'Are you sure you want to discard all your changes?', |
||||
() => { self.files.discardChanges(key) }, |
||||
() => {} |
||||
) |
||||
} */ |
||||
} else { |
||||
const folderPath = extractExternalFolder(key) |
||||
actions.Rename = () => { |
||||
if (self.files.isReadOnly(key)) { return tooltip('cannot rename folder. ' + self.files.type + ' is a read only explorer') } |
||||
var name = label.querySelector('span[data-path="' + key + '"]') |
||||
if (name) editModeOn(name) |
||||
} |
||||
actions.Delete = () => { |
||||
if (self.files.isReadOnly(key)) { return tooltip('cannot delete folder. ' + self.files.type + ' is a read only explorer') } |
||||
const currentFoldername = extractNameFromKey(key) |
||||
|
||||
modalDialogCustom.confirm('Confirm to delete folder', `Are you sure you want to delete ${currentFoldername} folder?`, |
||||
async () => { |
||||
const fileManager = self._deps.fileManager |
||||
const removeFolder = await fileManager.remove(key) |
||||
|
||||
if (!removeFolder) { |
||||
tooltip(`failed to remove ${key}. Make sure the directory is empty before removing it.`) |
||||
} |
||||
}, () => {}) |
||||
} |
||||
if (folderPath === 'browser/gists') { |
||||
actions['Push changes to gist'] = () => { |
||||
const id = key.substr(key.lastIndexOf('/') + 1, key.length - 1) |
||||
modalDialogCustom.confirm( |
||||
'Push back to Gist', |
||||
'Are you sure you want to push all your changes back to Gist?', |
||||
() => { self.toGist(id) }, |
||||
() => {} |
||||
) |
||||
} |
||||
} |
||||
} |
||||
MENU_HANDLE = contextMenu(event, actions) |
||||
}) |
||||
|
||||
self.treeView.event.register('leafRightClick', function (key, data, label, event) { |
||||
if (key === self.files.type) return |
||||
MENU_HANDLE && MENU_HANDLE.hide(null, true) |
||||
const actions = {} |
||||
const provider = self._deps.fileManager.fileProviderOf(key) |
||||
if (!provider.isExternalFolder(key)) { |
||||
actions['Create Folder'] = () => self.createNewFolder(self._deps.fileManager.extractPathOf(key)) |
||||
actions.Rename = () => { |
||||
if (self.files.isReadOnly(key)) { return tooltip('cannot rename file. ' + self.files.type + ' is a read only explorer') } |
||||
var name = label.querySelector('span[data-path="' + key + '"]') |
||||
if (name) editModeOn(name) |
||||
} |
||||
actions.Delete = () => { |
||||
if (self.files.isReadOnly(key)) { return tooltip('cannot delete file. ' + self.files.type + ' is a read only explorer') } |
||||
const currentFilename = extractNameFromKey(key) |
||||
|
||||
modalDialogCustom.confirm( |
||||
'Delete file', `Are you sure you want to delete ${currentFilename} file?`, |
||||
async () => { |
||||
const fileManager = self._deps.fileManager |
||||
const removeFile = await fileManager.remove(key) |
||||
|
||||
if (!removeFile) { |
||||
tooltip(`Failed to remove file ${key}.`) |
||||
} |
||||
}, |
||||
() => {} |
||||
) |
||||
} |
||||
if (key.endsWith('.js')) { |
||||
actions.Run = async () => { |
||||
provider.get(key, (error, content) => { |
||||
if (error) return console.log(error) |
||||
plugin.call('scriptRunner', 'execute', content) |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
MENU_HANDLE = contextMenu(event, actions) |
||||
}) |
||||
|
||||
self.treeView.event.register('leafClick', function (key, data, label) { |
||||
self.events.trigger('focus', [key]) |
||||
}) |
||||
|
||||
self.treeView.event.register('nodeClick', function (path, childrenContainer) { |
||||
if (!childrenContainer) return |
||||
if (childrenContainer.style.display === 'none') return |
||||
self.updatePath(path) |
||||
}) |
||||
|
||||
// register to main app, trigger when the current file in the editor changed
|
||||
self._deps.fileManager.events.on('currentFileChanged', (newFile) => { |
||||
const provider = self._deps.fileManager.fileProviderOf(newFile) |
||||
if (self.focusElement && self.focusPath !== newFile) { |
||||
self.focusElement.classList.remove('bg-secondary') |
||||
self.focusElement = null |
||||
self.focusPath = null |
||||
} |
||||
if (provider && (provider.type === files.type)) { |
||||
self.focusElement = self.treeView.labelAt(newFile) |
||||
if (self.focusElement) { |
||||
self.focusElement.classList.add('bg-secondary') |
||||
self.focusPath = newFile |
||||
} |
||||
} |
||||
}) |
||||
|
||||
self._deps.fileManager.events.on('noFileSelected', () => { |
||||
if (self.focusElement) { |
||||
self.focusElement.classList.remove('bg-secondary') |
||||
self.focusElement = null |
||||
self.focusPath = null |
||||
} |
||||
}) |
||||
|
||||
var textUnderEdit = null |
||||
|
||||
function selectElementContents (el) { |
||||
var range = document.createRange() |
||||
range.selectNodeContents(el) |
||||
var sel = window.getSelection() |
||||
sel.removeAllRanges() |
||||
sel.addRange(range) |
||||
} |
||||
|
||||
function editModeOn (label) { |
||||
textUnderEdit = label.innerText |
||||
label.setAttribute('contenteditable', true) |
||||
label.classList.add('bg-light') |
||||
label.focus() |
||||
selectElementContents(label) |
||||
} |
||||
|
||||
function editModeOff (event) { |
||||
const label = this |
||||
|
||||
const isFolder = label.className.indexOf('folder') !== -1 |
||||
function rename () { |
||||
var newPath = label.dataset.path |
||||
newPath = newPath.split('/') |
||||
newPath[newPath.length - 1] = label.innerText |
||||
newPath = newPath.join('/') |
||||
if (label.innerText === '') { |
||||
modalDialogCustom.alert('File name cannot be empty') |
||||
label.innerText = textUnderEdit |
||||
} else if (helper.checkSpecialChars(label.innerText)) { |
||||
modalDialogCustom.alert('Special characters are not allowed') |
||||
label.innerText = textUnderEdit |
||||
} else { |
||||
files.exists(newPath, (error, exist) => { |
||||
if (error) return modalDialogCustom.alert('Unexpected error while renaming: ' + error) |
||||
if (!exist) { |
||||
files.rename(label.dataset.path, newPath, isFolder) |
||||
} else { |
||||
modalDialogCustom.alert('File already exists.') |
||||
label.innerText = textUnderEdit |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
if (event.which === 13) event.preventDefault() |
||||
if ((event.type === 'blur' || event.which === 13) && label.getAttribute('contenteditable')) { |
||||
var save = textUnderEdit !== label.innerText |
||||
if (save) { |
||||
modalDialogCustom.confirm( |
||||
'Confirm to rename a ' + (isFolder ? 'folder' : 'file'), |
||||
'Are you sure you want to rename ' + textUnderEdit + '?', |
||||
() => { rename() }, |
||||
() => { label.innerText = textUnderEdit } |
||||
) |
||||
} |
||||
label.removeAttribute('contenteditable') |
||||
label.classList.remove('bg-light') |
||||
} |
||||
} |
||||
} |
||||
|
||||
fileExplorer.prototype.updatePath = function (path) { |
||||
this.files.resolveDirectory(path, (error, fileTree) => { |
||||
if (error) console.error(error) |
||||
if (!fileTree) return |
||||
var newTree = normalize(path, fileTree) |
||||
this.treeView.updateNodeFromJSON(path, newTree, true) |
||||
}) |
||||
} |
||||
|
||||
fileExplorer.prototype.hide = function () { |
||||
if (this.container) this.container.style.display = 'none' |
||||
} |
||||
|
||||
fileExplorer.prototype.show = function () { |
||||
if (this.container) this.container.style.display = 'block' |
||||
} |
||||
|
||||
fileExplorer.prototype.init = function () { |
||||
this.container = yo`<div></div>` |
||||
return this.container |
||||
} |
||||
|
||||
fileExplorer.prototype.publishToGist = function () { |
||||
modalDialogCustom.confirm( |
||||
'Create a public gist', |
||||
'Are you sure you want to publish all your files in browser directory anonymously as a public gist on github.com? Note: this will not include directories.', |
||||
() => { this.toGist() } |
||||
) |
||||
} |
||||
|
||||
fileExplorer.prototype.uploadFile = function (event) { |
||||
// TODO The file explorer is merely a view on the current state of
|
||||
// the files module. Please ask the user here if they want to overwrite
|
||||
// a file and then just use `files.add`. The file explorer will
|
||||
// pick that up via the 'fileAdded' event from the files module.
|
||||
|
||||
const self = this |
||||
|
||||
;[...event.target.files].forEach((file) => { |
||||
const files = this.files |
||||
function loadFile () { |
||||
var fileReader = new FileReader() |
||||
fileReader.onload = async function (event) { |
||||
if (helper.checkSpecialChars(file.name)) { |
||||
modalDialogCustom.alert('Special characters are not allowed') |
||||
return |
||||
} |
||||
var success = await files.set(name, event.target.result) |
||||
if (!success) { |
||||
modalDialogCustom.alert('Failed to create file ' + name) |
||||
} else { |
||||
self.events.trigger('focus', [name]) |
||||
} |
||||
} |
||||
fileReader.readAsText(file) |
||||
} |
||||
var name = files.type + '/' + file.name |
||||
files.exists(name, (error, exist) => { |
||||
if (error) console.log(error) |
||||
if (!exist) { |
||||
loadFile() |
||||
} else { |
||||
modalDialogCustom.confirm('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, () => { loadFile() }) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
fileExplorer.prototype.toGist = function (id) { |
||||
const proccedResult = function (error, data) { |
||||
if (error) { |
||||
modalDialogCustom.alert('Failed to manage gist: ' + error) |
||||
console.log('Failed to manage gist: ' + error) |
||||
} else { |
||||
if (data.html_url) { |
||||
modalDialogCustom.confirm('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, () => { |
||||
window.open(data.html_url, '_blank') |
||||
}) |
||||
} else { |
||||
modalDialogCustom.alert(data.message + ' ' + data.documentation_url + ' ' + JSON.stringify(data.errors, null, '\t')) |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This function is to get the original content of given gist |
||||
* @params id is the gist id to fetch |
||||
*/ |
||||
async function getOriginalFiles (id) { |
||||
if (!id) { |
||||
return [] |
||||
} |
||||
|
||||
const url = `https://api.github.com/gists/${id}` |
||||
const res = await fetch(url) |
||||
const data = await res.json() |
||||
return data.files || [] |
||||
} |
||||
|
||||
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
|
||||
const folder = id ? '/gists/' + id : '/' |
||||
this.packageFiles(this.files, folder, (error, packaged) => { |
||||
if (error) { |
||||
console.log(error) |
||||
modalDialogCustom.alert('Failed to create gist: ' + error.message) |
||||
} else { |
||||
// check for token
|
||||
var tokenAccess = this._deps.config.get('settings/gist-access-token') |
||||
if (!tokenAccess) { |
||||
modalDialogCustom.alert( |
||||
'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.' |
||||
) |
||||
} else { |
||||
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' + |
||||
queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist=' |
||||
const gists = new Gists({ token: tokenAccess }) |
||||
|
||||
if (id) { |
||||
const originalFileList = getOriginalFiles(id) |
||||
// Telling the GIST API to remove files
|
||||
const updatedFileList = Object.keys(packaged) |
||||
const allItems = Object.keys(originalFileList) |
||||
.filter(fileName => updatedFileList.indexOf(fileName) === -1) |
||||
.reduce((acc, deleteFileName) => ({ |
||||
...acc, |
||||
[deleteFileName]: null |
||||
}), originalFileList) |
||||
// adding new files
|
||||
updatedFileList.forEach((file) => { |
||||
const _items = file.split('/') |
||||
const _fileName = _items[_items.length - 1] |
||||
allItems[_fileName] = packaged[file] |
||||
}) |
||||
|
||||
tooltip('Saving gist (' + id + ') ...') |
||||
gists.edit({ |
||||
description: description, |
||||
public: true, |
||||
files: allItems, |
||||
id: id |
||||
}, (error, result) => { |
||||
proccedResult(error, result) |
||||
if (!error) { |
||||
for (const key in allItems) { |
||||
if (allItems[key] === null) delete allItems[key] |
||||
} |
||||
} |
||||
}) |
||||
} else { |
||||
// id is not existing, need to create a new gist
|
||||
tooltip('Creating a new gist ...') |
||||
gists.create({ |
||||
description: description, |
||||
public: true, |
||||
files: packaged |
||||
}, (error, result) => { |
||||
proccedResult(error, result) |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// return all the files, except the temporary/readonly ones..
|
||||
fileExplorer.prototype.packageFiles = function (filesProvider, directory, callback) { |
||||
const ret = {} |
||||
filesProvider.resolveDirectory(directory, (error, files) => { |
||||
if (error) callback(error) |
||||
else { |
||||
async.eachSeries(Object.keys(files), (path, cb) => { |
||||
if (filesProvider.isDirectory(path)) { |
||||
cb() |
||||
} else { |
||||
filesProvider.get(path, (error, content) => { |
||||
if (error) return cb(error) |
||||
if (/^\s+$/.test(content) || !content.length) { |
||||
content = '// this line is added to create a gist. Empty file is not allowed.' |
||||
} |
||||
ret[path] = { content } |
||||
cb() |
||||
}) |
||||
} |
||||
}, (error) => { |
||||
callback(error, ret) |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
fileExplorer.prototype.createNewFile = function (parentFolder = '/') { |
||||
const self = this |
||||
modalDialogCustom.prompt('Create new file', 'File Name (e.g Untitled.sol)', 'Untitled.sol', (input) => { |
||||
if (!input) input = 'New file' |
||||
helper.createNonClashingName(parentFolder + '/' + input, self.files, async (error, newName) => { |
||||
if (error) return tooltip('Failed to create file ' + newName + ' ' + error) |
||||
const fileManager = self._deps.fileManager |
||||
const createFile = await fileManager.writeFile(newName, '') |
||||
|
||||
if (!createFile) { |
||||
tooltip('Failed to create file ' + newName) |
||||
} else { |
||||
await fileManager.open(newName) |
||||
if (newName.includes('_test.sol')) { |
||||
self.events.trigger('newTestFileCreated', [newName]) |
||||
} |
||||
} |
||||
}) |
||||
}, null, true) |
||||
} |
||||
|
||||
fileExplorer.prototype.createNewFolder = function (parentFolder) { |
||||
const self = this |
||||
modalDialogCustom.prompt('Create new folder', '', 'New folder', (input) => { |
||||
if (!input) { |
||||
return tooltip('Failed to create folder. The name can not be empty') |
||||
} |
||||
|
||||
const currentPath = !parentFolder ? self._deps.fileManager.currentPath() : parentFolder |
||||
let newName = currentPath ? currentPath + '/' + input : self.files.type + '/' + input |
||||
|
||||
newName = newName + '/' |
||||
self.files.exists(newName, (error, exist) => { |
||||
if (error) return tooltip('Unexpected error while creating folder: ' + error) |
||||
if (!exist) { |
||||
self.files.set(newName, '') |
||||
} else { |
||||
tooltip('Folder already exists.', () => {}) |
||||
} |
||||
}) |
||||
}, null, true) |
||||
} |
||||
|
||||
fileExplorer.prototype.renderMenuItems = function () { |
||||
let items = '' |
||||
if (this.menuItems) { |
||||
items = this.menuItems.map(({ action, title, icon }) => { |
||||
if (action === 'uploadFile') { |
||||
return yo` |
||||
<label |
||||
id=${action} |
||||
data-id="fileExplorerUploadFile${action}" |
||||
class="${icon} mb-0 ${css.newFile}" |
||||
title="${title}" |
||||
> |
||||
<input id="fileUpload" data-id="fileExplorerFileUpload" type="file" onchange=${(event) => { |
||||
event.stopPropagation() |
||||
this.uploadFile(event) |
||||
}} multiple /> |
||||
</label> |
||||
` |
||||
} else { |
||||
return yo` |
||||
<span |
||||
id=${action} |
||||
data-id="fileExplorerNewFile${action}" |
||||
onclick=${(event) => { event.stopPropagation(); this[action]() }} |
||||
class="newFile ${icon} ${css.newFile}" |
||||
title=${title} |
||||
> |
||||
</span> |
||||
` |
||||
} |
||||
}) |
||||
} |
||||
return yo`<span class=" ${css.menu}">${items}</span>` |
||||
} |
||||
|
||||
fileExplorer.prototype.ensureRoot = function (cb) { |
||||
cb = cb || (() => {}) |
||||
var self = this |
||||
if (self.element) return cb() |
||||
const root = {} |
||||
root[this.files.type] = {} |
||||
var element = self.treeView.render(root, false) |
||||
element.classList.add(css.fileexplorer) |
||||
element.events = self.events |
||||
element.api = self.api |
||||
self.container.appendChild(element) |
||||
self.element = element |
||||
if (cb) cb() |
||||
self.treeView.expand(self.files.type) |
||||
} |
||||
|
||||
function normalize (path, filesList) { |
||||
var prefix = path.split('/')[0] |
||||
var newList = {} |
||||
Object.keys(filesList).forEach(key => { |
||||
newList[prefix + '/' + key] = filesList[key].isDirectory ? {} : { '/content': true } |
||||
}) |
||||
return newList |
||||
} |
||||
|
||||
module.exports = fileExplorer |
@ -1,135 +0,0 @@ |
||||
import * as packageJson from '../../../../../../package.json' |
||||
import { Plugin } from '@remixproject/engine' |
||||
const EventEmitter = require('events') |
||||
var Compiler = require('@remix-project/remix-solidity').Compiler |
||||
|
||||
const profile = { |
||||
name: 'solidity-logic', |
||||
displayName: 'Solidity compiler logic', |
||||
description: 'Compile solidity contracts - Logic', |
||||
methods: ['getCompilerState'], |
||||
version: packageJson.version |
||||
} |
||||
|
||||
class CompileTab extends Plugin { |
||||
constructor (queryParams, fileManager, editor, config, fileProvider, contentImport) { |
||||
super(profile) |
||||
this.event = new EventEmitter() |
||||
this.queryParams = queryParams |
||||
this.compilerImport = contentImport |
||||
this.compiler = new Compiler((url, cb) => this.compilerImport.resolveAndSave(url).then((result) => cb(null, result)).catch((error) => cb(error.message))) |
||||
this.fileManager = fileManager |
||||
this.editor = editor |
||||
this.config = config |
||||
this.fileProvider = fileProvider |
||||
} |
||||
|
||||
init () { |
||||
this.optimize = this.queryParams.get().optimize |
||||
this.optimize = this.optimize === 'true' |
||||
this.queryParams.update({ optimize: this.optimize }) |
||||
this.compiler.set('optimize', this.optimize) |
||||
|
||||
this.runs = this.queryParams.get().runs |
||||
this.runs = this.runs || 200 |
||||
this.queryParams.update({ runs: this.runs }) |
||||
this.compiler.set('runs', this.runs) |
||||
|
||||
this.evmVersion = this.queryParams.get().evmVersion |
||||
if (this.evmVersion === 'undefined' || this.evmVersion === 'null' || !this.evmVersion) { |
||||
this.evmVersion = null |
||||
} |
||||
this.queryParams.update({ evmVersion: this.evmVersion }) |
||||
this.compiler.set('evmVersion', this.evmVersion) |
||||
} |
||||
|
||||
setOptimize (newOptimizeValue) { |
||||
this.optimize = newOptimizeValue |
||||
this.queryParams.update({ optimize: this.optimize }) |
||||
this.compiler.set('optimize', this.optimize) |
||||
} |
||||
|
||||
setRuns (runs) { |
||||
this.runs = runs |
||||
this.queryParams.update({ runs: this.runs }) |
||||
this.compiler.set('runs', this.runs) |
||||
} |
||||
|
||||
setEvmVersion (newEvmVersion) { |
||||
this.evmVersion = newEvmVersion |
||||
this.queryParams.update({ evmVersion: this.evmVersion }) |
||||
this.compiler.set('evmVersion', this.evmVersion) |
||||
} |
||||
|
||||
/** |
||||
* Set the compiler to using Solidity or Yul (default to Solidity) |
||||
* @params lang {'Solidity' | 'Yul'} ... |
||||
*/ |
||||
setLanguage (lang) { |
||||
this.compiler.set('language', lang) |
||||
} |
||||
|
||||
getCompilerState () { |
||||
return this.compiler.state |
||||
} |
||||
|
||||
/** |
||||
* Compile a specific file of the file manager |
||||
* @param {string} target the path to the file to compile |
||||
*/ |
||||
compileFile (target) { |
||||
if (!target) throw new Error('No target provided for compiliation') |
||||
const provider = this.fileManager.fileProviderOf(target) |
||||
if (!provider) throw new Error(`cannot compile ${target}. Does not belong to any explorer`) |
||||
return new Promise((resolve, reject) => { |
||||
provider.get(target, (error, content) => { |
||||
if (error) return reject(error) |
||||
const sources = { [target]: { content } } |
||||
this.event.emit('startingCompilation') |
||||
// setTimeout fix the animation on chrome... (animation triggered by 'staringCompilation')
|
||||
setTimeout(() => { this.compiler.compile(sources, target); resolve() }, 100) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
async isHardhatProject () { |
||||
if (this.fileManager.mode === 'localhost') { |
||||
return await this.fileManager.exists('hardhat.config.js') |
||||
} else return false |
||||
} |
||||
|
||||
runCompiler (hhCompilation) { |
||||
try { |
||||
if (this.fileManager.mode === 'localhost' && hhCompilation) { |
||||
const { currentVersion, optimize, runs } = this.compiler.state |
||||
if (currentVersion) { |
||||
const fileContent = `module.exports = {
|
||||
solidity: '${currentVersion.substring(0, currentVersion.indexOf('+commit'))}', |
||||
settings: { |
||||
optimizer: { |
||||
enabled: ${optimize}, |
||||
runs: ${runs} |
||||
} |
||||
} |
||||
} |
||||
` |
||||
const configFilePath = 'remix-compiler.config.js' |
||||
this.fileManager.setFileContent(configFilePath, fileContent) |
||||
this.call('hardhat', 'compile', configFilePath).then((result) => { |
||||
this.call('terminal', 'log', { type: 'info', value: result }) |
||||
}).catch((error) => { |
||||
this.call('terminal', 'log', { type: 'error', value: error }) |
||||
}) |
||||
} |
||||
} |
||||
this.fileManager.saveCurrentFile() |
||||
this.event.emit('removeAnnotations') |
||||
var currentFile = this.config.get('currentFile') |
||||
return this.compileFile(currentFile) |
||||
} catch (err) { |
||||
console.error(err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
module.exports = CompileTab |
@ -1,580 +0,0 @@ |
||||
|
||||
import toaster from '../../ui/tooltip' |
||||
import { canUseWorker, baseURLBin, baseURLWasm, urlFromVersion, pathToURL, promisedMiniXhr } from '@remix-project/remix-solidity' |
||||
const yo = require('yo-yo') |
||||
const helper = require('../../../lib/helper') |
||||
const addTooltip = require('../../ui/tooltip') |
||||
const semver = require('semver') |
||||
const modalDialogCustom = require('../../ui/modal-dialog-custom') |
||||
const css = require('../styles/compile-tab-styles') |
||||
const _paq = window._paq = window._paq || [] |
||||
|
||||
class CompilerContainer { |
||||
constructor (compileTabLogic, editor, config, queryParams) { |
||||
this._view = {} |
||||
this.compileTabLogic = compileTabLogic |
||||
this.editor = editor |
||||
this.config = config |
||||
this.queryParams = queryParams |
||||
this.hhCompilation = false |
||||
|
||||
this.data = { |
||||
hideWarnings: config.get('hideWarnings') || false, |
||||
autoCompile: config.get('autoCompile'), |
||||
compileTimeout: null, |
||||
timeout: 300, |
||||
allversions: null, |
||||
selectedVersion: null, |
||||
defaultVersion: 'soljson-v0.8.4+commit.c7e474f2.js' // this default version is defined: in makeMockCompiler (for browser test)
|
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Update the compilation button with the name of the current file |
||||
*/ |
||||
set currentFile (name = '') { |
||||
if (name && name !== '') { |
||||
this._setCompilerVersionFromPragma(name) |
||||
} |
||||
if (!this._view.compilationButton) return |
||||
const button = this.compilationButton(name.split('/').pop()) |
||||
this._disableCompileBtn(!name || (name && !this.isSolFileSelected(name))) |
||||
yo.update(this._view.compilationButton, button) |
||||
} |
||||
|
||||
isSolFileSelected (currentFile = '') { |
||||
if (!currentFile) currentFile = this.config.get('currentFile') |
||||
if (!currentFile) return false |
||||
const extention = currentFile.substr(currentFile.length - 3, currentFile.length) |
||||
return extention.toLowerCase() === 'sol' || extention.toLowerCase() === 'yul' |
||||
} |
||||
|
||||
deactivate () { |
||||
// deactivate editor listeners
|
||||
this.editor.event.unregister('contentChanged') |
||||
this.editor.event.unregister('sessionSwitched') |
||||
} |
||||
|
||||
activate () { |
||||
this.currentFile = this.config.get('currentFile') |
||||
this.listenToEvents() |
||||
} |
||||
|
||||
listenToEvents () { |
||||
this.editor.event.register('sessionSwitched', () => { |
||||
if (!this._view.compileIcon) return |
||||
this.scheduleCompilation() |
||||
}) |
||||
|
||||
this.compileTabLogic.event.on('startingCompilation', () => { |
||||
if (!this._view.compileIcon) return |
||||
this._view.compileIcon.setAttribute('title', 'compiling...') |
||||
this._view.compileIcon.classList.remove(`${css.bouncingIcon}`) |
||||
this._view.compileIcon.classList.add(`${css.spinningIcon}`) |
||||
}) |
||||
|
||||
this.compileTabLogic.compiler.event.register('compilationDuration', (speed) => { |
||||
if (!this._view.warnCompilationSlow) return |
||||
if (speed > 1000) { |
||||
const msg = `Last compilation took ${speed}ms. We suggest to turn off autocompilation.` |
||||
this._view.warnCompilationSlow.setAttribute('title', msg) |
||||
this._view.warnCompilationSlow.style.visibility = 'visible' |
||||
} else { |
||||
this._view.warnCompilationSlow.style.visibility = 'hidden' |
||||
} |
||||
}) |
||||
|
||||
this.editor.event.register('contentChanged', () => { |
||||
if (!this._view.compileIcon) return |
||||
this.scheduleCompilation() |
||||
this._view.compileIcon.classList.add(`${css.bouncingIcon}`) // @TODO: compileView tab
|
||||
}) |
||||
|
||||
this.compileTabLogic.compiler.event.register('loadingCompiler', () => { |
||||
if (!this._view.compileIcon) return |
||||
this._disableCompileBtn(true) |
||||
this._view.compileIcon.setAttribute('title', 'compiler is loading, please wait a few moments.') |
||||
this._view.compileIcon.classList.add(`${css.spinningIcon}`) |
||||
this._view.warnCompilationSlow.style.visibility = 'hidden' |
||||
this._updateLanguageSelector() |
||||
}) |
||||
|
||||
this.compileTabLogic.compiler.event.register('compilerLoaded', () => { |
||||
if (!this._view.compileIcon) return |
||||
this._disableCompileBtn(false) |
||||
this._view.compileIcon.setAttribute('title', '') |
||||
this._view.compileIcon.classList.remove(`${css.spinningIcon}`) |
||||
if (this.data.autoCompile) this.compileIfAutoCompileOn() |
||||
}) |
||||
|
||||
this.compileTabLogic.compiler.event.register('compilationFinished', (success, data, source) => { |
||||
if (!this._view.compileIcon) return |
||||
this._view.compileIcon.setAttribute('title', 'idle') |
||||
this._view.compileIcon.classList.remove(`${css.spinningIcon}`) |
||||
this._view.compileIcon.classList.remove(`${css.bouncingIcon}`) |
||||
_paq.push(['trackEvent', 'compiler', 'compiled_with_version', this._retrieveVersion()]) |
||||
}) |
||||
} |
||||
|
||||
/************** |
||||
* SUBCOMPONENT |
||||
*/ |
||||
compilationButton (name = '') { |
||||
const displayed = name || '<no file selected>' |
||||
const disabled = name && this.isSolFileSelected() ? '' : 'disabled' |
||||
return yo` |
||||
<button id="compileBtn" data-id="compilerContainerCompileBtn" class="btn btn-primary btn-block ${disabled} mt-3" title="Compile" onclick="${this.compile.bind(this)}"> |
||||
<span>${this._view.compileIcon} Compile ${displayed}</span> |
||||
</button> |
||||
` |
||||
} |
||||
|
||||
_disableCompileBtn (shouldDisable) { |
||||
const btn = document.getElementById('compileBtn') |
||||
if (!btn) return |
||||
if (shouldDisable) { |
||||
btn.classList.add('disabled') |
||||
} else if (this.isSolFileSelected()) { |
||||
btn.classList.remove('disabled') |
||||
} |
||||
} |
||||
|
||||
// Load solc compiler version according to pragma in contract file
|
||||
_setCompilerVersionFromPragma (filename) { |
||||
if (!this.data.allversions) return |
||||
this.compileTabLogic.fileManager.readFile(filename).then(data => { |
||||
const pragmaArr = data.match(/(pragma solidity (.+?);)/g) |
||||
if (pragmaArr && pragmaArr.length === 1) { |
||||
const pragmaStr = pragmaArr[0].replace('pragma solidity', '').trim() |
||||
const pragma = pragmaStr.substring(0, pragmaStr.length - 1) |
||||
const releasedVersions = this.data.allversions.filter(obj => !obj.prerelease).map(obj => obj.version) |
||||
const allVersions = this.data.allversions.map(obj => this._retrieveVersion(obj.version)) |
||||
const currentCompilerName = this._retrieveVersion(this._view.versionSelector.selectedOptions[0].label) |
||||
// contains only numbers part, for example '0.4.22'
|
||||
const pureVersion = this._retrieveVersion() |
||||
// is nightly build newer than the last release
|
||||
const isNewestNightly = currentCompilerName.includes('nightly') && semver.gt(pureVersion, releasedVersions[0]) |
||||
// checking if the selected version is in the pragma range
|
||||
const isInRange = semver.satisfies(pureVersion, pragma) |
||||
// checking if the selected version is from official compilers list(excluding custom versions) and in range or greater
|
||||
const isOfficial = allVersions.includes(currentCompilerName) |
||||
if (isOfficial && (!isInRange && !isNewestNightly)) { |
||||
const compilerToLoad = semver.maxSatisfying(releasedVersions, pragma) |
||||
const compilerPath = this.data.allversions.filter(obj => !obj.prerelease && obj.version === compilerToLoad)[0].path |
||||
if (this.data.selectedVersion !== compilerPath) { |
||||
this.data.selectedVersion = compilerPath |
||||
this._updateVersionSelector() |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
_retrieveVersion (version) { |
||||
if (!version) version = this._view.versionSelector.value |
||||
if (version === 'builtin') version = this.data.defaultVersion |
||||
return semver.coerce(version) ? semver.coerce(version).version : '' |
||||
} |
||||
|
||||
render () { |
||||
this.compileTabLogic.compiler.event.register('compilerLoaded', (version) => this.setVersionText(version)) |
||||
this.fetchAllVersion((allversions, selectedVersion, isURL) => { |
||||
this.data.allversions = allversions |
||||
if (isURL) this._updateVersionSelector(selectedVersion) |
||||
else { |
||||
this.data.selectedVersion = selectedVersion |
||||
if (this._view.versionSelector) this._updateVersionSelector() |
||||
} |
||||
}) |
||||
|
||||
this.hardhatCompilation = yo`<div class="mt-2 ${css.compilerConfig} custom-control custom-checkbox" style="display:none">
|
||||
<input class="${css.autocompile} custom-control-input" onchange=${(e) => this.updatehhCompilation(e)} id="enableHardhat" type="checkbox" title="Enable Hardhat Compilation"> |
||||
<label class="form-check-label custom-control-label" for="enableHardhat">Enable Hardhat Compilation</label> |
||||
</div>` |
||||
this._view.warnCompilationSlow = yo`<i title="Compilation Slow" style="visibility:hidden" class="${css.warnCompilationSlow} fas fa-exclamation-triangle" aria-hidden="true"></i>` |
||||
this._view.compileIcon = yo`<i class="fas fa-sync ${css.icon}" aria-hidden="true"></i>` |
||||
this._view.autoCompile = yo`<input class="${css.autocompile} custom-control-input" onchange=${() => this.updateAutoCompile()} data-id="compilerContainerAutoCompile" id="autoCompile" type="checkbox" title="Auto compile">` |
||||
this._view.hideWarningsBox = yo`<input class="${css.autocompile} custom-control-input" onchange=${() => this.hideWarnings()} id="hideWarningsBox" type="checkbox" title="Hide warnings">` |
||||
if (this.data.autoCompile) this._view.autoCompile.setAttribute('checked', '') |
||||
if (this.data.hideWarnings) this._view.hideWarningsBox.setAttribute('checked', '') |
||||
|
||||
this._view.optimize = yo`<input onchange=${() => this.onchangeOptimize()} class="custom-control-input" id="optimize" type="checkbox">` |
||||
if (this.compileTabLogic.optimize) this._view.optimize.setAttribute('checked', '') |
||||
|
||||
this._view.runs = yo`<input
|
||||
min="1" |
||||
class="custom-select ml-2 ${css.runs}" |
||||
id="runs" |
||||
placeholder="200" |
||||
value="200" |
||||
type="number" |
||||
title="Estimated number of times each opcode of the deployed code will be executed across the life-time of the contract." |
||||
onchange=${() => this.onchangeRuns()} |
||||
>` |
||||
if (this.compileTabLogic.optimize) { |
||||
this._view.runs.removeAttribute('disabled') |
||||
this._view.runs.value = this.compileTabLogic.runs |
||||
} else { |
||||
this._view.runs.setAttribute('disabled', '') |
||||
} |
||||
|
||||
this._view.versionSelector = yo` |
||||
<select onchange="${() => this.onchangeLoadVersion()}" class="custom-select" id="versionSelector" disabled> |
||||
<option disabled selected>${this.data.defaultVersion}</option> |
||||
<option disabled>builtin</option> |
||||
</select>` |
||||
this._view.languageSelector = yo` |
||||
<select onchange="${() => this.onchangeLanguage()}" class="custom-select" id="compilierLanguageSelector" title="Available since v0.5.7"> |
||||
<option>Solidity</option> |
||||
<option>Yul</option> |
||||
</select>` |
||||
this._view.version = yo`<span id="version"></span>` |
||||
|
||||
this._view.evmVersionSelector = yo` |
||||
<select onchange="${() => this.onchangeEvmVersion()}" class="custom-select" id="evmVersionSelector"> |
||||
<option value="default" selected="selected">compiler default</option> |
||||
<option>berlin</option> |
||||
<option>muirGlacier</option> |
||||
<option>istanbul</option> |
||||
<option>petersburg</option> |
||||
<option>constantinople</option> |
||||
<option>byzantium</option> |
||||
<option>spuriousDragon</option> |
||||
<option>tangerineWhistle</option> |
||||
<option>homestead</option> |
||||
</select>` |
||||
if (this.compileTabLogic.evmVersion) { |
||||
const s = this._view.evmVersionSelector |
||||
let i |
||||
for (i = 0; i < s.options.length; i++) { |
||||
if (s.options[i].value === this.compileTabLogic.evmVersion) { |
||||
break |
||||
} |
||||
} |
||||
if (i === s.options.length) { // invalid evmVersion from queryParams
|
||||
s.selectedIndex = 0 // compiler default
|
||||
this.onchangeEvmVersion() |
||||
} else { |
||||
s.selectedIndex = i |
||||
this.onchangeEvmVersion() |
||||
} |
||||
} |
||||
|
||||
this._view.compilationButton = this.compilationButton() |
||||
|
||||
this._view.includeNightlies = yo` |
||||
<input class="mr-2 custom-control-input" id="nightlies" type="checkbox" onchange=${() => this._updateVersionSelector()}> |
||||
` |
||||
this._view.compileContainer = yo` |
||||
<section> |
||||
<!-- Select Compiler Version --> |
||||
<article> |
||||
<header class="${css.compilerSection} border-bottom"> |
||||
<div class="mb-2"> |
||||
<label class="${css.compilerLabel} form-check-label" for="versionSelector"> |
||||
Compiler |
||||
<button class="far fa-plus-square border-0 p-0 mx-2 btn-sm" onclick="${(e) => this.promtCompiler(e)}" title="Add a custom compiler with URL"></button> |
||||
</label> |
||||
${this._view.versionSelector} |
||||
</div> |
||||
<div class="mb-2 ${css.nightlyBuilds} custom-control custom-checkbox"> |
||||
${this._view.includeNightlies} |
||||
<label for="nightlies" class="form-check-label custom-control-label">Include nightly builds</label> |
||||
</div> |
||||
<div class="mb-2"> |
||||
<label class="${css.compilerLabel} form-check-label" for="compilierLanguageSelector">Language</label> |
||||
${this._view.languageSelector} |
||||
</div> |
||||
<div class="mb-2"> |
||||
<label class="${css.compilerLabel} form-check-label" for="evmVersionSelector">EVM Version</label> |
||||
${this._view.evmVersionSelector} |
||||
</div> |
||||
<div class="mt-3"> |
||||
<p class="mt-2 ${css.compilerLabel}">Compiler Configuration</p> |
||||
<div class="mt-2 ${css.compilerConfig} custom-control custom-checkbox"> |
||||
${this._view.autoCompile} |
||||
<label class="form-check-label custom-control-label" for="autoCompile">Auto compile</label> |
||||
</div> |
||||
<div class="mt-2 ${css.compilerConfig} custom-control custom-checkbox"> |
||||
<div class="justify-content-between align-items-center d-flex"> |
||||
${this._view.optimize} |
||||
<label class="form-check-label custom-control-label" for="optimize">Enable optimization</label> |
||||
${this._view.runs} |
||||
</div> |
||||
</div> |
||||
<div class="mt-2 ${css.compilerConfig} custom-control custom-checkbox"> |
||||
${this._view.hideWarningsBox} |
||||
<label class="form-check-label custom-control-label" for="hideWarningsBox">Hide warnings</label> |
||||
</div> |
||||
</div> |
||||
${this.hardhatCompilation} |
||||
${this._view.compilationButton} |
||||
</header> |
||||
</article> |
||||
<!-- Config --> |
||||
</section>` |
||||
|
||||
return this._view.compileContainer |
||||
} |
||||
|
||||
promtCompiler () { |
||||
modalDialogCustom.prompt( |
||||
'Add a custom compiler', |
||||
'URL', |
||||
'', |
||||
(url) => this.addCustomCompiler(url) |
||||
) |
||||
} |
||||
|
||||
addCustomCompiler (url) { |
||||
this.data.selectedVersion = this._view.versionSelector.value |
||||
this._updateVersionSelector(url) |
||||
} |
||||
|
||||
updateAutoCompile (event) { |
||||
this.config.set('autoCompile', this._view.autoCompile.checked) |
||||
} |
||||
|
||||
updatehhCompilation (event) { |
||||
this.hhCompilation = event.target.checked |
||||
} |
||||
|
||||
compile (event) { |
||||
const currentFile = this.config.get('currentFile') |
||||
if (!this.isSolFileSelected()) return |
||||
|
||||
this._setCompilerVersionFromPragma(currentFile) |
||||
this.compileTabLogic.runCompiler(this.hhCompilation) |
||||
} |
||||
|
||||
compileIfAutoCompileOn () { |
||||
if (this.config.get('autoCompile')) { |
||||
this.compile() |
||||
} |
||||
} |
||||
|
||||
hideWarnings (event) { |
||||
this.config.set('hideWarnings', this._view.hideWarningsBox.checked) |
||||
this.compileIfAutoCompileOn() |
||||
} |
||||
|
||||
/* |
||||
The following functions are handlers for internal events. |
||||
*/ |
||||
|
||||
onchangeOptimize () { |
||||
this.compileTabLogic.setOptimize(!!this._view.optimize.checked) |
||||
if (this.compileTabLogic.optimize) { |
||||
this._view.runs.removeAttribute('disabled') |
||||
this.compileTabLogic.setRuns(parseInt(this._view.runs.value)) |
||||
} else { |
||||
this.compileTabLogic.setRuns(200) |
||||
this._view.runs.setAttribute('disabled', '') |
||||
} |
||||
this.compileIfAutoCompileOn() |
||||
} |
||||
|
||||
onchangeRuns () { |
||||
this.compileTabLogic.setRuns(parseInt(this._view.runs.value)) |
||||
this.compileIfAutoCompileOn() |
||||
} |
||||
|
||||
onchangeLanguage () { |
||||
this.compileTabLogic.setLanguage(this._view.languageSelector.value) |
||||
this.compileIfAutoCompileOn() |
||||
} |
||||
|
||||
onchangeEvmVersion () { |
||||
const s = this._view.evmVersionSelector |
||||
let v = s.value |
||||
if (v === 'default') { |
||||
v = null |
||||
} |
||||
this.compileTabLogic.setEvmVersion(v) |
||||
for (let i = 0; i < s.options.length; i++) { |
||||
if (i === s.selectedIndex) { |
||||
s.options[s.selectedIndex].setAttribute('selected', 'selected') |
||||
} else { |
||||
s.options[i].removeAttribute('selected') |
||||
} |
||||
} |
||||
|
||||
this.compileIfAutoCompileOn() |
||||
} |
||||
|
||||
onchangeLoadVersion () { |
||||
this.data.selectedVersion = this._view.versionSelector.value |
||||
this._updateVersionSelector() |
||||
this._updateLanguageSelector() |
||||
} |
||||
|
||||
/* |
||||
The following functions map with the above event handlers. |
||||
They are an external API for modifying the compiler configuration. |
||||
*/ |
||||
setConfiguration (settings) { |
||||
this.setLanguage(settings.language) |
||||
this.setEvmVersion(settings.evmVersion) |
||||
this.setOptimize(settings.optimize) |
||||
this.setRuns(settings.runs) |
||||
this.setVersion(settings.version) |
||||
} |
||||
|
||||
setOptimize (enabled) { |
||||
this._view.optimize.checked = enabled |
||||
this.onchangeOptimize() |
||||
} |
||||
|
||||
setRuns (value) { |
||||
if (value) { |
||||
this._view.runs.value = value |
||||
this.onchangeRuns() |
||||
} |
||||
} |
||||
|
||||
setLanguage (lang) { |
||||
this._view.languageSelector.value = lang |
||||
this.onchangeLanguage() |
||||
} |
||||
|
||||
setEvmVersion (version) { |
||||
this._view.evmVersionSelector.value = version || 'default' |
||||
this.onchangeEvmVersion() |
||||
} |
||||
|
||||
setVersion (version) { |
||||
this._view.versionSelector.value = `soljson-v${version}.js` |
||||
this.onchangeLoadVersion() |
||||
} |
||||
|
||||
_shouldBeAdded (version) { |
||||
return !version.includes('nightly') || |
||||
(version.includes('nightly') && this._view.includeNightlies.checked) |
||||
} |
||||
|
||||
_updateVersionSelector (customUrl = '') { |
||||
// update selectedversion if previous one got filtered out
|
||||
if (!this.data.selectedVersion || !this._shouldBeAdded(this.data.selectedVersion)) { |
||||
this.data.selectedVersion = this.data.defaultVersion |
||||
} |
||||
this._view.versionSelector.innerHTML = '' |
||||
this._view.versionSelector.removeAttribute('disabled') |
||||
this.queryParams.update({ version: this.data.selectedVersion }) |
||||
let url |
||||
if (customUrl !== '') { |
||||
this.data.selectedVersion = customUrl |
||||
this._view.versionSelector.appendChild(yo`<option value="${customUrl}" selected>custom</option>`) |
||||
url = customUrl |
||||
this.queryParams.update({ version: this.data.selectedVersion }) |
||||
} else if (this.data.selectedVersion === 'builtin') { |
||||
let location = window.document.location |
||||
let path = location.pathname |
||||
if (!path.startsWith('/')) path = '/' + path |
||||
location = `${location.protocol}//${location.host}${path}assets/js` |
||||
if (location.endsWith('index.html')) location = location.substring(0, location.length - 10) |
||||
if (!location.endsWith('/')) location += '/' |
||||
url = location + 'soljson.js' |
||||
} else { |
||||
if (this.data.selectedVersion.indexOf('soljson') !== 0 || helper.checkSpecialChars(this.data.selectedVersion)) { |
||||
return console.log('loading ' + this.data.selectedVersion + ' not allowed') |
||||
} |
||||
url = `${urlFromVersion(this.data.selectedVersion)}` |
||||
} |
||||
|
||||
this.data.allversions.forEach(build => { |
||||
const option = build.path === this.data.selectedVersion |
||||
? yo`<option value="${build.path}" selected>${build.longVersion}</option>` |
||||
: yo`<option value="${build.path}">${build.longVersion}</option>` |
||||
|
||||
if (this._shouldBeAdded(option.innerText)) { |
||||
this._view.versionSelector.appendChild(option) |
||||
} |
||||
}) |
||||
if (this.data.selectedVersion !== 'builtin' && semver.lt(this._retrieveVersion(), 'v0.4.12+commit.194ff033.js')) { |
||||
toaster(yo` |
||||
<div> |
||||
<b>Old compiler usage detected.</b> |
||||
<p>You are using a compiler older than v0.4.12.</p> |
||||
<p>Some functionality may not work.</p> |
||||
</div>` |
||||
) |
||||
} |
||||
|
||||
// Workers cannot load js on "file:"-URLs and we get a
|
||||
// "Uncaught RangeError: Maximum call stack size exceeded" error on Chromium,
|
||||
// resort to non-worker version in that case.
|
||||
if (canUseWorker(this._retrieveVersion())) { |
||||
this.compileTabLogic.compiler.loadVersion(true, url) |
||||
this.setVersionText('(loading using worker)') |
||||
} else { |
||||
this.compileTabLogic.compiler.loadVersion(false, url) |
||||
this.setVersionText('(loading)') |
||||
} |
||||
} |
||||
|
||||
_updateLanguageSelector () { |
||||
// This is the first version when Yul is available
|
||||
if (!semver.valid(this._retrieveVersion()) || semver.lt(this._retrieveVersion(), 'v0.5.7+commit.6da8b019.js')) { |
||||
this._view.languageSelector.setAttribute('disabled', '') |
||||
this._view.languageSelector.value = 'Solidity' |
||||
this.compileTabLogic.setLanguage('Solidity') |
||||
} else { |
||||
this._view.languageSelector.removeAttribute('disabled') |
||||
} |
||||
} |
||||
|
||||
setVersionText (text) { |
||||
if (this._view.version) this._view.version.innerText = text |
||||
} |
||||
|
||||
// fetching both normal and wasm builds and creating a [version, baseUrl] map
|
||||
async fetchAllVersion (callback) { |
||||
let selectedVersion, allVersionsWasm, isURL |
||||
let allVersions = [{ path: 'builtin', longVersion: 'Stable local version - 0.8.4' }] |
||||
// fetch normal builds
|
||||
const binRes = await promisedMiniXhr(`${baseURLBin}/list.json`) |
||||
// fetch wasm builds
|
||||
const wasmRes = await promisedMiniXhr(`${baseURLWasm}/list.json`) |
||||
if (binRes.event.type === 'error' && wasmRes.event.type === 'error') { |
||||
selectedVersion = 'builtin' |
||||
return callback(allVersions, selectedVersion) |
||||
} |
||||
try { |
||||
const versions = JSON.parse(binRes.json).builds.slice().reverse() |
||||
allVersions = [...allVersions, ...versions] |
||||
selectedVersion = this.data.defaultVersion |
||||
if (this.queryParams.get().version) selectedVersion = this.queryParams.get().version |
||||
// Check if version is a URL and corresponding filename starts with 'soljson'
|
||||
if (selectedVersion.startsWith('https://')) { |
||||
const urlArr = selectedVersion.split('/') |
||||
if (urlArr[urlArr.length - 1].startsWith('soljson')) isURL = true |
||||
} |
||||
if (wasmRes.event.type !== 'error') { |
||||
allVersionsWasm = JSON.parse(wasmRes.json).builds.slice().reverse() |
||||
} |
||||
} catch (e) { |
||||
addTooltip('Cannot load compiler version list. It might have been blocked by an advertisement blocker. Please try deactivating any of them from this page and reload. Error: ' + e) |
||||
} |
||||
// replace in allVersions those compiler builds which exist in allVersionsWasm with new once
|
||||
if (allVersionsWasm && allVersions) { |
||||
allVersions.forEach((compiler, index) => { |
||||
const wasmIndex = allVersionsWasm.findIndex(wasmCompiler => { return wasmCompiler.longVersion === compiler.longVersion }) |
||||
if (wasmIndex !== -1) { |
||||
allVersions[index] = allVersionsWasm[wasmIndex] |
||||
pathToURL[compiler.path] = baseURLWasm |
||||
} else { |
||||
pathToURL[compiler.path] = baseURLBin |
||||
} |
||||
}) |
||||
} |
||||
callback(allVersions, selectedVersion, isURL) |
||||
} |
||||
|
||||
scheduleCompilation () { |
||||
if (!this.config.get('autoCompile')) return |
||||
if (this.data.compileTimeout) window.clearTimeout(this.data.compileTimeout) |
||||
this.data.compileTimeout = window.setTimeout(() => this.compileIfAutoCompileOn(), this.data.timeout) |
||||
} |
||||
} |
||||
|
||||
module.exports = CompilerContainer |
@ -1,122 +0,0 @@ |
||||
'use strict' |
||||
|
||||
var solcTranslate = require('solc/translate') |
||||
var remixLib = require('@remix-project/remix-lib') |
||||
var txHelper = remixLib.execution.txHelper |
||||
|
||||
module.exports = (contractName, contract, compiledSource) => { |
||||
return getDetails(contractName, contract, compiledSource) |
||||
} |
||||
|
||||
var getDetails = function (contractName, contract, source) { |
||||
var detail = {} |
||||
detail.name = contractName |
||||
detail.metadata = contract.metadata |
||||
if (contract.evm.bytecode.object) { |
||||
detail.bytecode = contract.evm.bytecode.object |
||||
} |
||||
|
||||
detail.abi = contract.abi |
||||
|
||||
if (contract.evm.bytecode.object) { |
||||
detail.bytecode = contract.evm.bytecode |
||||
detail.web3Deploy = gethDeploy(contractName.toLowerCase(), contract.abi, contract.evm.bytecode.object) |
||||
|
||||
detail.metadataHash = retrieveMetadataHash(contract.evm.bytecode.object) |
||||
if (detail.metadataHash) { |
||||
detail.swarmLocation = 'bzzr://' + detail.metadataHash |
||||
} |
||||
} |
||||
|
||||
detail.functionHashes = {} |
||||
for (var fun in contract.evm.methodIdentifiers) { |
||||
detail.functionHashes[contract.evm.methodIdentifiers[fun]] = fun |
||||
} |
||||
|
||||
detail.gasEstimates = formatGasEstimates(contract.evm.gasEstimates) |
||||
|
||||
detail.devdoc = contract.devdoc |
||||
detail.userdoc = contract.userdoc |
||||
|
||||
if (contract.evm.deployedBytecode && contract.evm.deployedBytecode.object.length > 0) { |
||||
detail['Runtime Bytecode'] = contract.evm.deployedBytecode |
||||
} |
||||
|
||||
if (source && contract.assembly !== null) { |
||||
detail.Assembly = solcTranslate.prettyPrintLegacyAssemblyJSON(contract.evm.legacyAssembly, source.content) |
||||
} |
||||
|
||||
return detail |
||||
} |
||||
|
||||
var retrieveMetadataHash = function (bytecode) { |
||||
var match = /a165627a7a72305820([0-9a-f]{64})0029$/.exec(bytecode) |
||||
if (!match) { |
||||
match = /a265627a7a72305820([0-9a-f]{64})6c6578706572696d656e74616cf50037$/.exec(bytecode) |
||||
} |
||||
if (match) { |
||||
return match[1] |
||||
} |
||||
} |
||||
|
||||
var gethDeploy = function (contractName, jsonInterface, bytecode) { |
||||
var code = '' |
||||
var funABI = txHelper.getConstructorInterface(jsonInterface) |
||||
|
||||
funABI.inputs.forEach(function (inp) { |
||||
code += 'var ' + inp.name + ' = /* var of type ' + inp.type + ' here */ ;\n' |
||||
}) |
||||
|
||||
contractName = contractName.replace(/[:./]/g, '_') |
||||
code += 'var ' + contractName + 'Contract = new web3.eth.Contract(' + JSON.stringify(jsonInterface).replace('\n', '') + ');' + |
||||
'\nvar ' + contractName + ' = ' + contractName + 'Contract.deploy({' + |
||||
"\n data: '0x" + bytecode + "', " + |
||||
'\n arguments: [' |
||||
|
||||
funABI.inputs.forEach(function (inp) { |
||||
code += '\n ' + inp.name + ',' |
||||
}) |
||||
|
||||
code += '\n ]' + |
||||
'\n}).send({' + |
||||
'\n from: web3.eth.accounts[0], ' + |
||||
"\n gas: '4700000'" + |
||||
'\n }, function (e, contract){' + |
||||
'\n console.log(e, contract);' + |
||||
"\n if (typeof contract.address !== 'undefined') {" + |
||||
"\n console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);" + |
||||
'\n }' + |
||||
'\n })' |
||||
|
||||
return code |
||||
} |
||||
|
||||
var formatGasEstimates = function (data) { |
||||
if (!data) return {} |
||||
if (data.creation === undefined && data.external === undefined && data.internal === undefined) return {} |
||||
|
||||
var gasToText = function (g) { |
||||
return g === null ? 'unknown' : g |
||||
} |
||||
|
||||
var ret = {} |
||||
var fun |
||||
if ('creation' in data) { |
||||
ret.Creation = data.creation |
||||
} |
||||
|
||||
if ('external' in data) { |
||||
ret.External = {} |
||||
for (fun in data.external) { |
||||
ret.External[fun] = gasToText(data.external[fun]) |
||||
} |
||||
} |
||||
|
||||
if ('internal' in data) { |
||||
ret.Internal = {} |
||||
for (fun in data.internal) { |
||||
ret.Internal[fun] = gasToText(data.internal[fun]) |
||||
} |
||||
} |
||||
return ret |
||||
} |
@ -1,51 +0,0 @@ |
||||
'use strict' |
||||
import * as remixLib from '@remix-project/remix-lib' |
||||
|
||||
const txHelper = remixLib.execution.txHelper |
||||
|
||||
export class CompilerAbstract { |
||||
public languageversion: string |
||||
public data: Record<string, any> |
||||
public source: Record<string, any> |
||||
|
||||
constructor (languageversion, data, source) { |
||||
this.languageversion = languageversion |
||||
this.data = data |
||||
this.source = source // source code
|
||||
} |
||||
|
||||
getContracts () { |
||||
return this.data.contracts |
||||
} |
||||
|
||||
getContract (name) { |
||||
return txHelper.getContract(name, this.data.contracts) |
||||
} |
||||
|
||||
visitContracts (calllback) { |
||||
return txHelper.visitContracts(this.data.contracts, calllback) |
||||
} |
||||
|
||||
getData () { |
||||
return this.data |
||||
} |
||||
|
||||
getAsts () { |
||||
return this.data.sources // ast
|
||||
} |
||||
|
||||
getSourceName (fileIndex) { |
||||
if (this.data && this.data.sources) { |
||||
return Object.keys(this.data.sources)[fileIndex] |
||||
} else if (Object.keys(this.source.sources).length === 1) { |
||||
// if we don't have ast, we return the only one filename present.
|
||||
const sourcesArray = Object.keys(this.source.sources) |
||||
return sourcesArray[0] |
||||
} |
||||
return null |
||||
} |
||||
|
||||
getSourceCode () { |
||||
return this.source |
||||
} |
||||
} |
@ -1,19 +0,0 @@ |
||||
'use strict' |
||||
import { canUseWorker, urlFromVersion } from './compiler-utils' |
||||
import { Compiler } from '@remix-project/remix-solidity' |
||||
import { CompilerAbstract } from './compiler-abstract' |
||||
|
||||
export const compile = async (compilationTargets, settings, contentResolverCallback) => { |
||||
return new Promise((resolve) => { |
||||
const compiler = new Compiler(contentResolverCallback) |
||||
compiler.set('evmVersion', settings.evmVersion) |
||||
compiler.set('optimize', settings.optimize) |
||||
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('compilerLoaded', () => compiler.compile(compilationTargets, '')) |
||||
}) |
||||
} |
@ -1,46 +0,0 @@ |
||||
const semver = require('semver') |
||||
const minixhr = require('minixhr') |
||||
/* global Worker */ |
||||
|
||||
export const baseURLBin = 'https://binaries.soliditylang.org/bin' |
||||
export const baseURLWasm = 'https://binaries.soliditylang.org/wasm' |
||||
|
||||
export const pathToURL = {} |
||||
|
||||
/** |
||||
* Retrieves the URL of the given compiler version |
||||
* @param version is the version of compiler with or without 'soljson-v' prefix and .js postfix |
||||
*/ |
||||
export function urlFromVersion (version) { |
||||
if (!version.startsWith('soljson-v')) version = 'soljson-v' + version |
||||
if (!version.endsWith('.js')) version = version + '.js' |
||||
return `${pathToURL[version]}/${version}` |
||||
} |
||||
|
||||
/** |
||||
* Checks if the worker can be used to load a compiler. |
||||
* checks a compiler whitelist, browser support and OS. |
||||
*/ |
||||
export function canUseWorker (selectedVersion) { |
||||
const version = semver.coerce(selectedVersion) |
||||
const isNightly = selectedVersion.includes('nightly') |
||||
return browserSupportWorker() && ( |
||||
// All compiler versions (including nightlies) after 0.6.3 are wasm compiled
|
||||
semver.gt(version, '0.6.3') || |
||||
// Only releases are wasm compiled starting with 0.3.6
|
||||
(semver.gte(version, '0.3.6') && !isNightly) |
||||
) |
||||
} |
||||
|
||||
function browserSupportWorker () { |
||||
return document.location.protocol !== 'file:' && Worker !== undefined |
||||
} |
||||
|
||||
// returns a promise for minixhr
|
||||
export function promisedMiniXhr (url) { |
||||
return new Promise((resolve) => { |
||||
minixhr(url, (json, event) => { |
||||
resolve({ json, event }) |
||||
}) |
||||
}) |
||||
} |
@ -1,5 +1,2 @@ |
||||
export * from './compileTabLogic' |
||||
export * from './compiler-abstract' |
||||
export * from './compiler-helpers' |
||||
export * from './compiler-utils' |
||||
export * from './contract-parser' |
||||
|
Loading…
Reference in new issue