Упрощение 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:
@@ -157,9 +157,7 @@ Item {
|
|||||||
background: Rectangle { color: Theme.editorBg }
|
background: Rectangle { color: Theme.editorBg }
|
||||||
onLinkActivated: (href) => {
|
onLinkActivated: (href) => {
|
||||||
if (href.startsWith("flora://copycode/")) {
|
if (href.startsWith("flora://copycode/")) {
|
||||||
const id = href.substring(17)
|
if (mdParser.copyCodeBlock(href.substring(17))) { copyFeedback.visible = true; copyTimer.restart() }
|
||||||
const code = mdParser.getCodeBlock(id)
|
|
||||||
if (code.length > 0) { mdParser.copyToClipboard(code); copyFeedback.visible = true; copyTimer.restart() }
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (href.startsWith("flora://task/")) {
|
if (href.startsWith("flora://task/")) {
|
||||||
|
|||||||
@@ -658,10 +658,7 @@ Item {
|
|||||||
|
|
||||||
function handleLink(href) {
|
function handleLink(href) {
|
||||||
if (href.startsWith("flora://copycode/")) {
|
if (href.startsWith("flora://copycode/")) {
|
||||||
const id = href.substring(17)
|
if (mdParser.copyCodeBlock(href.substring(17))) {
|
||||||
const code = mdParser.getCodeBlock(id)
|
|
||||||
if (code.length > 0) {
|
|
||||||
mdParser.copyToClipboard(code)
|
|
||||||
copyFeedback.text = qsTr("Скопировано!")
|
copyFeedback.text = qsTr("Скопировано!")
|
||||||
copyFeedback.visible = true
|
copyFeedback.visible = true
|
||||||
copyTimer.restart()
|
copyTimer.restart()
|
||||||
|
|||||||
@@ -19,12 +19,21 @@ Item {
|
|||||||
signal linkActivated(string href)
|
signal linkActivated(string href)
|
||||||
signal diffApplied()
|
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() {
|
function update() {
|
||||||
if (compareMarkdown.length > 0) {
|
if (compareMarkdown.length > 0) {
|
||||||
_diffReq = mdParser.parseBlocksWithDiffAsync(markdown, compareMarkdown, vaultRoot, fileDir)
|
asyncDebounce.restart()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
_diffReq = -1 // гасим возможный async в полёте: его ответ станет чужим
|
||||||
// Триггерим пересборку блоков
|
// Триггерим пересборку блоков
|
||||||
blocksModel.clear()
|
blocksModel.clear()
|
||||||
const blocks = mdParser.parseBlocks(markdown, vaultRoot, fileDir)
|
const blocks = mdParser.parseBlocks(markdown, vaultRoot, fileDir)
|
||||||
@@ -99,15 +108,12 @@ Item {
|
|||||||
codeId: blockCodeId
|
codeId: blockCodeId
|
||||||
highlightedText: blockHighlighted
|
highlightedText: blockHighlighted
|
||||||
onCopyRequested: (id) => {
|
onCopyRequested: (id) => {
|
||||||
const txt = mdParser.getCodeBlock(id)
|
if (mdParser.copyCodeBlock(id))
|
||||||
if (txt.length > 0) {
|
|
||||||
mdParser.copyToClipboard(txt)
|
|
||||||
root.linkActivated("flora://copycode/" + id)
|
root.linkActivated("flora://copycode/" + id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -377,11 +377,6 @@ QString GitManager::headOid(git_repository *repo) const
|
|||||||
return QString::fromUtf8(buf);
|
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,
|
QVariantList GitManager::fileHistory(const QString &vaultPath, const QString &filePath,
|
||||||
int limit) const
|
int limit) const
|
||||||
{
|
{
|
||||||
@@ -591,7 +586,7 @@ QString GitManager::restoreFile(const QString &vaultPath, const QString &filePat
|
|||||||
const QString absPath = QDir(vault).filePath(rel);
|
const QString absPath = QDir(vault).filePath(rel);
|
||||||
if (content.isNull() && content.isEmpty()) {
|
if (content.isNull() && content.isEmpty()) {
|
||||||
// Проверяем был ли файл в коммите вообще
|
// Проверяем был ли файл в коммите вообще
|
||||||
QVariantList hist = fileHistory(vaultPath, filePath);
|
QVariantList hist = fileHistory(vaultPath, filePath, 500);
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (auto v : hist) if (v.toMap().value("hash").toString() == commitHash) { found = true; break; }
|
for (auto v : hist) if (v.toMap().value("hash").toString() == commitHash) { found = true; break; }
|
||||||
if (!found) return QStringLiteral("Коммит не найден");
|
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
|
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()) {
|
if (!hist.isEmpty()) {
|
||||||
QDateTime dt = hist.first().toMap().value("time").toDateTime();
|
QDateTime dt = hist.first().toMap().value("time").toDateTime();
|
||||||
if (dt.isValid()) return dt;
|
if (dt.isValid()) return dt;
|
||||||
|
|||||||
@@ -36,9 +36,8 @@ public:
|
|||||||
Q_INVOKABLE QString fileGitStatusShort(const QString &vaultPath, const QString &filePath) const;
|
Q_INVOKABLE QString fileGitStatusShort(const QString &vaultPath, const QString &filePath) const;
|
||||||
|
|
||||||
// История файла: список коммитов где файл менялся. Сортировка новые сверху.
|
// История файла: список коммитов где файл менялся. Сортировка новые сверху.
|
||||||
// Каждый элемент: {hash, shortHash, message, author, time (QDateTime), timeStr, timeAgo}
|
// Каждый элемент: {hash, shortHash, message, author, time (QDateTime), timeStr, timeAgo}.
|
||||||
Q_INVOKABLE QVariantList fileHistory(const QString &vaultPath, const QString &filePath) const;
|
// Не больше limit свежих (limit<=0 — до 500).
|
||||||
// То же, но не больше limit свежих (limit<=0 — до 500, как раньше).
|
|
||||||
// Для кнопки «Показать ещё»: запросить limit+1, hasMore = (размер > limit).
|
// Для кнопки «Показать ещё»: запросить limit+1, hasMore = (размер > limit).
|
||||||
Q_INVOKABLE QVariantList fileHistory(const QString &vaultPath, const QString &filePath,
|
Q_INVOKABLE QVariantList fileHistory(const QString &vaultPath, const QString &filePath,
|
||||||
int limit) const;
|
int limit) const;
|
||||||
|
|||||||
+89
-78
@@ -425,6 +425,7 @@ MdParser::MdParser(QObject *parent)
|
|||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m(new Impl)
|
, m(new Impl)
|
||||||
{
|
{
|
||||||
|
m_diffCache.setMaxCost(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
MdParser::~MdParser()
|
MdParser::~MdParser()
|
||||||
@@ -445,6 +446,15 @@ void MdParser::copyToClipboard(const QString &text) const
|
|||||||
clip->setText(text);
|
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 {
|
namespace {
|
||||||
|
|
||||||
class Renderer
|
class Renderer
|
||||||
@@ -897,20 +907,9 @@ QVariantList MdParser::parseBlocks(const QString &markdown, const QString &vault
|
|||||||
m->codeCounter = 0;
|
m->codeCounter = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ищем fenced code blocks ```lang\ncode\n```
|
// Единая разбивка с diff-пайплайном (splitMdBlocks): заодно чинит ~~~-блоки,
|
||||||
static const QRegularExpression re(QStringLiteral("```([^`\\n]*?)\\n(.*?)\\n```"), QRegularExpression::DotMatchesEverythingOption);
|
// которые старый regex ``` не видел. renderInner состояние не трогает.
|
||||||
QRegularExpressionMatchIterator it = re.globalMatch(markdown);
|
auto appendHtml = [&](const QString &html) {
|
||||||
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;
|
QVariantMap block;
|
||||||
block["type"] = "html";
|
block["type"] = "html";
|
||||||
block["html"] = html;
|
block["html"] = html;
|
||||||
@@ -919,12 +918,12 @@ QVariantList MdParser::parseBlocks(const QString &markdown, const QString &vault
|
|||||||
block["codeId"] = "";
|
block["codeId"] = "";
|
||||||
block["highlightedText"] = "";
|
block["highlightedText"] = "";
|
||||||
blocks.append(block);
|
blocks.append(block);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
}
|
bool any = false;
|
||||||
QString langRaw = match.captured(1).trimmed();
|
for (const QString &part : splitMdBlocks(markdown)) {
|
||||||
QString code = match.captured(2);
|
QString lang, code;
|
||||||
QString lang = langRaw; // нормализация внутри highlight
|
if (isFenceBlock(part) && parseFenceBlock(part, &lang, &code)) {
|
||||||
QString codeId;
|
QString codeId;
|
||||||
if (m) {
|
if (m) {
|
||||||
codeId = QStringLiteral("code_%1").arg(m->codeCounter++);
|
codeId = QStringLiteral("code_%1").arg(m->codeCounter++);
|
||||||
@@ -945,41 +944,18 @@ QVariantList MdParser::parseBlocks(const QString &markdown, const QString &vault
|
|||||||
block["highlightedText"] = highlighted;
|
block["highlightedText"] = highlighted;
|
||||||
block["html"] = "";
|
block["html"] = "";
|
||||||
blocks.append(block);
|
blocks.append(block);
|
||||||
lastPos = end;
|
any = true;
|
||||||
}
|
} else {
|
||||||
// Остаток после последнего блока
|
const QString html = renderInner(part, vaultRoot, fileDir);
|
||||||
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()) {
|
if (!html.trimmed().isEmpty()) {
|
||||||
QVariantMap block;
|
appendHtml(html);
|
||||||
block["type"] = "html";
|
any = true;
|
||||||
block["html"] = html;
|
|
||||||
block["codeText"] = "";
|
|
||||||
block["language"] = "";
|
|
||||||
block["codeId"] = "";
|
|
||||||
block["highlightedText"] = "";
|
|
||||||
blocks.append(block);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Если вообще не нашли блоки, но markdown не пустой — вернём как html
|
// Если вообще не нашли блоки, но markdown не пустой — вернём как html
|
||||||
if (blocks.isEmpty() && !markdown.trimmed().isEmpty()) {
|
if (!any && !markdown.trimmed().isEmpty())
|
||||||
QString html = renderInner(markdown, vaultRoot, fileDir);
|
appendHtml(renderInner(markdown, vaultRoot, fileDir));
|
||||||
QVariantMap block;
|
|
||||||
block["type"] = "html";
|
|
||||||
block["html"] = html;
|
|
||||||
block["codeText"] = "";
|
|
||||||
block["language"] = "";
|
|
||||||
block["codeId"] = "";
|
|
||||||
block["highlightedText"] = "";
|
|
||||||
blocks.append(block);
|
|
||||||
}
|
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -997,11 +973,9 @@ static QVariantList buildDiffBlocks(MdParser::Impl *impl, const QString &oldMd,
|
|||||||
const QString &newMd, const QString &vaultRoot,
|
const QString &newMd, const QString &vaultRoot,
|
||||||
const QString &fileDir)
|
const QString &fileDir)
|
||||||
{
|
{
|
||||||
|
// Чистая функция сборки: codeId остаются пустыми, регистрирует вызыватель
|
||||||
|
// (нужно и для кэша, и для рабочего потока).
|
||||||
QVariantList blocks;
|
QVariantList blocks;
|
||||||
if (impl) {
|
|
||||||
impl->codeBlocks.clear();
|
|
||||||
impl->codeCounter = 0;
|
|
||||||
}
|
|
||||||
if (oldMd.isEmpty() && newMd.isEmpty())
|
if (oldMd.isEmpty() && newMd.isEmpty())
|
||||||
return blocks;
|
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) {
|
auto appendCodeBlock = [&](const QString &lang, const QString &code, const char *change, bool tint) {
|
||||||
QString codeId;
|
// codeId intentionally empty here: registers the caller (main thread) via
|
||||||
if (impl) {
|
// registerCodeBlocks, so the result is cacheable and thread-safe.
|
||||||
codeId = QStringLiteral("code_%1").arg(impl->codeCounter++);
|
const QString codeId;
|
||||||
impl->codeBlocks.insert(codeId, code);
|
|
||||||
}
|
|
||||||
CodeHighlighter hl(impl ? impl->m_repo : nullptr);
|
CodeHighlighter hl(impl ? impl->m_repo : nullptr);
|
||||||
QString highlighted = (impl && impl->m_repo) ? hl.highlight(code, lang, true)
|
QString highlighted = (impl && impl->m_repo) ? hl.highlight(code, lang, true)
|
||||||
: plainToRich(code);
|
: plainToRich(code);
|
||||||
@@ -1084,11 +1056,7 @@ static QVariantList buildDiffBlocks(MdParser::Impl *impl, const QString &oldMd,
|
|||||||
}
|
}
|
||||||
flushPending();
|
flushPending();
|
||||||
const QString lang = !newLang.isEmpty() ? newLang : oldLang;
|
const QString lang = !newLang.isEmpty() ? newLang : oldLang;
|
||||||
QString codeId;
|
const QString codeId; // registers the caller, see above
|
||||||
if (impl) {
|
|
||||||
codeId = QStringLiteral("code_%1").arg(impl->codeCounter++);
|
|
||||||
impl->codeBlocks.insert(codeId, newCode);
|
|
||||||
}
|
|
||||||
QVariantMap block;
|
QVariantMap block;
|
||||||
block["type"] = "code";
|
block["type"] = "code";
|
||||||
block["codeText"] = newCode;
|
block["codeText"] = newCode;
|
||||||
@@ -1141,40 +1109,83 @@ static QVariantList buildDiffBlocks(MdParser::Impl *impl, const QString &oldMd,
|
|||||||
return blocks;
|
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,
|
QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &newMd,
|
||||||
const QString &vaultRoot, const QString &fileDir)
|
const QString &vaultRoot, const QString &fileDir)
|
||||||
{
|
{
|
||||||
if (m) {
|
const QString key = diffCacheKey(oldMd, newMd, vaultRoot, fileDir);
|
||||||
m->codeBlocks.clear();
|
if (QVariantList *cached = m_diffCache.object(key)) {
|
||||||
m->codeCounter = 0;
|
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,
|
int MdParser::parseBlocksWithDiffAsync(const QString &oldMd, const QString &newMd,
|
||||||
const QString &vaultRoot, const QString &fileDir)
|
const QString &vaultRoot, const QString &fileDir)
|
||||||
{
|
{
|
||||||
const int id = ++m_asyncId;
|
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);
|
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();
|
QVariantList blocks = watcher->result();
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
if (id != m_asyncId || !m)
|
if (id != m_asyncId || !m)
|
||||||
return; // устарел — пользователь уже смотрит другую версию
|
return; // устарел — пользователь уже смотрит другую версию
|
||||||
|
if (cOld.size() + cNew.size() <= 1024 * 1024)
|
||||||
|
m_diffCache.insert(diffCacheKey(cOld, cNew, cVault, cDir), new QVariantList(blocks));
|
||||||
if (m->codeBlocks.size() > 2000) {
|
if (m->codeBlocks.size() > 2000) {
|
||||||
m->codeBlocks.clear(); // защита от бесконечного роста между полными перерисовками
|
m->codeBlocks.clear(); // защита от бесконечного роста между полными перерисовками
|
||||||
m->codeCounter = 0;
|
m->codeCounter = 0;
|
||||||
}
|
}
|
||||||
// Коды регистрируем здесь, в главном потоке, свежими id
|
// Коды регистрируем здесь, в главном потоке, свежими id
|
||||||
for (auto &v : blocks) {
|
registerCodeBlocks(blocks, m->codeBlocks, m->codeCounter);
|
||||||
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);
|
emit blocksReady(id, blocks);
|
||||||
});
|
});
|
||||||
watcher->setFuture(QtConcurrent::run([oldMd, newMd, vaultRoot, fileDir]() {
|
watcher->setFuture(QtConcurrent::run([oldMd, newMd, vaultRoot, fileDir]() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QCache>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVariantList>
|
#include <QVariantList>
|
||||||
@@ -46,6 +47,8 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE QString getCodeBlock(const QString &id) const;
|
Q_INVOKABLE QString getCodeBlock(const QString &id) const;
|
||||||
Q_INVOKABLE void copyToClipboard(const QString &text) 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;
|
Q_INVOKABLE QString highlightedCode(const QString &code, const QString &language) const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
@@ -62,4 +65,7 @@ private:
|
|||||||
|
|
||||||
Impl *m = nullptr;
|
Impl *m = nullptr;
|
||||||
int m_asyncId = 0;
|
int m_asyncId = 0;
|
||||||
|
// Кэш diff-блоков на время открытого диалога (ключ — хеши содержимого+пути).
|
||||||
|
// Хранятся БЕЗ регистрации кодов; регистрация — при каждой выдаче.
|
||||||
|
QCache<QString, QVariantList> m_diffCache;
|
||||||
};
|
};
|
||||||
+14
-14
@@ -49,7 +49,7 @@ int main(int argc, char *argv[])
|
|||||||
if(!git.hasCommits(vault)) return fail("hasCommits");
|
if(!git.hasCommits(vault)) return fail("hasCommits");
|
||||||
|
|
||||||
// История после v1: 1 элемент
|
// История после 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()));
|
if(hist.size()!=1) return fail("hist size 1", QString::number(hist.size()));
|
||||||
QString hash1 = hist.first().toMap().value("hash").toString();
|
QString hash1 = hist.first().toMap().value("hash").toString();
|
||||||
QString short1 = hist.first().toMap().value("shortHash").toString();
|
QString short1 = hist.first().toMap().value("shortHash").toString();
|
||||||
@@ -73,7 +73,7 @@ int main(int argc, char *argv[])
|
|||||||
QThread::msleep(1100); // чтобы время коммита отличалось
|
QThread::msleep(1100); // чтобы время коммита отличалось
|
||||||
QFile f2(file); f2.open(QIODevice::WriteOnly | QIODevice::Truncate); f2.write("Версия 2\nСтрока A\nСтрока B\n"); f2.close();
|
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());
|
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()));
|
if(hist2.size()!=2) return fail("hist size 2", QString::number(hist2.size()));
|
||||||
// Новый сверху
|
// Новый сверху
|
||||||
QString hash2 = hist2.first().toMap().value("hash").toString();
|
QString hash2 = hist2.first().toMap().value("hash").toString();
|
||||||
@@ -94,7 +94,7 @@ int main(int argc, char *argv[])
|
|||||||
QString other = vault+"/other.md";
|
QString other = vault+"/other.md";
|
||||||
QFile of(other); of.open(QIODevice::WriteOnly); of.write("other\n"); of.close();
|
QFile of(other); of.open(QIODevice::WriteOnly); of.write("other\n"); of.close();
|
||||||
if(!git.commitAll(vault, "add other")) return fail("commit other");
|
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()));
|
if(histAfterOther.size()!=2) return fail("hist after other should still 2", QString::number(histAfterOther.size()));
|
||||||
|
|
||||||
// Восстановление назад к v1
|
// Восстановление назад к v1
|
||||||
@@ -103,7 +103,7 @@ int main(int argc, char *argv[])
|
|||||||
QFile check(file); check.open(QIODevice::ReadOnly); QString cur = QString::fromUtf8(check.readAll());
|
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);
|
if(!cur.contains("Версия 1") || cur.contains("Строка B")) return fail("after restore v1 content", cur);
|
||||||
// История после восстановления должна иметь 3 элемента (v1, v2, восстановление)
|
// История после восстановления должна иметь 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.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());
|
if(!hist3.first().toMap().value("message").toString().contains("Восстановление")) return fail("restore commit message", hist3.first().toMap().value("message").toString());
|
||||||
// lastModified должно быть сейчас (только что)
|
// lastModified должно быть сейчас (только что)
|
||||||
@@ -117,14 +117,14 @@ int main(int argc, char *argv[])
|
|||||||
if(!err.isEmpty()) return fail("restore forward to v2", err);
|
if(!err.isEmpty()) return fail("restore forward to v2", err);
|
||||||
QFile check2(file); check2.open(QIODevice::ReadOnly); QString cur2 = QString::fromUtf8(check2.readAll());
|
QFile check2(file); check2.open(QIODevice::ReadOnly); QString cur2 = QString::fromUtf8(check2.readAll());
|
||||||
if(!cur2.contains("Версия 2")) return fail("after restore forward v2", cur2);
|
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()));
|
if(hist4.size()!=4) return fail("hist after forward restore 4", QString::number(hist4.size()));
|
||||||
|
|
||||||
// Ещё одно изменение вручную вперёд
|
// Ещё одно изменение вручную вперёд
|
||||||
QThread::msleep(1100);
|
QThread::msleep(1100);
|
||||||
QFile f3(file); f3.open(QIODevice::WriteOnly | QIODevice::Truncate); f3.write("Версия 3\nНовая строка\n"); f3.close();
|
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");
|
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.size()!=5) return fail("hist 5", QString::number(hist5.size()));
|
||||||
if(hist5.first().toMap().value("message").toString()!="v3") return fail("top v3");
|
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();
|
QFile f4(file); f4.open(QIODevice::WriteOnly | QIODevice::Append); f4.write("\nauto\n"); f4.close();
|
||||||
bool autoOk = git.autoCommit(vault, file);
|
bool autoOk = git.autoCommit(vault, file);
|
||||||
if(!autoOk) return fail("autoCommit", git.lastError());
|
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.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());
|
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 режет сверху, порядок новые сверху, кэш консистентен
|
// Пагинация: limit режет сверху, порядок новые сверху, кэш консистентен
|
||||||
{
|
{
|
||||||
QVariantList full = git.fileHistory(vault, file);
|
QVariantList full = git.fileHistory(vault, file, 500);
|
||||||
QVariantList part = git.fileHistory(vault, file, 2);
|
QVariantList part = git.fileHistory(vault, file, 2);
|
||||||
if(part.size()!=2) return fail("paged size", QString::number(part.size()));
|
if(part.size()!=2) return fail("paged size", QString::number(part.size()));
|
||||||
if(part.first().toMap().value("hash") != full.first().toMap().value("hash"))
|
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.write("late\n");
|
||||||
lf.close();
|
lf.close();
|
||||||
if(!git.commitAll(vault, "late file")) return fail("commit late", git.lastError());
|
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, lateHash)) return fail("late existed");
|
||||||
if(git.fileExistedAt(vault, late, hash1)) return fail("late should not exist at v1");
|
if(git.fileExistedAt(vault, late, hash1)) return fail("late should not exist at v1");
|
||||||
if(git.fileExistedAt(vault, late, "0000000000000000000000000000000000000000"))
|
if(git.fileExistedAt(vault, late, "0000000000000000000000000000000000000000"))
|
||||||
@@ -194,17 +194,17 @@ int main(int argc, char *argv[])
|
|||||||
re.write("v-final\n");
|
re.write("v-final\n");
|
||||||
re.close();
|
re.close();
|
||||||
if(!git.commitFile(vault, file, "final A")) return fail("commitFile A", git.lastError());
|
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);
|
QFile fa(file);
|
||||||
fa.open(QIODevice::WriteOnly | QIODevice::Append); fa.write("more A\n"); fa.close();
|
fa.open(QIODevice::WriteOnly | QIODevice::Append); fa.write("more A\n"); fa.close();
|
||||||
QFile fb(vault+"/other.md");
|
QFile fb(vault+"/other.md");
|
||||||
fb.open(QIODevice::WriteOnly | QIODevice::Append); fb.write("more B\n"); fb.close();
|
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.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, file, 500).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, vault+"/other.md", 500).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).first().toMap().value("message").toString().contains("final B"))
|
||||||
return fail("commitFile message");
|
return fail("commitFile message");
|
||||||
// commitFile без изменений — false с понятной ошибкой, как commitAll
|
// commitFile без изменений — false с понятной ошибкой, как commitAll
|
||||||
if(git.commitFile(vault, file, "noop")) return fail("commitFile noop should fail");
|
if(git.commitFile(vault, file, "noop")) return fail("commitFile noop should fail");
|
||||||
|
|||||||
@@ -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-блока
|
// parseBlocksWithDiff: правка в обычном тексте — два отдельных tinted-блока
|
||||||
// (del, затем add), как стандартный diff; словного уровня нет
|
// (del, затем add), как стандартный diff; словного уровня нет
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user