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"