File indexing completed on 2024-05-05 04:40:11

0001 /*
0002     SPDX-FileCopyrightText: 2006-2009 David Nolden <david.nolden.kdevelop@art-master.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "patchreview.h"
0008 
0009 #include <QDir>
0010 #include <QFileInfo>
0011 #include <QStandardPaths>
0012 #include <QTimer>
0013 #include <QMimeDatabase>
0014 
0015 #include <KActionCollection>
0016 #include <KLocalizedString>
0017 #include <KPluginFactory>
0018 #include <KMessageBox>
0019 #include <KIO/CopyJob>
0020 
0021 #include <interfaces/idocument.h>
0022 #include <interfaces/icore.h>
0023 #include <interfaces/idocumentcontroller.h>
0024 #include <interfaces/iuicontroller.h>
0025 #include <interfaces/contextmenuextension.h>
0026 #include <interfaces/context.h>
0027 #include <interfaces/editorcontext.h>
0028 
0029 #include <project/projectmodel.h>
0030 
0031 #include <sublime/message.h>
0032 #include <util/path.h>
0033 
0034 #ifdef WITH_KOMPAREDIFF2_5_4_OR_NEWER
0035 #include <KompareDiff2/DiffSettings>
0036 #include <KompareDiff2/Kompare>
0037 #include <KompareDiff2/KompareModelList>
0038 #else
0039 #include <libkomparediff2/komparemodellist.h>
0040 #include <libkomparediff2/kompare.h>
0041 #include <libkomparediff2/diffsettings.h>
0042 #endif
0043 
0044 #include <KTextEditor/Document>
0045 #include <KTextEditor/ModificationInterface>
0046 #include <KTextEditor/MovingRange>
0047 #include <KTextEditor/View>
0048 
0049 ///Whether arbitrary exceptions that occurred while diff-parsing within the library should be caught
0050 #define CATCHLIBDIFF
0051 
0052 /* Exclude this file from doublequote_chars check as krazy doesn't understand
0053    std::string*/
0054 //krazy:excludeall=doublequote_chars
0055 #include <sublime/controller.h>
0056 #include <sublime/mainwindow.h>
0057 #include <sublime/area.h>
0058 #include <sublime/document.h>
0059 #include <sublime/view.h>
0060 #include <vcs/widgets/vcsdiffpatchsources.h>
0061 #include "patchhighlighter.h"
0062 #include "patchreviewtoolview.h"
0063 #include "localpatchsource.h"
0064 #include "debug.h"
0065 
0066 
0067 using namespace KDevelop;
0068 
0069 namespace
0070 {
0071 // Maximum number of files to open directly within a tab when the review is started
0072 const int maximumFilesToOpenDirectly = 15;
0073 }
0074 
0075 void PatchReviewPlugin::seekHunk( bool forwards, const QUrl& fileName ) {
0076     try {
0077         qCDebug(PLUGIN_PATCHREVIEW) << forwards << fileName << fileName.isEmpty();
0078         if ( !m_modelList )
0079             throw "no model";
0080 
0081         for ( int a = 0; a < m_modelList->modelCount(); ++a ) {
0082             const Diff2::DiffModel* model = m_modelList->modelAt( a );
0083             if ( !model || !model->differences() )
0084                 continue;
0085 
0086             QUrl file = urlForFileModel( model );
0087 
0088             if ( !fileName.isEmpty() && fileName != file )
0089                 continue;
0090 
0091             IDocument* doc = ICore::self()->documentController()->documentForUrl( file );
0092 
0093             if ( doc && m_highlighters.contains( doc->url() ) && m_highlighters[doc->url()] ) {
0094                 if ( doc->textDocument() ) {
0095                     const QList<KTextEditor::MovingRange*> ranges = m_highlighters[doc->url()]->ranges();
0096 
0097                     KTextEditor::View * v = doc->activeTextView();
0098                     if ( v ) {
0099                         int bestLine = -1;
0100                         KTextEditor::Cursor c = v->cursorPosition();
0101                         for (auto* range : ranges) {
0102                             const int line = range->start().line();
0103 
0104                             if ( forwards ) {
0105                                 if ( line > c.line() && ( bestLine == -1 || line < bestLine ) )
0106                                     bestLine = line;
0107                             } else {
0108                                 if ( line < c.line() && ( bestLine == -1 || line > bestLine ) )
0109                                     bestLine = line;
0110                             }
0111                         }
0112                         if ( bestLine != -1 ) {
0113                             v->setCursorPosition( KTextEditor::Cursor( bestLine, 0 ) );
0114                             return;
0115                         } else if(fileName.isEmpty()) {
0116                             int next = qBound(0, forwards ? a+1 : a-1, m_modelList->modelCount()-1);
0117                             if (next < maximumFilesToOpenDirectly) {
0118                                 ICore::self()->documentController()->openDocument(urlForFileModel(m_modelList->modelAt(next)));
0119                             }
0120                         }
0121                     }
0122                 }
0123             }
0124         }
0125     } catch ( const QString & str ) {
0126         qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str;
0127     } catch ( const char * str ) {
0128         qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str;
0129     }
0130     qCDebug(PLUGIN_PATCHREVIEW) << "no matching hunk found";
0131 }
0132 
0133 void PatchReviewPlugin::addHighlighting( const QUrl& highlightFile, IDocument* document ) {
0134     try {
0135         if ( !modelList() )
0136             throw "no model";
0137 
0138         for ( int a = 0; a < modelList()->modelCount(); ++a ) {
0139             Diff2::DiffModel* model = modelList()->modelAt( a );
0140             if ( !model )
0141                 continue;
0142 
0143             QUrl file = urlForFileModel( model );
0144 
0145             if ( file != highlightFile )
0146                 continue;
0147 
0148             qCDebug(PLUGIN_PATCHREVIEW) << "highlighting" << file.toDisplayString();
0149 
0150             IDocument* doc = document;
0151             if( !doc )
0152                 doc = ICore::self()->documentController()->documentForUrl( file );
0153 
0154             qCDebug(PLUGIN_PATCHREVIEW) << "highlighting file" << file << "with doc" << doc;
0155 
0156             if ( !doc || !doc->textDocument() )
0157                 continue;
0158 
0159             removeHighlighting( file );
0160 
0161             m_highlighters[file] = new PatchHighlighter(model, doc, this, (qobject_cast<LocalPatchSource*>(m_patch.data()) == nullptr));
0162         }
0163     } catch ( const QString & str ) {
0164         qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
0165     } catch ( const char * str ) {
0166         qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
0167     }
0168 }
0169 
0170 void PatchReviewPlugin::highlightPatch() {
0171     try {
0172         if ( !modelList() )
0173             throw "no model";
0174 
0175         for ( int a = 0; a < modelList()->modelCount(); ++a ) {
0176             const Diff2::DiffModel* model = modelList()->modelAt( a );
0177             if ( !model )
0178                 continue;
0179 
0180             QUrl file = urlForFileModel( model );
0181 
0182             addHighlighting( file );
0183         }
0184     } catch ( const QString & str ) {
0185         qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
0186     } catch ( const char * str ) {
0187         qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
0188     }
0189 }
0190 
0191 void PatchReviewPlugin::removeHighlighting( const QUrl& file ) {
0192     if ( file.isEmpty() ) {
0193         ///Remove all highlighting
0194         qDeleteAll( m_highlighters );
0195         m_highlighters.clear();
0196     } else {
0197         HighlightMap::iterator it = m_highlighters.find( file );
0198         if ( it != m_highlighters.end() ) {
0199             delete *it;
0200             m_highlighters.erase( it );
0201         }
0202     }
0203 }
0204 
0205 void PatchReviewPlugin::notifyPatchChanged() {
0206     if (m_patch) {
0207         qCDebug(PLUGIN_PATCHREVIEW) << "notifying patch change: " << m_patch->file();
0208         m_updateKompareTimer->start();
0209     } else {
0210         m_updateKompareTimer->stop();
0211     }
0212 }
0213 
0214 void PatchReviewPlugin::forceUpdate() {
0215     if( m_patch ) {
0216         // don't trigger an update if we know the plugin cannot update itself
0217         auto* vcsPatch = qobject_cast<VCSDiffPatchSource*>(m_patch.data());
0218         if (!vcsPatch || vcsPatch->m_updater) {
0219             m_patch->update();
0220             notifyPatchChanged();
0221         }
0222     }
0223 }
0224 
0225 void PatchReviewPlugin::updateKompareModel() {
0226     if ( !m_patch ) {
0227         ///TODO: this method should be cleaned up, it can be called by the timer and
0228         ///      e.g. https://bugs.kde.org/show_bug.cgi?id=267187 shows how it could
0229         ///      lead to asserts before...
0230         return;
0231     }
0232 
0233     qCDebug(PLUGIN_PATCHREVIEW) << "updating model";
0234     removeHighlighting();
0235     m_modelList.reset( nullptr );
0236     m_depth = 0;
0237     delete m_diffSettings;
0238     {
0239         IDocument* patchDoc = ICore::self()->documentController()->documentForUrl( m_patch->file() );
0240         if( patchDoc )
0241             patchDoc->reload();
0242     }
0243 
0244     QString patchFile;
0245     if( m_patch->file().isLocalFile() )
0246         patchFile = m_patch->file().toLocalFile();
0247     else if( m_patch->file().isValid() && !m_patch->file().isEmpty() ) {
0248         patchFile = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
0249         bool ret = KIO::copy(m_patch->file(), QUrl::fromLocalFile(patchFile), KIO::HideProgressInfo)->exec();
0250         if( !ret ) {
0251             qCWarning(PLUGIN_PATCHREVIEW) << "Problem while downloading: " << m_patch->file() << "to" << patchFile;
0252             patchFile.clear();
0253         }
0254     }
0255 
0256     if (!patchFile.isEmpty()) //only try to construct the model if we have a patch to load
0257     try {
0258         m_diffSettings = new DiffSettings( nullptr );
0259         m_kompareInfo.reset( new Kompare::Info() );
0260         m_kompareInfo->localDestination = patchFile;
0261         m_kompareInfo->localSource = m_patch->baseDir().toLocalFile();
0262         m_kompareInfo->depth = m_patch->depth();
0263         m_kompareInfo->applied = m_patch->isAlreadyApplied();
0264 
0265 #ifdef WITH_KOMPAREDIFF2_5_4_OR_NEWER
0266         m_modelList.reset(new Diff2::KompareModelList(m_diffSettings.data(), this));
0267 #else
0268         m_modelList.reset( new Diff2::KompareModelList( m_diffSettings.data(), new QWidget, this ) );
0269 #endif
0270         m_modelList->slotKompareInfo( m_kompareInfo.data() );
0271 
0272         try {
0273             m_modelList->openDirAndDiff();
0274         } catch ( const QString & /*str*/ ) {
0275             throw;
0276         } catch ( ... ) {
0277             throw QStringLiteral( "lib/libdiff2 crashed, memory may be corrupted. Please restart kdevelop." );
0278         }
0279 
0280         for (m_depth = 0; m_depth < 10; ++m_depth) {
0281             bool allFound = true;
0282             for( int i = 0; i < m_modelList->modelCount(); i++ ) {
0283                 if (!QFile::exists(urlForFileModel(m_modelList->modelAt(i)).toLocalFile())) {
0284                     allFound = false;
0285                 }
0286             }
0287             if (allFound) {
0288                 break; // found depth
0289             }
0290         }
0291 
0292         emit patchChanged();
0293 
0294         for( int i = 0; i < m_modelList->modelCount(); i++ ) {
0295             const Diff2::DiffModel* model = m_modelList->modelAt( i );
0296             for (auto* difference : *model->differences()) {
0297                 difference->apply(m_patch->isAlreadyApplied());
0298             }
0299         }
0300 
0301         highlightPatch();
0302 
0303         return;
0304     } catch ( const QString & str ) {
0305         KMessageBox::error(nullptr, str, i18nc("@title:window", "Kompare Model Update"));
0306     } catch ( const char * str ) {
0307         KMessageBox::error(nullptr, QLatin1String(str), i18nc("@title:window", "Kompare Model Update"));
0308     }
0309     removeHighlighting();
0310     m_modelList.reset( nullptr );
0311     m_depth = 0;
0312     m_kompareInfo.reset( nullptr );
0313     delete m_diffSettings;
0314 
0315     emit patchChanged();
0316 }
0317 
0318 K_PLUGIN_FACTORY_WITH_JSON(KDevPatchReviewFactory, "kdevpatchreview.json",
0319                            registerPlugin<PatchReviewPlugin>();)
0320 
0321 class PatchReviewToolViewFactory : public KDevelop::IToolViewFactory
0322 {
0323 public:
0324     explicit PatchReviewToolViewFactory( PatchReviewPlugin *plugin ) : m_plugin( plugin ) {}
0325 
0326     QWidget* create( QWidget *parent = nullptr ) override {
0327         return new PatchReviewToolView( parent, m_plugin );
0328     }
0329 
0330     Qt::DockWidgetArea defaultPosition() const override
0331     {
0332         return Qt::BottomDockWidgetArea;
0333     }
0334 
0335     QString id() const override {
0336         return QStringLiteral("org.kdevelop.PatchReview");
0337     }
0338 
0339 private:
0340     PatchReviewPlugin *m_plugin;
0341 };
0342 
0343 PatchReviewPlugin::~PatchReviewPlugin()
0344 {
0345     removeHighlighting();
0346 
0347     // Tweak to work around a crash on OS X; see https://bugs.kde.org/show_bug.cgi?id=338829
0348     // and http://qt-project.org/forums/viewthread/38406/#162801
0349     // modified tweak: use setPatch() and deleteLater in that method.
0350     setPatch(nullptr);
0351 }
0352 
0353 void PatchReviewPlugin::closeReview()
0354 {
0355     if( m_patch ) {
0356         IDocument* patchDocument = ICore::self()->documentController()->documentForUrl( m_patch->file() );
0357         if (patchDocument) {
0358             // Revert modifications to the text document which we've done in updateReview
0359             patchDocument->setPrettyName( QString() );
0360             patchDocument->textDocument()->setReadWrite( true );
0361             auto* modif = qobject_cast<KTextEditor::ModificationInterface*>(patchDocument->textDocument());
0362             modif->setModifiedOnDiskWarning( true );
0363         }
0364 
0365         removeHighlighting();
0366         m_modelList.reset( nullptr );
0367         m_depth = 0;
0368 
0369         if (!qobject_cast<LocalPatchSource*>(m_patch.data())) {
0370             // make sure "show" button still openes the file dialog to open a custom patch file
0371             setPatch( new LocalPatchSource );
0372         } else
0373             emit patchChanged();
0374 
0375         auto oldArea = ICore::self()->uiController()->activeArea();
0376         if (oldArea->objectName() == QLatin1String("review")) {
0377             if (ICore::self()->documentController()->saveAllDocumentsForWindow(ICore::self()->uiController()->activeMainWindow(),
0378                                                                                IDocument::Default, true))
0379             {
0380                 ICore::self()->uiController()->switchToArea(m_lastArea.isEmpty() ? QStringLiteral("code") : m_lastArea,
0381                                                             KDevelop::IUiController::ThisWindow);
0382                 if (oldArea->workingSetPersistent()) {
0383                     ICore::self()->uiController()->activeArea()->setWorkingSet(oldArea->workingSet(), true, oldArea);
0384                 }
0385             }
0386         }
0387     }
0388 }
0389 
0390 void PatchReviewPlugin::cancelReview() {
0391     if( m_patch ) {
0392         m_patch->cancelReview();
0393         closeReview();
0394     }
0395 }
0396 
0397 void PatchReviewPlugin::finishReview(const QList<QUrl>& selection)
0398 {
0399     if( m_patch && m_patch->finishReview( selection ) ) {
0400         closeReview();
0401     }
0402 }
0403 
0404 void PatchReviewPlugin::startReview( IPatchSource* patch, IPatchReview::ReviewMode mode ) {
0405     Q_UNUSED( mode );
0406     emit startingNewReview();
0407     setPatch( patch );
0408     QMetaObject::invokeMethod(this, &PatchReviewPlugin::updateReview, Qt::QueuedConnection);
0409 }
0410 
0411 void PatchReviewPlugin::switchToEmptyReviewArea()
0412 {
0413     const auto allAreas = ICore::self()->uiController()->allAreas();
0414     for (Sublime::Area* area : allAreas) {
0415         if (area->objectName() == QLatin1String("review")) {
0416             area->setWorkingSet(QString(), false);
0417         }
0418     }
0419 
0420     QString areaName = ICore::self()->uiController()->activeArea()->objectName();
0421     if (areaName != QLatin1String("review")) {
0422         m_lastArea = areaName;
0423         ICore::self()->uiController()->switchToArea(QStringLiteral("review"), KDevelop::IUiController::ThisWindow);
0424     } else {
0425         m_lastArea.clear();
0426     }
0427 }
0428 
0429 QUrl PatchReviewPlugin::urlForFileModel( const Diff2::DiffModel* model )
0430 {
0431     KDevelop::Path path(QDir::cleanPath(m_patch->baseDir().toLocalFile()));
0432     QVector<QString> destPath = KDevelop::Path(QLatin1Char('/') + model->destinationPath()).segments();
0433     if (destPath.size() >= (int)m_depth) {
0434         destPath.remove(0, m_depth);
0435     }
0436     for (const QString& segment : qAsConst(destPath)) {
0437         path.addPath(segment);
0438     }
0439     path.addPath(model->destinationFile());
0440 
0441     return path.toUrl();
0442 }
0443 
0444 void PatchReviewPlugin::updateReview()
0445 {
0446     if( !m_patch )
0447         return;
0448 
0449     m_updateKompareTimer->stop();
0450 
0451     switchToEmptyReviewArea();
0452 
0453     KDevelop::IDocumentController *docController = ICore::self()->documentController();
0454     // don't add documents opened automatically to the Files/Open Recent list
0455     IDocument* futureActiveDoc = docController->openDocument( m_patch->file(), KTextEditor::Range::invalid(),
0456                                                               IDocumentController::DoNotAddToRecentOpen );
0457 
0458     updateKompareModel();
0459 
0460     if ( !m_modelList || !futureActiveDoc || !futureActiveDoc->textDocument() ) {
0461         // might happen if e.g. openDocument dialog was cancelled by user
0462         // or under the theoretic possibility of a non-text document getting opened
0463         return;
0464     }
0465 
0466     futureActiveDoc->textDocument()->setReadWrite( false );
0467     futureActiveDoc->setPrettyName(i18nc("@title complete patch", "Overview"));
0468     auto* modif = qobject_cast<KTextEditor::ModificationInterface*>(futureActiveDoc->textDocument());
0469     modif->setModifiedOnDiskWarning( false );
0470 
0471     docController->activateDocument( futureActiveDoc );
0472 
0473     auto* toolView = qobject_cast<PatchReviewToolView*>(ICore::self()->uiController()->findToolView(i18nc("@title:window", "Patch Review"), m_factory));
0474     Q_ASSERT( toolView );
0475 
0476     //Open all relates files
0477     for( int a = 0; a < m_modelList->modelCount() && a < maximumFilesToOpenDirectly; ++a ) {
0478         QUrl absoluteUrl = urlForFileModel( m_modelList->modelAt( a ) );
0479         if (absoluteUrl.isRelative()) {
0480             const QString messageText = i18n("The base directory of the patch must be an absolute directory.");
0481             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0482             ICore::self()->uiController()->postMessage(message);
0483             break;
0484         }
0485 
0486         if( QFileInfo::exists( absoluteUrl.toLocalFile() ) && absoluteUrl.toLocalFile() != QLatin1String("/dev/null") )
0487         {
0488             toolView->open( absoluteUrl, false );
0489         }else{
0490             // Maybe the file was deleted
0491             qCDebug(PLUGIN_PATCHREVIEW) << "could not open" << absoluteUrl << "because it doesn't exist";
0492         }
0493     }
0494 }
0495 
0496 void PatchReviewPlugin::setPatch( IPatchSource* patch ) {
0497     if ( patch == m_patch ) {
0498         return;
0499     }
0500 
0501     if( m_patch ) {
0502         disconnect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged );
0503         m_patch->deleteLater();
0504     }
0505     m_patch = patch;
0506 
0507     if( m_patch ) {
0508         qCDebug(PLUGIN_PATCHREVIEW) << "setting new patch" << patch->name() << "with file" << patch->file() << "basedir" << patch->baseDir();
0509 
0510         connect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged );
0511     }
0512     QString finishText = i18nc("@action", "Finish Review");
0513     if( m_patch && !m_patch->finishReviewCustomText().isEmpty() )
0514       finishText = m_patch->finishReviewCustomText();
0515     m_finishReview->setText( finishText );
0516     m_finishReview->setEnabled( patch );
0517 
0518     notifyPatchChanged();
0519 }
0520 
0521 PatchReviewPlugin::PatchReviewPlugin( QObject *parent, const QVariantList & )
0522     : KDevelop::IPlugin( QStringLiteral("kdevpatchreview"), parent ),
0523     m_patch( nullptr ), m_factory( new PatchReviewToolViewFactory( this ) )
0524 {
0525     qRegisterMetaType<const Diff2::DiffModel*>( "const Diff2::DiffModel*" );
0526 
0527     setXMLFile( QStringLiteral("kdevpatchreview.rc") );
0528 
0529     connect( ICore::self()->documentController(), &IDocumentController::documentClosed, this, &PatchReviewPlugin::documentClosed );
0530     connect( ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this, &PatchReviewPlugin::textDocumentCreated );
0531     connect( ICore::self()->documentController(), &IDocumentController::documentSaved, this, &PatchReviewPlugin::documentSaved );
0532 
0533     m_updateKompareTimer = new QTimer( this );
0534     m_updateKompareTimer->setSingleShot( true );
0535     m_updateKompareTimer->setInterval(500);
0536     connect( m_updateKompareTimer, &QTimer::timeout, this, &PatchReviewPlugin::updateKompareModel );
0537 
0538     m_finishReview = new QAction(i18nc("@action", "Finish Review"), this);
0539     m_finishReview->setIcon( QIcon::fromTheme( QStringLiteral("dialog-ok") ) );
0540     actionCollection()->setDefaultShortcut( m_finishReview, Qt::CTRL|Qt::Key_Return );
0541     actionCollection()->addAction(QStringLiteral("commit_or_finish_review"), m_finishReview);
0542 
0543     const auto allAreas = ICore::self()->uiController()->allAreas();
0544     for (Sublime::Area* area : allAreas) {
0545         if (area->objectName() == QLatin1String("review"))
0546             area->addAction(m_finishReview);
0547     }
0548 
0549     core()->uiController()->addToolView(i18nc("@title:window", "Patch Review"), m_factory, IUiController::None);
0550 
0551     areaChanged(ICore::self()->uiController()->activeArea());
0552 }
0553 
0554 void PatchReviewPlugin::documentClosed( IDocument* doc ) {
0555     removeHighlighting( doc->url() );
0556 }
0557 
0558 void PatchReviewPlugin::documentSaved( IDocument* doc ) {
0559     // Only update if the url is not the patch-file, because our call to
0560     // the reload() KTextEditor function also causes this signal,
0561     // which would lead to an endless update loop.
0562     // Also, don't automatically update local patch sources, because
0563     // they may correspond to static files which don't match any more
0564     // after an edit was done.
0565     if (m_patch && doc->url() != m_patch->file() && !qobject_cast<LocalPatchSource*>(m_patch.data())) {
0566         forceUpdate();
0567     }
0568 }
0569 
0570 void PatchReviewPlugin::textDocumentCreated( IDocument* doc ) {
0571     if (m_patch) {
0572         addHighlighting( doc->url(), doc );
0573     }
0574 }
0575 
0576 void PatchReviewPlugin::unload() {
0577     core()->uiController()->removeToolView( m_factory );
0578 
0579     KDevelop::IPlugin::unload();
0580 }
0581 
0582 void PatchReviewPlugin::areaChanged(Sublime::Area* area)
0583 {
0584     bool reviewing = area->objectName() == QLatin1String("review");
0585     m_finishReview->setEnabled(reviewing);
0586     if(!reviewing) {
0587         closeReview();
0588     }
0589 }
0590 
0591 KDevelop::ContextMenuExtension PatchReviewPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
0592 {
0593     QList<QUrl> urls;
0594 
0595     if ( context->type() == KDevelop::Context::FileContext ) {
0596         auto* filectx = static_cast<KDevelop::FileContext*>(context);
0597         urls = filectx->urls();
0598     } else if ( context->type() == KDevelop::Context::ProjectItemContext ) {
0599         auto* projctx = static_cast<KDevelop::ProjectItemContext*>(context);
0600         const auto items = projctx->items();
0601         for (KDevelop::ProjectBaseItem* item : items) {
0602             if ( item->file() ) {
0603                 urls << item->file()->path().toUrl();
0604             }
0605         }
0606     } else if ( context->type() == KDevelop::Context::EditorContext ) {
0607         auto* econtext = static_cast<KDevelop::EditorContext*>(context);
0608         urls << econtext->url();
0609 
0610         if (urls.constFirst().isEmpty()) {
0611             // This must be an Untitled document. The Review Patch action makes no sense for an unsaved document,
0612             // and triggering it causes an assertion failure in DocumentControllerPrivate::openDocumentInternal().
0613             // Do not add our context menu item to prevent this.
0614             urls.clear();
0615         }
0616     }
0617 
0618     if (urls.size() == 1) {
0619         auto* reviewAction = new QAction(QIcon::fromTheme(QStringLiteral("text-x-patch")),
0620                                          i18nc("@action:inmenu", "Review Patch"), parent);
0621         reviewAction->setData(QVariant(urls[0]));
0622         connect( reviewAction, &QAction::triggered, this, &PatchReviewPlugin::executeFileReviewAction );
0623         ContextMenuExtension cm;
0624         cm.addAction( KDevelop::ContextMenuExtension::VcsGroup, reviewAction );
0625         return cm;
0626     }
0627 
0628     return KDevelop::IPlugin::contextMenuExtension(context, parent);
0629 }
0630 
0631 void PatchReviewPlugin::executeFileReviewAction()
0632 {
0633     auto* reviewAction = qobject_cast<QAction*>(sender());
0634     KDevelop::Path path(reviewAction->data().toUrl());
0635     auto* ps = new LocalPatchSource();
0636     ps->setFilename(path.toUrl());
0637     ps->setBaseDir(path.parent().toUrl());
0638     ps->setAlreadyApplied(true);
0639     ps->createWidget();
0640     startReview(ps, OpenAndRaise);
0641 }
0642 
0643 #include "patchreview.moc"
0644 #include "moc_patchreview.cpp"