Skip to content

Commit 9e7fe98

Browse files
[Language Server]: Add basic document hover support.
1 parent 1d85eb5 commit 9e7fe98

File tree

7 files changed

+271
-10
lines changed

7 files changed

+271
-10
lines changed

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Compiler Features:
77
* Commandline Interface: Add `--no-cbor-metadata` that skips CBOR metadata from getting appended at the end of the bytecode.
88
* Standard JSON: Add a boolean field `settings.metadata.appendCBOR` that skips CBOR metadata from getting appended at the end of the bytecode.
99
* Yul Optimizer: Allow replacing the previously hard-coded cleanup sequence by specifying custom steps after a colon delimiter (``:``) in the sequence string.
10+
* Language Server: Add basic document hover support.
1011

1112

1213
Bugfixes:

libsolidity/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ set(sources
155155
interface/StorageLayout.h
156156
interface/Version.cpp
157157
interface/Version.h
158+
lsp/DocumentHoverHandler.cpp
159+
lsp/DocumentHoverHandler.h
158160
lsp/FileRepository.cpp
159161
lsp/FileRepository.h
160162
lsp/GotoDefinition.cpp
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
This file is part of solidity.
3+
4+
solidity is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
solidity is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with solidity. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
// SPDX-License-Identifier: GPL-3.0
18+
19+
#include <libsolidity/lsp/DocumentHoverHandler.h>
20+
#include <libsolidity/lsp/Utils.h>
21+
22+
#include <fmt/format.h>
23+
24+
namespace solidity::lsp
25+
{
26+
27+
using namespace std;
28+
29+
using namespace solidity::lsp;
30+
using namespace solidity::langutil;
31+
using namespace solidity::frontend;
32+
33+
namespace
34+
{
35+
36+
struct MarkdownBuilder
37+
{
38+
stringstream result;
39+
40+
MarkdownBuilder& solidityCode(string const& _code)
41+
{
42+
auto constexpr SolidityLanguageId = "solidity";
43+
result << "```" << SolidityLanguageId << '\n' << _code << "\n```\n\n";
44+
return *this;
45+
}
46+
47+
MarkdownBuilder& paragraph(string const& _text)
48+
{
49+
if (!_text.empty())
50+
{
51+
result << _text << '\n';
52+
if (_text.back() != '\n') // We want double-LF to ensure constructing a paragraph.
53+
result << '\n';
54+
}
55+
return *this;
56+
}
57+
};
58+
59+
}
60+
61+
void DocumentHoverHandler::operator()(MessageID _id, Json::Value const& _args)
62+
{
63+
auto const [sourceUnitName, lineColumn] = HandlerBase(*this).extractSourceUnitNameAndLineColumn(_args);
64+
auto const [sourceNode, sourceOffset] = m_server.astNodeAndOffsetAtSourceLocation(sourceUnitName, lineColumn);
65+
66+
MarkdownBuilder markdown{};
67+
auto rangeToHighlight = toRange(sourceNode->location());
68+
69+
// Try getting the type definition of the underlying AST node, if available.
70+
if (auto const* expression = dynamic_cast<Expression const*>(sourceNode))
71+
{
72+
if (auto const* declaration = ASTNode::referencedDeclaration(*expression))
73+
if (declaration->type())
74+
markdown.solidityCode(declaration->type()->toString(false));
75+
}
76+
else if (auto const* declaration = dynamic_cast<Declaration const*>(sourceNode))
77+
{
78+
if (declaration->type())
79+
markdown.solidityCode(declaration->type()->toString(false));
80+
}
81+
else if (auto const* identifierPath = dynamic_cast<IdentifierPath const*>(sourceNode))
82+
{
83+
for (size_t i = 0; i < identifierPath->path().size(); ++i)
84+
{
85+
if (identifierPath->pathLocations()[i].containsOffset(sourceOffset))
86+
{
87+
rangeToHighlight = toRange(identifierPath->pathLocations()[i]);
88+
89+
if (i < identifierPath->annotation().pathDeclarations.size())
90+
{
91+
Declaration const* declaration = identifierPath->annotation().pathDeclarations[i];
92+
if (declaration && declaration->type())
93+
markdown.solidityCode(declaration->type()->toString(false));
94+
if (auto const* structurallyDocumented = dynamic_cast<StructurallyDocumented const*>(declaration))
95+
if (structurallyDocumented->documentation()->text())
96+
markdown.paragraph(*structurallyDocumented->documentation()->text());
97+
}
98+
break;
99+
}
100+
}
101+
}
102+
103+
// If this AST node contains documentation itself, append it.
104+
if (auto const* documented = dynamic_cast<StructurallyDocumented const*>(sourceNode))
105+
{
106+
if (documented->documentation())
107+
markdown.paragraph(*documented->documentation()->text());
108+
}
109+
110+
auto tooltipText = markdown.result.str();
111+
112+
if (tooltipText.empty())
113+
{
114+
client().reply(_id, Json::nullValue);
115+
return;
116+
}
117+
118+
Json::Value reply = Json::objectValue;
119+
reply["range"] = rangeToHighlight;
120+
reply["contents"]["kind"] = "markdown";
121+
reply["contents"]["value"] = std::move(tooltipText);
122+
123+
client().reply(_id, reply);
124+
}
125+
126+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
This file is part of solidity.
3+
4+
solidity is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
solidity is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with solidity. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
// SPDX-License-Identifier: GPL-3.0
18+
#pragma once
19+
#include <libsolidity/lsp/HandlerBase.h>
20+
21+
namespace solidity::lsp
22+
{
23+
24+
class DocumentHoverHandler: public HandlerBase
25+
{
26+
public:
27+
using HandlerBase::HandlerBase;
28+
29+
void operator()(MessageID, Json::Value const&);
30+
};
31+
32+
}

libsolidity/lsp/LanguageServer.cpp

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <libsolidity/lsp/Utils.h>
2626

2727
// LSP feature implementations
28+
#include <libsolidity/lsp/DocumentHoverHandler.h>
2829
#include <libsolidity/lsp/GotoDefinition.h>
2930
#include <libsolidity/lsp/RenameSymbol.h>
3031
#include <libsolidity/lsp/SemanticTokensBuilder.h>
@@ -144,6 +145,7 @@ LanguageServer::LanguageServer(Transport& _transport):
144145
{"textDocument/didOpen", bind(&LanguageServer::handleTextDocumentDidOpen, this, _2)},
145146
{"textDocument/didChange", bind(&LanguageServer::handleTextDocumentDidChange, this, _2)},
146147
{"textDocument/didClose", bind(&LanguageServer::handleTextDocumentDidClose, this, _2)},
148+
{"textDocument/hover", DocumentHoverHandler(*this) },
147149
{"textDocument/rename", RenameSymbol(*this) },
148150
{"textDocument/implementation", GotoDefinition(*this) },
149151
{"textDocument/semanticTokens/full", bind(&LanguageServer::semanticTokensFull, this, _1, _2)},
@@ -417,6 +419,7 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
417419
replyArgs["capabilities"]["semanticTokensProvider"]["range"] = false;
418420
replyArgs["capabilities"]["semanticTokensProvider"]["full"] = true; // XOR requests.full.delta = true
419421
replyArgs["capabilities"]["renameProvider"] = true;
422+
replyArgs["capabilities"]["hoverProvider"] = true;
420423

421424
m_client.reply(_id, std::move(replyArgs));
422425
}
@@ -541,19 +544,21 @@ void LanguageServer::handleTextDocumentDidClose(Json::Value const& _args)
541544
compileAndUpdateDiagnostics();
542545
}
543546

