Commit 60d3b464 authored by Waqar Ahmed's avatar Waqar Ahmed
Browse files

Add Stage/Unstage/Discard to DiffWidget

parent 77097dee
......@@ -6,6 +6,7 @@
#include "kategitblameplugin.h"
#include "commitfilesview.h"
#include "diffparams.h"
#include <gitprocess.h>
......@@ -587,7 +588,9 @@ void KateGitBlamePluginView::hideToolView()
void KateGitBlamePluginView::showDiffForFile(const QByteArray &diffContents, const QString &file)
{
auto mw = m_mainWindow->window();
QMetaObject::invokeMethod(mw, "showDiff", Q_ARG(QByteArray, diffContents), Q_ARG(QString, file), Q_ARG(QString, {}));
DiffParams d;
d.tabTitle = file;
QMetaObject::invokeMethod(mw, "showDiff", Q_ARG(QByteArray, diffContents), Q_ARG(DiffParams, d));
}
#include "kategitblameplugin.moc"
......@@ -9,6 +9,7 @@
#include "branchdeletedialog.h"
#include "branchesdialog.h"
#include "comparebranchesview.h"
#include "diffparams.h"
#include "git/gitdiff.h"
#include "gitcommitdialog.h"
#include "gitstatusmodel.h"
......@@ -642,7 +643,14 @@ void GitWidget::showDiff(const QString &file, bool staged, bool showInKate)
} else {
if (showInKate) {
auto mw = mainWindow()->window();
QMetaObject::invokeMethod(mw, "showDiff", Q_ARG(QByteArray, git->readAllStandardOutput()), Q_ARG(QString, file), Q_ARG(QString, {}));
DiffParams d;
d.srcFile = file;
d.workingDir = m_activeGitDirPath;
d.arguments = git->arguments();
d.flags.setFlag(DiffParams::Flag::ShowStage, !staged);
d.flags.setFlag(DiffParams::Flag::ShowUnstage, staged);
d.flags.setFlag(DiffParams::Flag::ShowDiscard, !staged);
QMetaObject::invokeMethod(mw, "showDiff", Q_ARG(QByteArray, git->readAllStandardOutput()), Q_ARG(DiffParams, d));
return;
}
auto addContextMenuActions = [this, file, staged](KTextEditor::View *v) {
......
......@@ -7,6 +7,7 @@
#include "kateprojectview.h"
#include "branchcheckoutdialog.h"
#include "diffparams.h"
#include "filehistorywidget.h"
#include "git/gitutils.h"
#include "gitwidget.h"
......@@ -158,8 +159,9 @@ void KateProjectView::showFileGitHistory(const QString &file)
connect(fhs, &FileHistoryWidget::backClicked, this, &KateProjectView::setTreeViewAsCurrent);
connect(fhs, &FileHistoryWidget::commitClicked, this, [this, file](const QByteArray &diff, const QString &commit) {
auto mw = m_pluginView->mainWindow()->window();
const QString name = QStringLiteral("%1[%2]").arg(file, commit);
QMetaObject::invokeMethod(mw, "showDiff", Q_ARG(QByteArray, diff), Q_ARG(QString, name), Q_ARG(QString, {}));
DiffParams d;
d.tabTitle = QStringLiteral("%1[%2]").arg(file, commit);
QMetaObject::invokeMethod(mw, "showDiff", Q_ARG(QByteArray, diff), Q_ARG(DiffParams, d));
});
connect(fhs, &FileHistoryWidget::errorMessage, m_pluginView, [this](const QString &s, bool warn) {
QVariantMap genericMessage;
......
......@@ -22,6 +22,7 @@ target_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/session
${CMAKE_CURRENT_SOURCE_DIR}/quickopen
${CMAKE_CURRENT_SOURCE_DIR}/diff
${CMAKE_CURRENT_BINARY_DIR} # kateprivate_export.h
)
......@@ -111,9 +112,10 @@ target_sources(
data/kateprivate.qrc
hostprocess.cpp
diffwidget.cpp
diffeditor.cpp
difflinenumarea.cpp
diff/diffwidget.cpp
diff/diffeditor.cpp
diff/difflinenumarea.cpp
diff/gitdiff.cpp
)
if(BUILD_TESTING)
......
#include "diffeditor.h"
#include "difflinenumarea.h"
#include "diffwidget.h"
#include "ktexteditor_utils.h"
#include <QMenu>
......@@ -9,9 +10,11 @@
#include <KLocalizedString>
DiffEditor::DiffEditor(QWidget *parent)
DiffEditor::DiffEditor(DiffParams::Flags f, QWidget *parent)
: QPlainTextEdit(parent)
, m_lineNumArea(new LineNumArea(this))
, m_diffWidget(static_cast<DiffWidget *>(parent))
, m_flags(f)
{
setFrameStyle(QFrame::NoFrame);
......@@ -65,27 +68,109 @@ void DiffEditor::resizeEvent(QResizeEvent *event)
updateLineNumAreaGeometry();
}
KTextEditor::Range DiffEditor::selectionRange() const
{
const auto cursor = textCursor();
if (!cursor.hasSelection())
return KTextEditor::Range::invalid();
QTextCursor start = cursor;
start.setPosition(qMin(cursor.selectionStart(), cursor.selectionEnd()));
QTextCursor end = cursor;
end.setPosition(qMax(cursor.selectionStart(), cursor.selectionEnd()));
const int startLine = start.blockNumber();
const int endLine = end.blockNumber();
const int startColumn = start.selectionStart() - start.block().position();
const int endColumn = end.selectionEnd() - end.block().position();
return {startLine, startColumn, endLine, endColumn};
}
void DiffEditor::contextMenuEvent(QContextMenuEvent *e)
{
// Follow KTextEditor behaviour
if (!textCursor().hasSelection()) {
setTextCursor(cursorForPosition(e->pos()));
}
auto menu = createStandardContextMenu();
QAction *before = nullptr;
if (!menu->actions().isEmpty())
before = menu->actions().constFirst();
auto a = new QAction(i18n("Change Style"));
auto styleMenu = new QMenu(this);
styleMenu->addAction(i18n("Side By Side"), this, [this] {
Q_EMIT switchStyle(SideBySide);
});
styleMenu->addAction(i18n("Unified"), this, [this] {
Q_EMIT switchStyle(Unified);
});
styleMenu->addAction(i18n("Raw"), this, [this] {
Q_EMIT switchStyle(Raw);
});
a->setMenu(styleMenu);
menu->insertAction(before, a);
menu->exec(mapToGlobal(e->pos()));
{
auto a = new QAction(i18n("Change Style"));
auto styleMenu = new QMenu(this);
styleMenu->addAction(i18n("Side By Side"), this, [this] {
Q_EMIT switchStyle(SideBySide);
});
styleMenu->addAction(i18n("Unified"), this, [this] {
Q_EMIT switchStyle(Unified);
});
styleMenu->addAction(i18n("Raw"), this, [this] {
Q_EMIT switchStyle(Raw);
});
a->setMenu(styleMenu);
menu->insertAction(before, a);
}
addStageUnstageDiscardActions(menu);
menu->exec(viewport()->mapToGlobal(e->pos()));
}
void DiffEditor::addStageUnstageDiscardActions(QMenu *menu)
{
const auto selection = selectionRange();
const int lineCount = !selection.isValid() ? 1 : selection.numberOfLines() + 1;
int startLine = textCursor().blockNumber();
int endLine = startLine;
if (selection.isValid()) {
startLine = selection.start().line();
endLine = selection.end().line();
}
QAction *before = nullptr;
if (!menu->actions().isEmpty())
before = menu->actions().constFirst();
if (m_flags.testFlag(DiffParams::Flag::ShowStage)) {
auto a = new QAction(i18np("Stage Line", "Stage Lines", lineCount));
connect(a, &QAction::triggered, this, [=] {
Q_EMIT actionTriggered(this, startLine, endLine, (int)Line, DiffParams::Flag::ShowStage);
});
menu->insertAction(before, a);
a = new QAction(i18n("Stage Hunk"));
connect(a, &QAction::triggered, this, [=] {
Q_EMIT actionTriggered(this, startLine, endLine, (int)Hunk, DiffParams::Flag::ShowStage);
});
menu->insertAction(before, a);
}
if (m_flags.testFlag(DiffParams::Flag::ShowDiscard)) {
auto a = new QAction(i18np("Discard Line", "Discard Lines", lineCount));
connect(a, &QAction::triggered, this, [=] {
Q_EMIT actionTriggered(this, startLine, endLine, (int)Line, DiffParams::Flag::ShowDiscard);
});
menu->insertAction(before, a);
a = new QAction(i18n("Discard Hunk"));
connect(a, &QAction::triggered, this, [=] {
Q_EMIT actionTriggered(this, startLine, endLine, (int)Hunk, DiffParams::Flag::ShowDiscard);
});
menu->insertAction(before, a);
}
if (m_flags.testFlag(DiffParams::Flag::ShowUnstage)) {
auto a = new QAction(i18np("Unstage Line", "Unstage Lines", lineCount));
connect(a, &QAction::triggered, this, [=] {
Q_EMIT actionTriggered(this, startLine, endLine, (int)Line, DiffParams::Flag::ShowUnstage);
});
menu->insertAction(before, a);
a = new QAction(i18n("Unstage Hunk"));
connect(a, &QAction::triggered, this, [=] {
Q_EMIT actionTriggered(this, startLine, endLine, (int)Hunk, DiffParams::Flag::ShowUnstage);
});
menu->insertAction(before, a);
}
}
void DiffEditor::updateLineNumberArea(const QRect &rect, int dy)
......@@ -220,3 +305,20 @@ void DiffEditor::setLineNumberData(QVector<int> data, int maxLineNum)
m_lineNumArea->setMaxLineNum(maxLineNum);
updateLineNumberAreaWidth(0);
}
DiffEditor::State DiffEditor::saveState() const
{
return {verticalScrollBar()->value(), textCursor().position()};
}
void DiffEditor::restoreState(State s)
{
if (document() && document()->isEmpty()) {
return;
}
verticalScrollBar()->setValue(qMax(0, s.scrollValue));
auto cursor = textCursor();
cursor.setPosition(qMin(cursor.document()->characterCount(), s.cursorPosition));
setTextCursor(cursor);
}
#ifndef KATE_DIFF_EDITOR
#define KATE_DIFF_EDITOR
#include "diffparams.h"
#include <KTextEditor/Range>
#include <QPlainTextEdit>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
......@@ -28,7 +30,15 @@ class DiffEditor : public QPlainTextEdit
friend class LineNumArea;
public:
DiffEditor(QWidget *parent = nullptr);
enum { Line = 0, Hunk = 1 };
DiffEditor(DiffParams::Flags, QWidget *parent = nullptr);
struct State {
int scrollValue;
int cursorPosition;
};
State saveState() const;
void restoreState(State);
void clearData()
{
......@@ -43,6 +53,8 @@ public:
void setLineNumberData(QVector<int> data, int maxLineNum);
KTextEditor::Range selectionRange() const;
protected:
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent *e) override;
......@@ -55,6 +67,7 @@ private:
void updateLineNumberAreaWidth(int);
void updateDiffColors(bool darkMode);
void onContextMenuRequest();
void addStageUnstageDiscardActions(QMenu *menu);
QVector<LineHighlight> m_data;
QColor red1;
......@@ -62,9 +75,12 @@ private:
QColor green1;
QColor green2;
class LineNumArea *const m_lineNumArea;
class DiffWidget *const m_diffWidget;
DiffParams::Flags m_flags;
Q_SIGNALS:
void switchStyle(int);
void actionTriggered(DiffEditor *e, int startLine, int endLine, int actionType, DiffParams::Flag);
};
#endif
#ifndef KATE_DIFF_PARAMS
#define KATE_DIFF_PARAMS
#include <QMetaType>
#include <QStringList>
struct DiffParams {
enum Flag { ShowStage = 1, ShowUnstage = 2, ShowDiscard = 4 };
Q_DECLARE_FLAGS(Flags, Flag)
Q_FLAGS(Flags)
/**
* The tab title that will get shown in Kate.
* If not specified, srcFile/destFile will be
* used instead
*/
QString tabTitle;
/** Source File **/
QString srcFile;
/** Destination file **/
QString destFile;
/** Working dir i.e., to which
* src/dest files belong and where
* the command was run. This should
* normally be your repo base path
*/
QString workingDir;
/**
* The arguments that were passed to git
* when this diff was generated
*/
QStringList arguments;
/**
* These flags are used internally for e.g.,
* for the context menu actions
*/
Flags flags;
void clear()
{
tabTitle = srcFile = destFile = workingDir = QString();
arguments.clear();
flags = {};
}
};
Q_DECLARE_METATYPE(DiffParams)
#endif
......@@ -3,9 +3,9 @@
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "diffwidget.h"
#include "ktexteditor_utils.h"
#include "gitdiff.h"
#include "gitprocess.h"
#include "ktexteditor_utils.h"
#include <QApplication>
#include <QHBoxLayout>
......@@ -15,6 +15,7 @@
#include <QRegularExpression>
#include <QScrollBar>
#include <QSyntaxHighlighter>
#include <QTemporaryFile>
#include <KConfigGroup>
#include <KSharedConfig>
......@@ -23,23 +24,11 @@
#include <KSyntaxHighlighting/Repository>
#include <KSyntaxHighlighting/SyntaxHighlighter>
std::pair<uint, uint> parseRange(const QString &range)
{
int commaPos = range.indexOf(QLatin1Char(','));
if (commaPos > -1) {
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
return {range.midRef(0, commaPos).toInt(), range.midRef(commaPos + 1).toInt()};
#else
return {QStringView(range).sliced(0, commaPos).toInt(), QStringView(range).sliced(commaPos + 1).toInt()};
#endif
}
return {range.toInt(), 1};
}
DiffWidget::DiffWidget(QWidget *parent)
DiffWidget::DiffWidget(DiffParams p, QWidget *parent)
: QWidget(parent)
, m_left(new DiffEditor(this))
, m_right(new DiffEditor(this))
, m_left(new DiffEditor(p.flags, this))
, m_right(new DiffEditor(p.flags, this))
, m_params(p)
{
auto layout = new QHBoxLayout(this);
layout->addWidget(m_left);
......@@ -61,6 +50,7 @@ DiffWidget::DiffWidget(QWidget *parent)
for (auto *e : {m_left, m_right}) {
connect(e, &DiffEditor::switchStyle, this, &DiffWidget::handleStyleChange);
connect(e, &DiffEditor::actionTriggered, this, &DiffWidget::handleStageUnstage);
}
KSharedConfig::Ptr config = KSharedConfig::openConfig();
......@@ -75,7 +65,9 @@ void DiffWidget::handleStyleChange(int newStyle)
}
m_style = (DiffStyle)newStyle;
const auto diff = m_rawDiff;
const auto params = m_params;
clearData();
m_params = params;
if (m_style == SideBySide) {
m_left->setVisible(true);
......@@ -95,11 +87,151 @@ void DiffWidget::handleStyleChange(int newStyle)
}
}
void DiffWidget::handleStageUnstage(DiffEditor *e, int startLine, int endLine, int actionType, DiffParams::Flag f)
{
if (m_style == SideBySide) {
handleStageUnstage_sideBySide(e, startLine, endLine, actionType, f);
} else if (m_style == Unified) {
handleStageUnstage_unified(startLine, endLine, actionType, f);
} else if (m_style == Raw) {
handleStageUnstage_raw(startLine, endLine, actionType, f);
}
}
void DiffWidget::handleStageUnstage_unified(int startLine, int endLine, int actionType, DiffParams::Flag f)
{
handleStageUnstage_sideBySide(m_left, startLine, endLine, actionType, f);
}
void DiffWidget::handleStageUnstage_sideBySide(DiffEditor *e, int startLine, int endLine, int actionType, DiffParams::Flag flags)
{
bool added = e == m_right;
int diffLine = -1;
if (actionType == DiffEditor::Line) {
for (auto vToD : std::as_const(m_lineToRawDiffLine)) {
if (vToD.line == startLine && vToD.added == added) {
diffLine = vToD.diffLine;
break;
}
}
} else if (actionType == DiffEditor::Hunk) {
// find the first hunk smaller than/equal this line
for (auto it = m_lineToDiffHunkLine.crbegin(); it != m_lineToDiffHunkLine.crend(); ++it) {
if (it->line <= startLine) {
diffLine = it->diffLine;
break;
}
}
}
if (diffLine == -1) {
// Nothing found somehow
return;
}
doStageUnStage(diffLine, diffLine + (endLine - startLine), actionType, flags);
}
void DiffWidget::handleStageUnstage_raw(int startLine, int endLine, int actionType, DiffParams::Flag flags)
{
doStageUnStage(startLine, endLine + (endLine - startLine), actionType, flags);
}
void DiffWidget::doStageUnStage(int startLine, int endLine, int actionType, DiffParams::Flag flags)
{
VcsDiff d;
const auto stageOrDiscard = flags == DiffParams::Flag::ShowDiscard || flags == DiffParams::Flag::ShowUnstage;
const auto dir = stageOrDiscard ? VcsDiff::Reverse : VcsDiff::Forward;
d.setDiff(QString::fromUtf8(m_rawDiff));
const auto x = actionType == DiffEditor::Line ? d.subDiff(startLine, startLine + (endLine - startLine), dir) : d.subDiffHunk(startLine, dir);
if (flags & DiffParams::Flag::ShowStage) {
applyDiff(x.diff(), ApplyFlags::None);
} else if (flags & DiffParams::ShowUnstage) {
applyDiff(x.diff(), ApplyFlags::Staged);
} else if (flags & DiffParams::ShowDiscard) {
applyDiff(x.diff(), ApplyFlags::Discard);
}
}
void DiffWidget::applyDiff(const QString &diff, ApplyFlags flags)
{
// const QString diff = getDiff(v, flags & Hunk, flags & (Staged | Discard));
if (diff.isEmpty()) {
return;
}
QTemporaryFile *file = new QTemporaryFile(this);
if (!file->open()) {
// sendMessage(i18n("Failed to stage selection"), true);
return;
}
file->write(diff.toUtf8());
file->close();
Q_ASSERT(!m_params.workingDir.isEmpty());
QProcess *git = new QProcess(this);
QStringList args;
if (flags & Discard) {
args = QStringList{QStringLiteral("apply"), file->fileName()};
} else {
args = QStringList{QStringLiteral("apply"), QStringLiteral("--index"), QStringLiteral("--cached"), file->fileName()};
}
setupGitProcess(*git, m_params.workingDir, args);
connect(git, &QProcess::finished, this, [=](int exitCode, QProcess::ExitStatus es) {
if (es != QProcess::NormalExit || exitCode != 0) {
onError(git->readAllStandardError(), git->exitCode());
} else {
runGitDiff();
}
delete file;
git->deleteLater();
});
startHostProcess(*git, QProcess::ReadOnly);
}
void DiffWidget::runGitDiff()
{
const QStringList arguments = m_params.arguments;
const QString workingDir = m_params.workingDir;
if (workingDir.isEmpty() || arguments.isEmpty()) {
return;
}
auto leftState = m_left->saveState();
auto rightState = m_right->saveState();
QProcess *git = new QProcess(this);
setupGitProcess(*git, workingDir, arguments);
connect(git, &QProcess::finished, this, [=](int exitCode, QProcess::ExitStatus es) {
const auto params = m_params;
clearData();
m_params = params;
m_left->setUpdatesEnabled(false);
m_right->setUpdatesEnabled(false);
if (es != QProcess::NormalExit || exitCode != 0) {
onError(git->readAllStandardError(), git->exitCode());
} else {
openDiff(git->readAllStandardOutput());
m_left->restoreState(leftState);
m_right->restoreState(rightState);
}
m_left->setUpdatesEnabled(true);
m_right->setUpdatesEnabled(true);
git->deleteLater();
});
startHostProcess(*git, QProcess::ReadOnly);
}
void DiffWidget::clearData()
{
m_left->clearData();
m_right->clearData();
m_rawDiff.clear();
m_lineToRawDiffLine.clear();
m_lineToDiffHunkLine.clear();
m_params.clear();
}
void DiffWidget::diffDocs(KTextEditor::Document *l, KTextEditor::Document *r)
......@@ -277,6 +409,11 @@ void DiffWidget::parseAndShowDiff(const QByteArray &raw)
QString srcFile;
QString tgtFile;
// viewLine => rawDiffLine
QVector<ViewLineToDiffLine> lineToRawDiffLine;
// for Folding/stage/unstage hunk
QVector<ViewLineToDiffLine> linesWithHunkHeading;
int maxLineNoFound = 0;
int lineA = 0;
int lineB = 0;
......@@ -308,6 +445,7 @@ void DiffWidget::parseAndShowDiff(const QByteArray &raw)
lineNumsB.append(-1);
left.append(headingLeft);
right.append(headingRight);
linesWithHunkHeading.append({lineA, i, /*unused*/ false});
lineA++;
lineB++;
......@@ -345,6 +483,7 @@ void DiffWidget::parseAndShowDiff(const QByteArray &raw)
h.changes.push_back({0, l.size()});
rightHlts.push_back(h);
lineNumsB.append(tgtLine++);
lineToRawDiffLine.append({lineB, j, true});
right.append(l);
hunkChangedLinesB.emplace_back(l, lineB, h.added);
......@@ -363,6 +502,7 @@ void DiffWidget::parseAndShowDiff(const QByteArray &raw)
lineNumsA.append(srcLine++);
left.append(l);
hunkChangedLinesA.emplace_back(l, lineA, h.added);
lineToRawDiffLine.append({lineA, j, false});