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

0001 /*
0002     SPDX-FileCopyrightText: 2010 Olivier de Gaalon <olivier.jg@gmail.com>
0003     SPDX-FileCopyrightText: 2014 Kevin Funk <kfunk@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "renameassistant.h"
0009 
0010 #include "renameaction.h"
0011 #include "renamefileaction.h"
0012 #include <debug.h>
0013 #include "../codegen/basicrefactoring.h"
0014 #include "../duchain/duchain.h"
0015 #include "../duchain/duchainlock.h"
0016 #include "../duchain/duchainutils.h"
0017 #include "../duchain/declaration.h"
0018 #include "../duchain/functiondefinition.h"
0019 #include "../duchain/classfunctiondeclaration.h"
0020 
0021 #include <interfaces/icore.h>
0022 #include <interfaces/ilanguagecontroller.h>
0023 #include <interfaces/ilanguagesupport.h>
0024 
0025 #include <KTextEditor/Document>
0026 
0027 #include <KLocalizedString>
0028 
0029 using namespace KDevelop;
0030 
0031 namespace {
0032 bool rangesConnect(const KTextEditor::Range& firstRange, const KTextEditor::Range& secondRange)
0033 {
0034     return !firstRange.intersect(secondRange + KTextEditor::Range(0, -1, 0, +1)).isEmpty();
0035 }
0036 
0037 Declaration* declarationForChangedRange(KTextEditor::Document* doc, const KTextEditor::Range& changed)
0038 {
0039     const KTextEditor::Cursor cursor(changed.start());
0040     Declaration* declaration = DUChainUtils::itemUnderCursor(doc->url(), cursor).declaration;
0041 
0042     //If it's null we could be appending, but there's a case where appending gives a wrong decl
0043     //and not a null declaration ... "type var(init)", so check for that too
0044     if (!declaration || !rangesConnect(declaration->rangeInCurrentRevision(), changed)) {
0045         declaration =
0046             DUChainUtils::itemUnderCursor(doc->url(),
0047                                           KTextEditor::Cursor(cursor.line(), cursor.column() - 1)).declaration;
0048     }
0049 
0050     //In this case, we may either not have a decl at the cursor, or we got a decl, but are editing its use.
0051     //In either of those cases, give up and return 0
0052     if (!declaration || !rangesConnect(declaration->rangeInCurrentRevision(), changed)) {
0053         return nullptr;
0054     }
0055 
0056     return declaration;
0057 }
0058 }
0059 
0060 class KDevelop::RenameAssistantPrivate
0061 {
0062 public:
0063     explicit RenameAssistantPrivate(RenameAssistant* qq)
0064         : q(qq)
0065         , m_isUseful(false)
0066         , m_renameFile(false)
0067     {
0068     }
0069 
0070     void reset()
0071     {
0072         q->doHide();
0073         q->clearActions();
0074         m_oldDeclarationName = Identifier();
0075         m_newDeclarationRange.reset();
0076         m_oldDeclarationUses.clear();
0077         m_isUseful = false;
0078         m_renameFile = false;
0079     }
0080 
0081     RenameAssistant* q;
0082 
0083     KDevelop::Identifier m_oldDeclarationName;
0084     QString m_newDeclarationName;
0085     KDevelop::PersistentMovingRange::Ptr m_newDeclarationRange;
0086     QVector<RevisionedFileRanges> m_oldDeclarationUses;
0087 
0088     bool m_isUseful;
0089     bool m_renameFile;
0090     KTextEditor::Cursor m_lastChangedLocation;
0091     QPointer<KTextEditor::Document> m_lastChangedDocument = nullptr;
0092 };
0093 
0094 RenameAssistant::RenameAssistant(ILanguageSupport* supportedLanguage)
0095     : StaticAssistant(supportedLanguage)
0096     , d_ptr(new RenameAssistantPrivate(this))
0097 {
0098 }
0099 
0100 RenameAssistant::~RenameAssistant()
0101 {
0102 }
0103 
0104 QString RenameAssistant::title() const
0105 {
0106     return i18n("Rename");
0107 }
0108 
0109 bool RenameAssistant::isUseful() const
0110 {
0111     Q_D(const RenameAssistant);
0112 
0113     return d->m_isUseful;
0114 }
0115 
0116 void RenameAssistant::textChanged(KTextEditor::Document* doc, const KTextEditor::Range& invocationRange,
0117                                   const QString& removedText)
0118 {
0119     Q_D(RenameAssistant);
0120 
0121     clearActions();
0122     d->m_lastChangedLocation = invocationRange.end();
0123     d->m_lastChangedDocument = doc;
0124 
0125     if (!supportedLanguage()->refactoring()) {
0126         qCWarning(LANGUAGE) << "Refactoring not supported. Aborting.";
0127         return;
0128     }
0129 
0130     if (!doc)
0131         return;
0132 
0133     //If the inserted text isn't valid for a variable name, consider the editing ended
0134     QRegExp validDeclName(QStringLiteral("^[0-9a-zA-Z_]*$"));
0135     if (removedText.isEmpty() && !validDeclName.exactMatch(doc->text(invocationRange))) {
0136         d->reset();
0137         return;
0138     }
0139 
0140     const QUrl url = doc->url();
0141     const IndexedString indexedUrl(url);
0142     DUChainReadLocker lock;
0143 
0144     //If we've stopped editing m_newDeclarationRange or switched the view,
0145     // reset and see if there's another declaration being edited
0146     if (!d->m_newDeclarationRange.data() || !rangesConnect(d->m_newDeclarationRange->range(), invocationRange)
0147         || d->m_newDeclarationRange->document() != indexedUrl) {
0148         d->reset();
0149 
0150         Declaration* declAtCursor = declarationForChangedRange(doc, invocationRange);
0151         if (!declAtCursor) {
0152             // not editing a declaration
0153             return;
0154         }
0155 
0156         if (supportedLanguage()->refactoring()->shouldRenameUses(declAtCursor)) {
0157             const auto declUses = declAtCursor->uses();
0158             if (declUses.isEmpty()) {
0159                 // new declaration has no uses
0160                 return;
0161             }
0162 
0163             for (auto& ranges : declUses) {
0164                 for (const RangeInRevision range : ranges) {
0165                     KTextEditor::Range currentRange = declAtCursor->transformFromLocalRevision(range);
0166                     if (currentRange.isEmpty() ||
0167                         doc->text(currentRange) != declAtCursor->identifier().identifier().str()) {
0168                         return; // One of the uses is invalid. Maybe the replacement has already been performed.
0169                     }
0170                 }
0171             }
0172 
0173             d->m_oldDeclarationUses = RevisionedFileRanges::convert(declUses);
0174         } else if (supportedLanguage()->refactoring()->shouldRenameFile(declAtCursor)) {
0175             d->m_renameFile = true;
0176         } else {
0177             // not a valid declaration
0178             return;
0179         }
0180 
0181         d->m_oldDeclarationName = declAtCursor->identifier();
0182         KTextEditor::Range newRange = declAtCursor->rangeInCurrentRevision();
0183         if (removedText.isEmpty() && newRange.intersect(invocationRange).isEmpty()) {
0184             newRange = newRange.encompass(invocationRange); //if text was added to the ends, encompass it
0185         }
0186 
0187         d->m_newDeclarationRange = new PersistentMovingRange(newRange, indexedUrl, true);
0188     }
0189 
0190     //Unfortunately this happens when you make a selection including one end of the decl's range and replace it
0191     if (removedText.isEmpty() && d->m_newDeclarationRange->range().intersect(invocationRange).isEmpty()) {
0192         d->m_newDeclarationRange = new PersistentMovingRange(
0193             d->m_newDeclarationRange->range().encompass(invocationRange), indexedUrl, true);
0194     }
0195 
0196     d->m_newDeclarationName = doc->text(d->m_newDeclarationRange->range()).trimmed();
0197     if (d->m_newDeclarationName == d->m_oldDeclarationName.toString()) {
0198         d->reset();
0199         return;
0200     }
0201 
0202     if (d->m_renameFile &&
0203         supportedLanguage()->refactoring()->newFileName(url, d->m_newDeclarationName) == url.fileName()) {
0204         // no change, don't do anything
0205         return;
0206     }
0207 
0208     d->m_isUseful = true;
0209 
0210     IAssistantAction::Ptr action;
0211     if (d->m_renameFile) {
0212         action = new RenameFileAction(supportedLanguage()->refactoring(), url, d->m_newDeclarationName);
0213     } else {
0214         action = new RenameAction(d->m_oldDeclarationName, d->m_newDeclarationName, d->m_oldDeclarationUses);
0215     }
0216     connect(action.data(), &IAssistantAction::executed, this, [this] {
0217         Q_D(RenameAssistant);
0218         d->reset();
0219     });
0220     addAction(action);
0221     emit actionsChanged();
0222 }
0223 
0224 KTextEditor::Range KDevelop::RenameAssistant::displayRange() const
0225 {
0226     Q_D(const RenameAssistant);
0227 
0228     if (!d->m_lastChangedDocument) {
0229         return {};
0230     }
0231     auto range = d->m_lastChangedDocument->wordRangeAt(d->m_lastChangedLocation);
0232     qCDebug(LANGUAGE) << "range:" << range;
0233     return range;
0234 }
0235 
0236 #include "moc_renameassistant.cpp"