diff --git a/qml/panels/EditorPane.qml b/qml/panels/EditorPane.qml index 6fa2e35..3798326 100644 --- a/qml/panels/EditorPane.qml +++ b/qml/panels/EditorPane.qml @@ -157,9 +157,7 @@ Item { background: Rectangle { color: Theme.editorBg } onLinkActivated: (href) => { if (href.startsWith("flora://copycode/")) { - const id = href.substring(17) - const code = mdParser.getCodeBlock(id) - if (code.length > 0) { mdParser.copyToClipboard(code); copyFeedback.visible = true; copyTimer.restart() } + if (mdParser.copyCodeBlock(href.substring(17))) { copyFeedback.visible = true; copyTimer.restart() } return } if (href.startsWith("flora://task/")) { diff --git a/qml/panels/EditorPanel.qml b/qml/panels/EditorPanel.qml index ffa605b..bebe576 100644 --- a/qml/panels/EditorPanel.qml +++ b/qml/panels/EditorPanel.qml @@ -658,10 +658,7 @@ Item { function handleLink(href) { if (href.startsWith("flora://copycode/")) { - const id = href.substring(17) - const code = mdParser.getCodeBlock(id) - if (code.length > 0) { - mdParser.copyToClipboard(code) + if (mdParser.copyCodeBlock(href.substring(17))) { copyFeedback.text = qsTr("Скопировано!") copyFeedback.visible = true copyTimer.restart() diff --git a/qml/panels/MarkdownView.qml b/qml/panels/MarkdownView.qml index 0dd47ef..0efb553 100644 --- a/qml/panels/MarkdownView.qml +++ b/qml/panels/MarkdownView.qml @@ -19,12 +19,21 @@ Item { signal linkActivated(string href) signal diffApplied() - // Для обновления + // Для обновления. Async-ветка идёт через дебаунс: updateDiff() в диалоге меняет + // markdown и compareMarkdown по очереди, без дебаунса уходили бы два запроса + // (первый заведомо протухает и отбрасывается по _diffReq). + Timer { id: asyncDebounce; interval: 80; onTriggered: root._requestDiff() } + function _requestDiff() { + if (compareMarkdown.length === 0) + return // пока таймер ждал, сравнение уже сняли + _diffReq = mdParser.parseBlocksWithDiffAsync(markdown, compareMarkdown, vaultRoot, fileDir) + } function update() { if (compareMarkdown.length > 0) { - _diffReq = mdParser.parseBlocksWithDiffAsync(markdown, compareMarkdown, vaultRoot, fileDir) + asyncDebounce.restart() return } + _diffReq = -1 // гасим возможный async в полёте: его ответ станет чужим // Триггерим пересборку блоков blocksModel.clear() const blocks = mdParser.parseBlocks(markdown, vaultRoot, fileDir) @@ -99,11 +108,8 @@ Item { codeId: blockCodeId highlightedText: blockHighlighted onCopyRequested: (id) => { - const txt = mdParser.getCodeBlock(id) - if (txt.length > 0) { - mdParser.copyToClipboard(txt) + if (mdParser.copyCodeBlock(id)) root.linkActivated("flora://copycode/" + id) - } } } } diff --git a/src/git/GitManager.cpp b/src/git/GitManager.cpp index ba65493..6fa1312 100644 --- a/src/git/GitManager.cpp +++ b/src/git/GitManager.cpp @@ -377,11 +377,6 @@ QString GitManager::headOid(git_repository *repo) const 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 { @@ -591,7 +586,7 @@ QString GitManager::restoreFile(const QString &vaultPath, const QString &filePat const QString absPath = QDir(vault).filePath(rel); if (content.isNull() && content.isEmpty()) { // Проверяем был ли файл в коммите вообще - QVariantList hist = fileHistory(vaultPath, filePath); + QVariantList hist = fileHistory(vaultPath, filePath, 500); bool found = false; for (auto v : hist) if (v.toMap().value("hash").toString() == commitHash) { found = true; break; } if (!found) return QStringLiteral("Коммит не найден"); @@ -624,7 +619,7 @@ QString GitManager::workdirContent(const QString &filePath) const QDateTime GitManager::lastModifiedTime(const QString &vaultPath, const QString &filePath) const { - const QVariantList hist = fileHistory(vaultPath, filePath); + const QVariantList hist = fileHistory(vaultPath, filePath, 500); if (!hist.isEmpty()) { QDateTime dt = hist.first().toMap().value("time").toDateTime(); if (dt.isValid()) return dt; diff --git a/src/git/GitManager.h b/src/git/GitManager.h index 4cd7939..76276eb 100644 --- a/src/git/GitManager.h +++ b/src/git/GitManager.h @@ -36,9 +36,8 @@ public: Q_INVOKABLE QString fileGitStatusShort(const QString &vaultPath, const QString &filePath) const; // История файла: список коммитов где файл менялся. Сортировка новые сверху. - // Каждый элемент: {hash, shortHash, message, author, time (QDateTime), timeStr, timeAgo} - Q_INVOKABLE QVariantList fileHistory(const QString &vaultPath, const QString &filePath) const; - // То же, но не больше limit свежих (limit<=0 — до 500, как раньше). + // Каждый элемент: {hash, shortHash, message, author, time (QDateTime), timeStr, timeAgo}. + // Не больше limit свежих (limit<=0 — до 500). // Для кнопки «Показать ещё»: запросить limit+1, hasMore = (размер > limit). Q_INVOKABLE QVariantList fileHistory(const QString &vaultPath, const QString &filePath, int limit) const; diff --git a/src/markdown/MdParser.cpp b/src/markdown/MdParser.cpp index 0d0f329..ca085d2 100644 --- a/src/markdown/MdParser.cpp +++ b/src/markdown/MdParser.cpp @@ -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 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 &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 *watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, [this, watcher, id]() { + // Лямбде нужны копии параметров для кэширования по завершении + const QString cOld = oldMd, cNew = newMd, cVault = vaultRoot, cDir = fileDir; + connect(watcher, &QFutureWatcher::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]() { diff --git a/src/markdown/MdParser.h b/src/markdown/MdParser.h index 4a4f855..1b59cce 100644 --- a/src/markdown/MdParser.h +++ b/src/markdown/MdParser.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -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 m_diffCache; }; \ No newline at end of file diff --git a/tests/git_history_test.cpp b/tests/git_history_test.cpp index 25de507..11e89fd 100644 --- a/tests/git_history_test.cpp +++ b/tests/git_history_test.cpp @@ -49,7 +49,7 @@ int main(int argc, char *argv[]) if(!git.hasCommits(vault)) return fail("hasCommits"); // История после v1: 1 элемент - QVariantList hist = git.fileHistory(vault, file); + QVariantList hist = git.fileHistory(vault, file, 500); if(hist.size()!=1) return fail("hist size 1", QString::number(hist.size())); QString hash1 = hist.first().toMap().value("hash").toString(); QString short1 = hist.first().toMap().value("shortHash").toString(); @@ -73,7 +73,7 @@ int main(int argc, char *argv[]) QThread::msleep(1100); // чтобы время коммита отличалось QFile f2(file); f2.open(QIODevice::WriteOnly | QIODevice::Truncate); f2.write("Версия 2\nСтрока A\nСтрока B\n"); f2.close(); if(!git.commitAll(vault, "v2 - добавили строку B")) return fail("commit v2", git.lastError()); - QVariantList hist2 = git.fileHistory(vault, file); + QVariantList hist2 = git.fileHistory(vault, file, 500); if(hist2.size()!=2) return fail("hist size 2", QString::number(hist2.size())); // Новый сверху QString hash2 = hist2.first().toMap().value("hash").toString(); @@ -94,7 +94,7 @@ int main(int argc, char *argv[]) QString other = vault+"/other.md"; QFile of(other); of.open(QIODevice::WriteOnly); of.write("other\n"); of.close(); if(!git.commitAll(vault, "add other")) return fail("commit other"); - QVariantList histAfterOther = git.fileHistory(vault, file); + QVariantList histAfterOther = git.fileHistory(vault, file, 500); if(histAfterOther.size()!=2) return fail("hist after other should still 2", QString::number(histAfterOther.size())); // Восстановление назад к v1 @@ -103,7 +103,7 @@ int main(int argc, char *argv[]) QFile check(file); check.open(QIODevice::ReadOnly); QString cur = QString::fromUtf8(check.readAll()); if(!cur.contains("Версия 1") || cur.contains("Строка B")) return fail("after restore v1 content", cur); // История после восстановления должна иметь 3 элемента (v1, v2, восстановление) - QVariantList hist3 = git.fileHistory(vault, file); + QVariantList hist3 = git.fileHistory(vault, file, 500); if(hist3.size()!=3) return fail("hist after restore size 3", QString::number(hist3.size())); if(!hist3.first().toMap().value("message").toString().contains("Восстановление")) return fail("restore commit message", hist3.first().toMap().value("message").toString()); // lastModified должно быть сейчас (только что) @@ -117,14 +117,14 @@ int main(int argc, char *argv[]) if(!err.isEmpty()) return fail("restore forward to v2", err); QFile check2(file); check2.open(QIODevice::ReadOnly); QString cur2 = QString::fromUtf8(check2.readAll()); if(!cur2.contains("Версия 2")) return fail("after restore forward v2", cur2); - QVariantList hist4 = git.fileHistory(vault, file); + QVariantList hist4 = git.fileHistory(vault, file, 500); if(hist4.size()!=4) return fail("hist after forward restore 4", QString::number(hist4.size())); // Ещё одно изменение вручную вперёд QThread::msleep(1100); QFile f3(file); f3.open(QIODevice::WriteOnly | QIODevice::Truncate); f3.write("Версия 3\nНовая строка\n"); f3.close(); if(!git.commitAll(vault, "v3")) return fail("commit v3"); - QVariantList hist5 = git.fileHistory(vault, file); + QVariantList hist5 = git.fileHistory(vault, file, 500); if(hist5.size()!=5) return fail("hist 5", QString::number(hist5.size())); if(hist5.first().toMap().value("message").toString()!="v3") return fail("top v3"); @@ -146,7 +146,7 @@ int main(int argc, char *argv[]) QFile f4(file); f4.open(QIODevice::WriteOnly | QIODevice::Append); f4.write("\nauto\n"); f4.close(); bool autoOk = git.autoCommit(vault, file); if(!autoOk) return fail("autoCommit", git.lastError()); - QVariantList histAuto = git.fileHistory(vault, file); + QVariantList histAuto = git.fileHistory(vault, file, 500); if(histAuto.size()!=6) return fail("hist after auto 6", QString::number(histAuto.size())); if(!histAuto.first().toMap().value("message").toString().contains("Автосохранение")) return fail("auto message", histAuto.first().toMap().value("message").toString()); @@ -156,7 +156,7 @@ int main(int argc, char *argv[]) // Пагинация: limit режет сверху, порядок новые сверху, кэш консистентен { - QVariantList full = git.fileHistory(vault, file); + QVariantList full = git.fileHistory(vault, file, 500); QVariantList part = git.fileHistory(vault, file, 2); if(part.size()!=2) return fail("paged size", QString::number(part.size())); if(part.first().toMap().value("hash") != full.first().toMap().value("hash")) @@ -180,7 +180,7 @@ int main(int argc, char *argv[]) lf.write("late\n"); lf.close(); if(!git.commitAll(vault, "late file")) return fail("commit late", git.lastError()); - QString lateHash = git.fileHistory(vault, late).first().toMap().value("hash").toString(); + QString lateHash = git.fileHistory(vault, late, 500).first().toMap().value("hash").toString(); if(!git.fileExistedAt(vault, late, lateHash)) return fail("late existed"); if(git.fileExistedAt(vault, late, hash1)) return fail("late should not exist at v1"); if(git.fileExistedAt(vault, late, "0000000000000000000000000000000000000000")) @@ -194,17 +194,17 @@ int main(int argc, char *argv[]) re.write("v-final\n"); re.close(); if(!git.commitFile(vault, file, "final A")) return fail("commitFile A", git.lastError()); - const int histA = git.fileHistory(vault, file).size(); + const int histA = git.fileHistory(vault, file, 500).size(); // правим оба файла QFile fa(file); fa.open(QIODevice::WriteOnly | QIODevice::Append); fa.write("more A\n"); fa.close(); QFile fb(vault+"/other.md"); fb.open(QIODevice::WriteOnly | QIODevice::Append); fb.write("more B\n"); fb.close(); - const int histBBefore = git.fileHistory(vault, vault+"/other.md").size(); + const int histBBefore = git.fileHistory(vault, vault+"/other.md", 500).size(); if(!git.commitFile(vault, file, "final B")) return fail("commitFile B", git.lastError()); - if(git.fileHistory(vault, file).size() != histA + 1) return fail("commitFile history A"); - if(git.fileHistory(vault, vault+"/other.md").size() != histBBefore) return fail("commitFile затронул B"); - if(!git.fileHistory(vault, file).first().toMap().value("message").toString().contains("final B")) + if(git.fileHistory(vault, file, 500).size() != histA + 1) return fail("commitFile history A"); + if(git.fileHistory(vault, vault+"/other.md", 500).size() != histBBefore) return fail("commitFile затронул B"); + if(!git.fileHistory(vault, file, 500).first().toMap().value("message").toString().contains("final B")) return fail("commitFile message"); // commitFile без изменений — false с понятной ошибкой, как commitAll if(git.commitFile(vault, file, "noop")) return fail("commitFile noop should fail"); diff --git a/tests/mdparser_test.cpp b/tests/mdparser_test.cpp index 0e11067..9a42797 100644 --- a/tests/mdparser_test.cpp +++ b/tests/mdparser_test.cpp @@ -186,6 +186,15 @@ int main(int argc, char *argv[]) } } + // ~~~-заборы тоже разбираются на code-блоки (единая разбивка splitMdBlocks) + { + auto b = p.parseBlocks("~~~\ncode\n~~~\n", vault, fileDir); + if (b.size() != 1 || b[0].toMap().value("type").toString() != "code") + return fail("tilde fence"); + if (!b[0].toMap().value("codeText").toString().contains("code")) + return fail("tilde code"); + } + // parseBlocksWithDiff: правка в обычном тексте — два отдельных tinted-блока // (del, затем add), как стандартный diff; словного уровня нет {