251 lines
5.9 KiB
QML
251 lines
5.9 KiB
QML
import QtQuick
|
|
import QtQuick.Shapes
|
|
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 codeBackgroundColor: Colors.palette.m3surfaceContainerHigh
|
|
property color codeHeaderColor: Colors.palette.m3outline
|
|
|
|
// Highlighter spans for the current code; refreshed when the code or
|
|
// its language changes. Highlighting runs off the GUI thread; the
|
|
// token drops results that arrive after the code already changed.
|
|
property var codeSpans: []
|
|
property int highlightToken: 0
|
|
|
|
function refresh() {
|
|
const token = ++root.highlightToken;
|
|
codeSpans = [];
|
|
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, "<").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];
|
|
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));
|
|
}
|
|
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 + codeRect.implicitHeight + codeRect.anchors.margins + codeRect.anchors.topMargin
|
|
color: root.codeBackgroundColor
|
|
radius: Tokens.rounding.medium
|
|
|
|
onLanguageChanged: 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
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
Flickable {
|
|
id: codeFlick
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: Tokens.padding.small
|
|
|
|
CustomScrollBar.horizontal: CustomScrollBar {
|
|
flickable: codeFlick
|
|
}
|
|
TextAreaBase.flickable: TextAreaBase {
|
|
id: codeText
|
|
|
|
color: CodeColors.active.normal
|
|
font.family: Config.appearance.font.family.mono
|
|
font.pointSize: Tokens.font.size.small
|
|
textFormat: Text.RichText
|
|
text: root.highlightedHtml(root.code, root.codeSpans)
|
|
readOnly: true
|
|
}
|
|
}
|
|
}
|
|
}
|