Files
z-bar-qt/Modules/Notifications/Sidebar/Chat/Content/CodeBlockView.qml
T

158 lines
4.0 KiB
QML

import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Services
CustomClippingRect {
id: root
required property string language
required property string code
property bool copied: false
property color codeColor: Colors.palette.m3onSurfaceVariant
property color codeBackgroundColor: Colors.palette.m3surfaceContainerLow
property color codeHeaderColor: Colors.palette.m3outline
property color codeAccentColor: Colors.palette.m3primary
// Highlighter spans for the current code; refreshed when the code or
// its language changes.
property var codeSpans: []
function refresh() {
codeSpans = CodeHighlighter.highlight(root.code, root.language);
}
function roleColor(kind) {
switch (kind) {
case "comment":
return Colors.palette.m3outline;
case "string":
return Colors.palette.m3tertiary;
case "string.key":
return Colors.palette.m3secondary;
case "number":
case "constant":
return Colors.palette.m3tertiaryFixed;
case "keyword":
return root.codeAccentColor;
case "type":
return Colors.palette.m3secondary;
case "function":
case "method":
return Colors.palette.m3onSurface;
case "macro":
case "preproc":
return Colors.palette.m3secondaryContainer;
case "operator":
case "property":
return root.codeColor;
case "label":
case "attribute":
return Colors.palette.m3tertiaryFixedDim;
default:
return root.codeColor;
}
}
function escapeHtml(text) {
return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// Wraps the code in <font color> tags following the highlighter spans.
function highlightedHtml(code, spans) {
let out;
if (!spans.length) {
out = escapeHtml(code);
} else {
out = "";
let pos = 0;
for (let i = 0; i < spans.length; i++) {
const span = spans[i];
if (span.start > pos)
out += escapeHtml(code.slice(pos, span.start));
out += `<font color="${roleColor(span.kind)}">` + escapeHtml(code.slice(span.start, span.start + span.length)) + "</font>";
pos = span.start + span.length;
}
if (pos < code.length)
out += escapeHtml(code.slice(pos));
}
// RichText collapses HTML whitespace: break newlines, and protect
// line-leading indentation with nbsp (internal spaces stay normal
// so long lines can still wrap).
return out
.replace(/(^|\n)[ \t]+/g, ws => ws.replace(/[ \t]/g, "&nbsp;"))
.replace(/\n/g, "<br>");
}
implicitWidth: headerRow.implicitWidth + headerRow.anchors.leftMargin + headerRow.anchors.rightMargin
implicitHeight: headerRow.anchors.topMargin + headerRow.implicitHeight + codeText.implicitHeight + codeText.anchors.margins * 2
color: root.codeBackgroundColor
radius: Tokens.rounding.medium
onLanguageChanged: refresh()
onCodeChanged: refresh()
CustomText {
id: codeText
anchors.left: parent.left
anchors.top: headerRow.bottom
anchors.right: parent.right
anchors.margins: Tokens.padding.small
color: root.codeColor
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.small
textFormat: Text.RichText
text: root.highlightedHtml(root.code, root.codeSpans)
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
RowLayout {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Tokens.padding.small
anchors.leftMargin: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small
spacing: Tokens.spacing.small
CustomText {
Layout.alignment: Qt.AlignVCenter
color: root.codeHeaderColor
font.pointSize: Tokens.font.size.small
text: root.language
visible: root.language.length > 0
}
Item {
Layout.fillWidth: true
}
IconButton {
icon: root.copied ? "check" : "content_copy"
inactiveColor: "transparent"
inactiveOnColor: root.codeHeaderColor
type: IconButton.Text
onClicked: {
Quickshell.clipboardText = root.code;
root.copied = true;
copyResetTimer.restart();
}
Timer {
id: copyResetTimer
interval: 1500
onTriggered: root.copied = false
}
}
}
}