Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -69,6 +69,7 @@ void onGoToDefinition(Ctx C, TextDocumentPositionParams &Params) override; void onSwitchSourceHeader(Ctx C, TextDocumentIdentifier &Params) override; void onFileEvent(Ctx C, DidChangeWatchedFilesParams &Params) override; + void onCodeHover(Ctx C, TextDocumentPositionParams &Params) override; std::vector getFixIts(StringRef File, const clangd::Diagnostic &D); Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -47,7 +47,8 @@ "codeActionProvider": true, "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, "signatureHelpProvider": {"triggerCharacters": ["(",","]}, - "definitionProvider": true + "definitionProvider": true, + "hoverProvider": true }})"); if (Params.rootUri && !Params.rootUri->file.empty()) Server.setRootPath(Params.rootUri->file); @@ -178,7 +179,7 @@ std::string Locations; for (const auto &Item : Items->Value) { - Locations += Location::unparse(Item); + Locations += Location::unparse(Item.first); Locations += ","; } if (!Locations.empty()) @@ -193,6 +194,22 @@ C.reply(Result ? URI::unparse(URI::fromFile(*Result)) : R"("")"); } +void ClangdLSPServer::onCodeHover(Ctx C, TextDocumentPositionParams &Params) { + + Hover H = + Server + .findHover(Params.textDocument.uri.file, + Position{Params.position.line, Params.position.character}) + .Value; + + if (!(H.contents[0].codeBlockLanguage == "" && + H.contents[0].markdownString == "" && + H.contents[0].codeBlockValue == "")) + C.reply(Hover::unparse(H)); + else + C.reply("[]"); +} + ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, unsigned AsyncThreadsCount, bool SnippetCompletions, llvm::Optional ResourceDir, Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -62,8 +62,9 @@ Tagged(const Tagged &Other) : Value(Other.Value), Tag(Other.Tag) {} template - Tagged(Tagged &&Other) - : Value(std::move(Other.Value)), Tag(std::move(Other.Tag)) {} + Tagged(Tagged &&Other) : Value(std::move(Other.Value)), Tag(std::move(Other.Tag)) {} + + // template T Value = T(); VFSTag Tag = VFSTag(); @@ -277,13 +278,16 @@ IntrusiveRefCntPtr *UsedFS = nullptr); /// Get definition of symbol at a specified \p Line and \p Column in \p File. - llvm::Expected>> findDefinitions(PathRef File, + llvm::Expected>>> findDefinitions(PathRef File, Position Pos); + /// Helper function that returns a path to the corresponding source file when /// given a header file and vice versa. llvm::Optional switchSourceHeader(PathRef Path); + Tagged findHover(PathRef File, Position Pos); + /// Run formatting for \p Rng inside \p File. std::vector formatRange(PathRef File, Range Rng); /// Run formatting for the whole \p File. Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -8,6 +8,7 @@ //===-------------------------------------------------------------------===// #include "ClangdServer.h" +#include "Protocol.h" #include "clang/Format/Format.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInvocation.h" @@ -353,8 +354,14 @@ return Result; } -llvm::Expected>> +llvm::Expected>>> ClangdServer::findDefinitions(PathRef File, Position Pos) { + + auto FileContents = DraftMgr.getDraft(File); + assert(FileContents.Draft && + "findDefinitions is called for non-added document"); + + auto TaggedFS = FSProvider.getTaggedFileSystem(File); std::shared_ptr Resources = Units.getFile(File); @@ -363,7 +370,7 @@ "findDefinitions called on non-added file", llvm::errc::invalid_argument); - std::vector Result; + std::vector> Result; Resources->getAST().get()->runUnderLock([Pos, &Result, this](ParsedAST *AST) { if (!AST) return; @@ -431,6 +438,36 @@ return llvm::None; } +Tagged ClangdServer::findHover(PathRef File, Position Pos) { + auto FileContents = DraftMgr.getDraft(File); + assert(FileContents.Draft && "findHover is called for non-added document"); + + std::vector Contents; + MarkedString MS = MarkedString("", ""); + Contents.push_back(MS); + Range R; + // Hover FinalHover(MS, R); + Hover FinalHover(Contents, R); + auto TaggedFS = FSProvider.getTaggedFileSystem(File); + + std::shared_ptr Resources = Units.getFile(File); + assert(Resources && "Calling findDefinitions on non-added file"); + + std::vector> Result; + + Resources->getAST().get()->runUnderLock( + [Pos, &Result, &FinalHover, this](ParsedAST *AST) { + if (!AST) + return; + Result = clangd::findDefinitions(*AST, Pos, Logger); + if (!Result.empty()) { + FinalHover = clangd::getHover(*AST, Result[0]); + } + }); + + return make_tagged(std::move(FinalHover), TaggedFS.Tag); +} + std::future ClangdServer::scheduleReparseAndDiags( PathRef File, VersionedDraft Contents, std::shared_ptr Resources, Tagged> TaggedFS) { Index: clangd/ClangdUnit.h =================================================================== --- clangd/ClangdUnit.h +++ clangd/ClangdUnit.h @@ -305,8 +305,10 @@ clangd::Logger &Logger); /// Get definition of symbol at a specified \p Pos. -std::vector findDefinitions(ParsedAST &AST, Position Pos, - clangd::Logger &Logger); +std::vector> +findDefinitions(ParsedAST &AST, Position Pos, clangd::Logger &Logger); + +Hover getHover(ParsedAST &AST, std::pair LocationDecl); /// For testing/debugging purposes. Note that this method deserializes all /// unserialized Decls, so use with care. Index: clangd/ClangdUnit.cpp =================================================================== --- clangd/ClangdUnit.cpp +++ clangd/ClangdUnit.cpp @@ -911,7 +911,7 @@ /// Finds declarations locations that a given source location refers to. class DeclarationLocationsFinder : public index::IndexDataConsumer { - std::vector DeclarationLocations; + std::vector> DeclarationLocations; const SourceLocation &SearchedLocation; const ASTContext &AST; Preprocessor &PP; @@ -922,7 +922,7 @@ ASTContext &AST, Preprocessor &PP) : SearchedLocation(SearchedLocation), AST(AST), PP(PP) {} - std::vector takeLocations() { + std::vector> takeLocations() { // Don't keep the same location multiple times. // This can happen when nodes in the AST are visited twice. std::sort(DeclarationLocations.begin(), DeclarationLocations.end()); @@ -937,20 +937,65 @@ ArrayRef Relations, FileID FID, unsigned Offset, index::IndexDataConsumer::ASTNodeInfo ASTNode) override { + + // Keep default value. + SourceRange SR = D->getSourceRange(); + + if (auto FuncDecl = dyn_cast(D)) { + if (FuncDecl->isThisDeclarationADefinition()) + { + if (FuncDecl->hasBody()) + { + SR = SourceRange(D->getSourceRange().getBegin(), + FuncDecl->getBody()->getLocStart()); + } + } + } else if (auto ClassDecl = dyn_cast(D)) { + if (ClassDecl->isStruct()) { + SR = SourceRange(D->getSourceRange().getBegin(), + ClassDecl->getBraceRange().getBegin()); + } else if (ClassDecl->isClass()) { + if (ClassDecl->isThisDeclarationADefinition()) + SR = SourceRange(D->getSourceRange().getBegin(), + ClassDecl->getBraceRange().getBegin()); + } else if (ClassDecl->isEnum()) { + if (ClassDecl->isThisDeclarationADefinition()) + SR = SourceRange(D->getSourceRange().getBegin(), + ClassDecl->getBraceRange().getBegin()); + } + } else if (auto NameDecl = dyn_cast(D)) { + SourceLocation BeforeLBraceLoc = Lexer::getLocForEndOfToken( + D->getLocation(), 0, AST.getSourceManager(), AST.getLangOpts()); + if (BeforeLBraceLoc.isValid()) + SR = SourceRange(NameDecl->getLocStart(), BeforeLBraceLoc); + else + SR = D->getSourceRange(); + } else if (dyn_cast(D)) { + SR = D->getSourceRange(); + } + // for everything else in ValueDecl, so lvalues of variables, function + // designations and enum constants + else if (dyn_cast(D)) { + SR = D->getSourceRange(); + } + if (isSearchedLocation(FID, Offset)) { - addDeclarationLocation(D->getSourceRange()); + DeclarationLocations.push_back( + {getDeclarationLocation(D->getSourceRange()), D}); + } return true; } private: + bool isSearchedLocation(FileID FID, unsigned Offset) const { const SourceManager &SourceMgr = AST.getSourceManager(); return SourceMgr.getFileOffset(SearchedLocation) == Offset && SourceMgr.getFileID(SearchedLocation) == FID; } - void addDeclarationLocation(const SourceRange &ValSourceRange) { + Location getDeclarationLocation(const SourceRange &ValSourceRange) { const SourceManager &SourceMgr = AST.getSourceManager(); const LangOptions &LangOpts = AST.getLangOpts(); SourceLocation LocStart = ValSourceRange.getBegin(); @@ -967,7 +1012,7 @@ L.uri = URI::fromFile( SourceMgr.getFilename(SourceMgr.getSpellingLoc(LocStart))); L.range = R; - DeclarationLocations.push_back(L); + return L; } void finish() override { @@ -992,8 +1037,10 @@ PP.getMacroDefinitionAtLoc(IdentifierInfo, BeforeSearchedLocation); MacroInfo *MacroInf = MacroDef.getMacroInfo(); if (MacroInf) { - addDeclarationLocation(SourceRange(MacroInf->getDefinitionLoc(), - MacroInf->getDefinitionEndLoc())); + DeclarationLocations.push_back({getDeclarationLocation(SourceRange( + MacroInf->getDefinitionLoc(), + MacroInf->getDefinitionEndLoc())), + nullptr}); } } } @@ -1040,8 +1087,8 @@ } } // namespace -std::vector clangd::findDefinitions(ParsedAST &AST, Position Pos, - clangd::Logger &Logger) { +std::vector> +clangd::findDefinitions(ParsedAST &AST, Position Pos, clangd::Logger &Logger) { const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); const FileEntry *FE = SourceMgr.getFileEntryForID(SourceMgr.getMainFileID()); if (!FE) @@ -1063,6 +1110,116 @@ return DeclLocationsFinder->takeLocations(); } +Hover clangd::getHover(ParsedAST &AST, + std::pair LocationDecl) { + Location L = LocationDecl.first; + std::vector Contents; + unsigned Start; + unsigned End; + + const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); + Range R; + const FileEntry *FE = SourceMgr.getFileManager().getFile(L.uri.file); + FileID id = SourceMgr.translateFile(FE); + StringRef Ref = SourceMgr.getBufferData(id); + Start = SourceMgr.getFileOffset(SourceMgr.translateFileLineCol( + FE, L.range.start.line + 1, L.range.start.character + 1)); + End = SourceMgr.getFileOffset(SourceMgr.translateFileLineCol( + FE, L.range.end.line + 1, L.range.end.character + 1)); + Ref = Ref.slice(Start, End); + + if (LocationDecl.second) { + if (const NamedDecl *NamedD = dyn_cast(LocationDecl.second)) { + // Get the full qualified name, the non-qualified name and then diff them. + // If there's something left, use that as the scope in the hover, trimming + // the extra "::" + PrintingPolicy WithScopePP(AST.getASTContext().getLangOpts()); + std::string WithScopeBuf; + llvm::raw_string_ostream WithScopeOS(WithScopeBuf); + NamedD->printQualifiedName(WithScopeOS, WithScopePP); + + // Get all contexts for current NamedDecl + const DeclContext *Ctx = NamedD->getDeclContext(); + + // For ObjC methods, look through categories and use the interface as + // context. + if (auto *MD = dyn_cast(NamedD)) + if (auto *ID = MD->getClassInterface()) + Ctx = ID; + + typedef SmallVector ContextsTy; + ContextsTy Contexts; + + // Collect contexts. + while (Ctx && isa(Ctx)) { + Contexts.push_back(Ctx); + Ctx = Ctx->getParent(); + } + + std::string ResWithScope = WithScopeOS.str(); + if (!ResWithScope.empty()) { + // Get non-qualified name + std::string PrintedNameBuf; + llvm::raw_string_ostream PrintedNameOS(PrintedNameBuf); + NamedD->printName(PrintedNameOS); + auto Last = ResWithScope.rfind(PrintedNameOS.str()); + if (Last != std::string::npos) { + std::string Res = ResWithScope.substr(0, Last); + if (Res.length() > 2 && + Res.substr(Res.length() - 2, Res.length()) == "::") + Res = Res.substr(0, Res.length() - 2); + + if (!Res.empty()) { + if (!Contexts.empty()) { + if (Contexts[0]->isNamespace()) { + const NamespaceDecl *ND = cast(Contexts[0]); + const IdentifierInfo *II = ND->getIdentifier(); + + if (!II) + Res = "anonymous namespace"; + else if (Contexts[0]->isStdNamespace()) + Res = "std namespace"; + else + Res = "namespace named " + Res; + } else if (isa(Contexts[0])) { + std::string s = Contexts[0]->getDeclKindName(); + if (s == "CXXRecord") // doesnt work right currently + { + Res = "class named " + Res; + } else if (s == "Enum") { + Res = "Enum " + Res; + } + } else if (isa(Contexts[0])) { + } + } + MarkedString Info("In " + Res); + Contents.push_back(Info); + } else { + if (isa(NamedD)) { + MarkedString Info("global function"); + Contents.push_back(Info); + } + } + } + } + } + } else { + // If Decl is nullptr, then this was obtained form a Macro type such as + // #define, #ifdef and so on. + // TODO: support statements other than #define + // TODO: find a way to distinguish with null Decls + + MarkedString MS("#define statement"); + Contents.push_back(MS); + } + + MarkedString MS("C++", Ref); + Contents.push_back(MS); + R = L.range; + Hover H(Contents, R); + return H; +} + void ParsedAST::ensurePreambleDeclsDeserialized() { if (PendingTopLevelDecls.empty()) return; Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -21,6 +21,7 @@ #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_PROTOCOL_H #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_PROTOCOL_H +#include "clang/Basic/SourceLocation.h" #include "llvm/ADT/Optional.h" #include "llvm/Support/YAMLParser.h" #include @@ -393,6 +394,60 @@ parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger); }; +struct MarkedString { + /** + * MarkedString can be used to render human readable text. It is either a + * markdown string + * or a code-block that provides a language and a code snippet. The language + * identifier + * is sematically equal to the optional language identifier in fenced code + * blocks in GitHub + * issues. See + * https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting + * + * The pair of a language and a value is an equivalent to markdown: + * ``` + * ${language} + * ${value} + * ``` + * + * Note that markdown strings will be sanitized - that means html will be + * escaped. + */ + + MarkedString(std::string markdown) + : markdownString(markdown), codeBlockLanguage(""), codeBlockValue("") {} + + MarkedString(std::string blockLanguage, std::string blockValue) + : markdownString(""), codeBlockLanguage(blockLanguage), + codeBlockValue(blockValue) {} + + std::string markdownString; + std::string codeBlockLanguage; + std::string codeBlockValue; + + static std::string unparse(const MarkedString &MS); +}; + +struct Hover { + + Hover(std::vector contents, Range r) + : contents(contents), range(r) {} + + /** + * The hover's content + */ + std::vector 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. + */ + Range range; + + static std::string unparse(const Hover &H); +}; + /// The kind of a completion entry. enum class CompletionItemKind { Missing = 0, Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -880,6 +880,36 @@ return Result; } +std::string Hover::unparse(const Hover &H) { + std::string Result; + std::string ContentsStr; + for (auto &Contents : H.contents) { + ContentsStr += MarkedString::unparse(Contents) + ","; + } + if (!ContentsStr.empty()) + ContentsStr.pop_back(); + llvm::raw_string_ostream(Result) << llvm::format( + R"({"contents": [%s], "range": %s})", ContentsStr.c_str(), + Range::unparse(H.range).c_str()); + return Result; +} + +std::string MarkedString::unparse(const MarkedString &MS) { + std::string Result; + if (MS.markdownString != "") { + llvm::raw_string_ostream(Result) << llvm::format( + R"("%s")", llvm::yaml::escape(MS.markdownString).c_str()); + } else { + + llvm::raw_string_ostream(Result) + << llvm::format(R"({"language": "%s", "value": "%s"})", + (llvm::yaml::escape(MS.codeBlockLanguage)).c_str(), + (llvm::yaml::escape(MS.codeBlockValue)).c_str()); + } + + return Result; +} + std::string CompletionItem::unparse(const CompletionItem &CI) { std::string Result = "{"; llvm::raw_string_ostream Os(Result); Index: clangd/ProtocolHandlers.h =================================================================== --- clangd/ProtocolHandlers.h +++ clangd/ProtocolHandlers.h @@ -52,6 +52,7 @@ virtual void onGoToDefinition(Ctx C, TextDocumentPositionParams &Params) = 0; virtual void onSwitchSourceHeader(Ctx C, TextDocumentIdentifier &Params) = 0; virtual void onFileEvent(Ctx C, DidChangeWatchedFilesParams &Params) = 0; + virtual void onCodeHover(Ctx C, TextDocumentPositionParams &Params) = 0; }; void registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, JSONOutput &Out, Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -67,5 +67,6 @@ Register("textDocument/definition", &ProtocolCallbacks::onGoToDefinition); Register("textDocument/switchSourceHeader", &ProtocolCallbacks::onSwitchSourceHeader); + Register("textDocument/hover", &ProtocolCallbacks::onCodeHover); Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent); } Index: test/clangd/hover.test =================================================================== --- /dev/null +++ test/clangd/hover.test @@ -0,0 +1,52 @@ +# RUN: clangd -run-synchronously < %s | FileCheck %s +# It is absolutely vital that this file has CRLF line endings. +# +Content-Length: 125 + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} + +Content-Length: 407 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///main.cpp","languageId":"cpp","version":1,"text":"#define MACRO 1\nnamespace ns1 {\nint test = 5;\nstruct MyClass {\nint xasd;\nvoid anotherOperation() {\n}\nstatic int foo(MyClass*) {\nreturn 0;\n}\n\n};}\nint main() {\nint a;\na;\nint b = ns1::test;\nns1::MyClass* Params;\nParams->anotherOperation();\nMACRO;}\n"}}} + +Content-Length: 144 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":0,"character":12}}} +# Go to local variable +# CHECK: {"jsonrpc":"2.0","id":1,"result":{"contents": ["#define statement",{"language": "C++", "value": "MACRO 1"}], "range": {"start": {"line": 0, "character": 8}, "end": {"line": 0, "character": 15}}}} + +Content-Length: 144 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":14,"character":1}}} +# Go to local variable +# CHECK: {"jsonrpc":"2.0","id":1,"result":{"contents": [{"language": "C++", "value": "int a"}], "range": {"start": {"line": 13, "character": 0}, "end": {"line": 13, "character": 5}}}} + +Content-Length: 145 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":15,"character":15}}} +# Go to local variable +# CHECK: {"jsonrpc":"2.0","id":1,"result":{"contents": ["In namespace named ns1",{"language": "C++", "value": "int test = 5"}], "range": {"start": {"line": 2, "character": 0}, "end": {"line": 2, "character": 12}}}} + +Content-Length: 145 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":16,"character":10}}} +# Go to local variable +# CHECK: {"jsonrpc":"2.0","id":1,"result":{"contents": ["In namespace named ns1",{"language": "C++", "value": "struct MyClass {\nint xasd;\nvoid anotherOperation() {\n}\nstatic int foo(MyClass*) {\nreturn 0;\n}\n\n}"}], "range": {"start": {"line": 3, "character": 0}, "end": {"line": 11, "character": 1}}}} + +Content-Length: 145 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":17,"character":13}}} +# Go to local variable +# CHECK: {"jsonrpc":"2.0","id":1,"result":{"contents": ["In class named ns1::MyClass",{"language": "C++", "value": "void anotherOperation() {\n}"}], "range": {"start": {"line": 5, "character": 0}, "end": {"line": 6, "character": 1}}}} + +Content-Length: 145 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":18,"character":1}}} +# Go to local variable +# CHECK: {"jsonrpc":"2.0","id":1,"result":{"contents": ["#define statement",{"language": "C++", "value": "MACRO 1"}], "range": {"start": {"line": 0, "character": 8}, "end": {"line": 0, "character": 15}}}} + +Content-Length: 44 + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} + + Index: test/clangd/initialize-params-invalid.test =================================================================== --- test/clangd/initialize-params-invalid.test +++ test/clangd/initialize-params-invalid.test @@ -5,7 +5,7 @@ Content-Length: 142 {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":"","rootUri":"file:///path/to/workspace","capabilities":{},"trace":"off"}} -# CHECK: Content-Length: 535 +# CHECK: Content-Length: 568 # CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{ # CHECK: "textDocumentSync": 1, # CHECK: "documentFormattingProvider": true, @@ -14,7 +14,8 @@ # CHECK: "codeActionProvider": true, # CHECK: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, # CHECK: "signatureHelpProvider": {"triggerCharacters": ["(",","]}, -# CHECK: "definitionProvider": true +# CHECK: "definitionProvider": true, +# CHECK: "hoverProvider": true # CHECK: }}} # Content-Length: 44 Index: test/clangd/initialize-params.test =================================================================== --- test/clangd/initialize-params.test +++ test/clangd/initialize-params.test @@ -5,7 +5,7 @@ Content-Length: 143 {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"file:///path/to/workspace","capabilities":{},"trace":"off"}} -# CHECK: Content-Length: 535 +# CHECK: Content-Length: 568 # CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{ # CHECK: "textDocumentSync": 1, # CHECK: "documentFormattingProvider": true, @@ -14,7 +14,8 @@ # CHECK: "codeActionProvider": true, # CHECK: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, # CHECK: "signatureHelpProvider": {"triggerCharacters": ["(",","]}, -# CHECK: "definitionProvider": true +# CHECK: "definitionProvider": true, +# CHECK: "hoverProvider": true # CHECK: }}} # Content-Length: 44