Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial bits for integrating the LSP API. #609

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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:**
Expand Down
8 changes: 8 additions & 0 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface ReadCallbackReply {
error?: string;
contents?: string;
}

export interface Callbacks {
import (path: string): ReadCallbackReply;
}
89 changes: 89 additions & 0 deletions wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just run npm run lint:fix or the related package.json command to resolve these before committing.

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;
Expand Down Expand Up @@ -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.
Expand Down