1 /*
2     SPDX-FileCopyrightText: 2010 Olivier de Gaalon <olivier.jg@gmail.com>
3     SPDX-FileCopyrightText: 2014 Kevin Funk <kfunk@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-only
6 */
7 
8 #include "renameassistant.h"
9 
10 #include "renameaction.h"
11 #include "renamefileaction.h"
12 #include <debug.h>
13 #include "../codegen/basicrefactoring.h"
14 #include "../duchain/duchain.h"
15 #include "../duchain/duchainlock.h"
16 #include "../duchain/duchainutils.h"
17 #include "../duchain/declaration.h"
18 #include "../duchain/functiondefinition.h"
19 #include "../duchain/classfunctiondeclaration.h"
20 
21 #include <interfaces/icore.h>
22 #include <interfaces/ilanguagecontroller.h>
23 #include <interfaces/ilanguagesupport.h>
24 
25 #include <KTextEditor/Document>
26 
27 #include <KLocalizedString>
28 
29 using namespace KDevelop;
30 
31 namespace {
rangesConnect(const KTextEditor::Range & firstRange,const KTextEditor::Range & secondRange)32 bool rangesConnect(const KTextEditor::Range& firstRange, const KTextEditor::Range& secondRange)
33 {
34     return !firstRange.intersect(secondRange + KTextEditor::Range(0, -1, 0, +1)).isEmpty();
35 }
36 
declarationForChangedRange(KTextEditor::Document * doc,const KTextEditor::Range & changed)37 Declaration* declarationForChangedRange(KTextEditor::Document* doc, const KTextEditor::Range& changed)
38 {
39     const KTextEditor::Cursor cursor(changed.start());
40     Declaration* declaration = DUChainUtils::itemUnderCursor(doc->url(), cursor).declaration;
41 
42     //If it's null we could be appending, but there's a case where appending gives a wrong decl
43     //and not a null declaration ... "type var(init)", so check for that too
44     if (!declaration || !rangesConnect(declaration->rangeInCurrentRevision(), changed)) {
45         declaration =
46             DUChainUtils::itemUnderCursor(doc->url(),
47                                           KTextEditor::Cursor(cursor.line(), cursor.column() - 1)).declaration;
48     }
49 
50     //In this case, we may either not have a decl at the cursor, or we got a decl, but are editing its use.
51     //In either of those cases, give up and return 0
52     if (!declaration || !rangesConnect(declaration->rangeInCurrentRevision(), changed)) {
53         return nullptr;
54     }
55 
56     return declaration;
57 }
58 }
59 
60 class KDevelop::RenameAssistantPrivate
61 {
62 public:
RenameAssistantPrivate(RenameAssistant * qq)63     explicit RenameAssistantPrivate(RenameAssistant* qq)
64         : q(qq)
65         , m_isUseful(false)
66         , m_renameFile(false)
67     {
68     }
69 
reset()70     void reset()
71     {
72         q->doHide();
73         q->clearActions();
74         m_oldDeclarationName = Identifier();
75         m_newDeclarationRange.reset();
76         m_oldDeclarationUses.clear();
77         m_isUseful = false;
78         m_renameFile = false;
79     }
80 
81     RenameAssistant* q;
82 
83     KDevelop::Identifier m_oldDeclarationName;
84     QString m_newDeclarationName;
85     KDevelop::PersistentMovingRange::Ptr m_newDeclarationRange;
86     QVector<RevisionedFileRanges> m_oldDeclarationUses;
87 
88     bool m_isUseful;
89     bool m_renameFile;
90     KTextEditor::Cursor m_lastChangedLocation;
91     QPointer<KTextEditor::Document> m_lastChangedDocument = nullptr;
92 };
93 
RenameAssistant(ILanguageSupport * supportedLanguage)94 RenameAssistant::RenameAssistant(ILanguageSupport* supportedLanguage)
95     : StaticAssistant(supportedLanguage)
96     , d_ptr(new RenameAssistantPrivate(this))
97 {
98 }
99 
~RenameAssistant()100 RenameAssistant::~RenameAssistant()
101 {
102 }
103 
title() const104 QString RenameAssistant::title() const
105 {
106     return i18n("Rename");
107 }
108 
isUseful() const109 bool RenameAssistant::isUseful() const
110 {
111     Q_D(const RenameAssistant);
112 
113     return d->m_isUseful;
114 }
115 
textChanged(KTextEditor::Document * doc,const KTextEditor::Range & invocationRange,const QString & removedText)116 void RenameAssistant::textChanged(KTextEditor::Document* doc, const KTextEditor::Range& invocationRange,
117                                   const QString& removedText)
118 {
119     Q_D(RenameAssistant);
120 
121     clearActions();
122     d->m_lastChangedLocation = invocationRange.end();
123     d->m_lastChangedDocument = doc;
124 
125     if (!supportedLanguage()->refactoring()) {
126         qCWarning(LANGUAGE) << "Refactoring not supported. Aborting.";
127         return;
128     }
129 
130     if (!doc)
131         return;
132 
133     //If the inserted text isn't valid for a variable name, consider the editing ended
134     QRegExp validDeclName(QStringLiteral("^[0-9a-zA-Z_]*$"));
135     if (removedText.isEmpty() && !validDeclName.exactMatch(doc->text(invocationRange))) {
136         d->reset();
137         return;
138     }
139 
140     const QUrl url = doc->url();
141     const IndexedString indexedUrl(url);
142     DUChainReadLocker lock;
143 
144     //If we've stopped editing m_newDeclarationRange or switched the view,
145     // reset and see if there's another declaration being edited
146     if (!d->m_newDeclarationRange.data() || !rangesConnect(d->m_newDeclarationRange->range(), invocationRange)
147         || d->m_newDeclarationRange->document() != indexedUrl) {
148         d->reset();
149 
150         Declaration* declAtCursor = declarationForChangedRange(doc, invocationRange);
151         if (!declAtCursor) {
152             // not editing a declaration
153             return;
154         }
155 
156         if (supportedLanguage()->refactoring()->shouldRenameUses(declAtCursor)) {
157             const auto declUses = declAtCursor->uses();
158             if (declUses.isEmpty()) {
159                 // new declaration has no uses
160                 return;
161             }
162 
163             for (auto& ranges : declUses) {
164                 for (const RangeInRevision range : ranges) {
165                     KTextEditor::Range currentRange = declAtCursor->transformFromLocalRevision(range);
166                     if (currentRange.isEmpty() ||
167                         doc->text(currentRange) != declAtCursor->identifier().identifier().str()) {
168                         return; // One of the uses is invalid. Maybe the replacement has already been performed.
169                     }
170                 }
171             }
172 
173             d->m_oldDeclarationUses = RevisionedFileRanges::convert(declUses);
174         } else if (supportedLanguage()->refactoring()->shouldRenameFile(declAtCursor)) {
175             d->m_renameFile = true;
176         } else {
177             // not a valid declaration
178             return;
179         }
180 
181         d->m_oldDeclarationName = declAtCursor->identifier();
182         KTextEditor::Range newRange = declAtCursor->rangeInCurrentRevision();
183         if (removedText.isEmpty() && newRange.intersect(invocationRange).isEmpty()) {
184             newRange = newRange.encompass(invocationRange); //if text was added to the ends, encompass it
185         }
186 
187         d->m_newDeclarationRange = new PersistentMovingRange(newRange, indexedUrl, true);
188     }
189 
190     //Unfortunately this happens when you make a selection including one end of the decl's range and replace it
191     if (removedText.isEmpty() && d->m_newDeclarationRange->range().intersect(invocationRange).isEmpty()) {
192         d->m_newDeclarationRange = new PersistentMovingRange(
193             d->m_newDeclarationRange->range().encompass(invocationRange), indexedUrl, true);
194     }
195 
196     d->m_newDeclarationName = doc->text(d->m_newDeclarationRange->range()).trimmed();
197     if (d->m_newDeclarationName == d->m_oldDeclarationName.toString()) {
198         d->reset();
199         return;
200     }
201 
202     if (d->m_renameFile &&
203         supportedLanguage()->refactoring()->newFileName(url, d->m_newDeclarationName) == url.fileName()) {
204         // no change, don't do anything
205         return;
206     }
207 
208     d->m_isUseful = true;
209 
210     IAssistantAction::Ptr action;
211     if (d->m_renameFile) {
212         action = new RenameFileAction(supportedLanguage()->refactoring(), url, d->m_newDeclarationName);
213     } else {
214         action = new RenameAction(d->m_oldDeclarationName, d->m_newDeclarationName, d->m_oldDeclarationUses);
215     }
216     connect(action.data(), &IAssistantAction::executed, this, [this] {
217         Q_D(RenameAssistant);
218         d->reset();
219     });
220     addAction(action);
221     emit actionsChanged();
222 }
223 
displayRange() const224 KTextEditor::Range KDevelop::RenameAssistant::displayRange() const
225 {
226     Q_D(const RenameAssistant);
227 
228     if (!d->m_lastChangedDocument) {
229         return {};
230     }
231     auto range = d->m_lastChangedDocument->wordRangeAt(d->m_lastChangedLocation);
232     qCDebug(LANGUAGE) << "range:" << range;
233     return range;
234 }
235 
236 #include "moc_renameassistant.cpp"
237