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