File indexing completed on 2024-05-12 04:37:46

0001 /*
0002     SPDX-FileCopyrightText: 2014 Miquel Sabaté <mikisabate@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 // Qt
0008 #include <QAction>
0009 // KF
0010 #include <KParts/MainWindow>
0011 #include <KTextEditor/Document>
0012 #include <KTextEditor/View>
0013 // KDevelop
0014 #include <interfaces/icore.h>
0015 #include <interfaces/idocument.h>
0016 #include <interfaces/iuicontroller.h>
0017 #include <interfaces/idocumentcontroller.h>
0018 #include <interfaces/contextmenuextension.h>
0019 #include <language/duchain/duchain.h>
0020 #include <language/duchain/duchainlock.h>
0021 #include <language/duchain/duchainutils.h>
0022 #include <language/duchain/navigation/abstractnavigationwidget.h>
0023 #include <language/codegen/basicrefactoring.h>
0024 #include <language/interfaces/codecontext.h>
0025 #include <duchain/classdeclaration.h>
0026 #include <duchain/classfunctiondeclaration.h>
0027 #include <duchain/use.h>
0028 #include <sublime/message.h>
0029 
0030 #include "progressdialogs/refactoringdialog.h"
0031 #include <debug.h>
0032 
0033 #include "ui_basicrefactoring.h"
0034 
0035 namespace {
0036 QPair<QString, QString> splitFileAtExtension(const QString& fileName)
0037 {
0038     int idx = fileName.indexOf(QLatin1Char('.'));
0039     if (idx == -1) {
0040         return qMakePair(fileName, QString());
0041     }
0042     return qMakePair(fileName.left(idx), fileName.mid(idx));
0043 }
0044 }
0045 
0046 using namespace KDevelop;
0047 
0048 //BEGIN: BasicRefactoringCollector
0049 
0050 BasicRefactoringCollector::BasicRefactoringCollector(const IndexedDeclaration& decl)
0051     : UsesWidgetCollector(decl)
0052 {
0053     setCollectConstructors(true);
0054     setCollectDefinitions(true);
0055     setCollectOverloads(true);
0056 }
0057 
0058 QVector<IndexedTopDUContext> BasicRefactoringCollector::allUsingContexts() const
0059 {
0060     return m_allUsingContexts;
0061 }
0062 
0063 void BasicRefactoringCollector::processUses(KDevelop::ReferencedTopDUContext topContext)
0064 {
0065     m_allUsingContexts << IndexedTopDUContext(topContext.data());
0066     UsesWidgetCollector::processUses(topContext);
0067 }
0068 
0069 //END: BasicRefactoringCollector
0070 
0071 //BEGIN: BasicRefactoring
0072 
0073 BasicRefactoring::BasicRefactoring(QObject* parent)
0074     : QObject(parent)
0075 {
0076     /* There's nothing to do here. */
0077 }
0078 
0079 void BasicRefactoring::fillContextMenu(ContextMenuExtension& extension, Context* context, QWidget* parent)
0080 {
0081     auto* declContext = dynamic_cast<DeclarationContext*>(context);
0082     if (!declContext)
0083         return;
0084 
0085     DUChainReadLocker lock;
0086     Declaration* declaration = declContext->declaration().data();
0087     if (declaration && acceptForContextMenu(declaration)) {
0088         QFileInfo finfo(declaration->topContext()->url().str());
0089         if (finfo.isWritable()) {
0090             auto* action = new QAction(i18nc("@action", "Rename \"%1\"...",
0091                                             declaration->qualifiedIdentifier().toString()), parent);
0092             action->setData(QVariant::fromValue(IndexedDeclaration(declaration)));
0093             action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename")));
0094             connect(action, &QAction::triggered, this, &BasicRefactoring::executeRenameAction);
0095             extension.addAction(ContextMenuExtension::RefactorGroup, action);
0096         }
0097     }
0098 }
0099 
0100 bool BasicRefactoring::shouldRenameUses(KDevelop::Declaration* declaration) const
0101 {
0102     // Now we know we're editing a declaration, but some declarations we don't offer a rename for
0103     // basically that's any declaration that wouldn't be fully renamed just by renaming its uses().
0104     if (declaration->internalContext() || declaration->isForwardDeclaration()) {
0105         //make an exception for non-class functions
0106         if (!declaration->isFunctionDeclaration() || dynamic_cast<ClassFunctionDeclaration*>(declaration))
0107             return false;
0108     }
0109     return true;
0110 }
0111 
0112 QString BasicRefactoring::newFileName(const QUrl& current, const QString& newName)
0113 {
0114     QPair<QString, QString> nameExtensionPair = splitFileAtExtension(current.fileName());
0115     // if current file is lowercased, keep that
0116     if (nameExtensionPair.first == nameExtensionPair.first.toLower()) {
0117         return newName.toLower() + nameExtensionPair.second;
0118     } else {
0119         return newName + nameExtensionPair.second;
0120     }
0121 }
0122 
0123 DocumentChangeSet::ChangeResult BasicRefactoring::addRenameFileChanges(const QUrl& current,
0124                                                                        const QString& newName,
0125                                                                        DocumentChangeSet* changes)
0126 {
0127     return changes->addDocumentRenameChange(
0128         IndexedString(current), IndexedString(newFileName(current, newName)));
0129 }
0130 
0131 bool BasicRefactoring::shouldRenameFile(Declaration* declaration)
0132 {
0133     // only try to rename files when we renamed a class/struct
0134     if (!dynamic_cast<ClassDeclaration*>(declaration)) {
0135         return false;
0136     }
0137     const QUrl currUrl = declaration->topContext()->url().toUrl();
0138     const QString fileName = currUrl.fileName();
0139     const QPair<QString, QString> nameExtensionPair = splitFileAtExtension(fileName);
0140     // check whether we renamed something that is called like the document it lives in
0141     return nameExtensionPair.first.compare(declaration->identifier().toString(), Qt::CaseInsensitive) == 0;
0142 }
0143 
0144 DocumentChangeSet::ChangeResult BasicRefactoring::applyChanges(const QString& oldName, const QString& newName,
0145                                                                DocumentChangeSet& changes, DUContext* context,
0146                                                                int usedDeclarationIndex)
0147 {
0148     if (usedDeclarationIndex == std::numeric_limits<int>::max())
0149         return DocumentChangeSet::ChangeResult::successfulResult();
0150 
0151     for (int a = 0; a < context->usesCount(); ++a) {
0152         const Use& use(context->uses()[a]);
0153         if (use.m_declarationIndex != usedDeclarationIndex)
0154             continue;
0155         if (use.m_range.isEmpty()) {
0156             qCDebug(LANGUAGE) << "found empty use";
0157             continue;
0158         }
0159         DocumentChangeSet::ChangeResult result =
0160             changes.addChange(DocumentChange(context->url(), context->transformFromLocalRevision(use.m_range), oldName,
0161                                              newName));
0162         if (!result)
0163             return result;
0164     }
0165 
0166     const auto childContexts = context->childContexts();
0167     for (DUContext* child : childContexts) {
0168         DocumentChangeSet::ChangeResult result = applyChanges(oldName, newName, changes, child, usedDeclarationIndex);
0169         if (!result)
0170             return result;
0171     }
0172 
0173     return DocumentChangeSet::ChangeResult::successfulResult();
0174 }
0175 
0176 DocumentChangeSet::ChangeResult BasicRefactoring::applyChangesToDeclarations(const QString& oldName,
0177                                                                              const QString& newName,
0178                                                                              DocumentChangeSet& changes,
0179                                                                              const QList<IndexedDeclaration>& declarations)
0180 {
0181     for (auto& decl : declarations) {
0182         Declaration* declaration = decl.data();
0183         if (!declaration)
0184             continue;
0185         if (declaration->range().isEmpty())
0186             qCDebug(LANGUAGE) << "found empty declaration";
0187 
0188         TopDUContext* top = declaration->topContext();
0189         DocumentChangeSet::ChangeResult result =
0190             changes.addChange(DocumentChange(top->url(), declaration->rangeInCurrentRevision(), oldName, newName));
0191         if (!result)
0192             return result;
0193     }
0194 
0195     return DocumentChangeSet::ChangeResult::successfulResult();
0196 }
0197 
0198 KDevelop::IndexedDeclaration BasicRefactoring::declarationUnderCursor(bool allowUse)
0199 {
0200     KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView();
0201     if (!view)
0202         return KDevelop::IndexedDeclaration();
0203     KTextEditor::Document* doc = view->document();
0204 
0205     DUChainReadLocker lock;
0206     if (allowUse)
0207         return DUChainUtils::itemUnderCursor(doc->url(), KTextEditor::Cursor(view->cursorPosition())).declaration;
0208     else
0209         return DUChainUtils::declarationInLine(KTextEditor::Cursor(
0210                                                    view->cursorPosition()),
0211                                                DUChainUtils::standardContextForUrl(doc->url()));
0212 }
0213 
0214 void BasicRefactoring::startInteractiveRename(const KDevelop::IndexedDeclaration& decl)
0215 {
0216     DUChainReadLocker lock(DUChain::lock());
0217 
0218     Declaration* declaration = decl.data();
0219     if (!declaration) {
0220         auto* message = new Sublime::Message(i18n("No declaration under cursor"), Sublime::Message::Error);
0221         ICore::self()->uiController()->postMessage(message);
0222         return;
0223     }
0224     QFileInfo info(declaration->topContext()->url().str());
0225     if (!info.isWritable()) {
0226         const QString messageText = i18n("Declaration is located in non-writable file %1.",
0227                                 declaration->topContext()->url().str());
0228         auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0229         ICore::self()->uiController()->postMessage(message);
0230         return;
0231     }
0232 
0233     QString originalName = declaration->identifier().identifier().str();
0234     lock.unlock();
0235 
0236     NameAndCollector nc = newNameForDeclaration(DeclarationPointer(declaration));
0237 
0238     if (nc.newName == originalName || nc.newName.isEmpty())
0239         return;
0240 
0241     renameCollectedDeclarations(nc.collector.data(), nc.newName, originalName);
0242 }
0243 
0244 bool BasicRefactoring::acceptForContextMenu(const Declaration* decl)
0245 {
0246     // Default implementation. Some language plugins might override it to
0247     // handle some cases.
0248     Q_UNUSED(decl);
0249     return true;
0250 }
0251 
0252 void BasicRefactoring::executeRenameAction()
0253 {
0254     auto* action = qobject_cast<QAction*>(sender());
0255     if (action) {
0256         IndexedDeclaration decl = action->data().value<IndexedDeclaration>();
0257         
0258         {
0259             DUChainReadLocker lock;
0260             
0261             if (!decl.isValid())
0262                 decl = declarationUnderCursor();
0263 
0264             if (!decl.isValid())
0265                 return;
0266         }
0267 
0268         startInteractiveRename(decl);
0269     }
0270 }
0271 
0272 BasicRefactoring::NameAndCollector BasicRefactoring::newNameForDeclaration(
0273     const KDevelop::DeclarationPointer& declaration)
0274 {
0275     DUChainReadLocker lock;
0276     if (!declaration) {
0277         return {};
0278     }
0279 
0280     QSharedPointer<BasicRefactoringCollector> collector(new BasicRefactoringCollector(declaration.data()));
0281 
0282     Ui::RenameDialog renameDialog;
0283     QDialog dialog;
0284     renameDialog.setupUi(&dialog);
0285 
0286     UsesWidget uses(declaration.data(), collector);
0287 
0288     //So the context-links work
0289     auto* navigationWidget = declaration->context()->createNavigationWidget(declaration.data());
0290     if (navigationWidget)
0291         connect(&uses, &UsesWidget::navigateDeclaration, navigationWidget,
0292                 &AbstractNavigationWidget::navigateDeclaration);
0293 
0294     QString declarationName = declaration->toString();
0295     dialog.setWindowTitle(i18nc("@title:window Renaming some declaration", "Rename \"%1\"", declarationName));
0296     renameDialog.edit->setText(declaration->identifier().identifier().str());
0297     renameDialog.edit->selectAll();
0298 
0299     renameDialog.tabWidget->addTab(&uses, i18nc("@title:tab", "Uses"));
0300     if (navigationWidget)
0301         renameDialog.tabWidget->addTab(navigationWidget, i18nc("@title:tab", "Declaration Info"));
0302     lock.unlock();
0303 
0304     if (dialog.exec() != QDialog::Accepted)
0305         return {};
0306 
0307     const auto text = renameDialog.edit->text().trimmed();
0308     RefactoringProgressDialog refactoringProgress(i18n("Renaming \"%1\" to \"%2\"", declarationName,
0309                                                        text), collector.data());
0310     if (!collector->isReady()) {
0311         if (refactoringProgress.exec() != QDialog::Accepted) { // krazy:exclude=crashy
0312             return {};
0313         }
0314     }
0315 
0316     //TODO: input validation
0317     return {
0318                text, collector
0319     };
0320 }
0321 
0322 DocumentChangeSet BasicRefactoring::renameCollectedDeclarations(KDevelop::BasicRefactoringCollector* collector,
0323                                                                 const QString& replacementName,
0324                                                                 const QString& originalName, bool apply)
0325 {
0326     DocumentChangeSet changes;
0327     DUChainReadLocker lock;
0328 
0329     const auto allUsingContexts = collector->allUsingContexts();
0330     for (const KDevelop::IndexedTopDUContext collected : allUsingContexts) {
0331         QSet<int> hadIndices;
0332         const auto declarations = collector->declarations();
0333         for (const IndexedDeclaration decl : declarations) {
0334             uint usedDeclarationIndex = collected.data()->indexForUsedDeclaration(decl.data(), false);
0335             if (hadIndices.contains(usedDeclarationIndex))
0336                 continue;
0337             hadIndices.insert(usedDeclarationIndex);
0338             DocumentChangeSet::ChangeResult result = applyChanges(originalName, replacementName, changes,
0339                                                                   collected.data(), usedDeclarationIndex);
0340             if (!result) {
0341                 auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error);
0342                 ICore::self()->uiController()->postMessage(message);
0343                 return {};
0344             }
0345         }
0346     }
0347 
0348     DocumentChangeSet::ChangeResult result = applyChangesToDeclarations(originalName, replacementName, changes,
0349                                                                         collector->declarations());
0350     if (!result) {
0351         auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error);
0352         ICore::self()->uiController()->postMessage(message);
0353         return {};
0354     }
0355 
0356     ///We have to ignore failed changes for now, since uses of a constructor or of operator() may be created on "(" parens
0357     changes.setReplacementPolicy(DocumentChangeSet::IgnoreFailedChange);
0358 
0359     if (!apply) {
0360         return changes;
0361     }
0362 
0363     result = changes.applyAllChanges();
0364     if (!result) {
0365         auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error);
0366         ICore::self()->uiController()->postMessage(message);
0367     }
0368 
0369     return {};
0370 }
0371 
0372 //END: BasicRefactoring
0373 
0374 #include "moc_basicrefactoring.cpp"