Упрощение git/превью: copyCodeBlock, parseBlocks на splitMdBlocks, кэш diff, пагинация
- MdParser.copyCodeBlock(id): один вызов вместо get+copy в 3 QML-местах - parseBlocks переписан на splitMdBlocks (убит regex только для ```, заодно чинит ~~~); renderInner без побочек, save/restore удалены - Кэш diff-блоков (хеши+пути, 8 шт, >1МБ не кэшируем); регистрация кодов всегда в главном потоке - История: пагинация limit+Показать ещё с сохранением выбора; willDelete; busy только на время async - GitManager: commitFile точечно (общий commitFromIndex), fileExistedAt, workdirContent через DocumentManager.readTextFile, fileGitStatus через relativePath; удалены fileDiffText/Html и 2-arg fileHistory - MarkdownView: дебаунс async-ветки 80мс + гашение протухших ответов
This commit is contained in:
+113
-102
@@ -425,6 +425,7 @@ MdParser::MdParser(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m(new Impl)
|
||||
{
|
||||
m_diffCache.setMaxCost(8);
|
||||
}
|
||||
|
||||
MdParser::~MdParser()
|
||||
@@ -445,6 +446,15 @@ void MdParser::copyToClipboard(const QString &text) const
|
||||
clip->setText(text);
|
||||
}
|
||||
|
||||
bool MdParser::copyCodeBlock(const QString &id) const
|
||||
{
|
||||
const QString code = getCodeBlock(id);
|
||||
if (code.isEmpty())
|
||||
return false;
|
||||
copyToClipboard(code);
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
class Renderer
|
||||
@@ -897,80 +907,9 @@ QVariantList MdParser::parseBlocks(const QString &markdown, const QString &vault
|
||||
m->codeCounter = 0;
|
||||
}
|
||||
|
||||
// Ищем fenced code blocks ```lang\ncode\n```
|
||||
static const QRegularExpression re(QStringLiteral("```([^`\\n]*?)\\n(.*?)\\n```"), QRegularExpression::DotMatchesEverythingOption);
|
||||
QRegularExpressionMatchIterator it = re.globalMatch(markdown);
|
||||
int lastPos = 0;
|
||||
while (it.hasNext()) {
|
||||
QRegularExpressionMatch match = it.next();
|
||||
int start = match.capturedStart();
|
||||
int end = match.capturedEnd();
|
||||
// Текст до блока
|
||||
if (start > lastPos) {
|
||||
QString htmlPart = markdown.mid(lastPos, start - lastPos);
|
||||
if (!htmlPart.trimmed().isEmpty()) {
|
||||
QString html = renderInner(htmlPart, vaultRoot, fileDir);
|
||||
if (!html.trimmed().isEmpty()) {
|
||||
QVariantMap block;
|
||||
block["type"] = "html";
|
||||
block["html"] = html;
|
||||
block["codeText"] = "";
|
||||
block["language"] = "";
|
||||
block["codeId"] = "";
|
||||
block["highlightedText"] = "";
|
||||
blocks.append(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
QString langRaw = match.captured(1).trimmed();
|
||||
QString code = match.captured(2);
|
||||
QString lang = langRaw; // нормализация внутри highlight
|
||||
QString codeId;
|
||||
if (m) {
|
||||
codeId = QStringLiteral("code_%1").arg(m->codeCounter++);
|
||||
m->codeBlocks.insert(codeId, code);
|
||||
}
|
||||
CodeHighlighter hl(m ? m->m_repo : nullptr);
|
||||
QString highlighted;
|
||||
if (m && m->m_repo) {
|
||||
highlighted = hl.highlight(code, lang, true);
|
||||
} else {
|
||||
highlighted = plainToRich(code);
|
||||
}
|
||||
QVariantMap block;
|
||||
block["type"] = "code";
|
||||
block["codeText"] = code;
|
||||
block["language"] = lang.isEmpty() ? "text" : lang;
|
||||
block["codeId"] = codeId;
|
||||
block["highlightedText"] = highlighted;
|
||||
block["html"] = "";
|
||||
blocks.append(block);
|
||||
lastPos = end;
|
||||
}
|
||||
// Остаток после последнего блока
|
||||
if (lastPos < markdown.length()) {
|
||||
QString htmlPart = markdown.mid(lastPos);
|
||||
if (!htmlPart.trimmed().isEmpty()) {
|
||||
QHash<QString, QString> savedBlocks;
|
||||
int savedCounter = 0;
|
||||
if (m) { savedBlocks = m->codeBlocks; savedCounter = m->codeCounter; }
|
||||
QString html = render(htmlPart, vaultRoot, fileDir);
|
||||
if (m) { m->codeBlocks = savedBlocks; m->codeCounter = savedCounter; }
|
||||
if (!html.trimmed().isEmpty()) {
|
||||
QVariantMap block;
|
||||
block["type"] = "html";
|
||||
block["html"] = html;
|
||||
block["codeText"] = "";
|
||||
block["language"] = "";
|
||||
block["codeId"] = "";
|
||||
block["highlightedText"] = "";
|
||||
blocks.append(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Если вообще не нашли блоки, но markdown не пустой — вернём как html
|
||||
if (blocks.isEmpty() && !markdown.trimmed().isEmpty()) {
|
||||
QString html = renderInner(markdown, vaultRoot, fileDir);
|
||||
// Единая разбивка с diff-пайплайном (splitMdBlocks): заодно чинит ~~~-блоки,
|
||||
// которые старый regex ``` не видел. renderInner состояние не трогает.
|
||||
auto appendHtml = [&](const QString &html) {
|
||||
QVariantMap block;
|
||||
block["type"] = "html";
|
||||
block["html"] = html;
|
||||
@@ -979,7 +918,44 @@ QVariantList MdParser::parseBlocks(const QString &markdown, const QString &vault
|
||||
block["codeId"] = "";
|
||||
block["highlightedText"] = "";
|
||||
blocks.append(block);
|
||||
};
|
||||
|
||||
bool any = false;
|
||||
for (const QString &part : splitMdBlocks(markdown)) {
|
||||
QString lang, code;
|
||||
if (isFenceBlock(part) && parseFenceBlock(part, &lang, &code)) {
|
||||
QString codeId;
|
||||
if (m) {
|
||||
codeId = QStringLiteral("code_%1").arg(m->codeCounter++);
|
||||
m->codeBlocks.insert(codeId, code);
|
||||
}
|
||||
CodeHighlighter hl(m ? m->m_repo : nullptr);
|
||||
QString highlighted;
|
||||
if (m && m->m_repo) {
|
||||
highlighted = hl.highlight(code, lang, true);
|
||||
} else {
|
||||
highlighted = plainToRich(code);
|
||||
}
|
||||
QVariantMap block;
|
||||
block["type"] = "code";
|
||||
block["codeText"] = code;
|
||||
block["language"] = lang.isEmpty() ? "text" : lang;
|
||||
block["codeId"] = codeId;
|
||||
block["highlightedText"] = highlighted;
|
||||
block["html"] = "";
|
||||
blocks.append(block);
|
||||
any = true;
|
||||
} else {
|
||||
const QString html = renderInner(part, vaultRoot, fileDir);
|
||||
if (!html.trimmed().isEmpty()) {
|
||||
appendHtml(html);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Если вообще не нашли блоки, но markdown не пустой — вернём как html
|
||||
if (!any && !markdown.trimmed().isEmpty())
|
||||
appendHtml(renderInner(markdown, vaultRoot, fileDir));
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@@ -997,11 +973,9 @@ static QVariantList buildDiffBlocks(MdParser::Impl *impl, const QString &oldMd,
|
||||
const QString &newMd, const QString &vaultRoot,
|
||||
const QString &fileDir)
|
||||
{
|
||||
// Чистая функция сборки: codeId остаются пустыми, регистрирует вызыватель
|
||||
// (нужно и для кэша, и для рабочего потока).
|
||||
QVariantList blocks;
|
||||
if (impl) {
|
||||
impl->codeBlocks.clear();
|
||||
impl->codeCounter = 0;
|
||||
}
|
||||
if (oldMd.isEmpty() && newMd.isEmpty())
|
||||
return blocks;
|
||||
|
||||
@@ -1018,11 +992,9 @@ static QVariantList buildDiffBlocks(MdParser::Impl *impl, const QString &oldMd,
|
||||
};
|
||||
|
||||
auto appendCodeBlock = [&](const QString &lang, const QString &code, const char *change, bool tint) {
|
||||
QString codeId;
|
||||
if (impl) {
|
||||
codeId = QStringLiteral("code_%1").arg(impl->codeCounter++);
|
||||
impl->codeBlocks.insert(codeId, code);
|
||||
}
|
||||
// codeId intentionally empty here: registers the caller (main thread) via
|
||||
// registerCodeBlocks, so the result is cacheable and thread-safe.
|
||||
const QString codeId;
|
||||
CodeHighlighter hl(impl ? impl->m_repo : nullptr);
|
||||
QString highlighted = (impl && impl->m_repo) ? hl.highlight(code, lang, true)
|
||||
: plainToRich(code);
|
||||
@@ -1084,11 +1056,7 @@ static QVariantList buildDiffBlocks(MdParser::Impl *impl, const QString &oldMd,
|
||||
}
|
||||
flushPending();
|
||||
const QString lang = !newLang.isEmpty() ? newLang : oldLang;
|
||||
QString codeId;
|
||||
if (impl) {
|
||||
codeId = QStringLiteral("code_%1").arg(impl->codeCounter++);
|
||||
impl->codeBlocks.insert(codeId, newCode);
|
||||
}
|
||||
const QString codeId; // registers the caller, see above
|
||||
QVariantMap block;
|
||||
block["type"] = "code";
|
||||
block["codeText"] = newCode;
|
||||
@@ -1141,40 +1109,83 @@ static QVariantList buildDiffBlocks(MdParser::Impl *impl, const QString &oldMd,
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// Ключ кэша diff: размеры + хеши содержимого + пути (коллизии qHash гасим размерами)
|
||||
static QString diffCacheKey(const QString &oldMd, const QString &newMd,
|
||||
const QString &vaultRoot, const QString &fileDir)
|
||||
{
|
||||
return QString::number(oldMd.size()) + QLatin1Char(':') + QString::number(newMd.size())
|
||||
+ QLatin1Char(':') + QString::number(qHash(oldMd)) + QLatin1Char(':')
|
||||
+ QString::number(qHash(newMd)) + QLatin1Char('|') + vaultRoot
|
||||
+ QLatin1Char('|') + fileDir;
|
||||
}
|
||||
|
||||
// Регистрация кодовых блоков свежими id в главном потоке (блоки из кэша/потока
|
||||
// приходят с пустыми codeId)
|
||||
static void registerCodeBlocks(QVariantList &blocks, QHash<QString, QString> &codes, int &counter)
|
||||
{
|
||||
for (auto &v : blocks) {
|
||||
QVariantMap bm = v.toMap();
|
||||
if (bm.value("type").toString() == QLatin1String("code")) {
|
||||
const QString newId = QStringLiteral("code_%1").arg(counter++);
|
||||
codes.insert(newId, bm.value("codeText").toString());
|
||||
bm["codeId"] = newId;
|
||||
v = bm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &newMd,
|
||||
const QString &vaultRoot, const QString &fileDir)
|
||||
{
|
||||
if (m) {
|
||||
m->codeBlocks.clear();
|
||||
m->codeCounter = 0;
|
||||
const QString key = diffCacheKey(oldMd, newMd, vaultRoot, fileDir);
|
||||
if (QVariantList *cached = m_diffCache.object(key)) {
|
||||
QVariantList blocks = *cached;
|
||||
if (m)
|
||||
registerCodeBlocks(blocks, m->codeBlocks, m->codeCounter);
|
||||
return blocks;
|
||||
}
|
||||
return buildDiffBlocks(m, oldMd, newMd, vaultRoot, fileDir);
|
||||
QVariantList blocks = buildDiffBlocks(m, oldMd, newMd, vaultRoot, fileDir);
|
||||
if (oldMd.size() + newMd.size() <= 1024 * 1024)
|
||||
m_diffCache.insert(key, new QVariantList(blocks)); // без id — регистрирует получатель
|
||||
if (m)
|
||||
registerCodeBlocks(blocks, m->codeBlocks, m->codeCounter);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
int MdParser::parseBlocksWithDiffAsync(const QString &oldMd, const QString &newMd,
|
||||
const QString &vaultRoot, const QString &fileDir)
|
||||
{
|
||||
const int id = ++m_asyncId;
|
||||
// Быстрый путь: уже считали — отдать следующим тиком (контракт остаётся асинхронным,
|
||||
// чтобы QML не зависел от того, откуда пришёл ответ)
|
||||
if (oldMd.size() + newMd.size() <= 1024 * 1024) {
|
||||
if (QVariantList *cached = m_diffCache.object(diffCacheKey(oldMd, newMd, vaultRoot, fileDir))) {
|
||||
QVariantList blocks = *cached;
|
||||
QMetaObject::invokeMethod(this, [this, id, blocks]() mutable {
|
||||
if (id != m_asyncId || !m)
|
||||
return;
|
||||
registerCodeBlocks(blocks, m->codeBlocks, m->codeCounter);
|
||||
emit blocksReady(id, blocks);
|
||||
}, Qt::QueuedConnection);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
QFutureWatcher<QVariantList> *watcher = new QFutureWatcher<QVariantList>(this);
|
||||
connect(watcher, &QFutureWatcher<QVariantList>::finished, this, [this, watcher, id]() {
|
||||
// Лямбде нужны копии параметров для кэширования по завершении
|
||||
const QString cOld = oldMd, cNew = newMd, cVault = vaultRoot, cDir = fileDir;
|
||||
connect(watcher, &QFutureWatcher<QVariantList>::finished, this, [this, watcher, id, cOld, cNew, cVault, cDir]() {
|
||||
QVariantList blocks = watcher->result();
|
||||
watcher->deleteLater();
|
||||
if (id != m_asyncId || !m)
|
||||
return; // устарел — пользователь уже смотрит другую версию
|
||||
if (cOld.size() + cNew.size() <= 1024 * 1024)
|
||||
m_diffCache.insert(diffCacheKey(cOld, cNew, cVault, cDir), new QVariantList(blocks));
|
||||
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;
|
||||
}
|
||||
}
|
||||
registerCodeBlocks(blocks, m->codeBlocks, m->codeCounter);
|
||||
emit blocksReady(id, blocks);
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run([oldMd, newMd, vaultRoot, fileDir]() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <QCache>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
@@ -46,6 +47,8 @@ public:
|
||||
|
||||
Q_INVOKABLE QString getCodeBlock(const QString &id) const;
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text) const;
|
||||
// Скопировать кодовый блок по id (get + copy одним вызовом). false — нечего копировать.
|
||||
Q_INVOKABLE bool copyCodeBlock(const QString &id) const;
|
||||
Q_INVOKABLE QString highlightedCode(const QString &code, const QString &language) const;
|
||||
|
||||
signals:
|
||||
@@ -62,4 +65,7 @@ private:
|
||||
|
||||
Impl *m = nullptr;
|
||||
int m_asyncId = 0;
|
||||
// Кэш diff-блоков на время открытого диалога (ключ — хеши содержимого+пути).
|
||||
// Хранятся БЕЗ регистрации кодов; регистрация — при каждой выдаче.
|
||||
QCache<QString, QVariantList> m_diffCache;
|
||||
};
|
||||
Reference in New Issue
Block a user