Handle Windows line endings and verify empty code blocks
This commit is contained in:
1 parent
77186c4392
commit
c19750a800
4 files changed
+27
-16
No files matched your search
@@ -15,7 +15,7 @@ struct ChatTextSelection {
|
||||
QPersistentModelIndex index; // Строка начала drag, для которой delegate вычисляет диапазон текста.
|
||||
QPoint clickPos; // Точка press относительно visualRect(index).topLeft().
|
||||
QPoint dragPos; // Текущая/последняя точка drag в той же системе координат.
|
||||
int startChar = -1; // UTF-16 начало [startChar,endChar); -1 — delegate ещё не вычислил границы.
|
||||
int startChar = -1; // UTF-16 начало в видимом тексте [startChar,endChar); -1 — delegate ещё не вычислил границы.
|
||||
int endChar = -1; // UTF-16 конец без включения; недопустимый/пустой диапазон при Copy даёт весь текст строки.
|
||||
bool active = false; // Есть drag-выделение, в том числе после release; не равно isSelecting().
|
||||
};
|
||||
@@ -64,7 +64,7 @@ protected:
|
||||
private:
|
||||
/* Прокрутить историю по клавише без модификаторов; true, если событие принято. Фокус сохраняется. */
|
||||
bool scrollWithKey(QKeyEvent *event);
|
||||
/* Одна строка: выбранный UTF-16 фрагмент либо весь MsgTextRole; несколько — [time] author: text по возрастанию row.
|
||||
/* Одна строка: выбранный UTF-16 фрагмент либо весь видимый текст; несколько — [time] author: text по возрастанию row.
|
||||
* Пишет только QClipboard::Clipboard, без selection clipboard; без собственного выделения ничего не меняет. */
|
||||
void copySelection();
|
||||
void updateScrollDownBtn(); // Видимость по historical/atBottom; выше низа позиционирует кнопку справа внизу viewport.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "message_text.h"
|
||||
#include "messagedelegate.h"
|
||||
#include "debug_ui.h"
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <QCache>
|
||||
#include <QFontDatabase>
|
||||
#include <QFontMetricsF>
|
||||
@@ -13,11 +15,11 @@
|
||||
#include <KSyntaxHighlighting/Theme>
|
||||
|
||||
bool MessageText::isCode(int position) const {
|
||||
for (const auto& block : code) {
|
||||
if (position < block.start) break;
|
||||
if (position <= block.start + block.length) return true;
|
||||
}
|
||||
return false;
|
||||
auto next = std::upper_bound(code.cbegin(), code.cend(), position,
|
||||
[](int offset, const MessageCodeBlock& block) { return offset < block.start; });
|
||||
if (next == code.cbegin()) return false;
|
||||
const auto& block = *--next;
|
||||
return position <= block.start + block.length;
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -94,7 +96,7 @@ public:
|
||||
for (offset = block.start; offset < end;) {
|
||||
int newline = message.text.indexOf('\n', offset);
|
||||
if (newline < 0 || newline > end) newline = end;
|
||||
state = highlightLine(QStringView(message.text).mid(offset, newline - offset), state);
|
||||
state = highlightLine(message.text.mid(offset, newline - offset), state);
|
||||
offset = newline + 1;
|
||||
}
|
||||
}
|
||||
@@ -114,7 +116,8 @@ protected:
|
||||
|
||||
MessageText parseMessageText(const QString& source) {
|
||||
MessageText result;
|
||||
const auto lines = source.split('\n');
|
||||
QString normalized = source; normalized.replace("\r\n", "\n");
|
||||
const auto lines = normalized.split('\n');
|
||||
static const QRegularExpression fence(QStringLiteral("^```([A-Za-z0-9_+#.-]{0,32})$"));
|
||||
QVector<int> closing(lines.size(), -1);
|
||||
int next = -1;
|
||||
|
||||
@@ -30,6 +30,7 @@ private slots:
|
||||
}
|
||||
void formattedMessageSelection() {
|
||||
QFETCH(bool, outgoing); QFETCH(int, width); QFETCH(bool, dark);
|
||||
QScopedValueRollback<debug_level_t> logging(g_debug_config.category_levels[DEBUG_CATEGORY_GENERAL], DEBUG_LEVEL_DEBUG);
|
||||
const QString source = "**Bold** *italic* ~~gone~~\n```cpp\nint x = 42;\n// https://example.org\n```\nhttps://example.com";
|
||||
const QString visible = "Bold italic gone\nint x = 42;\n// https://example.org\nhttps://example.com";
|
||||
QStandardItemModel model;
|
||||
@@ -56,16 +57,17 @@ private slots:
|
||||
QImage image(view.viewport()->size(), QImage::Format_ARGB32); image.fill(Qt::white);
|
||||
{ QPainter painter(&image); delegate.paint(&painter, option, index); }
|
||||
// Начало берём с реально нарисованной первой строки, конец — с последней кликабельной ссылки.
|
||||
QPoint start, end; int firstY = -1;
|
||||
for (int y = option.rect.top(); y <= option.rect.bottom() && firstY < 0; ++y) {
|
||||
QPoint end; QRect firstLine;
|
||||
for (int y = option.rect.top(); y <= option.rect.bottom(); ++y) {
|
||||
QRect ink;
|
||||
for (int x = 0; x < image.width(); ++x) {
|
||||
const auto color = image.pixelColor(x, y);
|
||||
if (color.red() > 150 && color.blue() > 150 && color.green() < 60) {
|
||||
firstY = y; start = QPoint(x - 3, y + 2); break;
|
||||
}
|
||||
if (color.red() > 150 && color.blue() > 150 && color.green() < 60) ink |= QRect(x, y, 1, 1);
|
||||
}
|
||||
if (ink.isEmpty() && !firstLine.isEmpty()) break;
|
||||
firstLine |= ink;
|
||||
}
|
||||
QVERIFY(firstY >= 0);
|
||||
QVERIFY(!firstLine.isEmpty()); const QPoint start(firstLine.left() - 3, firstLine.center().y());
|
||||
QSet<QString> links;
|
||||
for (int y = option.rect.top(); y <= option.rect.bottom(); y += 5) {
|
||||
for (int x = 5; x < image.width(); x += 12) {
|
||||
@@ -87,7 +89,7 @@ private slots:
|
||||
QCOMPARE(QApplication::clipboard()->text(), visible); QCOMPARE(activated.count(), 0);
|
||||
QCOMPARE(index.data(MsgTextRole).toString(), source);
|
||||
}
|
||||
if (outgoing && width == 700 && dark) image.save("/tmp/chat-markup-selection.png");
|
||||
if (outgoing && width == 700 && dark) image.save(QDir::tempPath() + "/chat-markup-selection.png");
|
||||
}
|
||||
|
||||
void multilineSelectionFollowsPaintedText_data() {
|
||||
|
||||
@@ -24,6 +24,7 @@ private slots:
|
||||
QTest::newRow("code") << "before\n```cpp\n int x;\n\t/* **literal** */\n\n```\nafter"
|
||||
<< "before\n int x;\n\t/* **literal** */\n\nafter" << 0 << 1;
|
||||
QTest::newRow("empty-code") << "```\n```" << "" << 0 << 1;
|
||||
QTest::newRow("windows-newlines") << "```cpp\r\nint x;\r\n```" << "int x;" << 0 << 1;
|
||||
QTest::newRow("blank-code") << "```\n\n\n```" << "\n" << 0 << 1;
|
||||
QTest::newRow("two-blocks") << "```c\nint x;\n```\n**bold**\n```python\npass\n```"
|
||||
<< "int x;\nbold\npass" << 1 << 2;
|
||||
@@ -74,6 +75,11 @@ private slots:
|
||||
const auto unknown = formattedMessageText(index); QVERIFY(unknown); QVERIFY(unknown->lightSyntax.isEmpty());
|
||||
model.setData(index, "```\nint x;\n```", MsgTextRole);
|
||||
const auto plainCode = formattedMessageText(index); QVERIFY(plainCode); QVERIFY(plainCode->lightSyntax.isEmpty());
|
||||
model.setData(index, "```\n```", MsgTextRole);
|
||||
const auto emptyCode = formattedMessageText(index); QVERIFY(emptyCode);
|
||||
QTextLayout emptyLayout; emptyLayout.setFont(QApplication::font());
|
||||
QVERIFY(layoutFormattedMessage(emptyLayout, *emptyCode, 200, false) > 0);
|
||||
QCOMPARE(emptyLayout.lineCount(), 1); QVERIFY(emptyCode->isCode(emptyLayout.lineAt(0).textStart()));
|
||||
model.setData(index, "**invitation**", MsgTextRole); model.setData(index, "utun://abc", MsgInviteLinkRole);
|
||||
QVERIFY(!formattedMessageText(index));
|
||||
model.setData(index, QString(), MsgInviteLinkRole); model.setData(index, "poll", MsgContentTypeRole);
|
||||
|
||||
Reference in new issue
Block a user