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 @@ -118,6 +118,7 @@ }}, {"definitionProvider", true}, {"documentHighlightProvider", true}, + {"hoverProvider", true}, {"renameProvider", true}, {"executeCommandProvider", json::obj{ @@ -355,6 +356,19 @@ }); } +void ClangdLSPServer::onHover(TextDocumentPositionParams &Params) { + Server.findHover(Params.textDocument.uri.file(), Params.position, + [](llvm::Expected> H) { + 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 @@ -212,6 +212,10 @@ void(llvm::Expected>>)> Callback); + /// Get code hover for a given position. + void findHover(PathRef File, Position Pos, + UniqueFunction>)> Callback); + /// 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 @@ -491,6 +491,30 @@ WorkScheduler.runWithAST(File, BindWithForward(Action, std::move(Callback))); } +void ClangdServer::findHover( + PathRef File, Position Pos, + UniqueFunction>)> Callback) { + Hover FinalHover; + auto FileContents = DraftMgr.getDraft(File); + if (!FileContents.Draft) + return Callback(llvm::make_error( + "findHover called on non-added file", + llvm::errc::invalid_argument)); + + auto TaggedFS = FSProvider.getTaggedFileSystem(File); + + auto Action = [Pos, TaggedFS](decltype(Callback) Callback, + llvm::Expected InpAST) { + if (!InpAST) + return Callback(InpAST.takeError()); + + Hover Result = clangd::getHover(InpAST->AST, Pos); + Callback(make_tagged(std::move(Result), TaggedFS.Tag)); + }; + + WorkScheduler.runWithAST(File, BindWithForward(Action, std::move(Callback))); +} + void ClangdServer::scheduleReparseAndDiags( PathRef File, VersionedDraft Contents, Tagged> TaggedFS) { Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -480,6 +480,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 @@ -386,6 +386,36 @@ O.map("position", R.position); } +static StringRef toTextKind(MarkupKind Kind) +{ + switch (Kind) { + case MarkupKind::PlainText: + return "plaintext"; + case MarkupKind::Markdown: + return "markdown"; + } + llvm_unreachable("Invalid MarkupKind"); +} + +json::Expr toJSON(const MarkupContent &MC) { + if (MC.Value.empty()) + return nullptr; + + return json::obj{ + {"kind", toTextKind(MC.Kind)}, + {"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" #include "llvm/Support/Path.h" @@ -31,10 +32,15 @@ return nullptr; } +struct MacroDecl { + StringRef Name; + const MacroInfo *Info; +}; + /// Finds declarations locations that a given source location refers to. class DeclarationAndMacrosFinder : public index::IndexDataConsumer { std::vector Decls; - std::vector MacroInfos; + std::vector MacroInfos; const SourceLocation &SearchedLocation; const ASTContext &AST; Preprocessor &PP; @@ -54,10 +60,17 @@ return std::move(Decls); } - std::vector takeMacroInfos() { + std::vector takeMacroInfos() { // Don't keep the same Macro info multiple times. - std::sort(MacroInfos.begin(), MacroInfos.end()); - auto Last = std::unique(MacroInfos.begin(), MacroInfos.end()); + std::sort(MacroInfos.begin(), MacroInfos.end(), + [](const MacroDecl &Left, const MacroDecl &Right) { + return Left.Info < Right.Info; + }); + + auto Last = std::unique(MacroInfos.begin(), MacroInfos.end(), + [](const MacroDecl &Left, const MacroDecl &Right) { + return Left.Info == Right.Info; + }); MacroInfos.erase(Last, MacroInfos.end()); return std::move(MacroInfos); } @@ -111,7 +124,7 @@ PP.getMacroDefinitionAtLoc(IdentifierInfo, BeforeSearchedLocation); MacroInfo *MacroInf = MacroDef.getMacroInfo(); if (MacroInf) { - MacroInfos.push_back(MacroInf); + MacroInfos.push_back(MacroDecl{IdentifierInfo->getName(), MacroInf}); } } } @@ -176,8 +189,7 @@ DeclMacrosFinder, IndexOpts); std::vector Decls = DeclMacrosFinder->takeDecls(); - std::vector MacroInfos = - DeclMacrosFinder->takeMacroInfos(); + std::vector MacroInfos = DeclMacrosFinder->takeMacroInfos(); std::vector Result; for (auto Item : Decls) { @@ -187,7 +199,8 @@ } for (auto Item : MacroInfos) { - SourceRange SR(Item->getDefinitionLoc(), Item->getDefinitionEndLoc()); + SourceRange SR(Item.Info->getDefinitionLoc(), + Item.Info->getDefinitionEndLoc()); auto L = getDeclarationLocation(AST, SR); if (L) Result.push_back(*L); @@ -299,5 +312,138 @@ return DocHighlightsFinder->takeHighlights(); } +static PrintingPolicy PrintingPolicyForDecls(PrintingPolicy Base) { + PrintingPolicy Policy(Base); + + Policy.AnonymousTagLocations = false; + Policy.TerseOutput = true; + Policy.PolishForDeclaration = true; + Policy.ConstantsAsWritten = true; + Policy.SuppressTagKeyword = false; + + return Policy; +} + +/// Return a string representation (e.g. "class MyNamespace::MyClass") of +/// the type declaration \p TD. +static std::string TypeDeclToString(const TypeDecl *TD) { + QualType Type = TD->getASTContext().getTypeDeclType(TD); + + PrintingPolicy Policy = + PrintingPolicyForDecls(TD->getASTContext().getPrintingPolicy()); + + std::string Name; + llvm::raw_string_ostream Stream(Name); + Type.print(Stream, Policy); + + return Stream.str(); +} + +/// Return a string representation (e.g. "namespace ns1::ns2") of +/// the named declaration \p ND. +static std::string NamedDeclQualifiedName(const NamedDecl *ND, + StringRef Prefix) { + PrintingPolicy Policy = + PrintingPolicyForDecls(ND->getASTContext().getPrintingPolicy()); + + std::string Name; + llvm::raw_string_ostream Stream(Name); + Stream << Prefix << ' '; + ND->printQualifiedName(Stream, Policy); + + return Stream.str(); +} + +/// 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(); + + if (const TranslationUnitDecl *TUD = dyn_cast(DC)) + return std::string("global namespace"); + + if (const TypeDecl *TD = dyn_cast(DC)) + return TypeDeclToString(TD); + else if (const NamespaceDecl *ND = dyn_cast(DC)) + return NamedDeclQualifiedName(ND, "namespace"); + else if (const FunctionDecl *FD = dyn_cast(DC)) + return NamedDeclQualifiedName(FD, "function"); + + return llvm::None; +} + +/// Generate a \p Hover object given the declaration \p D. +static Hover getHoverContents(const Decl *D) { + 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"; + } + + // We want to include the template in the Hover. + if (TemplateDecl *TD = D->getDescribedTemplate()) + D = TD; + + std::string DeclText; + llvm::raw_string_ostream OS(DeclText); + + PrintingPolicy Policy = + PrintingPolicyForDecls(D->getASTContext().getPrintingPolicy()); + + D->print(OS, Policy); + + OS.flush(); + + H.Contents.Value += DeclText; + return H; +} + +/// Generate a \p Hover object given the macro \p MacroInf. +static Hover getHoverContents(StringRef MacroName) { + Hover H; + + H.Contents.Value = "#define "; + H.Contents.Value += MacroName; + + 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 Macros = DeclMacrosFinder->takeMacroInfos(); + if (!Macros.empty()) + return getHoverContents(Macros[0].Name); + + std::vector Decls = DeclMacrosFinder->takeDecls(); + if (!Decls.empty()) + return getHoverContents(Decls[0]); + + return Hover(); +} + } // namespace clangd } // namespace clang Index: test/clangd/hover.test =================================================================== --- /dev/null +++ test/clangd/hover.test @@ -0,0 +1,19 @@ +# RUN: clangd -lit-test < %s | FileCheck %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":"file:///main.cpp","languageId":"cpp","version":1,"text":"void foo(); int main() { foo(); }\n"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":0,"character":27}}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "contents": { +# CHECK-NEXT: "kind": "plaintext", +# CHECK-NEXT: "value": "Declared in global namespace\n\nvoid foo()" +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT:} +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: test/clangd/initialize-params-invalid.test =================================================================== --- test/clangd/initialize-params-invalid.test +++ test/clangd/initialize-params-invalid.test @@ -28,6 +28,7 @@ # CHECK-NEXT: "clangd.insertInclude" # 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 @@ -28,6 +28,7 @@ # CHECK-NEXT: "clangd.insertInclude" # 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 @@ -258,6 +258,311 @@ ElementsAre(Location{URIForFile{FooCpp}, SourceAnnotations.range()})); } +TEST(Hover, All) { + struct OneTest { + StringRef Input; + StringRef ExpectedHover; + }; + + OneTest Tests[] = { + { + R"cpp(// Local variable + int main() { + int bonjour; + ^bonjour = 2; + int test1 = bonjour; + } + )cpp", + "Declared in function main\n\nint bonjour", + }, + { + R"cpp(// Local variable in method + struct s { + void method() { + int bonjour; + ^bonjour = 2; + } + }; + )cpp", + "Declared in function s::method\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 {\n}", + }, + { + 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", + }, + { + 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 {\n}", + }, + { + 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 T foo()", + }, + { + R"cpp(// Anonymous union + struct outer { + union { + int abc, def; + } v; + }; + void g() { struct outer o; o.v.d^ef++; } + )cpp", + "Declared in union outer::(anonymous)\n\nint 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