Skip to content

[clangd] Add an indexedRename request #7973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 41 additions & 17 deletions clang-tools-extra/clangd/ClangdLSPServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -856,30 +856,53 @@ void ClangdLSPServer::onPrepareRename(const TextDocumentPositionParams &Params,
});
}

/// Validate that `Edits` are valid and form a `WorkspaceEdit` that contains
/// the edits as its `changes`.
static llvm::Expected<WorkspaceEdit>
formWorkspaceEdit(const FileEdits &Edits, const ClangdServer &Server) {
if (auto Err = validateEdits(Server, Edits))
return std::move(Err);
WorkspaceEdit Result;
// FIXME: use documentChanges if SupportDocumentChanges is true.
Result.changes.emplace();
for (const auto &Rep : Edits) {
(*Result.changes)[URI::createFile(Rep.first()).toString()] =
Rep.second.asTextEdits();
}
return Result;
}

void ClangdLSPServer::onRename(const RenameParams &Params,
Callback<WorkspaceEdit> Reply) {
Path File = std::string(Params.textDocument.uri.file());
if (!Server->getDraft(File))
return Reply(llvm::make_error<LSPError>(
"onRename called for non-added file", ErrorCode::InvalidParams));
auto Callback = [Reply = std::move(Reply),
this](llvm::Expected<RenameResult> R) mutable {
if (!R)
return Reply(R.takeError());
llvm::Expected<WorkspaceEdit> WorkspaceEdit =
formWorkspaceEdit(R->GlobalChanges, *Server);
Reply(std::move(WorkspaceEdit));
};
Server->rename(File, Params.position, Params.newName, Opts.Rename,
[File, Params, Reply = std::move(Reply),
this](llvm::Expected<RenameResult> R) mutable {
if (!R)
return Reply(R.takeError());
if (auto Err = validateEdits(*Server, R->GlobalChanges))
return Reply(std::move(Err));
WorkspaceEdit Result;
// FIXME: use documentChanges if SupportDocumentChanges is
// true.
Result.changes.emplace();
for (const auto &Rep : R->GlobalChanges) {
(*Result
.changes)[URI::createFile(Rep.first()).toString()] =
Rep.second.asTextEdits();
}
Reply(Result);
});
std::move(Callback));
}

void ClangdLSPServer::onIndexedRename(const IndexedRenameParams &Params,
Callback<WorkspaceEdit> Reply) {
auto Callback = [Reply = std::move(Reply),
this](llvm::Expected<FileEdits> Edits) mutable {
if (!Edits) {
return Reply(Edits.takeError());
}
llvm::Expected<WorkspaceEdit> WorkspaceEdit =
formWorkspaceEdit(*Edits, *Server);
Reply(std::move(WorkspaceEdit));
};
Server->indexedRename(Params.positions, Params.textDocument.uri.file(),
Params.oldName, Params.newName, std::move(Callback));
}

