forked from qt-creator/qt-creator
Git: Move Instant Blame into own class
Change-Id: Ic7bdfb47d0de2a5499e40c9eeaead8bbf30a12e5 Reviewed-by: Orgad Shaneh <orgads@gmail.com>
This commit is contained in:
committed by
André Hartmann
parent
5745145f5a
commit
ff9170b820
@@ -31,6 +31,7 @@ add_qtc_plugin(Git
|
||||
gitsubmiteditor.cpp gitsubmiteditor.h
|
||||
gitsubmiteditorwidget.cpp gitsubmiteditorwidget.h
|
||||
gitutils.cpp gitutils.h
|
||||
instantblame.cpp instantblame.h
|
||||
logchangedialog.cpp logchangedialog.h
|
||||
mergetool.cpp mergetool.h
|
||||
remotedialog.cpp remotedialog.h
|
||||
|
@@ -14,6 +14,7 @@
|
||||
#include "gitsubmiteditor.h"
|
||||
#include "gittr.h"
|
||||
#include "gitutils.h"
|
||||
#include "instantblame.h"
|
||||
#include "logchangedialog.h"
|
||||
#include "remotedialog.h"
|
||||
#include "stashdialog.h"
|
||||
@@ -37,10 +38,7 @@
|
||||
|
||||
#include <aggregation/aggregate.h>
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <texteditor/texteditortr.h>
|
||||
#include <texteditor/textmark.h>
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/async.h>
|
||||
@@ -183,84 +181,6 @@ const VcsBaseEditorParameters rebaseEditorParameters {
|
||||
"text/vnd.qtcreator.git.rebase"
|
||||
};
|
||||
|
||||
class CommitInfo {
|
||||
public:
|
||||
QString sha1;
|
||||
QString shortAuthor;
|
||||
QString author;
|
||||
QString authorMail;
|
||||
QDateTime authorTime;
|
||||
QString summary;
|
||||
FilePath filePath;
|
||||
};
|
||||
|
||||
class BlameMark : public TextEditor::TextMark
|
||||
{
|
||||
const CommitInfo m_info;
|
||||
public:
|
||||
BlameMark(const FilePath &fileName, int lineNumber, const CommitInfo &info)
|
||||
: TextEditor::TextMark(fileName,
|
||||
lineNumber,
|
||||
{Tr::tr("Git Blame"), Constants::TEXT_MARK_CATEGORY_BLAME})
|
||||
, m_info(info)
|
||||
{
|
||||
const QString text = info.shortAuthor + " " + info.authorTime.toString("yyyy-MM-dd");
|
||||
|
||||
setPriority(TextEditor::TextMark::LowPriority);
|
||||
setToolTip(toolTipText(info));
|
||||
setLineAnnotation(text);
|
||||
setSettingsPage(VcsBase::Constants::VCS_ID_GIT);
|
||||
setActionsProvider([info] {
|
||||
QAction *copyToClipboardAction = new QAction;
|
||||
copyToClipboardAction->setIcon(QIcon::fromTheme("edit-copy", Utils::Icons::COPY.icon()));
|
||||
copyToClipboardAction->setToolTip(TextEditor::Tr::tr("Copy SHA1 to Clipboard"));
|
||||
QObject::connect(copyToClipboardAction, &QAction::triggered, [info] {
|
||||
Utils::setClipboardAndSelection(info.sha1);
|
||||
});
|
||||
return QList<QAction *>{copyToClipboardAction};
|
||||
});
|
||||
}
|
||||
|
||||
bool addToolTipContent(QLayout *target) const final
|
||||
{
|
||||
auto textLabel = new QLabel;
|
||||
textLabel->setText(toolTip());
|
||||
target->addWidget(textLabel);
|
||||
QObject::connect(textLabel, &QLabel::linkActivated, textLabel, [this] {
|
||||
gitClient().show(m_info.filePath, m_info.sha1);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString toolTipText(const CommitInfo &info) const
|
||||
{
|
||||
QString result = QString(
|
||||
"<table>"
|
||||
" <tr><td>commit</td><td><a href>%1</a></td></tr>"
|
||||
" <tr><td>Author:</td><td>%2 <%3></td></tr>"
|
||||
" <tr><td>Date:</td><td>%4</td></tr>"
|
||||
" <tr></tr>"
|
||||
" <tr><td colspan='2' align='left'>%5</td></tr>"
|
||||
"</table>")
|
||||
.arg(info.sha1, info.author, info.authorMail,
|
||||
info.authorTime.toString("yyyy-MM-dd hh:mm:ss"), info.summary);
|
||||
|
||||
if (settings().instantBlameIgnoreSpaceChanges()
|
||||
|| settings().instantBlameIgnoreLineMoves()) {
|
||||
result.append(
|
||||
"<p>"
|
||||
//: %1 and %2 are the "ignore whitespace changes" and "ignore line moves" options
|
||||
+ Tr::tr("<b>Note:</b> \"%1\" or \"%2\""
|
||||
" is enabled in the instant blame settings.")
|
||||
.arg(GitSettings::trIgnoreWhitespaceChanges(),
|
||||
GitSettings::trIgnoreLineMoves())
|
||||
+ "</p>");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// GitPlugin
|
||||
|
||||
class GitPluginPrivate final : public VcsBasePluginPrivate
|
||||
@@ -413,12 +333,7 @@ public:
|
||||
void applyPatch(const FilePath &workingDirectory, QString file = {});
|
||||
void updateVersionWarning();
|
||||
|
||||
void setupInstantBlame();
|
||||
void instantBlameOnce();
|
||||
void forceInstantBlame();
|
||||
void instantBlame();
|
||||
void stopInstantBlame();
|
||||
bool refreshWorkingDirectory(const FilePath &workingDirectory);
|
||||
|
||||
void onApplySettings();
|
||||
|
||||
@@ -452,14 +367,7 @@ public:
|
||||
FilePath m_submitRepository;
|
||||
QString m_commitMessageFileName;
|
||||
|
||||
FilePath m_workingDirectory;
|
||||
QTextCodec *m_codec = nullptr;
|
||||
Author m_author;
|
||||
int m_lastVisitedEditorLine = -1;
|
||||
QTimer *m_cursorPositionChangedTimer = nullptr;
|
||||
std::unique_ptr<BlameMark> m_blameMark;
|
||||
QMetaObject::Connection m_blameCursorPosConn;
|
||||
QMetaObject::Connection m_documentChangedConn;
|
||||
InstantBlame m_instantBlame;
|
||||
|
||||
GitGrep gitGrep;
|
||||
|
||||
@@ -720,7 +628,6 @@ GitPluginPrivate::GitPluginPrivate()
|
||||
m_fileActions.reserve(10);
|
||||
m_projectActions.reserve(10);
|
||||
m_repositoryActions.reserve(50);
|
||||
m_codec = gitClient().defaultCommitEncoding();
|
||||
|
||||
Context context(Constants::GIT_CONTEXT);
|
||||
|
||||
@@ -1083,7 +990,7 @@ GitPluginPrivate::GitPluginPrivate()
|
||||
|
||||
connect(&settings(), &AspectContainer::applied, this, &GitPluginPrivate::onApplySettings);
|
||||
|
||||
setupInstantBlame();
|
||||
m_instantBlame.setup();
|
||||
}
|
||||
|
||||
void GitPluginPrivate::diffCurrentFile()
|
||||
@@ -1439,231 +1346,9 @@ void GitPluginPrivate::updateVersionWarning()
|
||||
});
|
||||
}
|
||||
|
||||
void GitPluginPrivate::setupInstantBlame()
|
||||
{
|
||||
m_cursorPositionChangedTimer = new QTimer(this);
|
||||
m_cursorPositionChangedTimer->setSingleShot(true);
|
||||
connect(m_cursorPositionChangedTimer, &QTimer::timeout, this, &GitPluginPrivate::instantBlame);
|
||||
|
||||
auto setupBlameForEditor = [this](Core::IEditor *editor) {
|
||||
if (!editor) {
|
||||
stopInstantBlame();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings().instantBlame()) {
|
||||
m_lastVisitedEditorLine = -1;
|
||||
stopInstantBlame();
|
||||
return;
|
||||
}
|
||||
|
||||
const TextEditorWidget *widget = TextEditorWidget::fromEditor(editor);
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
if (qobject_cast<const VcsBaseEditorWidget *>(widget))
|
||||
return; // Skip in VCS editors like log or blame
|
||||
|
||||
const Utils::FilePath workingDirectory = GitPlugin::currentState().currentFileTopLevel();
|
||||
if (!refreshWorkingDirectory(workingDirectory))
|
||||
return;
|
||||
|
||||
m_blameCursorPosConn = connect(widget, &QPlainTextEdit::cursorPositionChanged, this,
|
||||
[this] {
|
||||
if (!settings().instantBlame()) {
|
||||
disconnect(m_blameCursorPosConn);
|
||||
return;
|
||||
}
|
||||
m_cursorPositionChangedTimer->start(500);
|
||||
});
|
||||
IDocument *document = editor->document();
|
||||
m_documentChangedConn = connect(document, &IDocument::changed, this, [this, document] {
|
||||
if (!document->isModified())
|
||||
forceInstantBlame();
|
||||
});
|
||||
|
||||
forceInstantBlame();
|
||||
};
|
||||
|
||||
connect(&settings().instantBlame, &BaseAspect::changed, this, [this, setupBlameForEditor] {
|
||||
if (settings().instantBlame())
|
||||
setupBlameForEditor(EditorManager::currentEditor());
|
||||
else
|
||||
stopInstantBlame();
|
||||
});
|
||||
|
||||
connect(EditorManager::instance(), &EditorManager::currentEditorChanged,
|
||||
this, setupBlameForEditor);
|
||||
}
|
||||
|
||||
// Porcelain format of git blame output
|
||||
// 8b649d2d61416205977aba56ef93e1e1f155005e 5 5 1
|
||||
// author John Doe
|
||||
// author-mail <john.doe@gmail.com>
|
||||
// author-time 1613752276
|
||||
// author-tz +0100
|
||||
// committer John Doe
|
||||
// committer-mail <john.doe@gmail.com>
|
||||
// committer-time 1613752312
|
||||
// committer-tz +0100
|
||||
// summary Add greeting to script
|
||||
// boundary
|
||||
// filename foo
|
||||
// echo Hello World!
|
||||
|
||||
CommitInfo parseBlameOutput(const QStringList &blame, const Utils::FilePath &filePath,
|
||||
const Git::Internal::Author &author)
|
||||
{
|
||||
CommitInfo result;
|
||||
if (blame.size() <= 12)
|
||||
return result;
|
||||
|
||||
result.sha1 = blame.at(0).left(40);
|
||||
result.author = blame.at(1).mid(7);
|
||||
result.authorMail = blame.at(2).mid(13).chopped(1);
|
||||
if (result.author == author.name || result.authorMail == author.email)
|
||||
result.shortAuthor = Tr::tr("You");
|
||||
else
|
||||
result.shortAuthor = result.author;
|
||||
const uint timeStamp = blame.at(3).mid(12).toUInt();
|
||||
result.authorTime = QDateTime::fromSecsSinceEpoch(timeStamp);
|
||||
result.summary = blame.at(9).mid(8);
|
||||
result.filePath = filePath;
|
||||
return result;
|
||||
}
|
||||
|
||||
void GitPluginPrivate::instantBlameOnce()
|
||||
{
|
||||
if (!settings().instantBlame()) {
|
||||
const TextEditorWidget *widget = TextEditorWidget::currentTextEditorWidget();
|
||||
if (!widget)
|
||||
return;
|
||||
connect(EditorManager::instance(), &EditorManager::currentEditorChanged,
|
||||
this, [this] { m_blameMark.reset(); }, Qt::SingleShotConnection);
|
||||
|
||||
connect(widget, &QPlainTextEdit::cursorPositionChanged,
|
||||
this, [this] { m_blameMark.reset(); }, Qt::SingleShotConnection);
|
||||
|
||||
const Utils::FilePath workingDirectory = GitPlugin::currentState().topLevel();
|
||||
if (!refreshWorkingDirectory(workingDirectory))
|
||||
return;
|
||||
}
|
||||
|
||||
forceInstantBlame();
|
||||
}
|
||||
|
||||
void GitPluginPrivate::forceInstantBlame()
|
||||
{
|
||||
m_lastVisitedEditorLine = -1;
|
||||
instantBlame();
|
||||
}
|
||||
|
||||
void GitPluginPrivate::instantBlame()
|
||||
{
|
||||
const TextEditorWidget *widget = TextEditorWidget::currentTextEditorWidget();
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
if (widget->textDocument()->isModified()) {
|
||||
m_blameMark.reset();
|
||||
m_lastVisitedEditorLine = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const QTextCursor cursor = widget->textCursor();
|
||||
const QTextBlock block = cursor.block();
|
||||
const int line = block.blockNumber() + 1;
|
||||
const int lines = widget->document()->blockCount();
|
||||
|
||||
if (line >= lines) {
|
||||
m_blameMark.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_lastVisitedEditorLine == line)
|
||||
return;
|
||||
|
||||
m_lastVisitedEditorLine = line;
|
||||
|
||||
const Utils::FilePath filePath = widget->textDocument()->filePath();
|
||||
const QFileInfo fi(filePath.toString());
|
||||
const Utils::FilePath workingDirectory = Utils::FilePath::fromString(fi.path());
|
||||
const QString lineString = QString("%1,%1").arg(line);
|
||||
const auto commandHandler = [this, filePath, line](const CommandResult &result) {
|
||||
if (result.result() == ProcessResult::FinishedWithError &&
|
||||
result.cleanedStdErr().contains("no such path")) {
|
||||
stopInstantBlame();
|
||||
return;
|
||||
}
|
||||
const QString output = result.cleanedStdOut();
|
||||
if (output.isEmpty()) {
|
||||
stopInstantBlame();
|
||||
return;
|
||||
}
|
||||
const CommitInfo info = parseBlameOutput(output.split('\n'), filePath, m_author);
|
||||
m_blameMark.reset(new BlameMark(filePath, line, info));
|
||||
};
|
||||
QStringList options = {"blame", "-p"};
|
||||
if (settings().instantBlameIgnoreSpaceChanges())
|
||||
options.append("-w");
|
||||
if (settings().instantBlameIgnoreLineMoves())
|
||||
options.append("-M");
|
||||
options.append({"-L", lineString, "--", filePath.toString()});
|
||||
gitClient().vcsExecWithHandler(workingDirectory, options, this,
|
||||
commandHandler, RunFlags::NoOutput, m_codec);
|
||||
}
|
||||
|
||||
void GitPluginPrivate::stopInstantBlame()
|
||||
{
|
||||
m_blameMark.reset();
|
||||
m_cursorPositionChangedTimer->stop();
|
||||
disconnect(m_blameCursorPosConn);
|
||||
disconnect(m_documentChangedConn);
|
||||
}
|
||||
|
||||
bool GitPluginPrivate::refreshWorkingDirectory(const FilePath &workingDirectory)
|
||||
{
|
||||
if (workingDirectory.isEmpty())
|
||||
return false;
|
||||
|
||||
if (m_workingDirectory == workingDirectory)
|
||||
return true;
|
||||
|
||||
m_workingDirectory = workingDirectory;
|
||||
|
||||
const auto commitCodecHandler = [this, workingDirectory](const CommandResult &result) {
|
||||
QTextCodec *codec = nullptr;
|
||||
|
||||
if (result.result() == ProcessResult::FinishedWithSuccess) {
|
||||
const QString codecName = result.cleanedStdOut().trimmed();
|
||||
codec = QTextCodec::codecForName(codecName.toUtf8());
|
||||
} else {
|
||||
codec = gitClient().defaultCommitEncoding();
|
||||
}
|
||||
|
||||
if (m_codec != codec) {
|
||||
m_codec = codec;
|
||||
forceInstantBlame();
|
||||
}
|
||||
};
|
||||
gitClient().readConfigAsync(workingDirectory, {"config", "i18n.commitEncoding"},
|
||||
commitCodecHandler);
|
||||
|
||||
const auto authorHandler = [this, workingDirectory](const CommandResult &result) {
|
||||
if (result.result() == ProcessResult::FinishedWithSuccess) {
|
||||
const QString authorInfo = result.cleanedStdOut().trimmed();
|
||||
const Author author = gitClient().parseAuthor(authorInfo);
|
||||
|
||||
if (m_author != author) {
|
||||
m_author = author;
|
||||
forceInstantBlame();
|
||||
}
|
||||
}
|
||||
};
|
||||
gitClient().readConfigAsync(workingDirectory, {"var", "GIT_AUTHOR_IDENT"},
|
||||
authorHandler);
|
||||
|
||||
return true;
|
||||
m_instantBlame.once();
|
||||
}
|
||||
|
||||
IEditor *GitPluginPrivate::openSubmitEditor(const QString &fileName, const CommitData &cd)
|
||||
|
333
src/plugins/git/instantblame.cpp
Normal file
333
src/plugins/git/instantblame.cpp
Normal file
@@ -0,0 +1,333 @@
|
||||
// Copyright (C) 2023 Andre Hartmann (aha_1980@gmx.de)
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "instantblame.h"
|
||||
|
||||
#include "gitclient.h"
|
||||
#include "gitconstants.h"
|
||||
#include "gitplugin.h"
|
||||
#include "gitsettings.h"
|
||||
#include "gittr.h"
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <texteditor/texteditortr.h>
|
||||
#include <texteditor/textmark.h>
|
||||
|
||||
#include <utils/filepath.h>
|
||||
#include <utils/stringutils.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include <vcsbase/vcsbaseconstants.h>
|
||||
#include <vcsbase/vcsbaseeditor.h>
|
||||
#include <vcsbase/vcscommand.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QTextCodec>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QTimer>
|
||||
|
||||
namespace Git::Internal {
|
||||
|
||||
using namespace Core;
|
||||
using namespace TextEditor;
|
||||
using namespace Utils;
|
||||
using namespace VcsBase;
|
||||
|
||||
BlameMark::BlameMark(const FilePath &fileName, int lineNumber, const CommitInfo &info)
|
||||
: TextEditor::TextMark(fileName,
|
||||
lineNumber,
|
||||
{Tr::tr("Git Blame"), Constants::TEXT_MARK_CATEGORY_BLAME})
|
||||
, m_info(info)
|
||||
{
|
||||
const QString text = info.shortAuthor + " " + info.authorTime.toString("yyyy-MM-dd");
|
||||
|
||||
setPriority(TextEditor::TextMark::LowPriority);
|
||||
setToolTip(toolTipText(info));
|
||||
setLineAnnotation(text);
|
||||
setSettingsPage(VcsBase::Constants::VCS_ID_GIT);
|
||||
setActionsProvider([info] {
|
||||
QAction *copyToClipboardAction = new QAction;
|
||||
copyToClipboardAction->setIcon(QIcon::fromTheme("edit-copy", Utils::Icons::COPY.icon()));
|
||||
copyToClipboardAction->setToolTip(TextEditor::Tr::tr("Copy SHA1 to Clipboard"));
|
||||
QObject::connect(copyToClipboardAction, &QAction::triggered, [info] {
|
||||
Utils::setClipboardAndSelection(info.sha1);
|
||||
});
|
||||
return QList<QAction *>{copyToClipboardAction};
|
||||
});
|
||||
}
|
||||
|
||||
bool BlameMark::addToolTipContent(QLayout *target) const
|
||||
{
|
||||
auto textLabel = new QLabel;
|
||||
textLabel->setText(toolTip());
|
||||
target->addWidget(textLabel);
|
||||
QObject::connect(textLabel, &QLabel::linkActivated, textLabel, [this] {
|
||||
gitClient().show(m_info.filePath, m_info.sha1);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString BlameMark::toolTipText(const CommitInfo &info) const
|
||||
{
|
||||
QString result = QString(
|
||||
"<table>"
|
||||
" <tr><td>commit</td><td><a href>%1</a></td></tr>"
|
||||
" <tr><td>Author:</td><td>%2 <%3></td></tr>"
|
||||
" <tr><td>Date:</td><td>%4</td></tr>"
|
||||
" <tr></tr>"
|
||||
" <tr><td colspan='2' align='left'>%5</td></tr>"
|
||||
"</table>")
|
||||
.arg(info.sha1, info.author, info.authorMail,
|
||||
info.authorTime.toString("yyyy-MM-dd hh:mm:ss"), info.summary);
|
||||
|
||||
if (settings().instantBlameIgnoreSpaceChanges()
|
||||
|| settings().instantBlameIgnoreLineMoves()) {
|
||||
result.append(
|
||||
"<p>"
|
||||
//: %1 and %2 are the "ignore whitespace changes" and "ignore line moves" options
|
||||
+ Tr::tr("<b>Note:</b> \"%1\" or \"%2\""
|
||||
" is enabled in the instant blame settings.")
|
||||
.arg(GitSettings::trIgnoreWhitespaceChanges(),
|
||||
GitSettings::trIgnoreLineMoves())
|
||||
+ "</p>");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
InstantBlame::InstantBlame()
|
||||
{
|
||||
m_codec = gitClient().defaultCommitEncoding();
|
||||
}
|
||||
|
||||
void InstantBlame::setup()
|
||||
{
|
||||
m_cursorPositionChangedTimer = new QTimer(this);
|
||||
m_cursorPositionChangedTimer->setSingleShot(true);
|
||||
connect(m_cursorPositionChangedTimer, &QTimer::timeout, this, &InstantBlame::perform);
|
||||
|
||||
auto setupBlameForEditor = [this](Core::IEditor *editor) {
|
||||
if (!editor) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings().instantBlame()) {
|
||||
m_lastVisitedEditorLine = -1;
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const TextEditorWidget *widget = TextEditorWidget::fromEditor(editor);
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
if (qobject_cast<const VcsBaseEditorWidget *>(widget))
|
||||
return; // Skip in VCS editors like log or blame
|
||||
|
||||
const Utils::FilePath workingDirectory = GitPlugin::currentState().currentFileTopLevel();
|
||||
if (!refreshWorkingDirectory(workingDirectory))
|
||||
return;
|
||||
|
||||
m_blameCursorPosConn = connect(widget, &QPlainTextEdit::cursorPositionChanged, this,
|
||||
[this] {
|
||||
if (!settings().instantBlame()) {
|
||||
disconnect(m_blameCursorPosConn);
|
||||
return;
|
||||
}
|
||||
m_cursorPositionChangedTimer->start(500);
|
||||
});
|
||||
IDocument *document = editor->document();
|
||||
m_documentChangedConn = connect(document, &IDocument::changed, this, [this, document] {
|
||||
if (!document->isModified())
|
||||
force();
|
||||
});
|
||||
|
||||
force();
|
||||
};
|
||||
|
||||
connect(&settings().instantBlame, &BaseAspect::changed, this, [this, setupBlameForEditor] {
|
||||
if (settings().instantBlame())
|
||||
setupBlameForEditor(EditorManager::currentEditor());
|
||||
else
|
||||
stop();
|
||||
});
|
||||
|
||||
connect(EditorManager::instance(), &EditorManager::currentEditorChanged,
|
||||
this, setupBlameForEditor);
|
||||
}
|
||||
|
||||
// Porcelain format of git blame output
|
||||
// 8b649d2d61416205977aba56ef93e1e1f155005e 5 5 1
|
||||
// author John Doe
|
||||
// author-mail <john.doe@gmail.com>
|
||||
// author-time 1613752276
|
||||
// author-tz +0100
|
||||
// committer John Doe
|
||||
// committer-mail <john.doe@gmail.com>
|
||||
// committer-time 1613752312
|
||||
// committer-tz +0100
|
||||
// summary Add greeting to script
|
||||
// boundary
|
||||
// filename foo
|
||||
// echo Hello World!
|
||||
|
||||
static CommitInfo parseBlameOutput(const QStringList &blame, const Utils::FilePath &filePath,
|
||||
const Git::Internal::Author &author)
|
||||
{
|
||||
CommitInfo result;
|
||||
if (blame.size() <= 12)
|
||||
return result;
|
||||
|
||||
result.sha1 = blame.at(0).left(40);
|
||||
result.author = blame.at(1).mid(7);
|
||||
result.authorMail = blame.at(2).mid(13).chopped(1);
|
||||
if (result.author == author.name || result.authorMail == author.email)
|
||||
result.shortAuthor = Tr::tr("You");
|
||||
else
|
||||
result.shortAuthor = result.author;
|
||||
const uint timeStamp = blame.at(3).mid(12).toUInt();
|
||||
result.authorTime = QDateTime::fromSecsSinceEpoch(timeStamp);
|
||||
result.summary = blame.at(9).mid(8);
|
||||
result.filePath = filePath;
|
||||
return result;
|
||||
}
|
||||
|
||||
void InstantBlame::once()
|
||||
{
|
||||
if (!settings().instantBlame()) {
|
||||
const TextEditorWidget *widget = TextEditorWidget::currentTextEditorWidget();
|
||||
if (!widget)
|
||||
return;
|
||||
connect(EditorManager::instance(), &EditorManager::currentEditorChanged,
|
||||
this, [this] { m_blameMark.reset(); }, Qt::SingleShotConnection);
|
||||
|
||||
connect(widget, &QPlainTextEdit::cursorPositionChanged,
|
||||
this, [this] { m_blameMark.reset(); }, Qt::SingleShotConnection);
|
||||
|
||||
const Utils::FilePath workingDirectory = GitPlugin::currentState().topLevel();
|
||||
if (!refreshWorkingDirectory(workingDirectory))
|
||||
return;
|
||||
}
|
||||
|
||||
force();
|
||||
}
|
||||
|
||||
void InstantBlame::force()
|
||||
{
|
||||
m_lastVisitedEditorLine = -1;
|
||||
perform();
|
||||
}
|
||||
|
||||
void InstantBlame::perform()
|
||||
{
|
||||
const TextEditorWidget *widget = TextEditorWidget::currentTextEditorWidget();
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
if (widget->textDocument()->isModified()) {
|
||||
m_blameMark.reset();
|
||||
m_lastVisitedEditorLine = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const QTextCursor cursor = widget->textCursor();
|
||||
const QTextBlock block = cursor.block();
|
||||
const int line = block.blockNumber() + 1;
|
||||
const int lines = widget->document()->blockCount();
|
||||
|
||||
if (line >= lines) {
|
||||
m_lastVisitedEditorLine = -1;
|
||||
m_blameMark.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_lastVisitedEditorLine == line)
|
||||
return;
|
||||
|
||||
m_lastVisitedEditorLine = line;
|
||||
|
||||
const Utils::FilePath filePath = widget->textDocument()->filePath();
|
||||
const QFileInfo fi(filePath.toString());
|
||||
const Utils::FilePath workingDirectory = Utils::FilePath::fromString(fi.path());
|
||||
const QString lineString = QString("%1,%1").arg(line);
|
||||
const auto commandHandler = [this, filePath, line](const CommandResult &result) {
|
||||
if (result.result() == ProcessResult::FinishedWithError &&
|
||||
result.cleanedStdErr().contains("no such path")) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
const QString output = result.cleanedStdOut();
|
||||
if (output.isEmpty()) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
const CommitInfo info = parseBlameOutput(output.split('\n'), filePath, m_author);
|
||||
m_blameMark.reset(new BlameMark(filePath, line, info));
|
||||
};
|
||||
QStringList options = {"blame", "-p"};
|
||||
if (settings().instantBlameIgnoreSpaceChanges())
|
||||
options.append("-w");
|
||||
if (settings().instantBlameIgnoreLineMoves())
|
||||
options.append("-M");
|
||||
options.append({"-L", lineString, "--", filePath.toString()});
|
||||
gitClient().vcsExecWithHandler(workingDirectory, options, this,
|
||||
commandHandler, RunFlags::NoOutput, m_codec);
|
||||
}
|
||||
|
||||
void InstantBlame::stop()
|
||||
{
|
||||
m_blameMark.reset();
|
||||
m_cursorPositionChangedTimer->stop();
|
||||
disconnect(m_blameCursorPosConn);
|
||||
disconnect(m_documentChangedConn);
|
||||
}
|
||||
|
||||
bool InstantBlame::refreshWorkingDirectory(const FilePath &workingDirectory)
|
||||
{
|
||||
if (workingDirectory.isEmpty())
|
||||
return false;
|
||||
|
||||
if (m_workingDirectory == workingDirectory)
|
||||
return true;
|
||||
|
||||
m_workingDirectory = workingDirectory;
|
||||
|
||||
const auto commitCodecHandler = [this, workingDirectory](const CommandResult &result) {
|
||||
QTextCodec *codec = nullptr;
|
||||
|
||||
if (result.result() == ProcessResult::FinishedWithSuccess) {
|
||||
const QString codecName = result.cleanedStdOut().trimmed();
|
||||
codec = QTextCodec::codecForName(codecName.toUtf8());
|
||||
} else {
|
||||
codec = gitClient().defaultCommitEncoding();
|
||||
}
|
||||
|
||||
if (m_codec != codec) {
|
||||
m_codec = codec;
|
||||
force();
|
||||
}
|
||||
};
|
||||
gitClient().readConfigAsync(workingDirectory, {"config", "i18n.commitEncoding"},
|
||||
commitCodecHandler);
|
||||
|
||||
const auto authorHandler = [this, workingDirectory](const CommandResult &result) {
|
||||
if (result.result() == ProcessResult::FinishedWithSuccess) {
|
||||
const QString authorInfo = result.cleanedStdOut().trimmed();
|
||||
const Author author = gitClient().parseAuthor(authorInfo);
|
||||
|
||||
if (m_author != author) {
|
||||
m_author = author;
|
||||
force();
|
||||
}
|
||||
}
|
||||
};
|
||||
gitClient().readConfigAsync(workingDirectory, {"var", "GIT_AUTHOR_IDENT"},
|
||||
authorHandler);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // Git::Internal
|
68
src/plugins/git/instantblame.h
Normal file
68
src/plugins/git/instantblame.h
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2023 Andre Hartmann (aha_1980@gmx.de)
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gitclient.h"
|
||||
|
||||
#include <texteditor/textmark.h>
|
||||
|
||||
#include <utils/filepath.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QLayout;
|
||||
class QTextCodec;
|
||||
class QTimer;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace Git::Internal {
|
||||
|
||||
class CommitInfo {
|
||||
public:
|
||||
QString sha1;
|
||||
QString shortAuthor;
|
||||
QString author;
|
||||
QString authorMail;
|
||||
QDateTime authorTime;
|
||||
QString summary;
|
||||
Utils::FilePath filePath;
|
||||
};
|
||||
|
||||
class BlameMark : public TextEditor::TextMark
|
||||
{
|
||||
public:
|
||||
BlameMark(const Utils::FilePath &fileName, int lineNumber, const CommitInfo &info);
|
||||
bool addToolTipContent(QLayout *target) const;
|
||||
QString toolTipText(const CommitInfo &info) const;
|
||||
|
||||
private:
|
||||
const CommitInfo m_info;
|
||||
};
|
||||
|
||||
class InstantBlame : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
InstantBlame();
|
||||
|
||||
void setup();
|
||||
void once();
|
||||
void force();
|
||||
void stop();
|
||||
void perform();
|
||||
|
||||
private:
|
||||
bool refreshWorkingDirectory(const Utils::FilePath &workingDirectory);
|
||||
|
||||
Utils::FilePath m_workingDirectory;
|
||||
QTextCodec *m_codec = nullptr;
|
||||
Author m_author;
|
||||
int m_lastVisitedEditorLine = -1;
|
||||
QTimer *m_cursorPositionChangedTimer = nullptr;
|
||||
std::unique_ptr<BlameMark> m_blameMark;
|
||||
QMetaObject::Connection m_blameCursorPosConn;
|
||||
QMetaObject::Connection m_documentChangedConn;
|
||||
};
|
||||
|
||||
} // Git::Internal
|
Reference in New Issue
Block a user