Index: clang-tools-extra/trunk/clangd/CMakeLists.txt =================================================================== --- clang-tools-extra/trunk/clangd/CMakeLists.txt +++ clang-tools-extra/trunk/clangd/CMakeLists.txt @@ -27,6 +27,7 @@ clangSerialization clangTooling clangToolingCore + clangToolingRefactor ${LLVM_PTHREAD_LIB} ) Index: clang-tools-extra/trunk/clangd/ClangdLSPServer.h =================================================================== --- clang-tools-extra/trunk/clangd/ClangdLSPServer.h +++ clang-tools-extra/trunk/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: clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp +++ clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp @@ -57,6 +57,7 @@ {"triggerCharacters", {"(", ","}}, }}, {"definitionProvider", true}, + {"renameProvider", true}, {"executeCommandProvider", json::obj{ {"commands", {ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND}}, @@ -127,6 +128,22 @@ } } +void ClangdLSPServer::onRename(Ctx C, RenameParams &Params) { + auto File = Params.textDocument.uri.file; + auto Replacements = Server.rename(File, Params.position, Params.newName); + if (!Replacements) { + C.replyError( + ErrorCode::InternalError, + llvm::toString(Replacements.takeError())); + return; + } + 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: clang-tools-extra/trunk/clangd/ClangdServer.h =================================================================== --- clang-tools-extra/trunk/clangd/ClangdServer.h +++ clang-tools-extra/trunk/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. + Expected> 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: clang-tools-extra/trunk/clangd/ClangdServer.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ClangdServer.cpp +++ clang-tools-extra/trunk/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" @@ -51,6 +53,28 @@ return CompilerInvocation::GetResourcesPath("clangd", (void *)&Dummy); } +class RefactoringResultCollector final + : public tooling::RefactoringResultConsumer { +public: + void handleError(llvm::Error Err) override { + assert(!Result.hasValue()); + // FIXME: figure out a way to return better message for DiagnosticError. + // clangd uses llvm::toString to convert the Err to string, however, for + // DiagnosticError, only "clang diagnostic" will be generated. + Result = std::move(Err); + } + + // Using the handle(SymbolOccurrences) from parent class. + using tooling::RefactoringResultConsumer::handle; + + void handle(tooling::AtomicChanges SourceReplacements) override { + assert(!Result.hasValue()); + Result = std::move(SourceReplacements); + } + + Optional> Result; +}; + } // namespace size_t clangd::positionToOffset(StringRef Code, Position P) { @@ -333,6 +357,54 @@ return formatCode(Code, File, {tooling::Range(PreviousLBracePos, Len)}); } +Expected> +ClangdServer::rename(PathRef File, Position Pos, llvm::StringRef NewName) { + std::string Code = getDocument(File); + std::shared_ptr Resources = Units.getFile(File); + RefactoringResultCollector 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) { + ResultCollector.Result = Rename.takeError(); + return; + } + Rename->invoke(ResultCollector, Context); + }); + assert(ResultCollector.Result.hasValue()); + if (!ResultCollector.Result.getValue()) + return ResultCollector.Result->takeError(); + + std::vector Replacements; + for (const tooling::AtomicChange &Change : ResultCollector.Result->get()) { + 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. In the future, we might consider to + // support: + // * rename in any included header + // * rename only in the "main" header + // * provide an error if there are symbols we won't rename (e.g. + // std::vector) + // * rename globally in project + // * rename in open files + 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: clang-tools-extra/trunk/clangd/ClangdUnit.h =================================================================== --- clang-tools-extra/trunk/clangd/ClangdUnit.h +++ clang-tools-extra/trunk/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: clang-tools-extra/trunk/clangd/ClangdUnit.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ClangdUnit.cpp +++ clang-tools-extra/trunk/clangd/ClangdUnit.cpp @@ -1007,44 +1007,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, @@ -1436,3 +1398,43 @@ Lock.unlock(); File.RebuildCond.notify_all(); } + +SourceLocation clangd::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; +} Index: clang-tools-extra/trunk/clangd/Protocol.h =================================================================== --- clang-tools-extra/trunk/clangd/Protocol.h +++ clang-tools-extra/trunk/clangd/Protocol.h @@ -589,6 +589,20 @@ static json::Expr 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: clang-tools-extra/trunk/clangd/Protocol.cpp =================================================================== --- clang-tools-extra/trunk/clangd/Protocol.cpp +++ clang-tools-extra/trunk/clangd/Protocol.cpp @@ -1073,3 +1073,51 @@ {"signatures", json::ary(SH.signatures)}, }; } + +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: clang-tools-extra/trunk/clangd/ProtocolHandlers.h =================================================================== --- clang-tools-extra/trunk/clangd/ProtocolHandlers.h +++ clang-tools-extra/trunk/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: clang-tools-extra/trunk/clangd/ProtocolHandlers.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ProtocolHandlers.cpp +++ clang-tools-extra/trunk/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: clang-tools-extra/trunk/test/clangd/initialize-params-invalid.test =================================================================== --- clang-tools-extra/trunk/test/clangd/initialize-params-invalid.test +++ clang-tools-extra/trunk/test/clangd/initialize-params-invalid.test @@ -30,6 +30,7 @@ # CHECK-NEXT: "clangd.applyFix" # CHECK-NEXT: ] # CHECK-NEXT: }, +# CHECK-NEXT: "renameProvider": true, # CHECK-NEXT: "signatureHelpProvider": { # CHECK-NEXT: "triggerCharacters": [ # CHECK-NEXT: "(", Index: clang-tools-extra/trunk/test/clangd/initialize-params.test =================================================================== --- clang-tools-extra/trunk/test/clangd/initialize-params.test +++ clang-tools-extra/trunk/test/clangd/initialize-params.test @@ -30,6 +30,7 @@ # CHECK-NEXT: "clangd.applyFix" # CHECK-NEXT: ] # CHECK-NEXT: }, +# CHECK-NEXT: "renameProvider": true, # CHECK-NEXT: "signatureHelpProvider": { # CHECK-NEXT: "triggerCharacters": [ # CHECK-NEXT: "(", Index: clang-tools-extra/trunk/test/clangd/rename.test =================================================================== --- clang-tools-extra/trunk/test/clangd/rename.test +++ clang-tools-extra/trunk/test/clangd/rename.test @@ -0,0 +1,50 @@ +# RUN: clangd -pretty -run-synchronously < %s | FileCheck -strict-whitespace %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: 150 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///foo.cpp","languageId":"cpp","version":1,"text":"int foo;"}}} + +Content-Length: 159 + +{"jsonrpc":"2.0","id":1,"method":"textDocument/rename","params":{"textDocument":{"uri":"file:///foo.cpp"},"position":{"line":0,"character":5},"newName":"bar"}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file:///foo.cpp": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "bar", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 7 +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 4 +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: } +Content-Length: 159 + +{"jsonrpc":"2.0","id":2,"method":"textDocument/rename","params":{"textDocument":{"uri":"file:///foo.cpp"},"position":{"line":0,"character":2},"newName":"bar"}} +# CHECK: "error": { +# CHECK-NEXT: "code": -32603, +# CHECK-NEXT: "message": "clang diagnostic" +# CHECK-NEXT: }, +# CHECK-NEXT: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0" +Content-Length: 44 + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +Content-Length: 33 + +{"jsonrpc":"2.0":"method":"exit"}