[Language Server]: Add basic document hover support.

This commit is contained in:
Christian Parpart 2022-10-04 17:54:18 +02:00
parent 1d85eb5ccf
commit 9e7fe985bf
7 changed files with 273 additions and 12 deletions

View File

@ -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: Add basic document hover support.
Bugfixes:

View File

@ -155,6 +155,8 @@ set(sources
interface/StorageLayout.h
interface/Version.cpp
interface/Version.h
lsp/DocumentHoverHandler.cpp
lsp/DocumentHoverHandler.h
lsp/FileRepository.cpp
lsp/FileRepository.h
lsp/GotoDefinition.cpp

View File

@ -0,0 +1,126 @@
/*
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/DocumentHoverHandler.h>
#include <libsolidity/lsp/Utils.h>
#include <fmt/format.h>
namespace solidity::lsp
{
using namespace std;
using namespace solidity::lsp;
using namespace solidity::langutil;
using namespace solidity::frontend;
namespace
{
struct MarkdownBuilder
{
stringstream result;
MarkdownBuilder& solidityCode(string const& _code)
{
auto constexpr SolidityLanguageId = "solidity";
result << "```" << SolidityLanguageId << '\n' << _code << "\n```\n\n";
return *this;
}
MarkdownBuilder& paragraph(string const& _text)
{
if (!_text.empty())
{
result << _text << '\n';
if (_text.back() != '\n') // We want double-LF to ensure constructing a paragraph.
result << '\n';
}
return *this;
}
};
}
void DocumentHoverHandler::operator()(MessageID _id, Json::Value const& _args)
{
auto const [sourceUnitName, lineColumn] = HandlerBase(*this).extractSourceUnitNameAndLineColumn(_args);
auto const [sourceNode, sourceOffset] = m_server.astNodeAndOffsetAtSourceLocation(sourceUnitName, lineColumn);
MarkdownBuilder markdown{};
auto rangeToHighlight = toRange(sourceNode->location());
// Try getting the type definition of the underlying AST node, if available.
if (auto const* expression = dynamic_cast<Expression const*>(sourceNode))
{
if (auto const* declaration = ASTNode::referencedDeclaration(*expression))
if (declaration->type())
markdown.solidityCode(declaration->type()->toString(false));
}
else if (auto const* declaration = dynamic_cast<Declaration const*>(sourceNode))
{
if (declaration->type())
markdown.solidityCode(declaration->type()->toString(false));
}
else if (auto const* identifierPath = dynamic_cast<IdentifierPath const*>(sourceNode))
{
for (size_t i = 0; i < identifierPath->path().size(); ++i)
{
if (identifierPath->pathLocations()[i].containsOffset(sourceOffset))
{
rangeToHighlight = toRange(identifierPath->pathLocations()[i]);
if (i < identifierPath->annotation().pathDeclarations.size())
{
Declaration const* declaration = identifierPath->annotation().pathDeclarations[i];
if (declaration && declaration->type())
markdown.solidityCode(declaration->type()->toString(false));
if (auto const* structurallyDocumented = dynamic_cast<StructurallyDocumented const*>(declaration))
if (structurallyDocumented->documentation()->text())
markdown.paragraph(*structurallyDocumented->documentation()->text());
}
break;
}
}
}
// If this AST node contains documentation itself, append it.
if (auto const* documented = dynamic_cast<StructurallyDocumented const*>(sourceNode))
{
if (documented->documentation())
markdown.paragraph(*documented->documentation()->text());
}
auto tooltipText = markdown.result.str();
if (tooltipText.empty())
{
client().reply(_id, Json::nullValue);
return;
}
Json::Value reply = Json::objectValue;
reply["range"] = rangeToHighlight;
reply["contents"]["kind"] = "markdown";
reply["contents"]["value"] = std::move(tooltipText);
client().reply(_id, reply);
}
}

View File

@ -0,0 +1,32 @@
/*
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/HandlerBase.h>
namespace solidity::lsp
{
class DocumentHoverHandler: public HandlerBase
{
public:
using HandlerBase::HandlerBase;
void operator()(MessageID, Json::Value const&);
};
}

View File

@ -25,6 +25,7 @@
#include <libsolidity/lsp/Utils.h>
// LSP feature implementations
#include <libsolidity/lsp/DocumentHoverHandler.h>
#include <libsolidity/lsp/GotoDefinition.h>
#include <libsolidity/lsp/RenameSymbol.h>
#include <libsolidity/lsp/SemanticTokensBuilder.h>
@ -144,6 +145,7 @@ LanguageServer::LanguageServer(Transport& _transport):
{"textDocument/didOpen", bind(&LanguageServer::handleTextDocumentDidOpen, this, _2)},
{"textDocument/didChange", bind(&LanguageServer::handleTextDocumentDidChange, this, _2)},
{"textDocument/didClose", bind(&LanguageServer::handleTextDocumentDidClose, this, _2)},
{"textDocument/hover", DocumentHoverHandler(*this) },
{"textDocument/rename", RenameSymbol(*this) },
{"textDocument/implementation", GotoDefinition(*this) },
{"textDocument/semanticTokens/full", bind(&LanguageServer::semanticTokensFull, this, _1, _2)},
@ -417,6 +419,7 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
replyArgs["capabilities"]["semanticTokensProvider"]["range"] = false;
replyArgs["capabilities"]["semanticTokensProvider"]["full"] = true; // XOR requests.full.delta = true
replyArgs["capabilities"]["renameProvider"] = true;
replyArgs["capabilities"]["hoverProvider"] = true;
m_client.reply(_id, std::move(replyArgs));
}
@ -541,19 +544,21 @@ void LanguageServer::handleTextDocumentDidClose(Json::Value const& _args)
compileAndUpdateDiagnostics();
}
ASTNode const* LanguageServer::astNodeAtSourceLocation(std::string const& _sourceUnitName, LineColumn const& _filePos)
{
if (m_compilerStack.state() < CompilerStack::AnalysisPerformed)
return nullptr;
if (!m_fileRepository.sourceUnits().count(_sourceUnitName))
return nullptr;
if (optional<int> sourcePos =
m_compilerStack.charStream(_sourceUnitName).translateLineColumnToPosition(_filePos))
return locateInnermostASTNode(*sourcePos, m_compilerStack.ast(_sourceUnitName));
else
return nullptr;
return get<ASTNode const*>(astNodeAndOffsetAtSourceLocation(_sourceUnitName, _filePos));
}
tuple<ASTNode const*, int> LanguageServer::astNodeAndOffsetAtSourceLocation(std::string const& _sourceUnitName, LineColumn const& _filePos)
{
if (m_compilerStack.state() < CompilerStack::AnalysisPerformed)
return {nullptr, -1};
if (!m_fileRepository.sourceUnits().count(_sourceUnitName))
return {nullptr, -1};
optional<int> sourcePos = m_compilerStack.charStream(_sourceUnitName).translateLineColumnToPosition(_filePos);
if (!sourcePos)
return {nullptr, -1};
return {locateInnermostASTNode(*sourcePos, m_compilerStack.ast(_sourceUnitName)), *sourcePos};
}

View File

@ -77,6 +77,7 @@ public:
FileRepository& fileRepository() noexcept { return m_fileRepository; }
Transport& client() noexcept { return m_client; }
std::tuple<frontend::ASTNode const*, int> astNodeAndOffsetAtSourceLocation(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos);
frontend::ASTNode const* astNodeAtSourceLocation(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos);
frontend::CompilerStack const& compilerStack() const noexcept { return m_compilerStack; }

View File

@ -0,0 +1,94 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
/// Documenting another contract here.
contract AnotherContract {}
/// User being documented.
contract User
{
/// Some enum value.
enum SomeEnum
{
Red,
Blue
}
/// publicVariable being documented.
SomeEnum public publicVariable;
// ^ @Cursor1
// ^^^^^^^^ @Cursor1Range
// not documented
mapping(int => User.SomeEnum) someRemapping;
// ^ @Cursor2
// ^^^^^^^^ @Cursor2Range
/// Documenting the setContract().
function setValue(User.SomeEnum _value) public
// ^ @Cursor3
// ^^^^ @Cursor3Range
{
publicVariable = _value;
// ^ @Cursor4
// ^^^^^^^^^^^^^^ @Cursor4Range
}
function createAnotherContract() public returns (AnotherContract)
{
return new AnotherContract();
// ^ @Cursor5
// ^^^^^^^^^^^^^^^ @Cursor5Range
}
}
// ----
// -> textDocument/hover {
// "position": @Cursor1
// }
// <- {
// "contents": {
// "kind": "markdown",
// "value": "```solidity\ntype(enum User.SomeEnum)\n```\n\n"
// },
// "range": @Cursor1Range
// }
// -> textDocument/hover {
// "position": @Cursor2
// }
// <- {
// "contents": {
// "kind": "markdown",
// "value": "```solidity\ntype(enum User.SomeEnum)\n```\n\n"
// },
// "range": @Cursor2Range
// }
// -> textDocument/hover {
// "position": @Cursor3
// }
// <- {
// "contents": {
// "kind": "markdown",
// "value": "```solidity\ntype(contract User)\n```\n\nUser being documented.\n\n"
// },
// "range": @Cursor3Range
// }
// -> textDocument/hover {
// "position": @Cursor4
// }
// <- {
// "contents": {
// "kind": "markdown",
// "value": "```solidity\nenum User.SomeEnum\n```\n\n"
// },
// "range": @Cursor4Range
// }
// -> textDocument/hover {
// "position": @Cursor5
// }
// <- {
// "contents": {
// "kind": "markdown",
// "value": "```solidity\ntype(contract AnotherContract)\n```\n\nDocumenting another contract here.\n\n"
// },
// "range": @Cursor5Range
// }