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"