Files
tdesktop/Telegram/lib_spellcheck/spellcheck/spellcheck_highlight_syntax.cpp
allhaileris afb81b8278
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
init
2026-02-16 15:50:16 +03:00

296 lines
7.3 KiB
C++

// 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/spellcheck_highlight_syntax.h"
#include "base/base_file_utilities.h"
#include "base/debug_log.h"
#include "base/flat_map.h"
#include "crl/crl_object_on_queue.h"
#include "SyntaxHighlighter.h"
#include <QtCore/QFile>
#include <xxhash.h>
#include <variant>
#include <string>
void spellchecker_InitHighlightingResource() {
#ifdef Q_OS_MAC // Use resources from the .app bundle on macOS.
base::RegisterBundledResources(u"lib_spellcheck.rcc"_q);
#else // Q_OS_MAC
Q_INIT_RESOURCE(highlighting);
#endif // Q_OS_MAC
}
namespace Spellchecker {
namespace {
base::flat_map<XXH64_hash_t, EntitiesInText> Cache;
HighlightProcessId ProcessIdAutoIncrement/* = 0*/;
rpl::event_stream<HighlightProcessId> ReadyStream;
class QueuedHighlighter final {
public:
QueuedHighlighter();
struct Request {
uint64 hash = 0;
QString text;
QString language;
};
void process(Request request);
void notify(HighlightProcessId id);
private:
using Task = std::variant<Request, HighlightProcessId>;
std::vector<Task> _tasks;
std::unique_ptr<SyntaxHighlighter> _highlighter;
};
[[nodiscard]] crl::object_on_queue<QueuedHighlighter> &Highlighter() {
static auto result = crl::object_on_queue<QueuedHighlighter>();
return result;
}
[[nodiscard]] const QString &LookupAlias(const QString &language) {
static const auto kAliases = base::flat_map<QString, QString>{
{ u"diff"_q, u"git"_q },
{ u"patch"_q, u"git"_q },
};
const auto i = kAliases.find(language);
return (i != end(kAliases)) ? i->second : language;
}
QueuedHighlighter::QueuedHighlighter() {
spellchecker_InitHighlightingResource();
}
void QueuedHighlighter::process(Request request) {
if (!_highlighter) {
auto file = QFile(":/misc/grammars.dat");
const auto size = file.size();
const auto ok1 = file.open(QIODevice::ReadOnly);
auto grammars = std::string();
grammars.resize(size);
const auto ok2 = (file.read(grammars.data(), size) == size);
Assert(ok1 && ok2);
_highlighter = std::make_unique<SyntaxHighlighter>(grammars);
}
const auto text = request.text.toStdString();
const auto language = LookupAlias(request.language.toLower());
const auto tokens = _highlighter->tokenize(text, language.toStdString());
static const auto colors = base::flat_map<std::string, int>{
{ "comment" , 1 },
{ "block-comment", 1 },
{ "prolog" , 1 },
{ "doctype" , 1 },
{ "cdata" , 1 },
{ "punctuation" , 2 },
{ "property" , 3 },
{ "tag" , 3 },
{ "boolean" , 3 },
{ "number" , 3 },
{ "constant" , 3 },
{ "symbol" , 3 },
{ "deleted" , 3 },
{ "selector" , 4 },
{ "attr-name" , 4 },
{ "string" , 4 },
{ "char" , 4 },
{ "builtin" , 4 },
{ "operator" , 5 },
{ "entity" , 5 },
{ "url" , 5 },
{ "atrule" , 6 },
{ "attr-value" , 6 },
{ "keyword" , 6 },
{ "function" , 6 },
{ "class-name" , 7 },
{ "inserted" , 8 },
};
auto offset = 0;
auto entities = EntitiesInText();
auto rebuilt = QString();
rebuilt.reserve(request.text.size());
const auto enumerate = [&](
const TokenList &list,
const std::string &type,
auto &&self) -> void {
for (const auto &node : list) {
if (node.isSyntax()) {
const auto &syntax = static_cast<const Syntax&>(node);
self(syntax.children(), syntax.type(), self);
} else {
const auto text = static_cast<const Text&>(node).value();
const auto utf16 = QString::fromUtf8(
text.data(),
text.size());
const auto length = utf16.size();
rebuilt.append(utf16);
if (!type.empty()) {
const auto i = colors.find(type);
if (i != end(colors)) {
entities.push_back(EntityInText(
EntityType::Colorized,
offset,
length,
QChar(ushort(i->second))));
}
}
offset += length;
}
}
};
enumerate(tokens, std::string(), enumerate);
const auto hash = request.hash;
if (offset != request.text.size()) {
// Something went wrong.
LOG(("Highlighting Error: for language '%1', text: %2"
).arg(request.language, request.text));
entities.clear();
}
crl::on_main([hash, entities = std::move(entities)]() mutable {
Cache.emplace(hash, std::move(entities));
});
}
void QueuedHighlighter::notify(HighlightProcessId id) {
crl::on_main([=] {
ReadyStream.fire_copy(id);
});
}
struct CacheResult {
uint64 hash = 0;
const EntitiesInText *list = nullptr;
explicit operator bool() const {
return list != nullptr;
}
};
[[nodiscard]] CacheResult FindInCache(
const TextWithEntities &text,
EntitiesInText::const_iterator i) {
const auto view = QStringView(text.text).mid(i->offset(), i->length());
const auto language = i->data();
struct Destroyer {
void operator()(XXH64_state_t *state) {
if (state) {
XXH64_freeState(state);
}
}
};
static const auto S = std::unique_ptr<XXH64_state_t, Destroyer>(
XXH64_createState());
const auto state = S.get();
XXH64_reset(state, 0);
XXH64_update(state, view.data(), view.size() * sizeof(ushort));
XXH64_update(state, language.data(), language.size() * sizeof(ushort));
const auto hash = XXH64_digest(state);
const auto j = Cache.find(hash);
return { hash, (j != Cache.cend()) ? &j->second : nullptr };
}
EntitiesInText::iterator Insert(
TextWithEntities &text,
EntitiesInText::iterator i,
const EntitiesInText &entities) {
auto next = i + 1;
if (entities.empty()) {
return next;
}
const auto offset = i->offset();
if (next != text.entities.cend()
&& next->type() == entities.front().type()
&& next->offset() == offset + entities.front().offset()) {
return next;
}
const auto length = i->length();
for (const auto &entity : entities) {
if (entity.offset() + entity.length() > length) {
break;
}
auto j = text.entities.insert(next, entity);
j->shiftRight(offset);
next = j + 1;
}
return next;
}
void Schedule(
uint64 hash,
const TextWithEntities &text,
EntitiesInText::const_iterator i) {
Highlighter().with([
hash,
text = text.text.mid(i->offset(), i->length()),
language = i->data()
](QueuedHighlighter &instance) mutable {
instance.process({ hash, std::move(text), std::move(language) });
});
}
void Notify(uint64 processId) {
Highlighter().with([processId](QueuedHighlighter &instance) {
instance.notify(processId);
});
}
} // namespace
HighlightProcessId TryHighlightSyntax(TextWithEntities &text) {
auto b = text.entities.begin();
auto i = b;
auto e = text.entities.end();
const auto checking = [](const EntityInText &entity) {
return (entity.type() == EntityType::Pre)
&& !entity.data().isEmpty();
};
auto processId = HighlightProcessId();
while (true) {
i = std::find_if(i, e, checking);
if (i == e) {
break;
} else if (const auto already = FindInCache(text, i)) {
i = Insert(text, i, *already.list);
b = text.entities.begin();
e = text.entities.end();
} else {
Schedule(already.hash, text, i);
if (!processId) {
processId = ++ProcessIdAutoIncrement;
}
++i;
}
}
if (processId) {
Notify(processId);
}
return processId;
}
rpl::producer<HighlightProcessId> HighlightReady() {
return ReadyStream.events();
}
} // namespace Spellchecker