[clangd] Implement "textDocument/documentLink" protocol support

Summary:
This adds an implementation for the "textDocument/documentLink" LSP request.

It returns links for all `#include` directives to the resolved target files.

Fixes https://github.com/clangd/clangd/issues/217.

Reviewers: sammccall

Reviewed By: sammccall

Subscribers: ilya-biryukov, MaskRay, jkorous, arphaman, kadircet, usaxena95, cfe-commits

Tags: #clang

Differential Revision: https://reviews.llvm.org/D70872
This commit is contained in:
Michael Forster 2019-12-12 14:30:02 +01:00 committed by Sam McCall
parent 9c8cfa09d7
commit d6417f5584
11 changed files with 175 additions and 0 deletions

View File

@ -566,6 +566,10 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
{"declarationProvider", true},
{"definitionProvider", true},
{"documentHighlightProvider", true},
{"documentLinkProvider",
llvm::json::Object{
{"resolveProvider", false},
}},
{"hoverProvider", true},
{"renameProvider", std::move(RenameProvider)},
{"selectionRangeProvider", true},
@ -1200,6 +1204,25 @@ void ClangdLSPServer::onSelectionRange(
});
}
void ClangdLSPServer::onDocumentLink(
const DocumentLinkParams &Params,
Callback<std::vector<DocumentLink>> Reply) {
// TODO(forster): This currently resolves all targets eagerly. This is slow,
// because it blocks on the preamble/AST being built. We could respond to the
// request faster by using string matching or the lexer to find the includes
// and resolving the targets lazily.
Server->documentLinks(
Params.textDocument.uri.file(),
[Reply = std::move(Reply)](
llvm::Expected<std::vector<DocumentLink>> Links) mutable {
if (!Links) {
return Reply(Links.takeError());
}
return Reply(std::move(Links));
});
}
ClangdLSPServer::ClangdLSPServer(
class Transport &Transp, const FileSystemProvider &FSProvider,
const clangd::CodeCompleteOptions &CCOpts,
@ -1243,6 +1266,7 @@ ClangdLSPServer::ClangdLSPServer(
MsgHandler->bind("textDocument/typeHierarchy", &ClangdLSPServer::onTypeHierarchy);
MsgHandler->bind("typeHierarchy/resolve", &ClangdLSPServer::onResolveTypeHierarchy);
MsgHandler->bind("textDocument/selectionRange", &ClangdLSPServer::onSelectionRange);
MsgHandler->bind("textDocument/documentLink", &ClangdLSPServer::onDocumentLink);
// clang-format on
}

View File

@ -111,6 +111,8 @@ private:
Callback<std::vector<SymbolDetails>>);
void onSelectionRange(const SelectionRangeParams &,
Callback<std::vector<SelectionRange>>);
void onDocumentLink(const DocumentLinkParams &,
Callback<std::vector<DocumentLink>>);
std::vector<Fix> getFixes(StringRef File, const clangd::Diagnostic &D);

View File

@ -611,6 +611,17 @@ void ClangdServer::semanticRanges(PathRef File, Position Pos,
WorkScheduler.runWithAST("SemanticRanges", File, std::move(Action));
}
void ClangdServer::documentLinks(PathRef File,
Callback<std::vector<DocumentLink>> CB) {
auto Action =
[CB = std::move(CB)](llvm::Expected<InputsAndAST> InpAST) mutable {
if (!InpAST)
return CB(InpAST.takeError());
CB(clangd::getDocumentLinks(InpAST->AST));
};
WorkScheduler.runWithAST("DocumentLinks", File, std::move(Action));
}
std::vector<std::pair<Path, std::size_t>>
ClangdServer::getUsedBytesPerFile() const {
return WorkScheduler.getUsedBytesPerFile();

View File

@ -287,6 +287,9 @@ public:
void semanticRanges(PathRef File, Position Pos,
Callback<std::vector<Range>> CB);
/// Get all document links in a file.
void documentLinks(PathRef File, Callback<std::vector<DocumentLink>> CB);
/// Returns estimated memory usage for each of the currently open files.
/// The order of results is unspecified.
/// Overall memory usage of clangd may be significantly more than reported

View File

@ -1087,5 +1087,18 @@ llvm::json::Value toJSON(const SelectionRange &Out) {
}
return llvm::json::Object{{"range", Out.range}};
}
bool fromJSON(const llvm::json::Value &Params, DocumentLinkParams &R) {
llvm::json::ObjectMapper O(Params);
return O && O.map("textDocument", R.textDocument);
}
llvm::json::Value toJSON(const DocumentLink &DocumentLink) {
return llvm::json::Object{
{"range", DocumentLink.range},
{"target", DocumentLink.target},
};
}
} // namespace clangd
} // namespace clang

View File

