1 /*
2     SPDX-FileCopyrightText: 2006-2009 David Nolden <david.nolden.kdevelop@art-master.de>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "patchreview.h"
8 
9 #include <QDir>
10 #include <QFileInfo>
11 #include <QStandardPaths>
12 #include <QTimer>
13 #include <QMimeDatabase>
14 
15 #include <KActionCollection>
16 #include <KLocalizedString>
17 #include <KPluginFactory>
18 #include <KMessageBox>
19 #include <KIO/CopyJob>
20 
21 #include <interfaces/idocument.h>
22 #include <interfaces/icore.h>
23 #include <interfaces/idocumentcontroller.h>
24 #include <interfaces/iuicontroller.h>
25 #include <interfaces/contextmenuextension.h>
26 #include <interfaces/context.h>
27 #include <interfaces/editorcontext.h>
28 
29 #include <project/projectmodel.h>
30 
31 #include <sublime/message.h>
32 #include <util/path.h>
33 
34 #include <libkomparediff2/komparemodellist.h>
35 #include <libkomparediff2/kompare.h>
36 #include <libkomparediff2/diffsettings.h>
37 
38 #include <KTextEditor/Document>
39 #include <KTextEditor/ModificationInterface>
40 #include <KTextEditor/MovingRange>
41 #include <KTextEditor/View>
42 
43 ///Whether arbitrary exceptions that occurred while diff-parsing within the library should be caught
44 #define CATCHLIBDIFF
45 
46 /* Exclude this file from doublequote_chars check as krazy doesn't understand
47    std::string*/
48 //krazy:excludeall=doublequote_chars
49 #include <sublime/controller.h>
50 #include <sublime/mainwindow.h>
51 #include <sublime/area.h>
52 #include <sublime/document.h>
53 #include <sublime/view.h>
54 #include <vcs/widgets/vcsdiffpatchsources.h>
55 #include "patchhighlighter.h"
56 #include "patchreviewtoolview.h"
57 #include "localpatchsource.h"
58 #include "debug.h"
59 
60 
61 using namespace KDevelop;
62 
63 namespace
64 {
65 // Maximum number of files to open directly within a tab when the review is started
66 const int maximumFilesToOpenDirectly = 15;
67 }
68 
seekHunk(bool forwards,const QUrl & fileName)69 void PatchReviewPlugin::seekHunk( bool forwards, const QUrl& fileName ) {
70     try {
71         qCDebug(PLUGIN_PATCHREVIEW) << forwards << fileName << fileName.isEmpty();
72         if ( !m_modelList )
73             throw "no model";
74 
75         for ( int a = 0; a < m_modelList->modelCount(); ++a ) {
76             const Diff2::DiffModel* model = m_modelList->modelAt( a );
77             if ( !model || !model->differences() )
78                 continue;
79 
80             QUrl file = urlForFileModel( model );
81 
82             if ( !fileName.isEmpty() && fileName != file )
83                 continue;
84 
85             IDocument* doc = ICore::self()->documentController()->documentForUrl( file );
86 
87             if ( doc && m_highlighters.contains( doc->url() ) && m_highlighters[doc->url()] ) {
88                 if ( doc->textDocument() ) {
89                     const QList<KTextEditor::MovingRange*> ranges = m_highlighters[doc->url()]->ranges();
90 
91                     KTextEditor::View * v = doc->activeTextView();
92                     if ( v ) {
93                         int bestLine = -1;
94                         KTextEditor::Cursor c = v->cursorPosition();
95                         for (auto* range : ranges) {
96                             const int line = range->start().line();
97 
98                             if ( forwards ) {
99                                 if ( line > c.line() && ( bestLine == -1 || line < bestLine ) )
100                                     bestLine = line;
101                             } else {
102                                 if ( line < c.line() && ( bestLine == -1 || line > bestLine ) )
103                                     bestLine = line;
104                             }
105                         }
106                         if ( bestLine != -1 ) {
107                             v->setCursorPosition( KTextEditor::Cursor( bestLine, 0 ) );
108                             return;
109                         } else if(fileName.isEmpty()) {
110                             int next = qBound(0, forwards ? a+1 : a-1, m_modelList->modelCount()-1);
111                             if (next < maximumFilesToOpenDirectly) {
112                                 ICore::self()->documentController()->openDocument(urlForFileModel(m_modelList->modelAt(next)));
113                             }
114                         }
115                     }
116                 }
117             }
118         }
119     } catch ( const QString & str ) {
120         qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str;
121     } catch ( const char * str ) {
122         qCDebug(PLUGIN_PATCHREVIEW) << "seekHunk():" << str;
123     }
124     qCDebug(PLUGIN_PATCHREVIEW) << "no matching hunk found";
125 }
126 
addHighlighting(const QUrl & highlightFile,IDocument * document)127 void PatchReviewPlugin::addHighlighting( const QUrl& highlightFile, IDocument* document ) {
128     try {
129         if ( !modelList() )
130             throw "no model";
131 
132         for ( int a = 0; a < modelList()->modelCount(); ++a ) {
133             Diff2::DiffModel* model = modelList()->modelAt( a );
134             if ( !model )
135                 continue;
136 
137             QUrl file = urlForFileModel( model );
138 
139             if ( file != highlightFile )
140                 continue;
141 
142             qCDebug(PLUGIN_PATCHREVIEW) << "highlighting" << file.toDisplayString();
143 
144             IDocument* doc = document;
145             if( !doc )
146                 doc = ICore::self()->documentController()->documentForUrl( file );
147 
148             qCDebug(PLUGIN_PATCHREVIEW) << "highlighting file" << file << "with doc" << doc;
149 
150             if ( !doc || !doc->textDocument() )
151                 continue;
152 
153             removeHighlighting( file );
154 
155             m_highlighters[file] = new PatchHighlighter(model, doc, this, (qobject_cast<LocalPatchSource*>(m_patch.data()) == nullptr));
156         }
157     } catch ( const QString & str ) {
158         qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
159     } catch ( const char * str ) {
160         qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
161     }
162 }
163 
highlightPatch()164 void PatchReviewPlugin::highlightPatch() {
165     try {
166         if ( !modelList() )
167             throw "no model";
168 
169         for ( int a = 0; a < modelList()->modelCount(); ++a ) {
170             const Diff2::DiffModel* model = modelList()->modelAt( a );
171             if ( !model )
172                 continue;
173 
174             QUrl file = urlForFileModel( model );
175 
176             addHighlighting( file );
177         }
178     } catch ( const QString & str ) {
179         qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
180     } catch ( const char * str ) {
181         qCDebug(PLUGIN_PATCHREVIEW) << "highlightFile():" << str;
182     }
183 }
184 
removeHighlighting(const QUrl & file)185 void PatchReviewPlugin::removeHighlighting( const QUrl& file ) {
186     if ( file.isEmpty() ) {
187         ///Remove all highlighting
188         qDeleteAll( m_highlighters );
189         m_highlighters.clear();
190     } else {
191         HighlightMap::iterator it = m_highlighters.find( file );
192         if ( it != m_highlighters.end() ) {
193             delete *it;
194             m_highlighters.erase( it );
195         }
196     }
197 }
198 
notifyPatchChanged()199 void PatchReviewPlugin::notifyPatchChanged() {
200     if (m_patch) {
201         qCDebug(PLUGIN_PATCHREVIEW) << "notifying patch change: " << m_patch->file();
202         m_updateKompareTimer->start();
203     } else {
204         m_updateKompareTimer->stop();
205     }
206 }
207 
forceUpdate()208 void PatchReviewPlugin::forceUpdate() {
209     if( m_patch ) {
210         // don't trigger an update if we know the plugin cannot update itself
211         auto* vcsPatch = qobject_cast<VCSDiffPatchSource*>(m_patch.data());
212         if (!vcsPatch || vcsPatch->m_updater) {
213             m_patch->update();
214             notifyPatchChanged();
215         }
216     }
217 }
218 
updateKompareModel()219 void PatchReviewPlugin::updateKompareModel() {
220     if ( !m_patch ) {
221         ///TODO: this method should be cleaned up, it can be called by the timer and
222         ///      e.g. https://bugs.kde.org/show_bug.cgi?id=267187 shows how it could
223         ///      lead to asserts before...
224         return;
225     }
226 
227     qCDebug(PLUGIN_PATCHREVIEW) << "updating model";
228     removeHighlighting();
229     m_modelList.reset( nullptr );
230     m_depth = 0;
231     delete m_diffSettings;
232     {
233         IDocument* patchDoc = ICore::self()->documentController()->documentForUrl( m_patch->file() );
234         if( patchDoc )
235             patchDoc->reload();
236     }
237 
238     QString patchFile;
239     if( m_patch->file().isLocalFile() )
240         patchFile = m_patch->file().toLocalFile();
241     else if( m_patch->file().isValid() && !m_patch->file().isEmpty() ) {
242         patchFile = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
243         bool ret = KIO::copy(m_patch->file(), QUrl::fromLocalFile(patchFile), KIO::HideProgressInfo)->exec();
244         if( !ret ) {
245             qCWarning(PLUGIN_PATCHREVIEW) << "Problem while downloading: " << m_patch->file() << "to" << patchFile;
246             patchFile.clear();
247         }
248     }
249 
250     if (!patchFile.isEmpty()) //only try to construct the model if we have a patch to load
251     try {
252         m_diffSettings = new DiffSettings( nullptr );
253         m_kompareInfo.reset( new Kompare::Info() );
254         m_kompareInfo->localDestination = patchFile;
255         m_kompareInfo->localSource = m_patch->baseDir().toLocalFile();
256         m_kompareInfo->depth = m_patch->depth();
257         m_kompareInfo->applied = m_patch->isAlreadyApplied();
258 
259         m_modelList.reset( new Diff2::KompareModelList( m_diffSettings.data(), new QWidget, this ) );
260         m_modelList->slotKompareInfo( m_kompareInfo.data() );
261 
262         try {
263             m_modelList->openDirAndDiff();
264         } catch ( const QString & /*str*/ ) {
265             throw;
266         } catch ( ... ) {
267             throw QStringLiteral( "lib/libdiff2 crashed, memory may be corrupted. Please restart kdevelop." );
268         }
269 
270         for (m_depth = 0; m_depth < 10; ++m_depth) {
271             bool allFound = true;
272             for( int i = 0; i < m_modelList->modelCount(); i++ ) {
273                 if (!QFile::exists(urlForFileModel(m_modelList->modelAt(i)).toLocalFile())) {
274                     allFound = false;
275                 }
276             }
277             if (allFound) {
278                 break; // found depth
279             }
280         }
281 
282         emit patchChanged();
283 
284         for( int i = 0; i < m_modelList->modelCount(); i++ ) {
285             const Diff2::DiffModel* model = m_modelList->modelAt( i );
286             for (auto* difference : *model->differences()) {
287                 difference->apply(m_patch->isAlreadyApplied());
288             }
289         }
290 
291         highlightPatch();
292 
293         return;
294     } catch ( const QString & str ) {
295         KMessageBox::error(nullptr, str, i18nc("@title:window", "Kompare Model Update"));
296     } catch ( const char * str ) {
297         KMessageBox::error(nullptr, QLatin1String(str), i18nc("@title:window", "Kompare Model Update"));
298     }
299     removeHighlighting();
300     m_modelList.reset( nullptr );
301     m_depth = 0;
302     m_kompareInfo.reset( nullptr );
303     delete m_diffSettings;
304 
305     emit patchChanged();
306 }
307 
308 K_PLUGIN_FACTORY_WITH_JSON(KDevPatchReviewFactory, "kdevpatchreview.json",
309                            registerPlugin<PatchReviewPlugin>();)
310 
311 class PatchReviewToolViewFactory : public KDevelop::IToolViewFactory
312 {
313 public:
PatchReviewToolViewFactory(PatchReviewPlugin * plugin)314     explicit PatchReviewToolViewFactory( PatchReviewPlugin *plugin ) : m_plugin( plugin ) {}
315 
create(QWidget * parent=nullptr)316     QWidget* create( QWidget *parent = nullptr ) override {
317         return new PatchReviewToolView( parent, m_plugin );
318     }
319 
defaultPosition() const320     Qt::DockWidgetArea defaultPosition() const override
321     {
322         return Qt::BottomDockWidgetArea;
323     }
324 
id() const325     QString id() const override {
326         return QStringLiteral("org.kdevelop.PatchReview");
327     }
328 
329 private:
330     PatchReviewPlugin *m_plugin;
331 };
332 
~PatchReviewPlugin()333 PatchReviewPlugin::~PatchReviewPlugin()
334 {
335     removeHighlighting();
336 
337     // Tweak to work around a crash on OS X; see https://bugs.kde.org/show_bug.cgi?id=338829
338     // and http://qt-project.org/forums/viewthread/38406/#162801
339     // modified tweak: use setPatch() and deleteLater in that method.
340     setPatch(nullptr);
341 }
342 
closeReview()343 void PatchReviewPlugin::closeReview()
344 {
345     if( m_patch ) {
346         IDocument* patchDocument = ICore::self()->documentController()->documentForUrl( m_patch->file() );
347         if (patchDocument) {
348             // Revert modifications to the text document which we've done in updateReview
349             patchDocument->setPrettyName( QString() );
350             patchDocument->textDocument()->setReadWrite( true );
351             auto* modif = qobject_cast<KTextEditor::ModificationInterface*>(patchDocument->textDocument());
352             modif->setModifiedOnDiskWarning( true );
353         }
354 
355         removeHighlighting();
356         m_modelList.reset( nullptr );
357         m_depth = 0;
358 
359         if (!qobject_cast<LocalPatchSource*>(m_patch.data())) {
360             // make sure "show" button still openes the file dialog to open a custom patch file
361             setPatch( new LocalPatchSource );
362         } else
363             emit patchChanged();
364 
365         auto oldArea = ICore::self()->uiController()->activeArea();
366         if (oldArea->objectName() == QLatin1String("review")) {
367             if (ICore::self()->documentController()->saveAllDocumentsForWindow(ICore::self()->uiController()->activeMainWindow(),
368                                                                                IDocument::Default, true))
369             {
370                 ICore::self()->uiController()->switchToArea(m_lastArea.isEmpty() ? QStringLiteral("code") : m_lastArea,
371                                                             KDevelop::IUiController::ThisWindow);
372                 if (oldArea->workingSetPersistent()) {
373                     ICore::self()->uiController()->activeArea()->setWorkingSet(oldArea->workingSet(), true, oldArea);
374                 }
375             }
376         }
377     }
378 }
379 
cancelReview()380 void PatchReviewPlugin::cancelReview() {
381     if( m_patch ) {
382         m_patch->cancelReview();
383         closeReview();
384     }
385 }
386 
finishReview(const QList<QUrl> & selection)387 void PatchReviewPlugin::finishReview(const QList<QUrl>& selection)
388 {
389     if( m_patch && m_patch->finishReview( selection ) ) {
390         closeReview();
391     }
392 }
393 
startReview(IPatchSource * patch,IPatchReview::ReviewMode mode)394 void PatchReviewPlugin::startReview( IPatchSource* patch, IPatchReview::ReviewMode mode ) {
395     Q_UNUSED( mode );
396     emit startingNewReview();
397     setPatch( patch );
398     QMetaObject::invokeMethod(this, &PatchReviewPlugin::updateReview, Qt::QueuedConnection);
399 }
400 
switchToEmptyReviewArea()401 void PatchReviewPlugin::switchToEmptyReviewArea()
402 {
403     const auto allAreas = ICore::self()->uiController()->allAreas();
404     for (Sublime::Area* area : allAreas) {
405         if (area->objectName() == QLatin1String("review")) {
406             area->setWorkingSet(QString(), false);
407         }
408     }
409 
410     QString areaName = ICore::self()->uiController()->activeArea()->objectName();
411     if (areaName != QLatin1String("review")) {
412         m_lastArea = areaName;
413         ICore::self()->uiController()->switchToArea(QStringLiteral("review"), KDevelop::IUiController::ThisWindow);
414     } else {
415         m_lastArea.clear();
416     }
417 }
418 
urlForFileModel(const Diff2::DiffModel * model)419 QUrl PatchReviewPlugin::urlForFileModel( const Diff2::DiffModel* model )
420 {
421     KDevelop::Path path(QDir::cleanPath(m_patch->baseDir().toLocalFile()));
422     QVector<QString> destPath = KDevelop::Path(QLatin1Char('/') + model->destinationPath()).segments();
423     if (destPath.size() >= (int)m_depth) {
424         destPath.remove(0, m_depth);
425     }
426     for (const QString& segment : qAsConst(destPath)) {
427         path.addPath(segment);
428     }
429     path.addPath(model->destinationFile());
430 
431     return path.toUrl();
432 }
433 
updateReview()434 void PatchReviewPlugin::updateReview()
435 {
436     if( !m_patch )
437         return;
438 
439     m_updateKompareTimer->stop();
440 
441     switchToEmptyReviewArea();
442 
443     KDevelop::IDocumentController *docController = ICore::self()->documentController();
444     // don't add documents opened automatically to the Files/Open Recent list
445     IDocument* futureActiveDoc = docController->openDocument( m_patch->file(), KTextEditor::Range::invalid(),
446                                                               IDocumentController::DoNotAddToRecentOpen );
447 
448     updateKompareModel();
449 
450     if ( !m_modelList || !futureActiveDoc || !futureActiveDoc->textDocument() ) {
451         // might happen if e.g. openDocument dialog was cancelled by user
452         // or under the theoretic possibility of a non-text document getting opened
453         return;
454     }
455 
456     futureActiveDoc->textDocument()->setReadWrite( false );
457     futureActiveDoc->setPrettyName(i18nc("@title complete patch", "Overview"));
458     auto* modif = qobject_cast<KTextEditor::ModificationInterface*>(futureActiveDoc->textDocument());
459     modif->setModifiedOnDiskWarning( false );
460 
461     docController->activateDocument( futureActiveDoc );
462 
463     auto* toolView = qobject_cast<PatchReviewToolView*>(ICore::self()->uiController()->findToolView(i18nc("@title:window", "Patch Review"), m_factory));
464     Q_ASSERT( toolView );
465 
466     //Open all relates files
467     for( int a = 0; a < m_modelList->modelCount() && a < maximumFilesToOpenDirectly; ++a ) {
468         QUrl absoluteUrl = urlForFileModel( m_modelList->modelAt( a ) );
469         if (absoluteUrl.isRelative()) {
470             const QString messageText = i18n("The base directory of the patch must be an absolute directory.");
471             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
472             ICore::self()->uiController()->postMessage(message);
473             break;
474         }
475 
476         if( QFileInfo::exists( absoluteUrl.toLocalFile() ) && absoluteUrl.toLocalFile() != QLatin1String("/dev/null") )
477         {
478             toolView->open( absoluteUrl, false );
479         }else{
480             // Maybe the file was deleted
481             qCDebug(PLUGIN_PATCHREVIEW) << "could not open" << absoluteUrl << "because it doesn't exist";
482         }
483     }
484 }
485 
setPatch(IPatchSource * patch)486 void PatchReviewPlugin::setPatch( IPatchSource* patch ) {
487     if ( patch == m_patch ) {
488         return;
489     }
490 
491     if( m_patch ) {
492         disconnect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged );
493         m_patch->deleteLater();
494     }
495     m_patch = patch;
496 
497     if( m_patch ) {
498         qCDebug(PLUGIN_PATCHREVIEW) << "setting new patch" << patch->name() << "with file" << patch->file() << "basedir" << patch->baseDir();
499 
500         connect( m_patch.data(), &IPatchSource::patchChanged, this, &PatchReviewPlugin::notifyPatchChanged );
501     }
502     QString finishText = i18nc("@action", "Finish Review");
503     if( m_patch && !m_patch->finishReviewCustomText().isEmpty() )
504       finishText = m_patch->finishReviewCustomText();
505     m_finishReview->setText( finishText );
506     m_finishReview->setEnabled( patch );
507 
508     notifyPatchChanged();
509 }
510 
PatchReviewPlugin(QObject * parent,const QVariantList &)511 PatchReviewPlugin::PatchReviewPlugin( QObject *parent, const QVariantList & )
512     : KDevelop::IPlugin( QStringLiteral("kdevpatchreview"), parent ),
513     m_patch( nullptr ), m_factory( new PatchReviewToolViewFactory( this ) )
514 {
515     qRegisterMetaType<const Diff2::DiffModel*>( "const Diff2::DiffModel*" );
516 
517     setXMLFile( QStringLiteral("kdevpatchreview.rc") );
518 
519     connect( ICore::self()->documentController(), &IDocumentController::documentClosed, this, &PatchReviewPlugin::documentClosed );
520     connect( ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this, &PatchReviewPlugin::textDocumentCreated );
521     connect( ICore::self()->documentController(), &IDocumentController::documentSaved, this, &PatchReviewPlugin::documentSaved );
522 
523     m_updateKompareTimer = new QTimer( this );
524     m_updateKompareTimer->setSingleShot( true );
525     m_updateKompareTimer->setInterval(500);
526     connect( m_updateKompareTimer, &QTimer::timeout, this, &PatchReviewPlugin::updateKompareModel );
527 
528     m_finishReview = new QAction(i18nc("@action", "Finish Review"), this);
529     m_finishReview->setIcon( QIcon::fromTheme( QStringLiteral("dialog-ok") ) );
530     actionCollection()->setDefaultShortcut( m_finishReview, Qt::CTRL|Qt::Key_Return );
531     actionCollection()->addAction(QStringLiteral("commit_or_finish_review"), m_finishReview);
532 
533     const auto allAreas = ICore::self()->uiController()->allAreas();
534     for (Sublime::Area* area : allAreas) {
535         if (area->objectName() == QLatin1String("review"))
536             area->addAction(m_finishReview);
537     }
538 
539     core()->uiController()->addToolView(i18nc("@title:window", "Patch Review"), m_factory, IUiController::None);
540 
541     areaChanged(ICore::self()->uiController()->activeArea());
542 }
543 
documentClosed(IDocument * doc)544 void PatchReviewPlugin::documentClosed( IDocument* doc ) {
545     removeHighlighting( doc->url() );
546 }
547 
documentSaved(IDocument * doc)548 void PatchReviewPlugin::documentSaved( IDocument* doc ) {
549     // Only update if the url is not the patch-file, because our call to
550     // the reload() KTextEditor function also causes this signal,
551     // which would lead to an endless update loop.
552     // Also, don't automatically update local patch sources, because
553     // they may correspond to static files which don't match any more
554     // after an edit was done.
555     if (m_patch && doc->url() != m_patch->file() && !qobject_cast<LocalPatchSource*>(m_patch.data())) {
556         forceUpdate();
557     }
558 }
559 
textDocumentCreated(IDocument * doc)560 void PatchReviewPlugin::textDocumentCreated( IDocument* doc ) {
561     if (m_patch) {
562         addHighlighting( doc->url(), doc );
563     }
564 }
565 
unload()566 void PatchReviewPlugin::unload() {
567     core()->uiController()->removeToolView( m_factory );
568 
569     KDevelop::IPlugin::unload();
570 }
571 
areaChanged(Sublime::Area * area)572 void PatchReviewPlugin::areaChanged(Sublime::Area* area)
573 {
574     bool reviewing = area->objectName() == QLatin1String("review");
575     m_finishReview->setEnabled(reviewing);
576     if(!reviewing) {
577         closeReview();
578     }
579 }
580 
contextMenuExtension(KDevelop::Context * context,QWidget * parent)581 KDevelop::ContextMenuExtension PatchReviewPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
582 {
583     QList<QUrl> urls;
584 
585     if ( context->type() == KDevelop::Context::FileContext ) {
586         auto* filectx = static_cast<KDevelop::FileContext*>(context);
587         urls = filectx->urls();
588     } else if ( context->type() == KDevelop::Context::ProjectItemContext ) {
589         auto* projctx = static_cast<KDevelop::ProjectItemContext*>(context);
590         const auto items = projctx->items();
591         for (KDevelop::ProjectBaseItem* item : items) {
592             if ( item->file() ) {
593                 urls << item->file()->path().toUrl();
594             }
595         }
596     } else if ( context->type() == KDevelop::Context::EditorContext ) {
597         auto* econtext = static_cast<KDevelop::EditorContext*>(context);
598         urls << econtext->url();
599     }
600 
601     if (urls.size() == 1) {
602         auto* reviewAction = new QAction(QIcon::fromTheme(QStringLiteral("text-x-patch")),
603                                          i18nc("@action:inmenu", "Review Patch"), parent);
604         reviewAction->setData(QVariant(urls[0]));
605         connect( reviewAction, &QAction::triggered, this, &PatchReviewPlugin::executeFileReviewAction );
606         ContextMenuExtension cm;
607         cm.addAction( KDevelop::ContextMenuExtension::VcsGroup, reviewAction );
608         return cm;
609     }
610 
611     return KDevelop::IPlugin::contextMenuExtension(context, parent);
612 }
613 
executeFileReviewAction()614 void PatchReviewPlugin::executeFileReviewAction()
615 {
616     auto* reviewAction = qobject_cast<QAction*>(sender());
617     KDevelop::Path path(reviewAction->data().toUrl());
618     auto* ps = new LocalPatchSource();
619     ps->setFilename(path.toUrl());
620     ps->setBaseDir(path.parent().toUrl());
621     ps->setAlreadyApplied(true);
622     ps->createWidget();
623     startReview(ps, OpenAndRaise);
624 }
625 
626 #include "patchreview.moc"
627