544-
545547
ASTNode const* LanguageServer::astNodeAtSourceLocation(std::string const& _sourceUnitName, LineColumn const& _filePos)
546548
{
547-
if (m_compilerStack.state() < CompilerStack::AnalysisPerformed)
548-
return nullptr;
549+
return get<ASTNode const*>(astNodeAndOffsetAtSourceLocation(_sourceUnitName, _filePos));
550+
}
549551

552+
tuple<ASTNode const*, int> LanguageServer::astNodeAndOffsetAtSourceLocation(std::string const& _sourceUnitName, LineColumn const& _filePos)
553+
{
554+
if (m_compilerStack.state() < CompilerStack::AnalysisPerformed)
555+
return {nullptr, -1};
550556
if (!m_fileRepository.sourceUnits().count(_sourceUnitName))
551-
return nullptr;
557+
return {nullptr, -1};
552558

553-
if (optional<int> sourcePos =
554-
m_compilerStack.charStream(_sourceUnitName).translateLineColumnToPosition(_filePos))
555-
return locateInnermostASTNode(*sourcePos, m_compilerStack.ast(_sourceUnitName));
556-
else
557-
return nullptr;
558-
}
559+
optional<int> sourcePos = m_compilerStack.charStream(_sourceUnitName).translateLineColumnToPosition(_filePos);
560+
if (!sourcePos)
561+
return {nullptr, -1};
559562

