158 lines
4.0 KiB
QML
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
|
|
// 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, " "))
|
|
.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
|
|
}
|
|
}
|
|
}
|
|
}
|