Skip to content

Commit 39ca137

Browse files
committed
[clangd] Add an indexedRename request
This request can be used by sourcekit-lsp to get the edits necessary to rename a symbol whose occurrences are known from the IndexStore that is kept by sourcekit-lsp.
1 parent ea6d9dc commit 39ca137

File tree

11 files changed

+349
-50
lines changed

11 files changed

+349
-50
lines changed

clang-tools-extra/clangd/ClangdLSPServer.cpp

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -856,30 +856,53 @@ void ClangdLSPServer::onPrepareRename(const TextDocumentPositionParams &Params,
856856
});
857857
}
858858

859+
/// Validate that `Edits` are valid and form a `WorkspaceEdit` that contains
860+
/// the edits as its `changes`.
861+
static llvm::Expected<WorkspaceEdit>
862+
formWorkspaceEdit(const FileEdits &Edits, const ClangdServer &Server) {
863+
if (auto Err = validateEdits(Server, Edits))
864+
return std::move(Err);
865+
WorkspaceEdit Result;
866+
// FIXME: use documentChanges if SupportDocumentChanges is true.
867+
Result.changes.emplace();
868+
for (const auto &Rep : Edits) {
869+
(*Result.changes)[URI::createFile(Rep.first()).toString()] =
870+
Rep.second.asTextEdits();
871+
}
872+
return Result;
873+
}
874+
859875
void ClangdLSPServer::onRename(const RenameParams &Params,
860876
Callback<WorkspaceEdit> Reply) {
861877
Path File = std::string(Params.textDocument.uri.file());
862878
if (!Server->getDraft(File))
863879
return Reply(llvm::make_error<LSPError>(
864880
"onRename called for non-added file", ErrorCode::InvalidParams));
881+
auto Callback = [Reply = std::move(Reply),
882+
this](llvm::Expected<RenameResult> R) mutable {
883+
if (!R)
884+
return Reply(R.takeError());
885+
llvm::Expected<WorkspaceEdit> WorkspaceEdit =
886+
formWorkspaceEdit(R->GlobalChanges, *Server);
887+
Reply(std::move(WorkspaceEdit));
888+
};
865889
Server->rename(File, Params.position, Params.newName, Opts.Rename,
866-
[File, Params, Reply = std::move(Reply),
867-
this](llvm::Expected<RenameResult> R) mutable {
868-
if (!R)
869-
return Reply(R.takeError());
870-
if (auto Err = validateEdits(*Server, R->GlobalChanges))
871-
return Reply(std::move(Err));
872-
WorkspaceEdit Result;
873-
// FIXME: use documentChanges if SupportDocumentChanges is
874-
// true.
875-
Result.changes.emplace();
876-
for (const auto &Rep : R->GlobalChanges) {
877-
(*Result
878-
.changes)[URI::createFile(Rep.first()).toString()] =
879-
Rep.second.asTextEdits();
880-
}
881-
Reply(Result);
882-
});
890+
std::move(Callback));
891+
}
892+
893+
void ClangdLSPServer::onIndexedRename(const IndexedRenameParams &Params,
894+
Callback<WorkspaceEdit> Reply) {
895+
auto Callback = [Reply = std::move(Reply),
896+
this](llvm::Expected<FileEdits> Edits) mutable {
897+
if (!Edits) {
898+
return Reply(Edits.takeError());
899+
}
900+
llvm::Expected<WorkspaceEdit> WorkspaceEdit =
901+
formWorkspaceEdit(*Edits, *Server);
902+
Reply(std::move(WorkspaceEdit));
903+
};
904+
Server->indexedRename(Params.positions, Params.textDocument.uri.file(),
905+
Params.oldName, Params.newName, std::move(Callback));
883906
}
884907