563+
return {locateInnermostASTNode(*sourcePos, m_compilerStack.ast(_sourceUnitName)), *sourcePos};
564+
}

libsolidity/lsp/LanguageServer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class LanguageServer
7777

7878
FileRepository& fileRepository() noexcept { return m_fileRepository; }
7979
Transport& client() noexcept { return m_client; }
80+
std::tuple<frontend::ASTNode const*, int> astNodeAndOffsetAtSourceLocation(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos);
8081
frontend::ASTNode const* astNodeAtSourceLocation(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos);
8182
frontend::CompilerStack const& compilerStack() const noexcept { return m_compilerStack; }
8283

test/libsolidity/lsp/hover/hover.sol

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
/// Documenting another contract here.
5+
contract AnotherContract {}
6+
7+
/// User being documented.
8+
contract User
9+
{
10+
/// Some enum value.
11+
enum SomeEnum
12+
{
13+
Red,
14+
Blue
15+
}
16+
17+
/// publicVariable being documented.
18+
SomeEnum public publicVariable;
19+
// ^ @Cursor1
20+
// ^^^^^^^^ @Cursor1Range
21+
22+
// not documented
23+
mapping(int => User.SomeEnum) someRemapping;
24+
// ^ @Cursor2
25+
// ^^^^^^^^ @Cursor2Range
26+
27+
/// Documenting the setContract().
28+
function setValue(User.SomeEnum _value) public
29+
// ^ @Cursor3
30+
// ^^^^ @Cursor3Range
31+
{
32+
publicVariable = _value;
33+
// ^ @Cursor4
34+
// ^^^^^^^^^^^^^^ @Cursor4Range
35+
}
36+
37+
function createAnotherContract() public returns (AnotherContract)
38+
{
39+
return new AnotherContract();
40+
// ^ @Cursor5
41+
// ^^^^^^^^^^^^^^^ @Cursor5Range
42+
}
43+
}
44+
// ----
45+
// -> textDocument/hover {
46+
// "position": @Cursor1
47+
// }
48+
// <- {
49+
// "contents": {
50+
// "kind": "markdown",
51+
// "value": "```solidity\ntype(enum User.SomeEnum)\n```\n\n"
52+
// },
53+
// "range": @Cursor1Range
54+
// }
55+
// -> textDocument/hover {
56+
// "position": @Cursor2
57+
// }
58+
// <- {
59+
// "contents": {
60+
// "kind": "markdown",
61+
// "value": "```solidity\ntype(enum User.SomeEnum)\n```\n\n"
62+
// },
63+
// "range": @Cursor2Range
64+
// }
65+
// -> textDocument/hover {
66+
// "position": @Cursor3
67+
// }
68+
// <- {
69+
// "contents": {
70+
// "kind": "markdown",
71+
// "value": "```solidity\ntype(contract User)\n```\n\nUser being documented.\n\n"
72+
// },
73+
// "range": @Cursor3Range
74+
// }
75+
// -> textDocument/hover {
76+
// "position": @Cursor4
77+
// }
78+
// <- {
79+
// "contents": {
80+
// "kind": "markdown",
81+
// "value": "```solidity\nenum User.SomeEnum\n```\n\n"
82+
// },
83+
// "range": @Cursor4Range
84+
// }
85+
// -> textDocument/hover {
86+
// "position": @Cursor5
87+
// }
88+
// <- {
89+
// "contents": {
90+
// "kind": "markdown",
91+
// "value": "```solidity\ntype(contract AnotherContract)\n```\n\nDocumenting another contract here.\n\n"
92+
// },
93+
// "range": @Cursor5Range
94+
// }

0 commit comments

Comments
 (0)