Merge pull request #769 from ethereum/terminalFilter

add terminal menu bar with clear/filter/..
pull/1/head
yann300 8 years ago committed by GitHub
commit 46944c934f
  1. 24
      src/app/execution/txLogger.js
  2. 1
      src/app/files/file-explorer.js
  3. 57
      src/app/panels/editor-panel.js
  4. 393
      src/app/panels/terminal.js
  5. 4
      test-browser/tests/compiling.js

@ -16,19 +16,25 @@ class TxLogger {
constructor (opts = {}) {
this.event = new EventManager()
this.opts = opts
opts.api.editorpanel.registerLogType('knownTransaction', (data) => {
return renderKnownTransaction(this, data)
this.logKnownTX = opts.api.editorpanel.registerCommand('knownTransaction', (args, cmds, append) => {
var data = args[0]
var el = renderKnownTransaction(this, data)
append(el)
})
opts.api.editorpanel.registerLogType('unknownTransaction', (data) => {
return renderUnknownTransaction(this, data)
this.logUnknownTX = opts.api.editorpanel.registerCommand('unknownTransaction', (args, cmds, append) => {
var data = args[0]
var el = renderUnknownTransaction(this, data)
append(el)
})
opts.api.editorpanel.registerLogType('emptyBlock', (data) => {
return renderEmptyBlock(this, data)
this.logEmptyBlock = opts.api.editorpanel.registerCommand('emptyBlock', (args, cmds, append) => {
var data = args[0]
var el = renderEmptyBlock(this, data)
append(el)
})
opts.events.txListener.register('newBlock', (block) => {
if (!block.transactions.length) {
opts.api.editorpanel.log({type: 'emptyBlock', value: { block: block }})
this.logEmptyBlock({ block: block })
}
})
@ -43,12 +49,12 @@ function log (self, tx, api) {
if (resolvedTransaction) {
api.parseLogs(tx, resolvedTransaction.contractName, api.compiledContracts(), (error, logs) => {
if (!error) {
api.editorpanel.log({type: 'knownTransaction', value: { tx: tx, resolvedData: resolvedTransaction, logs: logs }})
self.logKnownTX({ tx: tx, resolvedData: resolvedTransaction, logs: logs })
}
})
} else {
// contract unknown - just displaying raw tx.
api.editorpanel.log({ type: 'unknownTransaction', value: { tx: tx } })
self.logUnknownTX({ tx: tx })
}
}

@ -70,7 +70,6 @@ function fileExplorer (appAPI, files) {
}
})
var self = this
var fileEvents = files.event
var treeView = new Treeview({
extractData: function (value, tree, key) {

@ -157,7 +157,49 @@ class EditorPanel {
context () {
return self._api.context()
}
}
},
banner: `
/******************************************************************************
...........................................
.....................:.....................
....................o:;....................
...................oo:;;...................
..................ooo:;;;..................
.................oooo:;;;;.................
................ooooo:;;;;;................
...............oooooo:;;;;;;...............
..............ooooooo:;;;;;;;..............
.............ooooooo;:';;;;;;;.............
............ooooo;;;;:'''';;;;;............
...........oo;;;;;;;;:'''''''';;...........
..........;;;;;;;;;;;:'''''''''''..........
..............;;;;;;;:'''''''..............
...........oo...;;;;;:'''''...;;...........
............oooo...;;:''...;;;;............
..............oooo...:...;;;;..............
...............oooooo:;;;;;;...............
................ooooo:;;;;;................
.................oooo:;;;;.................
..................ooo:;;;..................
...................oo:;;...................
....................o:;....................
.....................:.....................
...........................................
######## ######## ## ## #### ## ##
## ## ## ### ### ## ## ##
## ## ## #### #### ## ## ##
######## ###### ## ### ## ## ###
## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ##
## ## ######## ## ## #### ## ##
welcome to browser solidity
******************************************************************************/
`
})
}
self._components.terminal.event.register('resize', delta => self._adjustLayout('top', delta))
@ -194,6 +236,11 @@ class EditorPanel {
var self = this
self._view.tabs.onmouseenter()
}
log (data = {}) {
var self = this
var command = self._components.terminal.commands[data.type]
if (typeof command === 'function') command(data.value)
}
render () {
var self = this
if (self._view.el) return self._view.el
@ -215,13 +262,9 @@ class EditorPanel {
self._adjustLayout('top', self.data._layout.top.offset)
return self._view.el
}
registerLogType (typename, template) {
var self = this
self._components.terminal.registerType(typename, template)
}
log () {
registerCommand (name, command) {
var self = this
self._components.terminal._output.apply(self._components.terminal, arguments)
return self._components.terminal.registerCommand(name, command)
}
_renderTabsbar () {
var self = this

@ -1,4 +1,4 @@
/* global Node */
/* global Node, requestAnimationFrame */
var yo = require('yo-yo')
var csjs = require('csjs-inject')
var javascriptserialize = require('javascript-serialize')
@ -9,6 +9,7 @@ var EventManager = require('ethereum-remix').lib.EventManager
var Web3 = require('web3')
var executionContext = require('../../execution-context')
var Dropdown = require('../ui/dropdown')
var css = csjs`
.panel {
@ -27,17 +28,29 @@ var css = csjs`
.bar {
display : flex;
justify-content : flex-end;
min-height : 1.7em;
padding : 2px;
cursor : ns-resize;
background-color : #eef;
z-index : 3;
}
.menu {
position : relative;
display : flex;
align-items : center;
width : 100%;
}
.title {
margin-right : 15px;
}
.minimize {
text-align : center;
padding-top : 3px;
margin-left : auto;
width : 10px;
min-height : 100%;
cursor : pointer;
color : black;
}
.clear {
margin-right : 5px;
font-size : 15px;
cursor : pointer;
color : black;
}
@ -78,17 +91,7 @@ var css = csjs`
outline : none;
font-family : monospace;
}
.error {
color : red;
}
.info {
color : blue;
}
.log {
color : black;
}
.dragbarHorizontal {
position : absolute;
top : 0;
@ -120,63 +123,49 @@ var ghostbar = yo`<div class=${css.ghostbar}></div>`
class Terminal {
constructor (opts = { auto: true }) {
var self = this
self.event = new EventManager()
self._api = opts.api
self.data = {
lineLength: opts.lineLength || 80,
session: [],
banner: opts.banner || `
/******************************************************************************
...........................................
.....................:.....................
....................o:;....................
...................oo:;;...................
..................ooo:;;;..................
.................oooo:;;;;.................
................ooooo:;;;;;................
...............oooooo:;;;;;;...............
..............ooooooo:;;;;;;;..............
.............ooooooo;:';;;;;;;.............
............ooooo;;;;:'''';;;;;............
...........oo;;;;;;;;:'''''''';;...........
..........;;;;;;;;;;;:'''''''''''..........
..............;;;;;;;:'''''''..............
...........oo...;;;;;:'''''...;;...........
............oooo...;;:''...;;;;............
..............oooo...:...;;;;..............
...............oooooo:;;;;;;...............
................ooooo:;;;;;................
.................oooo:;;;;.................
..................ooo:;;;..................
...................oo:;;...................
....................o:;....................
.....................:.....................
...........................................
######## ######## ## ## #### ## ##
## ## ## ### ### ## ## ##
## ## ## #### #### ## ## ##
######## ###### ## ### ## ## ###
## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ##
## ## ######## ## ## #### ## ##
welcome to browser solidity
new features:
- dom terminal v0.0.1-alpha
******************************************************************************/
`
banner: opts.banner,
activeFilters: { commands: {}, input: '' }
}
self.event = new EventManager()
self._api = opts.api
self._view = { el: null, bar: null, input: null, term: null, journal: null, cli: null }
self._templates = {}
self.logger = {}
;['log', 'info', 'error'].forEach(typename => {
self.registerType(typename, self._blocksRenderer(typename))
self._components = {}
self._components.dropdown = new Dropdown({
options: [
'knownTransaction',
'unknownTransaction',
'script'
],
defaults: ['knownTransaction', 'script']
})
self._components.dropdown.event.register('deselect', function (label) {
self.updateJournal({ type: 'deselect', value: label })
})
self._components.dropdown.event.register('select', function (label) {
self.updateJournal({ type: 'select', value: label })
})
self._commands = {}
self.commands = {}
self._JOURNAL = []
self._jobs = []
self._INDEX = {}
self._INDEX.all = []
self._INDEX.allMain = []
self._INDEX.commands = {}
self._INDEX.commandsMain = {}
self.registerCommand('log', self._blocksRenderer('log'))
self.registerCommand('info', self._blocksRenderer('info'))
self.registerCommand('error', self._blocksRenderer('error'))
self.registerCommand('script', function execute (args, scopedCommands, append) {
var script = String(args[0])
scopedCommands.log(`> ${script}`)
self._shell(script, scopedCommands, function (error, output) {
if (error) scopedCommands.error(error)
else scopedCommands.log(output)
})
})
self._jsSandboxContext = {}
self._jsSandbox = vm.createContext(self._jsSandboxContext)
@ -198,14 +187,23 @@ class Terminal {
`
self._view.icon = yo`<i onmouseenter=${hover} onmouseleave=${hover} onmousedown=${minimize} class="${css.minimize} fa fa-angle-double-down"></i>`
self._view.dragbar = yo`<div onmousedown=${mousedown} class=${css.dragbarHorizontal}></div>`
self._view.dropdown = self._components.dropdown.render()
self._view.bar = yo`
<div class=${css.bar}>
${self._view.dragbar}
${self._view.icon}
<div class=${css.menu}>
<div class=${css.title}>Remix Terminal</div>
<div class=${css.clear} onclick=${clear}>
<i class="fa fa-ban" aria-hidden="true" onmouseenter=${hover} onmouseleave=${hover}></i>
</div>
${self._view.dropdown}
<input type="text" class=${css.filter} onkeyup=${filter}></div>
${self._view.icon}
</div>
</div>
`
self._view.term = yo`
<div class=${css.terminal} onscroll=${throttle(reattach, 50)} onclick=${focusinput}>
<div class=${css.terminal} onscroll=${throttle(reattach, 10)} onclick=${focusinput}>
${self._view.journal}
${self._view.cli}
</div>
@ -216,18 +214,17 @@ class Terminal {
${self._view.term}
</div>
`
self._output(self.data.banner)
if (self.data.banner) self.commands.log(self.data.banner)
function focusinput (event) {
if (self._view.journal.offsetHeight - (self._view.term.scrollTop + self._view.term.offsetHeight) < 50) {
refocus()
function throttle (fn, wait) {
var time = Date.now()
return function debounce () {
if ((time + wait - Date.now()) < 0) {
fn.apply(this, arguments)
time = Date.now()
}
}
}
function refocus () {
self._view.input.focus()
reattach({ currentTarget: self._view.term })
self.scroll2bottom()
}
var css2 = csjs`
.anchor {
position : static;
@ -264,18 +261,20 @@ class Terminal {
var placeholder = yo`<div class=${css2.anchor}>${background}${text}</div>`
var inserted = false
function throttle (fn, wait) {
var time = Date.now()
return function () {
if ((time + wait - Date.now()) < 0) {
fn.apply(this, arguments)
time = Date.now()
}
function focusinput (event) {
if (self._view.journal.offsetHeight - (self._view.term.scrollTop + self._view.term.offsetHeight) < 50) {
refocus()
}
}
function refocus () {
self._view.input.focus()
reattach({ currentTarget: self._view.term })
delete self.scroll2bottom
self.scroll2bottom()
}
function reattach (event) {
var el = event.currentTarget
var isBottomed = el.scrollHeight - el.scrollTop < el.clientHeight + 30
var isBottomed = el.scrollHeight - el.scrollTop - el.clientHeight < 30
if (isBottomed) {
if (inserted) {
text.innerText = ''
@ -320,11 +319,11 @@ class Terminal {
background.style.height = (self._view.journal.offsetHeight - (placeholder.offsetTop + placeholder.offsetHeight)) + 'px'
background.onclick = undefined
background.style.cursor = 'default'
background.style.pointerEvents = 'none'
} else {
background.style = ''
text.style = ''
background.onclick = function (event) {
console.error('background click')
placeholder.scrollIntoView()
check()
}
@ -341,6 +340,14 @@ class Terminal {
self.event.trigger('resize', [])
}
}
function filter (event) {
var input = event.currentTarget
self.updateJournal({ type: 'search', value: input.value })
}
function clear (event) {
refocus()
self._view.journal.innerHTML = ''
}
// ----------------- resizeable ui ---------------
function mousedown (event) {
event.preventDefault()
@ -385,7 +392,7 @@ class Terminal {
putCursor2End(self._view.input)
} else { // <enter>
event.preventDefault()
self.execute(self._view.input.innerText)
self.commands.script(self._view.input.innerText)
self._view.input.innerHTML = ''
}
}
@ -418,12 +425,78 @@ class Terminal {
editable.focus()
}
}
updateJournal (filterEvent) {
var self = this
var commands = self.data.activeFilters.commands
var value = filterEvent.value
if (filterEvent.type === 'select') {
commands[value] = true
if (!self._INDEX.commandsMain[value]) return
self._INDEX.commandsMain[value].forEach(item => {
item.root.steps.forEach(item => { self._JOURNAL[item.gidx] = item })
self._JOURNAL[item.gidx] = item
})
} else if (filterEvent.type === 'deselect') {
commands[value] = false
if (!self._INDEX.commandsMain[value]) return
self._INDEX.commandsMain[value].forEach(item => {
item.root.steps.forEach(item => { self._JOURNAL[item.gidx] = undefined })
self._JOURNAL[item.gidx] = undefined
})
} else if (filterEvent.type === 'search') {
if (value !== self.data.activeFilters.input) {
var query = self.data.activeFilters.input = value
var items = self._JOURNAL
for (var gidx = 0, len = items.length; gidx < len; gidx++) {
var item = items[gidx]
if (item) {
var show = query.length ? match(item.args, query) : true
item.hide = !show
}
}
}
}
var df = document.createDocumentFragment()
self._JOURNAL.forEach(item => {
if (item && item.el && !item.hide) df.appendChild(item.el)
})
requestAnimationFrame(function updateDOM () {
self._view.journal.innerHTML = ''
self._view.journal.appendChild(df)
})
}
_shouldAdd (item) {
var self = this
if (self.data.activeFilters.commands[item.root.cmd]) {
var query = self.data.activeFilters.input
var args = item.args
return query.length ? match(args, query) : true
}
}
_appendItem (item) {
var self = this
var { el, gidx } = item
self._JOURNAL[gidx] = item
if (!self._jobs.length) {
requestAnimationFrame(function updateTerminal () {
self._jobs.forEach(el => self._view.journal.appendChild(el))
self.scroll2bottom()
self._jobs = []
})
}
self._jobs.push(el)
}
scroll2bottom () {
var self = this
setTimeout(function () {
self._view.term.scrollTop = self._view.term.scrollHeight
}, 0)
}
_blocksRenderer (mode) {
var self = this
var modes = { log: true, info: true, error: true }
if (modes[mode]) {
return function render () {
var args = [].slice.call(arguments)
mode = { log: 'black', info: 'blue', error: 'red' }[mode] // defaults
if (mode) {
return function logger (args, scopedCommands, append) {
var types = args.map(type)
var values = javascriptserialize.apply(null, args).map(function (val, idx) {
if (typeof args[idx] === 'string') val = args[idx]
@ -432,69 +505,71 @@ class Terminal {
var lines = val.match(new RegExp(pattern, 'g'))
return lines.map(str => document.createTextNode(`${str}\n`))
})
return values
append(yo`<span style="color: ${mode};">${values}</span>`)
}
} else {
throw new Error('mode is not supported')
}
}
execute (script) {
_scopeCommands (append) {
var self = this
script = String(script)
self._output({ type: 'log', value: `> ${script}` })
self._shell(script, function (error, output) {
if (error) {
self._output({ type: 'error', value: error })
return error
} else {
self._output({ type: 'log', value: output })
return output
var scopedCommands = {}
Object.keys(self.commands).forEach(function makeScopedCommand (cmd) {
var command = self._commands[cmd]
scopedCommands[cmd] = function _command () {
var args = [...arguments]
command(args, scopedCommands, el => append(cmd, args, blockify(el)))
}
})
return scopedCommands
}
registerType (typename, template) {
registerCommand (name, command) {
var self = this
if (typeof template !== 'function') throw new Error('invalid template')
self._templates[typename] = template
self.logger[typename] = function () {
var args = [...arguments].map(x => ({ type: typename, value: x }))
self._output.apply(self, args)
name = String(name)
if (self._commands[name]) throw new Error(`command "${name}" exists already`)
if (typeof command !== 'function') throw new Error(`invalid command: ${command}`)
self._commands[name] = command
self._INDEX.commands[name] = []
self._INDEX.commandsMain[name] = []
self.commands[name] = function _command () {
var args = [...arguments]
var steps = []
var root = { steps, cmd: name }
var ITEM = { root, cmd: name }
root.gidx = self._INDEX.allMain.push(ITEM) - 1
root.idx = self._INDEX.commandsMain[name].push(ITEM) - 1
function append (cmd, params, el) {
var item
if (cmd) { // subcommand
item = { el, cmd, root }
} else { // command
item = ITEM
item.el = el
cmd = name
}
item.gidx = self._INDEX.all.push(item) - 1
item.idx = self._INDEX.commands[cmd].push(item) - 1
item.step = steps.push(item) - 1
item.args = params
if (self._shouldAdd(item)) self._appendItem(item)
}
var scopedCommands = self._scopeCommands(append)
command(args, scopedCommands, el => append(null, args, blockify(el)))
}
var help = typeof command.help === 'string' ? command.help : [
`// no help available for:`,
`terminal.commands.${name}(...)`
].join('\n')
self.commands[name].toString = _ => { return help }
self.commands[name].help = help
return self.commands[name]
}
log () {
// @TODO: temporary to not break stuff that uses the old API
this._output.apply(this, arguments)
}
_output () {
var self = this
var args = [...arguments]
self.data.session.push(args)
args.forEach(function (data) {
if (!data || !data.type) data = { type: 'log', value: data }
var render = self._templates[data.type]
var blocks = render(data.value)
blocks = [].concat(blocks)
blocks.forEach(function (block) {
self._view.journal.appendChild(yo`
<div class="${css.block} ${css[data.type] || data.type}">
${block}
</div>
`)
self.scroll2bottom()
})
})
}
scroll2bottom () {
var self = this
setTimeout(function () {
self._view.term.scrollTop = self._view.term.scrollHeight
}, 0)
}
_shell (script, done) { // default shell
_shell (script, scopedCommands, done) { // default shell
var self = this
var context = domTerminalFeatures(self, scopedCommands)
try {
var context = vm.createContext(Object.assign(self._jsSandboxContext, domTerminalFeatures(self)))
var result = vm.runInContext(script, context)
var cmds = vm.createContext(Object.assign(self._jsSandboxContext, context))
var result = vm.runInContext(script, cmds)
self._jsSandboxContext = Object.assign({}, context)
done(null, result)
} catch (error) {
@ -503,14 +578,40 @@ class Terminal {
}
}
function domTerminalFeatures (self) {
// @TODO add all the `console` functions
function domTerminalFeatures (self, scopedCommands) {
return {
web3: executionContext.getProvider() !== 'vm' ? new Web3(executionContext.web3().currentProvider) : null,
console: {
log: function () { self._output.apply(self, arguments) }
log: function () { scopedCommands.log.apply(scopedCommands, arguments) },
info: function () { scopedCommands.info.apply(scopedCommands, arguments) },
error: function () { scopedCommands.error.apply(scopedCommands, arguments) }
}
}
}
function findDeep (object, fn, found = { break: false, value: undefined }) {
if (typeof object !== 'object' || object === null) return
for (var i in object) {
if (found.break) break
var el = object[i]
if (!fn(el, i, object)) findDeep(el, fn, found)
else if (found.break = true) return found.value = el // eslint-disable-line
}
return found.value
}
function match (args, query) {
query = query.trim()
var isMatch = !!findDeep(args, function check (value, key) {
if (value === undefined || value === null) return false
if (typeof value === 'function') return false
if (typeof value === 'object') return false
var contains = String(value).indexOf(query.trim()) !== -1
return contains
})
return isMatch
}
function blockify (el) { return yo`<div class=${css.block}>${el}</div>` }
module.exports = Terminal

@ -32,8 +32,8 @@ function runTests (browser) {
.click('#runTabView div[class^="create"]')
.waitForElementPresent('.instance button[title="f - transact (not payable)"]')
.click('.instance button[title="f - transact (not payable)"]')
.waitForElementPresent('#editor-container div[class^="terminal"] .knownTransaction span[id="tx0xa178c603400a184ce5fedbcfab392d9b77822f6ffa7facdec693aded214523bc"]')
.assert.containsText('#editor-container div[class^="terminal"] .knownTransaction span[id="tx0xa178c603400a184ce5fedbcfab392d9b77822f6ffa7facdec693aded214523bc"]', '(vm): from:0xca3...a733c, to:0x692...77b3a, browser/Untitled.sol:TestContract.f(), value:0 wei, data:0x261...21ff0, 0 logs, hash:0xa17...523bc,DetailsDebug')
.waitForElementPresent('#editor-container div[class^="terminal"] span[id="tx0xa178c603400a184ce5fedbcfab392d9b77822f6ffa7facdec693aded214523bc"]')
.assert.containsText('#editor-container div[class^="terminal"] span[id="tx0xa178c603400a184ce5fedbcfab392d9b77822f6ffa7facdec693aded214523bc"]', '(vm): from:0xca3...a733c, to:0x692...77b3a, browser/Untitled.sol:TestContract.f(), value:0 wei, data:0x261...21ff0, 0 logs, hash:0xa17...523bc,DetailsDebug')
.end()
/*
@TODO: need to check now the return value of the function

Loading…
Cancel
Save