Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -75,6 +75,7 @@ void onFileEvent(DidChangeWatchedFilesParams &Params) override; void onCommand(ExecuteCommandParams &Params) override; void onRename(RenameParams &Parames) override; + void onHover(TextDocumentPositionParams &Params) override; std::vector getFixIts(StringRef File, const clangd::Diagnostic &D); Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -110,6 +110,7 @@ }}, {"definitionProvider", true}, {"documentHighlightProvider", true}, + {"hoverProvider", true}, {"renameProvider", true}, {"executeCommandProvider", json::obj{ @@ -320,6 +321,19 @@ reply(json::ary(Highlights->Value)); } +void ClangdLSPServer::onHover(TextDocumentPositionParams &Params) { + + llvm::Expected> H = + Server.findHover(Params.textDocument.uri.file, Params.position); + + if (!H) { + replyError(ErrorCode::InternalError, llvm::toString(H.takeError())); + return; + } + + reply(H->Value); +} + ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, unsigned AsyncThreadsCount, bool StorePreamblesInMemory, const clangd::CodeCompleteOptions &CCOpts, Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -223,6 +223,9 @@ llvm::Expected>> findDocumentHighlights(PathRef File, Position Pos); + /// Get code hover for a given position. + llvm::Expected> findHover(PathRef File, Position Pos); + /// Run formatting for \p Rng inside \p File with content \p Code. llvm::Expected formatRange(StringRef Code, PathRef File, Range Rng); Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -494,6 +494,28 @@ return blockingRunWithAST(WorkScheduler, File, Action); } +llvm::Expected> ClangdServer::findHover(PathRef File, + Position Pos) { + Hover FinalHover; + auto FileContents = DraftMgr.getDraft(File); + if (!FileContents.Draft) + return llvm::make_error( + "findDocumentHighlights called on non-added file", + llvm::errc::invalid_argument); + + auto TaggedFS = FSProvider.getTaggedFileSystem(File); + + using RetType = llvm::Expected>; + auto Action = [=](llvm::Expected InpAST) -> RetType { + if (!InpAST) + return InpAST.takeError(); + + auto Result = clangd::getHover(InpAST->AST, Pos); + return make_tagged(std::move(Result), TaggedFS.Tag); + }; + return blockingRunWithAST(WorkScheduler, File, Action); +} + std::future ClangdServer::scheduleReparseAndDiags( PathRef File, VersionedDraft Contents, Tagged> TaggedFS) { Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -407,6 +407,27 @@ }; bool fromJSON(const json::Expr &, TextDocumentPositionParams &); +enum class MarkupKind { + PlainText, + Markdown, +}; + +struct MarkupContent { + MarkupKind kind = MarkupKind::PlainText; + std::string value; +}; +json::Expr toJSON(const MarkupContent &MC); + +struct Hover { + /// The hover's content + MarkupContent contents; + + /// An optional range is a range inside a text document + /// that is used to visualize a hover, e.g. by changing the background color. + llvm::Optional range; +}; +json::Expr toJSON(const Hover &H); + /// The kind of a completion entry. enum class CompletionItemKind { Missing = 0, Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -320,6 +320,38 @@ O.map("position", R.position); } +json::Expr toJSON(const MarkupContent &MC) { + const char *KindStr = NULL; + + if (MC.value.empty()) + return nullptr; + + assert(MC.kind == MarkupKind::PlainText || MC.kind == MarkupKind::Markdown); + + switch (MC.kind) { + case MarkupKind::PlainText: + KindStr = "plaintext"; + break; + case MarkupKind::Markdown: + KindStr = "markdown"; + break; + } + + return json::obj{ + {"kind", KindStr}, + {"value", MC.value}, + }; +} + +json::Expr toJSON(const Hover &H) { + json::obj Result{{"contents", toJSON(H.contents)}}; + + if (H.range.hasValue()) + Result["range"] = toJSON(*H.range); + + return Result; +} + json::Expr toJSON(const CompletionItem &CI) { assert(!CI.label.empty() && "completion item label is required"); json::obj Result{{"label", CI.label}}; Index: clangd/ProtocolHandlers.h =================================================================== --- clangd/ProtocolHandlers.h +++ clangd/ProtocolHandlers.h @@ -51,6 +51,7 @@ virtual void onCommand(ExecuteCommandParams &Params) = 0; virtual void onRename(RenameParams &Parames) = 0; virtual void onDocumentHighlight(TextDocumentPositionParams &Params) = 0; + virtual void onHover(TextDocumentPositionParams &Params) = 0; }; void registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, JSONOutput &Out, Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -67,6 +67,7 @@ Register("textDocument/switchSourceHeader", &ProtocolCallbacks::onSwitchSourceHeader); Register("textDocument/rename", &ProtocolCallbacks::onRename); + Register("textDocument/hover", &ProtocolCallbacks::onHover); Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent); Register("workspace/executeCommand", &ProtocolCallbacks::onCommand); Register("textDocument/documentHighlight", Index: clangd/XRefs.h =================================================================== --- clangd/XRefs.h +++ clangd/XRefs.h @@ -27,6 +27,9 @@ std::vector findDocumentHighlights(ParsedAST &AST, Position Pos); +/// Get the hover information when hovering at \p Pos. +Hover getHover(ParsedAST &AST, Position Pos); + } // namespace clangd } // namespace clang #endif Index: clangd/XRefs.cpp =================================================================== --- clangd/XRefs.cpp +++ clangd/XRefs.cpp @@ -9,6 +9,7 @@ #include "XRefs.h" #include "Logger.h" #include "URI.h" +#include "clang/AST/DeclTemplate.h" #include "clang/Index/IndexDataConsumer.h" #include "clang/Index/IndexingAction.h" namespace clang { @@ -291,5 +292,277 @@ return DocHighlightsFinder->takeHighlights(); } +/// Returns the file buffer data that the given Location \p L points to. +static Optional getStringFromSourceRange(const ParsedAST &AST, + Location L) { + const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); + const FileEntry *FE = SourceMgr.getFileManager().getFile(L.uri.file); + + if (FE == nullptr) + return {}; + + FileID FID = SourceMgr.translateFile(FE); + StringRef TextRef = SourceMgr.getBufferData(FID); + + SourceLocation StartLoc = SourceMgr.translateFileLineCol( + FE, L.range.start.line + 1, L.range.start.character + 1); + SourceLocation EndLoc = SourceMgr.translateFileLineCol( + FE, L.range.end.line + 1, L.range.end.character + 1); + + unsigned Start = SourceMgr.getFileOffset(StartLoc); + unsigned End = SourceMgr.getFileOffset(EndLoc); + + return TextRef.slice(Start, End); +} + +/// Return a string representation (e.g. "class MyNamespace::MyClass") of +/// the named declaration \p ND. +static std::string NamedDeclToString(const NamedDecl *ND) { + std::string Name; + llvm::raw_string_ostream Stream(Name); + const char *keyword = nullptr; + + if (const TagDecl *TD = dyn_cast(ND)) { + switch (TD->getTagKind()) { + case TTK_Class: + keyword = "class"; + break; + case TTK_Enum: + keyword = "enum"; + break; + case TTK_Struct: + keyword = "struct"; + break; + case TTK_Union: + keyword = "union"; + break; + case TTK_Interface: + keyword = "__interface"; + break; + } + } else if (const NamespaceDecl *NSD = dyn_cast(ND)) + keyword = "namespace"; + + if (keyword != nullptr) + Stream << keyword << ' '; + + ND->printQualifiedName(Stream); + return Name; +} + +/// Given a declaration \p D, return a human-readable string representing the +/// scope in which it is declared. If the declaration is in the global scope, +/// return the string "global namespace". +static llvm::Optional getScopeName(const Decl *D) { + const DeclContext *DC = D->getDeclContext(); + + const TranslationUnitDecl *TUD = dyn_cast(DC); + if (TUD != nullptr) + return std::string("global namespace"); + + const NamedDecl *ND = dyn_cast(DC); + if (ND != nullptr) + return NamedDeclToString(ND); + + return {}; +} + +/// Find the indentation present before the start of the declaration at +/// \p DeclLocation and remove it from subsequent lines in \p DeclText. +static void tryFixupIndentation(std::string *DeclText, + const Location &DeclLocation, + const ParsedAST &AST) { + Location IndentationLoc = DeclLocation; + + IndentationLoc.range.end = IndentationLoc.range.start; + IndentationLoc.range.start.character = 0; + + Optional Indentation = + getStringFromSourceRange(AST, IndentationLoc); + + if (!Indentation) + return; + + // Be conservative, bail out if the declaration is preceded by anything else + // than tabs and spaces. + for (unsigned char c : *Indentation) { + if (c != ' ' && c != '\t') + return; + } + + std::string Needle = "\n"; + Needle += *Indentation; + + std::string::size_type pos = DeclText->find(Needle); + while (pos != std::string::npos) { + // Remove the indentation (but don't remove the \n!). + DeclText->erase(pos + 1, Needle.length() - 1); + pos = DeclText->find(Needle, pos + 1); + } +} + +/// Strip whitespaces and the opening bracket from the end of \p DeclText. For +/// example, this: +/// +/// int foo(int x) { +/// +/// will become +/// +/// int foo(int x) +/// +static void stripOpeningBracket(std::string *DeclText) { + while (!DeclText->empty() && + (DeclText->back() == ' ' || DeclText->back() == '\t' || + DeclText->back() == '\n' || DeclText->back() == '{')) + DeclText->pop_back(); +} + +/// Generate a \p Hover object given the declaration \p D. +static Hover getHoverContents(const Decl *D, ParsedAST &AST) { + Hover H; + llvm::Optional NamedScope = getScopeName(D); + + // Generate the "Declared in" section. + if (NamedScope) { + assert(!NamedScope->empty()); + + H.contents.value += "Declared in "; + H.contents.value += *NamedScope; + H.contents.value += "\n\n"; + } + + SourceRange SR = D->getSourceRange(); + + // We want to include the template in the Hover. + TemplateDecl *TD = D->getDescribedTemplate(); + if (TD != nullptr) + SR.setBegin(TD->getSourceRange().getBegin()); + + // If this is a function, class, namespace, etc., we don't want to include the + // body. Adjust the end of the source range so it doesn't include it. + if (const FunctionDecl *FD = dyn_cast(D)) { + if (FD->isThisDeclarationADefinition() && FD->getBody() != NULL) + SR.setEnd(FD->getBody()->getLocStart()); + } else if (const RecordDecl *RD = dyn_cast(D)) { + if (RD->isThisDeclarationADefinition()) + SR.setEnd(RD->getBraceRange().getBegin()); + } else if (const EnumDecl *ED = dyn_cast(D)) { + // C++ doesn't allow enum declarations that are not definitions, so this + // should always be true. + assert(ED->isThisDeclarationADefinition()); + SR.setEnd(ED->getBraceRange().getBegin()); + } else if (const NamespaceDecl *ND = dyn_cast(D)) { + // The AST doesn't seem the contain the location of the opening bracket as + // for the other Decl types. Work around this by building the string + // "namespace foo" by hand. + H.contents.value += "namespace "; + H.contents.value += ND->getName(); + return H; + } + + if (!SR.isValid()) + return Hover(); + + llvm::Optional L = getDeclarationLocation(AST, SR); + if (!L.hasValue()) + return Hover(); + + Optional TextRef = getStringFromSourceRange(AST, *L); + if (!TextRef) + return Hover(); + + std::string DeclText = *TextRef; + + // If the declaration spans multiple lines and is indented, the first line + // won't contain the indentation but the subsequent ones will. A declaration + // indented like this: + // + // template + // void foo (int x, + // int y) + // + // will give a result like this: + // + // template + // void foo (int x, + // int y) + // + // Try to fix it by removing the indentation found before the declaration + // from the subsequent lines. The result will look like: + // + // template + // void foo (int x, + // int y) + // + tryFixupIndentation(&DeclText, *L, AST); + + // If the referenced source text contains an opening bracket, strip it. + stripOpeningBracket(&DeclText); + + H.contents.value += DeclText; + + return H; +} + +/// Generate a \p Hover object given the macro \p MacroInf. +static Hover getHoverContents(const MacroInfo *MacroInf, ParsedAST &AST) { + SourceLocation begin = MacroInf->getDefinitionLoc(); + SourceLocation end = MacroInf->getDefinitionEndLoc(); + + if (!begin.isValid() || !end.isValid()) + return Hover(); + + llvm::Optional L = + getDeclarationLocation(AST, SourceRange(begin, end)); + if (!L.hasValue()) + return Hover(); + + Optional TextRef = getStringFromSourceRange(AST, *L); + if (!TextRef) + return Hover(); + + Hover H; + H.contents.value = "#define "; + H.contents.value += *TextRef; + + return H; +} + +Hover getHover(ParsedAST &AST, Position Pos) { + const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); + const FileEntry *FE = SourceMgr.getFileEntryForID(SourceMgr.getMainFileID()); + if (FE == nullptr) + return Hover(); + + SourceLocation SourceLocationBeg = getBeginningOfIdentifier(AST, Pos, FE); + auto DeclMacrosFinder = std::make_shared( + llvm::errs(), SourceLocationBeg, AST.getASTContext(), + AST.getPreprocessor()); + + index::IndexingOptions IndexOpts; + IndexOpts.SystemSymbolFilter = + index::IndexingOptions::SystemSymbolFilterKind::All; + IndexOpts.IndexFunctionLocals = true; + + indexTopLevelDecls(AST.getASTContext(), AST.getTopLevelDecls(), + DeclMacrosFinder, IndexOpts); + + std::vector Decls = DeclMacrosFinder->takeDecls(); + if (!Decls.empty()) { + assert(Decls[0] != nullptr); + if (Decls[0] != nullptr) + return getHoverContents(Decls[0], AST); + } + + std::vector Macros = DeclMacrosFinder->takeMacroInfos(); + if (!Macros.empty()) { + assert(Macros[0] != nullptr); + if (Macros[0] != nullptr) + return getHoverContents(Macros[0], AST); + } + + return Hover(); +} + } // namespace clangd } // namespace clang Index: test/clangd/hover.test =================================================================== --- /dev/null +++ test/clangd/hover.test @@ -0,0 +1,19 @@ +# RUN: clangd -run-synchronously < %s | FileCheck %s +# It is absolutely vital that this file has CRLF line endings. +# +Content-Length: 127 + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +Content-Length: 180 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///main.cpp","languageId":"cpp","version":1,"text":"void foo(); int main() { foo(); }\n"}}} +Content-Length: 146 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":0,"character":27}}} +# CHECK: {"id":1,"jsonrpc":"2.0","result":{"contents":{"kind":"plaintext","value":"Declared in global namespace\n\nvoid foo()"}}} +Content-Length: 46 + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +Content-Length: 35 + +{"jsonrpc":"2.0","method":"exit"} Index: test/clangd/initialize-params-invalid.test =================================================================== --- test/clangd/initialize-params-invalid.test +++ test/clangd/initialize-params-invalid.test @@ -27,6 +27,7 @@ # CHECK-NEXT: "clangd.applyFix" # CHECK-NEXT: ] # CHECK-NEXT: }, +# CHECK-NEXT: "hoverProvider": true, # CHECK-NEXT: "renameProvider": true, # CHECK-NEXT: "signatureHelpProvider": { # CHECK-NEXT: "triggerCharacters": [ Index: test/clangd/initialize-params.test =================================================================== --- test/clangd/initialize-params.test +++ test/clangd/initialize-params.test @@ -27,6 +27,7 @@ # CHECK-NEXT: "clangd.applyFix" # CHECK-NEXT: ] # CHECK-NEXT: }, +# CHECK-NEXT: "hoverProvider": true, # CHECK-NEXT: "renameProvider": true, # CHECK-NEXT: "signatureHelpProvider": { # CHECK-NEXT: "triggerCharacters": [ Index: unittests/clangd/XRefsTests.cpp =================================================================== --- unittests/clangd/XRefsTests.cpp +++ unittests/clangd/XRefsTests.cpp @@ -227,6 +227,316 @@ } } +TEST(Hover, All) { + + struct OneTest { + const char *input; + const char *expectedHover; + }; + + OneTest Tests[] = { + { + R"cpp(// Local variable + int main() { + int bonjour; + ^bonjour = 2; + int test1 = bonjour; + } + )cpp", + "Declared in main\n\nint bonjour", + }, + { + R"cpp(// Struct + namespace ns1 { + struct MyClass {}; + } // namespace ns1 + int main() { + ns1::My^Class* Params; + } + )cpp", + "Declared in namespace ns1\n\nstruct MyClass", + }, + { + R"cpp(// Class + namespace ns1 { + class MyClass {}; + } // namespace ns1 + int main() { + ns1::My^Class* Params; + } + )cpp", + "Declared in namespace ns1\n\nclass MyClass", + }, + { + R"cpp(// Union + namespace ns1 { + union MyUnion { int x; int y; }; + } // namespace ns1 + int main() { + ns1::My^Union Params; + } + )cpp", + "Declared in namespace ns1\n\nunion MyUnion", + }, + { + R"cpp(// Function definition via pointer + int foo(int) {} + int main() { + auto *X = &^foo; + } + )cpp", + "Declared in global namespace\n\nint foo(int)", + }, + { + R"cpp(// Function declaration via call + int foo(int); + int main() { + return ^foo(42); + } + )cpp", + "Declared in global namespace\n\nint foo(int)", + }, + { + R"cpp(// Field + struct Foo { int x; }; + int main() { + Foo bar; + bar.^x; + } + )cpp", + "Declared in struct Foo\n\nint x", + }, + { + R"cpp(// Field with initialization + struct Foo { int x = 5; }; + int main() { + Foo bar; + bar.^x; + } + )cpp", + "Declared in struct Foo\n\nint x = 5", + }, + { + R"cpp(// Static field + struct Foo { static int x; }; + int main() { + Foo::^x; + } + )cpp", + "Declared in struct Foo\n\nstatic int x", + }, + { + R"cpp(// Field, member initializer + struct Foo { + int x; + Foo() : ^x(0) {} + }; + )cpp", + "Declared in struct Foo\n\nint x", + }, + { + R"cpp(// Field, GNU old-style field designator + struct Foo { int x; }; + int main() { + Foo bar = { ^x : 1 }; + } + )cpp", + "Declared in struct Foo\n\nint x", + }, + { + R"cpp(// Field, field designator + struct Foo { int x; }; + int main() { + Foo bar = { .^x = 2 }; + } + )cpp", + "Declared in struct Foo\n\nint x", + }, + { + R"cpp(// Method call + struct Foo { int x(); }; + int main() { + Foo bar; + bar.^x(); + } + )cpp", + "Declared in struct Foo\n\nint x()", + }, + { + R"cpp(// Static method call + struct Foo { static int x(); }; + int main() { + Foo::^x(); + } + )cpp", + "Declared in struct Foo\n\nstatic int x()", + }, + { + R"cpp(// Typedef + typedef int Foo; + int main() { + ^Foo bar; + } + )cpp", + "Declared in global namespace\n\ntypedef int Foo", + }, + { + R"cpp(// Namespace + namespace ns { + struct Foo { static void bar(); } + } // namespace ns + int main() { ^ns::Foo::bar(); } + )cpp", + "Declared in global namespace\n\nnamespace ns", + }, + { + R"cpp(// Anonymous namespace + namespace ns { + namespace { + int foo; + } // anonymous namespace + } // namespace ns + int main() { ns::f^oo++; } + )cpp", + "Declared in namespace ns::(anonymous)\n\nint foo", + }, + { + R"cpp(// Macro + #define MACRO 0 + #define MACRO 1 + int main() { return ^MACRO; } + #define MACRO 2 + #undef macro + )cpp", + "#define MACRO 1", + }, + { + R"cpp(// Forward class declaration + class Foo; + class Foo {}; + F^oo* foo(); + )cpp", + "Declared in global namespace\n\nclass Foo", + }, + { + R"cpp(// Function declaration + void foo(); + void g() { f^oo(); } + void foo() {} + )cpp", + "Declared in global namespace\n\nvoid foo()", + }, + { + R"cpp(// Enum declaration + enum Hello { + ONE, TWO, THREE, + }; + void foo() { + Hel^lo hello = ONE; + } + )cpp", + "Declared in global namespace\n\nenum Hello", + }, + { + R"cpp(// Enumerator + enum Hello { + ONE, TWO, THREE, + }; + void foo() { + Hello hello = O^NE; + } + )cpp", + "Declared in enum Hello\n\nONE", + }, + { + R"cpp(// Enumerator in anonymous enum + enum { + ONE, TWO, THREE, + }; + void foo() { + int hello = O^NE; + } + )cpp", + "Declared in enum (anonymous)\n\nONE", + }, + { + R"cpp(// Global variable + static int hey = 10; + void foo() { + he^y++; + } + )cpp", + "Declared in global namespace\n\nstatic int hey = 10", + }, + { + R"cpp(// Global variable in namespace + namespace ns1 { + static int hey = 10; + } + void foo() { + ns1::he^y++; + } + )cpp", + "Declared in namespace ns1\n\nstatic int hey = 10", + }, + { + R"cpp(// Field in anonymous struct + static struct { + int hello; + } s; + void foo() { + s.he^llo++; + } + )cpp", + "Declared in struct (anonymous)\n\nint hello", + }, + { + R"cpp(// Templated function + template + T foo() { + return 17; + } + void g() { auto x = f^oo(); } + )cpp", + "Declared in global namespace\n\ntemplate \nT foo()", + }, + { + R"cpp(// Alternative indentation + namespace { + template + T + foo() + { + return 17; + } + } // anonymous namespace + void g() { auto x = f^oo(); } + )cpp", + "Declared in namespace (anonymous)\n\ntemplate \nT\nfoo()", + }, + { + R"cpp(// Alternative indentation + struct outer { + union { + int abc, def; + } v; + }; + void g() { struct outer o; o.v.d^ef++; } + )cpp", + "Declared in union outer::(anonymous)\n\nint abc, def", + }, + }; + + for (const OneTest &Test : Tests) { + Annotations T(Test.input); + auto AST = build(T.code()); + Hover H = getHover(AST, T.point()); + + EXPECT_EQ(H.contents.value, Test.expectedHover) << Test.input; + } +} + } // namespace } // namespace clangd } // namespace clang