kde-extraapps/kdevplatform/language/assistant/renameassistant.cpp
2015-07-26 14:23:17 +03:00

220 lines
7.7 KiB
C++

/*
Copyright 2010 Olivier de Gaalon <olivier.jg@gmail.com>
Copyright 2014 Kevin Funk <kfunk@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2 as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "renameassistant.h"
#include "renameaction.h"
#include "renamefileaction.h"
#include "../codegen/basicrefactoring.h"
#include "../codegen/documentchangeset.h"
#include "../duchain/duchain.h"
#include "../duchain/duchainlock.h"
#include "../duchain/duchainutils.h"
#include "../duchain/declaration.h"
#include "../duchain/functiondefinition.h"
#include "../duchain/classfunctiondeclaration.h"
#include <interfaces/icore.h>
#include <interfaces/ilanguagecontroller.h>
#include <ktexteditor/view.h>
#include <ktexteditor/document.h>
#include <klocalizedstring.h>
using namespace KDevelop;
namespace {
bool rangesConnect(const KTextEditor::Range& firstRange, const KTextEditor::Range& secondRange)
{
return !firstRange.intersect(secondRange + KTextEditor::Range(0, -1, 0, +1)).isEmpty();
}
Declaration* getDeclarationForChangedRange(KTextEditor::View* view, const KTextEditor::Range& changed)
{
SimpleCursor cursor(changed.start());
Declaration* declaration = DUChainUtils::itemUnderCursor(view->document()->url(), cursor);
//If it's null we could be appending, but there's a case where appending gives a wrong decl
//and not a null declaration ... "type var(init)", so check for that too
if (!declaration || !rangesConnect(declaration->rangeInCurrentRevision().textRange(), changed)) {
declaration = DUChainUtils::itemUnderCursor(view->document()->url(), SimpleCursor(cursor.line, --cursor.column));
}
//In this case, we may either not have a decl at the cursor, or we got a decl, but are editing its use.
//In either of those cases, give up and return 0
if (!declaration || !rangesConnect(declaration->rangeInCurrentRevision().textRange(), changed)) {
return 0;
}
return declaration;
}
}
struct RenameAssistant::Private
{
Private(RenameAssistant* qq)
: q(qq)
, m_isUseful(false)
, m_renameFile(false)
{
}
void reset()
{
q->doHide();
q->clearActions();
m_oldDeclarationName = Identifier();
m_newDeclarationRange.clear();
m_oldDeclarationUses.clear();
m_isUseful = false;
m_renameFile = false;
}
RenameAssistant* q;
KDevelop::Identifier m_oldDeclarationName;
QString m_newDeclarationName;
KDevelop::PersistentMovingRange::Ptr m_newDeclarationRange;
QVector<RevisionedFileRanges> m_oldDeclarationUses;
bool m_isUseful;
bool m_renameFile;
};
RenameAssistant::RenameAssistant(ILanguageSupport* supportedLanguage)
: StaticAssistant(supportedLanguage)
, d(new Private(this))
{
}
QString RenameAssistant::title() const
{
return tr("Rename");
}
bool RenameAssistant::isUseful() const
{
return d->m_isUseful;
}
void RenameAssistant::textChanged(KTextEditor::View* view, const KTextEditor::Range& invocationRange, const QString& removedText)
{
clearActions();
if (!supportedLanguage()->refactoring()) {
kWarning() << "Refactoring not supported. Aborting.";
return;
}
if (!view)
return;
//If the inserted text isn't valid for a variable name, consider the editing ended
QRegExp validDeclName("^[0-9a-zA-Z_]*$");
if (removedText.isEmpty() && !validDeclName.exactMatch(view->document()->text(invocationRange))) {
d->reset();
return;
}
const KUrl url = view->document()->url();
const IndexedString indexedUrl(url);
DUChainReadLocker lock;
//If we've stopped editing m_newDeclarationRange or switched the view,
// reset and see if there's another declaration being edited
if (!d->m_newDeclarationRange.data() || !rangesConnect(d->m_newDeclarationRange->range().textRange(), invocationRange)
|| d->m_newDeclarationRange->document() != indexedUrl) {
d->reset();
Declaration* declAtCursor = getDeclarationForChangedRange(view, invocationRange);
if (!declAtCursor) {
// not editing a declaration
return;
}
if (supportedLanguage()->refactoring()->shouldRenameUses(declAtCursor)) {
QMap< IndexedString, QList<RangeInRevision> > declUses = declAtCursor->uses();
if (declUses.isEmpty()) {
// new declaration is use-less
return;
}
for(QMap< IndexedString, QList< RangeInRevision > >::const_iterator it = declUses.constBegin();
it != declUses.constEnd(); ++it)
{
foreach(const RangeInRevision& range, it.value())
{
SimpleRange currentRange = declAtCursor->transformFromLocalRevision(range);
if(currentRange.isEmpty() || view->document()->text(currentRange.textRange()) != declAtCursor->identifier().identifier().str())
return; // One of the uses is invalid. Maybe the replacement has already been performed.
}
}
d->m_oldDeclarationUses = RevisionedFileRanges::convert(declUses);
} else if (supportedLanguage()->refactoring()->shouldRenameFile(declAtCursor)) {
d->m_renameFile = true;
} else {
// not a valid declaration
return;
}
d->m_oldDeclarationName = declAtCursor->identifier();
KTextEditor::Range newRange = declAtCursor->rangeInCurrentRevision().textRange();
if (removedText.isEmpty() && newRange.intersect(invocationRange).isEmpty()) {
newRange = newRange.encompass(invocationRange); //if text was added to the ends, encompass it
}
d->m_newDeclarationRange.attach(new PersistentMovingRange(newRange, indexedUrl, true));
}
//Unfortunately this happens when you make a selection including one end of the decl's range and replace it
if (removedText.isEmpty() && d->m_newDeclarationRange->range().textRange().intersect(invocationRange).isEmpty()) {
d->m_newDeclarationRange.attach(new PersistentMovingRange(
d->m_newDeclarationRange->range().textRange().encompass(invocationRange), indexedUrl, true));
}
d->m_newDeclarationName = view->document()->text(d->m_newDeclarationRange->range().textRange());
if (d->m_newDeclarationName == d->m_oldDeclarationName.toString()) {
d->reset();
return;
}
if (d->m_renameFile && supportedLanguage()->refactoring()->newFileName(url, d->m_newDeclarationName) == url.fileName()) {
// no change, don't do anything
return;
}
d->m_isUseful = true;
IAssistantAction::Ptr action;
if (d->m_renameFile) {
action.attach(new RenameFileAction(supportedLanguage()->refactoring(), url, d->m_newDeclarationName));
} else {
action.attach(new RenameAction(d->m_oldDeclarationName, d->m_newDeclarationName,
d->m_oldDeclarationUses));
}
connect(action.data(), SIGNAL(executed(IAssistantAction*)), SLOT(reset()));
addAction(action);
emit actionsChanged();
}
#include "moc_renameassistant.cpp"