autocomplete

pull/5370/head
filip mertens 3 years ago
parent dc6db65cea
commit 12e90191de
  1. 3
      apps/remix-ide/src/app/tabs/compile-tab.js
  2. 6
      apps/solidity-compiler/src/app/compiler-api.ts
  3. 53
      libs/remix-core-plugin/src/lib/editor-context-listener.ts
  4. 6
      libs/remix-solidity/src/compiler/compiler-input.ts
  5. 1
      libs/remix-solidity/src/compiler/compiler-utils.ts
  6. 5
      libs/remix-solidity/src/compiler/compiler-worker.ts
  7. 2
      libs/remix-solidity/src/compiler/compiler.ts
  8. 5
      libs/remix-solidity/src/compiler/types.ts
  9. 158
      libs/remix-ui/editor/src/lib/providers/completionProvider.ts
  10. 23
      libs/remix-ui/editor/src/lib/providers/hoverProvider.ts
  11. 2
      libs/remix-ui/editor/src/lib/providers/referenceProvider.ts
  12. 102
      libs/remix-ui/editor/src/lib/providers/signatureProvider.ts
  13. 3
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  14. 1
      libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx

@ -99,7 +99,8 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA
}
compile (fileName) {
this.call('notification', 'toast', compileToastMsg(this.currentRequest.from, fileName))
if(this.currentRequest.from !== 'contextualListener')
this.call('notification', 'toast', compileToastMsg(this.currentRequest.from, fileName))
super.compile(fileName)
}

