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:
2026-09-06 21:17:25 +03:00
parent f6074df0fc
commit 0846605c2b
12 changed files with 474 additions and 138 deletions
+11 -4
View File
@@ -127,8 +127,15 @@ int DocumentManager::openDocument(const QString &path)
QString DocumentManager::readFile(const QString &path, bool *ok) const
{
return readTextFile(path, 10LL * 1024 * 1024, ok);
}
QString DocumentManager::readTextFile(const QString &path, qint64 maxBytes, bool *ok)
{
if (maxBytes <= 0)
maxBytes = 10LL * 1024 * 1024;
QFileInfo fi(path);
if (fi.exists() && fi.size() > 10 * 1024 * 1024) {
if (fi.exists() && fi.size() > maxBytes) {
if (ok) *ok = false;
return QString();
}
@@ -137,9 +144,9 @@ QString DocumentManager::readFile(const QString &path, bool *ok) const
if (ok) *ok = false;
return QString();
}
// Ограничиваем чтение 10 МБ для защиты от OOM
const QByteArray bytes = f.read(10 * 1024 * 1024 + 1);
if (bytes.size() > 10 * 1024 * 1024) {
// Ограничиваем чтение для защиты от OOM
const QByteArray bytes = f.read(maxBytes + 1);
if (bytes.size() > maxBytes) {
if (ok) *ok = false;
return QString();
}
+5
View File
@@ -79,6 +79,11 @@ private:
void ensureAutosave();
QString readFile(const QString &path, bool *ok) const;
public:
// Общее чтение текстового файла с лимитом (защита от OOM). Используется и GitManager.
static QString readTextFile(const QString &path, qint64 maxBytes = 10LL * 1024 * 1024,
bool *ok = nullptr);
QList<Doc> m_docs;
int m_active = -1;
int m_secondary = -1;
+170 -81
View File
@@ -1,5 +1,7 @@
#include "git/GitManager.h"
#include "core/DocumentManager.h"
#include <QDateTime>
#include <QDir>
#include <QFile>
@@ -188,29 +190,13 @@ QString GitManager::fileGitStatus(const QString &vaultPath, const QString &fileP
if (vaultPath.isEmpty() || filePath.isEmpty())
return QStringLiteral("—");
const QString vault = QDir::cleanPath(vaultPath);
const QString file = QDir::cleanPath(filePath);
if (!file.startsWith(vault + '/') && file != vault)
const QString rel = relativePath(vaultPath, filePath);
if (rel.isEmpty())
return QStringLiteral("—");
git_repository *repo = nullptr;
if (git_repository_open(&repo, vault.toUtf8().constData()) < 0)
return QStringLiteral("—");
// относительный путь внутри репозитория
QString rel = file;
if (rel == vault) {
git_repository_free(repo);
return QStringLiteral("—");
}
if (rel.startsWith(vault)) {
rel = rel.mid(vault.length());
while (rel.startsWith("/") || rel.startsWith("\\"))
rel.remove(0, 1);
}
if (rel.isEmpty()) {
git_repository_free(repo);
return QStringLiteral("—");
}
unsigned int status = 0;
const int rc = git_status_file(&status, repo, rel.toUtf8().constData());
git_repository_free(repo);
@@ -252,6 +238,73 @@ QString GitManager::fileGitStatusShort(const QString &vaultPath, const QString &
return s;
}
bool GitManager::commitFromIndex(git_repository *repo, git_index *index, const QString &message)
{
// Не проверяем entrycount==0 отдельно — пустой индекс может означать удаление всех файлов,
// что тоже нужно коммитить. Проверяем равенство дерева ниже.
// Создаём дерево
git_oid treeId{};
git_tree *tree = nullptr;
if (git_index_write_tree(&treeId, index) < 0
|| git_tree_lookup(&tree, repo, &treeId) < 0) {
setLastError(QStringLiteral("Не удалось создать дерево: %1").arg(gitErr()));
return false;
}
// Родитель: head, если коммиты уже есть
git_reference *headRef = nullptr;
git_commit *parentCommit = nullptr;
int nParents = 0;
if (git_repository_head(&headRef, repo) == 0) {
git_object *obj = nullptr;
if (git_reference_peel(&obj, headRef, GIT_OBJECT_COMMIT) == 0) {
parentCommit = reinterpret_cast<git_commit *>(obj);
nParents = 1;
}
if (headRef) git_reference_free(headRef);
}
// Если дерево не изменилось относительно родителя — коммитить нечего.
if (parentCommit) {
const git_oid *prevTree = git_commit_tree_id(parentCommit);
if (prevTree && git_oid_equal(prevTree, &treeId)) {
git_commit_free(parentCommit);
git_tree_free(tree);
setLastError(QStringLiteral("Нет изменений для коммита"));
return false;
}
}
const git_commit *parents[1] = {parentCommit};
// Подпись из конфига репозитория
git_signature *author = nullptr;
if (git_signature_default(&author, repo) < 0) {
git_commit_free(parentCommit);
git_tree_free(tree);
setLastError(QStringLiteral("Не настроена git-личность: %1").arg(gitErr()));
return false;
}
git_oid commitId{};
const int crc = git_commit_create(&commitId, repo, "HEAD",
author, author, "UTF-8",
message.toUtf8().constData(),
tree, nParents, parents);
git_signature_free(author);
if (parentCommit) git_commit_free(parentCommit);
git_tree_free(tree);
if (crc < 0) {
setLastError(QStringLiteral("Не удалось создать коммит: %1").arg(gitErr()));
return false;
}
setLastError(QString());
return true;
}
bool GitManager::commitAll(const QString &path, const QString &message)
{
const QString cleanPath = QDir::cleanPath(path);
@@ -268,78 +321,74 @@ bool GitManager::commitAll(const QString &path, const QString &message)
// Добавляем все файлы, включая новые и удаления (с учётом .gitignore)
git_index_add_all(info.index, nullptr, GIT_INDEX_ADD_DEFAULT, nullptr, nullptr);
const int wrc = git_index_write(info.index);
if (wrc < 0) {
if (git_index_write(info.index) < 0) {
setLastError(QStringLiteral("Не удалось записать индекс: %1").arg(gitErr()));
return false;
}
// Не проверяем entrycount==0 отдельно — пустой индекс может означать удаление всех файлов,
// что тоже нужно коммитить. Проверяем равенство дерева ниже.
return commitFromIndex(info.repo, info.index, message);
}
// Создаём дерево
git_tree *tree = nullptr;
if (git_index_write_tree(&info.treeId, info.index) < 0
|| git_tree_lookup(&tree, info.repo, &info.treeId) < 0) {
setLastError(QStringLiteral("Не удалось создать дерево: %1").arg(gitErr()));
bool GitManager::commitFile(const QString &vaultPath, const QString &filePath, const QString &message)
{
const QString vault = QDir::cleanPath(vaultPath);
const QString rel = relativePath(vaultPath, filePath);
if (vault.isEmpty() || rel.isEmpty()) {
setLastError(QStringLiteral("Неверные пути"));
return false;
}
CommitInfo info;
if (git_repository_open(&info.repo, vault.toUtf8().constData()) < 0) {
setLastError(QStringLiteral("Хранилище не является git-репозиторием"));
return false;
}
// Родитель: head, если коммиты уже есть
git_reference *headRef = nullptr;
git_commit *parentCommit = nullptr;
int nParents = 0;
if (git_repository_head(&headRef, info.repo) == 0) {
git_object *obj = nullptr;
if (git_reference_peel(&obj, headRef, GIT_OBJECT_COMMIT) == 0) {
parentCommit = reinterpret_cast<git_commit *>(obj);
nParents = 1;
}
if (headRef) git_reference_free(headRef);
}
// Если дерево не изменилось относительно родителя — коммитить нечего.
if (parentCommit) {
const git_oid *prevTree = git_commit_tree_id(parentCommit);
if (prevTree && git_oid_equal(prevTree, &info.treeId)) {
git_commit_free(parentCommit);
git_tree_free(tree);
setLastError(QStringLiteral("Нет изменений для коммита"));
return false;
}
}
const git_commit *parents[1] = {parentCommit};
// Подпись из конфига репозитория
git_signature *author = nullptr;
if (git_signature_default(&author, info.repo) < 0) {
git_commit_free(parentCommit);
git_tree_free(tree);
setLastError(QStringLiteral("Не настроена git-личность: %1").arg(gitErr()));
return false;
}
info.author = author;
const int crc = git_commit_create(&info.commitId, info.repo, "HEAD",
author, author, "UTF-8",
message.toUtf8().constData(),
tree, nParents, parents);
if (parentCommit) git_commit_free(parentCommit);
git_tree_free(tree);
if (crc < 0) {
setLastError(QStringLiteral("Не удалось создать коммит: %1").arg(gitErr()));
if (git_repository_index(&info.index, info.repo) < 0) {
setLastError(QStringLiteral("Не удалось открыть индекс: %1").arg(gitErr()));
return false;
}
setLastError(QString());
return true;
// Точечно: существующий файл — в индекс, удалённый — из индекса
const QByteArray relBytes = rel.toUtf8();
int rc;
if (QFile::exists(QDir(vault).filePath(rel)))
rc = git_index_add_bypath(info.index, relBytes.constData());
else
rc = git_index_remove_bypath(info.index, relBytes.constData());
if (rc < 0) {
setLastError(QStringLiteral("Не удалось добавить файл в индекс: %1").arg(gitErr()));
return false;
}
if (git_index_write(info.index) < 0) {
setLastError(QStringLiteral("Не удалось записать индекс: %1").arg(gitErr()));
return false;
}
return commitFromIndex(info.repo, info.index, message);
}
QString GitManager::headOid(git_repository *repo) const
{
git_oid oid;
if (git_reference_name_to_id(&oid, repo, "HEAD") < 0)
return QString();
char buf[41] = {0};
git_oid_tostr(buf, sizeof(buf), &oid);
return QString::fromUtf8(buf);
}
QVariantList GitManager::fileHistory(const QString &vaultPath, const QString &filePath) const
{
return fileHistory(vaultPath, filePath, 500);
}
QVariantList GitManager::fileHistory(const QString &vaultPath, const QString &filePath,
int limit) const
{
QVariantList out;
if (limit <= 0)
limit = 500;
limit = qMin(limit, 2000);
const QString vault = QDir::cleanPath(vaultPath);
const QString rel = relativePath(vaultPath, filePath);
if (vault.isEmpty() || rel.isEmpty())
@@ -351,6 +400,13 @@ QVariantList GitManager::fileHistory(const QString &vaultPath, const QString &fi
git_repository_free(repo);
return out;
}
// История меняется только с HEAD — при том же HEAD отдаём кэш
const QString head = headOid(repo);
const QString key = vault + QLatin1Char('|') + rel + QLatin1Char('|') + head;
if (!head.isEmpty() && key == m_histKey && m_histCache.size() >= limit) {
git_repository_free(repo);
return m_histCache.mid(0, limit);
}
git_revwalk *walk = nullptr;
if (git_revwalk_new(&walk, repo) < 0) {
git_repository_free(repo);
@@ -361,7 +417,7 @@ QVariantList GitManager::fileHistory(const QString &vaultPath, const QString &fi
git_oid oid;
int iter = 0;
const int maxIter = 500; // ограничение для скорости
const int maxIter = limit; // ограничение для скорости
while (git_revwalk_next(&oid, walk) == 0 && iter < maxIter) {
++iter;
git_commit *commit = nullptr;
@@ -444,6 +500,10 @@ QVariantList GitManager::fileHistory(const QString &vaultPath, const QString &fi
}
git_revwalk_free(walk);
git_repository_free(repo);
if (!head.isEmpty()) {
m_histKey = key;
m_histCache = out;
}
return out;
}
@@ -488,6 +548,37 @@ QString GitManager::fileContentAt(const QString &vaultPath, const QString &fileP
return result;
}
bool GitManager::fileExistedAt(const QString &vaultPath, const QString &filePath,
const QString &commitHash) const
{
const QString vault = QDir::cleanPath(vaultPath);
const QString rel = relativePath(vaultPath, filePath);
if (vault.isEmpty() || rel.isEmpty() || commitHash.isEmpty())
return false;
git_repository *repo = nullptr;
if (git_repository_open(&repo, vault.toUtf8().constData()) < 0)
return false;
git_oid oid;
bool existed = false;
if (git_oid_fromstr(&oid, commitHash.toUtf8().constData()) == 0) {
git_commit *commit = nullptr;
if (git_commit_lookup(&commit, repo, &oid) == 0) {
git_tree *tree = nullptr;
if (git_commit_tree(&tree, commit) == 0) {
git_tree_entry *entry = nullptr;
if (git_tree_entry_bypath(&entry, tree, rel.toUtf8().constData()) == 0) {
existed = true;
git_tree_entry_free(entry);
}
git_tree_free(tree);
}
git_commit_free(commit);
}
}
git_repository_free(repo);
return existed;
}
QString GitManager::restoreFile(const QString &vaultPath, const QString &filePath, const QString &commitHash)
{
const QString vault = QDir::cleanPath(vaultPath);
@@ -528,10 +619,7 @@ QString GitManager::restoreFile(const QString &vaultPath, const QString &filePat
QString GitManager::workdirContent(const QString &filePath) const
{
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly) || f.size() > 2 * 1024 * 1024)
return QString();
return QString::fromUtf8(f.readAll());
return DocumentManager::readTextFile(filePath, 2LL * 1024 * 1024);
}
QDateTime GitManager::lastModifiedTime(const QString &vaultPath, const QString &filePath) const
@@ -568,8 +656,9 @@ bool GitManager::autoCommit(const QString &vaultPath, const QString &filePath)
const QString msg = fileName.isEmpty()
? QStringLiteral("Автосохранение")
: QStringLiteral("Автосохранение: %1").arg(fileName);
// commitAll вернёт false если нет изменений — это нормально
bool ok = commitAll(vaultPath, msg);
// Точечный коммит одного файла (а не всего хранилища) — сообщение честное.
// commitFile вернёт false если нет изменений — это нормально
bool ok = commitFile(vaultPath, filePath, msg);
if (!ok && lastError() == QStringLiteral("Нет изменений для коммита")) {
setLastError(QString());
return true; // считаем успехом
+20
View File
@@ -22,6 +22,10 @@ public:
// Коммитит все изменения в хранилище (рабочее дерево). Возвращает true/false.
Q_INVOKABLE bool commitAll(const QString &path, const QString &message);
// Коммитит только один файл (добавление/изменение/удаление). Возвращает true/false.
Q_INVOKABLE bool commitFile(const QString &vaultPath, const QString &filePath,
const QString &message);
// Есть ли в хранилище хотя бы один коммит.
Q_INVOKABLE bool hasCommits(const QString &path) const;
@@ -34,10 +38,18 @@ public:
// История файла: список коммитов где файл менялся. Сортировка новые сверху.
// Каждый элемент: {hash, shortHash, message, author, time (QDateTime), timeStr, timeAgo}
Q_INVOKABLE QVariantList fileHistory(const QString &vaultPath, const QString &filePath) const;
// То же, но не больше limit свежих (limit<=0 — до 500, как раньше).
// Для кнопки «Показать ещё»: запросить limit+1, hasMore = (размер > limit).
Q_INVOKABLE QVariantList fileHistory(const QString &vaultPath, const QString &filePath,
int limit) const;
// Содержимое файла в конкретном коммите (hash). Пусто если файл не существовал.
Q_INVOKABLE QString fileContentAt(const QString &vaultPath, const QString &filePath, const QString &commitHash) const;
// Существовал ли файл в коммите (отличает «пустой файл» от «файла не было»).
Q_INVOKABLE bool fileExistedAt(const QString &vaultPath, const QString &filePath,
const QString &commitHash) const;
// Текущее содержимое файла на диске (для сравнения с версией). Пусто при ошибке/лимите 2 МБ.
Q_INVOKABLE QString workdirContent(const QString &filePath) const;
@@ -66,7 +78,15 @@ private:
void setLastError(const QString &msg);
QString relativePath(const QString &vaultPath, const QString &filePath) const;
void ensureGitignore(const QString &vaultPath);
// OID текущего HEAD ("" если нет). Для инвалидации кэша истории.
QString headOid(git_repository *repo) const;
// Создаёт коммит из уже записанного индекса (общее для commitAll/commitFile).
bool commitFromIndex(git_repository *repo, git_index *index, const QString &message);
QString m_lastError;
static bool s_gitInitialized;
// Кэш истории: ключ vault|rel|headOid. История меняется только с HEAD.
mutable QString m_histKey;
mutable QVariantList m_histCache;
};
+82 -24
View File
@@ -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;
}
+13
View File
@@ -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;
};