hokein created this revision. hokein added reviewers: ilya-biryukov, sammccall. Herald added subscribers: usaxena95, kadircet, arphaman, jkorous, MaskRay. Herald added a project: clang.
This is the initial version. The cross-file rename is purely based on the index (plus a text-match verification). It is hidden under a command-line flag, and only available for a small set of symbols. Repository: rG LLVM Github Monorepo https://reviews.llvm.org/D69263 Files: clang-tools-extra/clangd/ClangdLSPServer.cpp clang-tools-extra/clangd/ClangdServer.cpp clang-tools-extra/clangd/ClangdServer.h clang-tools-extra/clangd/SourceCode.h clang-tools-extra/clangd/index/SymbolCollector.cpp clang-tools-extra/clangd/refactor/Rename.cpp clang-tools-extra/clangd/refactor/Rename.h clang-tools-extra/clangd/refactor/Tweak.h clang-tools-extra/clangd/tool/ClangdMain.cpp clang-tools-extra/clangd/unittests/RenameTests.cpp clang-tools-extra/clangd/unittests/SyncAPI.cpp clang-tools-extra/clangd/unittests/SyncAPI.h clang/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp
Index: clang/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp =================================================================== --- clang/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp +++ clang/lib/Tooling/Refactoring/Rename/USRFindingAction.cpp @@ -41,9 +41,10 @@ const NamedDecl *getCanonicalSymbolDeclaration(const NamedDecl *FoundDecl) { // If FoundDecl is a constructor or destructor, we want to instead take // the Decl of the corresponding class. - if (const auto *CtorDecl = dyn_cast<CXXConstructorDecl>(FoundDecl)) + if (const auto *CtorDecl = dyn_cast_or_null<CXXConstructorDecl>(FoundDecl)) FoundDecl = CtorDecl->getParent(); - else if (const auto *DtorDecl = dyn_cast<CXXDestructorDecl>(FoundDecl)) + else if (const auto *DtorDecl = + dyn_cast_or_null<CXXDestructorDecl>(FoundDecl)) FoundDecl = DtorDecl->getParent(); // FIXME: (Alex L): Canonicalize implicit template instantions, just like // the indexer does it. Index: clang-tools-extra/clangd/unittests/SyncAPI.h =================================================================== --- clang-tools-extra/clangd/unittests/SyncAPI.h +++ clang-tools-extra/clangd/unittests/SyncAPI.h @@ -38,8 +38,8 @@ llvm::Expected<std::vector<DocumentHighlight>> runFindDocumentHighlights(ClangdServer &Server, PathRef File, Position Pos); -llvm::Expected<std::vector<TextEdit>> -runRename(ClangdServer &Server, PathRef File, Position Pos, StringRef NewName); +llvm::Expected<FileEdits> runRename(ClangdServer &Server, PathRef File, + Position Pos, StringRef NewName); std::string runDumpAST(ClangdServer &Server, PathRef File); Index: clang-tools-extra/clangd/unittests/SyncAPI.cpp =================================================================== --- clang-tools-extra/clangd/unittests/SyncAPI.cpp +++ clang-tools-extra/clangd/unittests/SyncAPI.cpp @@ -96,10 +96,9 @@ return std::move(*Result); } -llvm::Expected<std::vector<TextEdit>> runRename(ClangdServer &Server, - PathRef File, Position Pos, - llvm::StringRef NewName) { - llvm::Optional<llvm::Expected<std::vector<TextEdit>>> Result; +llvm::Expected<FileEdits> runRename(ClangdServer &Server, PathRef File, + Position Pos, llvm::StringRef NewName) { + llvm::Optional<llvm::Expected<FileEdits>> Result; Server.rename(File, Pos, NewName, /*WantFormat=*/true, capture(Result)); return std::move(*Result); } Index: clang-tools-extra/clangd/unittests/RenameTests.cpp =================================================================== --- clang-tools-extra/clangd/unittests/RenameTests.cpp +++ clang-tools-extra/clangd/unittests/RenameTests.cpp @@ -22,6 +22,19 @@ return replacementToEdit(Code, arg).range == Range; } +RenameInputs localRenameInputs(const Annotations &Code, llvm::StringRef NewName, + llvm::StringRef Path, + const SymbolIndex *Index = nullptr) { + RenameInputs Inputs; + Inputs.Pos = Code.point(); + Inputs.MainFileCode = Code.code(); + Inputs.MainFilePath = Path; + Inputs.NewName = NewName; + Inputs.Index = Index; + Inputs.AllowCrossFile = false; + return Inputs; +} + TEST(RenameTest, SingleFile) { struct Test { const char* Before; @@ -80,10 +93,13 @@ auto TU = TestTU::withCode(Code.code()); auto AST = TU.build(); auto RenameResult = - renameWithinFile(AST, testPath(TU.Filename), Code.point(), "abcde"); + rename(AST, localRenameInputs(Code, "abcde", testPath(TU.Filename))); + ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError(); - auto ApplyResult = - tooling::applyAllReplacements(Code.code(), *RenameResult); + ASSERT_TRUE(RenameResult->size() == 1); + EXPECT_EQ(Code.code(), RenameResult->begin()->second.InitialCode); + auto ApplyResult = tooling::applyAllReplacements( + Code.code(), RenameResult->begin()->second.Replacements); ASSERT_TRUE(bool(ApplyResult)) << ApplyResult.takeError(); EXPECT_EQ(T.After, *ApplyResult) << T.Before; @@ -177,24 +193,27 @@ } auto AST = TU.build(); - auto Results = renameWithinFile(AST, testPath(TU.Filename), T.point(), - "dummyNewName", Case.Index); + auto Results = + rename(AST, localRenameInputs(T, "dummyNewName", testPath(TU.Filename), + Case.Index)); bool WantRename = true; if (T.ranges().empty()) WantRename = false; if (!WantRename) { assert(Case.ErrorMessage && "Error message must be set!"); - EXPECT_FALSE(Results) << "expected renameWithinFile returned an error: " - << T.code(); + EXPECT_FALSE(Results) + << "expected rename returned an error: " << T.code(); auto ActualMessage = llvm::toString(Results.takeError()); EXPECT_THAT(ActualMessage, testing::HasSubstr(Case.ErrorMessage)); } else { - EXPECT_TRUE(bool(Results)) << "renameWithinFile returned an error: " + EXPECT_TRUE(bool(Results)) << "rename returned an error: " << llvm::toString(Results.takeError()); + ASSERT_TRUE(Results->size() == 1); std::vector<testing::Matcher<tooling::Replacement>> Expected; for (const auto &R : T.ranges()) Expected.push_back(RenameRange(TU.Code, R)); - EXPECT_THAT(*Results, UnorderedElementsAreArray(Expected)); + EXPECT_THAT(Results->begin()->second.Replacements, + UnorderedElementsAreArray(Expected)); } } } Index: clang-tools-extra/clangd/tool/ClangdMain.cpp =================================================================== --- clang-tools-extra/clangd/tool/ClangdMain.cpp +++ clang-tools-extra/clangd/tool/ClangdMain.cpp @@ -263,6 +263,14 @@ CommaSeparated, }; +opt<bool> CrossFileRename{ + "cross-file-rename", + cat(Features), + desc("Enable cross-file rename fature. By default, " + "clangd only allows local rename."), + init(false), +}; + opt<unsigned> WorkerThreadsCount{ "j", cat(Misc), @@ -594,6 +602,7 @@ } Opts.StaticIndex = StaticIdx.get(); Opts.AsyncThreadsCount = WorkerThreadsCount; + Opts.CrossFileRename = CrossFileRename; clangd::CodeCompleteOptions CCOpts; CCOpts.IncludeIneligibleResults = IncludeIneligibleResults; Index: clang-tools-extra/clangd/refactor/Tweak.h =================================================================== --- clang-tools-extra/clangd/refactor/Tweak.h +++ clang-tools-extra/clangd/refactor/Tweak.h @@ -75,7 +75,7 @@ llvm::Optional<std::string> ShowMessage; /// A mapping from file path(the one used for accessing the underlying VFS) /// to edits. - llvm::StringMap<Edit> ApplyEdits; + FileEdits ApplyEdits; static Effect showMessage(StringRef S) { Effect E; Index: clang-tools-extra/clangd/refactor/Rename.h =================================================================== --- clang-tools-extra/clangd/refactor/Rename.h +++ clang-tools-extra/clangd/refactor/Rename.h @@ -10,6 +10,7 @@ #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_REFACTOR_RENAME_H #include "Protocol.h" +#include "SourceCode.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/Support/Error.h" @@ -18,13 +19,23 @@ class ParsedAST; class SymbolIndex; +struct RenameInputs { + Position Pos; // the position triggering the rename + llvm::StringRef NewName; + + llvm::StringRef MainFilePath; + llvm::StringRef MainFileCode; + + llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS; + const SymbolIndex *Index = nullptr; + + bool AllowCrossFile = false; // true if enable cross-file rename +}; + /// Renames all occurrences of the symbol at \p Pos to \p NewName. -/// Occurrences outside the current file are not modified. -/// Returns an error if rename a symbol that's used in another file (per the -/// index). -llvm::Expected<tooling::Replacements> -renameWithinFile(ParsedAST &AST, llvm::StringRef File, Position Pos, - llvm::StringRef NewName, const SymbolIndex *Index = nullptr); +/// If AllowCrossFile is false, returns an error if rename a symbol that's used +/// in another file (per the index). +llvm::Expected<FileEdits> rename(ParsedAST &AST, const RenameInputs &RInputs); } // namespace clangd } // namespace clang Index: clang-tools-extra/clangd/refactor/Rename.cpp =================================================================== --- clang-tools-extra/clangd/refactor/Rename.cpp +++ clang-tools-extra/clangd/refactor/Rename.cpp @@ -64,7 +64,7 @@ NoSymbolFound, NoIndexProvided, NonIndexable, - UsedOutsideFile, + UsedOutsideFile, // for within-file rename only. UnsupportedSymbol, }; @@ -116,6 +116,50 @@ return ReasonToReject::UsedOutsideFile; } +llvm::Optional<ReasonToReject> +renameableOutsideFile(const NamedDecl &RenameDecl, const SymbolIndex *Index) { + if (RenameDecl.getParentFunctionOrMethod()) + return None; // function-local symbols + + // Symbols in system headers are not renamable, as changing system headers is + // mostly undesired. + if (RenameDecl.getASTContext().getSourceManager().isInSystemHeader( + RenameDecl.getLocation())) + return ReasonToReject::UnsupportedSymbol; + + // Disable renaming all non-identifier symbols, e.g. coversion operators + // user-defined literals. + auto II = RenameDecl.getDeclName().getAsIdentifierInfo(); + if (!II || II->getName().empty()) + return ReasonToReject::UnsupportedSymbol; + + if (!Index) + return ReasonToReject::NoIndexProvided; + bool IsIndexable = SymbolCollector::shouldCollectSymbol( + RenameDecl, RenameDecl.getASTContext(), SymbolCollector::Options(), + /*IsMainFileSymbol=*/true); + if (!IsIndexable) + return ReasonToReject::NonIndexable; + + // FIXME: disable templates rename. When renaming templates, all related + // specializations need to be considered, which is not supported in our index. + if (RenameDecl.getDescribedTemplate()) + return ReasonToReject::UnsupportedSymbol; + + // A whitelist of renamable symbols. + if (llvm::isa<CXXRecordDecl>(RenameDecl)) + return None; + else if (const auto *S = llvm::dyn_cast<CXXMethodDecl>(&RenameDecl)) { + // FIXME: renaming virtual methods requires to rename all overridens in + // subclasses, which our index doesn't support yet. + if (!S->isVirtual()) + return None; + } else if (isa<FunctionDecl>(&RenameDecl)) { + return None; + } + return UnsupportedSymbol; +} + llvm::Error makeError(ReasonToReject Reason) { auto Message = [](ReasonToReject Reason) { switch (Reason) { @@ -156,28 +200,9 @@ return Result; } -} // namespace - llvm::Expected<tooling::Replacements> -renameWithinFile(ParsedAST &AST, llvm::StringRef File, Position Pos, - llvm::StringRef NewName, const SymbolIndex *Index) { - const SourceManager &SM = AST.getSourceManager(); - SourceLocation SourceLocationBeg = SM.getMacroArgExpandedLocation( - getBeginningOfIdentifier(Pos, SM, AST.getASTContext().getLangOpts())); - // FIXME: renaming macros is not supported yet, the macro-handling code should - // be moved to rename tooling library. - if (locateMacroAt(SourceLocationBeg, AST.getPreprocessor())) - return makeError(UnsupportedSymbol); - - const auto *RenameDecl = - tooling::getNamedDeclAt(AST.getASTContext(), SourceLocationBeg); - if (!RenameDecl) - return makeError(NoSymbolFound); - - if (auto Reject = - renamableWithinFile(*RenameDecl->getCanonicalDecl(), File, Index)) - return makeError(*Reject); - +renameWithinFile(ParsedAST &AST, const NamedDecl *RenameDecl, + llvm::StringRef NewName) { // Rename sometimes returns duplicate edits (which is a bug). A side-effect of // adding them to a single Replacements object is these are deduplicated. tooling::Replacements FilteredChanges; @@ -198,5 +223,184 @@ return FilteredChanges; } +// Return all rename occurrences (per the index) outside of the main file. +llvm::StringMap<std::vector<Range>> +findOccurrencesOutsideFile(const NamedDecl *RenameDecl, + llvm::StringRef MainFile, const SymbolIndex *Index) { + if (!Index) + return {}; + RefsRequest RQuest; + // FIXME: revisit the limit, and our index should have a way to inform + // us whether the ref results is completed. + RQuest.Limit = 100; + if (auto ID = getSymbolID(RenameDecl)) + RQuest.IDs.insert(*ID); + + // Helper. + auto ToRange = [](const SymbolLocation &L) { + Range R; + R.start.line = L.Start.line(); + R.start.character = L.Start.column(); + R.end.line = L.End.line(); + R.end.character = L.End.column(); + return R; + }; + llvm::StringMap<std::vector<Range>> + AffectedFiles; // absolute file path => rename ocurrences in that file. + Index->refs(RQuest, [&](const Ref &R) { + if (auto RefFilePath = filePath(R.Location, /*HintFilePath=*/MainFile)) { + if (*RefFilePath != MainFile) + AffectedFiles[*RefFilePath].push_back(ToRange(R.Location)); + } + }); + return AffectedFiles; +} + +// Run cross-file rename Per the index. +llvm::Expected<FileEdits> +renameOutsideFile(const NamedDecl *RenameDecl, llvm::StringRef MainFilePath, + llvm::StringRef NewName, const SymbolIndex *Index, + llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS) { + auto AffectedFiles = + findOccurrencesOutsideFile(RenameDecl, MainFilePath, Index); + // FIXME: make the limit customizable. + static constexpr size_t MaxLimitFiles = 50; + if (AffectedFiles.size() >= MaxLimitFiles) + return llvm::make_error<llvm::StringError>( + llvm::formatv( + "The number of affected files exceeds the max limit {0}: {1}", + MaxLimitFiles, AffectedFiles.size()), + llvm::inconvertibleErrorCode()); + + // Helpers. + auto ReadFileFromVfs = [&](llvm::StringRef FilePath) + -> llvm::Expected<std::unique_ptr<llvm::MemoryBuffer>> { + auto File = VFS.get()->openFileForRead(FilePath); + if (!File) + return llvm::errorCodeToError(File.getError()); + if (!*File) + return nullptr; + auto Stat = (*File)->status(); + if (!Stat) + return llvm::errorCodeToError(Stat.getError()); + + auto FileContent = File->get()->getBuffer(Stat->getName()); + if (!FileContent) + return llvm::errorCodeToError(FileContent.getError()); + return std::move(*FileContent); + }; + auto ToRangeOffset = + [](const clangd::Range &R, + llvm::StringRef Code) -> llvm::Expected<std::pair<size_t, size_t>> { + auto StartOffset = positionToOffset(Code, R.start); + if (!StartOffset) { + return StartOffset.takeError(); + } + auto EndOffset = positionToOffset(Code, R.end); + if (!EndOffset) { + return EndOffset.takeError(); + } + return std::make_pair<>(*StartOffset, *EndOffset); + }; + + // The cross-file rename is purely based on the index, as we don't want to + // build all ASTs for affected files, which may cause a performance hit. + // This is a correctness/performance tradeoff, we assume that the index is + // always update-to-date (or users should expect that the cross-file rename + // only works well when the bacground indexing is done). + // + // To mitigate the diverged issue of the index (e.g. xref may return implicit + // references which are non-eligible for rename), we do a post text-match + // verification, filtering out all non-matching occurrences. + FileEdits Results; + std::string OldName = RenameDecl->getNameAsString(); + for (const auto &FileAndOccurrences : AffectedFiles) { + llvm::StringRef FilePath = FileAndOccurrences.first(); + + auto Content = ReadFileFromVfs(FilePath); + if (!Content) { + elog("Fail to read file {0}: {1}", FilePath, Content.takeError()); + continue; + } + if (!*Content) + continue; + llvm::StringRef AffectedFileCode = (*Content)->getBuffer(); + + tooling::Replacements RenameEdit; + + // Now we do a text-match verification. + for (const Range &Occurrence : FileAndOccurrences.getValue()) { + // !positionToOffset is O(N), it is okay at the moment since we only + // process at most 100 references. + auto RangeOffset = ToRangeOffset(Occurrence, AffectedFileCode); + if (!RangeOffset) { + elog("Error on transfer range to offset: {0} ", + RangeOffset.takeError()); + continue; + } + auto ByteLength = RangeOffset->second - RangeOffset->first; + // FIXME: be more tolerant with the dirty buffer. + if (OldName != AffectedFileCode.substr(RangeOffset->first, ByteLength)) + continue; + + if (auto Err = RenameEdit.add(tooling::Replacement( + AffectedFileCode, RangeOffset->first, ByteLength, NewName))) + elog("Failed to add replacements: {0}", std::move(Err)); + } + if (!RenameEdit.empty()) + Results.try_emplace(FilePath, AffectedFileCode, std::move(RenameEdit)); + } + return Results; +} + +} // namespace + +llvm::Expected<FileEdits> rename(ParsedAST &AST, const RenameInputs &RInputs) { + const SourceManager &SM = AST.getSourceManager(); + SourceLocation SourceLocationBeg = + SM.getMacroArgExpandedLocation(getBeginningOfIdentifier( + RInputs.Pos, SM, AST.getASTContext().getLangOpts())); + // FIXME: renaming macros is not supported yet, the macro-handling code should + // be moved to rename tooling library. + if (locateMacroAt(SourceLocationBeg, AST.getPreprocessor())) + return makeError(UnsupportedSymbol); + + const NamedDecl *RenameDecl = tooling::getCanonicalSymbolDeclaration( + tooling::getNamedDeclAt(AST.getASTContext(), SourceLocationBeg)); + if (!RenameDecl) + return makeError(NoSymbolFound); + + if (!RInputs.AllowCrossFile) { + // Handle within-file rename. + auto Reject = renamableWithinFile(*RenameDecl->getCanonicalDecl(), + RInputs.MainFilePath, RInputs.Index); + if (Reject) + return makeError(*Reject); + auto MainFileRenameEdit = + renameWithinFile(AST, RenameDecl, RInputs.NewName); + if (!MainFileRenameEdit) + return MainFileRenameEdit.takeError(); + FileEdits Results; + Results.try_emplace(RInputs.MainFilePath, RInputs.MainFileCode, + std::move(*MainFileRenameEdit)); + return Results; + } + + // Handle cross-file rename. + if (auto Reject = renameableOutsideFile(*RenameDecl, RInputs.Index)) + return makeError(*Reject); + auto Results = renameOutsideFile(RenameDecl, RInputs.MainFilePath, + RInputs.NewName, RInputs.Index, RInputs.VFS); + if (!Results) + return Results.takeError(); + auto MainFileRenameEdit = renameWithinFile(AST, RenameDecl, RInputs.NewName); + if (!MainFileRenameEdit) + // FIXME: should we report the error to client? + return MainFileRenameEdit.takeError(); + Results->try_emplace(RInputs.MainFilePath, RInputs.MainFileCode, + std::move(*MainFileRenameEdit)); + return Results; +} + } // namespace clangd } // namespace clang Index: clang-tools-extra/clangd/index/SymbolCollector.cpp =================================================================== --- clang-tools-extra/clangd/index/SymbolCollector.cpp +++ clang-tools-extra/clangd/index/SymbolCollector.cpp @@ -263,10 +263,6 @@ Decl::FriendObjectKind::FOK_None) && !(Roles & static_cast<unsigned>(index::SymbolRole::Definition))) return true; - // Skip non-semantic references, we should start processing these when we - // decide to implement renaming with index support. - if ((Roles & static_cast<unsigned>(index::SymbolRole::NameReference))) - return true; // A declaration created for a friend declaration should not be used as the // canonical declaration in the index. Use OrigD instead, unless we've already // picked a replacement for D Index: clang-tools-extra/clangd/SourceCode.h =================================================================== --- clang-tools-extra/clangd/SourceCode.h +++ clang-tools-extra/clangd/SourceCode.h @@ -223,6 +223,7 @@ /// Checks whether the Replacements are applicable to given Code. bool canApplyTo(llvm::StringRef Code) const; }; +using FileEdits = llvm::StringMap<Edit>; /// Formats the edits and code around it according to Style. Changes /// Replacements to formatted ones if succeeds. Index: clang-tools-extra/clangd/ClangdServer.h =================================================================== --- clang-tools-extra/clangd/ClangdServer.h +++ clang-tools-extra/clangd/ClangdServer.h @@ -23,6 +23,7 @@ #include "index/Background.h" #include "index/FileIndex.h" #include "index/Index.h" +#include "refactor/Rename.h" #include "refactor/Tweak.h" #include "clang/Tooling/CompilationDatabase.h" #include "clang/Tooling/Core/Replacement.h" @@ -132,6 +133,9 @@ /// Enable semantic highlighting features. bool SemanticHighlighting = false; + /// Enable cross-file rename feature. + bool CrossFileRename = false; + /// Returns true if the tweak should be enabled. std::function<bool(const Tweak &)> TweakFilter = [](const Tweak &T) { return !T.hidden(); // only enable non-hidden tweaks. @@ -251,7 +255,7 @@ /// embedders could use this method to get all occurrences of the symbol (e.g. /// highlighting them in prepare stage). void rename(PathRef File, Position Pos, llvm::StringRef NewName, - bool WantFormat, Callback<std::vector<TextEdit>> CB); + bool WantFormat, Callback<FileEdits> CB); struct TweakRef { std::string ID; /// ID to pass for applyTweak. @@ -326,6 +330,9 @@ // can be caused by missing includes (e.g. member access in incomplete type). bool SuggestMissingIncludes = false; + // If this is true, enable cross-file rename. + bool CrossFileRename = false; + std::function<bool(const Tweak &)> TweakFilter; // GUARDED_BY(CachedCompletionFuzzyFindRequestMutex) Index: clang-tools-extra/clangd/ClangdServer.cpp =================================================================== --- clang-tools-extra/clangd/ClangdServer.cpp +++ clang-tools-extra/clangd/ClangdServer.cpp @@ -119,7 +119,8 @@ : nullptr), GetClangTidyOptions(Opts.GetClangTidyOptions), SuggestMissingIncludes(Opts.SuggestMissingIncludes), - TweakFilter(Opts.TweakFilter), WorkspaceRoot(Opts.WorkspaceRoot), + CrossFileRename(Opts.CrossFileRename), TweakFilter(Opts.TweakFilter), + WorkspaceRoot(Opts.WorkspaceRoot), // Pass a callback into `WorkScheduler` to extract symbols from a newly // parsed file and rebuild the file index synchronously each time an AST // is parsed. @@ -308,16 +309,28 @@ if (!InpAST) return CB(InpAST.takeError()); auto &AST = InpAST->AST; - // Performing the rename isn't substantially more expensive than doing an - // AST-based check, so we just rename and throw away the results. We may - // have to revisit this when we support cross-file rename. - auto Changes = renameWithinFile(AST, File, Pos, "dummy", Index); - if (!Changes) { - // LSP says to return null on failure, but that will result in a generic - // failure message. If we send an LSP error response, clients can surface - // the message to users (VSCode does). - return CB(Changes.takeError()); + // FIXME: for cross-file rename, we presume prepareRename always succeeds, + // revisit this strategy. + if (!CrossFileRename) { + // Performing the local rename isn't substantially more expensive than + // doing an AST-based check, so we just rename and throw away the results. + RenameInputs RInputs; + RInputs.MainFileCode = InpAST->Inputs.Contents; + RInputs.MainFilePath = File; + RInputs.NewName = "dummy"; + RInputs.Pos = Pos; + RInputs.AllowCrossFile = false; + RInputs.Index = Index; + RInputs.VFS = FSProvider.getFileSystem(); + auto Changes = clangd::rename(AST, RInputs); // within-file rename. + if (!Changes) { + // LSP says to return null on failure, but that will result in a generic + // failure message. If we send an LSP error response, clients can + // surface the message to users (VSCode does). + return CB(Changes.takeError()); + } } + SourceLocation Loc = getBeginningOfIdentifier( Pos, AST.getSourceManager(), AST.getASTContext().getLangOpts()); if (auto Range = getTokenRange(AST.getSourceManager(), @@ -330,32 +343,34 @@ } void ClangdServer::rename(PathRef File, Position Pos, llvm::StringRef NewName, - bool WantFormat, Callback<std::vector<TextEdit>> CB) { + bool WantFormat, Callback<FileEdits> CB) { auto Action = [File = File.str(), NewName = NewName.str(), Pos, WantFormat, CB = std::move(CB), this](llvm::Expected<InputsAndAST> InpAST) mutable { if (!InpAST) return CB(InpAST.takeError()); - auto Changes = renameWithinFile(InpAST->AST, File, Pos, NewName, Index); - if (!Changes) - return CB(Changes.takeError()); + RenameInputs RInputs; + RInputs.MainFileCode = InpAST->Inputs.Contents; + RInputs.MainFilePath = File; + RInputs.NewName = NewName; + RInputs.Pos = Pos; + RInputs.AllowCrossFile = CrossFileRename; + RInputs.Index = Index; + RInputs.VFS = FSProvider.getFileSystem(); + auto Edits = clangd::rename(InpAST->AST, RInputs); + if (!Edits) + return CB(Edits.takeError()); if (WantFormat) { auto Style = getFormatStyleForFile(File, InpAST->Inputs.Contents, InpAST->Inputs.FS.get()); - if (auto Formatted = - cleanupAndFormat(InpAST->Inputs.Contents, *Changes, Style)) - *Changes = std::move(*Formatted); - else - elog("Failed to format replacements: {0}", Formatted.takeError()); + for (auto &E : *Edits) { + if (auto Err = reformatEdit(E.getValue(), Style)) + elog("Failed to format replacements: {0}", std::move(Err)); + } } - - std::vector<TextEdit> Edits; - for (const auto &Rep : *Changes) - Edits.push_back(replacementToEdit(InpAST->Inputs.Contents, Rep)); - return CB(std::move(Edits)); + CB(std::move(*Edits)); }; - WorkScheduler.runWithAST("Rename", File, std::move(Action)); } Index: clang-tools-extra/clangd/ClangdLSPServer.cpp =================================================================== --- clang-tools-extra/clangd/ClangdLSPServer.cpp +++ clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -106,10 +106,10 @@ // Makes sure edits in \p E are applicable to latest file contents reported by // editor. If not generates an error message containing information about files // that needs to be saved. -llvm::Error validateEdits(const DraftStore &DraftMgr, const Tweak::Effect &E) { +llvm::Error validateEdits(const DraftStore &DraftMgr, const FileEdits &FE) { size_t InvalidFileCount = 0; llvm::StringRef LastInvalidFile; - for (const auto &It : E.ApplyEdits) { + for (const auto &It : FE) { if (auto Draft = DraftMgr.getDraft(It.first())) { // If the file is open in user's editor, make sure the version we // saw and current version are compatible as this is the text that @@ -704,7 +704,7 @@ if (R->ApplyEdits.empty()) return Reply("Tweak applied."); - if (auto Err = validateEdits(DraftMgr, *R)) + if (auto Err = validateEdits(DraftMgr, R->ApplyEdits)) return Reply(std::move(Err)); WorkspaceEdit WE; @@ -758,17 +758,23 @@ if (!Code) return Reply(llvm::make_error<LSPError>( "onRename called for non-added file", ErrorCode::InvalidParams)); - - Server->rename(File, Params.position, Params.newName, /*WantFormat=*/true, - [File, Code, Params, Reply = std::move(Reply)]( - llvm::Expected<std::vector<TextEdit>> Edits) mutable { - if (!Edits) - return Reply(Edits.takeError()); - - WorkspaceEdit WE; - WE.changes = {{Params.textDocument.uri.uri(), *Edits}}; - Reply(WE); - }); + Server->rename( + File, Params.position, Params.newName, + /*WantFormat=*/true, + [File, Code, Params, Reply = std::move(Reply), + this](llvm::Expected<FileEdits> Edits) mutable { + if (!Edits) + return Reply(Edits.takeError()); + if (auto Err = validateEdits(DraftMgr, *Edits)) + return Reply(std::move(Err)); + WorkspaceEdit Result; + Result.changes.emplace(); + for (const auto &Rep : *Edits) { + (*Result.changes)[URI::createFile(Rep.first()).toString()] = + Rep.second.asTextEdits(); + } + Reply(Result); + }); } void ClangdLSPServer::onDocumentDidClose(
_______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits