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"