init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,947 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "spellcheck/spelling_highlighter.h"
#include "base/weak_qptr.h"
#include "spellcheck/spellcheck_value.h"
#include "spellcheck/spellcheck_utils.h"
#include "spellcheck/spelling_highlighter_helper.h"
#include "ui/widgets/menu/menu.h"
#include "ui/text/text_entity.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/popup_menu.h"
#include "ui/ui_utility.h"
#include "base/qt/qt_common_adapters.h"
#include "base/platform/base_platform_info.h"
#include "base/event_filter.h"
#include "styles/palette.h"
namespace Spellchecker {
namespace {
constexpr auto kTagProperty = QTextFormat::UserProperty + 4;
const auto kUnspellcheckableTags = {
&Ui::InputField::kTagCode,
&Ui::InputField::kTagPre,
&Ui::InputField::kTagUnderline
};
constexpr auto kColdSpellcheckingTimeout = crl::time(1000);
constexpr auto kMaxDeadKeys = 1;
constexpr auto kSkippableFlags = 0
| TextParseLinks
| TextParseMentions
| TextParseHashtags
| TextParseBotCommands;
const auto kKeysToCheck = {
Qt::Key_Up,
Qt::Key_Down,
Qt::Key_Left,
Qt::Key_Right,
Qt::Key_PageUp,
Qt::Key_PageDown,
Qt::Key_Home,
Qt::Key_End,
};
inline int EndOfWord(const MisspelledWord &range) {
return range.first + range.second;
}
inline bool IntersectsWordRanges(
const MisspelledWord &range,
int pos2,
int len2) {
const auto l1 = range.first;
const auto r1 = EndOfWord(range) - 1;
const auto l2 = pos2;
const auto r2 = pos2 + len2 - 1;
return !(l1 > r2 || l2 > r1);
}
inline bool IntersectsWordRanges(
const MisspelledWord &range,
const MisspelledWord &range2) {
const auto l1 = range.first;
const auto r1 = EndOfWord(range) - 1;
const auto l2 = range2.first;
const auto r2 = EndOfWord(range2) - 1;
return !(l1 > r2 || l2 > r1);
}
inline bool IntersectsWordRanges(const EntityInText &e, int pos2, int len2) {
return IntersectsWordRanges({ e.offset(), e.length() }, pos2, len2);
}
inline bool IsTagUnspellcheckable(const QString &tag) {
if (tag.isEmpty()) {
return false;
}
for (const auto &single : TextUtilities::SplitTags(tag)) {
const auto isCommonFormatting = ranges::any_of(
kUnspellcheckableTags,
[&](const auto *t) { return (*t) == single; });
if (isCommonFormatting) {
return true;
}
if (Ui::InputField::IsValidMarkdownLink(single)) {
return true;
}
if (TextUtilities::IsMentionLink(single)) {
return true;
}
}
return false;
}
inline auto FindEntities(const QString &text) {
return TextUtilities::ParseEntities(text, kSkippableFlags).entities;
}
inline auto IntersectsAnyOfEntities(
int pos,
int len,
EntitiesInText entities) {
return !entities.empty() && ranges::any_of(entities, [&](const auto &e) {
return IntersectsWordRanges(e, pos, len);
});
}
inline QChar AddedSymbol(QStringView text, int position, int added) {
if (added != 1 || position >= text.size()) {
return QChar();
}
return text.at(position);
}
inline MisspelledWord CorrectAccentValues(
const QString &oldText,
const QString &newText) {
auto diff = std::vector<int>();
const auto sizeOfDiff = newText.size() - oldText.size();
if (sizeOfDiff <= 0 || sizeOfDiff > kMaxDeadKeys) {
return MisspelledWord();
}
for (auto i = 0; i < oldText.size(); i++) {
if (oldText.at(i) != newText.at(i + diff.size())) {
diff.push_back(i);
if (diff.size() > kMaxDeadKeys) {
return MisspelledWord();
}
}
}
if (diff.size() == 0) {
return MisspelledWord(oldText.size(), sizeOfDiff);
}
return MisspelledWord(diff.front(), diff.size() > 1 ? diff.back() : 1);
}
inline MisspelledWord RangeFromCursorSelection(const QTextCursor &cursor) {
const auto start = cursor.selectionStart();
return MisspelledWord(start, cursor.selectionEnd() - start);
}
[[nodiscard]] bool SpellcheckSuggestEvent(not_null<QKeyEvent*> e) {
const auto modifier = Platform::IsMac()
? Qt::MetaModifier
: Qt::ControlModifier;
return (e->key() == Qt::Key_Space) && e->modifiers().testFlag(modifier);
}
} // namespace
SpellingHighlighter::SpellingHighlighter(
not_null<Ui::InputField*> field,
rpl::producer<bool> enabled,
std::optional<CustomContextMenuItem> customContextMenuItem)
: QSyntaxHighlighter(field->rawTextEdit()->document())
, _cursor(QTextCursor(document()))
, _coldSpellcheckingTimer([=] { checkChangedText(); })
, _field(field)
, _textEdit(field->rawTextEdit())
, _customContextMenuItem(customContextMenuItem) {
#ifdef Q_OS_WIN
Platform::Spellchecker::Init();
#endif // !Q_OS_WIN
_cachedRanges = MisspelledWords();
// Use the patched SpellCheckUnderline style.
_misspelledFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
style::PaletteChanged(
) | rpl::on_next([=] {
updatePalette();
rehighlight();
}, _lifetime);
updatePalette();
_field->documentContentsChanges(
) | rpl::on_next([=](const auto &value) {
const auto &[pos, removed, added] = value;
contentsChange(pos, removed, added);
}, _lifetime);
_field->markdownTagApplies(
) | rpl::on_next([=](auto markdownTag) {
if (!IsTagUnspellcheckable(markdownTag.tag)) {
return;
}
_cachedRanges = ranges::views::all(
_cachedRanges
) | ranges::views::filter([&](const auto &range) {
return !IntersectsWordRanges(
range,
markdownTag.internalStart,
markdownTag.internalLength);
}) | ranges::to_vector;
rehighlight();
}, _lifetime);
updateDocumentText();
std::move(
enabled
) | rpl::on_next([=](bool value) {
setEnabled(value);
if (_enabled) {
_field->installEventFilter(this);
_textEdit->installEventFilter(this);
_textEdit->viewport()->installEventFilter(this);
} else {
_field->installEventFilter(this);
_textEdit->removeEventFilter(this);
_textEdit->viewport()->removeEventFilter(this);
}
}, _lifetime);
Spellchecker::SupportedScriptsChanged(
) | rpl::on_next([=] {
checkCurrentText();
}, _lifetime);
}
void SpellingHighlighter::updatePalette() {
_misspelledFormat.setUnderlineColor(st::spellUnderline->c);
}
void SpellingHighlighter::contentsChange(int pos, int removed, int added) {
if (!_enabled) {
return;
}
if (document()->isEmpty()) {
updateDocumentText();
_cachedRanges.clear();
return;
}
{
const auto oldText = documentText().mid(
pos,
documentText().indexOf(QChar::ParagraphSeparator, pos));
updateDocumentText();
const auto b = findBlock(pos);
const auto bLen = (document()->blockCount() > 1)
? b.length()
: b.text().size();
// This is a workaround for typing accents.
// For example, when the user press the dead key (e.g. ` or ´),
// Qt sends wrong values. E.g. if a text length is 100,
// then values will be 0, 100, 100.
// This invokes to re-check the entire text.
// The Mac's accent menu has a pretty similar behavior.
if ((b.position() == pos) && (bLen == added)) {
const auto newText = b.text();
const auto diff = added - removed;
// The plain text of the document cannot contain dead keys.
if (!diff) {
if (!oldText.compare(newText, Qt::CaseSensitive)) {
const auto c = RangeFromCursorSelection(
_textEdit->textCursor());
// If the cursor has a selection for the entire text,
// we probably just changed its formatting.
// So if we find the unspellcheckable tag,
// we can clear cached ranges of misspelled words.
if (!c.first && c.second == bLen) {
if (hasUnspellcheckableTag(pos, added)) {
_cachedRanges.clear();
rehighlight();
} else {
checkCurrentText();
}
}
return;
}
} else if (diff > 0 && diff <= kMaxDeadKeys) {
const auto [p, l] = CorrectAccentValues(oldText, newText);
if (l) {
pos = p + b.position();
added = l;
removed = 0;
}
}
}
}
const auto shift = [&](auto chars) {
ranges::for_each(_cachedRanges, [&](auto &range) {
if (range.first >= pos + removed) {
range.first += chars;
}
});
};
// Shift to the right all words after the cursor, when adding text.
if (added > 0) {
shift(added);
}
// Remove all words that are in the selection.
// Remove the word that is under the cursor.
const auto wordUnderPos = getWordUnderPosition(pos);
// If the cursor is between spaces,
// QTextCursor::WordUnderCursor highlights the word on the left
// even if the word is not under the cursor.
// Example: "super | test", where | is the cursor position.
// In this example QTextCursor::WordUnderCursor will select "super".
const auto isPosNotInWord = pos > EndOfWord(wordUnderPos);
_cachedRanges = (
_cachedRanges
) | ranges::views::filter([&](const auto &range) {
const auto isIntersected = IntersectsWordRanges(range, wordUnderPos);
if (isIntersected) {
return isPosNotInWord;
}
return !(isIntersected
|| (removed > 0 && IntersectsWordRanges(range, pos, removed)));
}) | ranges::to_vector;
// Shift to the left all words after the cursor, when deleting text.
if (removed > 0) {
shift(-removed);
}
// Normally we should to invoke rehighlighting to immediately apply
// shifting of ranges. But we don't have to do this because the call of
// contentsChange() is performed before the application's call of
// highlightBlock().
_addedSymbols += added;
_removedSymbols += removed;
// The typing of text character by character should produce
// the same _lastPosition, _addedSymbols and _removedSymbols values
// as removing and pasting several characters at a time.
if (!_lastPosition || (removed == 1)) {
_lastPosition = pos;
}
const auto addedSymbol = AddedSymbol(documentText(), pos, added);
if ((removed == 1) || addedSymbol.isLetterOrNumber()) {
if (_coldSpellcheckingTimer.isActive()) {
_coldSpellcheckingTimer.cancel();
}
_coldSpellcheckingTimer.callOnce(kColdSpellcheckingTimeout);
} else {
// We forcefully increase the range of check
// when inserting a non-char. This can help when the user inserts
// a non-char in the middle of a word.
if (!(addedSymbol.isNull()
|| addedSymbol.isSpace()
|| addedSymbol.isLetterOrNumber())) {
_lastPosition--;
_addedSymbols++;
}
if (_isLastKeyRepeat) {
return;
}
checkChangedText();
}
}
void SpellingHighlighter::checkChangedText() {
const auto pos = _lastPosition;
const auto added = _addedSymbols;
const auto removed = _removedSymbols;
_lastPosition = 0;
_removedSymbols = 0;
_addedSymbols = 0;
if (_coldSpellcheckingTimer.isActive()) {
_coldSpellcheckingTimer.cancel();
}
const auto wordUnderCursor = getWordUnderPosition(pos);
// If the length of the word is 0, there is no sense in checking it.
if (!wordUnderCursor.second) {
return;
}
const auto wordInCacheIt = [=] {
return ranges::find_if(_cachedRanges, [&](auto &&w) {
return w.first >= wordUnderCursor.first;
});
};
if (added > 0) {
const auto lastWordNewSelection = getWordUnderPosition(pos + added);
// This is the same word.
if (wordUnderCursor == lastWordNewSelection) {
checkSingleWord(wordUnderCursor);
return;
}
const auto beginNewSelection = wordUnderCursor.first;
const auto endNewSelection = EndOfWord(lastWordNewSelection);
auto callback = [=](MisspelledWords &&r) {
ranges::insert(_cachedRanges, wordInCacheIt(), std::move(r));
};
invokeCheckText(
beginNewSelection,
endNewSelection - beginNewSelection,
std::move(callback));
return;
}
if (removed > 0) {
checkSingleWord(wordUnderCursor);
}
}
MisspelledWords SpellingHighlighter::filterSkippableWords(
MisspelledWords &ranges) {
const auto text = documentText();
if (text.isEmpty()) {
return MisspelledWords();
}
return ranges | ranges::views::filter([&](const auto &range) {
return !isSkippableWord(range);
}) | ranges::to_vector;
}
bool SpellingHighlighter::isSkippableWord(const MisspelledWord &range) {
return isSkippableWord(range.first, range.second);
}
bool SpellingHighlighter::isSkippableWord(int position, int length) {
if (hasUnspellcheckableTag(position, length)) {
return true;
}
const auto text = documentText();
const auto ref = base::StringViewMid(text, position, length);
if (ref.isNull()) {
return true;
}
return IsWordSkippable(ref);
}
void SpellingHighlighter::checkCurrentText() {
if (document()->isEmpty()) {
_cachedRanges.clear();
return;
}
invokeCheckText(0, size(), [&](MisspelledWords &&ranges) {
_cachedRanges = std::move(ranges);
});
}
void SpellingHighlighter::invokeCheckText(
int textPosition,
int textLength,
Fn<void(MisspelledWords &&ranges)> callback) {
if (!_enabled) {
return;
}
const auto rangesOffset = textPosition;
const auto text = partDocumentText(textPosition, textLength);
const auto weak = base::make_weak(this);
_countOfCheckingTextAsync++;
crl::async([=,
text = std::move(text),
callback = std::move(callback)]() mutable {
MisspelledWords misspelledWordRanges;
Platform::Spellchecker::CheckSpellingText(
text,
&misspelledWordRanges);
if (rangesOffset) {
ranges::for_each(misspelledWordRanges, [&](auto &&range) {
range.first += rangesOffset;
});
}
crl::on_main(weak, [=,
text = std::move(text),
ranges = std::move(misspelledWordRanges),
callback = std::move(callback)]() mutable {
_countOfCheckingTextAsync--;
// Checking a large part of text can take an unknown amount of
// time. So we have to compare the text before and after async
// work.
// If the text has changed during async and we have more async,
// we don't perform further refreshing of cache and underlines.
// But if it was the last async, we should invoke a new one.
if (compareDocumentText(text, textPosition, textLength)) {
if (!_countOfCheckingTextAsync) {
checkCurrentText();
}
return;
}
auto filtered = filterSkippableWords(ranges);
// When we finish checking the text, the user can
// supplement the last word and there may be a situation where
// a part of the last word may not be underlined correctly.
// Example:
// 1. We insert a text with an incomplete last word.
// "Time in a bottl".
// 2. We don't wait for the check to be finished
// and end the last word with the letter "e".
// 3. invokeCheckText() will mark the last word "bottl" as
// misspelled.
// 4. checkSingleWord() will mark the "bottle" as correct and
// leave it as it is.
// 5. The first five letters of the "bottle" will be underlined
// and the sixth will not be underlined.
// We can fix it with a check of completeness of the last word.
if (filtered.size()) {
const auto lastWord = filtered.back();
if (const auto endOfText = textPosition + textLength;
EndOfWord(lastWord) == endOfText) {
const auto word = getWordUnderPosition(endOfText);
if (EndOfWord(word) != endOfText) {
filtered.pop_back();
checkSingleWord(word);
}
}
}
callback(std::move(filtered));
for (const auto &b : blocksFromRange(textPosition, textLength)) {
rehighlightBlock(b);
}
});
});
}
void SpellingHighlighter::checkSingleWord(const MisspelledWord &singleWord) {
const auto weak = base::make_weak(this);
auto w = partDocumentText(singleWord.first, singleWord.second);
if (isSkippableWord(singleWord)) {
return;
}
crl::async([=,
w = std::move(w),
singleWord = std::move(singleWord)]() mutable {
if (Platform::Spellchecker::CheckSpelling(std::move(w))) {
return;
}
crl::on_main(weak, [=,
singleWord = std::move(singleWord)]() mutable {
const auto posOfWord = singleWord.first;
ranges::insert(
_cachedRanges,
ranges::find_if(_cachedRanges, [&](auto &&w) {
return w.first >= posOfWord;
}),
singleWord);
rehighlightBlock(findBlock(posOfWord));
});
});
}
bool SpellingHighlighter::hasUnspellcheckableTag(int begin, int length) {
// This method is called only in the context of separate words,
// so it is not supposed that the word can be in more than one block.
const auto block = findBlock(begin);
length = std::min(block.position() + block.length() - begin, length);
for (auto it = block.begin(); !(it.atEnd()); ++it) {
const auto fragment = it.fragment();
if (!fragment.isValid()) {
continue;
}
const auto frPos = fragment.position();
const auto frLen = fragment.length();
if (!IntersectsWordRanges({ frPos, frLen }, begin, length)) {
continue;
}
const auto format = fragment.charFormat();
if (!format.hasProperty(kTagProperty)) {
continue;
}
const auto tag = format.property(kTagProperty).toString();
if (IsTagUnspellcheckable(tag)) {
return true;
}
}
return false;
}
MisspelledWord SpellingHighlighter::getWordUnderPosition(int position) {
if (position < 0) {
position = 0;
}
_cursor.setPosition(std::min(position, size()));
_cursor.select(QTextCursor::WordUnderCursor);
return RangeFromCursorSelection(_cursor);
}
void SpellingHighlighter::highlightBlock(const QString &text) {
if (_cachedRanges.empty() || !_enabled || text.isEmpty()) {
return;
}
const auto entities = FindEntities(text);
const auto bPos = currentBlock().position();
const auto bLen = currentBlock().length();
ranges::for_each((
_cachedRanges
// Skip the all words outside the current block.
) | ranges::views::filter([&](const auto &range) {
return IntersectsWordRanges(range, bPos, bLen);
}), [&](const auto &range) {
const auto posInBlock = range.first - bPos;
if (IntersectsAnyOfEntities(posInBlock, range.second, entities)) {
return;
}
setFormat(posInBlock, range.second, _misspelledFormat);
});
setCurrentBlockState(0);
}
bool SpellingHighlighter::eventFilter(QObject *o, QEvent *e) {
if (!_enabled) {
return false;
} else if (o == _field) {
if (e->type() == QEvent::KeyPress) {
const auto k = static_cast<QKeyEvent*>(e);
if (SpellcheckSuggestEvent(k)) {
showSpellcheckerMenu();
return true;
}
}
return false;
}
if (e->type() == QEvent::ContextMenu) {
const auto c = static_cast<QContextMenuEvent*>(e);
const auto menu = _textEdit->createStandardContextMenu();
if (!menu || !c) {
return false;
}
// Copy of QContextMenuEvent.
auto copyEvent = std::make_shared<QContextMenuEvent>(
c->reason(),
c->pos(),
c->globalPos());
auto showMenu = [=, copyEvent = std::move(copyEvent)] {
_contextMenuCreated.fire({ menu, copyEvent });
};
addSpellcheckerActions(
std::move(menu),
_textEdit->cursorForPosition(c->pos()),
std::move(showMenu),
c->globalPos());
return true;
} else if (e->type() == QEvent::KeyPress) {
const auto k = static_cast<QKeyEvent*>(e);
if (ranges::contains(kKeysToCheck, k->key())) {
if (_addedSymbols + _removedSymbols + _lastPosition) {
checkCurrentText();
}
} else if ((o == _textEdit) && k->isAutoRepeat()) {
_isLastKeyRepeat = true;
}
} else if (_isLastKeyRepeat && (o == _textEdit)) {
if (e->type() == QEvent::FocusOut) {
_isLastKeyRepeat = false;
if (_addedSymbols + _removedSymbols + _lastPosition) {
checkCurrentText();
}
} else if (e->type() == QEvent::KeyRelease) {
const auto k = static_cast<QKeyEvent*>(e);
if (!k->isAutoRepeat()) {
_isLastKeyRepeat = false;
_coldSpellcheckingTimer.callOnce(kColdSpellcheckingTimeout);
}
}
} else if ((o == _textEdit->viewport())
&& (e->type() == QEvent::MouseButtonPress)) {
if (_addedSymbols + _removedSymbols + _lastPosition) {
checkCurrentText();
}
}
return false;
}
bool SpellingHighlighter::enabled() {
return _enabled;
}
void SpellingHighlighter::setEnabled(bool enabled) {
_enabled = enabled;
if (_enabled) {
updateDocumentText();
checkCurrentText();
} else {
_cachedRanges.clear();
rehighlight();
}
}
QString SpellingHighlighter::documentText() {
return _lastPlainText;
}
void SpellingHighlighter::updateDocumentText() {
_lastPlainText = document()->toRawText();
}
QString SpellingHighlighter::partDocumentText(int pos, int length) {
return _lastPlainText.mid(pos, length);
}
int SpellingHighlighter::size() {
return document()->characterCount() - 1;
}
QTextBlock SpellingHighlighter::findBlock(int pos) {
return document()->findBlock(pos);
}
std::vector<QTextBlock> SpellingHighlighter::blocksFromRange(
int pos,
int length) {
auto b = findBlock(pos);
auto blocks = std::vector<QTextBlock>{b};
const auto end = pos + length;
while (!b.contains(end) && (b != document()->end())) {
if ((b = b.next()).isValid()) {
blocks.push_back(b);
}
}
return blocks;
}
int SpellingHighlighter::compareDocumentText(
const QString &text,
int textPos,
int textLen) {
if (_lastPlainText.size() < textPos + textLen) {
return -1;
}
const auto p = base::StringViewMid(_lastPlainText, textPos, textLen);
if (p.isNull()) {
return -1;
}
return text.compare(p, Qt::CaseSensitive);
}
void SpellingHighlighter::addSpellcheckerActions(
not_null<QMenu*> parentMenu,
QTextCursor cursorForPosition,
Fn<void()> showMenuCallback,
QPoint mousePosition) {
const auto menu = new QMenu(
ph::lng_spellchecker_submenu(ph::now),
parentMenu);
auto addToParentAndShow = [=](int) {
if (!menu->isEmpty()) {
using namespace Spelling::Helper;
if (IsContextMenuTop(parentMenu, mousePosition)) {
parentMenu->addSeparator();
parentMenu->addMenu(menu);
} else {
const auto first = parentMenu->actions().first();
parentMenu->insertMenu(first, menu);
parentMenu->insertSeparator(first);
}
}
showMenuCallback();
};
fillSpellcheckerMenu(menu, cursorForPosition, addToParentAndShow);
}
void SpellingHighlighter::showSpellcheckerMenu() {
auto menu = std::make_unique<QMenu>();
const auto raw = menu.get();
const auto cursor = _textEdit->textCursor();
auto rect = _textEdit->cursorRect(cursor);
rect.setTopLeft(_textEdit->viewport()->mapToGlobal(rect.topLeft()));
auto show = [=, menu = std::move(menu)](int firstSuggestion) mutable {
if (!menu->isEmpty()) {
_menu = base::make_unique_q<Ui::PopupMenu>(
_textEdit,
menu.release(),
_field->st().menu);
_menu->setForcedVerticalOrigin(
Ui::PopupMenu::VerticalOrigin::Top);
base::install_event_filter(_menu.get(), [=](
not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
const auto k = static_cast<QKeyEvent*>(e.get());
if (SpellcheckSuggestEvent(k)) {
auto event = QKeyEvent(
QEvent::KeyPress,
Qt::Key_Down,
Qt::KeyboardModifiers());
_menu->menu()->handleKeyPress(&event);
}
}
return base::EventFilterResult::Continue;
});
_menu->popup(rect.topLeft());
if (_menu && firstSuggestion >= 0) {
_menu->menu()->setSelected(firstSuggestion, false);
}
}
};
fillSpellcheckerMenu(raw, cursor, std::move(show));
}
void SpellingHighlighter::fillSpellcheckerMenu(
not_null<QMenu*> menu,
QTextCursor cursorForPosition,
FnMut<void(int firstSuggestionIndex)> show) {
const auto customItem = !Platform::Spellchecker::IsSystemSpellchecker()
&& _customContextMenuItem.has_value();
cursorForPosition.select(QTextCursor::WordUnderCursor);
// There is no reason to call async work if the word is skippable.
const auto skippable = [&] {
const auto &[p, l] = RangeFromCursorSelection(cursorForPosition);
const auto e = FindEntities(findBlock(p).text());
return (!l
|| isSkippableWord(p, l)
|| IntersectsAnyOfEntities(p, l, e));
}();
if (customItem) {
menu->addAction(
_customContextMenuItem->title,
_customContextMenuItem->callback);
}
if (skippable) {
show(-1);
return;
}
const auto word = cursorForPosition.selectedText();
auto fillMenu = [
=,
show = std::move(show),
menu = std::move(menu)
](
bool isCorrect,
const auto &suggestions,
const auto &newTextCursor) mutable {
auto firstSuggestionIndex = -1;
const auto guard = gsl::finally([&] {
show(firstSuggestionIndex);
});
const auto addSeparator = [&] {
if (!menu->isEmpty()) {
menu->addSeparator();
}
};
if (isCorrect) {
if (Platform::Spellchecker::IsWordInDictionary(word)) {
addSeparator();
auto remove = [=] {
Platform::Spellchecker::RemoveWord(word);
checkCurrentText();
};
menu->addAction(
ph::lng_spellchecker_remove(ph::now),
std::move(remove));
}
return;
}
addSeparator();
auto add = [=] {
Platform::Spellchecker::AddWord(word);
checkCurrentText();
};
menu->addAction(ph::lng_spellchecker_add(ph::now), std::move(add));
auto ignore = [=] {
Platform::Spellchecker::IgnoreWord(word);
checkCurrentText();
};
menu->addAction(
ph::lng_spellchecker_ignore(ph::now),
std::move(ignore));
if (suggestions.empty()) {
return;
}
addSeparator();
for (const auto &suggestion : suggestions) {
if (firstSuggestionIndex < 0) {
firstSuggestionIndex = menu->actions().size();
}
auto replaceWord = [=] {
const auto oldTextCursor = _textEdit->textCursor();
_textEdit->setTextCursor(newTextCursor);
_textEdit->textCursor().insertText(suggestion);
_textEdit->setTextCursor(oldTextCursor);
};
menu->addAction(suggestion, std::move(replaceWord));
}
};
const auto weak = base::make_weak(this);
crl::async([=,
newTextCursor = std::move(cursorForPosition),
fillMenu = std::move(fillMenu),
word = std::move(word)
]() mutable {
const auto isCorrect = Platform::Spellchecker::CheckSpelling(word);
auto suggestions = std::vector<QString>();
if (!isCorrect) {
Platform::Spellchecker::FillSuggestionList(word, &suggestions);
}
crl::on_main(weak, [=,
newTextCursor = std::move(newTextCursor),
suggestions = std::move(suggestions),
fillMenu = std::move(fillMenu)
]() mutable {
fillMenu(
isCorrect,
std::move(suggestions),
std::move(newTextCursor));
});
});
}
} // namespace Spellchecker