void ClangdLSPServer::onDocumentDidClose(
Expand Down Expand Up @@ -1635,6 +1658,7 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
Bind.method("textDocument/switchSourceHeader", this, &ClangdLSPServer::onSwitchSourceHeader);
Bind.method("textDocument/prepareRename", this, &ClangdLSPServer::onPrepareRename);
Bind.method("textDocument/rename", this, &ClangdLSPServer::onRename);
Bind.method("workspace/indexedRename", this, &ClangdLSPServer::onIndexedRename);
Bind.method("textDocument/hover", this, &ClangdLSPServer::onHover);
Bind.method("textDocument/documentSymbol", this, &ClangdLSPServer::onDocumentSymbol);
Bind.method("workspace/executeCommand", this, &ClangdLSPServer::onCommand);
Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/clangd/ClangdLSPServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
void onPrepareRename(const TextDocumentPositionParams &,
Callback<PrepareRenameResult>);
void onRename(const RenameParams &, Callback<WorkspaceEdit>);
void onIndexedRename(const IndexedRenameParams &, Callback<WorkspaceEdit>);
void onHover(const TextDocumentPositionParams &,
Callback<std::optional<Hover>>);
void onPrepareTypeHierarchy(const TypeHierarchyPrepareParams &,
Expand Down
44 changes: 44 additions & 0 deletions clang-tools-extra/clangd/ClangdServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,50 @@ void ClangdServer::rename(PathRef File, Position Pos, llvm::StringRef NewName,
WorkScheduler->runWithAST("Rename", File, std::move(Action));
}

void ClangdServer::indexedRename(
const std::map<URIForFile, std::vector<Position>> &Positions,
PathRef PrimaryFile, llvm::StringRef OldName, llvm::StringRef NewName,
Callback<FileEdits> CB) {
ParseInputs Inputs;
Inputs.TFS = &TFS;
Inputs.CompileCommand = CDB.getCompileCommand(PrimaryFile)
.value_or(CDB.getFallbackCommand(PrimaryFile));
IgnoreDiagnostics IgnoreDiags;
std::unique_ptr<CompilerInvocation> CI =
buildCompilerInvocation(Inputs, IgnoreDiags);
if (!CI) {
return CB(llvm::make_error<llvm::StringError>(
"Unable to get compiler arguments for primary file",
llvm::inconvertibleErrorCode()));
}
const LangOptions &LangOpts = CI->getLangOpts();

tooling::SymbolName OldSymbolName(OldName, LangOpts);
tooling::SymbolName NewSymbolName(NewName, LangOpts);

llvm::StringMap<std::vector<Range>> FilesToRanges;
for (auto Entry : Positions) {
std::vector<Range> &Ranges = FilesToRanges[Entry.first.file()];
for (Position Pos : Entry.second) {
// Compute the range for the given position:
// - If the old name is a simple identifier, we can add its length to the
// start position's column because identifiers can't contain newlines
// - If we have a multi-piece symbol name, them `editsForLocations` will
// only look at the start of the range to call
// `findObjCSymbolSelectorPieces`. It is thus fine to use an empty
// range that points to the symbol's start.
Position End = Pos;
if (std::optional<std::string> Identifier =
OldSymbolName.getSinglePiece()) {
End.line += Identifier->size();
}
Ranges.push_back({Pos, End});
}
}
CB(editsForLocations(FilesToRanges, OldSymbolName, NewSymbolName,
*getHeaderFS().view(std::nullopt), LangOpts));
}

// May generate several candidate selections, due to SelectionTree ambiguity.
// vector of pointers because GCC doesn't like non-copyable Selection.
static llvm::Expected<std::vector<std::unique_ptr<Tweak::Selection>>>
Expand Down
11 changes: 11 additions & 0 deletions clang-tools-extra/clangd/ClangdServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,17 @@ class ClangdServer {
void rename(PathRef File, Position Pos, llvm::StringRef NewName,
const RenameOptions &Opts, Callback<RenameResult> CB);

/// Rename all occurrences of a symbol named `OldName` to `NewName` at the
/// given `Positions`.
///
/// `PrimaryFile` is used to determine the language options for the symbol to
/// rename, eg. to decide whether `OldName` and `NewName` are Objective-C
/// selectors or normal identifiers.
void
indexedRename(const std::map<URIForFile, std::vector<Position>> &Positions,
PathRef PrimaryFile, llvm::StringRef OldName,
llvm::StringRef NewName, Callback<FileEdits> CB);

struct TweakRef {
std::string ID; /// ID to pass for applyTweak.
std::string Title; /// A single-line message to show in the UI.
Expand Down
9 changes: 9 additions & 0 deletions clang-tools-extra/clangd/Protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,15 @@ bool fromJSON(const llvm::json::Value &Params, RenameParams &R,
O.map("position", R.position) && O.map("newName", R.newName);
}

bool fromJSON(const llvm::json::Value &Params,
IndexedRenameParams &IndexedRename, llvm::json::Path P) {
llvm::json::ObjectMapper O(Params, P);
return O && O.map("textDocument", IndexedRename.textDocument) &&
O.map("oldName", IndexedRename.oldName) &&
O.map("newName", IndexedRename.newName) &&
O.map("positions", IndexedRename.positions);
}

llvm::json::Value toJSON(const DocumentHighlight &DH) {
return llvm::json::Object{
{"range", toJSON(DH.range)},
Expand Down
48 changes: 48 additions & 0 deletions clang-tools-extra/clangd/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ struct URIForFile {
llvm::json::Value toJSON(const URIForFile &U);
bool fromJSON(const llvm::json::Value &, URIForFile &, llvm::json::Path);

template <typename T>
bool fromJSON(const llvm::json::Value &E, std::map<URIForFile, T> &Out,
llvm::json::Path P) {
if (auto *O = E.getAsObject()) {
Out.clear();
for (const auto &KV : *O) {
URIForFile URI;
fromJSON(llvm::json::Value(KV.first), URI, P);
if (!fromJSON(KV.second, Out[URI], P.field(KV.first)))
return false;
}
return true;
}
P.report("expected object");
return false;
}

struct TextDocumentIdentifier {
/// The text document's URI.
URIForFile uri;
Expand Down Expand Up @@ -1447,6 +1464,37 @@ struct RenameParams {
};
bool fromJSON(const llvm::json::Value &, RenameParams &, llvm::json::Path);

/// Rename all occurrences of a symbol named `oldName` to `newName` at the
/// given `positions`.
///
/// The use case of this method is for when the positions to rename are already
/// known, eg. from an index lookup outside of clangd's built-in index. In
/// particular, it determines the edits necessary to rename multi-piece
/// Objective-C selector names.
///
/// `textDocument` is used to determine the language options for the symbol to
/// rename, eg. to decide whether `oldName` and `newName` are Objective-C
/// selectors or normal identifiers.
///
/// This is a clangd extension.
struct IndexedRenameParams {
/// The document in which the declaration to rename is declared. Its compiler
/// arguments are used to infer language settings for the rename.
TextDocumentIdentifier textDocument;

/// The old name of the symbol.
std::string oldName;

/// The new name of the symbol.
std::string newName;

/// The positions at which the symbol is known to appear and that should be
/// renamed.
std::map<URIForFile, std::vector<Position>> positions;
};
bool fromJSON(const llvm::json::Value &, IndexedRenameParams &,
llvm::json::Path);

enum class DocumentHighlightKind { Text = 1, Read = 2, Write = 3 };

/// A document highlight is a range inside a text document which deserves
Expand Down
75 changes: 42 additions & 33 deletions clang-tools-extra/clangd/refactor/Rename.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -702,40 +702,9 @@ renameOutsideFile(const NamedDecl &RenameDecl, llvm::StringRef MainFilePath,
Index, MaxLimitFiles);
if (!AffectedFiles)
return AffectedFiles.takeError();
FileEdits Results;
for (auto &FileAndOccurrences : *AffectedFiles) {
llvm::StringRef FilePath = FileAndOccurrences.first();

auto ExpBuffer = FS.getBufferForFile(FilePath);
if (!ExpBuffer) {
elog("Fail to read file content: Fail to open file {0}: {1}", FilePath,
ExpBuffer.getError().message());
continue;
}

auto AffectedFileCode = (*ExpBuffer)->getBuffer();
syntax::UnexpandedTokenBuffer Tokens(
AffectedFileCode, RenameDecl.getASTContext().getLangOpts());
std::optional<std::vector<Range>> RenameRanges =
adjustRenameRanges(Tokens, OldName.getNamePieces().front(),
std::move(FileAndOccurrences.second));
if (!RenameRanges) {
// Our heuristics fails to adjust rename ranges to the current state of
// the file, it is most likely the index is stale, so we give up the
// entire rename.
return error("Index results don't match the content of file {0} "
"(the index may be stale)",
FilePath);
}
auto RenameEdit = buildRenameEdit(FilePath, AffectedFileCode, *RenameRanges,
OldName, NewName, Tokens);
if (!RenameEdit)
return error("failed to rename in file {0}: {1}", FilePath,
RenameEdit.takeError());
if (!RenameEdit->Replacements.empty())
Results.insert({FilePath, std::move(*RenameEdit)});
}
return Results;
return editsForLocations(*AffectedFiles, OldName, NewName, FS,
RenameDecl.getASTContext().getLangOpts());
}

// A simple edit is either changing line or column, but not both.
Expand Down Expand Up @@ -778,6 +747,46 @@ void findNearMiss(

} // namespace

llvm::Expected<FileEdits>
editsForLocations(const llvm::StringMap<std::vector<Range>> &Ranges,
const SymbolName &OldName, const SymbolName &NewName,
llvm::vfs::FileSystem &FS, const LangOptions &LangOpts) {
FileEdits Results;
for (auto &FileAndOccurrences : Ranges) {
llvm::StringRef FilePath = FileAndOccurrences.first();

auto ExpBuffer = FS.getBufferForFile(FilePath);
if (!ExpBuffer) {
elog("Fail to read file content: Fail to open file {0}: {1}", FilePath,
ExpBuffer.getError().message());
continue;
}

auto AffectedFileCode = (*ExpBuffer)->getBuffer();
syntax::UnexpandedTokenBuffer Tokens(AffectedFileCode, LangOpts);
std::optional<std::vector<Range>> RenameRanges =
adjustRenameRanges(Tokens, OldName.getNamePieces().front(),
std::move(FileAndOccurrences.second));
if (!RenameRanges) {
// Our heuristics fails to adjust rename ranges to the current state of
// the file, it is most likely the index is stale, so we give up the
// entire rename.
elog("Index results don't match the content of file {0} (the index may "
"be stale)",
FilePath);
continue;
}
auto RenameEdit = buildRenameEdit(FilePath, AffectedFileCode, *RenameRanges,
OldName, NewName, Tokens);
if (!RenameEdit)
return error("failed to rename in file {0}: {1}", FilePath,
RenameEdit.takeError());
if (!RenameEdit->Replacements.empty())
Results.insert({FilePath, std::move(*RenameEdit)});
}
return Results;
}

llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
assert(!RInputs.Index == !RInputs.FS &&
"Index and FS must either both be specified or both null.");
Expand Down
16 changes: 16 additions & 0 deletions clang-tools-extra/clangd/refactor/Rename.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ struct RenameInputs {
RenameOptions Opts = {};
};

/// Compute the edits that need to be applied to rename symbols in `Ranges` from
/// `OldName` to `NewName`. The key of `Ranges` is the file path of the file in
/// which the range resides.
///
/// If `OldName` and `NewName` are single-piece identifiers, this just creates
/// edits to change the ranges to `NewName`.
///
/// If `OldName` and `NewName` are multi-piece Objective-C selectors, only the
/// start of the ranges is considered and the file is lexed to find the argument
/// labels of the selector to rename.
llvm::Expected<FileEdits>
editsForLocations(const llvm::StringMap<std::vector<Range>> &Ranges,
const tooling::SymbolName &OldName,
const tooling::SymbolName &NewName, llvm::vfs::FileSystem &FS,
const LangOptions &LangOpts);

struct RenameResult {
/// The range of the symbol that the user can attempt to rename.
Range Target;
Expand Down
Loading