From 12fd21a88015d3e6e89bfeb8dd16b271e163da4a Mon Sep 17 00:00:00 2001 From: Christian Kandeler Date: Tue, 18 May 2021 12:59:15 +0200 Subject: [PATCH] ClangCodeModel: Implement global renaming via clangd Note that we do not use the LSP rename functionality. We do "manual" renaming the same way as in the built-in code model, but based on the references found by clangd. Change-Id: Ifa5597efe5c89c8f9204a4f5323bc755544696cf Reviewed-by: Qt CI Bot Reviewed-by: David Schulz --- src/plugins/clangcodemodel/clangdclient.cpp | 131 +++++++++++++++--- src/plugins/clangcodemodel/clangdclient.h | 4 +- .../clangcodemodel/clangrefactoringengine.cpp | 19 ++- .../clangcodemodel/clangrefactoringengine.h | 4 +- .../clangcodemodel/test/clangdtests.cpp | 2 +- src/plugins/cppeditor/cppeditorwidget.cpp | 3 +- src/plugins/cpptools/cppfindreferences.cpp | 95 ++++++------- src/plugins/cpptools/cppfindreferences.h | 3 + src/plugins/cpptools/cppmodelmanager.cpp | 2 +- 9 files changed, 186 insertions(+), 77 deletions(-) diff --git a/src/plugins/clangcodemodel/clangdclient.cpp b/src/plugins/clangcodemodel/clangdclient.cpp index 4c158055406..0f477fd8111 100644 --- a/src/plugins/clangcodemodel/clangdclient.cpp +++ b/src/plugins/clangcodemodel/clangdclient.cpp @@ -32,7 +32,12 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -42,6 +47,7 @@ using namespace CPlusPlus; using namespace Core; using namespace LanguageClient; using namespace LanguageServerProtocol; +using namespace ProjectExplorer; namespace ClangCodeModel { namespace Internal { @@ -296,15 +302,20 @@ public: QString fileContent; AstNode ast; }; +class ReplacementData { +public: + QString oldSymbolName; + QString newSymbolName; + QSet fileRenameCandidates; +}; class ReferencesData { public: - void setCanceled() { search->setUserData(true); } - bool isCanceled() const { return search && search->userData().toBool(); } - QMap fileData; QList pendingAstRequests; QPointer search; + Utils::optional replacementData; quint64 key; + bool canceled = false; }; class ClangdClient::Private @@ -313,9 +324,14 @@ public: Private(ClangdClient *q) : q(q) {} void handleFindUsagesResult(quint64 key, const QList &locations); - void addSearchResultsForFile(const ReferencesData &refData, const Utils::FilePath &file, + static void handleRenameRequest(const SearchResult *search, + const ReplacementData &replacementData, + const QString &newSymbolName, + const QList &checkedItems, + bool preserveCase); + void addSearchResultsForFile(ReferencesData &refData, const Utils::FilePath &file, const ReferencesFileData &fileData); - void reportAllSearchResultsAndFinish(const ReferencesData &data); + void reportAllSearchResultsAndFinish(ReferencesData &data); void finishSearch(const ReferencesData &refData, bool canceled); ClangdClient * const q; @@ -326,7 +342,7 @@ public: bool isTesting = false; }; -ClangdClient::ClangdClient(ProjectExplorer::Project *project, const Utils::FilePath &jsonDbDir) +ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir) : Client(clientInterface(jsonDbDir)), d(new Private(this)) { setName(tr("clangd")); @@ -355,7 +371,7 @@ ClangdClient::ClangdClient(ProjectExplorer::Project *project, const Utils::FileP // Report all search results found so far. for (quint64 key : d->runningFindUsages.keys()) - d->reportAllSearchResultsAndFinish(d->runningFindUsages.value(key)); + d->reportAllSearchResultsAndFinish(d->runningFindUsages[key]); QTC_CHECK(d->runningFindUsages.isEmpty()); }); @@ -388,13 +404,9 @@ void ClangdClient::closeExtraFile(const Utils::FilePath &filePath) TextDocumentIdentifier{DocumentUri::fromFilePath(filePath)}))); } -void ClangdClient::findUsages(TextEditor::TextDocument *document, const QTextCursor &cursor) +void ClangdClient::findUsages(TextEditor::TextDocument *document, const QTextCursor &cursor, + const Utils::optional &replacement) { - if (versionNumber() < QVersionNumber(13)) { - symbolSupport().findUsages(document, cursor); - return; - } - QTextCursor termCursor(cursor); termCursor.select(QTextCursor::WordUnderCursor); const QString searchTerm = termCursor.selectedText(); // TODO: This will be wrong for e.g. operators. Use a Symbol info request to get the real symbol string. @@ -402,19 +414,42 @@ void ClangdClient::findUsages(TextEditor::TextDocument *document, const QTextCur return; ReferencesData refData; + refData.key = d->nextFindUsagesKey++; + if (replacement) { + ReplacementData replacementData; + replacementData.oldSymbolName = searchTerm; + replacementData.newSymbolName = *replacement; + if (replacementData.newSymbolName.isEmpty()) + replacementData.newSymbolName = replacementData.oldSymbolName; + refData.replacementData = replacementData; + } refData.search = SearchResultWindow::instance()->startNewSearch( tr("C++ Usages:"), {}, searchTerm, - SearchResultWindow::SearchOnly, + replacement ? SearchResultWindow::SearchAndReplace : SearchResultWindow::SearchOnly, SearchResultWindow::PreserveCaseDisabled, "CppEditor"); refData.search->setFilter(new CppTools::CppSearchResultFilter); + if (refData.replacementData) { + refData.search->setTextToReplace(refData.replacementData->newSymbolName); + const auto renameFilesCheckBox = new QCheckBox; + renameFilesCheckBox->setVisible(false); + refData.search->setAdditionalReplaceWidget(renameFilesCheckBox); + const auto renameHandler = + [search = refData.search](const QString &newSymbolName, + const QList &checkedItems, + bool preserveCase) { + const auto replacementData = search->userData().value(); + Private::handleRenameRequest(search, replacementData, newSymbolName, checkedItems, + preserveCase); + }; + connect(refData.search, &SearchResult::replaceButtonClicked, renameHandler); + } connect(refData.search, &SearchResult::activated, [](const SearchResultItem& item) { Core::EditorManager::openEditorAtSearchResult(item); }); SearchResultWindow::instance()->popup(IOutputPane::ModeSwitch | IOutputPane::WithFocus); - refData.key = d->nextFindUsagesKey++; d->runningFindUsages.insert(refData.key, refData); const Utils::optional requestId = symbolSupport().findUsages( @@ -431,7 +466,7 @@ void ClangdClient::findUsages(TextEditor::TextDocument *document, const QTextCur if (refData == d->runningFindUsages.end()) return; cancelRequest(*requestId); - refData->setCanceled(); + refData->canceled = true; refData->search->disconnect(this); d->finishSearch(*refData, true); }); @@ -462,7 +497,7 @@ void ClangdClient::Private::handleFindUsagesResult(quint64 key, const QListsearch || refData->isCanceled()) { + if (!refData->search || refData->canceled) { finishSearch(*refData, true); return; } @@ -478,7 +513,7 @@ void ClangdClient::Private::handleFindUsagesResult(quint64 key, const QListsetCanceled(); + refData->canceled = true; refData->search->disconnect(q); for (const MessageId &id : qAsConst(refData->pendingAstRequests)) q->cancelRequest(id); @@ -499,7 +534,8 @@ void ClangdClient::Private::handleFindUsagesResult(quint64 key, const QListfileData.size(); - if (refData->fileData.size() > 15) { // TODO: If we need to keep this, make it configurable. + if (refData->replacementData || q->versionNumber() < QVersionNumber(13) + || refData->fileData.size() > 15) { // TODO: If we need to keep this, make it configurable. qCDebug(clangdLog) << "skipping AST retrieval"; reportAllSearchResultsAndFinish(*refData); return; @@ -520,7 +556,7 @@ void ClangdClient::Private::handleFindUsagesResult(quint64 key, const QListsearch || refData->isCanceled()) + if (!refData->search || refData->canceled) return; ReferencesFileData &data = refData->fileData[loc]; const auto result = response.result(); @@ -545,7 +581,33 @@ void ClangdClient::Private::handleFindUsagesResult(quint64 key, const QList &checkedItems, + bool preserveCase) +{ + const QStringList fileNames = TextEditor::BaseFileFind::replaceAll(newSymbolName, checkedItems, + preserveCase); + if (!fileNames.isEmpty()) + SearchResultWindow::instance()->hide(); + + const auto renameFilesCheckBox = qobject_cast(search->additionalReplaceWidget()); + QTC_ASSERT(renameFilesCheckBox, return); + if (!renameFilesCheckBox->isChecked()) + return; + + QVector fileNodes; + for (const Utils::FilePath &file : replacementData.fileRenameCandidates) { + Node * const node = ProjectTree::nodeForFile(file); + if (node) + fileNodes << node; + } + if (!fileNodes.isEmpty()) + CppTools::renameFilesForSymbol(replacementData.oldSymbolName, newSymbolName, fileNodes); +} + +void ClangdClient::Private::addSearchResultsForFile(ReferencesData &refData, const Utils::FilePath &file, const ReferencesFileData &fileData) { @@ -563,6 +625,15 @@ void ClangdClient::Private::addSearchResultsForFile(const ReferencesData &refDat item.setMainRange(SymbolSupport::convertRange(range)); item.setUseTextEditorFont(true); item.setLineText(rangeWithText.second); + if (refData.search->supportsReplace()) { + const bool fileInSession = SessionManager::projectForFile(file); + item.setSelectForReplacement(fileInSession); + if (fileInSession && file.toFileInfo().baseName().compare( + refData.replacementData->oldSymbolName, + Qt::CaseInsensitive) == 0) { + refData.replacementData->fileRenameCandidates << file; // TODO: We want to do this only for types. Use SymbolInformation once we have it. + } + } items << item; } if (isTesting) @@ -571,11 +642,11 @@ void ClangdClient::Private::addSearchResultsForFile(const ReferencesData &refDat refData.search->addResults(items, SearchResult::AddOrdered); } -void ClangdClient::Private::reportAllSearchResultsAndFinish(const ReferencesData &refData) +void ClangdClient::Private::reportAllSearchResultsAndFinish(ReferencesData &refData) { for (auto it = refData.fileData.begin(); it != refData.fileData.end(); ++it) addSearchResultsForFile(refData, it.key().toFilePath(), it.value()); - finishSearch(refData, refData.isCanceled()); + finishSearch(refData, refData.canceled); } void ClangdClient::Private::finishSearch(const ReferencesData &refData, bool canceled) @@ -585,9 +656,23 @@ void ClangdClient::Private::finishSearch(const ReferencesData &refData, bool can } else if (refData.search) { refData.search->finishSearch(canceled); refData.search->disconnect(q); + if (refData.replacementData) { + const auto renameCheckBox = qobject_cast( + refData.search->additionalReplaceWidget()); + QTC_CHECK(renameCheckBox); + const QSet files = refData.replacementData->fileRenameCandidates; + renameCheckBox->setText(tr("Re&name %n files", nullptr, files.size())); + const QStringList filesForUser = Utils::transform(files, + [](const Utils::FilePath &fp) { return fp.toUserOutput(); }); + renameCheckBox->setToolTip(tr("Files:\n%1").arg(filesForUser.join('\n'))); + renameCheckBox->setVisible(true); + refData.search->setUserData(QVariant::fromValue(*refData.replacementData)); + } } runningFindUsages.remove(refData.key); } } // namespace Internal } // namespace ClangCodeModel + +Q_DECLARE_METATYPE(ClangCodeModel::Internal::ReplacementData) diff --git a/src/plugins/clangcodemodel/clangdclient.h b/src/plugins/clangcodemodel/clangdclient.h index f5d7c022f72..c2f61c1ba90 100644 --- a/src/plugins/clangcodemodel/clangdclient.h +++ b/src/plugins/clangcodemodel/clangdclient.h @@ -26,6 +26,7 @@ #pragma once #include +#include #include @@ -49,7 +50,8 @@ public: void openExtraFile(const Utils::FilePath &filePath, const QString &content = {}); void closeExtraFile(const Utils::FilePath &filePath); - void findUsages(TextEditor::TextDocument *document, const QTextCursor &cursor); + void findUsages(TextEditor::TextDocument *document, const QTextCursor &cursor, + const Utils::optional &replacement); void enableTesting(); diff --git a/src/plugins/clangcodemodel/clangrefactoringengine.cpp b/src/plugins/clangcodemodel/clangrefactoringengine.cpp index ba418e7e032..4f4dfb1a551 100644 --- a/src/plugins/clangcodemodel/clangrefactoringengine.cpp +++ b/src/plugins/clangcodemodel/clangrefactoringengine.cpp @@ -86,6 +86,23 @@ void RefactoringEngine::startLocalRenaming(const CppTools::CursorInEditor &data, m_watcher->setFuture(cursorFuture); } +void RefactoringEngine::globalRename(const CppTools::CursorInEditor &cursor, + CppTools::UsagesCallback &&callback, + const QString &replacement) +{ + ProjectExplorer::Project * const project + = ProjectExplorer::SessionManager::projectForFile(cursor.filePath()); + ClangdClient * const client = ClangModelManagerSupport::instance()->clientForProject(project); + if (!client || !client->isFullyIndexed()) { + CppTools::CppModelManager::builtinRefactoringEngine() + ->globalRename(cursor, std::move(callback), replacement); + return; + } + QTC_ASSERT(client->documentOpen(cursor.textDocument()), + client->openDocument(cursor.textDocument())); + client->findUsages(cursor.textDocument(), cursor.cursor(), replacement); +} + void RefactoringEngine::findUsages(const CppTools::CursorInEditor &cursor, CppTools::UsagesCallback &&callback) const { @@ -99,7 +116,7 @@ void RefactoringEngine::findUsages(const CppTools::CursorInEditor &cursor, } QTC_ASSERT(client->documentOpen(cursor.textDocument()), client->openDocument(cursor.textDocument())); - client->findUsages(cursor.textDocument(), cursor.cursor()); + client->findUsages(cursor.textDocument(), cursor.cursor(), {}); } } // namespace Internal diff --git a/src/plugins/clangcodemodel/clangrefactoringengine.h b/src/plugins/clangcodemodel/clangrefactoringengine.h index ca5f04c8008..30729c86b96 100644 --- a/src/plugins/clangcodemodel/clangrefactoringengine.h +++ b/src/plugins/clangcodemodel/clangrefactoringengine.h @@ -44,8 +44,8 @@ public: void startLocalRenaming(const CppTools::CursorInEditor &data, CppTools::ProjectPart *projectPart, RenameCallback &&renameSymbolsCallback) override; - void globalRename(const CppTools::CursorInEditor &, CppTools::UsagesCallback &&, - const QString &) override {} + void globalRename(const CppTools::CursorInEditor &cursor, CppTools::UsagesCallback &&callback, + const QString &replacement) override; void findUsages(const CppTools::CursorInEditor &cursor, CppTools::UsagesCallback &&callback) const override; void globalFollowSymbol(const CppTools::CursorInEditor &, diff --git a/src/plugins/clangcodemodel/test/clangdtests.cpp b/src/plugins/clangcodemodel/test/clangdtests.cpp index 0145f79cdbb..5380e11ac68 100644 --- a/src/plugins/clangcodemodel/test/clangdtests.cpp +++ b/src/plugins/clangcodemodel/test/clangdtests.cpp @@ -168,7 +168,7 @@ void ClangdTests::testFindReferences() QTextCursor cursor((doc)->document()); \ cursor.setPosition((pos)); \ searchResults.clear(); \ - client->findUsages((doc), cursor); \ + client->findUsages((doc), cursor, {}); \ QVERIFY(waitForSignalOrTimeout(client, &ClangdClient::findUsagesDone)); \ } while (false) diff --git a/src/plugins/cppeditor/cppeditorwidget.cpp b/src/plugins/cppeditor/cppeditorwidget.cpp index c7ca20d4217..9f8854baeac 100644 --- a/src/plugins/cppeditor/cppeditorwidget.cpp +++ b/src/plugins/cppeditor/cppeditorwidget.cpp @@ -491,7 +491,8 @@ void CppEditorWidget::renameUsages(const QString &replacement, QTextCursor curso { if (cursor.isNull()) cursor = textCursor(); - CppTools::CursorInEditor cursorInEditor{cursor, textDocument()->filePath(), this}; + CppTools::CursorInEditor cursorInEditor{cursor, textDocument()->filePath(), this, + textDocument()}; QPointer cppEditorWidget = this; d->m_modelManager->globalRename(cursorInEditor, [=](const CppTools::Usages &usages) { diff --git a/src/plugins/cpptools/cppfindreferences.cpp b/src/plugins/cpptools/cppfindreferences.cpp index 2ffb129adb6..e93fc545ead 100644 --- a/src/plugins/cpptools/cppfindreferences.cpp +++ b/src/plugins/cpptools/cppfindreferences.cpp @@ -59,6 +59,8 @@ using namespace ProjectExplorer; namespace CppTools { +namespace { static bool isAllLowerCase(const QString &text) { return text.toLower() == text; } } + SearchResultColor::Style colorStyleForUsageType(CPlusPlus::Usage::Type type) { switch (type) { @@ -75,6 +77,51 @@ SearchResultColor::Style colorStyleForUsageType(CPlusPlus::Usage::Type type) return SearchResultColor::Style::Default; // For dumb compilers. } +void renameFilesForSymbol(const QString &oldSymbolName, const QString &newSymbolName, + const QVector &files) +{ + Internal::CppFileSettings settings; + settings.fromSettings(Core::ICore::settings()); + + const QStringList newPaths = + Utils::transform(files, + [&oldSymbolName, newSymbolName, &settings](const Node *node) -> QString { + const QFileInfo fi = node->filePath().toFileInfo(); + const QString oldBaseName = fi.baseName(); + QString newBaseName = newSymbolName; + + // 1) new symbol lowercase: new base name lowercase + if (isAllLowerCase(newSymbolName)) { + newBaseName = newSymbolName; + + // 2) old base name mixed case: new base name is verbatim symbol name + } else if (!isAllLowerCase(oldBaseName)) { + newBaseName = newSymbolName; + + // 3) old base name lowercase, old symbol mixed case: new base name lowercase + } else if (!isAllLowerCase(oldSymbolName)) { + newBaseName = newSymbolName.toLower(); + + // 4) old base name lowercase, old symbol lowercase, new symbol mixed case: + // use the preferences setting for new base name case + } else if (settings.lowerCaseFiles) { + newBaseName = newSymbolName.toLower(); + } + + if (newBaseName == oldBaseName) + return QString(); + + return fi.absolutePath() + "/" + newBaseName + '.' + fi.completeSuffix(); + }); + + for (int i = 0; i < files.size(); ++i) { + if (!newPaths.at(i).isEmpty()) { + Node *node = files.at(i); + ProjectExplorerPlugin::renameFile(node, newPaths.at(i)); + } + } +} + QWidget *CppSearchResultFilter::createWidget() { const auto widget = new QWidget; @@ -469,11 +516,6 @@ void CppFindReferences::findAll_helper(SearchResult *search, CPlusPlus::Symbol * connect(progress, &FutureProgress::clicked, search, &SearchResult::popup); } -static bool isAllLowerCase(const QString &text) -{ - return text.toLower() == text; -} - void CppFindReferences::onReplaceButtonClicked(const QString &text, const QList &items, bool preserveCase) @@ -495,48 +537,7 @@ void CppFindReferences::onReplaceButtonClicked(const QString &text, if (!renameFilesCheckBox || !renameFilesCheckBox->isChecked()) return; - CppFileSettings settings; - settings.fromSettings(Core::ICore::settings()); - - const QStringList newPaths = - Utils::transform(parameters.filesToRename, - [¶meters, text, &settings](const Node *node) -> QString { - const QFileInfo fi = node->filePath().toFileInfo(); - const QString oldSymbolName = parameters.prettySymbolName; - const QString oldBaseName = fi.baseName(); - const QString newSymbolName = text; - QString newBaseName = newSymbolName; - - // 1) new symbol lowercase: new base name lowercase - if (isAllLowerCase(newSymbolName)) { - newBaseName = newSymbolName; - - // 2) old base name mixed case: new base name is verbatim symbol name - } else if (!isAllLowerCase(oldBaseName)) { - newBaseName = newSymbolName; - - // 3) old base name lowercase, old symbol mixed case: new base name lowercase - } else if (!isAllLowerCase(oldSymbolName)) { - newBaseName = newSymbolName.toLower(); - - // 4) old base name lowercase, old symbol lowercase, new symbol mixed case: - // use the preferences setting for new base name case - } else if (settings.lowerCaseFiles) { - newBaseName = newSymbolName.toLower(); - } - - if (newBaseName == oldBaseName) - return QString(); - - return fi.absolutePath() + "/" + newBaseName + '.' + fi.completeSuffix(); - }); - - for (int i = 0; i < parameters.filesToRename.size(); ++i) { - if (!newPaths.at(i).isEmpty()) { - Node *node = parameters.filesToRename.at(i); - ProjectExplorerPlugin::renameFile(node, newPaths.at(i)); - } - } + renameFilesForSymbol(parameters.prettySymbolName, text, parameters.filesToRename); } void CppFindReferences::searchAgain() diff --git a/src/plugins/cpptools/cppfindreferences.h b/src/plugins/cpptools/cppfindreferences.h index 9434a8c587d..8d74c2ddd34 100644 --- a/src/plugins/cpptools/cppfindreferences.h +++ b/src/plugins/cpptools/cppfindreferences.h @@ -49,6 +49,9 @@ namespace CppTools { class CppModelManager; Core::SearchResultColor::Style CPPTOOLS_EXPORT colorStyleForUsageType(CPlusPlus::Usage::Type type); +void CPPTOOLS_EXPORT renameFilesForSymbol(const QString &oldSymbolName, + const QString &newSymbolName, + const QVector &files); class CPPTOOLS_EXPORT CppSearchResultFilter : public Core::SearchResultFilter { diff --git a/src/plugins/cpptools/cppmodelmanager.cpp b/src/plugins/cpptools/cppmodelmanager.cpp index 6d33d64e096..2bbb17de384 100644 --- a/src/plugins/cpptools/cppmodelmanager.cpp +++ b/src/plugins/cpptools/cppmodelmanager.cpp @@ -323,7 +323,7 @@ void CppModelManager::startLocalRenaming(const CursorInEditor &data, void CppModelManager::globalRename(const CursorInEditor &data, UsagesCallback &&renameCallback, const QString &replacement) { - RefactoringEngineInterface *engine = getRefactoringEngine(d->m_refactoringEngines); + RefactoringEngineInterface *engine = getRefactoringEngine(d->m_refactoringEngines, false); QTC_ASSERT(engine, return;); engine->globalRename(data, std::move(renameCallback), replacement); }