kategitblameplugin.cpp 12.2 KB
Newer Older
1
2
3
4
5
6
7
/*
    SPDX-FileCopyrightText: 2021 Kåre Särs <kare.sars@iki.fi>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "kategitblameplugin.h"
8
#include "gitblametooltip.h"
9
10
11
12
13
14
15

#include <algorithm>

#include <KConfigGroup>
#include <KLocalizedString>
#include <KPluginFactory>
#include <KSharedConfig>
16
#include <KTextEditor/Editor>
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <KTextEditor/Document>
#include <KTextEditor/InlineNoteInterface>
#include <KTextEditor/InlineNoteProvider>
#include <KTextEditor/MainWindow>
#include <KTextEditor/View>

#include <QUrl>
#include <QDir>

#include <QFontMetricsF>
#include <QHash>
#include <QPainter>
#include <QRegularExpression>
#include <QVariant>

GitBlameInlineNoteProvider::GitBlameInlineNoteProvider(KTextEditor::Document *doc, KateGitBlamePlugin *plugin)
    : m_doc(doc)
    , m_plugin(plugin)
{
    for (auto view : m_doc->views()) {
        qobject_cast<KTextEditor::InlineNoteInterface *>(view)->registerInlineNoteProvider(this);
    }

    connect(m_doc, &KTextEditor::Document::viewCreated, this, [this](KTextEditor::Document *, KTextEditor::View *view) {
        qobject_cast<KTextEditor::InlineNoteInterface *>(view)->registerInlineNoteProvider(this);
    });

    // textInserted and textRemoved are emitted per line, then the last line is followed by a textChanged signal
    connect(m_doc, &KTextEditor::Document::textInserted, this, [/*this*/](KTextEditor::Document *, const KTextEditor::Cursor &/*cur*/, const QString &/*str*/) {
        //qDebug() << cur.line() << str << this;
    });
    connect(m_doc, &KTextEditor::Document::textRemoved, this, [/*this*/](KTextEditor::Document *, const KTextEditor::Range &/*range*/, const QString &/*str*/) {
        //qDebug() << range.start() << str << this;
    });

    connect(m_doc, &KTextEditor::Document::textChanged, this, [/*this*/](KTextEditor::Document *) {
        //qDebug() << this;
    });
}

GitBlameInlineNoteProvider::~GitBlameInlineNoteProvider()
{
    for (auto view : m_doc->views()) {
        qobject_cast<KTextEditor::InlineNoteInterface *>(view)->unregisterInlineNoteProvider(this);
    }
}

QVector<int> GitBlameInlineNoteProvider::inlineNotes(int line) const
{
    if (!m_plugin->hasBlameInfo()) {
        return QVector<int>();
    }

    int lineLen = m_doc->line(line).size();
    for (const auto view: m_doc->views()) {
        if (view->cursorPosition().line() == line) {
73
            return QVector<int>{lineLen + 4};
74
75
76
77
78
79
80
        }
    }
    return QVector<int>();
}

QSize GitBlameInlineNoteProvider::inlineNoteSize(const KTextEditor::InlineNote &note) const
{
81
    return QSize(note.lineHeight() * 50, note.lineHeight());
82
83
84
85
86
87
88
89
90
91
92
}

void GitBlameInlineNoteProvider::paintInlineNote(const KTextEditor::InlineNote &note, QPainter &painter) const
{
    QFont font = note.font();
    painter.setFont(font);
    const QFontMetrics fm(note.font());

    int lineNr = note.position().line();
    const KateGitBlameInfo &info = m_plugin->blameInfo(lineNr, m_doc->line(lineNr));

93
94
95
    QString text = info.title.isEmpty() ?
    QStringLiteral(" %1: %2 ").arg(info.name, m_locale.toString(info.date, QLocale::NarrowFormat)) :
    QStringLiteral(" %1: %2: %3 ").arg(info.name, m_locale.toString(info.date, QLocale::NarrowFormat), info.title);
96
    QRect rectangle{0, 0, fm.horizontalAdvance(text), note.lineHeight()};
97

98
99
100
101
    auto editor = KTextEditor::Editor::instance();
    auto color = QColor::fromRgba(editor->theme().textColor(KSyntaxHighlighting::Theme::Normal));
    color.setAlpha(0);
    painter.setPen(color);
102
    color.setAlpha(8);
103
    painter.setBrush(color);
104
    painter.drawRect(rectangle);
105

106
107
108
    color.setAlpha(note.underMouse() ? 130 : 90);
    painter.setPen(color);
    painter.setBrush(color);
109
110
111
    painter.drawText(rectangle, text);
}

112
void GitBlameInlineNoteProvider::inlineNoteActivated(const KTextEditor::InlineNote &note, Qt::MouseButtons buttons, const QPoint &)
113
{
114
115
116
    if ((buttons & Qt::LeftButton) != 0) {
        int lineNr = note.position().line();
        const KateGitBlameInfo &info = m_plugin->blameInfo(lineNr, m_doc->line(lineNr));
117
        m_plugin->showCommitInfo(info.commitHash);
118
    }
119
120
121
122
123
124
125
126
127
}