@ -1250,6 +1250,39 @@ struct SelectionRange {
};
llvm::json::Value toJSON(const SelectionRange &);
/// Parameters for the document link request.
struct DocumentLinkParams {
/// The document to provide document links for.
TextDocumentIdentifier textDocument;
};
bool fromJSON(const llvm::json::Value &, DocumentLinkParams &);
/// A range in a text document that links to an internal or external resource,
/// like another text document or a web site.
struct DocumentLink {
/// The range this link applies to.
Range range;
/// The uri this link points to. If missing a resolve request is sent later.
URIForFile target;
// TODO(forster): The following optional fields defined by the language
// server protocol are unsupported:
//
// data?: any - A data entry field that is preserved on a document link
// between a DocumentLinkRequest and a
// DocumentLinkResolveRequest.
friend bool operator==(const DocumentLink &LHS, const DocumentLink &RHS) {
return LHS.range == RHS.range && LHS.target == RHS.target;
}
friend bool operator!=(const DocumentLink &LHS, const DocumentLink &RHS) {
return !(LHS == RHS);
}
};
llvm::json::Value toJSON(const DocumentLink &DocumentLink);
} // namespace clangd
} // namespace clang

View File

@ -166,6 +166,26 @@ llvm::Optional<Location> makeLocation(ASTContext &AST, SourceLocation TokLoc,
} // namespace
std::vector<DocumentLink> getDocumentLinks(ParsedAST &AST) {
const auto &SM = AST.getSourceManager();
auto MainFilePath =
getCanonicalPath(SM.getFileEntryForID(SM.getMainFileID()), SM);
if (!MainFilePath) {
elog("Failed to get a path for the main file, so no links");
return {};
}
std::vector<DocumentLink> Result;
for (auto &Inc : AST.getIncludeStructure().MainFileIncludes) {
if (!Inc.Resolved.empty()) {
Result.push_back(DocumentLink(
{Inc.R, URIForFile::canonicalize(Inc.Resolved, *MainFilePath)}));
}
}
return Result;
}
std::vector<LocatedSymbol> locateSymbolAt(ParsedAST &AST, Position Pos,
const SymbolIndex *Index) {
const auto &SM = AST.getSourceManager();

View File

@ -49,6 +49,9 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &, const LocatedSymbol &);
std::vector<LocatedSymbol> locateSymbolAt(ParsedAST &AST, Position Pos,
const SymbolIndex *Index = nullptr);
/// Get all document links
std::vector<DocumentLink> getDocumentLinks(ParsedAST &AST);
/// Returns highlights for all usages of a symbol at \p Pos.
std::vector<DocumentHighlight> findDocumentHighlights(ParsedAST &AST,
Position Pos);

View File

@ -0,0 +1,42 @@
# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}}
---
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"#include <stdint.h>\n#include <stddef.h>"}}}
---
{"jsonrpc":"2.0","id":2,"method":"textDocument/documentLink","params":{"textDocument":{"uri":"test:///main.cpp"}}}
# CHECK: "id": 2,
# CHECK-NEXT: "jsonrpc": "2.0",
# CHECK-NEXT: "result": [
# CHECK-NEXT: {
# CHECK-NEXT: "range": {
# CHECK-NEXT: "end": {
# CHECK-NEXT: "character": 19,
# CHECK-NEXT: "line": 0
# CHECK-NEXT: },
# CHECK-NEXT: "start": {
# CHECK-NEXT: "character": 9,
# CHECK-NEXT: "line": 0
# CHECK-NEXT: }
# CHECK-NEXT: },
# CHECK-NEXT: "target": "file://{{.*}}/stdint.h"
# CHECK-NEXT: },
# CHECK-NEXT: {
# CHECK-NEXT: "range": {
# CHECK-NEXT: "end": {
# CHECK-NEXT: "character": 19,
# CHECK-NEXT: "line": 1
# CHECK-NEXT: },
# CHECK-NEXT: "start": {
# CHECK-NEXT: "character": 9,
# CHECK-NEXT: "line": 1
# CHECK-NEXT: }
# CHECK-NEXT: },
# CHECK-NEXT: "target": "file://{{.*}}/stddef.h"
# CHECK-NEXT: }
# CHECK-NEXT: ]
# CHECK-NEXT:}
---
{"jsonrpc":"2.0","id":3,"method":"shutdown"}
---
{"jsonrpc":"2.0","method":"exit"}

View File

@ -18,6 +18,9 @@
# CHECK-NEXT: "definitionProvider": true,
# CHECK-NEXT: "documentFormattingProvider": true,
# CHECK-NEXT: "documentHighlightProvider": true,
# CHECK-NEXT: "documentLinkProvider": {
# CHECK-NEXT: "resolveProvider": false
# CHECK-NEXT: }
# CHECK-NEXT: "documentOnTypeFormattingProvider": {
# CHECK-NEXT: "firstTriggerCharacter": "\n",
# CHECK-NEXT: "moreTriggerCharacter": []

View File

@ -1048,6 +1048,27 @@ TEST(GetNonLocalDeclRefs, All) {
}
}
TEST(DocumentLinks, All) {
Annotations MainCpp(R"cpp(
#include $foo[["foo.h"]]
int end_of_preamble = 0;
#include $bar[["bar.h"]]
)cpp");
TestTU TU;
TU.Code = MainCpp.code();
TU.AdditionalFiles = {{"foo.h", ""}, {"bar.h", ""}};
auto AST = TU.build();
EXPECT_THAT(
clangd::getDocumentLinks(AST),
ElementsAre(
DocumentLink({MainCpp.range("foo"),
URIForFile::canonicalize(testPath("foo.h"), "")}),
DocumentLink({MainCpp.range("bar"),
URIForFile::canonicalize(testPath("bar.h"), "")})));
}
} // namespace
} // namespace clangd
} // namespace clang