@ -316,7 +316,13 @@ export const CompilerApiMixin = (Base) => class extends Base {
}
}
}
this.data.eventHandlers.onAstFinished = async (success, data, source, input, version) => {
this.emit('astFinished', source.target, source, 'soljson', data, input, version)
}
this.compiler.event.register('compilationFinished', this.data.eventHandlers.onCompilationFinished)
this.compiler.event.register('astFinished', this.data.eventHandlers.onAstFinished)
this.data.eventHandlers.onThemeChanged = (theme) => {
const invert = theme.quality === 'dark' ? 1 : 0

@ -2,10 +2,12 @@
import { Plugin } from '@remixproject/engine'
import { sourceMappingDecoder } from '@remix-project/remix-debug'
import { compile } from '@remix-project/remix-solidity'
import { canUseWorker, Compiler, CompilerAbstract, urlFromVersion } from '@remix-project/remix-solidity-ts'
const profile = {
name: 'contextualListener',
methods: ['getNodeById', 'positionOfDefinition', 'definitionAtPosition', 'jumpToDefinition', 'referrencesAtPosition', 'nodesAtEditorPosition', 'referencesOf', 'getActiveHighlights', 'gasEstimation', 'declarationOf', 'jumpToPosition'],
methods: ['getNodes', 'compile', 'getNodeById', 'getLastCompilationResult', 'positionOfDefinition', 'definitionAtPosition', 'jumpToDefinition', 'referrencesAtPosition', 'nodesAtEditorPosition', 'referencesOf', 'getActiveHighlights', 'gasEstimation', 'declarationOf', 'jumpToPosition'],
events: [],
version: '0.0.1'
}
@ -36,6 +38,9 @@ export class EditorContextListener extends Plugin {
contract: any
activated: boolean
compiler: Compiler
lastCompilationResult: any
constructor(astWalker) {
super(profile)
this.activated = false
@ -51,19 +56,26 @@ export class EditorContextListener extends Plugin {
onActivation() {
this.on('editor', 'contentChanged', () => { this._stopHighlighting() })
this.on('solidity', 'compilationFinished', (file, source, languageVersion, data, input, version) => {
if (languageVersion.indexOf('soljson') !== 0) return
this.on('solidity', 'astFinished', async (file, source, languageVersion, data, input, version) => {
console.log('compilation result', Object.keys(data.sources))
if (languageVersion.indexOf('soljson') !== 0 || !data.sources) return
if (data.sources && Object.keys(data.sources).length === 0) return
this.lastCompilationResult = new CompilerAbstract(languageVersion, data, source, input)
this._stopHighlighting()
this._index = {
Declarations: {},
FlatReferences: {}
}
this._buildIndex(data, source)
this.emit('astFinished')
})
setInterval(async () => {
const compilationResult = await this.call('compilerArtefacts', 'getLastCompilationResult')
const compilationResult = this.lastCompilationResult // await this.call('compilerArtefacts', 'getLastCompilationResult')
if (compilationResult && compilationResult.languageversion.indexOf('soljson') === 0) {
let currentFile
try {
currentFile = await this.call('fileManager', 'file')
@ -77,8 +89,15 @@ export class EditorContextListener extends Plugin {
)
}
}, 1000)
}
async getLastCompilationResult() {
return this.lastCompilationResult
}
this.activated = true
async compile() {
this.currentFile = await this.call('fileManager', 'file')
return await this.call('solidity', 'compile', this.currentFile)
}
getActiveHighlights() {
@ -116,7 +135,7 @@ export class EditorContextListener extends Plugin {
}
async nodesAtEditorPosition(position: any) {
const lastCompilationResult = await this.call('compilerArtefacts', 'getLastCompilationResult')
const lastCompilationResult = this.lastCompilationResult // await this.call('compilerArtefacts', 'getLastCompilationResult')
if (!lastCompilationResult) return false
let urlFromPath = await this.call('fileManager', 'getUrlFromPath', this.currentFile)
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0 && lastCompilationResult.data) {
@ -146,7 +165,7 @@ export class EditorContextListener extends Plugin {
async definitionAtPosition(position: any) {
const nodes = await this.nodesAtEditorPosition(position)
console.log(nodes)
console.log('nodes at position', nodes)
console.log(this._index.FlatReferences)
let nodeDefinition: any
let node: any
@ -164,10 +183,10 @@ export class EditorContextListener extends Plugin {
}
}
return nodeDefinition
}else{
} else {
return false
}
}
async positionOfDefinition(node: any) {
@ -185,14 +204,18 @@ export class EditorContextListener extends Plugin {
async jumpToDefinition(position: any) {
const node = await this.definitionAtPosition(position)
const sourcePosition = await this.positionOfDefinition(node)
if(sourcePosition){
if (sourcePosition) {
await this.jumpToPosition(sourcePosition)
}
}
}
async getNodes() {
return this._index.FlatReferences
}
/*
* onClick jump to position of ast node in the editor
*/
/*
* onClick jump to position of ast node in the editor
*/
async jumpToPosition(position: any) {
const jumpToLine = async (fileName: string, lineColumn: any) => {
if (fileName !== await this.call('fileManager', 'file')) {
@ -203,7 +226,7 @@ export class EditorContextListener extends Plugin {
this.call('editor', 'gotoLine', lineColumn.start.line, lineColumn.end.column + 1)
}
}
const lastCompilationResult = await this.call('compilerArtefacts', 'getLastCompilationResult')
const lastCompilationResult = this.lastCompilationResult // await this.call('compilerArtefacts', 'getLastCompilationResult')
console.log(lastCompilationResult.getSourceCode().sources)
console.log(position)
if (lastCompilationResult && lastCompilationResult.languageversion.indexOf('soljson') === 0 && lastCompilationResult.data) {

@ -12,14 +12,18 @@ export default (sources: Source, opts: CompilerInputOptions): string => {
runs: opts.runs || 200
},
libraries: opts.libraries,
parserErrorRecovery: false,
outputSelection: {
'*': {
'': ['ast'],
'*': ['abi', 'metadata', 'devdoc', 'userdoc', 'storageLayout', 'evm.legacyAssembly', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'evm.gasEstimates', 'evm.assembly']
'*': []
//'*' : ['abi', 'metadata']
// '*': ['abi', 'metadata', 'devdoc', 'userdoc', 'storageLayout', 'evm.legacyAssembly', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'evm.gasEstimates', 'evm.assembly']
}
}
}
}
if (opts.evmVersion) {
if (opts.evmVersion.toLowerCase() == 'default') {
opts.evmVersion = null

@ -12,6 +12,7 @@ export const pathToURL = {}
* @param version is the version of compiler with or without 'soljson-v' prefix and .js postfix
*/
export function urlFromVersion (version) {
console.log('urlFromVersion', version)
let url
if (version === 'builtin') {
let location: string | Location = window.document.location

@ -26,7 +26,10 @@ export default function (self) { // eslint-disable-line @typescript-eslint/expli
missingInputs.push(path)
return { error: 'Deferred import' }
}
return compiler.compile(input, { import: missingInputsCallback })
//console.log(input)
const ret = compiler.compile(input, { import: missingInputsCallback })
//console.log(JSON.parse(ret))
return ret
} catch (exception) {
return JSON.stringify({ error: 'Uncaught JavaScript exception:\n' + exception })
}

@ -162,6 +162,7 @@ export class Compiler {
}
this.event.trigger('compilationFinished', [true, data, source, input, version])
}
this.event.trigger('astFinished', [true, data, source, input, version])
}
/**
@ -252,6 +253,7 @@ export class Compiler {
*/
loadWorker (url: string): void {
console.log("WEB", webworkify)
this.state.worker = webworkify(require.resolve('./compiler-worker'))
const jobs: Record<'sources', SourceWithTarget> [] = []

@ -77,7 +77,8 @@ export interface CompilerInput {
// after remappings were applied.
// If this key is an empty string, that refers to a global level.
[fileName: string]: Record<string, string>
}
},
parserErrorRecovery: boolean,
// The following can be used to select desired outputs based
// on file and contract names.
// If this field is omitted, then the compiler loads and does type checking,
@ -123,7 +124,7 @@ export interface CompilerInput {
outputSelection?: {
'*': {
'': [ 'ast' ],
'*': [ 'abi', 'metadata', 'devdoc', 'userdoc', 'storageLayout', 'evm.legacyAssembly', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'evm.gasEstimates', 'evm.assembly' ]
'*': any
}
}
}

@ -8,17 +8,169 @@ export class RemixCompletionProvider {
this.monaco = monaco
}
triggerCharacters = ['.', ' ']
async provideCompletionItems(model: any, position: any) {
console.log('AUTOCOMPLETE')
return new Promise((resolve, reject) => {
this.props.plugin.once('contextualListener', 'astFinished', async () => {
console.log('AST FINISHED')
resolve(await this.run(model, position))
})
this.props.plugin.call('contextualListener', 'compile')
})
}
async run(model: any, position: any) {
const textUntilPosition = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
});
console.log(textUntilPosition)
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn
};
const nodes = await this.props.plugin.call('contextualListener', 'getNodes')
console.log('NODES', nodes)
console.log('NODES', Object.values(nodes))
const getLinks = async (node: any) => {
const position = await this.props.plugin.call('contextualListener', 'positionOfDefinition', node)
const lastCompilationResult = await this.props.plugin.call('contextualListener', 'getLastCompilationResult')
const filename = lastCompilationResult.getSourceName(position.file)
console.log(filename, position)
const lineColumn = await this.props.plugin.call('offsetToLineColumnConverter', 'offsetToLineColumn',
position,
position.file,
lastCompilationResult.getSourceCode().sources,
lastCompilationResult.getAsts())
return `${filename} ${lineColumn.start.line}:${lineColumn.start.column}`
}
const getDocs = async (node: any) => {
if (node.documentation && node.documentation.text) {
let text = ''
node.documentation.text.split('\n').forEach(line => {
text += `${line.trim()}\n`
})
return text
}
}
const getParamaters = async (parameters: any) => {
if (parameters && parameters.parameters) {
let params = []
for (const param of parameters.parameters) {
params.push(await getVariableDeclaration(param))
}
return `(${params.join(', ')})`
}
}
const completeParameters = async (parameters: any) => {
if (parameters && parameters.parameters) {
let params = []
for (const param of parameters.parameters) {
params.push(param.name)
}
return `(${params.join(', ')})`
}
}
const getVariableDeclaration = async (node: any) => {
if (node.typeDescriptions && node.typeDescriptions.typeString) {
return `${node.typeDescriptions.typeString}${node.name && node.name.length ? ` ${node.name}` : ''}`
} else
if (node.typeName && node.typeName.name) {
return `${node.typeName.name}${node.name && node.name.length ? ` ${node.name}` : ''}`
} else {
return `${node.name && node.name.length ? ` ${node.name}` : ''}`
}
}
const suggestions = []
for(const node of Object.values(nodes) as any[]){
if (node.nodeType === 'VariableDeclaration') {
const completion = {
label: {label: `"${node.name}"`, description: await getLinks(node), detail: ` ${await getVariableDeclaration(node)}`},
kind: this.monaco.languages.CompletionItemKind.Variable,
insertText: node.name,
range: range,
documentation: await getDocs(node)
}
suggestions.push(completion)
} else
if (node.nodeType === 'FunctionDefinition') {
const completion = {
label: {label: `"${node.name}"`, description: await getLinks(node), detail: ` -> ${node.name} ${await getParamaters(node.parameters)}`},
kind: this.monaco.languages.CompletionItemKind.Function,
insertText: `${node.name} ${await completeParameters(node.parameters)}`,
range: range,
documentation: await getDocs(node)
}
suggestions.push(completion)
} else if
(node.nodeType === 'ContractDefinition') {
const completion = {
label: {label: `"${node.name}"`, description: await getLinks(node), detail: ` ${node.name}`},
kind: this.monaco.languages.CompletionItemKind.Interface,
insertText: node.name,
range: range,
documentation: await getDocs(node)
}
suggestions.push(completion)
} else if
(node.nodeType === 'StructDefinition') {
const completion = {
label: {label: `"${node.name}"`, description: await getLinks(node), detail: ` ${node.name}`},
kind: this.monaco.languages.CompletionItemKind.Struct,
insertText: node.name,
range: range,
documentation: await getDocs(node)
}
suggestions.push(completion)
} else if
(node.nodeType === 'EnumDefinition') {
const completion = {
label: {label: `"${node.name}"`, description: await getLinks(node), detail: ` ${node.name}`},
kind: this.monaco.languages.CompletionItemKind.Enum,
insertText: node.name,
range: range,
documentation: await getDocs(node)
}
suggestions.push(completion)
} else if
(node.nodeType === 'EventDefinition') {
const completion = {
label: {label: `"${node.name}"`, description: await getLinks(node), detail: ` ${node.name}`},
kind: this.monaco.languages.CompletionItemKind.Event,
insertText: node.name,
range: range,
documentation: await getDocs(node)
}
suggestions.push(completion)
} else if
(node.nodeType === 'ModifierDefinition') {
const completion = {
label: {label: `"${node.name}"`, description: await getLinks(node), detail: ` ${node.name}`},
kind: this.monaco.languages.CompletionItemKind.Method,
insertText: node.name,
range: range,
documentation: await getDocs(node)
}
suggestions.push(completion)
}
}
console.log(suggestions)
return {
suggestions: []
suggestions
}
}
}

@ -10,6 +10,16 @@ export class RemixHoverProvider {
}
provideHover = async function (model: any, position: any) {
return new Promise((resolve, reject) => {
this.props.plugin.once('contextualListener', 'astFinished', async () => {
console.log('AST FINISHED')
resolve(await this.run(model, position))
})
this.props.plugin.call('contextualListener', 'compile')
})
}
async run(model: any, position: any) {
const cursorPosition = this.props.editorAPI.getHoverPosition(position)
const nodeDefinition = await this.props.plugin.call('contextualListener', 'definitionAtPosition', cursorPosition)
@ -31,7 +41,7 @@ export class RemixHoverProvider {
const getLinks = async (node: any) => {
const position = await this.props.plugin.call('contextualListener', 'positionOfDefinition', node)
const lastCompilationResult = await this.props.plugin.call('compilerArtefacts', 'getLastCompilationResult')
const lastCompilationResult = await this.props.plugin.call('contextualListener', 'getLastCompilationResult')
const filename = lastCompilationResult.getSourceName(position.file)
console.log(filename, position)
const lineColumn = await this.props.plugin.call('offsetToLineColumnConverter', 'offsetToLineColumn',
@ -47,9 +57,14 @@ export class RemixHoverProvider {
const getVariableDeclaration = async (node: any) => {
if (node.typeDescriptions && node.typeDescriptions.typeString) {
return `${node.typeDescriptions.typeString}${node.name && node.name.length ? ` ${node.name}` : ''}`
}
} else
if (node.typeName && node.typeName.name) {
return `${node.typeName.name}${node.name && node.name.length ? ` ${node.name}` : ''}`
} else {
return `${node.name && node.name.length ? ` ${node.name}` : ''}`
}
}
const getParamaters = async (parameters: any) => {
if (parameters && parameters.parameters) {
@ -90,7 +105,7 @@ export class RemixHoverProvider {
contents.push({
value: 'No definition found. Please compile the source code.'
})
}
}
if (nodeDefinition) {
if (nodeDefinition.absolutePath) {

@ -14,7 +14,7 @@ export class RemixReferenceProvider {
const nodes = await this.props.plugin.call('contextualListener', 'referrencesAtPosition', cursorPosition)
const references = []
if (nodes && nodes.length) {
const compilationResult = await this.props.plugin.call('compilerArtefacts', 'getLastCompilationResult')
const compilationResult = await this.props.plugin.call('contextualListener', 'getLastCompilationResult')
const file = await this.props.plugin.call('fileManager', 'file')
if (compilationResult && compilationResult.data && compilationResult.data.sources[file]) {
for (const node of nodes) {

@ -12,29 +12,107 @@ export class RemixSignatureProvider {
async provideSignatureHelp(model: any, position: any, token: any, context: any) {
console.log(`providing signature help`, context);
console.log(position)
const line = model.getLineContent(position.lineNumber);
return new Promise((resolve, reject) => {
this.props.plugin.once('contextualListener', 'astFinished', async() => {
console.log('AST FINISHED')
resolve (await this.run(model, position, token, context))
})
this.props.plugin.call('contextualListener', 'compile').then(async () => {
})
})
}
parseParamaters(lineTrimmed: string) {
const len = lineTrimmed.length;
let modLine = '';
let k = 0;
while (k < len) {
const char = lineTrimmed.substring(k, k+1);
if (char == "'" || char == '"') {
do {
k++;
if (k >= len)
break;
const nChar = lineTrimmed.substring(k, k+1);
if (nChar == char)
break;
} while (k < len);
k++;
continue;
}
modLine += char;
k++;
}
console.log('MODLINE', modLine)
}
async run(model: any, position: any, token: any, context: any){
const line = model.getLineContent(position.lineNumber);
const lineTrimmed = line.trim();
this.parseParamaters(lineTrimmed)
// find position of the opening parenthesis before the cursor
const openParenIndex = line.lastIndexOf('(', position.column - 1);
console.log(openParenIndex)
const newPosition = new this.monaco.Position(position.lineNumber - 1, openParenIndex -1);
const newPosition = new this.monaco.Position(position.lineNumber, openParenIndex -1);
const cursorPosition = this.props.editorAPI.getHoverPosition(newPosition)
const nodeDefinition = await this.props.plugin.call('contextualListener', 'definitionAtPosition', cursorPosition)
console.log(nodeDefinition)
const getVariableDeclaration = async (node: any) => {
if (node.typeDescriptions && node.typeDescriptions.typeString) {
return `${node.typeDescriptions.typeString}${node.name && node.name.length ? ` ${node.name}` : ''}`
} else
if(node.typeName && node.typeName.name){
return `${node.typeName.name}${node.name && node.name.length ? ` ${node.name}` : ''}`
} else {
return `${node.name && node.name.length ? ` ${node.name}` : ''}`
}
}
const getParamaters = async (parameters: any) => {
console.log('PARAMETERS', parameters)
if (parameters && parameters.parameters) {
let params = []
for (const param of parameters.parameters) {
params.push(await getVariableDeclaration(param))
}
console.log('PARAMS', params)
return params
}
}
const getParamtersString = async (parameters: any) => {
if(parameters)
return `(${(await getParamaters(parameters)).join(', ')})`
return ''
}
const getParameterLabels = async (parameters: any) => {
const params = await getParamaters(parameters)
if(!params) return []
const arr = params.map(param => {
return {
label: param,
documentation: param
}
})
console.log(arr)
return arr
}
console.log('DEFS', nodeDefinition)
if(!nodeDefinition.nodeType || nodeDefinition.nodeType !== 'FunctionDefinition') return null
return {
value: {
signatures: [{
label: "test(var1, var2)",
parameters: [{
label: "var1",
documentation: "param 1"
},
{
label: "var2",
documentation: "param 2"
}]
label:`function ${nodeDefinition.name} ${await getParamtersString(nodeDefinition.parameters)}`,
parameters: await getParameterLabels(nodeDefinition.parameters),
}],
activeSignature: 0,
activeParameter: 0

@ -440,10 +440,9 @@ export const EditorUI = (props: EditorUIProps) => {
monacoRef.current.languages.registerReferenceProvider('remix-solidity', new RemixReferenceProvider(props, monaco))
monacoRef.current.languages.registerHoverProvider('remix-solidity', new RemixHoverProvider(props, monaco))
monacoRef.current.languages.registerCompletionItemProvider('remix-solidity', new RemixCompletionProvider(props, monaco))
monacoRef.current.languages.registerSignatureHelpProvider('remix-solidity', new RemixSignatureProvider(props, monaco))
// monacoRef.current.languages.registerSignatureHelpProvider('remix-solidity', new RemixSignatureProvider(props, monaco))
loadTypes(monacoRef.current)
props.plugin.call()
}
return (

@ -381,6 +381,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => {
// 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.
console.log('VERSION', selectedVersion)
if (selectedVersion === 'builtin') selectedVersion = state.defaultVersion
if (selectedVersion !== 'builtin' && canUseWorker(selectedVersion)) {
compileTabLogic.compiler.loadVersion(true, url)

Loading…
Cancel
Save