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 string html: ""
property bool colored: false
property string requestedCode: ""
property string requestedLanguage: ""
property int highlightToken: 0
function refresh() {
requestedCode = root.code;
requestedLanguage = root.language;
const token = ++root.highlightToken;
CodeHighlighter.highlight(root.code, root.language, root, token);
}
function onHighlightSpans(token, spans) {
if (token !== root.highlightToken)
return;
if (root.code !== root.requestedCode || root.language !== root.requestedLanguage)
return;
html = spans.length ? root.highlightedHtml(root.code, spans) : "";
colored = root.visible && html.length > 0;
}
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, ">");
}
// Mirrors the whitespace rules of the rich text rendering below (leading
// whitespace kept, mid-line runs collapsed, tabs as single spaces) so the
// plain and colored layouts are identical.
function plainForDisplay(code): string {
return code.split("\n").map(line => {
const i = line.search(/\S/);
if (i < 0)
return "";
const lead = line.slice(0, i).replace(/\t/g, " ");
return lead + line.slice(i).replace(/\s+/g, " ").replace(/\s+$/, "");
}).join("\n");
}
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: {
html = "";
colored = false;
if (root.visible)
refreshTimer.restart();
}
onCodeChanged: {
colored = false;
if (root.visible)
refreshTimer.restart();
}
onVisibleChanged: if (root.visible)
refreshTimer.restart()
Timer {
id: refreshTimer
interval: 200
repeat: false
onTriggered: 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: Tokens.font.label.builders.small.capitalization(Font.Capitalize).build()
text: CodeIcons.name(root.language)
}
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
}
}
}
CustomRect {
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
clip: true
Flickable {
id: codeFlick
anchors.fill: parent
anchors.margins: Tokens.padding.small
boundsBehavior: Flickable.StopAtBounds
contentWidth: codeText.implicitWidth
CustomScrollBar.horizontal: CustomScrollBar {
flickable: codeFlick
parent: codeFlick.parent
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
}
CustomText {
id: codeText
color: CodeColors.active.normal
font: Tokens.font.mono.small
textFormat: Text.RichText
text: root.colored ? root.html : root.plainForDisplay(root.code)
}
}
}
}