diff --git a/qml/dialogs/HistoryDialog.qml b/qml/dialogs/HistoryDialog.qml
index f2361b0..7f9b7f0 100644
--- a/qml/dialogs/HistoryDialog.qml
+++ b/qml/dialogs/HistoryDialog.qml
@@ -366,8 +366,8 @@ Dialog {
Layout.fillWidth: true
Layout.margins: 6
spacing: 12
- Row { spacing: 4; Rectangle { width:12; height:12; color:"#e6ffec"; border.color:"#1a7f37"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("добавлено"); color: Theme.textMuted; font.pixelSize:10 } }
- Row { spacing: 4; Rectangle { width:12; height:12; color:"#ffebe9"; border.color:"#cf222e"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("удалено"); color: Theme.textMuted; font.pixelSize:10 } }
+ Row { spacing: 4; Rectangle { width:12; height:12; color:"#b7f5c6"; border.color:"#1a7f37"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("добавлено"); color: Theme.textMuted; font.pixelSize:10 } }
+ Row { spacing: 4; Rectangle { width:12; height:12; color:"#ffd6dd"; border.color:"#e5484d"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("удалено"); color: Theme.textMuted; font.pixelSize:10 } }
Row { visible: tabBar.currentIndex === 0; spacing: 4; Rectangle { width:12; height:12; color:"#ddf4ff"; border.color:"#0969da"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("заголовок"); color: Theme.textMuted; font.pixelSize:10 } }
Label {
text: tabBar.currentIndex === 0
diff --git a/src/git/GitManager.cpp b/src/git/GitManager.cpp
index 0e4d572..bb1ce6e 100644
--- a/src/git/GitManager.cpp
+++ b/src/git/GitManager.cpp
@@ -574,9 +574,9 @@ QString GitManager::fileDiffHtml(const QString &vaultPath, const QString &filePa
QChar first = rawLine.at(0);
QString esc = escHtml(rawLine);
if (first == '+') {
- html += QStringLiteral("
%1
").arg(esc);
+ html += QStringLiteral("%1
").arg(esc);
} else if (first == '-') {
- html += QStringLiteral("%1
").arg(esc);
+ html += QStringLiteral("%1
").arg(esc);
} else if (first == '@') {
html += QStringLiteral("%1
").arg(esc);
} else if (first == ' ') {
diff --git a/src/markdown/MdParser.cpp b/src/markdown/MdParser.cpp
index e6c98dd..b86ef28 100644
--- a/src/markdown/MdParser.cpp
+++ b/src/markdown/MdParser.cpp
@@ -208,12 +208,12 @@ QVector diffMdBlocks(const QStringList &a, const QStringList &b)
return ops;
}
-// Цветная обёртка изменённого блока (те же цвета, что в fileDiffHtml)
+// Цветная обёртка изменённого блока: насыщенные розовый/зелёный, текст читается
QString tintDiffHtml(const QString &inner, bool isDel)
{
if (isDel)
- return QStringLiteral("%1
").arg(inner);
- return QStringLiteral("%1
").arg(inner);
+ return QStringLiteral("%1
").arg(inner);
+ return QStringLiteral("%1
").arg(inner);
}
QString fileHrefFor(const QString &path)
@@ -1015,6 +1015,14 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
blocks.append(block);
};
+ // Насыщенная полупрозрачная подложка для кода: пузырь CodeBlock всегда тёмно-синий,
+ // светлый текст на ней читается, в отличие от пастели
+ auto tintCodeHtml = [&](const QString &inner, bool isDel) -> QString {
+ const QString bg = isDel ? QStringLiteral("rgba(244,63,94,0.45)")
+ : QStringLiteral("rgba(34,197,94,0.45)");
+ return QStringLiteral("%2
").arg(bg, inner);
+ };
+
auto appendCodeBlock = [&](const QString &lang, const QString &code, const char *change, bool tint) {
QString codeId;
if (m) {
@@ -1025,7 +1033,7 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
QString highlighted = (m && m->m_repo) ? hl.highlight(code, lang, true)
: plainToRich(code);
if (tint)
- highlighted = tintDiffHtml(highlighted, change[0] == 'd');
+ highlighted = tintCodeHtml(highlighted, change[0] == 'd');
QVariantMap block;
block["type"] = "code";
block["codeText"] = code;
@@ -1037,13 +1045,85 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
blocks.append(block);
};
+ // Изменённый код (del-блок сразу за add-блоком): один блок с построчной
+ // подсветкой внутри — изменения видны там, где сделаны. Копируется новое.
+ 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);
+ const QStringList oldLines = oldCode.split(QLatin1Char('\n'));
+ const QStringList newLines = newCode.split(QLatin1Char('\n'));
+ const QStringList oldFrag = oldHl.split(QStringLiteral("
"));
+ const QStringList newFrag = newHl.split(QStringLiteral("
"));
+ if (oldFrag.size() != oldLines.size() || newFrag.size() != newLines.size())
+ return false; // fallback: два отдельных блока
+ const QVector lops = diffMdBlocks(oldLines, newLines);
+ QStringList out;
+ QStringList pending;
+ auto flushPending = [&]() {
+ if (!pending.isEmpty()) {
+ out.append(pending.join(QStringLiteral("
")));
+ pending.clear();
+ }
+ };
+ int oi = 0, ni = 0;
+ for (const DiffOp &lop : lops) {
+ if (lop.kind == '=') {
+ pending.append(oldFrag[oi]);
+ ++oi; ++ni;
+ } else {
+ flushPending();
+ const bool isDel = lop.kind == '-';
+ QString frag = isDel ? oldFrag[oi++] : newFrag[ni++];
+ if (frag.isEmpty())
+ frag = QStringLiteral("
");
+ const QString mark = isDel
+ ? QStringLiteral("\u2212 ")
+ : QStringLiteral("+ ");
+ const QString bg = isDel ? QStringLiteral("rgba(244,63,94,0.45)")
+ : QStringLiteral("rgba(34,197,94,0.45)");
+ out.append(QStringLiteral("%2%3
").arg(bg, mark, frag));
+ }
+ }
+ flushPending();
+ const QString lang = !newLang.isEmpty() ? newLang : oldLang;
+ QString codeId;
+ if (m) {
+ codeId = QStringLiteral("code_%1").arg(m->codeCounter++);
+ m->codeBlocks.insert(codeId, newCode);
+ }
+ QVariantMap block;
+ block["type"] = "code";
+ block["codeText"] = newCode;
+ block["language"] = lang.isEmpty() ? "text" : lang;
+ block["codeId"] = codeId;
+ block["highlightedText"] = out.join(QString());
+ block["html"] = "";
+ block["change"] = "mod";
+ blocks.append(block);
+ return true;
+ };
+
const QVector ops = diffMdBlocks(splitMdBlocks(oldMd), splitMdBlocks(newMd));
- for (const DiffOp &op : ops) {
+ for (int k = 0; k < ops.size(); ++k) {
+ const DiffOp &op = ops[k];
const bool changed = op.kind != '=';
const bool isDel = op.kind == '-';
const char *change = op.kind == '=' ? "same" : (isDel ? "del" : "add");
QString lang, code;
- if (isFenceBlock(op.text) && parseFenceBlock(op.text, &lang, &code)) {
+ const bool isCode = isFenceBlock(op.text) && parseFenceBlock(op.text, &lang, &code);
+ if (isCode && isDel && k + 1 < ops.size() && ops[k + 1].kind == '+') {
+ QString nlang, ncode;
+ if (isFenceBlock(ops[k + 1].text) && parseFenceBlock(ops[k + 1].text, &nlang, &ncode)
+ && appendMergedCode(lang, code, nlang, ncode)) {
+ ++k;
+ continue;
+ }
+ }
+ if (isCode) {
appendCodeBlock(lang, code, change, changed);
} else {
QString html = renderKeep(op.text);
diff --git a/tests/git_history_test.cpp b/tests/git_history_test.cpp
index 8167328..2dae463 100644
--- a/tests/git_history_test.cpp
+++ b/tests/git_history_test.cpp
@@ -100,7 +100,7 @@ int main(int argc, char *argv[])
if(!diff.contains("+") || !diff.contains("-")) return fail("diff should have + -", diff);
if(!diff.contains("Версия 1") || !diff.contains("Версия 2")) return fail("diff contains versions", diff);
QString diffHtml = git.fileDiffHtml(vault, file, hash1);
- if(!diffHtml.contains("background:#e6ffec") || !diffHtml.contains("background:#ffebe9")) return fail("diffHtml colors", diffHtml.left(500));
+ if(!diffHtml.contains("background:#b7f5c6") || !diffHtml.contains("background:#ffd6dd")) return fail("diffHtml colors", diffHtml.left(500));
// forward/backward: добавляем ещё файл other.md, коммитим, история note.md не должна вырасти
QString other = vault+"/other.md";
diff --git a/tests/mdparser_test.cpp b/tests/mdparser_test.cpp
index b3da56d..ce29778 100644
--- a/tests/mdparser_test.cpp
+++ b/tests/mdparser_test.cpp
@@ -155,13 +155,13 @@ int main(int argc, char *argv[])
if (changeOf(0) != "same") return fail("diff[0] должен быть same", changeOf(0));
if (d[0].toMap().value("type").toString() != "html") return fail("diff[0] тип");
if (changeOf(1) != "del") return fail("diff[1] должен быть del", changeOf(1));
- if (!d[1].toMap().value("html").toString().contains("ffebe9")) return fail("diff[1] без красной подсветки");
+ if (!d[1].toMap().value("html").toString().contains("ffd6dd")) return fail("diff[1] без розовой подсветки");
if (changeOf(2) != "del" || d[2].toMap().value("type").toString() != "code")
return fail("diff[2] код del", changeOf(2));
- if (!d[2].toMap().value("highlightedText").toString().contains("ffebe9"))
- return fail("diff[2] код без красной подсветки");
+ if (!d[2].toMap().value("highlightedText").toString().contains("244,63,94"))
+ return fail("diff[2] код без розовой подсветки");
if (changeOf(3) != "add") return fail("diff[3] должен быть add", changeOf(3));
- if (!d[3].toMap().value("html").toString().contains("e6ffec")) return fail("diff[3] без зелёной подсветки");
+ if (!d[3].toMap().value("html").toString().contains("b7f5c6")) return fail("diff[3] без зелёной подсветки");
if (changeOf(4) != "add" || d[4].toMap().value("type").toString() != "code")
return fail("diff[4] код add", changeOf(4));
// кодовые блоки сохранили id для копирования
@@ -177,12 +177,34 @@ int main(int argc, char *argv[])
for (const auto &v : d) {
if (v.toMap().value("change").toString() != "same")
return fail("identical должен быть same");
- if (v.toMap().value("html").toString().contains("ffebe9")
- || v.toMap().value("html").toString().contains("e6ffec"))
+ if (v.toMap().value("html").toString().contains("ffd6dd")
+ || v.toMap().value("html").toString().contains("b7f5c6"))
return fail("identical с подсветкой");
}
}
+ // parseBlocksWithDiff: правка внутри кода — ОДИН блок с построчным diff внутри
+ {
+ const QString oldMd = "# T\n\n```cpp\nint a = 1;\nint b = 2;\n```\n";
+ const QString newMd = "# T\n\n```cpp\nint a = 1;\nint b = 3;\n```\n";
+ auto d = p.parseBlocksWithDiff(oldMd, newMd, vault, fileDir);
+ if (d.size() != 2) return fail("merge размер", QString::number(d.size()));
+ if (d[0].toMap().value("change").toString() != "same") return fail("merge[0] same");
+ if (d[1].toMap().value("type").toString() != "code"
+ || d[1].toMap().value("change").toString() != "mod")
+ return fail("merge[1] должен быть code/mod", d[1].toMap().value("change").toString());
+ const QString hl = d[1].toMap().value("highlightedText").toString();
+ if (!hl.contains("244,63,94")) return fail("merge без розовой строки");
+ if (!hl.contains("34,197,94")) return fail("merge без зелёной строки");
+ // маркеры −/+ у изменённых строк (текст режется спанами подсветки, ищем маркеры)
+ if (!hl.contains(QChar(0x2212))) return fail("merge без маркера −");
+ if (!hl.contains("+ ")) return fail("merge без маркера +");
+ // копируется новое содержимое
+ const QString cid = d[1].toMap().value("codeId").toString();
+ if (cid.isEmpty()) return fail("merge codeId пуст");
+ if (!p.getCodeBlock(cid).contains("int b = 3;")) return fail("merge copy даёт не новое", p.getCodeBlock(cid));
+ }
+
std::printf("MDPARSER TEST OK\n");
return 0;
}
\ No newline at end of file