Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -26,6 +26,7 @@ clangSerialization clangTooling clangToolingCore + clangToolingRefactor ${LLVM_PTHREAD_LIB} ) Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -70,6 +70,7 @@ void onSwitchSourceHeader(Ctx C, TextDocumentIdentifier &Params) override; void onFileEvent(Ctx C, DidChangeWatchedFilesParams &Params) override; void onCommand(Ctx C, ExecuteCommandParams &Params) override; + void onRename(Ctx C, RenameParams &Parames) override; std::vector getFixIts(StringRef File, const clangd::Diagnostic &D); Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -46,6 +46,7 @@ "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, "signatureHelpProvider": {"triggerCharacters": ["(",","]}, "definitionProvider": true, + "renameProvider": true, "executeCommandProvider": {"commands": [")" + ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND + R"("]} }})"); @@ -112,6 +113,17 @@ } } +void ClangdLSPServer::onRename(Ctx C, RenameParams &Params) { + auto File = Params.textDocument.uri.file; + std::vector Replacements = + Server.Rename(File, Params.position, Params.newName); + std::string Code = Server.getDocument(File); + std::vector Edits = replacementsToEdits(Code, Replacements); + WorkspaceEdit WE; + WE.changes = {{llvm::yaml::escape(Params.textDocument.uri.uri), Edits}}; + C.reply(WorkspaceEdit::unparse(WE)); +} + void ClangdLSPServer::onDocumentDidClose(Ctx C, DidCloseTextDocumentParams &Params) { Server.removeDocument(Params.textDocument.uri.file); Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -290,6 +290,10 @@ std::vector formatFile(PathRef File); /// Run formatting after a character was typed at \p Pos in \p File. std::vector formatOnType(PathRef File, Position Pos); + /// Rename all occurrences of the symbol at the \p Pos in \p File to + /// \p NewName. + std::vector Rename(PathRef File, Position Pos, + llvm::StringRef NewName); /// Gets current document contents for \p File. \p File must point to a /// currently tracked file. Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -9,6 +9,8 @@ #include "ClangdServer.h" #include "clang/Format/Format.h" +#include "clang/Tooling/Refactoring/RefactoringResultConsumer.h" +#include "clang/Tooling/Refactoring/Rename/RenamingAction.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInvocation.h" #include "clang/Tooling/CompilationDatabase.h" @@ -333,6 +335,53 @@ return formatCode(Code, File, {tooling::Range(PreviousLBracePos, Len)}); } +std::vector +ClangdServer::Rename(PathRef File, Position Pos, llvm::StringRef NewName) { + std::string Code = getDocument(File); + std::shared_ptr Resources = Units.getFile(File); + class RenameResultCollector final + : public tooling::RefactoringResultConsumer { + public: + void handleError(llvm::Error Err) override {} + + void handle(tooling::SymbolOccurrences) override {} + + void handle(tooling::AtomicChanges SourceReplacements) override { + Result = std::move(SourceReplacements); + } + + tooling::AtomicChanges Result; + }; + RenameResultCollector ResultCollector; + Resources->getAST().get()->runUnderLock([&](ParsedAST *AST) { + const SourceManager &SourceMgr = AST->getASTContext().getSourceManager(); + const FileEntry *FE = + SourceMgr.getFileEntryForID(SourceMgr.getMainFileID()); + if (!FE) + return; + SourceLocation SourceLocationBeg = + clangd::getBeginningOfIdentifier(*AST, Pos, FE); + tooling::RefactoringRuleContext Context( + AST->getASTContext().getSourceManager()); + Context.setASTContext(AST->getASTContext()); + auto rename = clang::tooling::RenameOccurrences::initiate( + Context, SourceRange(SourceLocationBeg), NewName.str()); + if (rename) + rename->invoke(ResultCollector, Context); + }); + std::vector Replacements; + for (const tooling::AtomicChange &Change : ResultCollector.Result) { + tooling::Replacements ChangeReps = Change.getReplacements(); + for (const auto &Rep : ChangeReps) { + // FIXME: Right now we only support renaming the main file, so we drop + // replacements not for the main file. + if (Rep.getFilePath() == File) + Replacements.push_back(Rep); + } + } + return Replacements; +} + std::string ClangdServer::getDocument(PathRef File) { auto draft = DraftMgr.getDraft(File); assert(draft.Draft && "File is not tracked, cannot get contents"); Index: clangd/ClangdUnit.h =================================================================== --- clangd/ClangdUnit.h +++ clangd/ClangdUnit.h @@ -304,6 +304,10 @@ std::shared_ptr PCHs, clangd::Logger &Logger); +/// Get the beginning SourceLocation at a specified \p Pos. +SourceLocation getBeginningOfIdentifier(ParsedAST &Unit, const Position &Pos, + const FileEntry *FE); + /// Get definition of symbol at a specified \p Pos. std::vector findDefinitions(ParsedAST &AST, Position Pos, clangd::Logger &Logger); Index: clangd/ClangdUnit.cpp =================================================================== --- clangd/ClangdUnit.cpp +++ clangd/ClangdUnit.cpp @@ -1001,44 +1001,6 @@ } }; -SourceLocation getBeginningOfIdentifier(ParsedAST &Unit, const Position &Pos, - const FileEntry *FE) { - // The language server protocol uses zero-based line and column numbers. - // Clang uses one-based numbers. - - const ASTContext &AST = Unit.getASTContext(); - const SourceManager &SourceMgr = AST.getSourceManager(); - - SourceLocation InputLocation = - getMacroArgExpandedLocation(SourceMgr, FE, Pos); - if (Pos.character == 0) { - return InputLocation; - } - - // This handle cases where the position is in the middle of a token or right - // after the end of a token. In theory we could just use GetBeginningOfToken - // to find the start of the token at the input position, but this doesn't - // work when right after the end, i.e. foo|. - // So try to go back by one and see if we're still inside the an identifier - // token. If so, Take the beginning of this token. - // (It should be the same identifier because you can't have two adjacent - // identifiers without another token in between.) - SourceLocation PeekBeforeLocation = getMacroArgExpandedLocation( - SourceMgr, FE, Position{Pos.line, Pos.character - 1}); - Token Result; - if (Lexer::getRawToken(PeekBeforeLocation, Result, SourceMgr, - AST.getLangOpts(), false)) { - // getRawToken failed, just use InputLocation. - return InputLocation; - } - - if (Result.is(tok::raw_identifier)) { - return Lexer::GetBeginningOfToken(PeekBeforeLocation, SourceMgr, - AST.getLangOpts()); - } - - return InputLocation; -} } // namespace std::vector clangd::findDefinitions(ParsedAST &AST, Position Pos, @@ -1430,3 +1392,48 @@ Lock.unlock(); File.RebuildCond.notify_all(); } + +namespace clang { +namespace clangd { + +SourceLocation getBeginningOfIdentifier(ParsedAST &Unit, const Position &Pos, + const FileEntry *FE) { + // The language server protocol uses zero-based line and column numbers. + // Clang uses one-based numbers. + + const ASTContext &AST = Unit.getASTContext(); + const SourceManager &SourceMgr = AST.getSourceManager(); + + SourceLocation InputLocation = + getMacroArgExpandedLocation(SourceMgr, FE, Pos); + if (Pos.character == 0) { + return InputLocation; + } + + // This handle cases where the position is in the middle of a token or right + // after the end of a token. In theory we could just use GetBeginningOfToken + // to find the start of the token at the input position, but this doesn't + // work when right after the end, i.e. foo|. + // So try to go back by one and see if we're still inside the an identifier + // token. If so, Take the beginning of this token. + // (It should be the same identifier because you can't have two adjacent + // identifiers without another token in between.) + SourceLocation PeekBeforeLocation = getMacroArgExpandedLocation( + SourceMgr, FE, Position{Pos.line, Pos.character - 1}); + Token Result; + if (Lexer::getRawToken(PeekBeforeLocation, Result, SourceMgr, + AST.getLangOpts(), false)) { + // getRawToken failed, just use InputLocation. + return InputLocation; + } + + if (Result.is(tok::raw_identifier)) { + return Lexer::GetBeginningOfToken(PeekBeforeLocation, SourceMgr, + AST.getLangOpts()); + } + + return InputLocation; +} + +} // namespace clangd +} // namespace clang Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -572,6 +572,20 @@ static std::string unparse(const SignatureHelp &); }; +struct RenameParams { + /// The document that was opened. + TextDocumentIdentifier textDocument; + + /// The position at which this request was sent. + Position position; + + /// The new name of the symbol. + std::string newName; + + static llvm::Optional parse(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger); +}; + } // namespace clangd } // namespace clang Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -1133,3 +1133,51 @@ Result.push_back('}'); return Result; } + +llvm::Optional +RenameParams::parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger) { + RenameParams Result; + for (auto &NextKeyValue : *Params) { + auto *KeyString = dyn_cast(NextKeyValue.getKey()); + if (!KeyString) + return llvm::None; + + llvm::SmallString<10> KeyStorage; + StringRef KeyValue = KeyString->getValue(KeyStorage); + + if (KeyValue == "textDocument") { + auto *Value = + dyn_cast_or_null(NextKeyValue.getValue()); + if (!Value) + continue; + auto *Map = dyn_cast(Value); + if (!Map) + return llvm::None; + auto Parsed = TextDocumentIdentifier::parse(Map, Logger); + if (!Parsed) + return llvm::None; + Result.textDocument = std::move(*Parsed); + } else if (KeyValue == "position") { + auto *Value = + dyn_cast_or_null(NextKeyValue.getValue()); + if (!Value) + continue; + auto Parsed = Position::parse(Value, Logger); + if (!Parsed) + return llvm::None; + Result.position = std::move(*Parsed); + } else if (KeyValue == "newName") { + auto *Value = NextKeyValue.getValue(); + if (!Value) + continue; + auto *Node = dyn_cast(Value); + if (!Node) + return llvm::None; + llvm::SmallString<10> Storage; + Result.newName = Node->getValue(Storage); + } else { + logIgnoredField(KeyValue, Logger); + } + } + return Result; +} Index: clangd/ProtocolHandlers.h =================================================================== --- clangd/ProtocolHandlers.h +++ clangd/ProtocolHandlers.h @@ -53,6 +53,7 @@ virtual void onSwitchSourceHeader(Ctx C, TextDocumentIdentifier &Params) = 0; virtual void onFileEvent(Ctx C, DidChangeWatchedFilesParams &Params) = 0; virtual void onCommand(Ctx C, ExecuteCommandParams &Params) = 0; + virtual void onRename(Ctx C, RenameParams &Parames) = 0; }; void registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, JSONOutput &Out, Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -71,6 +71,7 @@ Register("textDocument/definition", &ProtocolCallbacks::onGoToDefinition); Register("textDocument/switchSourceHeader", &ProtocolCallbacks::onSwitchSourceHeader); + Register("textDocument/rename", &ProtocolCallbacks::onRename); Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent); Register("workspace/executeCommand", &ProtocolCallbacks::onCommand); } 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: 606 +# CHECK: Content-Length: 640 # CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{ # CHECK: "textDocumentSync": 1, # CHECK: "documentFormattingProvider": true, @@ -15,6 +15,7 @@ # CHECK: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, # CHECK: "signatureHelpProvider": {"triggerCharacters": ["(",","]}, # CHECK: "definitionProvider": true, +# CHECK: "renameProvider": true, # CHECK: "executeCommandProvider": {"commands": ["clangd.applyFix"]} # CHECK: }}} # 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: 606 +# CHECK: Content-Length: 640 # CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{ # CHECK: "textDocumentSync": 1, # CHECK: "documentFormattingProvider": true, @@ -15,6 +15,7 @@ # CHECK: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, # CHECK: "signatureHelpProvider": {"triggerCharacters": ["(",","]}, # CHECK: "definitionProvider": true, +# CHECK: "renameProvider": true, # CHECK: "executeCommandProvider": {"commands": ["clangd.applyFix"]} # CHECK: }}} # Index: test/clangd/rename.test =================================================================== --- /dev/null +++ test/clangd/rename.test @@ -0,0 +1,28 @@ +# 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: 186 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///foo.cpp","languageId":"cpp","version":1,"text":"int foo;"}}} +# +# CHECK: {"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"file:///foo.cpp","diagnostics":[]}} +# + +Content-Length: 159 + +{"jsonrpc":"2.0","id":2,"method":"textDocument/rename","params":{"textDocument":{"uri":"file:///foo.cpp"},"position":{"line":0,"character":5},"newName":"bar"}} +# +# CHECK: {"jsonrpc":"2.0","id":2,"result":{"changes": {"file:///foo.cpp": [{"range": {"start": {"line": 0, "character": 4}, "end": {"line": 0, "character": 7}}, "newText": "bar"}]}}} +# + +Content-Length: 44 + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +# CHECK: {"jsonrpc":"2.0","id":3,"result":null} +Content-Length: 33 + +{"jsonrpc":"2.0":"method":"exit"}