File indexing completed on 2024-04-28 04:38:49

0001 /*
0002     SPDX-FileCopyrightText: 2020 Jonathan Verner <jonathan@temno.eu>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "diffviewsctrl.h"
0008 
0009 #include <interfaces/icore.h>
0010 #include <interfaces/idocumentcontroller.h>
0011 #include <interfaces/iproject.h>
0012 #include <interfaces/iprojectcontroller.h>
0013 #include <interfaces/iruncontroller.h>
0014 #include <util/path.h>
0015 #include <vcs/vcsdiff.h>
0016 #include <vcs/vcsjob.h>
0017 
0018 #include <KActionCollection>
0019 #include <KColorScheme>
0020 #include <KLocalizedString>
0021 #include <KMessageBox>
0022 #include <KMessageBox_KDevCompat>
0023 #include <KTextEditor/MovingInterface>
0024 #include <KTextEditor/View>
0025 
0026 #include <QAction>
0027 #include <QIcon>
0028 #include <QMenu>
0029 
0030 using namespace KDevelop;
0031 
0032 /**
0033  * A helper function which returns the gitplugin responsible
0034  * for a given url
0035  */
0036 GitPlugin* gitForUrl(const QUrl& url)
0037 {
0038     auto* project = ICore::self()->projectController()->findProjectForUrl(url);
0039     auto* vcsplugin = (project ? project->versionControlPlugin() : nullptr);
0040     return (vcsplugin ? vcsplugin->extension<GitPlugin>() : nullptr);
0041 }
0042 
0043 bool DiffViewsCtrl::ViewData::isValid() const
0044 {
0045     return (project != nullptr && vcs != nullptr && doc != nullptr && ktDoc != nullptr );
0046 }
0047 
0048 DiffViewsCtrl::DiffViewsCtrl(QObject* parent)
0049     : QObject(parent)
0050     , m_stageSelectedAct(
0051           new QAction(QIcon::fromTheme(QStringLiteral("view-add")), i18n("Stage selected lines or hunk"), this))
0052     , m_unstageSelectedAct(
0053           new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Unstage selected lines or hunk"), this))
0054     , m_revertSelectedAct(
0055           new QAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Revert selected lines or hunk"), this))
0056     , m_gotoSrcLineAct(
0057           new QAction(QIcon::fromTheme(QStringLiteral("go-parent-folder")), i18n("Go to line in source"), this))
0058 
0059 {
0060     // Setup style attributes for highlighting diffs
0061     // (green background for +, red background+black fg for -, gray color for @@)
0062     auto colors = KColorScheme();
0063 
0064     // Prevent triggering the actions via shortcuts when a tool view, such as Terminal tool view, has focus.
0065     // KTextEditor::ViewPrivate::setupActions() sets Qt::WidgetWithChildrenShortcut context
0066     // for all actions it adds to the view's actionCollection(). However, only the narrower
0067     // Qt::WidgetShortcut context prevents our actions from stealing key presses from a focused tool view.
0068     for (auto* action : {m_stageSelectedAct, m_unstageSelectedAct, m_revertSelectedAct, m_gotoSrcLineAct}) {
0069         action->setShortcutContext(Qt::WidgetShortcut);
0070     }
0071 
0072     // Connect the diff windows actions
0073     connect(m_stageSelectedAct, &QAction::triggered, this, [=] { applySelected(Stage); });
0074     connect(m_unstageSelectedAct, &QAction::triggered, this, [=] { applySelected(Unstage); });
0075     connect(m_revertSelectedAct, &QAction::triggered, this, &DiffViewsCtrl::revertSelected);
0076     connect(m_gotoSrcLineAct, &QAction::triggered, this, &DiffViewsCtrl::gotoSrcLine);
0077 }
0078 
0079 DiffViewsCtrl::~DiffViewsCtrl()
0080 {
0081     // Close the diff views so that kdevelop does
0082     // not show a lot of Untitled empty tabs on
0083     // when starting again
0084     for (const auto& d : qAsConst(m_views)) {
0085         if (d.second.doc)
0086             d.second.doc->close();
0087     }
0088     m_views.clear();
0089 }
0090 
0091 void DiffViewsCtrl::setupDiffActions(KTextEditor::View* view, const RepoStatusModel::Areas diffType) const
0092 {
0093     // Context Menu Setup
0094     QMenu* ret = new QMenu;
0095     if (diffType == RepoStatusModel::Index || diffType == RepoStatusModel::IndexRoot) {
0096         ret->addAction(m_unstageSelectedAct);
0097     } else if (diffType == RepoStatusModel::WorkTree || diffType == RepoStatusModel::WorkTreeRoot) {
0098         ret->addAction(m_stageSelectedAct);
0099         ret->addAction(m_revertSelectedAct);
0100     }
0101     ret->addAction(m_gotoSrcLineAct);
0102     view->setContextMenu(ret);
0103 
0104     // Set the text of the actions based on whether some lines
0105     // are selected or not
0106     connect(view, &KTextEditor::View::contextMenuAboutToShow, this, [=] {
0107         auto haveSelection = !view->selectionRange().isEmpty();
0108         if (haveSelection) {
0109             m_unstageSelectedAct->setText(i18n("Unstage selected lines"));
0110             m_stageSelectedAct->setText(i18n("Stage selected lines"));
0111             m_revertSelectedAct->setText(i18n("Revert selected lines"));
0112         } else {
0113             m_unstageSelectedAct->setText(i18n("Unstage selected hunk"));
0114             m_stageSelectedAct->setText(i18n("Stage selected hunk"));
0115             m_revertSelectedAct->setText(i18n("Revert selected hunk"));
0116         }
0117     });
0118 
0119     // Add the actions to the view action collection, so that
0120     // shortcuts work and can be edited
0121     auto actCollection = view->actionCollection();
0122     if (diffType == RepoStatusModel::Index || diffType == RepoStatusModel::IndexRoot) {
0123         actCollection->addAction(QStringLiteral("git_unstage_selected"), m_unstageSelectedAct);
0124         actCollection->addAction(QStringLiteral("git_goto_source"), m_gotoSrcLineAct);
0125         actCollection->setDefaultShortcut(m_unstageSelectedAct, i18n("S"));
0126         actCollection->setDefaultShortcut(m_gotoSrcLineAct, i18n("G"));
0127     } else if (diffType == RepoStatusModel::WorkTree || diffType == RepoStatusModel::WorkTreeRoot) {
0128         actCollection->addAction(QStringLiteral("git_stage_selected"), m_stageSelectedAct);
0129         actCollection->addAction(QStringLiteral("git_revert_selected"), m_revertSelectedAct);
0130         actCollection->addAction(QStringLiteral("git_goto_source"), m_gotoSrcLineAct);
0131         actCollection->setDefaultShortcut(m_stageSelectedAct, i18n("S"));
0132         actCollection->setDefaultShortcut(m_gotoSrcLineAct, i18n("G"));
0133     }
0134 }
0135 
0136 const QString DiffViewsCtrl::viewKey(const QUrl& url, RepoStatusModel::Areas area)
0137 {
0138     if (area == RepoStatusModel::WorkTreeRoot || area == RepoStatusModel::IndexRoot) {
0139         if ( auto* project = ICore::self()->projectController()->findProjectForUrl(url) ) {
0140             return project->path().toUrl().toString() + QStringLiteral(":") + QString::number(area);
0141         }
0142         return QStringLiteral(":") + QString::number(area);
0143     }
0144     return url.toString() + QStringLiteral(":") + QString::number(area);
0145 }
0146 
0147 const DiffViewsCtrl::ViewData DiffViewsCtrl::createView(const QUrl& url, RepoStatusModel::Areas area)
0148 {
0149     auto* docCtrl = ICore::self()->documentController();
0150 
0151     // If an appropriate view is already cached
0152     // return it
0153     QString key = viewKey(url, area);
0154     auto viewDataIt = m_views.find(key);
0155     if (viewDataIt != m_views.end()) {
0156         return viewDataIt->second;
0157     }
0158 
0159     // Create a new view and populate the
0160     // ViewData structure
0161     ViewData data;
0162     data.project = ICore::self()->projectController()->findProjectForUrl(url);
0163 
0164     if (data.project == nullptr)
0165         return data;
0166 
0167     data.area = area;
0168     data.doc = docCtrl->openDocumentFromText(QString());
0169     data.ktDoc = data.doc->textDocument();
0170     data.url = url;
0171     data.vcs = gitForUrl(url);
0172 
0173     // Cache the new view data
0174     m_views[key] = data;
0175 
0176     // Set the view title
0177     if (area == RepoStatusModel::Index)
0178         data.doc->setPrettyName(i18n("%1 (staged)", url.fileName()));
0179     else if (area == RepoStatusModel::IndexRoot)
0180         data.doc->setPrettyName(i18n("Staged (%1)", url.fileName()));
0181     else if (area == RepoStatusModel::WorkTreeRoot)
0182         data.doc->setPrettyName(i18n("Unstaged (%1)", url.fileName()));
0183     else if (area == RepoStatusModel::WorkTree)
0184         data.doc->setPrettyName(i18n("%1 (unstaged)", url.fileName()));
0185 
0186     // Connect cleanup handlers on document/project closure and kdevelop shutdown
0187     connect(ICore::self()->projectController(), &IProjectController::projectClosed, this, [=] (KDevelop::IProject* proj) {
0188         if (proj == data.project) {
0189             auto dataIt = m_views.find(key);
0190             if (dataIt != m_views.end())
0191                 dataIt->second.doc->close();
0192         }
0193     });
0194     connect(ICore::self(), &ICore::aboutToShutdown, this, [=] {
0195         auto dataIt = m_views.find(key);
0196         if (dataIt != m_views.end())
0197             dataIt->second.doc->close();
0198     });
0199     connect(data.ktDoc, &KTextEditor::Document::aboutToClose, this, [=]() { m_views.erase(key); });
0200 
0201     // Set the context menu for the document & add the appropriate actions to it
0202     const auto& views = data.ktDoc->views();
0203     for (auto view : views)
0204         setupDiffActions(view, area);
0205 
0206     return data;
0207 }
0208 
0209 const DiffViewsCtrl::ViewData DiffViewsCtrl::activeView()
0210 {
0211     auto view = ICore::self()->documentController()->activeTextDocumentView();
0212     auto doc = view->document();
0213     if (view) {
0214         for (auto data : m_views) {
0215             if (data.second.ktDoc == doc) {
0216                 data.second.actView = view;
0217                 return data.second;
0218             }
0219         }
0220     }
0221     ViewData ret;
0222     return ret;
0223 }
0224 
0225 void DiffViewsCtrl::updateDiff(const QUrl& url, const RepoStatusModel::Areas area, const UpdateDiffParams p)
0226 {
0227     // If p == NoActivate and the url+area has no associated view
0228     // return early
0229     auto key = viewKey(url, area);
0230     if (p == NoActivate && m_views.find(key) == m_views.end())
0231         return;
0232 
0233     if (auto* vcs = gitForUrl(url)) {
0234         VcsRevision src, dst;
0235         if (area == RepoStatusModel::Index || area == RepoStatusModel::IndexRoot) {
0236             dst = VcsRevision::createSpecialRevision(VcsRevision::Working);
0237             src = VcsRevision::createSpecialRevision(VcsRevision::Head);
0238             src.setRevisionValue(QStringLiteral("HEAD"), VcsRevision::Special);
0239         } else if (area == RepoStatusModel::WorkTree || area == RepoStatusModel::WorkTreeRoot) {
0240             dst = VcsRevision::createSpecialRevision(VcsRevision::Base);
0241             src = VcsRevision::createSpecialRevision(VcsRevision::Working);
0242         } else
0243             return;
0244         VcsJob* job = nullptr;
0245         if (area == RepoStatusModel::Index || area == RepoStatusModel::WorkTree)
0246             job = vcs->diff(url, src, dst, IBasicVersionControl::NonRecursive);
0247         else if (area == RepoStatusModel::IndexRoot || area == RepoStatusModel::WorkTreeRoot)
0248             job = vcs->diff(url, src, dst);
0249         if (job) {
0250             job->setProperty("key", QVariant::fromValue<QString>(key));
0251             job->setProperty("url", QVariant::fromValue<QUrl>(url));
0252             job->setProperty("area", area);
0253             job->setProperty("activate", p);
0254             connect(job, &VcsJob::resultsReady, this, &DiffViewsCtrl::diffReady);
0255             ICore::self()->runController()->registerJob(job);
0256         }
0257     }
0258 }
0259 
0260 void DiffViewsCtrl::updateProjectDiffs(KDevelop::IProject* proj)
0261 {
0262     for(auto [_, vData] : m_views) {
0263         Q_UNUSED(_);
0264         if (vData.project == proj)
0265             updateDiff(vData.url, vData.area, UpdateDiffParams::NoActivate);
0266     }
0267 }
0268 
0269 void DiffViewsCtrl::updateUrlDiffs(const QUrl& url)
0270 {
0271     if (auto* project = ICore::self()->projectController()->findProjectForUrl(url)) {
0272         for(auto [_, vData] : m_views) {
0273             Q_UNUSED(_);
0274             if (vData.project != project)
0275                 continue;
0276             if (
0277                 vData.url == url ||
0278                 vData.area == RepoStatusModel::WorkTreeRoot ||
0279                 vData.area == RepoStatusModel::IndexRoot ||
0280                 vData.area == RepoStatusModel::ConflictRoot ||
0281                 vData.area == RepoStatusModel::UntrackedRoot
0282             )
0283                 updateDiff(vData.url, vData.area, UpdateDiffParams::NoActivate);
0284         }
0285     }
0286 }
0287 
0288 
0289 
0290 void DiffViewsCtrl::diffReady(KDevelop::VcsJob* diffJob)
0291 {
0292     if (diffJob->status() == VcsJob::JobSucceeded) {
0293         // Fetch the job results
0294         auto diff = diffJob->fetchResults().value<VcsDiff>();
0295         auto key = diffJob->property("key").toString();
0296         auto p = (UpdateDiffParams)diffJob->property("activate").toInt();
0297 
0298         ViewData vData;
0299         auto vDataIt = m_views.find(key);
0300 
0301         // If the diff is empty, close the view if present
0302         // and return
0303         if (diff.isEmpty()) {
0304             if (vDataIt != m_views.end() && vDataIt->second.doc)
0305                 vDataIt->second.doc->close();
0306             return;
0307         }
0308 
0309         if (vDataIt != m_views.end()) {
0310             vData = vDataIt->second;
0311         } else {
0312             vData = createView(diffJob->property("url").toUrl(),
0313                                (RepoStatusModel::Areas)diffJob->property("area").toInt());
0314             if (! vData.isValid())
0315                 return;
0316         }
0317 
0318         auto position = vData.ktDoc->views().constFirst()->cursorPosition(); // assume there is only one view
0319         vData.ktDoc->setReadWrite(true);
0320         vData.ktDoc->setText(diff.diff());
0321         vData.ktDoc->setReadWrite(false);
0322         vData.ktDoc->setModified(false);
0323         vData.ktDoc->views().constFirst()->setCursorPosition(position); // assume there is only one view
0324         vData.ktDoc->setMode(QStringLiteral("diff"));
0325         vData.ktDoc->setHighlightingMode(QStringLiteral("diff"));
0326 
0327         // Activate the diff document, if required
0328         if (p == Activate) {
0329             auto* docCtrl = ICore::self()->documentController();
0330             docCtrl->activateDocument(vData.doc);
0331         }
0332     }
0333 }
0334 
0335 void DiffViewsCtrl::revertSelected()
0336 {
0337     auto res = KMessageBox::questionTwoActions(
0338         nullptr,
0339         i18n("The selected lines will be reverted and the changes lost. This "
0340              "operation cannot be undone. Continue?"),
0341         {}, KGuiItem(i18nc("@action:button", "Revert"), QStringLiteral("list-remove")), KStandardGuiItem::cancel());
0342     if (res != KMessageBox::PrimaryAction)
0343         return;
0344 
0345     applySelected(Revert);
0346 }
0347 
0348 void DiffViewsCtrl::applySelected(DiffViewsCtrl::ApplyAction act)
0349 {
0350     auto vData = activeView();
0351     if (! vData.isValid() )
0352         return;
0353 
0354     if (vData.area != RepoStatusModel::None) {
0355         // Setup arguments to subDiff & apply based on the required action
0356         auto [direction, params] = [act]() -> std::pair<VcsDiff::DiffDirection, GitPlugin::ApplyParams> {
0357             switch (act) {
0358             case Stage:
0359                 return { VcsDiff::Normal, GitPlugin::Index };
0360             case Unstage:
0361                 return { VcsDiff::Reverse, GitPlugin::Index };
0362             case Revert:
0363                 return { VcsDiff::Reverse, GitPlugin::WorkTree };
0364             }
0365             Q_UNREACHABLE();
0366         }();
0367 
0368         // Construct the selected diff (either from the selected lines
0369         // or the hunk containing the current cursor position)
0370         VcsDiff fullDiff, selectedDiff;
0371         fullDiff.setDiff(vData.ktDoc->text());
0372         fullDiff.setBaseDiff(vData.project->path().toUrl());
0373         auto range = vData.actView->selectionRange();
0374         if (range.isEmpty()) {
0375             selectedDiff = fullDiff.subDiffHunk(vData.actView->cursorPosition().line(), direction);
0376         } else {
0377             int startLine = range.start().line();
0378             int endLine = range.end().line();
0379             selectedDiff = fullDiff.subDiff(startLine, endLine, direction);
0380         }
0381 
0382         // Run the apply job
0383         VcsJob* indexJob = vData.vcs->apply(selectedDiff, params);
0384         connect(indexJob, &VcsJob::resultsReady, this, [=] {
0385             if (indexJob->status() == VcsJob::JobSucceeded) {
0386                 updateUrlDiffs(vData.url);
0387             }
0388         });
0389         ICore::self()->runController()->registerJob(indexJob);
0390     }
0391 }
0392 
0393 void DiffViewsCtrl::gotoSrcLine()
0394 {
0395     auto vData = activeView();
0396     if (!vData.isValid() || !vData.actView)
0397         return;
0398 
0399     auto* docCtrl = ICore::self()->documentController();
0400     auto diffLn = vData.actView->cursorPosition().line();
0401     auto diffCol = vData.actView->cursorPosition().column();
0402     VcsDiff diff;
0403     diff.setDiff(vData.ktDoc->text());
0404 
0405     // Find the closest line in the diff which has a corresponding
0406     // source line
0407     auto last_line = vData.ktDoc->documentEnd().line();
0408     int delta = 0;
0409     while(diffLn - delta >= 1 || diffLn + delta < last_line) {
0410         auto src = diff.diffLineToTarget(diffLn-delta);
0411         if ( src.line < 0 ) src = diff.diffLineToTarget(diffLn+delta);
0412         if ( src.line >= 0 ) {
0413             auto path = KDevelop::Path(vData.project->path(), src.path);
0414             if (auto* srcDoc = docCtrl->openDocument(path.toUrl())) {
0415                 srcDoc->setCursorPosition(KTextEditor::Cursor(src.line, diffCol-1));
0416                 docCtrl->activateDocument(srcDoc);
0417             }
0418             return;
0419         }
0420         delta += 1;
0421     }
0422 }
0423 
0424 #include "moc_diffviewsctrl.cpp"