[clangd] Add rename support.

Summary:
Make clangd handle "textDocument/rename" request. The rename
functionality comes from the "local-rename" sub-tool of clang-refactor.

Currently clangd only supports local rename (only symbol occurrences in
the main file will be renamed).

Reviewers: sammccall, ilya-biryukov

Reviewed By: sammccall

Subscribers: cfe-commits, ioeric, arphaman, mgorny

Differential Revision: https://reviews.llvm.org/D39676

llvm-svn: 317780
This commit is contained in:
Haojian Wu 2017-11-09 11:30:04 +00:00
parent 578a425890
commit 345099ca19
14 changed files with 255 additions and 38 deletions

View File

@ -27,6 +27,7 @@ add_clang_library(clangDaemon
clangSerialization
clangTooling
clangToolingCore
clangToolingRefactor
${LLVM_PTHREAD_LIB}
)

View File

@ -57,6 +57,7 @@ void ClangdLSPServer::onInitialize(Ctx C, InitializeParams &Params) {
{"triggerCharacters", {"(", ","}},
}},
{"definitionProvider", true},
{"renameProvider", true},
{"executeCommandProvider",
json::obj{
{"commands", {ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND}},
@ -127,6 +128,22 @@ void ClangdLSPServer::onCommand(Ctx C, ExecuteCommandParams &Params) {
}
}
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<TextEdit> 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);

View File

@ -70,6 +70,7 @@ private:
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<clang::tooling::Replacement>
getFixIts(StringRef File, const clangd::Diagnostic &D);

View File

@ -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 @@ std::string getStandardResourceDir() {
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<Expected<tooling::AtomicChanges>> Result;
};
} // namespace
size_t clangd::positionToOffset(StringRef Code, Position P) {
@ -333,6 +357,54 @@ std::vector<tooling::Replacement> ClangdServer::formatOnType(PathRef File,
return formatCode(Code, File, {tooling::Range(PreviousLBracePos, Len)});
}
Expected<std::vector<tooling::Replacement>>
ClangdServer::rename(PathRef File, Position Pos, llvm::StringRef NewName) {
std::string Code = getDocument(File);
std::shared_ptr<CppFile> 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<tooling::Replacement> 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");

View File

@ -290,6 +290,10 @@ public:
std::vector<tooling::Replacement> formatFile(PathRef File);
/// Run formatting after a character was typed at \p Pos in \p File.
std::vector<tooling::Replacement> formatOnType(PathRef File, Position Pos);
/// Rename all occurrences of the symbol at the \p Pos in \p File to
/// \p NewName.
Expected<std::vector<tooling::Replacement>> rename(PathRef File, Position Pos,
llvm::StringRef NewName);
/// Gets current document contents for \p File. \p File must point to a
/// currently tracked file.

View File

@ -1007,44 +1007,6 @@ private:
}
};
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<Location> clangd::findDefinitions(ParsedAST &AST, Position Pos,
@ -1436,3 +1398,43 @@ CppFile::RebuildGuard::~RebuildGuard() {
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;
}

View File

@ -304,6 +304,10 @@ SignatureHelp signatureHelp(PathRef FileName,
std::shared_ptr<PCHContainerOperations> 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<Location> findDefinitions(ParsedAST &AST, Position Pos,
clangd::Logger &Logger);

View File

@ -1073,3 +1073,51 @@ json::Expr SignatureHelp::unparse(const SignatureHelp &SH) {
{"signatures", json::ary(SH.signatures)},
};
}
llvm::Optional<RenameParams>
RenameParams::parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger) {
RenameParams Result;
for (auto &NextKeyValue : *Params) {
auto *KeyString = dyn_cast<llvm::yaml::ScalarNode>(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<llvm::yaml::MappingNode>(NextKeyValue.getValue());
if (!Value)
continue;
auto *Map = dyn_cast<llvm::yaml::MappingNode>(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<llvm::yaml::MappingNode>(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<llvm::yaml::ScalarNode>(Value);
if (!Node)
return llvm::None;
llvm::SmallString<10> Storage;
Result.newName = Node->getValue(Storage);
} else {
logIgnoredField(KeyValue, Logger);
}
}
return Result;
}

View File

@ -589,6 +589,20 @@ struct SignatureHelp {
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<RenameParams> parse(llvm::yaml::MappingNode *Params,
clangd::Logger &Logger);
};
} // namespace clangd
} // namespace clang

View File

@ -71,6 +71,7 @@ void clangd::registerCallbackHandlers(JSONRPCDispatcher &Dispatcher,
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);
}

View File

@ -53,6 +53,7 @@ public:
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,

View File

@ -30,6 +30,7 @@ Content-Length: 142
# CHECK-NEXT: "clangd.applyFix"
# CHECK-NEXT: ]
# CHECK-NEXT: },
# CHECK-NEXT: "renameProvider": true,
# CHECK-NEXT: "signatureHelpProvider": {
# CHECK-NEXT: "triggerCharacters": [
# CHECK-NEXT: "(",

View File

@ -30,6 +30,7 @@ Content-Length: 143
# CHECK-NEXT: "clangd.applyFix"
# CHECK-NEXT: ]
# CHECK-NEXT: },
# CHECK-NEXT: "renameProvider": true,
# CHECK-NEXT: "signatureHelpProvider": {
# CHECK-NEXT: "triggerCharacters": [
# CHECK-NEXT: "(",

View File

@ -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"}