Merge branch 'master' of https://github.com/ethereum/remix-project into panels
commit
db728677f0
@ -1,194 +0,0 @@ |
||||
'use strict' |
||||
import { sourceMappingDecoder } from '@remix-project/remix-debug' |
||||
const yo = require('yo-yo') |
||||
const globalRegistry = require('../../global/registry') |
||||
|
||||
const css = require('./styles/contextView-styles') |
||||
|
||||
/* |
||||
Display information about the current focused code: |
||||
- if it's a reference, display information about the declaration |
||||
- jump to the declaration |
||||
- number of references |
||||
- rename declaration/references |
||||
*/ |
||||
class ContextView { |
||||
constructor (opts, localRegistry) { |
||||
this._components = {} |
||||
this._components.registry = localRegistry || globalRegistry |
||||
this.contextualListener = opts.contextualListener |
||||
this.editor = opts.editor |
||||
this._deps = { |
||||
compilersArtefacts: this._components.registry.get('compilersartefacts').api, |
||||
offsetToLineColumnConverter: this._components.registry.get('offsettolinecolumnconverter').api, |
||||
config: this._components.registry.get('config').api, |
||||
fileManager: this._components.registry.get('filemanager').api |
||||
} |
||||
this._view = null |
||||
this._nodes = null |
||||
this._current = null |
||||
this.sourceMappingDecoder = sourceMappingDecoder |
||||
this.previousElement = null |
||||
this.contextualListener.event.register('contextChanged', nodes => { |
||||
this.show() |
||||
this._nodes = nodes |
||||
this.update() |
||||
}) |
||||
this.contextualListener.event.register('stopHighlighting', () => { |
||||
}) |
||||
} |
||||
|
||||
render () { |
||||
const view = yo` |
||||
<div class="${css.contextview} ${css.contextviewcontainer} bg-light text-dark border-0"> |
||||
<div class=${css.container}> |
||||
${this._renderTarget()} |
||||
</div> |
||||
</div>` |
||||
if (!this._view) { |
||||
this._view = view |
||||
} |
||||
return view |
||||
} |
||||
|
||||
hide () { |
||||
if (this._view) { |
||||
this._view.style.display = 'none' |
||||
} |
||||
} |
||||
|
||||
show () { |
||||
if (this._view) { |
||||
this._view.style.display = 'block' |
||||
} |
||||
} |
||||
|
||||
update () { |
||||
if (this._view) { |
||||
yo.update(this._view, this.render()) |
||||
} |
||||
} |
||||
|
||||
_renderTarget () { |
||||
let last |
||||
const previous = this._current |
||||
if (this._nodes && this._nodes.length) { |
||||
last = this._nodes[this._nodes.length - 1] |
||||
if (isDefinition(last)) { |
||||
this._current = last |
||||
} else { |
||||
const target = this.contextualListener.declarationOf(last) |
||||
if (target) { |
||||
this._current = target |
||||
} else { |
||||
this._current = null |
||||
} |
||||
} |
||||
} |
||||
if (!this._current || !previous || previous.id !== this._current.id || (this.previousElement && !this.previousElement.children.length)) { |
||||
this.previousElement = this._render(this._current, last) |
||||
} |
||||
return this.previousElement |
||||
} |
||||
|
||||
_jumpToInternal (position) { |
||||
const jumpToLine = (lineColumn) => { |
||||
if (lineColumn.start && lineColumn.start.line && lineColumn.start.column) { |
||||
this.editor.gotoLine(lineColumn.start.line, lineColumn.end.column + 1) |
||||
} |
||||
} |
||||
const lastCompilationResult = this._deps.compilersArtefacts.__last |
||||
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0 && lastCompilationResult.data) { |
||||
const lineColumn = this._deps.offsetToLineColumnConverter.offsetToLineColumn( |
||||
position, |
||||
position.file, |
||||
lastCompilationResult.getSourceCode().sources, |
||||
lastCompilationResult.getAsts()) |
||||
const filename = lastCompilationResult.getSourceName(position.file) |
||||
// TODO: refactor with rendererAPI.errorClick
|
||||
if (filename !== this._deps.config.get('currentFile')) { |
||||
const provider = this._deps.fileManager.fileProviderOf(filename) |
||||
if (provider) { |
||||
provider.exists(filename).then(exist => { |
||||
this._deps.fileManager.open(filename) |
||||
jumpToLine(lineColumn) |
||||
}).catch(error => { |
||||
if (error) return console.log(error) |
||||
}) |
||||
} |
||||
} else { |
||||
jumpToLine(lineColumn) |
||||
} |
||||
} |
||||
} |
||||
|
||||
_render (node, nodeAtCursorPosition) { |
||||
if (!node) return yo`<div></div>` |
||||
let references = this.contextualListener.referencesOf(node) |
||||
const type = node.typeDescriptions && node.typeDescriptions.typeString ? node.typeDescriptions.typeString : node.nodeType |
||||
references = `${references ? references.length : '0'} reference(s)` |
||||
|
||||
let ref = 0 |
||||
const nodes = this.contextualListener.getActiveHighlights() |
||||
for (const k in nodes) { |
||||
if (nodeAtCursorPosition.id === nodes[k].nodeId) { |
||||
ref = k |
||||
break |
||||
} |
||||
} |
||||
|
||||
// JUMP BETWEEN REFERENCES
|
||||
const jump = (e) => { |
||||
e.target.dataset.action === 'next' ? ref++ : ref-- |
||||
if (ref < 0) ref = nodes.length - 1 |
||||
if (ref >= nodes.length) ref = 0 |
||||
this._jumpToInternal(nodes[ref].position) |
||||
} |
||||
|
||||
const jumpTo = () => { |
||||
if (node && node.src) { |
||||
const position = this.sourceMappingDecoder.decode(node.src) |
||||
if (position) { |
||||
this._jumpToInternal(position) |
||||
} |
||||
} |
||||
} |
||||
|
||||
const showGasEstimation = () => { |
||||
if (node.nodeType === 'FunctionDefinition') { |
||||
const result = this.contextualListener.gasEstimation(node) |
||||
const executionCost = ' Execution cost: ' + result.executionCost + ' gas' |
||||
const codeDepositCost = 'Code deposit cost: ' + result.codeDepositCost + ' gas' |
||||
const estimatedGas = result.codeDepositCost ? `${codeDepositCost}, ${executionCost}` : `${executionCost}` |
||||
return yo` |
||||
<div class=${css.gasEstimation}> |
||||
<i class="fas fa-gas-pump ${css.gasStationIcon}" title='Gas estimation'></i> |
||||
<span>${estimatedGas}</span> |
||||
</div> |
||||
` |
||||
} |
||||
} |
||||
|
||||
return yo` |
||||
<div class=${css.line}>${showGasEstimation()} |
||||
<div title=${type} class=${css.type}>${type}</div> |
||||
<div title=${node.name} class=${css.name}>${node.name}</div> |
||||
<i class="fas fa-share ${css.jump}" aria-hidden="true" onclick=${jumpTo}></i> |
||||
<span class=${css.referencesnb}>${references}</span> |
||||
<i data-action='previous' class="fas fa-chevron-up ${css.jump}" aria-hidden="true" onclick=${jump}></i> |
||||
<i data-action='next' class="fas fa-chevron-down ${css.jump}" aria-hidden="true" onclick=${jump}></i> |
||||
</div> |
||||
` |
||||
} |
||||
} |
||||
|
||||
function isDefinition (node) { |
||||
return node.nodeType === 'ContractDefinition' || |
||||
node.nodeType === 'FunctionDefinition' || |
||||
node.nodeType === 'ModifierDefinition' || |
||||
node.nodeType === 'VariableDeclaration' || |
||||
node.nodeType === 'StructDefinition' || |
||||
node.nodeType === 'EventDefinition' |
||||
} |
||||
|
||||
module.exports = ContextView |
@ -0,0 +1,4 @@ |
||||
{ |
||||
"presets": ["@nrwl/react/babel"], |
||||
"plugins": [] |
||||
} |
@ -0,0 +1,19 @@ |
||||
{ |
||||
"env": { |
||||
"browser": true, |
||||
"es6": true |
||||
}, |
||||
"extends": "../../../.eslintrc", |
||||
"globals": { |
||||
"Atomics": "readonly", |
||||
"SharedArrayBuffer": "readonly" |
||||
}, |
||||
"parserOptions": { |
||||
"ecmaVersion": 11, |
||||
"sourceType": "module" |
||||
}, |
||||
"rules": { |
||||
"no-unused-vars": "off", |
||||
"@typescript-eslint/no-unused-vars": "error" |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
# remix-ui-editor-context-view |
||||
|
||||
This library was generated with [Nx](https://nx.dev). |
||||
|
||||
## Running unit tests |
||||
|
||||
Run `nx test remix-ui-editor-context-view` to execute the unit tests via [Jest](https://jestjs.io). |
@ -0,0 +1 @@ |
||||
export * from './lib/remix-ui-editor-context-view'; |
@ -0,0 +1,191 @@ |
||||
import React, { useEffect, useState, useRef } from 'react' // eslint-disable-line
|
||||
import { sourceMappingDecoder } from '@remix-project/remix-debug' |
||||
|
||||
import './remix-ui-editor-context-view.css' |
||||
|
||||
/* eslint-disable-next-line */ |
||||
|
||||
export type astNode = { |
||||
name: string, |
||||
id: number, |
||||
children: Array<any>, |
||||
typeDescriptions: any, |
||||
nodeType: String, |
||||
src: any, |
||||
nodeId: any, |
||||
position: any |
||||
} |
||||
|
||||
export type onContextListenerChangedListener = (nodes: Array<astNode>) => void |
||||
|
||||
export type gasEstimationType = { |
||||
executionCost: string, |
||||
codeDepositCost: string |
||||
} |
||||
export interface RemixUiEditorContextViewProps { |
||||
hide: boolean, |
||||
gotoLine: (line: number, column: number) => void, |
||||
openFile: (fileName: string) => void, |
||||
getLastCompilationResult: () => any, |
||||
offsetToLineColumn: (position: any, file: any, sources: any, asts: any) => any, |
||||
getCurrentFileName: () => String |
||||
onContextListenerChanged: (listener: onContextListenerChangedListener) => void |
||||
referencesOf: (nodes: astNode) => Array<astNode> |
||||
getActiveHighlights: () => Array<astNode> |
||||
gasEstimation: (node: astNode) => gasEstimationType |
||||
declarationOf: (node: astNode) => astNode |
||||
} |
||||
|
||||
function isDefinition (node: any) { |
||||
return node.nodeType === 'ContractDefinition' || |
||||
node.nodeType === 'FunctionDefinition' || |
||||
node.nodeType === 'ModifierDefinition' || |
||||
node.nodeType === 'VariableDeclaration' || |
||||
node.nodeType === 'StructDefinition' || |
||||
node.nodeType === 'EventDefinition' |
||||
} |
||||
|
||||
|
||||
|
||||
type nullableAstNode = astNode | null |
||||
|
||||
export function RemixUiEditorContextView (props: RemixUiEditorContextViewProps) { |
||||
/* |
||||
gotoLineDisableRef is used to temporarily disable the update of the view. |
||||
e.g when the user ask the component to "gotoLine" we don't want to rerender the component (but just to put the mouse on the desired line) |
||||
*/ |
||||
const gotoLineDisableRef = useRef(false) |
||||
const [state, setState] = useState<{ |
||||
nodes: Array<astNode>, |
||||
references: Array<astNode>, |
||||
activeHighlights: Array<any> |
||||
currentNode: nullableAstNode, |
||||
gasEstimation: gasEstimationType |
||||
}>({ |
||||
nodes: [], |
||||
references: [], |
||||
activeHighlights: [], |
||||
currentNode: null, |
||||
gasEstimation: { executionCost: '', codeDepositCost: '' } |
||||
}) |
||||
|
||||
useEffect(() => { |
||||
props.onContextListenerChanged(async (nodes: Array<astNode>) => { |
||||
if (gotoLineDisableRef.current) { |
||||
gotoLineDisableRef.current = false |
||||
return |
||||
} |
||||
let currentNode |
||||
if (!props.hide && nodes && nodes.length) { |
||||
currentNode = nodes[nodes.length - 1] |
||||
if (!isDefinition(currentNode)) { |
||||
currentNode = await props.declarationOf(currentNode) |
||||
} |
||||
} |
||||
let references |
||||
let gasEstimation |
||||
if (currentNode) { |
||||
references = await props.referencesOf(currentNode) |
||||
if (currentNode.nodeType === 'FunctionDefinition') { |
||||
gasEstimation = await props.gasEstimation(currentNode) |
||||
}
|
||||
} |
||||
let activeHighlights = await props.getActiveHighlights() |
||||
setState(prevState => { |
||||
return { ...prevState, nodes, references, activeHighlights, currentNode, gasEstimation } |
||||
}) |
||||
}) |
||||
}, []) |
||||
|
||||
/* |
||||
* show gas estimation |
||||
*/ |
||||
const gasEstimation = (node) => { |
||||
if (node.nodeType === 'FunctionDefinition') { |
||||
const result: gasEstimationType = state.gasEstimation |
||||
const executionCost = ' Execution cost: ' + result.executionCost + ' gas' |
||||
const codeDepositCost = 'Code deposit cost: ' + result.codeDepositCost + ' gas' |
||||
const estimatedGas = result.codeDepositCost ? `${codeDepositCost}, ${executionCost}` : `${executionCost}` |
||||
return ( |
||||
<div className="gasEstimation"> |
||||
<i className="fas fa-gas-pump gasStationIcon" title='Gas estimation'></i> |
||||
<span>{estimatedGas}</span> |
||||
</div> |
||||
) |
||||
} else { |
||||
return (<div></div>) |
||||
} |
||||
} |
||||
|
||||
/* |
||||
* onClick jump to ast node in the editor |
||||
*/ |
||||
const _jumpToInternal = async (position: any) => { |
||||
const jumpToLine = async (fileName: string, lineColumn: any) => { |
||||
if (fileName !== await props.getCurrentFileName()) { |
||||
await props.openFile(fileName) |
||||
} |
||||
if (lineColumn.start && lineColumn.start.line && lineColumn.start.column) { |
||||
gotoLineDisableRef.current = true |
||||
props.gotoLine(lineColumn.start.line, lineColumn.end.column + 1) |
||||
} |
||||
} |
||||
const lastCompilationResult = await props.getLastCompilationResult() |
||||
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0 && lastCompilationResult.data) { |
||||
const lineColumn = await props.offsetToLineColumn( |
||||
position, |
||||
position.file, |
||||
lastCompilationResult.getSourceCode().sources, |
||||
lastCompilationResult.getAsts()) |
||||
const filename = lastCompilationResult.getSourceName(position.file) |
||||
// TODO: refactor with rendererAPI.errorClick
|
||||
jumpToLine(filename, lineColumn) |
||||
} |
||||
} |
||||
|
||||
const _render = (node: nullableAstNode) => { |
||||
if (!node) return (<div></div>) |
||||
const references = state.references |
||||
const type = node.typeDescriptions && node.typeDescriptions.typeString ? node.typeDescriptions.typeString : node.nodeType |
||||
const referencesCount = `${references ? references.length : '0'} reference(s)` |
||||
|
||||
let ref = 0 |
||||
const nodes: Array<astNode> = state.activeHighlights |
||||
|
||||
const jumpTo = () => { |
||||
if (node && node.src) { |
||||
const position = sourceMappingDecoder.decode(node.src) |
||||
if (position) { |
||||
_jumpToInternal(position) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// JUMP BETWEEN REFERENCES
|
||||
const jump = (e: any) => { |
||||
e.target.dataset.action === 'next' ? ref++ : ref-- |
||||
if (ref < 0) ref = nodes.length - 1 |
||||
if (ref >= nodes.length) ref = 0 |
||||
_jumpToInternal(nodes[ref].position) |
||||
} |
||||
|
||||
return ( |
||||
<div className="line">{gasEstimation(node)} |
||||
<div title={type} className="type">{type}</div> |
||||
<div title={node.name} className="name mr-2">{node.name}</div> |
||||
<i className="fas fa-share jump" data-action='gotoref' aria-hidden="true" onClick={jumpTo}></i> |
||||
<span className="referencesnb">{referencesCount}</span> |
||||
<i data-action='previous' className="fas fa-chevron-up jump" aria-hidden="true" onClick={jump}></i> |
||||
<i data-action='next' className="fas fa-chevron-down jump" aria-hidden="true" onClick={jump}></i> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
return ( |
||||
!props.hide && <div className="container-context-view contextviewcontainer bg-light text-dark border-0 py-1"> |
||||
{_render(state.currentNode)} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
export default RemixUiEditorContextView |
@ -0,0 +1,16 @@ |
||||
{ |
||||
"extends": "../../../tsconfig.base.json", |
||||
"compilerOptions": { |
||||
"jsx": "react", |
||||
"allowJs": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true |
||||
}, |
||||
"files": [], |
||||
"include": [], |
||||
"references": [ |
||||
{ |
||||
"path": "./tsconfig.lib.json" |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,13 @@ |
||||
{ |
||||
"extends": "./tsconfig.json", |
||||
"compilerOptions": { |
||||
"outDir": "../../../dist/out-tsc", |
||||
"types": ["node"] |
||||
}, |
||||
"files": [ |
||||
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", |
||||
"../../../node_modules/@nrwl/react/typings/image.d.ts" |
||||
], |
||||
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"], |
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] |
||||
} |
Loading…
Reference in new issue