better scrollbar in codeblocks + enter anim for delegates

This commit is contained in:
2026-08-27 01:47:17 +02:00
parent c33dbc147f
commit 293bf67e09
16 changed files with 458 additions and 229 deletions
+151 -114
View File
@@ -21,7 +21,6 @@ namespace ZShell::llm {
namespace hl {
// Role ids; 0 means "no color".
enum Role : uint8_t {
None = 0,
Comment,
@@ -43,29 +42,43 @@ enum Role : uint8_t {
const char* roleName(Role role) {
switch (role) {
case Comment: return "comment";
case String: return "string";
case StringKey: return "string.key";
case Number: return "number";
case Constant: return "constant";
case Keyword: return "keyword";
case Type: return "type";
case Function: return "function";
case Method: return "method";
case Macro: return "macro";
case Preproc: return "preproc";
case Operator: return "operator";
case Property: return "property";
case Label: return "label";
case Attribute: return "attribute";
default: return "";
case Comment:
return "comment";
case String:
return "string";
case StringKey:
return "string.key";
case Number:
return "number";
case Constant:
return "constant";
case Keyword:
return "keyword";
case Type:
return "type";
case Function:
return "function";
case Method:
return "method";
case Macro:
return "macro";
case Preproc:
return "preproc";
case Operator:
return "operator";
case Property:
return "property";
case Label:
return "label";
case Attribute:
return "attribute";
default:
return "";
}
}
using LanguageFn = const TSLanguage* (*)();
// Grammar registry generated by CMake from the installed grammars and
// their highlight queries (see CMakeLists.txt).
const QHash<QString, CodeHighlighter::Grammar>& grammars() {
static const QHash<QString, CodeHighlighter::Grammar> grammars = [] {
QHash<QString, CodeHighlighter::Grammar> map;
@@ -89,8 +102,6 @@ const QHash<QString, CodeHighlighter::Grammar>& grammars() {
CodeHighlighter* CodeHighlighter::s_instance = nullptr;
const QHash<QString, QString>& CodeHighlighter::aliases() {
// Language tags as written in code fences (and common variants) to
// grammar id.
static const QHash<QString, QString> aliases = [] {
QHash<QString, QString> map;
map.insert("c", "c");
@@ -145,49 +156,38 @@ const QHash<QString, QString>& CodeHighlighter::aliases() {
uint8_t CodeHighlighter::roleFor(const char* name, uint32_t length) {
const QString n = QString::fromUtf8(name, length);
if (n == "comment")
return hl::Role::Comment;
if (n == "comment") return hl::Role::Comment;
if (n.startsWith("string"))
return n == "string.special.key" ? hl::Role::StringKey : hl::Role::String;
if (n == "escape" || n == "regexp")
return hl::Role::String;
if (n.startsWith("number"))
return hl::Role::Number;
if (n.startsWith("constant") || n == "boolean" || n == "bool"
|| n.startsWith("character"))
return n == "string.special.key" ? hl::Role::StringKey
: hl::Role::String;
if (n == "escape" || n == "regexp") return hl::Role::String;
if (n.startsWith("number")) return hl::Role::Number;
if (n.startsWith("constant") || n == "boolean" || n == "bool" ||
n.startsWith("character"))
return hl::Role::Constant;
if (n.startsWith("keyword"))
return hl::Role::Keyword;
if (n == "type" || n.startsWith("type."))
if (n.startsWith("keyword")) return hl::Role::Keyword;
if (n == "type" || n.startsWith("type.")) return hl::Role::Type;
if (n.startsWith("namespace") || n.startsWith("module") ||
n == "support.type" || n == "support.namespace")
return hl::Role::Type;
if (n.startsWith("namespace") || n.startsWith("module")
|| n == "support.type" || n == "support.namespace")
return hl::Role::Type;
if (n.startsWith("function") || n == "constructor"
|| n.startsWith("support.function"))
if (n.startsWith("function") || n == "constructor" ||
n.startsWith("support.function"))
return hl::Role::Function;
if (n == "method" || n == "method.builtin")
return hl::Role::Method;
if (n.startsWith("macro"))
return hl::Role::Macro;
if (n.startsWith("preproc"))
return hl::Role::Preproc;
if (n == "operator" || n == "punctuation.operator" || n.startsWith("operator.")
|| n.startsWith("punctuation"))
if (n == "method" || n == "method.builtin") return hl::Role::Method;
if (n.startsWith("macro")) return hl::Role::Macro;
if (n.startsWith("preproc")) return hl::Role::Preproc;
if (n == "operator" || n == "punctuation.operator" ||
n.startsWith("operator.") || n.startsWith("punctuation"))
return hl::Role::Operator;
if (n == "property" || n == "field" || n.startsWith("property."))
return hl::Role::Property;
if (n == "label")
return hl::Role::Label;
if (n == "label") return hl::Role::Label;
if (n.startsWith("attribute") || n == "annotation")
return hl::Role::Attribute;
// HTML tag names, CSS variables and friends.
if (n == "tag" || (n.startsWith("tag.") && n != "tag.delimiter"))
return hl::Role::Keyword;
if (n.startsWith("variable"))
return hl::Role::Constant;
if (n.startsWith("support"))
return hl::Role::Function;
if (n.startsWith("variable")) return hl::Role::Constant;
if (n.startsWith("support")) return hl::Role::Function;
return hl::Role::None;
}
@@ -195,68 +195,116 @@ const char* CodeHighlighter::roleName(uint8_t role) {
return hl::roleName(static_cast<hl::Role>(role));
}
QString CodeHighlighter::resolveId(const QString& language) {
const QString tag = language.trimmed().toLower();
const QString alias = aliases().value(tag);
return alias.isEmpty() ? tag : alias; // unknown tags = grammar id
}
QString CodeHighlighter::cacheKey(const QString& id, const QString& code) {
return id + QLatin1Char('\x01') + QString::number(code.size()) +
QLatin1Char('\x01') + QString::number(qHash(code));
}
QVariantList CodeHighlighter::lookupSpans(
const QString& code, const QString& language) const {
if (code.isEmpty()) return {};
const QString key = cacheKey(resolveId(language), code);
QMutexLocker locker(&m_cacheMutex);
const auto it = m_spanCache.constFind(key);
if (it == m_spanCache.constEnd() || it->code != code) return {};
// Most recently used; eviction drops the oldest entries first.
const qsizetype pos = m_spanCacheOrder.indexOf(key);
if (pos >= 0) m_spanCacheOrder.move(pos, m_spanCacheOrder.size() - 1);
return it->spans;
}
void CodeHighlighter::storeSpans(
const QString& code,
const QString& language,
const QVariantList& spans) const {
if (spans.isEmpty() || code.isEmpty()) return;
static constexpr int kMaxEntries = 32;
static constexpr int kMaxBytes = 1024 * 1024;
const QString key = cacheKey(resolveId(language), code);
const int bytes = static_cast<int>(code.toUtf8().size());
QMutexLocker locker(&m_cacheMutex);
auto it = m_spanCache.find(key);
if (it != m_spanCache.end()) {
m_spanCacheBytes -= static_cast<int>(it->code.toUtf8().size());
m_spanCache.erase(it);
m_spanCacheOrder.removeAll(key);
}
while (m_spanCacheOrder.size() >= kMaxEntries ||
m_spanCacheBytes + bytes > kMaxBytes) {
if (m_spanCacheOrder.isEmpty()) break;
const QString oldest = m_spanCacheOrder.takeFirst();
m_spanCacheBytes -=
static_cast<int>(m_spanCache.value(oldest).code.toUtf8().size());
m_spanCache.remove(oldest);
}
m_spanCache.insert(key, SpanCacheEntry{code, spans});
m_spanCacheOrder.append(key);
m_spanCacheBytes += bytes;
}
void CodeHighlighter::highlight(
const QString& code, const QString& language, QObject* target, int token) {
QThreadPool::globalInstance()->start([this, target, token, code, language]() {
const QVariantList spans = doHighlight(code, language);
// The target item may be long gone by now (delegates are
// recreated constantly while chats load); a destroyed target is
// simply skipped. Deliver through the app instance (never
// destroyed) and re-check there: posting to `target` from the
// pool thread would race with its destruction.
QPointer<QObject> guard(target);
QPointer<QObject> targetGuard(target);
const QVariantList cached = lookupSpans(code, language);
if (!cached.isEmpty()) {
QMetaObject::invokeMethod(
QCoreApplication::instance(),
[guard, token, spans]() {
if (!guard)
return;
// QML functions are only invokable by their generic
// QVariant overload, so pass untyped arguments.
targetGuard,
"onHighlightSpans",
Qt::DirectConnection,
Q_ARG(QVariant, token),
Q_ARG(QVariant, cached));
return;
}
QThreadPool::globalInstance()->start(
[this, target, token, code, language]() {
const QVariantList spans = doHighlight(code, language);
storeSpans(code, language, spans);
QPointer<QObject> guard(target);
QMetaObject::invokeMethod(
guard, "onHighlightSpans",
Q_ARG(QVariant, token), Q_ARG(QVariant, spans));
},
Qt::QueuedConnection);
});
QCoreApplication::instance(),
[guard, token, spans]() {
if (!guard) return;
QMetaObject::invokeMethod(
guard,
"onHighlightSpans",
Q_ARG(QVariant, token),
Q_ARG(QVariant, spans));
},
Qt::QueuedConnection);
});
}
QVariantList CodeHighlighter::doHighlight(
const QString& code, const QString& language) const {
QVariantList spans;
if (code.isEmpty())
return spans;
if (code.isEmpty()) return spans;
const QString tag = language.trimmed().toLower();
const QString id = [&] {
const QString alias = aliases().value(tag);
return alias.isEmpty() ? tag : alias; // unknown tags = grammar id
}();
const QString id = resolveId(language);
const Grammar& grammar = hl::grammars().value(id);
if (grammar.libs.empty())
return spans;
if (grammar.libs.empty()) return spans;
// Guard against pathological blocks; highlighting is best-effort.
static constexpr size_t kMaxBytes = 512 * 1024;
const QByteArray utf8 = code.toUtf8();
if (static_cast<size_t>(utf8.size()) > kMaxBytes)
return spans;
if (static_cast<size_t>(utf8.size()) > kMaxBytes) return spans;
const TSLanguage* lang = nullptr;
TSQuery* query = nullptr;
{
QMutexLocker locker(&m_stateMutex);
auto& state = m_states[id];
// A missing library is retriable (it may be installed while the
// shell runs); an ABI mismatch on every candidate is not. Cache
// successes and permanent failures; leave retriable misses out.
if (!state || (!state->lang && !state->bad)) {
std::shared_ptr<State> fresh = std::make_shared<State>();
bool abiMismatch = false;
for (size_t i = 0; i < grammar.libs.size(); ++i) {
void* lib = dlopen(grammar.libs[i].c_str(),
RTLD_NOW | RTLD_LOCAL);
if (!lib)
continue;
void* lib =
dlopen(grammar.libs[i].c_str(), RTLD_NOW | RTLD_LOCAL);
if (!lib) continue;
auto* symbol = reinterpret_cast<hl::LanguageFn>(
dlsym(lib, grammar.symbols[i].c_str()));
if (!symbol) {
@@ -266,7 +314,7 @@ QVariantList CodeHighlighter::doHighlight(
const TSLanguage* candidate = symbol();
const uint32_t version = ts_language_abi_version(candidate);
if (version < TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION ||
version > TREE_SITTER_LANGUAGE_VERSION) {
version > TREE_SITTER_LANGUAGE_VERSION) {
dlclose(lib);
abiMismatch = true;
continue;
@@ -276,7 +324,6 @@ QVariantList CodeHighlighter::doHighlight(
break;
}
if (fresh->lang) {
// Candidates in priority order; first that compiles wins.
for (const char* source : grammar.queries) {
TSQueryError errorType = TSQueryErrorNone;
uint32_t errorOffset = 0;
@@ -286,21 +333,17 @@ QVariantList CodeHighlighter::doHighlight(
static_cast<uint32_t>(std::strlen(source)),
&errorOffset,
&errorType);
if (!candidate)
continue;
if (!candidate) continue;
fresh->query = candidate;
break;
}
if (!fresh->query)
fresh->bad = true;
if (!fresh->query) fresh->bad = true;
} else if (abiMismatch) {
fresh->bad = true;
}
if (fresh->lang || fresh->bad)
state = std::move(fresh);
if (fresh->lang || fresh->bad) state = std::move(fresh);
}
if (!state || state->bad || !state->lang)
return spans;
if (!state || state->bad || !state->lang) return spans;
lang = static_cast<const TSLanguage*>(state->lang);
query = static_cast<TSQuery*>(state->query);
}
@@ -317,13 +360,9 @@ QVariantList CodeHighlighter::doHighlight(
TSQueryCursor* cursor = ts_query_cursor_new();
ts_query_cursor_exec(cursor, query, ts_tree_root_node(tree));
// Per-byte winner table: captures arrive in document order and later
// captures overwrite earlier ones (tree-sitter highlight convention).
const uint32_t size = static_cast<uint32_t>(utf8.size());
std::vector<uint8_t> kinds(size, 0);
// QML slices the code by UTF-16 code unit, so spans must be in code
// units, not bytes. cu[b] = code units before byte b.
std::vector<uint32_t> cu(size + 1, 0);
for (uint32_t b = 0; b < size; ++b) {
cu[b + 1] = cu[b];
@@ -333,9 +372,9 @@ QVariantList CodeHighlighter::doHighlight(
else if (c < 0xC0)
; // continuation byte
else if (c < 0xF0)
cu[b + 1] += 1; // 2/3-byte lead -> BMP -> one unit
cu[b + 1] += 1;
else
cu[b + 1] += 2; // 4-byte lead -> surrogate pair
cu[b + 1] += 2;
}
TSQueryMatch match;
@@ -343,14 +382,13 @@ QVariantList CodeHighlighter::doHighlight(
while (ts_query_cursor_next_capture(cursor, &match, &captureIndex)) {
const TSQueryCapture& capture = match.captures[captureIndex];
uint32_t nameLength = 0;
const char* name = ts_query_capture_name_for_id(query, capture.index, &nameLength);
const char* name =
ts_query_capture_name_for_id(query, capture.index, &nameLength);
const uint8_t role = roleFor(name, nameLength);
if (role == 0)
continue;
if (role == 0) continue;
const uint32_t start = ts_node_start_byte(capture.node);
const uint32_t end = ts_node_end_byte(capture.node);
if (end <= start || end > size)
continue;
if (end <= start || end > size) continue;
std::fill(kinds.begin() + start, kinds.begin() + end, role);
}
@@ -378,8 +416,7 @@ QVariantList CodeHighlighter::doHighlight(
}
CodeHighlighter* CodeHighlighter::create(QQmlEngine*, QJSEngine*) {
if (!s_instance)
s_instance = new CodeHighlighter();
if (!s_instance) s_instance = new CodeHighlighter();
return s_instance;
}