From dca6e10beb3f50c3dc49174abdeb8637d0f27753 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 7 Jun 2018 22:00:46 +0300 Subject: [PATCH] Fix markdown apply to text with emoji. --- .../chat_helpers/message_field.cpp | 10 +- .../SourceFiles/ui/widgets/input_fields.cpp | 121 +++++++++++------- .../SourceFiles/ui/widgets/input_fields.h | 10 +- 3 files changed, 91 insertions(+), 50 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/message_field.cpp b/Telegram/SourceFiles/chat_helpers/message_field.cpp index 4da871d3f..77fc6908d 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.cpp +++ b/Telegram/SourceFiles/chat_helpers/message_field.cpp @@ -609,19 +609,21 @@ void MessageLinksParser::parse() { const auto markdownTagsEnd = markdownTags.end(); const auto markdownTagsAllow = [&](int from, int length) { while (markdownTag != markdownTagsEnd - && (markdownTag->start + markdownTag->length <= from + && (markdownTag->adjustedStart + + markdownTag->adjustedLength <= from || !markdownTag->closed)) { ++markdownTag; continue; } if (markdownTag == markdownTagsEnd - || markdownTag->start >= from + length) { + || markdownTag->adjustedStart >= from + length) { return true; } // Ignore http-links that are completely inside some tags. // This will allow sending http://test.com/__test__/test correctly. - return (markdownTag->start > from - || markdownTag->start + markdownTag->length < from + length); + return (markdownTag->adjustedStart > from) + || (markdownTag->adjustedStart + + markdownTag->adjustedLength < from + length); }; const auto len = text.size(); diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp index 1399be385..ac194db87 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp +++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp @@ -272,12 +272,22 @@ public: , _items(_expressions.size()) { } - void feed(const QString &text, const QString &textTag) { + // Here we use the fact that text either contains only emoji + // { adjustedTextLength = text.size() * (emojiLength - 1) } + // or contains no emoji at all and can have tag edges in the middle + // { adjustedTextLength = 0 }. + // + // Otherwise we would have to pass emoji positions inside text. + void feed( + const QString &text, + int adjustedTextLength, + const QString &textTag) { if (!_tags) { return; } const auto guard = gsl::finally([&] { - _currentLength += text.size(); + _currentInternalLength += text.size(); + _currentAdjustedLength += adjustedTextLength; }); if (!textTag.isEmpty()) { finishTags(); @@ -290,7 +300,7 @@ public: while (true) { for (; tryFinishTag != _currentFreeTag; ++tryFinishTag) { auto &tag = (*_tags)[tryFinishTag]; - if (tag.length >= 0) { + if (tag.internalLength >= 0) { continue; } @@ -298,8 +308,12 @@ public: Assert(i != end(_tagIndices)); const auto tagIndex = i->second; - _items[tagIndex].applyOffset( - tag.start + tag.tag.size() + 1 - _currentLength); + const auto atLeastOffset = + tag.internalStart + + tag.tag.size() + + 1 + - _currentInternalLength; + _items[tagIndex].applyOffset(atLeastOffset); fillItem( tagIndex, @@ -311,10 +325,7 @@ public: const auto position = matchPosition(tagIndex, Edge::Close); if (position < kInvalidPosition) { const auto till = position + tag.tag.size(); - finishTag( - tryFinishTag, - _currentLength + till, - true); + finishTag(tryFinishTag, till, true); _items[tagIndex].applyOffset(till); } } @@ -325,9 +336,7 @@ public: if (min < 0) { return; } - startTag( - _currentLength + matchPosition(min, Edge::Open), - _expressions[min].tag); + startTag(matchPosition(min, Edge::Open), _expressions[min].tag); } } @@ -342,13 +351,18 @@ public: } private: - void finishTag(int index, int end, bool closed) { + void finishTag(int index, int offsetFromAccumulated, bool closed) { Expects(_tags != nullptr); Expects(index >= 0 && index < _tags->size()); auto &tag = (*_tags)[index]; - if (tag.length < 0) { - tag.length = end - tag.start; + if (tag.internalLength < 0) { + tag.internalLength = _currentInternalLength + + offsetFromAccumulated + - tag.internalStart; + tag.adjustedLength = _currentAdjustedLength + + offsetFromAccumulated + - tag.adjustedStart; tag.closed = closed; } if (index == _currentTag) { @@ -369,25 +383,33 @@ private: } const auto endPosition = newlinePosition( text, - std::max(0, tag.start + 1 - _currentLength)); + std::max(0, tag.internalStart + 1 - _currentInternalLength)); if (matchPosition(tagIndex, Edge::Close) <= endPosition) { return false; } - finishTag(index, _currentLength + endPosition, false); + finishTag(index, endPosition, false); return true; } void finishTags() { while (_currentTag != _currentFreeTag) { - finishTag(_currentTag, _currentLength, false); + finishTag(_currentTag, 0, false); } } - void startTag(int offset, const QString &tag) { + void startTag(int offsetFromAccumulated, const QString &tag) { Expects(_tags != nullptr); + const auto newTag = InputField::MarkdownTag{ + _currentInternalLength + offsetFromAccumulated, + -1, + _currentAdjustedLength + offsetFromAccumulated, + -1, + false, + tag + }; if (_currentFreeTag < _tags->size()) { - (*_tags)[_currentFreeTag] = { offset, -1, false, tag }; + (*_tags)[_currentFreeTag] = newTag; } else { - _tags->push_back({ offset, -1, false, tag }); + _tags->push_back(newTag); } ++_currentFreeTag; } @@ -447,7 +469,8 @@ private: int _currentTag = 0; int _currentFreeTag = 0; - int _currentLength = 0; + int _currentInternalLength = 0; + int _currentAdjustedLength = 0; }; @@ -1762,11 +1785,11 @@ QString InputField::getTextPart( if (full || !text.isEmpty()) { lastTag = format.property(kTagProperty).toString(); tagAccumulator.feed(lastTag, result.size()); - markdownTagAccumulator.feed(text, lastTag); } auto begin = text.data(); auto ch = begin; + auto adjustedLength = text.size(); for (const auto end = begin + text.size(); ch != end; ++ch) { if (IsNewline(*ch) && ch->unicode() != '\r') { *ch = QLatin1Char('\n'); @@ -1778,6 +1801,7 @@ QString InputField::getTextPart( if (ch > begin) { result.append(begin, ch - begin); } + adjustedLength += (emojiText.size() - 1); if (!emojiText.isEmpty()) { result.append(emojiText); } @@ -1788,12 +1812,16 @@ QString InputField::getTextPart( if (ch > begin) { result.append(begin, ch - begin); } + + if (full || !text.isEmpty()) { + markdownTagAccumulator.feed(text, adjustedLength, lastTag); + } } block = block.next(); if (block != till) { result.append('\n'); - markdownTagAccumulator.feed(newline, lastTag); + markdownTagAccumulator.feed(newline, 1, lastTag); } } @@ -2151,14 +2179,17 @@ void InputField::highlightMarkdown() { from = b; }; for (const auto &tag : _lastMarkdownTags) { - if (tag.start > from) { - applyColor(from, tag.start, QColor(0, 0, 0)); - } else if (tag.start < from) { + if (tag.internalStart > from) { + applyColor(from, tag.internalStart, QColor(0, 0, 0)); + } else if (tag.internalStart < from) { continue; } - applyColor(tag.start, tag.start + tag.length, tag.closed - ? QColor(0, 128, 0) - : QColor(128, 0, 0)); + applyColor( + tag.internalStart, + tag.internalStart + tag.internalLength, + (tag.closed + ? QColor(0, 128, 0) + : QColor(128, 0, 0))); } auto cursor = textCursor(); cursor.movePosition(QTextCursor::End); @@ -2352,36 +2383,38 @@ TextWithTags InputField::getTextWithAppliedMarkdown() const { const auto linksEnd = links.end(); for (const auto &tag : _lastMarkdownTags) { const auto tagLength = int(tag.tag.size()); - if (!tag.closed || tag.start < from) { + if (!tag.closed || tag.adjustedStart < from) { continue; } - const auto entityLength = tag.length - 2 * tagLength; + const auto entityLength = tag.adjustedLength - 2 * tagLength; if (entityLength <= 0) { continue; } - addOriginalTagsUpTill(tag.start); + addOriginalTagsUpTill(tag.adjustedStart); + const auto tagAdjustedEnd = tag.adjustedStart + tag.adjustedLength; if (originalTag != originalTagsEnd - && originalTag->offset < tag.start + tag.length) { + && originalTag->offset < tagAdjustedEnd) { continue; } while (link != linksEnd - && link->offset() + link->length() <= tag.start) { + && link->offset() + link->length() <= tag.adjustedStart) { ++link; } if (link != linksEnd - && link->offset() < tag.start + tag.length - && (link->offset() + link->length() > tag.start + tag.length - || link->offset() < tag.start)) { + && link->offset() < tagAdjustedEnd + && (link->offset() + link->length() > tagAdjustedEnd + || link->offset() < tag.adjustedStart)) { continue; } - addOriginalTextUpTill(tag.start); + addOriginalTextUpTill(tag.adjustedStart); result.tags.push_back(TextWithTags::Tag{ int(result.text.size()), entityLength, tag.tag }); - result.text.append( - originalText.midRef(tag.start + tagLength, entityLength)); - from = tag.start + tag.length; + result.text.append(originalText.midRef( + tag.adjustedStart + tagLength, + entityLength)); + from = tag.adjustedStart + tag.adjustedLength; removed += 2 * tagLength; } addOriginalTagsUpTill(originalText.size()); @@ -2749,8 +2782,8 @@ void InputField::processInstantReplaces(const QString &appended) { } const auto position = textCursor().position(); for (const auto &tag : _lastMarkdownTags) { - if (tag.start < position - && tag.start + tag.length >= position + if (tag.internalStart < position + && tag.internalStart + tag.internalLength >= position && (tag.tag == kTagCode || tag.tag == kTagPre)) { return; } diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.h b/Telegram/SourceFiles/ui/widgets/input_fields.h index 15eabca55..e91fbae78 100644 --- a/Telegram/SourceFiles/ui/widgets/input_fields.h +++ b/Telegram/SourceFiles/ui/widgets/input_fields.h @@ -125,8 +125,14 @@ public: using TagList = TextWithTags::Tags; struct MarkdownTag { - int start = 0; - int length = 0; + // With each emoji being QChar::ObjectReplacementCharacter. + int internalStart = 0; + int internalLength = 0; + + // Adjusted by emoji to match _lastTextWithTags. + int adjustedStart = 0; + int adjustedLength = 0; + bool closed = false; QString tag; };