1 /*
2     SPDX-FileCopyrightText: 2014 Miquel Sabaté <mikisabate@gmail.com>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 // Qt
8 #include <QAction>
9 // KF
10 #include <KParts/MainWindow>
11 #include <KTextEditor/Document>
12 #include <KTextEditor/View>
13 // KDevelop
14 #include <interfaces/icore.h>
15 #include <interfaces/idocument.h>
16 #include <interfaces/iuicontroller.h>
17 #include <interfaces/idocumentcontroller.h>
18 #include <interfaces/contextmenuextension.h>
19 #include <language/duchain/duchain.h>
20 #include <language/duchain/duchainlock.h>
21 #include <language/duchain/duchainutils.h>
22 #include <language/duchain/navigation/abstractnavigationwidget.h>
23 #include <language/codegen/basicrefactoring.h>
24 #include <language/interfaces/codecontext.h>
25 #include <duchain/classdeclaration.h>
26 #include <duchain/classfunctiondeclaration.h>
27 #include <duchain/use.h>
28 #include <sublime/message.h>
29 
30 #include "progressdialogs/refactoringdialog.h"
31 #include <debug.h>
32 
33 #include "ui_basicrefactoring.h"
34 
35 namespace {
splitFileAtExtension(const QString & fileName)36 QPair<QString, QString> splitFileAtExtension(const QString& fileName)
37 {
38     int idx = fileName.indexOf(QLatin1Char('.'));
39     if (idx == -1) {
40         return qMakePair(fileName, QString());
41     }
42     return qMakePair(fileName.left(idx), fileName.mid(idx));
43 }
44 }
45 
46 using namespace KDevelop;
47 
48 //BEGIN: BasicRefactoringCollector
49 
BasicRefactoringCollector(const IndexedDeclaration & decl)50 BasicRefactoringCollector::BasicRefactoringCollector(const IndexedDeclaration& decl)
51     : UsesWidgetCollector(decl)
52 {
53     setCollectConstructors(true);
54     setCollectDefinitions(true);
55     setCollectOverloads(true);
56 }
57 
allUsingContexts() const58 QVector<IndexedTopDUContext> BasicRefactoringCollector::allUsingContexts() const
59 {
60     return m_allUsingContexts;
61 }
62 
processUses(KDevelop::ReferencedTopDUContext topContext)63 void BasicRefactoringCollector::processUses(KDevelop::ReferencedTopDUContext topContext)
64 {
65     m_allUsingContexts << IndexedTopDUContext(topContext.data());
66     UsesWidgetCollector::processUses(topContext);
67 }
68 
69 //END: BasicRefactoringCollector
70 
71 //BEGIN: BasicRefactoring
72 
BasicRefactoring(QObject * parent)73 BasicRefactoring::BasicRefactoring(QObject* parent)
74     : QObject(parent)
75 {
76     /* There's nothing to do here. */
77 }
78 
fillContextMenu(ContextMenuExtension & extension,Context * context,QWidget * parent)79 void BasicRefactoring::fillContextMenu(ContextMenuExtension& extension, Context* context, QWidget* parent)
80 {
81     auto* declContext = dynamic_cast<DeclarationContext*>(context);
82     if (!declContext)
83         return;
84 
85     DUChainReadLocker lock;
86     Declaration* declaration = declContext->declaration().data();
87     if (declaration && acceptForContextMenu(declaration)) {
88         QFileInfo finfo(declaration->topContext()->url().str());
89         if (finfo.isWritable()) {
90             auto* action = new QAction(i18nc("@action", "Rename \"%1\"...",
91                                             declaration->qualifiedIdentifier().toString()), parent);
92             action->setData(QVariant::fromValue(IndexedDeclaration(declaration)));
93             action->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename")));
94             connect(action, &QAction::triggered, this, &BasicRefactoring::executeRenameAction);
95             extension.addAction(ContextMenuExtension::RefactorGroup, action);
96         }
97     }
98 }
99 
shouldRenameUses(KDevelop::Declaration * declaration) const100 bool BasicRefactoring::shouldRenameUses(KDevelop::Declaration* declaration) const
101 {
102     // Now we know we're editing a declaration, but some declarations we don't offer a rename for
103     // basically that's any declaration that wouldn't be fully renamed just by renaming its uses().
104     if (declaration->internalContext() || declaration->isForwardDeclaration()) {
105         //make an exception for non-class functions
106         if (!declaration->isFunctionDeclaration() || dynamic_cast<ClassFunctionDeclaration*>(declaration))
107             return false;
108     }
109     return true;
110 }
111 
newFileName(const QUrl & current,const QString & newName)112 QString BasicRefactoring::newFileName(const QUrl& current, const QString& newName)
113 {
114     QPair<QString, QString> nameExtensionPair = splitFileAtExtension(current.fileName());
115     // if current file is lowercased, keep that
116     if (nameExtensionPair.first == nameExtensionPair.first.toLower()) {
117         return newName.toLower() + nameExtensionPair.second;
118     } else {
119         return newName + nameExtensionPair.second;
120     }
121 }
122 
addRenameFileChanges(const QUrl & current,const QString & newName,DocumentChangeSet * changes)123 DocumentChangeSet::ChangeResult BasicRefactoring::addRenameFileChanges(const QUrl& current,
124                                                                        const QString& newName,
125                                                                        DocumentChangeSet* changes)
126 {
127     return changes->addDocumentRenameChange(
128         IndexedString(current), IndexedString(newFileName(current, newName)));
129 }
130 
shouldRenameFile(Declaration * declaration)131 bool BasicRefactoring::shouldRenameFile(Declaration* declaration)
132 {
133     // only try to rename files when we renamed a class/struct
134     if (!dynamic_cast<ClassDeclaration*>(declaration)) {
135         return false;
136     }
137     const QUrl currUrl = declaration->topContext()->url().toUrl();
138     const QString fileName = currUrl.fileName();
139     const QPair<QString, QString> nameExtensionPair = splitFileAtExtension(fileName);
140     // check whether we renamed something that is called like the document it lives in
141     return nameExtensionPair.first.compare(declaration->identifier().toString(), Qt::CaseInsensitive) == 0;
142 }
143 
applyChanges(const QString & oldName,const QString & newName,DocumentChangeSet & changes,DUContext * context,int usedDeclarationIndex)144 DocumentChangeSet::ChangeResult BasicRefactoring::applyChanges(const QString& oldName, const QString& newName,
145                                                                DocumentChangeSet& changes, DUContext* context,
146                                                                int usedDeclarationIndex)
147 {
148     if (usedDeclarationIndex == std::numeric_limits<int>::max())
149         return DocumentChangeSet::ChangeResult::successfulResult();
150 
151     for (int a = 0; a < context->usesCount(); ++a) {
152         const Use& use(context->uses()[a]);
153         if (use.m_declarationIndex != usedDeclarationIndex)
154             continue;
155         if (use.m_range.isEmpty()) {
156             qCDebug(LANGUAGE) << "found empty use";
157             continue;
158         }
159         DocumentChangeSet::ChangeResult result =
160             changes.addChange(DocumentChange(context->url(), context->transformFromLocalRevision(use.m_range), oldName,
161                                              newName));
162         if (!result)
163             return result;
164     }
165 
166     const auto childContexts = context->childContexts();
167     for (DUContext* child : childContexts) {
168         DocumentChangeSet::ChangeResult result = applyChanges(oldName, newName, changes, child, usedDeclarationIndex);
169         if (!result)
170             return result;
171     }
172 
173     return DocumentChangeSet::ChangeResult::successfulResult();
174 }
175 
applyChangesToDeclarations(const QString & oldName,const QString & newName,DocumentChangeSet & changes,const QList<IndexedDeclaration> & declarations)176 DocumentChangeSet::ChangeResult BasicRefactoring::applyChangesToDeclarations(const QString& oldName,
177                                                                              const QString& newName,
178                                                                              DocumentChangeSet& changes,
179                                                                              const QList<IndexedDeclaration>& declarations)
180 {
181     for (auto& decl : declarations) {
182         Declaration* declaration = decl.data();
183         if (!declaration)
184             continue;
185         if (declaration->range().isEmpty())
186             qCDebug(LANGUAGE) << "found empty declaration";
187 
188         TopDUContext* top = declaration->topContext();
189         DocumentChangeSet::ChangeResult result =
190             changes.addChange(DocumentChange(top->url(), declaration->rangeInCurrentRevision(), oldName, newName));
191         if (!result)
192             return result;
193     }
194 
195     return DocumentChangeSet::ChangeResult::successfulResult();
196 }
197 
declarationUnderCursor(bool allowUse)198 KDevelop::IndexedDeclaration BasicRefactoring::declarationUnderCursor(bool allowUse)
199 {
200     KTextEditor::View* view = ICore::self()->documentController()->activeTextDocumentView();
201     if (!view)
202         return KDevelop::IndexedDeclaration();
203     KTextEditor::Document* doc = view->document();
204 
205     DUChainReadLocker lock;
206     if (allowUse)
207         return DUChainUtils::itemUnderCursor(doc->url(), KTextEditor::Cursor(view->cursorPosition())).declaration;
208     else
209         return DUChainUtils::declarationInLine(KTextEditor::Cursor(
210                                                    view->cursorPosition()),
211                                                DUChainUtils::standardContextForUrl(doc->url()));
212 }
213 
startInteractiveRename(const KDevelop::IndexedDeclaration & decl)214 void BasicRefactoring::startInteractiveRename(const KDevelop::IndexedDeclaration& decl)
215 {
216     DUChainReadLocker lock(DUChain::lock());
217 
218     Declaration* declaration = decl.data();
219     if (!declaration) {
220         auto* message = new Sublime::Message(i18n("No declaration under cursor"), Sublime::Message::Error);
221         ICore::self()->uiController()->postMessage(message);
222         return;
223     }
224     QFileInfo info(declaration->topContext()->url().str());
225     if (!info.isWritable()) {
226         const QString messageText = i18n("Declaration is located in non-writable file %1.",
227                                 declaration->topContext()->url().str());
228         auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
229         ICore::self()->uiController()->postMessage(message);
230         return;
231     }
232 
233     QString originalName = declaration->identifier().identifier().str();
234     lock.unlock();
235 
236     NameAndCollector nc = newNameForDeclaration(DeclarationPointer(declaration));
237 
238     if (nc.newName == originalName || nc.newName.isEmpty())
239         return;
240 
241     renameCollectedDeclarations(nc.collector.data(), nc.newName, originalName);
242 }
243 
acceptForContextMenu(const Declaration * decl)244 bool BasicRefactoring::acceptForContextMenu(const Declaration* decl)
245 {
246     // Default implementation. Some language plugins might override it to
247     // handle some cases.
248     Q_UNUSED(decl);
249     return true;
250 }
251 
executeRenameAction()252 void BasicRefactoring::executeRenameAction()
253 {
254     auto* action = qobject_cast<QAction*>(sender());
255     if (action) {
256         IndexedDeclaration decl = action->data().value<IndexedDeclaration>();
257         if (!decl.isValid())
258             decl = declarationUnderCursor();
259 
260         if (!decl.isValid())
261             return;
262 
263         startInteractiveRename(decl);
264     }
265 }
266 
newNameForDeclaration(const KDevelop::DeclarationPointer & declaration)267 BasicRefactoring::NameAndCollector BasicRefactoring::newNameForDeclaration(
268     const KDevelop::DeclarationPointer& declaration)
269 {
270     DUChainReadLocker lock;
271     if (!declaration) {
272         return {};
273     }
274 
275     QSharedPointer<BasicRefactoringCollector> collector(new BasicRefactoringCollector(declaration.data()));
276 
277     Ui::RenameDialog renameDialog;
278     QDialog dialog;
279     renameDialog.setupUi(&dialog);
280 
281     UsesWidget uses(declaration.data(), collector);
282 
283     //So the context-links work
284     auto* navigationWidget = declaration->context()->createNavigationWidget(declaration.data());
285     if (navigationWidget)
286         connect(&uses, &UsesWidget::navigateDeclaration, navigationWidget,
287                 &AbstractNavigationWidget::navigateDeclaration);
288 
289     QString declarationName = declaration->toString();
290     dialog.setWindowTitle(i18nc("@title:window Renaming some declaration", "Rename \"%1\"", declarationName));
291     renameDialog.edit->setText(declaration->identifier().identifier().str());
292     renameDialog.edit->selectAll();
293 
294     renameDialog.tabWidget->addTab(&uses, i18nc("@title:tab", "Uses"));
295     if (navigationWidget)
296         renameDialog.tabWidget->addTab(navigationWidget, i18nc("@title:tab", "Declaration Info"));
297     lock.unlock();
298 
299     if (dialog.exec() != QDialog::Accepted)
300         return {};
301 
302     const auto text = renameDialog.edit->text().trimmed();
303     RefactoringProgressDialog refactoringProgress(i18n("Renaming \"%1\" to \"%2\"", declarationName,
304                                                        text), collector.data());
305     if (!collector->isReady()) {
306         if (refactoringProgress.exec() != QDialog::Accepted) { // krazy:exclude=crashy
307             return {};
308         }
309     }
310 
311     //TODO: input validation
312     return {
313                text, collector
314     };
315 }
316 
renameCollectedDeclarations(KDevelop::BasicRefactoringCollector * collector,const QString & replacementName,const QString & originalName,bool apply)317 DocumentChangeSet BasicRefactoring::renameCollectedDeclarations(KDevelop::BasicRefactoringCollector* collector,
318                                                                 const QString& replacementName,
319                                                                 const QString& originalName, bool apply)
320 {
321     DocumentChangeSet changes;
322     DUChainReadLocker lock;
323 
324     const auto allUsingContexts = collector->allUsingContexts();
325     for (const KDevelop::IndexedTopDUContext collected : allUsingContexts) {
326         QSet<int> hadIndices;
327         const auto declarations = collector->declarations();
328         for (const IndexedDeclaration decl : declarations) {
329             uint usedDeclarationIndex = collected.data()->indexForUsedDeclaration(decl.data(), false);
330             if (hadIndices.contains(usedDeclarationIndex))
331                 continue;
332             hadIndices.insert(usedDeclarationIndex);
333             DocumentChangeSet::ChangeResult result = applyChanges(originalName, replacementName, changes,
334                                                                   collected.data(), usedDeclarationIndex);
335             if (!result) {
336                 auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error);
337                 ICore::self()->uiController()->postMessage(message);
338                 return {};
339             }
340         }
341     }
342 
343     DocumentChangeSet::ChangeResult result = applyChangesToDeclarations(originalName, replacementName, changes,
344                                                                         collector->declarations());
345     if (!result) {
346         auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error);
347         ICore::self()->uiController()->postMessage(message);
348         return {};
349     }
350 
351     ///We have to ignore failed changes for now, since uses of a constructor or of operator() may be created on "(" parens
352     changes.setReplacementPolicy(DocumentChangeSet::IgnoreFailedChange);
353 
354     if (!apply) {
355         return changes;
356     }
357 
358     result = changes.applyAllChanges();
359     if (!result) {
360         auto* message = new Sublime::Message(i18n("Failed to apply changes: %1", result.m_failureReason), Sublime::Message::Error);
361         ICore::self()->uiController()->postMessage(message);
362     }
363 
364     return {};
365 }
366 
367 //END: BasicRefactoring
368