mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
Initial implementation of Language Server
This commit is contained in:
committed by
chriseth
parent
dc1dff975a
commit
927b24df1f
@@ -155,6 +155,12 @@ set(sources
|
||||
interface/StorageLayout.h
|
||||
interface/Version.cpp
|
||||
interface/Version.h
|
||||
lsp/LanguageServer.cpp
|
||||
lsp/LanguageServer.h
|
||||
lsp/FileRepository.cpp
|
||||
lsp/FileRepository.h
|
||||
lsp/Transport.cpp
|
||||
lsp/Transport.h
|
||||
parsing/DocStringParser.cpp
|
||||
parsing/DocStringParser.h
|
||||
parsing/Parser.cpp
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This file is part of solidity.
|
||||
|
||||
solidity is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
solidity is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with solidity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
#include <libsolidity/lsp/FileRepository.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace solidity;
|
||||
using namespace solidity::lsp;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
string stripFilePrefix(string const& _path)
|
||||
{
|
||||
if (_path.find("file://") == 0)
|
||||
return _path.substr(7);
|
||||
else
|
||||
return _path;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
string FileRepository::sourceUnitNameToClientPath(string const& _sourceUnitName) const
|
||||
{
|
||||
if (m_sourceUnitNamesToClientPaths.count(_sourceUnitName))
|
||||
return m_sourceUnitNamesToClientPaths.at(_sourceUnitName);
|
||||
else if (_sourceUnitName.find("file://") == 0)
|
||||
return _sourceUnitName;
|
||||
else
|
||||
return "file://" + (m_fileReader.basePath() / _sourceUnitName).generic_string();
|
||||
}
|
||||
|
||||
string FileRepository::clientPathToSourceUnitName(string const& _path) const
|
||||
{
|
||||
return m_fileReader.cliPathToSourceUnitName(stripFilePrefix(_path));
|
||||
}
|
||||
|
||||
map<string, string> const& FileRepository::sourceUnits() const
|
||||
{
|
||||
return m_fileReader.sourceUnits();
|
||||
}
|
||||
|
||||
void FileRepository::setSourceByClientPath(string const& _uri, string _text)
|
||||
{
|
||||
// This is needed for uris outside the base path. It can lead to collisions,
|
||||
// but we need to mostly rewrite this in a future version anyway.
|
||||
m_sourceUnitNamesToClientPaths.emplace(clientPathToSourceUnitName(_uri), _uri);
|
||||
m_fileReader.addOrUpdateFile(stripFilePrefix(_uri), move(_text));
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
This file is part of solidity.
|
||||
|
||||
solidity is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
solidity is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with solidity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#pragma once
|
||||
|
||||
#include <libsolidity/interface/FileReader.h>
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
namespace solidity::lsp
|
||||
{
|
||||
|
||||
class FileRepository
|
||||
{
|
||||
public:
|
||||
explicit FileRepository(boost::filesystem::path const& _basePath):
|
||||
m_fileReader(_basePath) {}
|
||||
|
||||
boost::filesystem::path const& basePath() const { return m_fileReader.basePath(); }
|
||||
|
||||
/// Translates a compiler-internal source unit name to an LSP client path.
|
||||
std::string sourceUnitNameToClientPath(std::string const& _sourceUnitName) const;
|
||||
/// Translates an LSP client path into a compiler-internal source unit name.
|
||||
std::string clientPathToSourceUnitName(std::string const& _uri) const;
|
||||
|
||||
/// @returns all sources by their compiler-internal source unit name.
|
||||
std::map<std::string, std::string> const& sourceUnits() const;
|
||||
/// Changes the source identified by the LSP client path _uri to _text.
|
||||
void setSourceByClientPath(std::string const& _uri, std::string _text);
|
||||
|
||||
frontend::ReadCallback::Callback reader() { return m_fileReader.reader(); }
|
||||
|
||||
private:
|
||||
std::map<std::string, std::string> m_sourceUnitNamesToClientPaths;
|
||||
frontend::FileReader m_fileReader;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
/*
|
||||
This file is part of solidity.
|
||||
|
||||
solidity is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
solidity is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with solidity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#include <libsolidity/ast/AST.h>
|
||||
#include <libsolidity/ast/ASTUtils.h>
|
||||
#include <libsolidity/ast/ASTVisitor.h>
|
||||
#include <libsolidity/interface/ReadFile.h>
|
||||
#include <libsolidity/interface/StandardCompiler.h>
|
||||
#include <libsolidity/lsp/LanguageServer.h>
|
||||
|
||||
#include <liblangutil/SourceReferenceExtractor.h>
|
||||
#include <liblangutil/CharStream.h>
|
||||
|
||||
#include <libsolutil/Visitor.h>
|
||||
#include <libsolutil/JSON.h>
|
||||
|
||||
#include <boost/exception/diagnostic_information.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::placeholders;
|
||||
|
||||
using namespace solidity::lsp;
|
||||
using namespace solidity::langutil;
|
||||
using namespace solidity::frontend;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
Json::Value toJson(LineColumn _pos)
|
||||
{
|
||||
Json::Value json = Json::objectValue;
|
||||
json["line"] = max(_pos.line, 0);
|
||||
json["character"] = max(_pos.column, 0);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
Json::Value toJsonRange(LineColumn const& _start, LineColumn const& _end)
|
||||
{
|
||||
Json::Value json;
|
||||
json["start"] = toJson(_start);
|
||||
json["end"] = toJson(_end);
|
||||
return json;
|
||||
}
|
||||
|
||||
optional<LineColumn> parseLineColumn(Json::Value const& _lineColumn)
|
||||
{
|
||||
if (_lineColumn.isObject() && _lineColumn["line"].isInt() && _lineColumn["character"].isInt())
|
||||
return LineColumn{_lineColumn["line"].asInt(), _lineColumn["character"].asInt()};
|
||||
else
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
constexpr int toDiagnosticSeverity(Error::Type _errorType)
|
||||
{
|
||||
// 1=Error, 2=Warning, 3=Info, 4=Hint
|
||||
switch (Error::errorSeverity(_errorType))
|
||||
{
|
||||
case Error::Severity::Error: return 1;
|
||||
case Error::Severity::Warning: return 2;
|
||||
case Error::Severity::Info: return 3;
|
||||
}
|
||||
solAssert(false);
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LanguageServer::LanguageServer(Transport& _transport):
|
||||
m_client{_transport},
|
||||
m_handlers{
|
||||
{"$/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); }},
|
||||
{"initialize", bind(&LanguageServer::handleInitialize, this, _1, _2)},
|
||||
{"initialized", [](auto, auto) {}},
|
||||
{"shutdown", [this](auto, auto) { m_state = State::ShutdownRequested; }},
|
||||
{"textDocument/didOpen", bind(&LanguageServer::handleTextDocumentDidOpen, this, _1, _2)},
|
||||
{"textDocument/didChange", bind(&LanguageServer::handleTextDocumentDidChange, this, _1, _2)},
|
||||
{"textDocument/didClose", bind(&LanguageServer::handleTextDocumentDidClose, this, _1, _2)},
|
||||
{"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _1, _2)},
|
||||
},
|
||||
m_fileRepository("/" /* basePath */),
|
||||
m_compilerStack{m_fileRepository.reader()}
|
||||
{
|
||||
}
|
||||
|
||||
optional<SourceLocation> LanguageServer::parsePosition(
|
||||
string const& _sourceUnitName,
|
||||
Json::Value const& _position
|
||||
) const
|
||||
{
|
||||
if (!m_fileRepository.sourceUnits().count(_sourceUnitName))
|
||||
return nullopt;
|
||||
|
||||
if (optional<LineColumn> lineColumn = parseLineColumn(_position))
|
||||
if (optional<int> const offset = CharStream::translateLineColumnToPosition(
|
||||
m_fileRepository.sourceUnits().at(_sourceUnitName),
|
||||
*lineColumn
|
||||
))
|
||||
return SourceLocation{*offset, *offset, make_shared<string>(_sourceUnitName)};
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
optional<SourceLocation> LanguageServer::parseRange(string const& _sourceUnitName, Json::Value const& _range) const
|
||||
{
|
||||
if (!_range.isObject())
|
||||
return nullopt;
|
||||
optional<SourceLocation> start = parsePosition(_sourceUnitName, _range["start"]);
|
||||
optional<SourceLocation> end = parsePosition(_sourceUnitName, _range["end"]);
|
||||
if (!start || !end)
|
||||
return nullopt;
|
||||
solAssert(*start->sourceName == *end->sourceName);
|
||||
start->end = end->end;
|
||||
return start;
|
||||
}
|
||||
|
||||
Json::Value LanguageServer::toRange(SourceLocation const& _location) const
|
||||
{
|
||||
if (!_location.hasText())
|
||||
return toJsonRange({}, {});
|
||||
|
||||
solAssert(_location.sourceName, "");
|
||||
CharStream const& stream = m_compilerStack.charStream(*_location.sourceName);
|
||||
LineColumn start = stream.translatePositionToLineColumn(_location.start);
|
||||
LineColumn end = stream.translatePositionToLineColumn(_location.end);
|
||||
return toJsonRange(start, end);
|
||||
}
|
||||
|
||||
Json::Value LanguageServer::toJson(SourceLocation const& _location) const
|
||||
{
|
||||
solAssert(_location.sourceName);
|
||||
Json::Value item = Json::objectValue;
|
||||
item["uri"] = m_fileRepository.sourceUnitNameToClientPath(*_location.sourceName);
|
||||
item["range"] = toRange(_location);
|
||||
return item;
|
||||
}
|
||||
|
||||
void LanguageServer::changeConfiguration(Json::Value const& _settings)
|
||||
{
|
||||
m_settingsObject = _settings;
|
||||
}
|
||||
|
||||
void LanguageServer::compile()
|
||||
{
|
||||
// For files that are not open, we have to take changes on disk into account,
|
||||
// so we just remove all non-open files.
|
||||
|
||||
FileRepository oldRepository(m_fileRepository.basePath());
|
||||
swap(oldRepository, m_fileRepository);
|
||||
|
||||
for (string const& fileName: m_openFiles)
|
||||
m_fileRepository.setSourceByClientPath(
|
||||
fileName,
|
||||
oldRepository.sourceUnits().at(oldRepository.clientPathToSourceUnitName(fileName))
|
||||
);
|
||||
|
||||
// TODO: optimize! do not recompile if nothing has changed (file(s) not flagged dirty).
|
||||
|
||||
m_compilerStack.reset(false);
|
||||
m_compilerStack.setSources(m_fileRepository.sourceUnits());
|
||||
m_compilerStack.compile(CompilerStack::State::AnalysisPerformed);
|
||||
}
|
||||
|
||||
void LanguageServer::compileAndUpdateDiagnostics()
|
||||
{
|
||||
compile();
|
||||
|
||||
// These are the source units we will sent diagnostics to the client for sure,
|
||||
// even if it is just to clear previous diagnostics.
|
||||
map<string, Json::Value> diagnosticsBySourceUnit;
|
||||
for (string const& sourceUnitName: m_fileRepository.sourceUnits() | ranges::views::keys)
|
||||
diagnosticsBySourceUnit[sourceUnitName] = Json::arrayValue;
|
||||
for (string const& sourceUnitName: m_nonemptyDiagnostics)
|
||||
diagnosticsBySourceUnit[sourceUnitName] = Json::arrayValue;
|
||||
|
||||
for (shared_ptr<Error const> const& error: m_compilerStack.errors())
|
||||
{
|
||||
SourceLocation const* location = error->sourceLocation();
|
||||
if (!location || !location->sourceName)
|
||||
// LSP only has diagnostics applied to individual files.
|
||||
continue;
|
||||
|
||||
Json::Value jsonDiag;
|
||||
jsonDiag["source"] = "solc";
|
||||
jsonDiag["severity"] = toDiagnosticSeverity(error->type());
|
||||
jsonDiag["code"] = Json::UInt64{error->errorId().error};
|
||||
string message = error->typeName() + ":";
|
||||
if (string const* comment = error->comment())
|
||||
message += " " + *comment;
|
||||
jsonDiag["message"] = move(message);
|
||||
jsonDiag["range"] = toRange(*location);
|
||||
|
||||
if (auto const* secondary = error->secondarySourceLocation())
|
||||
for (auto&& [secondaryMessage, secondaryLocation]: secondary->infos)
|
||||
{
|
||||
Json::Value jsonRelated;
|
||||
jsonRelated["message"] = secondaryMessage;
|
||||
jsonRelated["location"] = toJson(secondaryLocation);
|
||||
jsonDiag["relatedInformation"].append(jsonRelated);
|
||||
}
|
||||
|
||||
diagnosticsBySourceUnit[*location->sourceName].append(jsonDiag);
|
||||
}
|
||||
|
||||
m_nonemptyDiagnostics.clear();
|
||||
for (auto&& [sourceUnitName, diagnostics]: diagnosticsBySourceUnit)
|
||||
{
|
||||
Json::Value params;
|
||||
params["uri"] = m_fileRepository.sourceUnitNameToClientPath(sourceUnitName);
|
||||
if (!diagnostics.empty())
|
||||
m_nonemptyDiagnostics.insert(sourceUnitName);
|
||||
params["diagnostics"] = move(diagnostics);
|
||||
m_client.notify("textDocument/publishDiagnostics", move(params));
|
||||
}
|
||||
}
|
||||
|
||||
bool LanguageServer::run()
|
||||
{
|
||||
while (m_state != State::ExitRequested && m_state != State::ExitWithoutShutdown && !m_client.closed())
|
||||
{
|
||||
MessageID id;
|
||||
try
|
||||
{
|
||||
optional<Json::Value> const jsonMessage = m_client.receive();
|
||||
if (!jsonMessage)
|
||||
continue;
|
||||
|
||||
if ((*jsonMessage)["method"].isString())
|
||||
{
|
||||
string const methodName = (*jsonMessage)["method"].asString();
|
||||
id = (*jsonMessage)["id"];
|
||||
|
||||
if (auto handler = valueOrDefault(m_handlers, methodName))
|
||||
handler(id, (*jsonMessage)["params"]);
|
||||
else
|
||||
m_client.error(id, ErrorCode::MethodNotFound, "Unknown method " + methodName);
|
||||
}
|
||||
else
|
||||
m_client.error({}, ErrorCode::ParseError, "\"method\" has to be a string.");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
m_client.error(id, ErrorCode::InternalError, "Unhandled exception: "s + boost::current_exception_diagnostic_information());
|
||||
}
|
||||
}
|
||||
return m_state == State::ExitRequested;
|
||||
}
|
||||
|
||||
bool LanguageServer::checkServerInitialized(MessageID _id)
|
||||
{
|
||||
if (m_state != State::Initialized)
|
||||
{
|
||||
m_client.error(_id, ErrorCode::ServerNotInitialized, "Server is not properly initialized.");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
|
||||
{
|
||||
if (m_state != State::Started)
|
||||
{
|
||||
m_client.error(_id, ErrorCode::RequestFailed, "Initialize called at the wrong time.");
|
||||
return;
|
||||
}
|
||||
m_state = State::Initialized;
|
||||
|
||||
// The default of FileReader is to use `.`, but the path from where the LSP was started
|
||||
// should not matter.
|
||||
string rootPath("/");
|
||||
if (Json::Value uri = _args["rootUri"])
|
||||
{
|
||||
rootPath = uri.asString();
|
||||
if (!boost::starts_with(rootPath, "file://"))
|
||||
{
|
||||
m_client.error(_id, ErrorCode::InvalidParams, "rootUri only supports file URI scheme.");
|
||||
return;
|
||||
}
|
||||
rootPath = rootPath.substr(7);
|
||||
}
|
||||
else if (Json::Value rootPath = _args["rootPath"])
|
||||
rootPath = rootPath.asString();
|
||||
|
||||
m_fileRepository = FileRepository(boost::filesystem::path(rootPath));
|
||||
if (_args["initializationOptions"].isObject())
|
||||
changeConfiguration(_args["initializationOptions"]);
|
||||
|
||||
Json::Value replyArgs;
|
||||
replyArgs["serverInfo"]["name"] = "solc";
|
||||
replyArgs["serverInfo"]["version"] = string(VersionNumber);
|
||||
replyArgs["capabilities"]["textDocumentSync"]["openClose"] = true;
|
||||
replyArgs["capabilities"]["textDocumentSync"]["change"] = 2; // 0=none, 1=full, 2=incremental
|
||||
|
||||
m_client.reply(_id, move(replyArgs));
|
||||
}
|
||||
|
||||
|
||||
void LanguageServer::handleWorkspaceDidChangeConfiguration(MessageID _id, Json::Value const& _args)
|
||||
{
|
||||
if (!checkServerInitialized(_id))
|
||||
return;
|
||||
|
||||
if (_args["settings"].isObject())
|
||||
changeConfiguration(_args["settings"]);
|
||||
}
|
||||
|
||||
void LanguageServer::handleTextDocumentDidOpen(MessageID _id, Json::Value const& _args)
|
||||
{
|
||||
if (!checkServerInitialized(_id))
|
||||
return;
|
||||
|
||||
if (!_args["textDocument"])
|
||||
m_client.error(_id, ErrorCode::RequestFailed, "Text document parameter missing.");
|
||||
|
||||
string text = _args["textDocument"]["text"].asString();
|
||||
string uri = _args["textDocument"]["uri"].asString();
|
||||
m_openFiles.insert(uri);
|
||||
m_fileRepository.setSourceByClientPath(uri, move(text));
|
||||
compileAndUpdateDiagnostics();
|
||||
}
|
||||
|
||||
void LanguageServer::handleTextDocumentDidChange(MessageID _id, Json::Value const& _args)
|
||||
{
|
||||
if (!checkServerInitialized(_id))
|
||||
return;
|
||||
|
||||
string const uri = _args["textDocument"]["uri"].asString();
|
||||
|
||||
for (Json::Value jsonContentChange: _args["contentChanges"])
|
||||
{
|
||||
if (!jsonContentChange.isObject())
|
||||
{
|
||||
m_client.error(_id, ErrorCode::RequestFailed, "Invalid content reference.");
|
||||
return;
|
||||
}
|
||||
|
||||
string const sourceUnitName = m_fileRepository.clientPathToSourceUnitName(uri);
|
||||
if (!m_fileRepository.sourceUnits().count(sourceUnitName))
|
||||
{
|
||||
m_client.error(_id, ErrorCode::RequestFailed, "Unknown file: " + uri);
|
||||
return;
|
||||
}
|
||||
|
||||
string text = jsonContentChange["text"].asString();
|
||||
if (jsonContentChange["range"].isObject()) // otherwise full content update
|
||||
{
|
||||
optional<SourceLocation> change = parseRange(sourceUnitName, jsonContentChange["range"]);
|
||||
if (!change || !change->hasText())
|
||||
{
|
||||
m_client.error(
|
||||
_id,
|
||||
ErrorCode::RequestFailed,
|
||||
"Invalid source range: " + jsonCompactPrint(jsonContentChange["range"])
|
||||
);
|
||||
return;
|
||||
}
|
||||
string buffer = m_fileRepository.sourceUnits().at(sourceUnitName);
|
||||
buffer.replace(static_cast<size_t>(change->start), static_cast<size_t>(change->end - change->start), move(text));
|
||||
text = move(buffer);
|
||||
}
|
||||
m_fileRepository.setSourceByClientPath(uri, move(text));
|
||||
}
|
||||
|
||||
compileAndUpdateDiagnostics();
|
||||
}
|
||||
|
||||
void LanguageServer::handleTextDocumentDidClose(MessageID _id, Json::Value const& _args)
|
||||
{
|
||||
if (!checkServerInitialized(_id))
|
||||
return;
|
||||
|
||||
if (!_args["textDocument"])
|
||||
m_client.error(_id, ErrorCode::RequestFailed, "Text document parameter missing.");
|
||||
|
||||
string uri = _args["textDocument"]["uri"].asString();
|
||||
m_openFiles.erase(uri);
|
||||
|
||||
compileAndUpdateDiagnostics();
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
This file is part of solidity.
|
||||
|
||||
solidity is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
solidity is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with solidity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#pragma once
|
||||
|
||||
#include <libsolidity/lsp/Transport.h>
|
||||
#include <libsolidity/lsp/FileRepository.h>
|
||||
#include <libsolidity/interface/CompilerStack.h>
|
||||
#include <libsolidity/interface/FileReader.h>
|
||||
|
||||
#include <json/value.h>
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace solidity::lsp
|
||||
{
|
||||
|
||||
enum class ErrorCode;
|
||||
|
||||
/**
|
||||
* Solidity Language Server, managing one LSP client.
|
||||
* This implements a subset of LSP version 3.16 that can be found at:
|
||||
* https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/
|
||||
*/
|
||||
class LanguageServer
|
||||
{
|
||||
public:
|
||||
/// @param _transport Customizable transport layer.
|
||||
explicit LanguageServer(Transport& _transport);
|
||||
|
||||
/// Re-compiles the project and updates the diagnostics pushed to the client.
|
||||
void compileAndUpdateDiagnostics();
|
||||
|
||||
/// Loops over incoming messages via the transport layer until shutdown condition is met.
|
||||
///
|
||||
/// The standard shutdown condition is when the maximum number of consecutive failures
|
||||
/// has been exceeded.
|
||||
///
|
||||
/// @return boolean indicating normal or abnormal termination.
|
||||
bool run();
|
||||
|
||||
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.
|
||||
bool checkServerInitialized(MessageID _id);
|
||||
void handleInitialize(MessageID _id, Json::Value const& _args);
|
||||
void handleWorkspaceDidChangeConfiguration(MessageID _id, Json::Value const& _args);
|
||||
void handleTextDocumentDidOpen(MessageID _id, Json::Value const& _args);
|
||||
void handleTextDocumentDidChange(MessageID _id, Json::Value const& _args);
|
||||
void handleTextDocumentDidClose(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::optional<langutil::SourceLocation> parsePosition(
|
||||
std::string const& _sourceUnitName,
|
||||
Json::Value const& _position
|
||||
) const;
|
||||
/// @returns the source location given a source unit name and an LSP Range object,
|
||||
/// or nullopt on failure.
|
||||
std::optional<langutil::SourceLocation> parseRange(
|
||||
std::string const& _sourceUnitName,
|
||||
Json::Value const& _range
|
||||
) const;
|
||||
Json::Value toRange(langutil::SourceLocation const& _location) const;
|
||||
Json::Value toJson(langutil::SourceLocation const& _location) const;
|
||||
|
||||
// LSP related member fields
|
||||
using MessageHandler = std::function<void(MessageID, Json::Value const&)>;
|
||||
|
||||
enum class State { Started, Initialized, ShutdownRequested, ExitRequested, ExitWithoutShutdown };
|
||||
State m_state = State::Started;
|
||||
|
||||
Transport& m_client;
|
||||
std::map<std::string, MessageHandler> m_handlers;
|
||||
|
||||
/// Set of files known to be open by the client.
|
||||
std::set<std::string> m_openFiles;
|
||||
/// Set of source unit names for which we sent diagnostics to the client in the last iteration.
|
||||
std::set<std::string> m_nonemptyDiagnostics;
|
||||
FileRepository m_fileRepository;
|
||||
|
||||
frontend::CompilerStack m_compilerStack;
|
||||
|
||||
/// User-supplied custom configuration settings (such as EVM version).
|
||||
Json::Value m_settingsObject;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
This file is part of solidity.
|
||||
|
||||
solidity is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
solidity is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with solidity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#include <libsolidity/lsp/Transport.h>
|
||||
|
||||
#include <libsolutil/JSON.h>
|
||||
#include <libsolutil/Visitor.h>
|
||||
#include <libsolutil/CommonIO.h>
|
||||
#include <liblangutil/Exceptions.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
using namespace std;
|
||||
using namespace solidity::lsp;
|
||||
|
||||
IOStreamTransport::IOStreamTransport(istream& _in, ostream& _out):
|
||||
m_input{_in},
|
||||
m_output{_out}
|
||||
{
|
||||
}
|
||||
|
||||
IOStreamTransport::IOStreamTransport():
|
||||
IOStreamTransport(cin, cout)
|
||||
{
|
||||
}
|
||||
|
||||
bool IOStreamTransport::closed() const noexcept
|
||||
{
|
||||
return m_input.eof();
|
||||
}
|
||||
|
||||
optional<Json::Value> IOStreamTransport::receive()
|
||||
{
|
||||
auto const headers = parseHeaders();
|
||||
if (!headers)
|
||||
{
|
||||
error({}, ErrorCode::ParseError, "Could not parse RPC headers.");
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
if (!headers->count("content-length"))
|
||||
{
|
||||
error({}, ErrorCode::ParseError, "No content-length header found.");
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
string const data = util::readBytes(m_input, stoul(headers->at("content-length")));
|
||||
|
||||
Json::Value jsonMessage;
|
||||
string jsonParsingErrors;
|
||||
solidity::util::jsonParseStrict(data, jsonMessage, &jsonParsingErrors);
|
||||
if (!jsonParsingErrors.empty() || !jsonMessage || !jsonMessage.isObject())
|
||||
{
|
||||
error({}, ErrorCode::ParseError, "Could not parse RPC JSON payload. " + jsonParsingErrors);
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
return {move(jsonMessage)};
|
||||
}
|
||||
|
||||
void IOStreamTransport::notify(string _method, Json::Value _message)
|
||||
{
|
||||
Json::Value json;
|
||||
json["method"] = move(_method);
|
||||
json["params"] = move(_message);
|
||||
send(move(json));
|
||||
}
|
||||
|
||||
void IOStreamTransport::reply(MessageID _id, Json::Value _message)
|
||||
{
|
||||
Json::Value json;
|
||||
json["result"] = move(_message);
|
||||
send(move(json), _id);
|
||||
}
|
||||
|
||||
void IOStreamTransport::error(MessageID _id, ErrorCode _code, string _message)
|
||||
{
|
||||
Json::Value json;
|
||||
json["error"]["code"] = static_cast<int>(_code);
|
||||
json["error"]["message"] = move(_message);
|
||||
send(move(json), _id);
|
||||
}
|
||||
|
||||
void IOStreamTransport::send(Json::Value _json, MessageID _id)
|
||||
{
|
||||
solAssert(_json.isObject());
|
||||
_json["jsonrpc"] = "2.0";
|
||||
if (_id != Json::nullValue)
|
||||
_json["id"] = _id;
|
||||
|
||||
string const jsonString = solidity::util::jsonCompactPrint(_json);
|
||||
|
||||
m_output << "Content-Length: " << jsonString.size() << "\r\n";
|
||||
m_output << "\r\n";
|
||||
m_output << jsonString;
|
||||
|
||||
m_output.flush();
|
||||
}
|
||||
|
||||
optional<map<string, string>> IOStreamTransport::parseHeaders()
|
||||
{
|
||||
map<string, string> headers;
|
||||
|
||||
while (true)
|
||||
{
|
||||
string line;
|
||||
getline(m_input, line);
|
||||
if (boost::trim_copy(line).empty())
|
||||
break;
|
||||
|
||||
auto const delimiterPos = line.find(':');
|
||||
if (delimiterPos == string::npos)
|
||||
return nullopt;
|
||||
|
||||
string name = boost::to_lower_copy(line.substr(0, delimiterPos));
|
||||
string value = line.substr(delimiterPos + 1);
|
||||
if (!headers.emplace(
|
||||
boost::trim_copy(name),
|
||||
boost::trim_copy(value)
|
||||
).second)
|
||||
return nullopt;
|
||||
}
|
||||
return {move(headers)};
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
This file is part of solidity.
|
||||
|
||||
solidity is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
solidity is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with solidity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#pragma once
|
||||
|
||||
#include <json/value.h>
|
||||
|
||||
#include <functional>
|
||||
#include <iosfwd>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
namespace solidity::lsp
|
||||
{
|
||||
|
||||
using MessageID = Json::Value;
|
||||
|
||||
enum class ErrorCode
|
||||
{
|
||||
// Defined by JSON RPC
|
||||
ParseError = -32700,
|
||||
MethodNotFound = -32601,
|
||||
InvalidParams = -32602,
|
||||
InternalError = -32603,
|
||||
|
||||
// Defined by the protocol.
|
||||
ServerNotInitialized = -32002,
|
||||
RequestFailed = -32803
|
||||
};
|
||||
|
||||
/**
|
||||
* Transport layer API
|
||||
*
|
||||
* The transport layer API is abstracted to make LSP more testable as well as
|
||||
* this way it could be possible to support other transports (HTTP for example) easily.
|
||||
*/
|
||||
class Transport
|
||||
{
|
||||
public:
|
||||
virtual ~Transport() = default;
|
||||
|
||||
virtual bool closed() const noexcept = 0;
|
||||
virtual std::optional<Json::Value> receive() = 0;
|
||||
virtual void notify(std::string _method, Json::Value _params) = 0;
|
||||
virtual void reply(MessageID _id, Json::Value _result) = 0;
|
||||
virtual void error(MessageID _id, ErrorCode _code, std::string _message) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* LSP Transport using JSON-RPC over iostreams.
|
||||
*/
|
||||
class IOStreamTransport: public Transport
|
||||
{
|
||||
public:
|
||||
/// Constructs a standard stream transport layer.
|
||||
///
|
||||
/// @param _in for example std::cin (stdin)
|
||||
/// @param _out for example std::cout (stdout)
|
||||
IOStreamTransport(std::istream& _in, std::ostream& _out);
|
||||
|
||||
// Constructs a JSON transport using standard I/O streams.
|
||||
IOStreamTransport();
|
||||
|
||||
bool closed() const noexcept override;
|
||||
std::optional<Json::Value> receive() override;
|
||||
void notify(std::string _method, Json::Value _params) override;
|
||||
void reply(MessageID _id, Json::Value _result) override;
|
||||
void error(MessageID _id, ErrorCode _code, std::string _message) override;
|
||||
|
||||
protected:
|
||||
/// Sends an arbitrary raw message to the client.
|
||||
///
|
||||
/// Used by the notify/reply/error function family.
|
||||
virtual void send(Json::Value _message, MessageID _id = Json::nullValue);
|
||||
|
||||
/// Parses header section from the client including message-delimiting empty line.
|
||||
std::optional<std::map<std::string, std::string>> parseHeaders();
|
||||
|
||||
private:
|
||||
std::istream& m_input;
|
||||
std::ostream& m_output;
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user