forked from OSchip/llvm-project
[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:
parent
578a425890
commit
345099ca19
|
@ -27,6 +27,7 @@ add_clang_library(clangDaemon
|
|||
clangSerialization
|
||||
clangTooling
|
||||
clangToolingCore
|
||||
clangToolingRefactor
|
||||
${LLVM_PTHREAD_LIB}
|
||||
)
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: "(",
|
||||
|
|
|
@ -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: "(",
|
||||
|
|
|
@ -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"}
|
Loading…
Reference in New Issue