885908
void ClangdLSPServer::onDocumentDidClose(
@@ -1635,6 +1658,7 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
16351658
Bind.method("textDocument/switchSourceHeader", this, &ClangdLSPServer::onSwitchSourceHeader);
16361659
Bind.method("textDocument/prepareRename", this, &ClangdLSPServer::onPrepareRename);
16371660
Bind.method("textDocument/rename", this, &ClangdLSPServer::onRename);
1661+
Bind.method("workspace/indexedRename", this, &ClangdLSPServer::onIndexedRename);
16381662
Bind.method("textDocument/hover", this, &ClangdLSPServer::onHover);
16391663
Bind.method("textDocument/documentSymbol", this, &ClangdLSPServer::onDocumentSymbol);
16401664
Bind.method("workspace/executeCommand", this, &ClangdLSPServer::onCommand);

clang-tools-extra/clangd/ClangdLSPServer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
136136
void onPrepareRename(const TextDocumentPositionParams &,
137137
Callback<PrepareRenameResult>);
138138
void onRename(const RenameParams &, Callback<WorkspaceEdit>);
139+
void onIndexedRename(const IndexedRenameParams &, Callback<WorkspaceEdit>);
139140
void onHover(const TextDocumentPositionParams &,
140141
Callback<std::optional<Hover>>);
141142
void onPrepareTypeHierarchy(const TypeHierarchyPrepareParams &,

clang-tools-extra/clangd/ClangdServer.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,50 @@ void ClangdServer::rename(PathRef File, Position Pos, llvm::StringRef NewName,
624624
WorkScheduler->runWithAST("Rename", File, std::move(Action));
625625
}
626626

627+
void ClangdServer::indexedRename(
628+
const std::map<URIForFile, std::vector<Position>> &Positions,
629+
PathRef PrimaryFile, llvm::StringRef OldName, llvm::StringRef NewName,
630+
Callback<FileEdits> CB) {
631+
ParseInputs Inputs;
632+
Inputs.TFS = &TFS;
633+
Inputs.CompileCommand = CDB.getCompileCommand(PrimaryFile)
634+
.value_or(CDB.getFallbackCommand(PrimaryFile));
635+
IgnoreDiagnostics IgnoreDiags;
636+
std::unique_ptr<CompilerInvocation> CI =
637+
buildCompilerInvocation(Inputs, IgnoreDiags);
638+
if (!CI) {
639+
return CB(llvm::make_error<llvm::StringError>(
640+
"Unable to get compiler arguments for primary file",
641+
llvm::inconvertibleErrorCode()));
642+
}
643+
const LangOptions &LangOpts = CI->getLangOpts();
644+
645+
tooling::SymbolName OldSymbolName(OldName, LangOpts);
646+
tooling::SymbolName NewSymbolName(NewName, LangOpts);
647+
648+
llvm::StringMap<std::vector<Range>> FilesToRanges;
649+
for (auto Entry : Positions) {
650+
std::vector<Range> &Ranges = FilesToRanges[Entry.first.file()];
651+
for (Position Pos : Entry.second) {
652+
// Compute the range for the given position:
653+
// - If the old name is a simple identifier, we can add its length to the
654+
// start position's column because identifiers can't contain newlines
655+
// - If we have a multi-piece symbol name, them `editsForLocations` will
656+
// only look at the start of the range to call
657+
// `findObjCSymbolSelectorPieces`. It is thus fine to use an empty
658+
// range that points to the symbol's start.
659+
Position End = Pos;
660+
if (std::optional<std::string> Identifier =
661+
OldSymbolName.getSinglePiece()) {
662+
End.line += Identifier->size();
663+
}
664+
Ranges.push_back({Pos, End});
665+
}
666+
}
667+
CB(editsForLocations(FilesToRanges, OldSymbolName, NewSymbolName,
668+
*getHeaderFS().view(std::nullopt), LangOpts));
669+
}
670+
627671
// May generate several candidate selections, due to SelectionTree ambiguity.
628672
// vector of pointers because GCC doesn't like non-copyable Selection.
629673
static llvm::Expected<std::vector<std::unique_ptr<Tweak::Selection>>>

clang-tools-extra/clangd/ClangdServer.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,17 @@ class ClangdServer {
341341
void rename(PathRef File, Position Pos, llvm::StringRef NewName,
342342
const RenameOptions &Opts, Callback<RenameResult> CB);
343343

344+
/// Rename all occurrences of a symbol named `OldName` to `NewName` at the
345+
/// given `Positions`.
346+
///
347+
/// `PrimaryFile` is used to determine the language options for the symbol to
348+
/// rename, eg. to decide whether `OldName` and `NewName` are Objective-C
349+
/// selectors or normal identifiers.
350+
void
351+
indexedRename(const std::map<URIForFile, std::vector<Position>> &Positions,
352+
PathRef PrimaryFile, llvm::StringRef OldName,
353+
llvm::StringRef NewName, Callback<FileEdits> CB);
354+
344355
struct TweakRef {
345356
std::string ID; /// ID to pass for applyTweak.
346357
std::string Title; /// A single-line message to show in the UI.

clang-tools-extra/clangd/Protocol.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,15 @@ bool fromJSON(const llvm::json::Value &Params, RenameParams &R,
11921192
O.map("position", R.position) && O.map("newName", R.newName);
11931193
}
11941194

1195+
bool fromJSON(const llvm::json::Value &Params,
1196+
IndexedRenameParams &IndexedRename, llvm::json::Path P) {
1197+
llvm::json::ObjectMapper O(Params, P);
1198+
return O && O.map("textDocument", IndexedRename.textDocument) &&
1199+
O.map("oldName", IndexedRename.oldName) &&
1200+
O.map("newName", IndexedRename.newName) &&
1201+
O.map("positions", IndexedRename.positions);
1202+
}
1203+
11951204
llvm::json::Value toJSON(const DocumentHighlight &DH) {
11961205
return llvm::json::Object{
11971206
{"range", toJSON(DH.range)},

clang-tools-extra/clangd/Protocol.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,23 @@ struct URIForFile {
128128
llvm::json::Value toJSON(const URIForFile &U);
129129
bool fromJSON(const llvm::json::Value &, URIForFile &, llvm::json::Path);
130130

131+
template <typename T>
132+
bool fromJSON(const llvm::json::Value &E, std::map<URIForFile, T> &Out,
133+
llvm::json::Path P) {
134+
if (auto *O = E.getAsObject()) {
135+
Out.clear();
136+
for (const auto &KV : *O) {
137+
URIForFile URI;
138+
fromJSON(llvm::json::Value(KV.first), URI, P);
139+
if (!fromJSON(KV.second, Out[URI], P.field(KV.first)))
140+
return false;
141+
}
142+
return true;
143+
}
144+
P.report("expected object");
145+
return false;
146+
}
147+
131148
struct TextDocumentIdentifier {
132149
/// The text document's URI.
133150
URIForFile uri;
@@ -1447,6 +1464,37 @@ struct RenameParams {
14471464
};
14481465
bool fromJSON(const llvm::json::Value &, RenameParams &, llvm::json::Path);
14491466

1467+
/// Rename all occurrences of a symbol named `oldName` to `newName` at the
1468+
/// given `positions`.
1469+
///
1470+
/// The use case of this method is for when the positions to rename are already
1471+
/// known, eg. from an index lookup outside of clangd's built-in index. In
1472+
/// particular, it determines the edits necessary to rename multi-piece
1473+
/// Objective-C selector names.
1474+
///
1475+
/// `textDocument` is used to determine the language options for the symbol to
1476+
/// rename, eg. to decide whether `oldName` and `newName` are Objective-C
1477+
/// selectors or normal identifiers.
1478+
///
1479+
/// This is a clangd extension.
1480+
struct IndexedRenameParams {
1481+
/// The document in which the declaration to rename is declared. Its compiler
1482+
/// arguments are used to infer language settings for the rename.
1483+
TextDocumentIdentifier textDocument;
1484+
1485+
/// The old name of the symbol.
1486+
std::string oldName;
1487+
1488+
/// The new name of the symbol.
1489+
std::string newName;
1490+
1491+
/// The positions at which the symbol is known to appear and that should be
1492+
/// renamed.
1493+
std::map<URIForFile, std::vector<Position>> positions;
1494+
};
1495+
bool fromJSON(const llvm::json::Value &, IndexedRenameParams &,
1496+
llvm::json::Path);
1497+
14501498
enum class DocumentHighlightKind { Text = 1, Read = 2, Write = 3 };
14511499

14521500
/// A document highlight is a range inside a text document which deserves

clang-tools-extra/clangd/refactor/Rename.cpp

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -702,40 +702,9 @@ renameOutsideFile(const NamedDecl &RenameDecl, llvm::StringRef MainFilePath,
702702
Index, MaxLimitFiles);
703703
if (!AffectedFiles)
704704
return AffectedFiles.takeError();
705-
FileEdits Results;
706-
for (auto &FileAndOccurrences : *AffectedFiles) {
707-
llvm::StringRef FilePath = FileAndOccurrences.first();
708705

709-
auto ExpBuffer = FS.getBufferForFile(FilePath);
710-
if (!ExpBuffer) {
711-
elog("Fail to read file content: Fail to open file {0}: {1}", FilePath,
712-
ExpBuffer.getError().message());
713-
continue;
714-
}
715-
716-
auto AffectedFileCode = (*ExpBuffer)->getBuffer();
717-
syntax::UnexpandedTokenBuffer Tokens(
718-
AffectedFileCode, RenameDecl.getASTContext().getLangOpts());
719-
std::optional<std::vector<Range>> RenameRanges =
720-
adjustRenameRanges(Tokens, OldName.getNamePieces().front(),
721-
std::move(FileAndOccurrences.second));
722-
if (!RenameRanges) {
723-
// Our heuristics fails to adjust rename ranges to the current state of
724-
// the file, it is most likely the index is stale, so we give up the
725-
// entire rename.
726-
return error("Index results don't match the content of file {0} "
727-
"(the index may be stale)",
728-
FilePath);
729-
}
730-
auto RenameEdit = buildRenameEdit(FilePath, AffectedFileCode, *RenameRanges,
731-
OldName, NewName, Tokens);
732-
if (!RenameEdit)
733-
return error("failed to rename in file {0}: {1}", FilePath,
734-
RenameEdit.takeError());
735-
if (!RenameEdit->Replacements.empty())
736-
Results.insert({FilePath, std::move(*RenameEdit)});
737-
}
738-
return Results;
706+
return editsForLocations(*AffectedFiles, OldName, NewName, FS,
707+
RenameDecl.getASTContext().getLangOpts());
739708
}
740709

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

779748
} // namespace
780749

750+
llvm::Expected<FileEdits>
751+
editsForLocations(const llvm::StringMap<std::vector<Range>> &Ranges,
752+
const SymbolName &OldName, const SymbolName &NewName,
753+
llvm::vfs::FileSystem &FS, const LangOptions &LangOpts) {
754+
FileEdits Results;
755+
for (auto &FileAndOccurrences : Ranges) {
756+
llvm::StringRef FilePath = FileAndOccurrences.first();
757+
758+
auto ExpBuffer = FS.getBufferForFile(FilePath);
759+
if (!ExpBuffer) {
760+
elog("Fail to read file content: Fail to open file {0}: {1}", FilePath,
761+
ExpBuffer.getError().message());
762+
continue;
763+
}
764+
765+
auto AffectedFileCode = (*ExpBuffer)->getBuffer();
766+
syntax::UnexpandedTokenBuffer Tokens(AffectedFileCode, LangOpts);
767+
std::optional<std::vector<Range>> RenameRanges =
768+
adjustRenameRanges(Tokens, OldName.getNamePieces().front(),
769+
std::move(FileAndOccurrences.second));
770+
if (!RenameRanges) {
771+
// Our heuristics fails to adjust rename ranges to the current state of
772+
// the file, it is most likely the index is stale, so we give up the
773+
// entire rename.
774+
elog("Index results don't match the content of file {0} (the index may "
775+
"be stale)",
776+
FilePath);
777+
continue;
778+
}
779+
auto RenameEdit = buildRenameEdit(FilePath, AffectedFileCode, *RenameRanges,
780+
OldName, NewName, Tokens);
781+
if (!RenameEdit)
782+
return error("failed to rename in file {0}: {1}", FilePath,
783+
RenameEdit.takeError());
784+
if (!RenameEdit->Replacements.empty())
785+
Results.insert({FilePath, std::move(*RenameEdit)});
786+
}
787+
return Results;
788+
}
789+
781790
llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
782791
assert(!RInputs.Index == !RInputs.FS &&
783792
"Index and FS must either both be specified or both null.");

clang-tools-extra/clangd/refactor/Rename.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ struct RenameInputs {
5555
RenameOptions Opts = {};
5656
};
5757

58+
/// Compute the edits that need to be applied to rename symbols in `Ranges` from
59+
/// `OldName` to `NewName`. The key of `Ranges` is the file path of the file in
60+
/// which the range resides.
61+
///
62+
/// If `OldName` and `NewName` are single-piece identifiers, this just creates
63+
/// edits to change the ranges to `NewName`.
64+
///
65+
/// If `OldName` and `NewName` are multi-piece Objective-C selectors, only the
66+
/// start of the ranges is considered and the file is lexed to find the argument
67+
/// labels of the selector to rename.
68+
llvm::Expected<FileEdits>
69+
editsForLocations(const llvm::StringMap<std::vector<Range>> &Ranges,
70+
const tooling::SymbolName &OldName,
71+
const tooling::SymbolName &NewName, llvm::vfs::FileSystem &FS,
72+
const LangOptions &LangOpts);
73+
5874
struct RenameResult {
5975
/// The range of the symbol that the user can attempt to rename.
6076
Range Target;

0 commit comments

Comments
 (0)