Use Kate's native diff viewer, add per-file and full changeset diff

- Integrate with libkateprivate's Utils::showDiff() to use Kate's built-in
  diff widget (respects user's Unified/SideBySide/Raw style preference)
- Add bundled kate-compat headers for DiffParams and Utils::showDiff decl
- CMakeLists.txt auto-detects libkateprivate, falls back to plain text diff
- Add 'Diff selected' button: shows diff for selected files with
  stage/unstage/discard actions available in the diff viewer
- Add 'Diff All' button: shows git diff HEAD for full changeset review
- Staged diffs show unstage action, unstaged diffs show stage+discard
- ReloadOnShow flag keeps diff views fresh after interactive operations
This commit is contained in:
Levi Neely 2026-07-16 04:21:51 +02:00
parent 163c3d18a9
commit ac8980a203
5 changed files with 265 additions and 23 deletions

View File

@ -8,7 +8,7 @@ set(CMAKE_AUTOMOC ON)
find_package(ECM 5.80 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
# Probe for KF6 before including KDE macros (which need QT_MAJOR_VERSION)
# --- Detect Qt6/KF6 or fall back to Qt5/KF5 ---
find_package(KF6TextEditor QUIET CONFIG)
if(KF6TextEditor_FOUND)
set(QT_MAJOR_VERSION 6)
@ -47,6 +47,18 @@ else()
message(STATUS "Building Git++ with KF5/Qt5")
endif()
# --- Find libkateprivate for diff widget support ---
# The library may only exist as a versioned .so (no dev symlink)
file(GLOB _KATEPRIVATE_CANDIDATES "/usr/lib64/libkateprivate.so*" "/usr/lib/libkateprivate.so*")
if(_KATEPRIVATE_CANDIDATES)
list(GET _KATEPRIVATE_CANDIDATES 0 KATEPRIVATE_LIB)
message(STATUS "Found libkateprivate: ${KATEPRIVATE_LIB}")
add_definitions(-DHAVE_KATE_DIFF)
else()
set(KATEPRIVATE_LIB "")
message(STATUS "libkateprivate not found — diff will open as plain text")
endif()
kcoreaddons_add_plugin(gitplusplus
SOURCES
gitplusplus.cpp
@ -55,5 +67,9 @@ kcoreaddons_add_plugin(gitplusplus
)
target_link_libraries(gitplusplus ${KF_LINK_LIBS})
target_include_directories(gitplusplus PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(KATEPRIVATE_LIB)
target_include_directories(gitplusplus PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/kate-compat)
target_link_libraries(gitplusplus ${KATEPRIVATE_LIB})
endif()

View File

@ -2,6 +2,10 @@
#include "gitprocess.h"
#include "gitstatusmodel.h"
#ifdef HAVE_KATE_DIFF
#include "ktexteditor_utils.h"
#endif
#include <KPluginFactory>
#include <KLocalizedString>
#include <ktexteditor_version.h>
@ -139,6 +143,8 @@ GitPlusPlusView::GitPlusPlusView(GitPlusPlusPlugin *plugin, KTextEditor::MainWin
m_stageBtn = makeToolButton(QStringLiteral("list-add"), QStringLiteral("Stage"));
m_unstageBtn = makeToolButton(QStringLiteral("list-remove"), QStringLiteral("Unstage"));
m_discardBtn = makeToolButton(QStringLiteral("edit-delete"), QStringLiteral("Discard"));
auto *diffBtn = makeToolButton(QStringLiteral("vcs-diff"), QStringLiteral("Diff selected (or all)"));
auto *diffAllBtn = makeToolButton(QStringLiteral("view-split-left-right"), QStringLiteral("Diff entire changeset"));
m_commitBtn = makeToolButton(QStringLiteral("vcs-commit"), QStringLiteral("Commit"));
m_pushBtn = makeToolButton(QStringLiteral("vcs-push"), QStringLiteral("Push"));
m_pullBtn = makeToolButton(QStringLiteral("vcs-pull"), QStringLiteral("Pull"));
@ -150,6 +156,8 @@ GitPlusPlusView::GitPlusPlusView(GitPlusPlusPlugin *plugin, KTextEditor::MainWin
toolbar->addWidget(m_stageBtn);
toolbar->addWidget(m_unstageBtn);
toolbar->addWidget(m_discardBtn);
toolbar->addWidget(diffBtn);
toolbar->addWidget(diffAllBtn);
toolbar->addWidget(m_commitBtn);
toolbar->addWidget(m_pushBtn);
toolbar->addWidget(m_pullBtn);
@ -317,6 +325,8 @@ GitPlusPlusView::GitPlusPlusView(GitPlusPlusPlugin *plugin, KTextEditor::MainWin
connect(m_stageBtn, &QToolButton::clicked, this, &GitPlusPlusView::stageSelected);
connect(m_unstageBtn, &QToolButton::clicked, this, &GitPlusPlusView::unstageSelected);
connect(m_discardBtn, &QToolButton::clicked, this, &GitPlusPlusView::discardSelected);
connect(diffBtn, &QToolButton::clicked, this, &GitPlusPlusView::diffSelected);
connect(diffAllBtn, &QToolButton::clicked, this, &GitPlusPlusView::diffAll);
connect(m_commitBtn, &QToolButton::clicked, this, &GitPlusPlusView::commitChanges);
connect(m_pushBtn, &QToolButton::clicked, this, &GitPlusPlusView::push);
connect(m_pullBtn, &QToolButton::clicked, this, &GitPlusPlusView::pull);
@ -467,6 +477,28 @@ void GitPlusPlusView::setMessage(const QString &text, bool isError)
m_statusLabel->setStyleSheet(isError ? QStringLiteral("color: red;") : QStringLiteral("color: green;"));
}
void GitPlusPlusView::showDiffOutput(const QByteArray &diff, const QString &title, const QStringList &gitArgs, int extraFlags)
{
#ifdef HAVE_KATE_DIFF
DiffParams params;
params.tabTitle = title;
params.workingDir = m_gitDir;
params.arguments = gitArgs;
params.flags = DiffParams::Flag(DiffParams::ShowFileName | extraFlags);
params.updateStatusCallback = [this]() { updateStatus(); };
Utils::showDiff(diff, params, m_mainWindow);
#else
Q_UNUSED(title);
Q_UNUSED(gitArgs);
Q_UNUSED(extraFlags);
auto *view = m_mainWindow->openUrl(QUrl());
if (view) {
view->document()->setText(QString::fromUtf8(diff));
view->document()->setHighlightingMode(QStringLiteral("Diff"));
}
#endif
}
void GitPlusPlusView::refreshAll()
{
updateStatus();
@ -872,6 +904,147 @@ void GitPlusPlusView::discardSelected()
QStringLiteral("Failed to discard"));
}
void GitPlusPlusView::diffSelected()
{
// Get all selected file entries (from any section)
QStringList stagedFiles;
QStringList unstagedFiles;
const auto indexes = m_treeView->selectionModel()->selectedIndexes();
for (const auto &proxyIdx : indexes) {
QModelIndex idx = m_proxy->mapToSource(proxyIdx);
if (idx.internalId() == 0xFFFFFFFF)
continue; // skip top-level category nodes
int nodeType = int(idx.internalId());
QString file = idx.data(GitStatusModel::FilePathRole).toString();
if (file.isEmpty())
continue;
if (nodeType == GitStatusModel::NodeStaged)
stagedFiles.append(file);
else if (nodeType == GitStatusModel::NodeChanged)
unstagedFiles.append(file);
// Untracked files: show as new file diff
else if (nodeType == GitStatusModel::NodeUntracked)
unstagedFiles.append(file);
}
// If nothing selected, fall through to diffAll
if (stagedFiles.isEmpty() && unstagedFiles.isEmpty()) {
diffAll();
return;
}
// Show staged diff for selected staged files
if (!stagedFiles.isEmpty()) {
QStringList args = {QStringLiteral("diff"), QStringLiteral("--cached"), QStringLiteral("--")};
args.append(stagedFiles);
auto *proc = gitProcess(args);
if (!proc) return;
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, proc, stagedFiles, args](int exitCode, QProcess::ExitStatus) {
if (exitCode == 0) {
QByteArray output = proc->readAllStandardOutput();
if (!output.trimmed().isEmpty()) {
QString title = stagedFiles.size() == 1
? QStringLiteral("Staged: %1").arg(stagedFiles.first())
: QStringLiteral("Staged changes (%1 files)").arg(stagedFiles.size());
#ifdef HAVE_KATE_DIFF
showDiffOutput(output, title, args,
DiffParams::ShowUnstage | DiffParams::ReloadOnShow);
#else
showDiffOutput(output, title, args);
#endif
} else {
setMessage(QStringLiteral("No staged diff for selection"));
}
}
proc->deleteLater();
});
proc->start();
}
// Show unstaged diff for selected changed/untracked files
if (!unstagedFiles.isEmpty()) {
QStringList args = {QStringLiteral("diff"), QStringLiteral("--")};
args.append(unstagedFiles);
auto *proc = gitProcess(args);
if (!proc) return;
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, proc, unstagedFiles, args](int exitCode, QProcess::ExitStatus) {
if (exitCode == 0) {
QByteArray output = proc->readAllStandardOutput();
if (!output.trimmed().isEmpty()) {
QString title = unstagedFiles.size() == 1
? QStringLiteral("Unstaged: %1").arg(unstagedFiles.first())
: QStringLiteral("Unstaged changes (%1 files)").arg(unstagedFiles.size());
#ifdef HAVE_KATE_DIFF
showDiffOutput(output, title, args,
DiffParams::ShowStage | DiffParams::ShowDiscard | DiffParams::ReloadOnShow);
#else
showDiffOutput(output, title, args);
#endif
} else {
setMessage(QStringLiteral("No unstaged diff for selection"));
}
}
proc->deleteLater();
});
proc->start();
}
}
void GitPlusPlusView::diffAll()
{
if (m_gitDir.isEmpty()) {
setMessage(QStringLiteral("No git directory found"), true);
return;
}
// Show combined diff: staged + unstaged in one view
// Use "git diff HEAD" which shows all changes (staged and unstaged) relative to HEAD
QStringList args = {QStringLiteral("diff"), QStringLiteral("HEAD")};
auto *proc = gitProcess(args);
if (!proc) return;
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, proc, args](int exitCode, QProcess::ExitStatus) {
if (exitCode == 0) {
QByteArray output = proc->readAllStandardOutput();
if (output.trimmed().isEmpty()) {
setMessage(QStringLiteral("No changes to diff"));
} else {
showDiffOutput(output, QStringLiteral("All changes"), args);
setMessage(QStringLiteral("Showing all changes"));
}
} else {
// HEAD might not exist (initial commit) — fall back to staged + unstaged separately
QString err = QString::fromUtf8(proc->readAllStandardError()).trimmed();
if (err.contains(QStringLiteral("unknown revision"))) {
// No commits yet — show diff of staged (index vs empty)
auto *proc2 = gitProcess({QStringLiteral("diff"), QStringLiteral("--cached")});
if (proc2) {
connect(proc2, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, proc2](int exitCode2, QProcess::ExitStatus) {
if (exitCode2 == 0) {
QByteArray out2 = proc2->readAllStandardOutput();
if (!out2.trimmed().isEmpty()) {
showDiffOutput(out2, QStringLiteral("All changes (initial commit)"),
{QStringLiteral("diff"), QStringLiteral("--cached")});
} else {
setMessage(QStringLiteral("No changes to diff"));
}
}
proc2->deleteLater();
});
proc2->start();
}
} else {
setMessage(QStringLiteral("Diff failed: ") + err, true);
}
}
proc->deleteLater();
});
proc->start();
}
void GitPlusPlusView::commitChanges()
{
QString msg = m_commitMsgEdit->toPlainText().trimmed();
@ -1046,16 +1219,12 @@ void GitPlusPlusView::compareBranch()
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, proc, branch](int exitCode, QProcess::ExitStatus) {
if (exitCode == 0) {
QString diff = QString::fromUtf8(proc->readAllStandardOutput());
if (diff.isEmpty()) {
QByteArray diff = proc->readAllStandardOutput();
if (diff.trimmed().isEmpty()) {
setMessage(QStringLiteral("No differences between %1 and %2").arg(m_currentBranch, branch));
} else {
// Open diff in a new untitled document
auto *view = m_mainWindow->openUrl(QUrl());
if (view) {
view->document()->setText(diff);
view->document()->setHighlightingMode(QStringLiteral("Diff"));
}
showDiffOutput(diff, QStringLiteral("Diff: %1...%2").arg(m_currentBranch, branch),
{QStringLiteral("diff"), m_currentBranch + QStringLiteral("...") + branch});
setMessage(QStringLiteral("Comparing %1...%2").arg(m_currentBranch, branch));
}
} else {
@ -1292,15 +1461,12 @@ void GitPlusPlusView::stashDiff()
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, proc, stash](int exitCode, QProcess::ExitStatus) {
if (exitCode == 0) {
QString output = QString::fromUtf8(proc->readAllStandardOutput());
if (output.isEmpty()) {
QByteArray output = proc->readAllStandardOutput();
if (output.trimmed().isEmpty()) {
setMessage(QStringLiteral("Empty stash diff"));
} else {
auto *view = m_mainWindow->openUrl(QUrl());
if (view) {
view->document()->setText(output);
view->document()->setHighlightingMode(QStringLiteral("Diff"));
}
showDiffOutput(output, QStringLiteral("Diff - %1").arg(stash),
{QStringLiteral("stash"), QStringLiteral("show"), QStringLiteral("-p"), stash});
setMessage(QStringLiteral("Diff: ") + stash);
}
} else {
@ -1344,12 +1510,9 @@ void GitPlusPlusView::openCommit()
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, proc, ref](int exitCode, QProcess::ExitStatus) {
if (exitCode == 0) {
QString output = QString::fromUtf8(proc->readAllStandardOutput());
auto *view = m_mainWindow->openUrl(QUrl());
if (view) {
view->document()->setText(output);
view->document()->setHighlightingMode(QStringLiteral("Diff"));
}
QByteArray output = proc->readAllStandardOutput();
showDiffOutput(output, QStringLiteral("Commit: %1").arg(ref),
{QStringLiteral("show"), QStringLiteral("--stat"), QStringLiteral("--patch"), ref});
setMessage(QStringLiteral("Showing commit: ") + ref);
} else {
QString err = QString::fromUtf8(proc->readAllStandardError()).trimmed();

View File

@ -68,6 +68,8 @@ private Q_SLOTS:
void unstageSelected();
void discardSelected();
void commitChanges();
void diffSelected();
void diffAll();
// Remote
void push();
@ -111,6 +113,7 @@ private:
void runSimpleGit(const QStringList &args, const QString &successMsg, const QString &errorMsg);
void runRemoteGit(const QStringList &args, const QString &opName);
void setMessage(const QString &text, bool isError = false);
void showDiffOutput(const QByteArray &diff, const QString &title, const QStringList &gitArgs = {}, int extraFlags = 0);
void updateIndicators();
void setRemoteBusy(bool busy, const QString &opName = {});
void refreshAll();

44
kate-compat/diffparams.h Normal file
View File

@ -0,0 +1,44 @@
/*
SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
Minimal copy of Kate's DiffParams for external plugin use.
*/
#pragma once
#include <QMetaType>
#include <QString>
#include <QStringList>
#include <functional>
struct DiffParams {
enum Flag {
ShowStage = 1,
ShowUnstage = 2,
ShowDiscard = 4,
ShowFileName = 8,
ShowCommitInfo = 16,
ReloadOnShow = 32,
ShowFullContext = 64,
ShowEditLeftSide = 128,
ShowEditRightSide = 256
};
Q_DECLARE_FLAGS(Flags, Flag)
QString tabTitle;
QString srcFile;
QString destFile;
QString workingDir;
QStringList arguments;
Flags flags;
void clear()
{
tabTitle = srcFile = destFile = workingDir = QString();
arguments.clear();
flags = {};
updateStatusCallback = nullptr;
}
std::function<void()> updateStatusCallback;
};

View File

@ -0,0 +1,16 @@
/*
SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
Minimal declaration of Utils::showDiff for external plugin use.
*/
#pragma once
#include "diffparams.h"
#include <QByteArray>
namespace KTextEditor { class MainWindow; }
namespace Utils {
void showDiff(const QByteArray &diff, const DiffParams &params, KTextEditor::MainWindow *mainWindow);
}