Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -71,6 +71,8 @@ JSONOutput &Out) override; void onGoToDefinition(TextDocumentPositionParams Params, StringRef ID, JSONOutput &Out) override; + void onDocumentHighlight(TextDocumentPositionParams Params, StringRef ID, + JSONOutput &Out) override; private: ClangdLSPServer &LangServer; @@ -87,7 +89,8 @@ "documentOnTypeFormattingProvider": {"firstTriggerCharacter":"}","moreTriggerCharacter":[]}, "codeActionProvider": true, "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, - "definitionProvider": true + "definitionProvider": true, + "documentHighlightProvider": true }}})"); } @@ -220,6 +223,28 @@ R"(,"result":[)" + Locations + R"(]})"); } +void ClangdLSPServer::LSPProtocolCallbacks::onDocumentHighlight( + TextDocumentPositionParams Params, StringRef ID, JSONOutput &Out) { + + auto Items = LangServer.Server + .findDocumentHighlights(Params.textDocument.uri.file, + Position{Params.position.line, + Params.position.character}) + .Value; + + std::string Highlights; + + for (const auto &Item : Items) { + Highlights += DocumentHighlight::unparse(Item); + Highlights += ","; + } + if (!Highlights.empty()) + Highlights.pop_back(); + Out.writeMessage( + R"({"jsonrpc":"2.0","id":)" + ID.str() + + R"(,"result":[)" + Highlights + R"(]})"); +} + ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, unsigned AsyncThreadsCount, bool SnippetCompletions, llvm::Optional ResourceDir) Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -245,6 +245,9 @@ /// Get definition of symbol at a specified \p Line and \p Column in \p File. Tagged> findDefinitions(PathRef File, Position Pos); + /// Get document highlights for a symbol hovered on. + Tagged> findDocumentHighlights(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 @@ -158,8 +158,8 @@ DocVersion Version = DraftMgr.updateDraft(File, Contents); auto TaggedFS = FSProvider.getTaggedFileSystem(File); - std::shared_ptr Resources = - Units.getOrCreateFile(File, ResourceDir, CDB, PCHs, TaggedFS.Value, Logger); + std::shared_ptr Resources = Units.getOrCreateFile( + File, ResourceDir, CDB, PCHs, TaggedFS.Value, Logger); return scheduleReparseAndDiags(File, VersionedDraft{Version, Contents.str()}, std::move(Resources), std::move(TaggedFS)); } @@ -288,6 +288,26 @@ return make_tagged(std::move(Result), TaggedFS.Tag); } +Tagged> +ClangdServer::findDocumentHighlights(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); + assert(Resources && "Calling findDefinitions on non-added file"); + + std::vector Result; + Resources->getAST().get()->runUnderLock([Pos, &Result, this](ParsedAST *AST) { + if (!AST) + return; + Result = clangd::findDocumentHighlights(*AST, Pos, Logger); + }); + return make_tagged(std::move(Result), 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 @@ -263,6 +263,10 @@ /// Get definition of symbol at a specified \p Pos. std::vector findDefinitions(ParsedAST &AST, Position Pos, clangd::Logger &Logger); + +std::vector findDocumentHighlights(ParsedAST &AST, Position Pos, + clangd::Logger &Logger); + /// 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 @@ -716,6 +716,7 @@ ArrayRef Relations, FileID FID, unsigned Offset, index::IndexDataConsumer::ASTNodeInfo ASTNode) override { + if (isSearchedLocation(FID, Offset)) { addDeclarationLocation(D->getSourceRange()); } @@ -779,6 +780,167 @@ } }; +/// Finds declarations locations that a given source location refers to. +class TargetDeclarationFinder : public index::IndexDataConsumer { + std::vector DeclarationLocations; + const SourceLocation &SearchedLocation; + const ASTContext &AST; + Preprocessor &PP; + +public: + TargetDeclarationFinder(raw_ostream &OS, + const SourceLocation &SearchedLocation, + ASTContext &AST, Preprocessor &PP) + : SearchedLocation(SearchedLocation), AST(AST), PP(PP) {} + + unsigned TargetDeclFileOffset; + FileID TargetDeclFileID; + + bool + handleDeclOccurence(const Decl *D, index::SymbolRoleSet Roles, + ArrayRef Relations, FileID FID, + unsigned Offset, + index::IndexDataConsumer::ASTNodeInfo ASTNode) override { + + if (isSearchedLocation(FID, Offset)) { + TargetDeclFileOffset = + AST.getSourceManager().getFileOffset(D->getSourceRange().getBegin()); + TargetDeclFileID = FID; + } + return true; + } + +private: + bool isSearchedLocation(FileID FID, unsigned Offset) const { + const SourceManager &SourceMgr = AST.getSourceManager(); + return SourceMgr.getFileOffset(SearchedLocation) == Offset && + SourceMgr.getFileID(SearchedLocation) == FID; + } +}; + +/// Finds document highlights that a given FileID and file offset refers to. +class DocumentHighlightsFinder : public index::IndexDataConsumer { + std::vector DeclarationLocations; + const SourceLocation &SearchedLocation; + const ASTContext &AST; + Preprocessor &PP; + +public: + unsigned TargetDeclFileOffset; + FileID TargetDeclFileID; + + DocumentHighlightsFinder(raw_ostream &OS, + const SourceLocation &SearchedLocation, + ASTContext &AST, Preprocessor &PP, + unsigned TargetDeclFileOffset, + FileID TargetDeclFileID) + : SearchedLocation(SearchedLocation), AST(AST), PP(PP), + TargetDeclFileOffset(TargetDeclFileOffset), + TargetDeclFileID(TargetDeclFileID) {} + + std::vector takeHighlights() { + // 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()); + auto last = + std::unique(DeclarationLocations.begin(), DeclarationLocations.end()); + DeclarationLocations.erase(last, DeclarationLocations.end()); + return std::move(DeclarationLocations); + } + + bool + handleDeclOccurence(const Decl *D, index::SymbolRoleSet Roles, + ArrayRef Relations, FileID FID, + unsigned Offset, + index::IndexDataConsumer::ASTNodeInfo ASTNode) override { + const SourceManager &SourceMgr = AST.getSourceManager(); + + if (isValidHighlight( + SourceMgr.getFileID(D->getSourceRange().getBegin()), + SourceMgr.getFileOffset(D->getSourceRange().getBegin()))) { + SourceLocation Begin, End; + const LangOptions &LangOpts = AST.getLangOpts(); + SourceLocation StartOfFileLoc = SourceMgr.getLocForStartOfFile(FID); + SourceLocation HightlightStartLoc = + StartOfFileLoc.getLocWithOffset(Offset); + End = Lexer::getLocForEndOfToken(HightlightStartLoc, 0, SourceMgr, + LangOpts); + SourceRange SR(HightlightStartLoc, End); + DocumentHighlightKind Kind; + switch (Roles) { + case (unsigned)index::SymbolRole::Read: + Kind = DocumentHighlightKind::Read; + break; + case (unsigned)index::SymbolRole::Write: + Kind = DocumentHighlightKind::Write; + break; + default: + Kind = DocumentHighlightKind::Text; + break; + } + + addHighlightLocation(SR, Kind); + } + + return true; + } + +private: + bool isValidHighlight(FileID FID, unsigned Offset) const { + return TargetDeclFileOffset == Offset && TargetDeclFileID == FID; + } + + void addHighlightLocation(const SourceRange &ValSourceRange, + DocumentHighlightKind Kind) { + const SourceManager &SourceMgr = AST.getSourceManager(); + const LangOptions &LangOpts = AST.getLangOpts(); + SourceLocation LocStart = ValSourceRange.getBegin(); + SourceLocation LocEnd = Lexer::getLocForEndOfToken(ValSourceRange.getEnd(), + 0, SourceMgr, LangOpts); + Position Begin; + Begin.line = SourceMgr.getSpellingLineNumber(LocStart) - 1; + Begin.character = SourceMgr.getSpellingColumnNumber(LocStart) - 1; + Position End; + End.line = SourceMgr.getSpellingLineNumber(LocEnd) - 1; + End.character = SourceMgr.getSpellingColumnNumber(LocEnd) - 1; + Range R = {Begin, End}; + DocumentHighlight DH; + DH.range = R; + DH.kind = Kind; + DeclarationLocations.push_back(DH); + } + + void finish() override { + // Also handle possible macro at the searched location. + Token Result; + if (!Lexer::getRawToken(SearchedLocation, Result, AST.getSourceManager(), + AST.getLangOpts(), false)) { + if (Result.is(tok::raw_identifier)) { + PP.LookUpIdentifierInfo(Result); + } + IdentifierInfo *IdentifierInfo = Result.getIdentifierInfo(); + if (IdentifierInfo && IdentifierInfo->hadMacroDefinition()) { + std::pair DecLoc = + AST.getSourceManager().getDecomposedExpansionLoc(SearchedLocation); + // Get the definition just before the searched location so that a macro + // referenced in a '#undef MACRO' can still be found. + SourceLocation BeforeSearchedLocation = getMacroArgExpandedLocation( + AST.getSourceManager(), + AST.getSourceManager().getFileEntryForID(DecLoc.first), + DecLoc.second - 1); + MacroDefinition MacroDef = + PP.getMacroDefinitionAtLoc(IdentifierInfo, BeforeSearchedLocation); + MacroInfo *MacroInf = MacroDef.getMacroInfo(); + if (MacroInf) { + addHighlightLocation(SourceRange(MacroInf->getDefinitionLoc(), + MacroInf->getDefinitionEndLoc()), + DocumentHighlightKind::Text); + } + } + } + } +}; + SourceLocation getBeginningOfIdentifier(ParsedAST &Unit, const Position &Pos, const FileEntry *FE) { // The language server protocol uses zero-based line and column numbers. @@ -842,6 +1004,42 @@ return DeclLocationsFinder->takeLocations(); } +std::vector +clangd::findDocumentHighlights(ParsedAST &AST, Position Pos, + clangd::Logger &Logger) { + const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); + const FileEntry *FE = SourceMgr.getFileEntryForID(SourceMgr.getMainFileID()); + if (!FE) + return {}; + + SourceLocation SourceLocationBeg = getBeginningOfIdentifier(AST, Pos, FE); + + auto DeclLocationsFinder = 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(), + DeclLocationsFinder, IndexOpts); + + auto DocHighlightsFinder = std::make_shared( + llvm::errs(), SourceLocationBeg, AST.getASTContext(), + AST.getPreprocessor(), DeclLocationsFinder->TargetDeclFileOffset, + DeclLocationsFinder->TargetDeclFileID); + + IndexOpts.SystemSymbolFilter = + index::IndexingOptions::SystemSymbolFilterKind::All; + IndexOpts.IndexFunctionLocals = true; + + indexTopLevelDecls(AST.getASTContext(), AST.getTopLevelDecls(), + DocHighlightsFinder, IndexOpts); + + return DocHighlightsFinder->takeHighlights(); +} + void ParsedAST::ensurePreambleDeclsDeserialized() { if (PendingTopLevelDecls.empty()) return; Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -416,6 +416,41 @@ static std::string unparse(const CompletionItem &P); }; +enum class DocumentHighlightKind { + Text = 1, + Read = 2, + Write = 3 +}; + +/** + * A document highlight is a range inside a text document which deserves + * special attention. Usually a document highlight is visualized by changing + * the background color of its range. + * + */ +struct DocumentHighlight{ + /** + * The range this highlight applies to. + */ + Range range; + + /** + * The highlight kind, default is DocumentHighlightKind.Text. + */ + DocumentHighlightKind kind = DocumentHighlightKind::Text; + + friend bool operator<(const DocumentHighlight &LHS, const DocumentHighlight &RHS) { + return std::tie(LHS.range) < std::tie(RHS.range); + } + + friend bool operator==(const DocumentHighlight &LHS, const DocumentHighlight &RHS) { + return LHS.kind == RHS.kind && LHS.range == RHS.range; + } + + static std::string unparse(const DocumentHighlight &DH); +}; + + } // namespace clangd } // namespace clang Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -774,3 +774,11 @@ Result.back() = '}'; return Result; } + + +std::string DocumentHighlight::unparse(const DocumentHighlight &DH) { + std::string Result; + llvm::raw_string_ostream(Result) << llvm::format( + R"({"range": %s, "number": %d})", Range::unparse(DH.range).c_str(), DH.kind); + return Result; + } \ No newline at end of file Index: clangd/ProtocolHandlers.h =================================================================== --- clangd/ProtocolHandlers.h +++ clangd/ProtocolHandlers.h @@ -47,7 +47,9 @@ virtual void onCompletion(TextDocumentPositionParams Params, StringRef ID, JSONOutput &Out) = 0; virtual void onGoToDefinition(TextDocumentPositionParams Params, StringRef ID, - JSONOutput &Out) = 0; + JSONOutput &Out) = 0; + virtual void onDocumentHighlight(TextDocumentPositionParams Params, StringRef ID, + JSONOutput &Out) = 0; }; void regiterCallbackHandlers(JSONRPCDispatcher &Dispatcher, JSONOutput &Out, Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -204,6 +204,24 @@ ProtocolCallbacks &Callbacks; }; +struct DocumentHighlightHandler : Handler { + DocumentHighlightHandler(JSONOutput &Output, ProtocolCallbacks &Callbacks) + : Handler(Output), Callbacks(Callbacks) {} + + void handleMethod(llvm::yaml::MappingNode *Params, StringRef ID) override { + auto TDPP = TextDocumentPositionParams::parse(Params, Output); + if (!TDPP) { + Output.log("Failed to decode TextDocumentPositionParams!\n"); + return; + } + + Callbacks.onDocumentHighlight(*TDPP, ID, Output); + } + +private: + ProtocolCallbacks &Callbacks; +}; + } // namespace void clangd::regiterCallbackHandlers(JSONRPCDispatcher &Dispatcher, @@ -240,4 +258,7 @@ Dispatcher.registerHandler( "textDocument/definition", llvm::make_unique(Out, Callbacks)); + Dispatcher.registerHandler( + "textDocument/documentHighlight", + llvm::make_unique(Out, Callbacks)); } Index: test/clangd/documenthighlight.test =================================================================== --- /dev/null +++ test/clangd/documenthighlight.test @@ -0,0 +1,29 @@ +# 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: 455 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///main.cpp","languageId":"cpp","version":1,"text":"#define MACRO 1\nnamespace ns1 {\nstruct MyClass {\nint xasd;\nvoid anotherOperation() {\n}\nstatic int foo(MyClass*) {\nreturn 0;\n}\n\n};\nstruct Foo {\nint xasd;\n};\n}\nint main() {\nint bonjour;\nbonjour = 2;\nns1::Foo bar = { xasd : 1};\nbar.xasd = 3;\nns1::MyClass* Params;\nParams->anotherOperation();}\n"}}} + +Content-Length: 156 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/documentHighlight","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":17,"character":2}}} +# Go to local variable +# CHECK: {"jsonrpc":"2.0","id":1,"result":[{"range": {"start": {"line": 16, "character": 4}, "end": {"line": 16, "character": 12}}, "number": 1},{"range": {"start": {"line": 17, "character": 0}, "end": {"line": 17, "character": 7}}, "number": 1}]} + +Content-Length: 157 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/documentHighlight","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":18,"character":17}}} +# Go to local variable +# CHECK: {"jsonrpc":"2.0","id":1,"result":[{"range": {"start": {"line": 12, "character": 4}, "end": {"line": 12, "character": 9}}, "number": 1},{"range": {"start": {"line": 18, "character": 17}, "end": {"line": 18, "character": 21}}, "number": 1},{"range": {"start": {"line": 19, "character": 4}, "end": {"line": 19, "character": 8}}, "number": 1}]} + +Content-Length: 157 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/documentHighlight","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":21,"character":10}}} +# Go to local variable +# CHECK: {"jsonrpc":"2.0","id":1,"result":[{"range": {"start": {"line": 4, "character": 5}, "end": {"line": 4, "character": 22}}, "number": 1},{"range": {"start": {"line": 21, "character": 8}, "end": {"line": 21, "character": 25}}, "number": 1}]} +