mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
Language Server: Constrain server feature set based on advertised client capabilities.
This commit is contained in:
parent
2cb618a5c3
commit
5432c5cdb0
@ -7,6 +7,7 @@ Compiler Features:
|
||||
* Commandline Interface: Add `--no-cbor-metadata` that skips CBOR metadata from getting appended at the end of the bytecode.
|
||||
* Standard JSON: Add a boolean field `settings.metadata.appendCBOR` that skips CBOR metadata from getting appended at the end of the bytecode.
|
||||
* Yul Optimizer: Allow replacing the previously hard-coded cleanup sequence by specifying custom steps after a colon delimiter (``:``) in the sequence string.
|
||||
* Language Server: Constrain server feature set based on advertised client capabilities.
|
||||
|
||||
|
||||
Bugfixes:
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#include <libsolidity/lsp/GotoDefinition.h>
|
||||
#include <libsolidity/lsp/LanguageServer.h>
|
||||
#include <libsolidity/lsp/Transport.h> // for RequestError
|
||||
#include <libsolidity/lsp/Utils.h>
|
||||
#include <libsolidity/ast/AST.h>
|
||||
@ -32,6 +33,23 @@ using namespace solidity::langutil;
|
||||
using namespace solidity::lsp;
|
||||
using namespace std;
|
||||
|
||||
Json::Value GotoDefinition::onReportCapabilities(Json::Value const& _clientCapabilities)
|
||||
{
|
||||
Json::Value replyCapabilities = Json::objectValue;
|
||||
if (_clientCapabilities["textDocument"]["definition"])
|
||||
{
|
||||
replyCapabilities["definitionProvider"] = true;
|
||||
m_server.registerHandler("textDocument/definition", *this);
|
||||
}
|
||||
|
||||
if (_clientCapabilities["textDocument"]["implementation"])
|
||||
{
|
||||
replyCapabilities["implementationProvider"] = true;
|
||||
m_server.registerHandler("textDocument/implementation", *this);
|
||||
}
|
||||
return replyCapabilities;
|
||||
}
|
||||
|
||||
void GotoDefinition::operator()(MessageID _id, Json::Value const& _args)
|
||||
{
|
||||
auto const [sourceUnitName, lineColumn] = extractSourceUnitNameAndLineColumn(_args);
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#include <libsolidity/lsp/HandlerBase.h>
|
||||
#include <libsolidity/lsp/Transport.h>
|
||||
|
||||
namespace solidity::lsp
|
||||
{
|
||||
@ -25,6 +26,8 @@ class GotoDefinition: public HandlerBase
|
||||
public:
|
||||
explicit GotoDefinition(LanguageServer& _server): HandlerBase(_server) {}
|
||||
|
||||
Json::Value onReportCapabilities(Json::Value const& _clientCapabilities) override;
|
||||
|
||||
void operator()(MessageID, Json::Value const&);
|
||||
};
|
||||
|
||||
|
@ -31,6 +31,21 @@ using namespace solidity::lsp;
|
||||
using namespace solidity::util;
|
||||
using namespace std;
|
||||
|
||||
CharStreamProvider const& HandlerBase::charStreamProvider() const noexcept
|
||||
{
|
||||
return m_server.compilerStack();
|
||||
}
|
||||
|
||||
FileRepository& HandlerBase::fileRepository() const noexcept
|
||||
{
|
||||
return m_server.fileRepository();
|
||||
}
|
||||
|
||||
Transport& HandlerBase::client() const noexcept
|
||||
{
|
||||
return m_server.client();
|
||||
}
|
||||
|
||||
Json::Value HandlerBase::toRange(SourceLocation const& _location) const
|
||||
{
|
||||
if (!_location.hasText())
|
||||
|
@ -18,17 +18,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <libsolidity/lsp/FileRepository.h>
|
||||
#include <libsolidity/lsp/LanguageServer.h>
|
||||
|
||||
#include <liblangutil/SourceLocation.h>
|
||||
#include <liblangutil/CharStreamProvider.h>
|
||||
|
||||
#include <libsolutil/JSON.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace solidity::lsp
|
||||
{
|
||||
|
||||
class Transport;
|
||||
class LanguageServer;
|
||||
|
||||
/**
|
||||
* Helper base class for implementing handlers.
|
||||
@ -37,6 +39,16 @@ class HandlerBase
|
||||
{
|
||||
public:
|
||||
explicit HandlerBase(LanguageServer& _server): m_server{_server} {}
|
||||
virtual ~HandlerBase() = default;
|
||||
|
||||
/// Callback to be invoked on every custom handler that can be used to decide whether to enable
|
||||
/// or disable this feature on the server side.
|
||||
///
|
||||
/// @param _clientCapabilities JSON node to the root of the client advertised capabilities to be used
|
||||
/// to decide if the implemented feature should be enabled or not.
|
||||
/// @returns JSON object, with mapping to the capabilities to reply back with.
|
||||
/// Use this to write the capabilities-reply with respect the implementation.
|
||||
virtual Json::Value onReportCapabilities(Json::Value const& /*_clientCapabilities*/) { return Json::objectValue; }
|
||||
|
||||
Json::Value toRange(langutil::SourceLocation const& _location) const;
|
||||
Json::Value toJson(langutil::SourceLocation const& _location) const;
|
||||
@ -45,9 +57,9 @@ public:
|
||||
/// from the JSON-RPC parameters.
|
||||
std::pair<std::string, langutil::LineColumn> extractSourceUnitNameAndLineColumn(Json::Value const& _params) const;
|
||||
|
||||
langutil::CharStreamProvider const& charStreamProvider() const noexcept { return m_server.compilerStack(); }
|
||||
FileRepository& fileRepository() const noexcept { return m_server.fileRepository(); }
|
||||
Transport& client() const noexcept { return m_server.client(); }
|
||||
langutil::CharStreamProvider const& charStreamProvider() const noexcept;
|
||||
FileRepository& fileRepository() const noexcept;
|
||||
Transport& client() const noexcept;
|
||||
|
||||
protected:
|
||||
LanguageServer& m_server;
|
||||
|
@ -128,11 +128,57 @@ Json::Value semanticTokensLegend()
|
||||
return legend;
|
||||
}
|
||||
|
||||
class SemanticTokensHandler: public HandlerBase
|
||||
{
|
||||
public:
|
||||
using HandlerBase::HandlerBase;
|
||||
Json::Value onReportCapabilities(Json::Value const& _clientCapabilities) override;
|
||||
void operator()(MessageID _id, Json::Value const& _args);
|
||||
};
|
||||
|
||||
Json::Value SemanticTokensHandler::onReportCapabilities(Json::Value const& _clientCapabilities)
|
||||
{
|
||||
Json::Value replyCapabilities = Json::objectValue;
|
||||
|
||||
if (_clientCapabilities["textDocument"]["semanticTokens"])
|
||||
{
|
||||
replyCapabilities["semanticTokensProvider"]["legend"] = semanticTokensLegend();
|
||||
replyCapabilities["semanticTokensProvider"]["range"] = false;
|
||||
replyCapabilities["semanticTokensProvider"]["full"] = true; // XOR requests.full.delta = true
|
||||
|
||||
m_server.registerHandler("textDocument/semanticTokens/full", *this);
|
||||
}
|
||||
|
||||
return replyCapabilities;
|
||||
}
|
||||
|
||||
void SemanticTokensHandler::operator()(MessageID _id, Json::Value const& _args)
|
||||
{
|
||||
auto const uri = _args["textDocument"]["uri"];
|
||||
|
||||
m_server.compile();
|
||||
|
||||
auto const sourceName = fileRepository().uriToSourceUnitName(uri.as<string>());
|
||||
SourceUnit const& ast = m_server.compilerStack().ast(sourceName);
|
||||
charStreamProvider().charStream(sourceName);
|
||||
|
||||
Json::Value data = SemanticTokensBuilder().build(ast, charStreamProvider().charStream(sourceName));
|
||||
|
||||
Json::Value reply = Json::objectValue;
|
||||
reply["data"] = data;
|
||||
|
||||
client().reply(_id, std::move(reply));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LanguageServer::LanguageServer(Transport& _transport):
|
||||
m_client{_transport},
|
||||
m_handlers{
|
||||
m_onDemandHandlers{
|
||||
make_unique<GotoDefinition>(*this),
|
||||
make_unique<RenameSymbol>(*this),
|
||||
make_unique<SemanticTokensHandler>(*this),
|
||||
},
|
||||
m_coreHandlers{
|
||||
{"$/cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}},
|
||||
{"cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}},
|
||||
{"exit", [this](auto, auto) { m_state = (m_state == State::ShutdownRequested ? State::ExitRequested : State::ExitWithoutShutdown); }},
|
||||
@ -146,14 +192,20 @@ LanguageServer::LanguageServer(Transport& _transport):
|
||||
{"textDocument/didClose", bind(&LanguageServer::handleTextDocumentDidClose, this, _2)},
|
||||
{"textDocument/rename", RenameSymbol(*this) },
|
||||
{"textDocument/implementation", GotoDefinition(*this) },
|
||||
{"textDocument/semanticTokens/full", bind(&LanguageServer::semanticTokensFull, this, _1, _2)},
|
||||
{"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _2)},
|
||||
},
|
||||
m_client{_transport},
|
||||
m_handlers{m_coreHandlers},
|
||||
m_fileRepository("/" /* basePath */, {} /* no search paths */),
|
||||
m_compilerStack{m_fileRepository.reader()}
|
||||
{
|
||||
}
|
||||
|
||||
void LanguageServer::registerHandler(std::string _name, MessageHandler _handler)
|
||||
{
|
||||
m_handlers[std::move(_name)] = std::move(_handler);
|
||||
}
|
||||
|
||||
Json::Value LanguageServer::toRange(SourceLocation const& _location)
|
||||
{
|
||||
return HandlerBase(*this).toRange(_location);
|
||||
@ -409,14 +461,23 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
|
||||
Json::Value replyArgs;
|
||||
replyArgs["serverInfo"]["name"] = "solc";
|
||||
replyArgs["serverInfo"]["version"] = string(VersionNumber);
|
||||
replyArgs["capabilities"]["definitionProvider"] = true;
|
||||
replyArgs["capabilities"]["implementationProvider"] = true;
|
||||
replyArgs["capabilities"]["textDocumentSync"]["change"] = 2; // 0=none, 1=full, 2=incremental
|
||||
replyArgs["capabilities"]["textDocumentSync"]["openClose"] = true;
|
||||
replyArgs["capabilities"]["semanticTokensProvider"]["legend"] = semanticTokensLegend();
|
||||
replyArgs["capabilities"]["semanticTokensProvider"]["range"] = false;
|
||||
replyArgs["capabilities"]["semanticTokensProvider"]["full"] = true; // XOR requests.full.delta = true
|
||||
replyArgs["capabilities"]["renameProvider"] = true;
|
||||
|
||||
m_handlers = m_coreHandlers;
|
||||
m_handlers["textDocument/didOpen"] = bind(&LanguageServer::handleTextDocumentDidOpen, this, _2);
|
||||
m_handlers["textDocument/didChange"] = bind(&LanguageServer::handleTextDocumentDidChange, this, _2);
|
||||
m_handlers["textDocument/didClose"] = bind(&LanguageServer::handleTextDocumentDidClose, this, _2);
|
||||
m_handlers["workspace/didChangeConfiguration"] = bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _2);
|
||||
|
||||
for (unique_ptr<HandlerBase>& handler: m_onDemandHandlers)
|
||||
{
|
||||
Json::Value replyCapabilities = handler->onReportCapabilities(_args["capabilities"]);
|
||||
if (!replyCapabilities.isObject())
|
||||
continue;
|
||||
for (Json::String const& memberName: replyCapabilities.getMemberNames())
|
||||
replyArgs["capabilities"][memberName] = replyCapabilities[memberName];
|
||||
}
|
||||
|
||||
m_client.reply(_id, std::move(replyArgs));
|
||||
}
|
||||
@ -427,23 +488,6 @@ void LanguageServer::handleInitialized(MessageID, Json::Value const&)
|
||||
compileAndUpdateDiagnostics();
|
||||
}
|
||||
|
||||
void LanguageServer::semanticTokensFull(MessageID _id, Json::Value const& _args)
|
||||
{
|
||||
auto uri = _args["textDocument"]["uri"];
|
||||
|
||||
compile();
|
||||
|
||||
auto const sourceName = m_fileRepository.uriToSourceUnitName(uri.as<string>());
|
||||
SourceUnit const& ast = m_compilerStack.ast(sourceName);
|
||||
m_compilerStack.charStream(sourceName);
|
||||
Json::Value data = SemanticTokensBuilder().build(ast, m_compilerStack.charStream(sourceName));
|
||||
|
||||
Json::Value reply = Json::objectValue;
|
||||
reply["data"] = data;
|
||||
|
||||
m_client.reply(_id, std::move(reply));
|
||||
}
|
||||
|
||||
void LanguageServer::handleWorkspaceDidChangeConfiguration(Json::Value const& _args)
|
||||
{
|
||||
requireServerInitialized();
|
||||
|
@ -18,6 +18,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <libsolidity/lsp/Transport.h>
|
||||
#include <libsolidity/lsp/HandlerBase.h>
|
||||
#include <libsolidity/lsp/FileRepository.h>
|
||||
#include <libsolidity/interface/CompilerStack.h>
|
||||
#include <libsolidity/interface/FileReader.h>
|
||||
@ -26,6 +27,7 @@
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@ -80,6 +82,13 @@ public:
|
||||
frontend::ASTNode const* astNodeAtSourceLocation(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos);
|
||||
frontend::CompilerStack const& compilerStack() const noexcept { return m_compilerStack; }
|
||||
|
||||
using MessageHandler = std::function<void(MessageID, Json::Value const&)>;
|
||||
|
||||
void registerHandler(std::string name, MessageHandler handler);
|
||||
|
||||
/// Compile everything until after analysis phase.
|
||||
void compile();
|
||||
|
||||
private:
|
||||
/// Checks if the server is initialized (to be used by messages that need it to be initialized).
|
||||
/// Reports an error and returns false if not.
|
||||
@ -93,18 +102,12 @@ private:
|
||||
void handleTextDocumentDidClose(Json::Value const& _args);
|
||||
void handleRename(Json::Value const& _args);
|
||||
void handleGotoDefinition(MessageID _id, Json::Value const& _args);
|
||||
void semanticTokensFull(MessageID _id, Json::Value const& _args);
|
||||
|
||||
/// Invoked when the server user-supplied configuration changes (initiated by the client).
|
||||
void changeConfiguration(Json::Value const&);
|
||||
|
||||
/// Compile everything until after analysis phase.
|
||||
void compile();
|
||||
|
||||
std::vector<boost::filesystem::path> allSolidityFilesFromProject() const;
|
||||
|
||||
using MessageHandler = std::function<void(MessageID, Json::Value const&)>;
|
||||
|
||||
Json::Value toRange(langutil::SourceLocation const& _location);
|
||||
Json::Value toJson(langutil::SourceLocation const& _location);
|
||||
|
||||
@ -113,6 +116,11 @@ private:
|
||||
enum class State { Started, Initialized, ShutdownRequested, ExitRequested, ExitWithoutShutdown };
|
||||
State m_state = State::Started;
|
||||
|
||||
// Contains the list of features that are only advertised if advertised by the client
|
||||
// during initialization stage.
|
||||
std::array<std::unique_ptr<HandlerBase>, 3> m_onDemandHandlers;
|
||||
std::map<std::string, MessageHandler> m_coreHandlers;
|
||||
|
||||
Transport& m_client;
|
||||
std::map<std::string, MessageHandler> m_handlers;
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#include <libsolidity/lsp/RenameSymbol.h>
|
||||
#include <libsolidity/lsp/LanguageServer.h>
|
||||
#include <libsolidity/lsp/Utils.h>
|
||||
|
||||
#include <libyul/AST.h>
|
||||
@ -48,6 +49,19 @@ CallableDeclaration const* extractCallableDeclaration(FunctionCall const& _funct
|
||||
|
||||
}
|
||||
|
||||
Json::Value RenameSymbol::onReportCapabilities(Json::Value const& _clientCapabilities)
|
||||
{
|
||||
Json::Value replyCapabilities = Json::objectValue;
|
||||
|
||||
if (_clientCapabilities["textDocument"]["rename"])
|
||||
{
|
||||
replyCapabilities["renameProvider"] = true;
|
||||
m_server.registerHandler("textDocument/rename", *this);
|
||||
}
|
||||
|
||||
return replyCapabilities;
|
||||
}
|
||||
|
||||
void RenameSymbol::operator()(MessageID _id, Json::Value const& _args)
|
||||
{
|
||||
auto const&& [sourceUnitName, lineColumn] = extractSourceUnitNameAndLineColumn(_args);
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#include <libsolidity/lsp/HandlerBase.h>
|
||||
#include <libsolidity/lsp/Transport.h>
|
||||
#include <libsolidity/ast/AST.h>
|
||||
#include <libsolidity/ast/ASTVisitor.h>
|
||||
|
||||
@ -27,6 +28,8 @@ class RenameSymbol: public HandlerBase
|
||||
public:
|
||||
explicit RenameSymbol(LanguageServer& _server): HandlerBase(_server) {}
|
||||
|
||||
Json::Value onReportCapabilities(Json::Value const& _clientCapabilities) override;
|
||||
|
||||
void operator()(MessageID, Json::Value const&);
|
||||
protected:
|
||||
// Nested class because otherwise `RenameSymbol` couldn't be easily used
|
||||
|
@ -927,7 +927,13 @@ class SolidityLSPTestSuite: # {{{
|
||||
'trace': 'messages',
|
||||
'capabilities': {
|
||||
'textDocument': {
|
||||
'publishDiagnostics': {'relatedInformation': True}
|
||||
'definition': {'dynamicRegistration': True},
|
||||
'documentHighlight': {'dynamicRegistration': True},
|
||||
'implementation': {'dynamicRegistration': True},
|
||||
'publishDiagnostics': {'relatedInformation': True},
|
||||
'references': {'dynamicRegistration': True},
|
||||
'rename': {'dynamicRegistration': True},
|
||||
'semanticTokens': {'dynamicRegistration': True}
|
||||
},
|
||||
'workspace': {
|
||||
'applyEdit': True,
|
||||
|
Loading…
Reference in New Issue
Block a user