diff --git a/README.md b/README.md index b1378a57..5986fa95 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ an error, and keep re-running the compiler until all of them are resolved. Example: ```javascript -var solc = require('solc'); +const solc = require('solc'); var input = { language: 'Solidity', @@ -204,6 +204,70 @@ The low-level API is as follows: For examples how to use them, please refer to the README of the above mentioned solc-js releases. +#### Language Server Mode + +Since version 0.8.11, the solidity compiler natively supports the +language server protocol. With solc-js, you can now use it as follows: + +```javascript +const solc = require('solc'); + +// Callback to be invoked when additional files have to be opened during +// source code analysis stage. +// +// This function behaves similar to the compilation file reader callback. +function fileReadCallback(path) +{ + if ('path' === 'file:///project/lib.sol') { + return { + contents: 'library L { function f() internal returns (uint) { return 7; } }'; + }; + } + return { error: 'File not found' }; +} + +// Put solcjs into LSP mode. +// Needs to be called only once before the actual LSP I/O calls. +solc.lsp.start(fileReadCallback); + +// Send some LSP JSON-RPC message and optionally receive a reply. +const lspInitializationMessage = { + 'jsonrpc': '2.0', + 'method': 'initialize', + 'params': { + 'rootUri': 'file:///project/', + 'capabilities': { + 'textDocument': { + 'publishDiagnostics': {'relatedInformation': true} + }, + 'workspace': { + 'applyEdit': true, + 'configuration': true, + 'didChangeConfiguration': {'dynamicRegistration': true}, + 'workspaceEdit': {'documentChanges': true}, + 'workspaceFolders': true + } + } + } +}; +solc.lsp.sendReceive(JSON.stringify(lspInitializationMessage))); +solc.lsp.sendReceive(JSON.stringify({'jsonrpc': '2.0', 'method': 'initialized'})); + +// Now, with the LSP server, being set up the following +// can be called as often as needed. +function lspRoundtrip(jsonRpcInputObject) +{ + return JSON.parse(solc.lsp.sendReceive(JSON.stringify(jsonRpcInputObject))); +} +``` + +This is a low level API endpoint for use by language server clients, +such as Visual Studio Code, or any other editor. +In order to know what you can pass in and what can come out, +it is highly recommended to have a look at: + + https://microsoft.github.io/language-server-protocol/specification + ### Using with Electron **Note:** diff --git a/common/interfaces.ts b/common/interfaces.ts new file mode 100644 index 00000000..a22750b3 --- /dev/null +++ b/common/interfaces.ts @@ -0,0 +1,8 @@ +export interface ReadCallbackReply { + error?: string; + contents?: string; +} + +export interface Callbacks { + import (path: string): ReadCallbackReply; +} diff --git a/wrapper.ts b/wrapper.ts index 36c18d74..df47c21e 100755 --- a/wrapper.ts +++ b/wrapper.ts @@ -3,6 +3,7 @@ import { https } from 'follow-redirects'; import MemoryStream from 'memorystream'; import assert from 'assert'; import * as semver from 'semver'; +import { Callbacks } from './common/interfaces'; const Module = module.constructor as any; @@ -45,6 +46,7 @@ function setupMethods (soljson) { reset = soljson.cwrap('solidity_reset', null, []); } + // Copies the string at @p str to @p ptr. const copyToCString = function (str, ptr) { const length = soljson.lengthBytesUTF8(str); // This is allocating memory using solc's allocator. @@ -60,6 +62,88 @@ function setupMethods (soljson) { soljson.setValue(ptr, buffer, '*'); }; + // Creates a wrapper around `int solidity_lsp_start(callbacks: Callbacks)`. + const createWrappedLspStart = function () { + if (!('_solidity_lsp_start' in soljson)) { + return () => { + throw new Error('lsp is not supported on this version.'); + }; + } + + const wrappedLspStart = soljson.cwrap('solidity_lsp_start', 'number', ['number']); + + return function (callbacks: Callbacks) { + const readCallback = callbacks.import; + + assert(typeof readCallback === 'function', 'Invalid callback specified.'); + const copyFromCString = soljson.UTF8ToString || soljson.Pointer_stringify; + + const wrappedReadCallback = function (path: string, contents: string, error: string) { + console.log("wrappedReadCallback: \"" + path + "\""); + + // Calls the user-supplied file read callback and passes the return values + // accordingly to either @p contents or into @p error on failure. + const result = readCallback(copyFromCString(path)); + + if (typeof result.contents === 'string') { + copyToCString(result.contents, contents); + } + + if (typeof result.error === 'string') { + copyToCString(result.error, error); + } + }; + + const addFunction = soljson.addFunction || soljson.Runtime.addFunction; + const removeFunction = soljson.removeFunction || soljson.Runtime.removeFunction; + const wrappedReadCallbackId = addFunction(wrappedReadCallback, 'ii'); + + try { + // call solidity_lsp_start(callbacks) + const output = wrappedLspStart(wrappedReadCallbackId); + removeFunction(wrappedReadCallbackId); + return output; + } catch (e) { + removeFunction(wrappedReadCallbackId); + throw e; + } + + // NOTE: We MUST NOT reset the compiler here. + // We instead could try to make sure to only release memory that is safe + // to be released. Probably by clearly defining semantics and memory + // lifetimes of output strings. + }; + }; + + // C signature : int solidity_lsp_send(char const* jsonRpcInputObject); + // TS signature : int send(object jsonRpcInputObject); + const createWrappedLspSend = function () { + if (!('_solidity_lsp_send' in soljson)) { + return () => { + throw new Error('lsp is not supported on this version.'); + }; + } + + const wrappedLspSend = soljson.cwrap('solidity_lsp_send', 'number', ['string']); + return (input: string) => wrappedLspSend(JSON.stringify(input)); + }; + + // C signature : char* solidity_lsp_send_receive(char const* jsonRpcInputObject); + // TS signature : object sendReceive(object jsonRpcInputObject); + // + // sendReceive send one message to the LSP server (notification or method call). + // The method call may reply with zero or one message that is going to be returned. + const createWrappedLspSendReceive = function () { + if (!('_solidity_lsp_send_receive' in soljson)) { + return () => { + throw new Error('lsp is not supported on this version.'); + }; + } + + const wrappedLspSendReceive = soljson.cwrap('solidity_lsp_send_receive', 'string', ['string']); + return (input: string) => JSON.parse(wrappedLspSendReceive(JSON.stringify(input))); + }; + // This is to support multiple versions of Emscripten. // Take a single `ptr` and returns a `str`. const copyFromCString = soljson.UTF8ToString || soljson.Pointer_stringify; @@ -324,6 +408,11 @@ function setupMethods (soljson) { importCallback: compileJSONCallback !== null || compileStandard !== null, nativeStandardJSON: compileStandard !== null }, + lsp: { + start: createWrappedLspStart(), + send: createWrappedLspSend(), + sendReceive: createWrappedLspSendReceive() + }, compile: compileStandardWrapper, // Loads the compiler of the given version from the github repository // instead of from the local filesystem.