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

355 lines
13 KiB
C++

/* This file is part of KDevelop
*
* Copyright 2014 Miquel Sabaté <mikisabate@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// Qt
#include <QAction>
// KDE / KDevelop
#include <KParts/MainWindow>
#include <KTextEditor/Document>
#include <kmessagebox.h>
#include <interfaces/icore.h>
#include <interfaces/idocument.h>
#include <interfaces/iuicontroller.h>
#include <interfaces/idocumentcontroller.h>
#include <interfaces/contextmenuextension.h>
#include <language/duchain/duchain.h>
#include <language/duchain/duchainlock.h>
#include <language/duchain/duchainutils.h>
#include <language/duchain/navigation/abstractnavigationwidget.h>
#include <language/codegen/basicrefactoring.h>
#include <language/interfaces/codecontext.h>
#include <duchain/classdeclaration.h>
#include <duchain/classfunctiondeclaration.h>
#include <duchain/use.h>
#include "progressdialogs/refactoringdialog.h"
#include "ui_basicrefactoring.h"
namespace {
QPair<QString, QString> splitFileAtExtension(const QString& fileName)
{
int idx = fileName.indexOf('.');
if (idx == -1) {
return qMakePair(fileName, QString());
}
return qMakePair(fileName.left(idx), fileName.mid(idx));
}
}
using namespace KDevelop;
//BEGIN: BasicRefactoringCollector
BasicRefactoringCollector::BasicRefactoringCollector(const IndexedDeclaration &decl)
: UsesWidgetCollector(decl)
{
setCollectConstructors(true);
setCollectDefinitions(true);
setCollectOverloads(true);
}
QVector<IndexedTopDUContext> BasicRefactoringCollector::allUsingContexts() const
{
return m_allUsingContexts;
}
void BasicRefactoringCollector::processUses(KDevelop::ReferencedTopDUContext topContext)
{
m_allUsingContexts << IndexedTopDUContext(topContext.data());
UsesWidgetCollector::processUses(topContext);
}
//END: BasicRefactoringCollector
//BEGIN: BasicRefactoring
BasicRefactoring::BasicRefactoring(QObject *parent)
: QObject(parent)
{
/* There's nothing to do here. */
}
void BasicRefactoring::fillContextMenu(ContextMenuExtension &extension, Context *context)
{
DeclarationContext *declContext = dynamic_cast<DeclarationContext *>(context);
if (!declContext)
return;
DUChainReadLocker lock;
Declaration *declaration = declContext->declaration().data();
if (declaration && acceptForContextMenu(declaration)) {
QFileInfo finfo(declaration->topContext()->url().str());
if (finfo.isWritable()) {
QAction *action = new QAction(i18n("Rename \"%1\"...", declaration->qualifiedIdentifier().toString()), 0);
action->setData(QVariant::fromValue(IndexedDeclaration(declaration)));
action->setIcon(KIcon("edit-rename"));
connect(action, SIGNAL(triggered(bool)), this, SLOT(executeRenameAction()));
extension.addAction(ContextMenuExtension::RefactorGroup, action);
}
}
}
bool BasicRefactoring::shouldRenameUses(KDevelop::Declaration* declaration) const
{
// Now we know we're editing a declaration, but some declarations we don't offer a rename for
// basically that's any declaration that wouldn't be fully renamed just by renaming its uses().
if (declaration->internalContext() || declaration->isForwardDeclaration()) {
//make an exception for non-class functions
if (!declaration->isFunctionDeclaration() || dynamic_cast<ClassFunctionDeclaration*>(declaration))
return false;
}
return true;
}
QString BasicRefactoring::newFileName(const KUrl& current, const QString& newName)
{
QPair<QString, QString> nameExtensionPair = splitFileAtExtension(current.fileName());
// if current file is lowercased, keep that
if (nameExtensionPair.first == nameExtensionPair.first.toLower()) {
return newName.toLower() + nameExtensionPair.second;
} else {
return newName + nameExtensionPair.second;
}
}
DocumentChangeSet::ChangeResult BasicRefactoring::addRenameFileChanges(const KUrl& current,
const QString& newName,
DocumentChangeSet* changes)
{
return changes->addDocumentRenameChange(
IndexedString(current), IndexedString(newFileName(current, newName)));
}
bool BasicRefactoring::shouldRenameFile(Declaration* declaration)
{
// only try to rename files when we renamed a class/struct
if (!dynamic_cast<ClassDeclaration*>(declaration)) {
return false;
}
const KUrl currUrl = declaration->topContext()->url().toUrl();
const QString fileName = currUrl.fileName();
const QPair<QString, QString> nameExtensionPair = splitFileAtExtension(fileName);
// check whether we renamed something that is called like the document it lives in
return nameExtensionPair.first.compare(declaration->identifier().toString(), Qt::CaseInsensitive) == 0;
}
DocumentChangeSet::ChangeResult BasicRefactoring::applyChanges(const QString &oldName, const QString &newName,
DocumentChangeSet &changes, DUContext *context,
int usedDeclarationIndex)
{
if (usedDeclarationIndex == std::numeric_limits<int>::max())
return DocumentChangeSet::ChangeResult(true);
for (int a = 0; a < context->usesCount(); ++a) {
const Use &use(context->uses()[a]);
if (use.m_declarationIndex != usedDeclarationIndex)
continue;
if (use.m_range.isEmpty()) {
kDebug() << "found empty use";
continue;
}
DocumentChangeSet::ChangeResult result = changes.addChange(DocumentChange(context->url(), context->transformFromLocalRevision(use.m_range), oldName, newName));
if (!result)
return result;
}
foreach (DUContext *child, context->childContexts()) {
DocumentChangeSet::ChangeResult result = applyChanges(oldName, newName, changes, child, usedDeclarationIndex);
if (!result)
return result;
}
return DocumentChangeSet::ChangeResult(true);
}
DocumentChangeSet::ChangeResult BasicRefactoring::applyChangesToDeclarations(const QString &oldName,
const QString &newName,
DocumentChangeSet &changes,
const QList<IndexedDeclaration> &declarations)
{
foreach (const IndexedDeclaration &decl, declarations) {
Declaration *declaration = decl.data();
if (!declaration)
continue;
if (declaration->range().isEmpty())
kDebug() << "found empty declaration";
TopDUContext *top = declaration->topContext();
DocumentChangeSet::ChangeResult result = changes.addChange(DocumentChange(top->url(), declaration->rangeInCurrentRevision(), oldName, newName));
if (!result)
return result;
}
return DocumentChangeSet::ChangeResult(true);
}
KDevelop::IndexedDeclaration BasicRefactoring::declarationUnderCursor(bool allowUse)
{
KDevelop::IDocument *doc = ICore::self()->documentController()->activeDocument();
if (doc && doc->textDocument() && doc->textDocument()->activeView()) {
DUChainReadLocker lock;
if (allowUse)
return DUChainUtils::itemUnderCursor(doc->url(), SimpleCursor(doc->textDocument()->activeView()->cursorPosition()));
else
return DUChainUtils::declarationInLine(SimpleCursor(doc->textDocument()->activeView()->cursorPosition()), DUChainUtils::standardContextForUrl(doc->url()));
}
return KDevelop::IndexedDeclaration();
}
void BasicRefactoring::startInteractiveRename(const KDevelop::IndexedDeclaration &decl)
{
DUChainReadLocker lock(DUChain::lock());
Declaration *declaration = decl.data();
if (!declaration) {
KMessageBox::error(ICore::self()->uiController()->activeMainWindow(), i18n("No declaration under cursor"));
return;
}
QFileInfo info(declaration->topContext()->url().str());
if (!info.isWritable()) {
KMessageBox::error(ICore::self()->uiController()->activeMainWindow(),
i18n("Declaration is located in non-writeable file %1.", declaration->topContext()->url().str()));
return;
}
QString originalName = declaration->identifier().identifier().str();
lock.unlock();
NameAndCollector nc = newNameForDeclaration(DeclarationPointer(declaration));
if (nc.newName == originalName || nc.newName.isEmpty())
return;
renameCollectedDeclarations(nc.collector.data(), nc.newName, originalName);
}
bool BasicRefactoring::acceptForContextMenu(const Declaration *decl)
{
// Default implementation. Some language plugins might override it to
// handle some cases.
Q_UNUSED(decl);
return true;
}
void BasicRefactoring::executeRenameAction()
{
QAction *action = qobject_cast<QAction *>(sender());
if (action) {
IndexedDeclaration decl = action->data().value<IndexedDeclaration>();
if(!decl.isValid())
decl = declarationUnderCursor();
startInteractiveRename(decl);
}
}
BasicRefactoring::NameAndCollector BasicRefactoring::newNameForDeclaration(const KDevelop::DeclarationPointer& declaration)
{
DUChainReadLocker lock;
if (!declaration) {
return {};
}
QSharedPointer<BasicRefactoringCollector> collector(new BasicRefactoringCollector(declaration.data()));
Ui::RenameDialog renameDialog;
QDialog dialog;
renameDialog.setupUi(&dialog);
UsesWidget uses(declaration.data(), collector);
//So the context-links work
QWidget *navigationWidget = declaration->context()->createNavigationWidget(declaration.data());
AbstractNavigationWidget* abstractNavigationWidget = dynamic_cast<AbstractNavigationWidget*>(navigationWidget);
if (abstractNavigationWidget)
connect(&uses, SIGNAL(navigateDeclaration(KDevelop::IndexedDeclaration)), abstractNavigationWidget, SLOT(navigateDeclaration(KDevelop::IndexedDeclaration)));
QString declarationName = declaration->toString();
dialog.setWindowTitle(i18nc("Renaming some declaration", "Rename \"%1\"", declarationName));
renameDialog.edit->setText(declaration->identifier().identifier().str());
renameDialog.edit->selectAll();
renameDialog.tabWidget->addTab(&uses, i18n("Uses"));
if (navigationWidget)
renameDialog.tabWidget->addTab(navigationWidget, i18n("Declaration Info"));
lock.unlock();
if (dialog.exec() != QDialog::Accepted)
return {};
RefactoringProgressDialog refactoringProgress(i18n("Renaming \"%1\" to \"%2\"", declarationName, renameDialog.edit->text()), collector.data());
if (!collector->isReady()) {
refactoringProgress.exec();
if (refactoringProgress.result() != QDialog::Accepted) {
return {};
}
}
//TODO: input validation
return {renameDialog.edit->text(),collector};
}
DocumentChangeSet BasicRefactoring::renameCollectedDeclarations(KDevelop::BasicRefactoringCollector* collector, const QString& replacementName, const QString& originalName, bool apply)
{
DocumentChangeSet changes;
DUChainReadLocker lock;
foreach (const KDevelop::IndexedTopDUContext& collected, collector->allUsingContexts()) {
QSet<int> hadIndices;
foreach (const IndexedDeclaration& decl, collector->declarations()) {
uint usedDeclarationIndex = collected.data()->indexForUsedDeclaration(decl.data(), false);
if (hadIndices.contains(usedDeclarationIndex))
continue;
hadIndices.insert(usedDeclarationIndex);
DocumentChangeSet::ChangeResult result = applyChanges(originalName, replacementName, changes, collected.data(), usedDeclarationIndex);
if (!result) {
KMessageBox::error(0, i18n("Applying changes failed: %1", result.m_failureReason));
return {};
}
}
}
DocumentChangeSet::ChangeResult result = applyChangesToDeclarations(originalName, replacementName, changes, collector->declarations());
if (!result) {
KMessageBox::error(0, i18n("Applying changes failed: %1", result.m_failureReason));
return {};
}
///We have to ignore failed changes for now, since uses of a constructor or of operator() may be created on "(" parens
changes.setReplacementPolicy(DocumentChangeSet::IgnoreFailedChange);
if (!apply) {
return changes;
}
result = changes.applyAllChanges();
if (!result) {
KMessageBox::error(0, i18n("Applying changes failed: %1", result.m_failureReason));
}
return {};
}
//END: BasicRefactoring
#include "moc_basicrefactoring.cpp"