Browse Source

Completion Service

* Adding global variables
* Adding global functions
* Adding global units
* Adding initial support for dot notation autocomplete
* Adding language units
Juan Blanco 3 years ago
parent
commit
c37d0d6412
5 changed files with 311 additions and 13 deletions
  1. 1 1
      package.json
  2. 11 0
      solidity.configuration.json
  3. 247 2
      src/completionService.ts
  4. 51 9
      src/server.ts
  5. 1 1
      syntaxes/solidity.json

+ 1 - 1
package.json

@@ -7,7 +7,7 @@
     "blockchain",
     "compiler"
   ],
-  "version": "0.0.25",
+  "version": "0.0.26",
   "publisher": "JuanBlanco",
   "engines": {
     "vscode": "^1.8.0"

+ 11 - 0
solidity.configuration.json

@@ -10,5 +10,16 @@
 		["{", "}"],
 		["[", "]"],
 		["(", ")"]
+	],
+	"autoClosingPairs": [
+		{ "open": "{", "close": "}" },
+		{ "open": "[", "close": "]" },
+		{ "open": "(", "close": ")" },
+		{ "open": "/**", "close": " */", "notIn": ["string"] }
+	],
+	"surroundingPairs": [
+		["{", "}"],
+		["[", "]"],
+		["(", ")"]
 	]
 }

+ 247 - 2
src/completionService.ts

@@ -1,7 +1,7 @@
 import * as solparse from 'solparse';
 import * as projectService from './projectService';
 import {ContractCollection} from './model/contractsCollection';