K_PLUGIN_FACTORY_WITH_JSON(KateGitBlamePluginFactory, "kategitblameplugin.json", registerPlugin<KateGitBlamePlugin>();)

KateGitBlamePlugin::KateGitBlamePlugin(QObject *parent, const QList<QVariant> &)
    : KTextEditor::Plugin(parent)
    , m_blameInfoProc(this)
{

128
129
130
    connect(&m_blameInfoProc, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &KateGitBlamePlugin::blameFinished);

    connect(&m_showProc, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &KateGitBlamePlugin::showFinished);
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
}

KateGitBlamePlugin::~KateGitBlamePlugin()
{
    qDeleteAll(m_inlineNoteProviders);
}

QObject *KateGitBlamePlugin::createView(KTextEditor::MainWindow *mainWindow)
{
    m_mainWindow = mainWindow;
    for (auto view : m_mainWindow->views()) {
        addDocument(view->document());
    }

    connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, [this](KTextEditor::View *view) {
        addDocument(view->document());
    });

    connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KateGitBlamePlugin::viewChanged);

    return nullptr;
}

void KateGitBlamePlugin::addDocument(KTextEditor::Document *doc)
{
    if (!m_inlineNoteProviders.contains(doc)) {
        m_inlineNoteProviders.insert(doc, new GitBlameInlineNoteProvider(doc, this));
    }

    connect(doc, &KTextEditor::Document::destroyed, this, [this, doc]() {
        m_inlineNoteProviders.remove(doc);
    });
163
164
165
166
167
168
    connect(doc, &KTextEditor::Document::reloaded, this, [this, doc]() {
        startBlameProcess(doc->url());
    });
    connect(doc, &KTextEditor::Document::documentSavedOrUploaded, this, [this, doc]() {
        startBlameProcess(doc->url());
    });
169
170
171
172
173
174
175
176
177
}

void KateGitBlamePlugin::viewChanged(KTextEditor::View *view)
{
    m_blameInfo.clear();

    if (view == nullptr || view->document() == nullptr) {
        return;
    }
178
179
180
    m_blameInfoView = view;
    startBlameProcess(view->document()->url());
}
181

182
183
void KateGitBlamePlugin::startBlameProcess(const QUrl &url)
{
184
185
186
187
188
189
190
191
192
    if (m_blameInfoProc.state() != QProcess::NotRunning) {
        // Wait for the previous process to be done...
        return;
    }

    QString fileName{url.fileName()};
    QDir dir{url.toLocalFile()};
    dir.cdUp();

193
    QStringList args{QStringLiteral("blame"), QStringLiteral("--date=iso-strict"), QStringLiteral("./%1").arg(fileName)};
194
195

    m_blameInfoProc.setWorkingDirectory(dir.absolutePath());
196
    m_blameInfoProc.start(QStringLiteral("git"), args, QIODevice::ReadOnly);
197
198
}

199
void KateGitBlamePlugin::startShowProcess(const QUrl &url, const QString &hash)
200
{
201
202
203
204
205
206
    if (m_showProc.state() != QProcess::NotRunning) {
        // Wait for the previous process to be done...
        return;
    }
    if (hash == m_activeCommitInfo.m_hash) {
        // We have already the data
207
208
209
210
211
212
        return;
    }

    QDir dir{url.toLocalFile()};
    dir.cdUp();

213
    QStringList args{QStringLiteral("show"),  hash};
214
    m_showProc.setWorkingDirectory(dir.absolutePath());
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
    m_showProc.start(QStringLiteral("git"), args, QIODevice::ReadOnly);

}


void KateGitBlamePlugin::showCommitInfo(const QString &hash)
{
    if (!m_mainWindow || !m_mainWindow->activeView() || !m_mainWindow->activeView()->document()) {
        return;
    }

    if (hash == m_activeCommitInfo.m_hash) {
        m_showHash.clear();
        m_tooltip.show(m_activeCommitInfo.m_content, m_mainWindow->activeView());
    }
    else {
        m_showHash = hash;
        startShowProcess(m_mainWindow->activeView()->document()->url(), hash);
    }
234
235
236
237
}


void KateGitBlamePlugin::blameFinished(int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/)
238
239
240
241
242
243
244
245
246
247
248
{
    QString stdErr = QString::fromUtf8(m_blameInfoProc.readAllStandardError());
    const QStringList stdOut = QString::fromUtf8(m_blameInfoProc.readAllStandardOutput()).split(QLatin1Char('\n'));

    // check if the git process was running for a previous document when the view changed.
    // if that is the case re-trigger the process and skip this data
    if (m_blameInfoView != m_mainWindow->activeView()) {
        viewChanged(m_mainWindow->activeView());
        return;
    }

249
    const static QRegularExpression lineReg(QStringLiteral("(\\S+)[^\\(]+\\((.*)\\s+(\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d\\S+)[^\\)]+\\)\\s(.*)"));
250
251
252
253
254
255

    m_blameInfo.clear();

    for (const auto &line: stdOut) {
        const QRegularExpressionMatch match = lineReg.match(line);
        if (match.hasMatch()) {
256
            m_blameInfo.append({ match.captured(1), match.captured(2).trimmed(),
257
                QDateTime::fromString(match.captured(3), Qt::ISODate), QString(), match.captured(4) });
258
259
260
261
        }
    }
}

262
263
264
265
266
267
268
269
void KateGitBlamePlugin::showFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    QString stdErr = QString::fromUtf8(m_showProc.readAllStandardError());
    const QString stdOut = QString::fromUtf8(m_showProc.readAllStandardOutput());

    if (exitCode != 0 || exitStatus != QProcess::NormalExit) {
        return;
    }
270
271
272
273
274
    QStringList args = m_showProc.arguments();
    if (args.size() != 2) {
        qWarning() << "Wrong number of parameters:" << args;
        return;
    }
275

276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
    int titleStart = 0;
    for (int i = 0; i < 4; ++i) {
        titleStart = stdOut.indexOf(QLatin1Char('\n'), titleStart+1);
        if (titleStart < 0 || titleStart >= stdOut.size()-1) {
            qWarning() << "This is not a known git show format";
            return;
        }
    }

    int titleEnd = stdOut.indexOf(QLatin1Char('\n'), titleStart+1);
    if (titleEnd < 0 || titleEnd >= stdOut.size()-1) {
        qWarning() << "This is not a known git show format";
        return;
    }

    m_activeCommitInfo.m_title = stdOut.mid(titleStart, titleEnd-titleStart);
    m_activeCommitInfo.m_hash = args[1];
    m_activeCommitInfo.m_title = m_activeCommitInfo.m_title.trimmed();
    m_activeCommitInfo.m_content = stdOut;

    if (!m_showHash.isEmpty() && m_showHash != args[1]) {
        startShowProcess(m_mainWindow->activeView()->document()->url(), m_showHash);
        return;
    }
    if (!m_showHash.isEmpty()) {
        m_showHash.clear();
        m_tooltip.show(stdOut, m_mainWindow->activeView());
    }
304
305
}

306
307
308
309
310
311
312
bool KateGitBlamePlugin::hasBlameInfo() const
{
    return !m_blameInfo.isEmpty();
}

const KateGitBlameInfo &KateGitBlamePlugin::blameInfo(int lineNr, const QStringView &lineText)
{
313
    if (m_blameInfo.isEmpty()) {
314
        return blameGetUpdateInfo(-1);
315
    }
316
317
318
319
320

    int adjustedLineNr = lineNr + m_lineOffset;

    if (adjustedLineNr >= 0 && adjustedLineNr < m_blameInfo.size()) {
        if (m_blameInfo[adjustedLineNr].line == lineText) {
321
            return blameGetUpdateInfo(adjustedLineNr);
322
323
324
325
326
327
        }
    }

    // FIXME search for the matching line
    // search for the line 100 lines before and after until it matches
    m_lineOffset = 0;
328
329
330
331
    while (m_lineOffset < 100  &&
        lineNr+m_lineOffset >= 0 &&
        lineNr+m_lineOffset < m_blameInfo.size())
    {
332
        if (m_blameInfo[lineNr+m_lineOffset].line == lineText) {
333
            return blameGetUpdateInfo(lineNr+m_lineOffset);
334
335
336
337
338
        }
        m_lineOffset++;
    }

    m_lineOffset = 0;
339
340
341
342
    while (m_lineOffset > -100  &&
        lineNr+m_lineOffset >= 0 &&
        (lineNr+m_lineOffset) < m_blameInfo.size())
    {
343
        if (m_blameInfo[lineNr+m_lineOffset].line == lineText) {
344
            return blameGetUpdateInfo(lineNr+m_lineOffset);
345
346
347
348
        }
        m_lineOffset--;
    }

349
    return blameGetUpdateInfo(-1);
350
351
}

352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
const KateGitBlameInfo &KateGitBlamePlugin::blameGetUpdateInfo(int lineNr)
{
    static const KateGitBlameInfo dummy{QStringLiteral("hash"),
        i18n("Not Committed Yet"),
        QDateTime::currentDateTime(),
        QStringLiteral(""),
        QStringLiteral("")
    };

    if (m_blameInfo.isEmpty() || lineNr < 0 || lineNr >= m_blameInfo.size()) {
        return dummy;
    }

    KateGitBlameInfo &info = m_blameInfo[lineNr];
    if (info.commitHash == m_activeCommitInfo.m_hash) {
        if (info.title != m_activeCommitInfo.m_title) {
            info.title = m_activeCommitInfo.m_title;
        }
    }
    else {
        startShowProcess(m_mainWindow->activeView()->document()->url(), info.commitHash);
    }
    return info;
}
376
377
378
379
380

void KateGitBlamePlugin::readConfig()
{
}

381
382
383
384
385
386
387
void KateGitBlamePlugin::CommitInfo::clear()
{
    m_hash.clear();
    m_title.clear();
    m_content.clear();
}

388
#include "kategitblameplugin.moc"