add .resolveDirectory + refactor remove .list & .listAsTree

pull/3094/head
serapath 7 years ago
parent cd3b84972c
commit 6967f501be
  1. 2
      package.json
  2. 13
      src/app.js
  3. 6
      src/app/compiler/compiler.js
  4. 5
      src/app/files/basicReadOnlyExplorer.js
  5. 132
      src/app/files/browser-files.js
  6. 38
      src/app/files/file-explorer.js
  7. 64
      src/app/files/fileManager.js
  8. 243
      src/app/files/shared-folder.js
  9. 2
      src/app/panels/file-panel.js

@ -44,7 +44,7 @@
"remix-debugger": "latest", "remix-debugger": "latest",
"remix-lib": "latest", "remix-lib": "latest",
"remix-solidity": "latest", "remix-solidity": "latest",
"remixd": "^0.1.2", "remixd": "git+https://github.com/ethereum/remixd.git",
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"selenium-standalone": "^6.0.1", "selenium-standalone": "^6.0.1",
"solc": "https://github.com/ethereum/solc-js", "solc": "https://github.com/ethereum/solc-js",

@ -453,10 +453,15 @@ function run () {
}) })
// insert ballot contract if there are no files available // insert ballot contract if there are no files available
if (!loadingFromGist && Object.keys(filesProviders['browser'].list()).length === 0) { if (!loadingFromGist) {
if (!filesProviders['browser'].set(examples.ballot.name, examples.ballot.content)) { filesProviders['browser'].resolveDirectory('', (error, filesList) => {
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 (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 window.syncStorage = chromeCloudStorageSync

@ -49,7 +49,9 @@ function Compiler (handleImportCall) {
self.lastCompilationResult = null self.lastCompilationResult = null
self.event.trigger('compilationFinished', [false, {'error': { formattedMessage: error, severity: 'error' }}, files]) self.event.trigger('compilationFinished', [false, {'error': { formattedMessage: error, severity: 'error' }}, files])
} else { } else {
compileJSON(input, optimize ? 1 : 0) setTimeout(function () {
compileJSON(input, optimize ? 1 : 0)
}, 0)
} }
}) })
} }
@ -233,7 +235,7 @@ function Compiler (handleImportCall) {
// Set a safe fallback until the new one is loaded // Set a safe fallback until the new one is loaded
setCompileJSON(function (source, optimize) { setCompileJSON(function (source, optimize) {
compilationFinished({error: 'Compiler not yet loaded.'}) compilationFinished({ error: { formattedMessage: 'Compiler not yet loaded.' } })
}) })
var newScript = document.createElement('script') var newScript = document.createElement('script')

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

@ -7,23 +7,17 @@ function Files (storage) {
this.event = event this.event = event
var readonly = {} var readonly = {}
this.type = 'browser' this.type = 'browser'
this.filesTree = null
this.exists = function (path) { this.exists = function (path) {
var unprefixedpath = this.removePrefix(path) var unprefixedpath = this.removePrefix(path)
// NOTE: ignore the config file // NOTE: ignore the config file
if (path === '.remix.config') { if (path === '.remix.config') return false
return false
}
return this.isReadOnly(unprefixedpath) || storage.exists(unprefixedpath) return this.isReadOnly(unprefixedpath) || storage.exists(unprefixedpath)
} }
this.init = function (cb) { this.init = function (cb) {
listAsTree(this, this.list(), (error, tree) => { this.resolveDirectory('', (error, filesTree) => cb && cb(error))
this.filesTree = tree
if (cb) cb(error)
})
} }
this.get = function (path, cb) { this.get = function (path, cb) {
@ -113,29 +107,6 @@ function Files (storage) {
return false 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 // Tree model for files
// { // {
@ -148,23 +119,49 @@ function Files (storage) {
// } // }
// } // }
// //
this.listAsTree = function (path, level) { this.resolveDirectory = function (path, callback) {
var nodes = path ? path.split('/') : [] var self = this
var tree = this.filesTree // path = '' + (path || '')
try { setTimeout(function () {
while (nodes.length) { function hashmapize (obj, path, val) {
var key = nodes.shift() var nodes = path.split('/')
if (key) tree = tree[key] 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
} }
} catch (e) { var filesList = {}
tree = {} // add r/w filesList to the list
} storage.keys().forEach((path) => {
if (level) { // NOTE: as a temporary measure do not show the config file
var leveltree = {} if (path !== '.remix.config') {
build(tree, level, leveltree) filesList[self.type + '/' + path] = false
tree = leveltree }
} })
return tree // add r/o files to the list
Object.keys(readonly).forEach((path) => {
filesList[self.type + '/' + path] = true
})
var tree = {}
// This does not include '.remix.config', because it is filtered
// inside list().
Object.keys(filesList).forEach(function (path) {
hashmapize(tree, path, {
'/readonly': self.isReadOnly(path),
'/content': self.get(path)
})
})
callback(null, tree)
}, 0)
}
this.removePrefix = function (path) {
return path.indexOf(this.type + '/') === 0 ? path.replace(this.type + '/', '') : path
} }
// rename .browser-solidity.json to .remix.config // rename .browser-solidity.json to .remix.config
@ -176,44 +173,3 @@ function Files (storage) {
} }
module.exports = Files module.exports = Files
function build (tree, level, leveltree) {
if (!level) return
Object.keys(tree).forEach(key => {
var value = tree[key]
var more = value === Object(value)
if (more) {
leveltree[key] = {}
build(value, level - 1, leveltree[key])
} else leveltree[key] = value
})
}
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
}
var tree = {}
// This does not include '.remix.config', because it is filtered
// inside list().
Object.keys(filesList).forEach(function (path) {
hashmapize(tree, path, {
'/readonly': self.isReadOnly(path),
'/content': self.get(path)
})
})
callback(null, tree)
}

@ -346,17 +346,21 @@ function fileExplorer (appAPI, files) {
} }
function fileAdded (filepath) { function fileAdded (filepath) {
var el = treeView.render(files.listAsTree()) self.files.resolveDirectory('./', (error, files) => {
el.className = css.fileexplorer if (error) console.error(error)
self.element.parentElement.replaceChild(el, self.element) var element = self.treeView.render(files)
self.element = el element.className = css.fileexplorer
self.element.parentElement.replaceChild(element, self.element)
self.element = element
})
} }
} }
/* /*
HELPER FUNCTIONS HELPER FUNCTIONS
*/ */
function adaptEnvironment (label, focus, hover, li) { function adaptEnvironment (label, focus, hover) {
var li = getLiFrom(label) // @TODO: maybe this gets refactored?
li.style.position = 'relative' li.style.position = 'relative'
var span = li.firstChild var span = li.firstChild
// add focus // add focus
@ -367,7 +371,8 @@ function adaptEnvironment (label, focus, hover, li) {
span.addEventListener('mouseout', hover) span.addEventListener('mouseout', hover)
} }
function unadaptEnvironment (label, focus, hover, li) { function unadaptEnvironment (label, focus, hover) {
var li = getLiFrom(label) // @TODO: maybe this gets refactored?
var span = li.firstChild var span = li.firstChild
li.style.position = undefined li.style.position = undefined
// remove focus // remove focus
@ -411,11 +416,18 @@ function expandPathTo (li) {
} }
fileExplorer.prototype.init = function () { fileExplorer.prototype.init = function () {
var files = this.files.listAsTree() var self = this
var element = this.treeView.render(files) self.files.resolveDirectory('/', (error, files) => {
element.className = css.fileexplorer if (error) console.error(error)
element.events = this.events var element = self.treeView.render(files)
element.api = this.api element.className = css.fileexplorer
this.element = element element.events = self.events
return element 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) { switchFile (file) {
var self = this
if (!file) { if (!file) {
var fileList = Object.keys(this.opt.filesProviders['browser'].list()) self.opt.filesProviders['browser'].resolveDirectory('', (error, filesList) => {
if (fileList.length) { if (error) console.error(error)
file = fileList[0] var fileList = Object.keys(flatten(filesList))
} if (fileList.length) {
} file = fileList[0]
if (!file) return if (file) _switchFile(file)
this.saveCurrentFile() }
this.opt.config.set('currentFile', file) })
this.refreshTabs(file) } else _switchFile(file)
this.fileProviderOf(file).get(file, (error, content) => { function _switchFile () {
if (error) { self.saveCurrentFile()
console.log(error) self.opt.config.set('currentFile', file)
} else { self.refreshTabs(file)
if (this.fileProviderOf(file).isReadOnly(file)) { self.fileProviderOf(file).get(file, (error, content) => {
this.opt.editor.openReadOnly(file, content) if (error) {
console.log(error)
} else { } 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) { fileProviderOf (file) {
@ -173,3 +179,23 @@ class FileManager {
} }
module.exports = 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,62 @@
'use strict' 'use strict'
var async = require('async')
var EventManager = require('remix-lib').EventManager 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) { constructor (remixd) {
this.event = new EventManager() this.event = new EventManager()
this.remixd = remixd this._remixd = remixd
this.files = null this.remixd = remixapi(remixd, this)
this.filesContent = {}
this.filesTree = null
this.type = 'localhost' this.type = 'localhost'
this.error = { this.error = { 'EEXIST': 'File already exists' }
'EEXIST': 'File already exists' this._isReady = true
}
this.remixd.event.register('notified', (data) => { remixd.event.register('notified', (data) => {
if (data.scope === 'sharedfolder') { if (data.scope === 'sharedfolder') {
if (data.name === 'created') { if (data.name === 'created') {
this.init(() => { this.init(() => {
@ -24,7 +67,7 @@ class SharedFolder {
this.event.trigger('fileRemoved', [this.type + '/' + data.value.path]) this.event.trigger('fileRemoved', [this.type + '/' + data.value.path])
}) })
} else if (data.name === 'changed') { } 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) { if (error) {
console.log(error) console.log(error)
} else { } else {
@ -38,38 +81,43 @@ class SharedFolder {
}) })
} }
isConnected () {
return this._isReady
}
close (cb) { close (cb) {
this.remixd.close() this.remixd.exit()
this.files = null this._isReady = false
this.filesTree = null
cb() cb()
} }
init (cb) { init (cb) {
this.remixd.call('sharedfolder', 'list', {}, (error, filesList) => { this._isReady = true
if (error) { cb()
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)
})
}
})
} }
// @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) { 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 if (!this.files) return false
return this.files[path] !== undefined return this.files[path] !== undefined
} }
get (path, cb) { get (path, cb) {
var unprefixedpath = this.removePrefix(path) var unprefixedpath = this.removePrefix(path)
this.remixd.call('sharedfolder', 'get', {path: unprefixedpath}, (error, content) => { this._remixd.call('sharedfolder', 'get', {path: unprefixedpath}, (error, content) => {
if (!error) { if (!error) {
this.filesContent[path] = content this.filesContent[path] = content
cb(error, content) cb(error, content)
@ -83,7 +131,7 @@ class SharedFolder {
set (path, content, cb) { set (path, content, cb) {
var unprefixedpath = this.removePrefix(path) 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) if (cb) cb(error, result)
var path = this.type + '/' + unprefixedpath var path = this.type + '/' + unprefixedpath
this.filesContent[path] this.filesContent[path]
@ -103,7 +151,7 @@ class SharedFolder {
remove (path) { remove (path) {
var unprefixedpath = this.removePrefix(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) if (error) console.log(error)
var path = this.type + '/' + unprefixedpath var path = this.type + '/' + unprefixedpath
delete this.filesContent[path] delete this.filesContent[path]
@ -116,7 +164,7 @@ class SharedFolder {
rename (oldPath, newPath, isFolder) { rename (oldPath, newPath, isFolder) {
var unprefixedoldPath = this.removePrefix(oldPath) var unprefixedoldPath = this.removePrefix(oldPath)
var unprefixednewPath = this.removePrefix(newPath) 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) { if (error) {
console.log(error) console.log(error)
if (this.error[error.code]) error = this.error[error.code] if (this.error[error.code]) error = this.error[error.code]
@ -134,27 +182,26 @@ class SharedFolder {
return true return true
} }
list () { //
return this.files // Tree model for files
} // {
// 'a': { }, // empty directory 'a'
listAsTree (path, level) { // 'b': {
var nodes = path ? path.split('/') : [] // 'c': {}, // empty directory 'b/c'
var tree = this.filesTree // 'd': { '/readonly': true, '/content': 'Hello World' } // files 'b/c/d'
try { // 'e': { '/readonly': false, '/path': 'b/c/d' } // symlink to 'b/c/d'
while (nodes.length) { // 'f': { '/readonly': false, '/content': '<executable>', '/mode': 0755 }
var key = nodes.shift() // }
if (key) tree = tree[key] // }
} //
} catch (e) { resolveDirectory (path, callback) {
tree = {} var self = this
} path = '' + (path || '')
if (level) { path = pathtool.join('./', path)
var leveltree = {} buildList(self, path, (error, fileTree) => {
build(tree, level, leveltree) if (error) return callback(error)
tree = leveltree callback(null, { [self.type]: fileTree })
} })
return tree
} }
removePrefix (path) { removePrefix (path) {
@ -162,67 +209,33 @@ class SharedFolder {
} }
} }
// function remixapi (remixd, self) {
// Tree model for files const read = (path, callback) => {
// { path = '' + (path || '')
// 'a': { }, // empty directory 'a' path = pathtool.join('./', path)
// 'b': { remixd.call('sharedfolder', 'get', { path }, (error, content) => callback(error, content))
// '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 build (tree, level, leveltree) {
if (!level) return
Object.keys(tree).forEach(key => {
var value = tree[key]
var more = value === Object(value)
if (more) {
leveltree[key] = {}
build(value, level - 1, leveltree[key])
} else leveltree[key] = value
})
}
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
} }
const write = (path, content, callback) => {
var tree = {} path = '' + (path || '')
path = pathtool.join('./', path)
// This does not include '.remix.config', because it is filtered remixd.call('sharedfolder', 'set', { path, content }, (error, result) => callback(error, result))
// inside list(). }
async.eachSeries(Object.keys(filesList), function (path, cb) { const rename = (path, newpath, callback) => {
self.get(path, (error, content) => { path = '' + (path || '')
if (error) { path = pathtool.join('./', path)
console.log(error) remixd.call('sharedfolder', 'rename', { oldPath: path, newPath: newpath }, (error, result) => callback(error, result))
cb(error) }
} else { const remove = (path, callback) => {
self.filesContent[path] = content path = '' + (path || '')
hashmapize(tree, path, { path = pathtool.join('./', path)
'/readonly': filesList[path], remixd.call('sharedfolder', 'remove', { path }, (error, result) => callback(error, result))
'/content': content }
}) const dir = (path, callback) => {
cb() path = '' + (path || '')
} path = pathtool.join('./', path)
}) remixd.call('sharedfolder', 'resolveDirectory', { path }, (error, filesList) => callback(error, filesList))
}, (error) => { }
callback(error, tree) 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 () { function connectToLocalhost () {
var container = document.querySelector('.filesystemexplorer') var container = document.querySelector('.filesystemexplorer')
if (filesProvider['localhost'].files !== null) { if (filesProvider['localhost'].isConnected()) {
filesProvider['localhost'].close((error) => { filesProvider['localhost'].close((error) => {
if (error) console.log(error) if (error) console.log(error)
}) })

Loading…
Cancel
Save