-import { CompletionItem, CompletionItemKind } from 'vscode-languageserver';
+import { CompletionItem, CompletionItemKind, Command } from 'vscode-languageserver';
 
 export class CompletionService {
 
@@ -70,7 +70,6 @@ export class CompletionService {
         let result = solparse.parse(documentText);
         // console.log(JSON.stringify(result));
         // TODO struct, modifier
-        // Find imports
         result.body.forEach(element => {
             if (element.type === 'ContractStatement' ||  element.type === 'LibraryStatement') {
                 let contractName = element.name;
@@ -115,6 +114,252 @@ export class CompletionService {
         // console.log('total completion items' + completionItems.length);
         return completionItems;
     }
+}
+
+export function GetCompletionTypes(): CompletionItem[] {
+    let completionItems = [];
+    let types = ['address', 'string', 'bytes', 'byte', 'int', 'uint', 'bool', 'hash'];
+    types.forEach(type => {
+        let completionItem =  CompletionItem.create(type);
+        completionItem.kind = CompletionItemKind.Keyword;
+        completionItem.detail = type + ' type';
+        completionItems.push(completionItem);
+    });
+    // add mapping
+    return completionItems;
+}
+
+
+export function GeCompletionUnits(): CompletionItem[] {
+    let completionItems = [];
+    let etherUnits = ['wei', 'finney', 'szabo', 'ether'] ;
+    etherUnits.forEach(unit => {
+        let completionItem =  CompletionItem.create(unit);
+        completionItem.kind = CompletionItemKind.Unit;
+        completionItem.detail = unit + ': ether unit';
+        completionItems.push(completionItem);
+    });
+
+    let timeUnits = ['seconds', 'minutes', 'hours', 'days', 'weeks', 'years'];
+    timeUnits.forEach(unit => {
+        let completionItem =  CompletionItem.create(unit);
+        completionItem.kind = CompletionItemKind.Unit;
+        completionItem.detail = unit + ': time unit';
+        completionItems.push(completionItem);
+    });
+
+    return completionItems;
+}
+
+export function GetGlobalVariables(): CompletionItem[] {
+    return [
+        {
+            detail: 'Current block',
+            kind: CompletionItemKind.Variable,
+            label: 'block',
+        },
+        {
+            detail: 'Current Message',
+            kind: CompletionItemKind.Variable,
+            label: 'msg',
+        },
+        {
+            detail: '(uint): current block timestamp (alias for block.timestamp)',
+            kind: CompletionItemKind.Variable,
+            label: 'now',
+        },
+        {
+            detail: 'Current transaction',
+            kind: CompletionItemKind.Variable,
+            label: 'tx',
+        },
+    ];
+}
+
+export function GetGlobalFunctions(): CompletionItem[] {
+    return [
+        {
+            detail: 'assert(bool condition): throws if the condition is not met - to be used for internal errors.',
+            insertText: 'assert(${1:condition});',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Function,
+            label: 'assert',
+        },
+        {
+            detail: 'require(bool condition): throws if the condition is not met - to be used for errors in inputs or external components.',
+            insertText: 'require(${1:condition});',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'require',
+        },
+        {
+            detail: 'revert(): abort execution and revert state changes',
+            insertText: 'revert();',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'revert',
+        },
+        {
+            detail: 'addmod(uint x, uint y, uint k) returns (uint):' +
+                    'compute (x + y) % k where the addition is performed with arbitrary precision and does not wrap around at 2**256',
+            insertText: 'addmod(${1:x},${2:y},${3:k})',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'addmod',
+        },
+        {
+            detail: 'mulmod(uint x, uint y, uint k) returns (uint):' +
+                    'compute (x * y) % k where the multiplication is performed with arbitrary precision and does not wrap around at 2**256',
+            insertText: 'mulmod(${1:x},${2:y},${3:k})',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'mulmod',
+        },
+        {
+            detail: 'keccak256(...) returns (bytes32):' +
+                    'compute the Ethereum-SHA-3 (Keccak-256) hash of the (tightly packed) arguments',
+            insertText: 'keccak256(${1:x})',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'keccak256',
+        },
+        {
+            detail: 'sha256(...) returns (bytes32):' +
+                    'compute the SHA-256 hash of the (tightly packed) arguments',
+            insertText: 'sha256(${1:x})',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'sha256',
+        },
+        {
+            detail: 'sha3(...) returns (bytes32):' +
+                    'alias to keccak256',
+            insertText: 'sha3(${1:x})',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'sha3',
+        },
+        {
+            detail: 'ripemd160(...) returns (bytes20):' +
+                    'compute RIPEMD-160 hash of the (tightly packed) arguments',
+            insertText: 'ripemd160(${1:x})',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'ripemd160',
+        },
+        {
+            detail: 'ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):' +
+                    'recover the address associated with the public key from elliptic curve signature or return zero on error',
+            insertText: 'ecrecover(${1:hash},${2:v},${3:r},${4:s})',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'ecrecover',
+        },
+
+    ];
+}
+
+export function GetContextualAutoCompleteByGlobalVariable(lineText: string, wordEndPosition: number): CompletionItem[] {
+    if (isAutocompleteTrigeredByVariableName('block', lineText, wordEndPosition)) {
+        return getBlockCompletionItems();
+    }
+    if (isAutocompleteTrigeredByVariableName('msg', lineText, wordEndPosition)) {
+        return getMsgCompletionItems();
+    }
+    if (isAutocompleteTrigeredByVariableName('tx', lineText, wordEndPosition)) {
+        return getTxCompletionItems();
+    }
+    return null;
+}
 
+function isAutocompleteTrigeredByVariableName(variableName: string, lineText: string, wordEndPosition: number): Boolean {
+    const nameLength = variableName.length;
+    if (wordEndPosition >= nameLength
+        // does it equal our name?
+        && lineText.substr(wordEndPosition - nameLength, nameLength) === variableName) {
+          return true;
+        }
+    return false;
+}
+
+function getBlockCompletionItems(): CompletionItem[] {
+    return [
+        {
+            detail: '(address): Current block miner’s address',
+            kind: CompletionItemKind.Property,
+            label: 'coinbase',
+        },
+        {
+            detail: '(bytes32): Hash of the given block - only works for 256 most recent blocks excluding current',
+            insertText: 'blockhash(${1:blockNumber});',
+            insertTextFormat: 2,
+            kind: CompletionItemKind.Method,
+            label: 'blockhash',
+        },
+        {
+            detail: '(uint): current block difficulty',
+            kind: CompletionItemKind.Property,
+            label: 'difficulty',
+        },
+        {
+            detail: '(uint): current block gaslimit',
+            kind: CompletionItemKind.Property,
+            label: 'gasLimit',
+        },
+        {
+            detail: '(uint): current block number',
+            kind: CompletionItemKind.Property,
+            label: 'number',
+        },
+        {
+            detail: '(uint): current block timestamp as seconds since unix epoch',
+            kind: CompletionItemKind.Property,
+            label: 'timestamp',
+        },
+    ];
 }
 
+function getTxCompletionItems(): CompletionItem[] {
+    return [
+        {
+            detail: '(uint): gas price of the transaction',
+            kind: CompletionItemKind.Property,
+            label: 'gas',
+        },
+        {
+            detail: '(address): sender of the transaction (full call chain)',
+            kind: CompletionItemKind.Property,
+            label: 'origin',
+        },
+    ];
+}
+
+function getMsgCompletionItems(): CompletionItem[] {
+    return [
+        {
+            detail: '(bytes): complete calldata',
+            kind: CompletionItemKind.Property,
+            label: 'data',
+        },
+        {
+            detail: '(uint): remaining gas',
+            kind: CompletionItemKind.Property,
+            label: 'gas',
+        },
+        {
+            detail: '(address): sender of the message (current call)',
+            kind: CompletionItemKind.Property,
+            label: 'sender',
+        },
+        {
+            detail: '(bytes4): first four bytes of the calldata (i.e. function identifier)',
+            kind: CompletionItemKind.Property,
+            label: 'sig',
+        },
+        {
+            detail: '(uint): number of wei sent with the message',
+            kind: CompletionItemKind.Property,
+            label: 'value',
+        },
+    ];
+}

+ 51 - 9
src/server.ts

@@ -2,7 +2,9 @@
 
 import {SolcCompiler} from './solcCompiler';
 import {SoliumService} from './solium';
-import {CompletionService} from './completionService';
+import {CompletionService, GetCompletionTypes,
+        GetContextualAutoCompleteByGlobalVariable, GeCompletionUnits,
+        GetGlobalFunctions, GetGlobalVariables} from './completionService';
 
 import {
     createConnection, IConnection,
@@ -11,6 +13,7 @@ import {
     Files, DiagnosticSeverity, Diagnostic,
     TextDocumentChangeEvent, TextDocumentPositionParams,
     CompletionItem, CompletionItemKind,
+    Range, Position, Location,
 } from 'vscode-languageserver';
 
 interface Settings {
@@ -83,24 +86,61 @@ function validate(document) {
 connection.onCompletion((textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
     // The pass parameter contains the position of the text document in
     // which code complete got requested. For the example we ignore this
-    // info and always provide the same completion items.
+    // info and always provide the same completion items
+    let completionItems = [];
     try {
         let document = documents.get(textDocumentPosition.textDocument.uri);
         const documentPath = Files.uriToFilePath(textDocumentPosition.textDocument.uri);
         const documentText = document.getText();
+        let lines = documentText.split(/\r?\n/g);
+        let position = textDocumentPosition.position;
+
+        let start = 0;
+        let triggeredByDot = false;
+        for (let i = position.character; i >= 0; i--) {
+            if (lines[position.line[i]] === ' ') {
+                triggeredByDot = false;
+                i = 0;
+                start = 0;
+            }
+            if (lines[position.line][i] === '.') {
+                start = i;
+                i = 0;
+                triggeredByDot = true;
+            }
+        }
+
+        if (triggeredByDot) {
+            let globalVariableContext = GetContextualAutoCompleteByGlobalVariable(lines[position.line], start);
+            if (globalVariableContext != null) {
+                completionItems = completionItems.concat(globalVariableContext);
+            }
+            return completionItems;
+        }
+
         const service = new CompletionService(rootPath);
-        let completionItems = service.getAllCompletionItems(documentText, documentPath);
-        return completionItems;
+        completionItems = completionItems.concat(service.getAllCompletionItems(documentText, documentPath));
+
     } catch (error) {
         // graceful catch
-        // console.log(error);
+       // console.log(error);
+    } finally {
+
+        completionItems = completionItems.concat(GetCompletionTypes());
+        completionItems = completionItems.concat(GeCompletionUnits());
+        completionItems = completionItems.concat(GetGlobalFunctions());
+        completionItems = completionItems.concat(GetGlobalVariables());
     }
+    return completionItems;
 });
 
+
+
+
 // This handler resolve additional information for the item selected in
 // the completion list.
  // connection.onCompletionResolve((item: CompletionItem): CompletionItem => {
- //   return item;
+ //   item.
  // });
 
 function validateAllDocuments() {
@@ -127,7 +167,7 @@ function startValidation() {
 documents.onDidChangeContent(event => {
     if (!validatingDocument && !validatingAllDocuments) {
         validatingDocument = true; // control the flag at a higher level
-        // slow down, give enough time to type (3 seconds?)
+        // slow down, give enough time to type (1.5 seconds?)
         setTimeout( () =>  validate(event.document), validationDelay);
     }
 });
@@ -143,12 +183,14 @@ documents.listen(connection);
 connection.onInitialize((result): InitializeResult => {
     rootPath = result.rootPath;
     solcCompiler = new SolcCompiler(rootPath);
-    soliumService = new SoliumService(null, connection);
-    startValidation();
+    if (soliumService == null) {
+        soliumService = new SoliumService(null, connection);
+    }
     return {
         capabilities: {
             completionProvider: {
                 resolveProvider: false,
+                triggerCharacters: [ '.' ],
             },
             textDocumentSync: documents.syncKind,
         },

+ 1 - 1
syntaxes/solidity.json

@@ -73,7 +73,7 @@
         },
         {
             "comment": "Langauge keywords",
-            "match": "\\b(var|import|function|enum|constant|if|else|for|while|do|break|continue|throw|returns?|private|public|external|inherited|storage|delete|memory|this|suicide|let|new|is|\\_)\\b",
+            "match": "\\b(var|import|function|enum|constant|if|else|for|while|do|break|continue|throw|returns?|private|public|external|inherited|storage|delete|memory|this|suicide|let|new|is|ether|wei|finney|szabo|seconds|minutes|hours|days|weeks|years\\_)\\b",
             "name": "keyword.control"
         },
         {