Merge pull request #944 from ethereum/file-exlorer

add support for large directories to file-explorer
pull/1/head
yann300 7 years ago committed by GitHub
commit 419dcc71d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      ci/browser_tests.sh
  2. 7
      package.json
  3. 13
      src/app.js
  4. 2
      src/app/compiler/compiler.js
  5. 5
      src/app/files/basicReadOnlyExplorer.js
  6. 56
      src/app/files/browser-files.js
  7. 42
      src/app/files/file-explorer.js
  8. 64
      src/app/files/fileManager.js
  9. 222
      src/app/files/shared-folder.js
  10. 2
      src/app/panels/file-panel.js

@ -3,7 +3,6 @@
set -e
setupRemixd () {
npm install remixd
mkdir remixdSharedfolder
cd remixdSharedfolder
echo "contract test1 { function get () returns (uint) { return 8; }}" > contract1.sol
@ -15,7 +14,7 @@ setupRemixd () {
cd ..
echo 'sharing folder: '
echo $PWD
./../node_modules/.bin/remixd -S $PWD &
node ../node_modules/remixd/src/main.js -s $PWD &
cd ..
}

@ -9,6 +9,7 @@
"babel-plugin-transform-object-assign": "^6.22.0",
"babel-plugin-yo-yoify": "^0.3.3",
"babel-polyfill": "^6.22.0",
"babel-preset-env": "^1.6.1",
"babel-preset-es2015": "^6.24.0",
"babel-preset-stage-0": "^6.24.1",
"babelify": "^7.3.0",
@ -44,7 +45,7 @@
"remix-debugger": "latest",
"remix-lib": "latest",
"remix-solidity": "latest",
"remixd": "^0.1.2",
"remixd": "git+https://github.com/ethereum/remixd.git",
"rimraf": "^2.6.1",
"selenium-standalone": "^6.0.1",
"solc": "https://github.com/ethereum/solc-js",
@ -155,12 +156,12 @@
"nightwatch_remote_safari": "nightwatch --config nightwatch.js --env safari",
"onchange": "onchange build/app.js -- npm-run-all lint",
"prepublish": "mkdirp build; npm-run-all -ls downloadsolc build",
"remixd": "remixd -S ./contracts",
"remixd": "node ./node_modules/remixd/src/main.js -s ./contracts",
"selenium": "execr --silent selenium-standalone start",
"selenium-install": "selenium-standalone install",
"serve": "execr --silent http-server .",
"sourcemap": "exorcist --root ../ build/app.js.map > build/app.js",
"start": "npm-run-all -lpr serve watch onchange",
"start": "npm-run-all -lpr serve watch onchange remixd",
"test": "npm run csslint; standard && node test/index.js",
"test-browser": "npm-run-all -lpr selenium downloadsolc make-mock-compiler serve browsertest",
"watch": "watchify src/index.js -dv -p browserify-reload -o build/app.js"

@ -453,10 +453,15 @@ function run () {
})
// insert ballot contract if there are no files available
if (!loadingFromGist && Object.keys(filesProviders['browser'].list()).length === 0) {
if (!filesProviders['browser'].set(examples.ballot.name, examples.ballot.content)) {
modalDialogCustom.alert('Failed to store example contract in browser. Remix will not work properly. Please ensure Remix has access to LocalStorage. Safari in Private mode is known not to work.')
}
if (!loadingFromGist) {
filesProviders['browser'].resolveDirectory('', (error, filesList) => {
if (error) console.error(error)
if (Object.keys(filesList).length === 0) {
if (!filesProviders['browser'].set(examples.ballot.name, examples.ballot.content)) {
modalDialogCustom.alert('Failed to store example contract in browser. Remix will not work properly. Please ensure Remix has access to LocalStorage. Safari in Private mode is known not to work.')
}
}
})
}
window.syncStorage = chromeCloudStorageSync

@ -233,7 +233,7 @@ function Compiler (handleImportCall) {
// Set a safe fallback until the new one is loaded
setCompileJSON(function (source, optimize) {
compilationFinished({error: 'Compiler not yet loaded.'})
compilationFinished({ error: { formattedMessage: 'Compiler not yet loaded.' } })
})
var newScript = document.createElement('script')

@ -80,7 +80,8 @@ class BasicReadOnlyExplorer {
// }
// }
//
listAsTree () {
resolveDirectory (path, callback /* (error, filesList) => { } */) {
// path = '' + (path || '')
function hashmapize (obj, path, val) {
var nodes = path.split('/')
var i = 0
@ -107,7 +108,7 @@ class BasicReadOnlyExplorer {
'/content': self.get(path)
})
})
return tree
callback(null, tree)
}
removePrefix (path) {

@ -11,9 +11,7 @@ function Files (storage) {
this.exists = function (path) {
var unprefixedpath = this.removePrefix(path)
// NOTE: ignore the config file
if (path === '.remix.config') {
return false
}
if (path === '.remix.config') return false
return this.isReadOnly(unprefixedpath) || storage.exists(unprefixedpath)
}
@ -105,29 +103,6 @@ function Files (storage) {
return false
}
this.list = function () {
var files = {}
// add r/w files to the list
storage.keys().forEach((path) => {
// NOTE: as a temporary measure do not show the config file
if (path !== '.remix.config') {
files[this.type + '/' + path] = false
}
})
// add r/o files to the list
Object.keys(readonly).forEach((path) => {
files[this.type + '/' + path] = true
})
return files
}
this.removePrefix = function (path) {
return path.indexOf(this.type + '/') === 0 ? path.replace(this.type + '/', '') : path
}
//
// Tree model for files
// {
@ -140,11 +115,12 @@ function Files (storage) {
// }
// }
//
this.listAsTree = function () {
this.resolveDirectory = function (path, callback) {
var self = this
// path = '' + (path || '')
function hashmapize (obj, path, val) {
var nodes = path.split('/')
var i = 0
for (; i < nodes.length - 1; i++) {
var node = nodes[i]
if (obj[node] === undefined) {
@ -152,22 +128,34 @@ function Files (storage) {
}
obj = obj[node]
}
obj[nodes[i]] = val
}
var filesList = {}
// add r/w filesList to the list
storage.keys().forEach((path) => {
// NOTE: as a temporary measure do not show the config file
if (path !== '.remix.config') {
filesList[self.type + '/' + path] = false
}
})
// add r/o files to the list
Object.keys(readonly).forEach((path) => {
filesList[self.type + '/' + path] = true
})
var tree = {}
var self = this
// This does not include '.remix.config', because it is filtered
// inside list().
Object.keys(this.list()).forEach(function (path) {
Object.keys(filesList).forEach(function (path) {
hashmapize(tree, path, {
'/readonly': self.isReadOnly(path),
'/content': self.get(path)
})
})
return tree
callback(null, tree)
}
this.removePrefix = function (path) {
return path.indexOf(this.type + '/') === 0 ? path.replace(this.type + '/', '') : path
}
// rename .browser-solidity.json to .remix.config

@ -99,13 +99,13 @@ function fileExplorer (appAPI, files) {
})) : undefined
}
},
formatSelf: function (key, data) {
formatSelf: function (key, data, li) {
var isRoot = data.path.indexOf('/') === -1
return yo`<label class="${data.children ? css.folder : css.file}"
data-path="${data.path}"
style="${isRoot ? 'font-weight:bold;' : ''}"
onload=${function (el) { adaptEnvironment(el, focus, hover) }}
onunload=${function (el) { unadaptEnvironment(el, focus, hover) }}
onload=${function (el) { adaptEnvironment(el, focus, hover, li) }}
onunload=${function (el) { unadaptEnvironment(el, focus, hover, li) }}
onclick=${editModeOn}
onkeydown=${editModeOff}
onblur=${editModeOff}
@ -346,10 +346,13 @@ function fileExplorer (appAPI, files) {
}
function fileAdded (filepath) {
var el = treeView.render(files.listAsTree())
el.className = css.fileexplorer
self.element.parentElement.replaceChild(el, self.element)
self.element = el
self.files.resolveDirectory('./', (error, files) => {
if (error) console.error(error)
var element = self.treeView.render(files)
element.className = css.fileexplorer
self.element.parentElement.replaceChild(element, self.element)
self.element = element
})
}
}
@ -357,7 +360,7 @@ function fileExplorer (appAPI, files) {
HELPER FUNCTIONS
*/
function adaptEnvironment (label, focus, hover) {
var li = getLiFrom(label)
var li = getLiFrom(label) // @TODO: maybe this gets refactored?
li.style.position = 'relative'
var span = li.firstChild
// add focus
@ -369,7 +372,7 @@ function adaptEnvironment (label, focus, hover) {
}
function unadaptEnvironment (label, focus, hover) {
var li = getLiFrom(label)
var li = getLiFrom(label) // @TODO: maybe this gets refactored?
var span = li.firstChild
li.style.position = undefined
// remove focus
@ -413,11 +416,18 @@ function expandPathTo (li) {
}
fileExplorer.prototype.init = function () {
var files = this.files.listAsTree()
var element = this.treeView.render(files)
element.className = css.fileexplorer
element.events = this.events
element.api = this.api
this.element = element
return element
var self = this
self.files.resolveDirectory('/', (error, files) => {
if (error) console.error(error)
var element = self.treeView.render(files)
element.className = css.fileexplorer
element.events = self.events
element.api = self.api
setTimeout(function () {
self.element.parentElement.replaceChild(element, self.element)
self.element = element
}, 0)
})
self.element = yo`<div></div>`
return self.element
}

@ -120,28 +120,34 @@ class FileManager {
}
switchFile (file) {
var self = this
if (!file) {
var fileList = Object.keys(this.opt.filesProviders['browser'].list())
if (fileList.length) {
file = fileList[0]
}
}
if (!file) return
this.saveCurrentFile()
this.opt.config.set('currentFile', file)
this.refreshTabs(file)
this.fileProviderOf(file).get(file, (error, content) => {
if (error) {
console.log(error)
} else {
if (this.fileProviderOf(file).isReadOnly(file)) {
this.opt.editor.openReadOnly(file, content)
self.opt.filesProviders['browser'].resolveDirectory('', (error, filesTree) => {
if (error) console.error(error)
var fileList = Object.keys(flatten(filesTree))
if (fileList.length) {
file = fileList[0]
if (file) _switchFile(file)
}
})
} else _switchFile(file)
function _switchFile () {
self.saveCurrentFile()
self.opt.config.set('currentFile', file)
self.refreshTabs(file)
self.fileProviderOf(file).get(file, (error, content) => {
if (error) {
console.log(error)
} else {
this.opt.editor.open(file, content)
if (self.fileProviderOf(file).isReadOnly(file)) {
self.opt.editor.openReadOnly(file, content)
} else {
self.opt.editor.open(file, content)
}
self.event.trigger('currentFileChanged', [file, self.fileProviderOf(file)])
}
this.event.trigger('currentFileChanged', [file, this.fileProviderOf(file)])
}
})
})
}
}
fileProviderOf (file) {
@ -173,3 +179,23 @@ class FileManager {
}
module.exports = FileManager
function flatten (tree) {
var flat = {}
var names = Object.keys(tree || {})
if (!names.length) return
else {
names.forEach(name => {
if ('/content' in tree[name]) flat[name] = false
else {
var subflat = flatten(tree[name])
if (!subflat) {
// empty folder
} else {
Object.keys(subflat).forEach(path => { flat[name + '/' + path] = false })
}
}
})
return flat
}
}

@ -1,19 +1,63 @@
'use strict'
var async = require('async')
var EventManager = require('remix-lib').EventManager
var pathtool = require('path')
function buildList (self, path = '', callback) {
path = '' + (path || '')
self.remixd.dir(path, (error, filesList) => {
if (error) console.error(error)
var list = Object.keys(filesList)
var counter = list.length
var fileTree = {}
if (!counter) callback(null, fileTree)
for (var i = 0, name, len = counter; i < len; i++) {
name = list[i]
if (filesList[name].isDirectory) {
setFolder(self, path, name, fileTree, finish)
} else {
setFileContent(self, path, name, fileTree, finish)
}
}
function finish (error) {
if (error) console.error(error)
counter--
if (!counter) callback(null, fileTree)
}
})
}
function setFolder (self, path, name, fileTree, done) {
buildList(self, name, (error, subFileTree) => {
if (error) console.error(error)
name = name.replace(path, '')
if (name[0] === '/') name = name.substring(1)
fileTree[name] = subFileTree
done(null)
})
}
function setFileContent (self, path, name, fileTree, done) {
self.remixd.read(name, (error, result) => {
if (error) console.error(error)
name = name.replace(path, '')
if (name[0] === '/') name = name.substring(1)
fileTree[name] = {
'/content': result.content,
'/readonly': result.readonly
}
done(null)
})
}
class SharedFolder {
module.exports = class SharedFolder {
constructor (remixd) {
this.event = new EventManager()
this.remixd = remixd
this.files = null
this.filesContent = {}
this.filesTree = null
this._remixd = remixd
this.remixd = remixapi(remixd, this)
this.type = 'localhost'
this.error = {
'EEXIST': 'File already exists'
}
this.remixd.event.register('notified', (data) => {
this.error = { 'EEXIST': 'File already exists' }
this._isReady = false
this.filesContent = {}
remixd.event.register('notified', (data) => {
if (data.scope === 'sharedfolder') {
if (data.name === 'created') {
this.init(() => {
@ -24,7 +68,7 @@ class SharedFolder {
this.event.trigger('fileRemoved', [this.type + '/' + data.value.path])
})
} else if (data.name === 'changed') {
this.remixd.call('sharedfolder', 'get', {path: data.value}, (error, content) => {
this._remixd.call('sharedfolder', 'get', {path: data.value}, (error, content) => {
if (error) {
console.log(error)
} else {
@ -38,41 +82,46 @@ class SharedFolder {
})
}
isConnected () {
return this._isReady
}
close (cb) {
this.remixd.close()
this.files = null
this.filesTree = null
this.remixd.exit()
this._isReady = false
cb()
}
init (cb) {
this.remixd.call('sharedfolder', 'list', {}, (error, filesList) => {
if (error) {
cb(error)
} else {
this.files = {}
for (var k in filesList) {
this.files[this.type + '/' + k] = filesList[k]
}
listAsTree(this, this.files, (error, tree) => {
this.filesTree = tree
cb(error)
})
}
})
this._isReady = true
cb()
}
// @TODO: refactor all `this._remixd.call(....)` uses into `this.remixd[api](...)`
// where `api = ...`:
// this.remixd.read(path, (error, content) => {})
// this.remixd.write(path, content, (error, result) => {})
// this.remixd.rename(path1, path2, (error, result) => {})
// this.remixd.remove(path, (error, result) => {})
// this.remixd.dir(path, (error, filesList) => {})
//
// this.remixd.exists(path, (error, isValid) => {})
exists (path) {
// @TODO: add new remixd.exists() method
// we remove the this.files = null at the beggining
// modify the exists() (cause it is using the this.files) to use remixd
// yes for the exists I think you might need another remixd function
if (!this.files) return false
return this.files[path] !== undefined
}
get (path, cb) {
var unprefixedpath = this.removePrefix(path)
this.remixd.call('sharedfolder', 'get', {path: unprefixedpath}, (error, content) => {
this._remixd.call('sharedfolder', 'get', {path: unprefixedpath}, (error, file) => {
if (!error) {
this.filesContent[path] = content
cb(error, content)
this.filesContent[path] = file.content
cb(error, file.content)
} else {
// display the last known content.
// TODO should perhaps better warn the user that the file is not synced.
@ -83,10 +132,9 @@ class SharedFolder {
set (path, content, cb) {
var unprefixedpath = this.removePrefix(path)
this.remixd.call('sharedfolder', 'set', {path: unprefixedpath, content: content}, (error, result) => {
this._remixd.call('sharedfolder', 'set', {path: unprefixedpath, content: content}, (error, result) => {
if (cb) cb(error, result)
var path = this.type + '/' + unprefixedpath
this.filesContent[path]
this.event.trigger('fileChanged', [path])
})
return true
@ -103,7 +151,7 @@ class SharedFolder {
remove (path) {
var unprefixedpath = this.removePrefix(path)
this.remixd.call('sharedfolder', 'remove', {path: unprefixedpath}, (error, result) => {
this._remixd.call('sharedfolder', 'remove', {path: unprefixedpath}, (error, result) => {
if (error) console.log(error)
var path = this.type + '/' + unprefixedpath
delete this.filesContent[path]
@ -116,7 +164,7 @@ class SharedFolder {
rename (oldPath, newPath, isFolder) {
var unprefixedoldPath = this.removePrefix(oldPath)
var unprefixednewPath = this.removePrefix(newPath)
this.remixd.call('sharedfolder', 'rename', {oldPath: unprefixedoldPath, newPath: unprefixednewPath}, (error, result) => {
this._remixd.call('sharedfolder', 'rename', {oldPath: unprefixedoldPath, newPath: unprefixednewPath}, (error, result) => {
if (error) {
console.log(error)
if (this.error[error.code]) error = this.error[error.code]
@ -134,12 +182,26 @@ class SharedFolder {
return true
}
list () {
return this.files
}
listAsTree () {
return this.filesTree
//
// Tree model for files
// {
// 'a': { }, // empty directory 'a'
// 'b': {
// 'c': {}, // empty directory 'b/c'
// 'd': { '/readonly': true, '/content': 'Hello World' } // files 'b/c/d'
// 'e': { '/readonly': false, '/path': 'b/c/d' } // symlink to 'b/c/d'
// 'f': { '/readonly': false, '/content': '<executable>', '/mode': 0755 }
// }
// }
//
resolveDirectory (path, callback) {
var self = this
path = '' + (path || '')
path = pathtool.join('./', path)
buildList(self, path, (error, fileTree) => {
if (error) return callback(error)
callback(null, { [self.type]: fileTree })
})
}
removePrefix (path) {
@ -147,55 +209,33 @@ class SharedFolder {
}
}
//
// Tree model for files
// {
// 'a': { }, // empty directory 'a'
// 'b': {
// 'c': {}, // empty directory 'b/c'
// 'd': { '/readonly': true, '/content': 'Hello World' } // files 'b/c/d'
// 'e': { '/readonly': false, '/path': 'b/c/d' } // symlink to 'b/c/d'
// 'f': { '/readonly': false, '/content': '<executable>', '/mode': 0755 }
// }
// }
//
function listAsTree (self, filesList, callback) {
function hashmapize (obj, path, val) {
var nodes = path.split('/')
var i = 0
for (; i < nodes.length - 1; i++) {
var node = nodes[i]
if (obj[node] === undefined) {
obj[node] = {}
}
obj = obj[node]
}
obj[nodes[i]] = val
function remixapi (remixd, self) {
const read = (path, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'get', { path }, (error, content) => callback(error, content))
}
var tree = {}
// This does not include '.remix.config', because it is filtered
// inside list().
async.eachSeries(Object.keys(filesList), function (path, cb) {
self.get(path, (error, content) => {
if (error) {
console.log(error)
cb(error)
} else {
self.filesContent[path] = content
hashmapize(tree, path, {
'/readonly': filesList[path],
'/content': content
})
cb()
}
})
}, (error) => {
callback(error, tree)
})
const write = (path, content, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'set', { path, content }, (error, result) => callback(error, result))
}
const rename = (path, newpath, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'rename', { oldPath: path, newPath: newpath }, (error, result) => callback(error, result))
}
const remove = (path, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'remove', { path }, (error, result) => callback(error, result))
}
const dir = (path, callback) => {
path = '' + (path || '')
path = pathtool.join('./', path)
remixd.call('sharedfolder', 'resolveDirectory', { path }, (error, filesList) => callback(error, filesList))
}
const exit = () => { remixd.close() }
const api = { read, write, rename, remove, dir, exit, event: remixd.event }
return api
}
module.exports = SharedFolder

@ -305,7 +305,7 @@ function filepanel (appAPI, filesProvider) {
*/
function connectToLocalhost () {
var container = document.querySelector('.filesystemexplorer')
if (filesProvider['localhost'].files !== null) {
if (filesProvider['localhost'].isConnected()) {
filesProvider['localhost'].close((error) => {
if (error) console.log(error)
})

Loading…
Cancel
Save