import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Services
CustomRect {
id: root
required property string language
required property string code
property bool copied: false
property color codeBackgroundColor: Colors.palette.m3surfaceContainerHigh
property color codeHeaderColor: Colors.palette.m3outline
property var codeSpans: []
property int highlightToken: 0
function refresh() {
const token = ++root.highlightToken;
CodeHighlighter.highlight(root.code, root.language, root, token);
}
function onHighlightSpans(token, spans) {
if (token !== root.highlightToken)
return;
codeSpans = spans;
}
function roleColor(kind) {
var s = CodeColors.active;
switch (kind) {
case "comment":
return s.comment;
case "string":
return s.string;
case "string.key":
return s.stringKey;
case "number":
case "constant":
return s.number;
case "keyword":
return s.keyword;
case "type":
return s.type;
case "function":
return s.functions;
case "method":
return s.method ?? s.functions;
case "macro":
return s.macro;
case "preproc":
return s.preproc ?? s.macro;
case "operator":
return s.operator ?? s.normal;
case "property":
return s.property ?? s.normal;
case "label":
return s.label;
case "attribute":
return s.attribute ?? s.label;
default:
return s.normal;
}
}
function escapeHtml(text): string {
return text.replace(/&/g, "&").replace(//g, ">");
}
function highlightedHtml(code, spans): string {
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];
const start = Math.min(span.start, code.length);
const end = Math.min(span.start + span.length, code.length);
if (end <= pos)
continue;
if (start > pos)
out += escapeHtml(code.slice(pos, start));
out += `` + escapeHtml(code.slice(start, end)) + "";
pos = end;
}
if (pos < code.length)
out += escapeHtml(code.slice(pos));
}
return out.replace(/(^|\n)[ \t]+/g, ws => ws.replace(/[ \t]/g, " ")).replace(/\n/g, "
");
}
implicitWidth: headerRow.implicitWidth + headerRow.anchors.leftMargin + headerRow.anchors.rightMargin
implicitHeight: headerRow.anchors.topMargin + headerRow.implicitHeight + codeRect.implicitHeight + codeRect.anchors.margins + codeRect.anchors.topMargin
color: root.codeBackgroundColor
radius: Tokens.rounding.medium
onLanguageChanged: {
codeSpans = [];
refresh();
}
onCodeChanged: refresh()
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
Item {
id: iconItem
readonly property string iconPath: CodeIcons.path(root.language)
Layout.fillHeight: true
implicitWidth: height
Shape {
id: shape
anchors.centerIn: parent
visible: iconItem.iconPath !== ""
preferredRendererType: Shape.CurveRenderer
scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width)
ShapePath {
strokeColor: "transparent"
fillColor: Colors.palette.m3tertiary
fillRule: ShapePath.WindingFill
PathSvg {
path: iconItem.iconPath
}
}
}
Shape {
id: fallbackShape
anchors.centerIn: parent
anchors.verticalCenterOffset: -1
visible: iconItem.iconPath === ""
preferredRendererType: Shape.CurveRenderer
scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width)
ShapePath {
strokeColor: Colors.palette.m3tertiary
strokeWidth: 16
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathSvg {
path: "M 40 64 L 112 128 L 40 192"
}
}
ShapePath {
strokeColor: Colors.palette.m3tertiary
strokeWidth: 16
capStyle: ShapePath.RoundCap
fillColor: "transparent"
PathSvg {
path: "M 120 192 L 216 192"
}
}
}
}
CustomText {
id: langText
Layout.alignment: Qt.AlignVCenter
color: root.codeHeaderColor
font.pointSize: Tokens.font.size.small
text: CodeIcons.name(root.language)
visible: root.language.length > 0
}
Item {
Layout.fillWidth: true
}
IconButton {
icon: root.copied ? "check" : "content_copy"
inactiveColor: "transparent"
inactiveOnColor: root.codeHeaderColor
label.animate: true
type: IconButton.Text
onClicked: {
Quickshell.clipboardText = root.code;
root.copied = true;
copyResetTimer.restart();
}
Timer {
id: copyResetTimer
interval: 1500
onTriggered: root.copied = false
}
}
}
CustomClippingRect {
id: codeRect
anchors.left: parent.left
anchors.top: headerRow.bottom
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Tokens.padding.small
radius: root.radius - anchors.margins
anchors.margins: Tokens.padding.extraSmall
implicitWidth: codeText.implicitWidth + codeFlick.anchors.margins * 2
implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2
color: CodeColors.active.bg
CustomText {
id: code
anchors.top: parent.top
anchors.bottom: parent.bottom
x: implicitWidth * (0 - codeFlick.visibleArea.xPosition) + Tokens.padding.small
anchors.margins: Tokens.padding.small
text: root.highlightedHtml(root.code, root.codeSpans)
textFormat: Text.RichText
color: CodeColors.active.normal
layer.enabled: true
clip: false
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.small
}
Flickable {
id: codeFlick
anchors.fill: parent
anchors.margins: Tokens.padding.small
CustomScrollBar.horizontal: CustomScrollBar {
flickable: codeFlick
parent: codeFlick.parent
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
}
TextAreaBase.flickable: TextAreaBase {
id: codeText
color: CodeColors.active.normal
layer.enabled: true
leftInset: Tokens.padding.small
clip: false
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.small
textFormat: Text.RichText
text: code.text
readOnly: true
}
}
}
}