Commit 7de7ca01 authored by Waqar Ahmed's avatar Waqar Ahmed Committed by Christoph Cullmann
Browse files

Add a diff viewer to kate

For now it can only handle word-diffs and doesn't have any features
besides viewing diffs
parent e161dcb9
......@@ -623,12 +623,15 @@ void GitWidget::openAtHEAD(const QString &file)
startHostProcess(*git, QProcess::ReadOnly);
}
void GitWidget::showDiff(const QString &file, bool staged)
void GitWidget::showDiff(const QString &file, bool staged, bool showInKate)
{
auto args = QStringList{QStringLiteral("diff")};
if (staged) {
args.append(QStringLiteral("--staged"));
}
if (showInKate) {
args.append(QStringLiteral("--word-diff=porcelain"));
}
if (!file.isEmpty()) {
args.append(QStringLiteral("--"));
......@@ -636,10 +639,15 @@ void GitWidget::showDiff(const QString &file, bool staged)
}
auto git = gitp(args);
connect(git, &QProcess::finished, this, [this, file, staged, git](int exitCode, QProcess::ExitStatus es) {
connect(git, &QProcess::finished, this, [this, file, staged, git, showInKate](int exitCode, QProcess::ExitStatus es) {
if (es != QProcess::NormalExit || exitCode != 0) {
sendMessage(i18n("Failed to get Diff of file: %1", QString::fromUtf8(git->readAllStandardError())), true);
} else {
if (showInKate) {
auto mw = mainWindow()->window();
QMetaObject::invokeMethod(mw, "showWordDiff", Q_ARG(QByteArray, git->readAllStandardOutput()), Q_ARG(QString, file), Q_ARG(QString, {}));
return;
}
auto addContextMenuActions = [this, file, staged](KTextEditor::View *v) {
QMenu *menu = new QMenu(v);
if (!staged) {
......@@ -781,7 +789,7 @@ void GitWidget::applyDiff(const QString &fileName, ApplyFlags flags, KTextEditor
} else {
// close and reopen doc to show updated diff
if (v && v->document()) {
showDiff(fileName, flags & Staged);
showDiff(fileName, flags & Staged, false);
}
// must come at the end
QTimer::singleShot(10, this, [this] {
......@@ -1173,6 +1181,7 @@ void GitWidget::treeViewContextMenuEvent(QContextMenuEvent *e)
const bool untracked = statusItemType == GitStatusModel::NodeUntrack;
auto openFile = menu.addAction(i18n("Open File"));
auto diff = untracked ? nullptr : menu.addAction(QIcon::fromTheme(QStringLiteral("vcs-diff")), i18n("Diff"));
auto showDiffAct = untracked ? nullptr : menu.addAction(QIcon::fromTheme(QStringLiteral("vcs-diff")), i18n("Show Raw Diff"));
auto launchDifftoolAct = untracked ? nullptr : menu.addAction(QIcon::fromTheme(QStringLiteral("kdiff3")), i18n("Show in External Git Diff Tool"));
auto openAtHead = untracked ? nullptr : menu.addAction(i18n("Open at HEAD"));
......@@ -1200,8 +1209,8 @@ void GitWidget::treeViewContextMenuEvent(QContextMenuEvent *e)
}
} else if (act == openAtHead && !untracked) {
openAtHEAD(idx.data(GitStatusModel::FileNameRole).toString());
} else if (showDiffAct && act == showDiffAct && !untracked) {
showDiff(file, staged);
} else if ((showDiffAct || diff) && (act == showDiffAct || act == diff) && !untracked) {
showDiff(file, staged, /*showInKate=*/act == diff);
} else if (act == discardAct && untracked) {
auto ret = confirm(this, i18n("Are you sure you want to remove this file?"), KStandardGuiItem::remove());
if (ret == KMessageBox::Yes) {
......
......@@ -124,7 +124,7 @@ private:
void discard(const QStringList &files);
void clean(const QStringList &files);
void openAtHEAD(const QString &file);
void showDiff(const QString &file, bool staged);
void showDiff(const QString &file, bool staged, bool showInKate = false);
void launchExternalDiffTool(const QString &file, bool staged);
void commitChanges(const QString &msg, const QString &desc, bool signOff, bool amend = false);
enum ApplyFlags { None = 0, Staged = 1, Hunk = 2, Discard = 4 };
......
......@@ -110,6 +110,8 @@ target_sources(
data/kateprivate.qrc
hostprocess.cpp
diffwidget.cpp
)
if(BUILD_TESTING)
......
/*
SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "diffwidget.h"
#include "gitprocess.h"
#include "ktexteditor_utils.h"
#include <QApplication>
#include <QHBoxLayout>
#include <QPainter>
#include <QPainterPath>
#include <QRegularExpression>
#include <QScrollBar>
#include <QSyntaxHighlighter>
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Format>
#include <KSyntaxHighlighting/Repository>
#include <KSyntaxHighlighting/SyntaxHighlighter>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
using IntT = qsizetype;
#else
using IntT = int;
#endif
struct Change {
IntT pos;
IntT len;
};
struct LineHilight {
QVector<Change> changes;
IntT line;
bool added;
};
class DiffHighlighter : public QSyntaxHighlighter
{
public:
DiffHighlighter(QTextDocument *parent)
: QSyntaxHighlighter(parent)
{
}
void highlightBlock(const QString &) override
{
auto block = currentBlock();
int num = block.blockNumber();
auto it = std::find_if(data.cbegin(), data.cend(), [num](LineHilight hl) {
return hl.line == num;
});
if (it != data.cend()) {
QColor color = it->added ? Qt::green : Qt::red;
const auto changes = it->changes;
for (const auto c : changes) {
setFormat(c.pos, c.len, color);
}
}
}
void clearData()
{
data.clear();
}
void appendData(const QVector<LineHilight> &newData)
{
data.append(newData);
}
private:
QVector<LineHilight> data;
};
class DiffEditor : public QPlainTextEdit
{
public:
DiffEditor(QWidget *parent = nullptr)
: QPlainTextEdit(parent)
{
red1 = QColor("#c87872");
red1.setAlphaF(0.2);
green1 = QColor("#678528");
green1.setAlphaF(0.2);
auto c = QColor(254, 147, 140);
c.setAlphaF(0.1);
red2 = c;
c = QColor(166, 226, 46);
c.setAlphaF(0.1);
green2 = c;
auto updateEditorColors = [this](KTextEditor::Editor *e) {
if (!e)
return;
auto theme = e->theme();
auto bg = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::BackgroundColor));
auto fg = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::TextStyle::Normal));
auto sel = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::TextSelection));
auto pal = palette();
pal.setColor(QPalette::Base, bg);
pal.setColor(QPalette::Text, fg);
pal.setColor(QPalette::Highlight, sel);
pal.setColor(QPalette::HighlightedText, fg);
setPalette(pal);
};
connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateEditorColors);
updateEditorColors(KTextEditor::Editor::instance());
}
void paintEvent(QPaintEvent *e) override
{
bool textPainted = false;
if (!getPaintContext().selections.isEmpty()) {
QPlainTextEdit::paintEvent(e);
textPainted = true;
}
QPainter p(viewport());
QPointF offset(contentOffset());
QTextBlock block = firstVisibleBlock();
const auto viewportRect = viewport()->rect();
while (block.isValid()) {
QRectF r = blockBoundingRect(block).translated(offset);
auto layout = block.layout();
auto hl = dataForLine(block.blockNumber());
if (hl && layout) {
const auto changes = hl->changes;
for (auto c : changes) {
// full line background is colored
p.fillRect(r, hl->added ? green1 : red1);
QTextLine sl = layout->lineForTextPosition(c.pos);
QTextLine el = layout->lineForTextPosition(c.pos + c.len);
// color any word diffs
if (sl.isValid() && sl.lineNumber() == el.lineNumber()) {
int sx = sl.cursorToX(c.pos);
int ex = el.cursorToX(c.pos + c.len);
QRectF r = sl.naturalTextRect();
r.setLeft(sx);
r.setRight(ex);
r.moveTop(offset.y() + (sl.height() * sl.lineNumber()));
p.fillRect(r, hl->added ? green2 : red2);
} else {
QPainterPath path;
int i = sl.lineNumber() + 1;
int end = el.lineNumber();
QRectF rect = sl.naturalTextRect();
rect.setLeft(sl.cursorToX(c.pos));
rect.moveTop(offset.y() + (sl.height() * sl.lineNumber()));
path.addRect(rect);
for (; i <= end; ++i) {
auto line = layout->lineAt(i);
rect = line.naturalTextRect();
rect.moveTop(offset.y() + (line.height() * line.lineNumber()));
if (i == end) {
rect.setRight(el.cursorToX(c.pos + c.len));
}
path.addRect(rect);
}
p.fillPath(path, hl->added ? green2 : red2);
}
}
}
if (block.text().startsWith(QStringLiteral("@@ "))) {
p.save();
p.setPen(Qt::red);
p.setBrush(Qt::NoBrush);
QRectF copy = r;
copy.setRight(copy.right() - 1);
p.drawRect(copy);
p.restore();
}
offset.ry() += r.height();
if (offset.y() > viewportRect.height()) {
break;
}
block = block.next();
}
if (!textPainted) {
QPlainTextEdit::paintEvent(e);
}
}
void clearData()
{
data.clear();
}
void appendData(const QVector<LineHilight> &newData)
{
data.append(newData);
}
const LineHilight *dataForLine(int line)
{
auto it = std::find_if(data.cbegin(), data.cend(), [line](LineHilight hl) {
return hl.line == line;
});
return it == data.cend() ? nullptr : &(*it);
}
private:
QVector<LineHilight> data;
QColor red1;
QColor red2;
QColor green1;
QColor green2;
};
DiffWidget::DiffWidget(QWidget *parent)
: QWidget(parent)
, m_left(new DiffEditor(this))
, m_right(new DiffEditor(this))
{
auto layout = new QHBoxLayout(this);
layout->addWidget(m_left);
layout->addWidget(m_right);
m_left->setFont(Utils::editorFont());
m_right->setFont(Utils::editorFont());
leftHl = new KSyntaxHighlighting::SyntaxHighlighter(m_left->document());
rightHl = new KSyntaxHighlighting::SyntaxHighlighter(m_right->document());
leftHl->setTheme(KTextEditor::Editor::instance()->theme());
rightHl->setTheme(KTextEditor::Editor::instance()->theme());
connect(m_left->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int v) {
const QSignalBlocker b(m_left);
m_right->verticalScrollBar()->setValue(v);
});
connect(m_right->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int v) {
const QSignalBlocker b(m_right);
m_left->verticalScrollBar()->setValue(v);
});
}
void DiffWidget::diffDocs(KTextEditor::Document *l, KTextEditor::Document *r)
{
m_left->clear();
m_right->clear();
m_left->clearData();
m_right->clearData();
const auto &repo = KTextEditor::Editor::instance()->repository();
const auto def = repo.definitionForMimeType(l->mimeType());
if (l->mimeType() == r->mimeType()) {
leftHl->setDefinition(def);
rightHl->setDefinition(def);
} else {
leftHl->setDefinition(def);
rightHl->setDefinition(repo.definitionForMimeType(r->mimeType()));
}
const QString left = l->url().toLocalFile();
const QString right = r->url().toLocalFile();
QPointer<QProcess> git = new QProcess(this);
setupGitProcess(*git,
qApp->applicationDirPath(),
{QStringLiteral("diff"), QStringLiteral("--word-diff=porcelain"), QStringLiteral("--no-index"), left, right});
connect(git, &QProcess::readyReadStandardOutput, this, [this, git]() {
onTextReceived(git->readAllStandardOutput());
});
connect(git, &QProcess::readyReadStandardError, this, [this, git]() {
onError(git->readAllStandardError(), -1);
});
connect(git, &QProcess::finished, this, [this, git] {
git->deleteLater();
if (git->exitStatus() != QProcess::NormalExit) {
onError(git->readAllStandardError(), git->exitCode());
}
});
git->start();
}
void DiffWidget::openWordDiff(const QByteArray &raw)
{
// printf("show diff:\n%s\n================================", raw.constData());
const QStringList text = QString::fromUtf8(raw).replace(QStringLiteral("\r\n"), QStringLiteral("\n")).split(QLatin1Char('\n'));
static const QRegularExpression HUNK_HEADER_RE(QStringLiteral("^@@ -([0-9,]+) \\+([0-9,]+) @@(.*)"));
QStringList left;
QStringList right;
left.append(QString());
right.append(QString());
QVector<LineHilight> leftHlts;
QVector<LineHilight> rightHlts;
int lineNo = 0;
for (int i = 0; i < text.size(); ++i) {
const QString &line = text.at(i);
const auto match = HUNK_HEADER_RE.match(line);
if (!match.hasMatch())
continue;
// printf("new hunk");
for (int j = i + 1; j < text.size(); j++) {
QString l = text.at(j);
if (l.startsWith(QLatin1Char(' '))) {
l = l.mid(1);
left.back().append(l);
right.back().append(l);
} else if (l.startsWith(QLatin1Char('+'))) {
// qDebug() << "- line";
l = l.mid(1);
LineHilight h;
h.line = lineNo;
h.added = true;
h.changes.push_back({right.back().size(), l.size()});
if (!rightHlts.isEmpty() && rightHlts.back().line == lineNo) {
rightHlts.back().changes.append(h.changes);
} else {
rightHlts.push_back(h);
}
right.back().append(l);
} else if (l.startsWith(QLatin1Char('-'))) {
l = l.mid(1);
// qDebug() << "+ line: " << l;
LineHilight h;
h.line = lineNo;
h.added = false;
h.changes.push_back({left.back().size(), l.size()});
if (!leftHlts.isEmpty() && leftHlts.back().line == lineNo) {
leftHlts.back().changes.append(h.changes);
} else {
leftHlts.push_back(h);
}
left.back().append(l);
} else if (l.startsWith(QLatin1Char('~'))) {
left.append(QString());
right.append(QString());
lineNo++;
}
if (l.startsWith(QStringLiteral("@@ ")) && HUNK_HEADER_RE.match(l).hasMatch()) {
// printf("break: %s\n", l.toUtf8().constData());
i = j - 1;
// 2 empty lines for hunk
left.append(QString());
right.append(QString());
left.append(l);
right.append(l);
lineNo += 2;
break;
}
}
}
QString leftText = left.join(QLatin1Char('\n'));
QString rightText = right.join(QLatin1Char('\n'));
m_left->appendData(leftHlts);
m_right->appendData(rightHlts);
m_left->appendPlainText(leftText);
m_right->appendPlainText(rightText);
}
void DiffWidget::onTextReceived(const QByteArray &raw)
{
// printf("Got Text: \n%s\n==============\n", raw.constData());
openWordDiff(raw);
}
void DiffWidget::onError(const QByteArray &error, int /*code*/)
{
// printf("Got error: \n%s\n==============\n", error.constData());
}
/*
SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QPlainTextEdit>
#include <QWidget>
#include <KTextEditor/Document>
namespace KSyntaxHighlighting
{
class SyntaxHighlighter;
}
class DiffWidget : public QWidget
{
Q_OBJECT
public:
explicit DiffWidget(QWidget *parent = nullptr);
void diffDocs(KTextEditor::Document *l, KTextEditor::Document *r);
void openWordDiff(const QByteArray &diff);
Q_INVOKABLE bool shouldClose()
{
return true;
}
private:
void onTextReceived(const QByteArray &text);
void onError(const QByteArray &error, int code);
class DiffEditor *m_left;
class DiffEditor *m_right;
KSyntaxHighlighting::SyntaxHighlighter *leftHl;
KSyntaxHighlighting::SyntaxHighlighter *rightHl;
};
......@@ -72,6 +72,8 @@
#include <QTimer>
#include <QToolButton>
#include "diffwidget.h"
#include <ktexteditor/sessionconfiginterface.h>
// END
......@@ -1305,6 +1307,22 @@ void KateMainWindow::addWidgetAsTab(QWidget *widget)
vs->addWidgetAsTab(widget);
}
void KateMainWindow::showWordDiff(const QByteArray &wordDiff, const QString &fileName1, const QString &fileName2)
{
auto getFileName = [](const QString &s) {
int lastSlash = s.lastIndexOf(QLatin1Char('/'));
return lastSlash == -1 ? s : s.mid(lastSlash + 1);
};
auto w = new DiffWidget(this);
if (fileName2.isEmpty())
w->setWindowTitle(i18n("Diff %1", getFileName(fileName1)));
else
w->setWindowTitle(i18n("Diff %1..%2", getFileName(fileName1), getFileName(fileName2)));
addWidgetAsTab(w);
w->openWordDiff(wordDiff);
}
void KateMainWindow::mousePressEvent(QMouseEvent *e)
{
switch (e->button()) {
......
......@@ -512,6 +512,8 @@ public Q_SLOTS:
void addWidgetAsTab(QWidget *widget);
void showWordDiff(const QByteArray &wordDiff, const QString &fileName1, const QString &fileName2);
private Q_SLOTS:
void slotUpdateBottomViewBar();
......
......@@ -7,6 +7,7 @@
*/
#include "kateviewspace.h"
#include "diffwidget.h"
#include "kateapp.h"
#include "katedebug.h"
#include "katedocmanager.h"
......@@ -18,9 +19,8 @@
#include "kateviewmanager.h"
#include "tabmimedata.h"
#include <KActionCollection>
#include <KAcceleratorManager>
#include <KActionCollection>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
......@@ -877,6 +877,8 @@ void KateViewSpace::showContextMenu(int idx, const QPoint &globalPos)
}
auto *doc = m_tabBar->tabDocument(idx);
auto activeDocument =
KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView()->document(); // used for mCompareWithActive which is used with another
if (!doc) {
// This tab is holding some other widget
// Show only "close tab" for now
......@@ -911,6 +913,14 @@ void KateViewSpace::showContextMenu(int idx, const QPoint &globalPos)
QAction *aRenameFile = addActionFromCollection(&menu, "file_rename");
QAction *aDeleteFile = addActionFromCollection(&menu, "file_delete");
menu.addSeparator();
QAction *compare = menu.addAction(i18n("Compare with active document"));
connect(compare, &QAction::triggered, this, [this, activeDocument, doc] {
auto w = new DiffWidget(this);
w->setWindowTitle(i18n("Diff %1 .. %2", activeDocument->documentName(), doc->documentName()));
w->diffDocs(activeDocument, doc);
addWidgetAsTab(w);
});
compare->setVisible(doc != activeDocument);
QMenu *mCompareWithActive = new QMenu(i18n("Compare with active document"), &menu);
mCompareWithActive->setIcon(QIcon::fromTheme(QStringLiteral("vcs-diff")));
menu.addMenu(mCompareWithActive);
......@@ -927,9 +937,6 @@ void KateViewSpace::showContextMenu(int idx, const QPoint &globalPos)
aFileProperties->setEnabled(false);
mCompareWithActive->setEnabled(false);
}
auto activeDocument =
KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView()->document(); // used for mCompareWithActive which is used with another
// tab which is not active
// both documents must have urls and must not be the same to have the compare feature enabled
if (activeDocument->url().isEmpty() || activeDocument == doc) {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment