Git: кэш+пагинация истории, diff в потоке, точечный коммит, честное удаление
- История: кэш по vault|rel|headOid, fileHistory(vault,file,limit), в диалоге Показать ещё по 60 с сохранением выбора - Diff считается в QtConcurrent-пуле (parseBlocksWithDiffAsync+blocksReady, stale-guard), спиннер busy; MarkdownView.diffApplied - commitFile точечно вместо всего хранилища (общий commitFromIndex), autoCommit честный; fileExistedAt + willDelete в подтверждении - DocumentManager.readTextFile общий (GitManager.workdirContent через него); fileGitStatus через relativePath - Тесты: пагинация/кэш, existed, commitFile не трогает B, async==sync
This commit is contained in:
+82
-24
@@ -20,12 +20,14 @@
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFutureWatcher>
|
||||
#include <QGuiApplication>
|
||||
#include <QImageReader>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QTime>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
using KSyntaxHighlighting::AbstractHighlighter;
|
||||
using KSyntaxHighlighting::Definition;
|
||||
@@ -854,8 +856,10 @@ QString MdParser::render(const QString &markdown, const QString &vaultRoot,
|
||||
return renderInner(markdown, vaultRoot, fileDir);
|
||||
}
|
||||
|
||||
QString MdParser::renderInner(const QString &markdown, const QString &vaultRoot,
|
||||
const QString &fileDir)
|
||||
// Файлово-статический рендер с явным Impl: безопасен для рабочего потока
|
||||
// (свой парсер cmark на вызов, никакой разделяемой мутации кроме переданного impl).
|
||||
static QString renderWithImpl(MdParser::Impl *impl, const QString &markdown,
|
||||
const QString &vaultRoot, const QString &fileDir)
|
||||
{
|
||||
cmark_gfm_core_extensions_ensure_registered();
|
||||
cmark_parser *parser = cmark_parser_new(CMARK_OPT_DEFAULT);
|
||||
@@ -868,7 +872,7 @@ QString MdParser::renderInner(const QString &markdown, const QString &vaultRoot,
|
||||
cmark_parser_feed(parser, utf8.constData(), static_cast<size_t>(utf8.size()));
|
||||
cmark_node *doc = cmark_parser_finish(parser);
|
||||
|
||||
Renderer r(m, vaultRoot, fileDir);
|
||||
Renderer r(impl, vaultRoot, fileDir);
|
||||
QString html = r.renderTree(doc);
|
||||
|
||||
cmark_node_free(doc);
|
||||
@@ -876,6 +880,12 @@ QString MdParser::renderInner(const QString &markdown, const QString &vaultRoot,
|
||||
return html;
|
||||
}
|
||||
|
||||
QString MdParser::renderInner(const QString &markdown, const QString &vaultRoot,
|
||||
const QString &fileDir)
|
||||
{
|
||||
return renderWithImpl(m, markdown, vaultRoot, fileDir);
|
||||
}
|
||||
|
||||
QVariantList MdParser::parseBlocks(const QString &markdown, const QString &vaultRoot, const QString &fileDir)
|
||||
{
|
||||
QVariantList blocks;
|
||||
@@ -981,13 +991,16 @@ QString MdParser::highlightedCode(const QString &code, const QString &language)
|
||||
return hl.highlight(code, lang, true);
|
||||
}
|
||||
|
||||
QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &newMd,
|
||||
const QString &vaultRoot, const QString &fileDir)
|
||||
// Потокобезопасное ядро diff-блоков: работает с явно переданным Impl
|
||||
// (в главном потоке — m, в рабочем — локальный), состояние только в нём.
|
||||
static QVariantList buildDiffBlocks(MdParser::Impl *impl, const QString &oldMd,
|
||||
const QString &newMd, const QString &vaultRoot,
|
||||
const QString &fileDir)
|
||||
{
|
||||
QVariantList blocks;
|
||||
if (m) {
|
||||
m->codeBlocks.clear();
|
||||
m->codeCounter = 0;
|
||||
if (impl) {
|
||||
impl->codeBlocks.clear();
|
||||
impl->codeCounter = 0;
|
||||
}
|
||||
if (oldMd.isEmpty() && newMd.isEmpty())
|
||||
return blocks;
|
||||
@@ -1006,13 +1019,13 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
|
||||
|
||||
auto appendCodeBlock = [&](const QString &lang, const QString &code, const char *change, bool tint) {
|
||||
QString codeId;
|
||||
if (m) {
|
||||
codeId = QStringLiteral("code_%1").arg(m->codeCounter++);
|
||||
m->codeBlocks.insert(codeId, code);
|
||||
if (impl) {
|
||||
codeId = QStringLiteral("code_%1").arg(impl->codeCounter++);
|
||||
impl->codeBlocks.insert(codeId, code);
|
||||
}
|
||||
CodeHighlighter hl(m ? m->m_repo : nullptr);
|
||||
QString highlighted = (m && m->m_repo) ? hl.highlight(code, lang, true)
|
||||
: plainToRich(code);
|
||||
CodeHighlighter hl(impl ? impl->m_repo : nullptr);
|
||||
QString highlighted = (impl && impl->m_repo) ? hl.highlight(code, lang, true)
|
||||
: plainToRich(code);
|
||||
if (tint)
|
||||
highlighted = tintDiffHtml(highlighted, change[0] == 'd');
|
||||
QVariantMap block;
|
||||
@@ -1030,11 +1043,11 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
|
||||
// подсветкой внутри — изменения видны там, где сделаны. Копируется новое.
|
||||
auto appendMergedCode = [&](const QString &oldLang, const QString &oldCode,
|
||||
const QString &newLang, const QString &newCode) -> bool {
|
||||
CodeHighlighter hl(m ? m->m_repo : nullptr);
|
||||
const QString oldHl = (m && m->m_repo) ? hl.highlight(oldCode, oldLang, true)
|
||||
: plainToRich(oldCode);
|
||||
const QString newHl = (m && m->m_repo) ? hl.highlight(newCode, newLang, true)
|
||||
: plainToRich(newCode);
|
||||
CodeHighlighter hl(impl ? impl->m_repo : nullptr);
|
||||
const QString oldHl = (impl && impl->m_repo) ? hl.highlight(oldCode, oldLang, true)
|
||||
: plainToRich(oldCode);
|
||||
const QString newHl = (impl && impl->m_repo) ? hl.highlight(newCode, newLang, true)
|
||||
: plainToRich(newCode);
|
||||
const QStringList oldLines = oldCode.split(QLatin1Char('\n'));
|
||||
const QStringList newLines = newCode.split(QLatin1Char('\n'));
|
||||
const QStringList oldFrag = oldHl.split(QStringLiteral("<br/>"));
|
||||
@@ -1072,9 +1085,9 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
|
||||
flushPending();
|
||||
const QString lang = !newLang.isEmpty() ? newLang : oldLang;
|
||||
QString codeId;
|
||||
if (m) {
|
||||
codeId = QStringLiteral("code_%1").arg(m->codeCounter++);
|
||||
m->codeBlocks.insert(codeId, newCode);
|
||||
if (impl) {
|
||||
codeId = QStringLiteral("code_%1").arg(impl->codeCounter++);
|
||||
impl->codeBlocks.insert(codeId, newCode);
|
||||
}
|
||||
QVariantMap block;
|
||||
block["type"] = "code";
|
||||
@@ -1109,7 +1122,7 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
|
||||
if (isCode) {
|
||||
appendCodeBlock(lang, code, change, changed);
|
||||
} else {
|
||||
QString html = renderInner(op.text, vaultRoot, fileDir);
|
||||
QString html = renderWithImpl(impl, op.text, vaultRoot, fileDir);
|
||||
if (html.trimmed().isEmpty())
|
||||
continue;
|
||||
if (changed)
|
||||
@@ -1121,9 +1134,54 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
|
||||
// Пустой результат при непустом вводе — отдать как один html-блок
|
||||
if (blocks.isEmpty() && (!oldMd.trimmed().isEmpty() || !newMd.trimmed().isEmpty())) {
|
||||
const QString src = !oldMd.trimmed().isEmpty() ? oldMd : newMd;
|
||||
const QString html = renderInner(src, vaultRoot, fileDir);
|
||||
const QString html = renderWithImpl(impl, src, vaultRoot, fileDir);
|
||||
if (!html.trimmed().isEmpty())
|
||||
appendHtmlBlock(html, "same");
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &newMd,
|
||||
const QString &vaultRoot, const QString &fileDir)
|
||||
{
|
||||
if (m) {
|
||||
m->codeBlocks.clear();
|
||||
m->codeCounter = 0;
|
||||
}
|
||||
return buildDiffBlocks(m, oldMd, newMd, vaultRoot, fileDir);
|
||||
}
|
||||
|
||||
int MdParser::parseBlocksWithDiffAsync(const QString &oldMd, const QString &newMd,
|
||||
const QString &vaultRoot, const QString &fileDir)
|
||||
{
|
||||
const int id = ++m_asyncId;
|
||||
QFutureWatcher<QVariantList> *watcher = new QFutureWatcher<QVariantList>(this);
|
||||
connect(watcher, &QFutureWatcher<QVariantList>::finished, this, [this, watcher, id]() {
|
||||
QVariantList blocks = watcher->result();
|
||||
watcher->deleteLater();
|
||||
if (id != m_asyncId || !m)
|
||||
return; // устарел — пользователь уже смотрит другую версию
|
||||
if (m->codeBlocks.size() > 2000) {
|
||||
m->codeBlocks.clear(); // защита от бесконечного роста между полными перерисовками
|
||||
m->codeCounter = 0;
|
||||
}
|
||||
// Коды регистрируем здесь, в главном потоке, свежими id
|
||||
for (auto &v : blocks) {
|
||||
QVariantMap bm = v.toMap();
|
||||
if (bm.value("type").toString() == QLatin1String("code")) {
|
||||
const QString newId = QStringLiteral("code_%1").arg(m->codeCounter++);
|
||||
m->codeBlocks.insert(newId, bm.value("codeText").toString());
|
||||
bm["codeId"] = newId;
|
||||
v = bm;
|
||||
}
|
||||
}
|
||||
emit blocksReady(id, blocks);
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run([oldMd, newMd, vaultRoot, fileDir]() {
|
||||
MdParser::Impl local; // свой Repository подсветки — главный поток не трогаем
|
||||
if (!local.m_repo)
|
||||
return QVariantList();
|
||||
return buildDiffBlocks(&local, oldMd, newMd, vaultRoot, fileDir);
|
||||
}));
|
||||
return id;
|
||||
}
|
||||
@@ -36,10 +36,22 @@ public:
|
||||
const QString &vaultRoot,
|
||||
const QString &fileDir);
|
||||
|
||||
// То же, но в рабочем потоке (UI не виснет на больших файлах).
|
||||
// Возвращает requestId; результат приходит сигналом blocksReady (устаревшие id игнорить).
|
||||
// Регистрация кодов для копирования выполняется в главном потоке при приёме.
|
||||
Q_INVOKABLE int parseBlocksWithDiffAsync(const QString &oldMd,
|
||||
const QString &newMd,
|
||||
const QString &vaultRoot,
|
||||
const QString &fileDir);
|
||||
|
||||
Q_INVOKABLE QString getCodeBlock(const QString &id) const;
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text) const;
|
||||
Q_INVOKABLE QString highlightedCode(const QString &code, const QString &language) const;
|
||||
|
||||
signals:
|
||||
void blocksReady(int requestId, const QVariantList &blocks);
|
||||
|
||||
public:
|
||||
struct Impl;
|
||||
private:
|
||||
// Внутренний рендер без сброса codeBlocks/codeCounter (для parseBlocks*,
|
||||
@@ -49,4 +61,5 @@ private:
|
||||
const QString &fileDir);
|
||||
|
||||
Impl *m = nullptr;
|
||||
int m_asyncId = 0;
|
||||
};
|
||||
Reference in New Issue
Block a user