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"