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