forked from qt-creator/qt-creator
Explicitly reset the document when the document gets closed. Task-number: QTCREATORBUG-30824 Change-Id: I4fb3d6fd6041990e5b8b4f6b7c4fd9ebc62f5a4a Reviewed-by: Orgad Shaneh <orgads@gmail.com> Reviewed-by: André Hartmann <aha_1980@gmx.de>
377 lines
12 KiB
C++
377 lines
12 KiB
C++
// 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 <QAction>
|
|
#include <QDateTime>
|
|
#include <QLabel>
|
|
#include <QLayout>
|
|
#include <QLoggingCategory>
|
|
#include <QTextCodec>
|
|
#include <QTimer>
|
|
|
|
namespace Git::Internal {
|
|
|
|
static Q_LOGGING_CATEGORY(log, "qtc.vcs.git.instantblame", QtWarningMsg);
|
|
|
|
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();
|
|
m_cursorPositionChangedTimer = new QTimer(this);
|
|
m_cursorPositionChangedTimer->setSingleShot(true);
|
|
connect(m_cursorPositionChangedTimer, &QTimer::timeout, this, &InstantBlame::perform);
|
|
}
|
|
|
|
void InstantBlame::setup()
|
|
{
|
|
qCDebug(log) << "Setup";
|
|
|
|
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) {
|
|
qCInfo(log) << "Cannot get widget for editor" << editor;
|
|
return;
|
|
}
|
|
|
|
if (qobject_cast<const VcsBaseEditorWidget *>(widget)) {
|
|
qCDebug(log) << "Deactivating in VCS editors";
|
|
return; // Skip in VCS editors like log or blame
|
|
}
|
|
|
|
const FilePath workingDirectory = currentState().currentFileTopLevel();
|
|
if (!refreshWorkingDirectory(workingDirectory))
|
|
return;
|
|
|
|
qCInfo(log) << "Adding blame cursor connection";
|
|
m_blameCursorPosConn = connect(widget, &QPlainTextEdit::cursorPositionChanged, this,
|
|
[this] {
|
|
if (!settings().instantBlame()) {
|
|
disconnect(m_blameCursorPosConn);
|
|
return;
|
|
}
|
|
m_cursorPositionChangedTimer->start(500);
|
|
});
|
|
m_document = editor->document();
|
|
m_documentChangedConn = connect(m_document, &IDocument::changed,
|
|
this, &InstantBlame::slotDocumentChanged,
|
|
Qt::UniqueConnection);
|
|
|
|
force();
|
|
};
|
|
|
|
connect(&settings().instantBlame, &BaseAspect::changed, this, [this, setupBlameForEditor] {
|
|
if (settings().instantBlame())
|
|
setupBlameForEditor(EditorManager::currentEditor());
|
|
else
|
|
stop();
|
|
});
|
|
|
|
connect(EditorManager::instance(), &EditorManager::currentEditorChanged,
|
|
this, setupBlameForEditor);
|
|
connect(EditorManager::instance(), &EditorManager::documentClosed,
|
|
this, [this](IDocument *doc) {
|
|
if (m_document != doc)
|
|
return;
|
|
disconnect(m_documentChangedConn);
|
|
m_document = nullptr;
|
|
});
|
|
}
|
|
|
|
// 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) {
|
|
qCWarning(log) << "Cannot get current text editor 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 FilePath workingDirectory = currentState().topLevel();
|
|
if (!refreshWorkingDirectory(workingDirectory))
|
|
return;
|
|
}
|
|
|
|
force();
|
|
}
|
|
|
|
void InstantBlame::force()
|
|
{
|
|
qCDebug(log) << "Forcing blame now";
|
|
m_lastVisitedEditorLine = -1;
|
|
perform();
|
|
}
|
|
|
|
void InstantBlame::perform()
|
|
{
|
|
const TextEditorWidget *widget = TextEditorWidget::currentTextEditorWidget();
|
|
if (!widget) {
|
|
qCWarning(log) << "Cannot get current text editor widget";
|
|
return;
|
|
}
|
|
|
|
if (widget->textDocument()->isModified()) {
|
|
qCDebug(log) << "Document is modified, pausing blame";
|
|
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;
|
|
|
|
qCDebug(log) << "New editor line:" << line;
|
|
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()});
|
|
qCDebug(log) << "Running git" << options;
|
|
gitClient().vcsExecWithHandler(workingDirectory, options, this,
|
|
commandHandler, RunFlags::NoOutput, m_codec);
|
|
}
|
|
|
|
void InstantBlame::stop()
|
|
{
|
|
qCInfo(log) << "Stopping blame now";
|
|
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;
|
|
|
|
qCInfo(log) << "Setting new working directory:" << workingDirectory;
|
|
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) {
|
|
qCInfo(log) << "Setting new text codec:" << codec->name();
|
|
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) {
|
|
qCInfo(log) << "Setting new author name:" << author.name << author.email;
|
|
m_author = author;
|
|
force();
|
|
}
|
|
}
|
|
};
|
|
gitClient().readConfigAsync(workingDirectory, {"var", "GIT_AUTHOR_IDENT"},
|
|
authorHandler);
|
|
|
|
return true;
|
|
}
|
|
|
|
void InstantBlame::slotDocumentChanged()
|
|
{
|
|
if (m_document == nullptr) {
|
|
qCWarning(log) << "Document is invalid, disconnecting.";
|
|
disconnect(m_documentChangedConn);
|
|
return;
|
|
}
|
|
|
|
const bool modified = m_document->isModified();
|
|
qCDebug(log) << "Document is changed, modified:" << modified;
|
|
if (m_modified && !modified)
|
|
force();
|
|
m_modified = modified;
|
|
}
|
|
|
|
} // Git::Internal
|