1use crate::decoration::{DecoratedExt as _, Decoration};
4use crate::stream::{ProgressState, StreamedContent};
5use crate::surface::CardFrameExt as _;
6use crate::theme::SemanticStyledExt as _;
7use gpui::{
8 App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
9 SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, div,
10 prelude::FluentBuilder as _,
11};
12use gpui_component::{
13 ActiveTheme as _, StyledExt as _, clipboard::Clipboard, h_flex, text::TextView, v_flex,
14};
15
16#[derive(IntoElement)]
30pub struct CodeBlock {
31 id: ElementId,
32 style: StyleRefinement,
33 decoration: Decoration,
34 code: SharedString,
35 language: Option<SharedString>,
36 streaming: bool,
37 failed: Option<SharedString>,
38 copyable: bool,
39}
40
41impl CodeBlock {
42 pub fn decoration(mut self, decoration: Decoration) -> Self {
48 self.decoration = decoration;
49 self
50 }
51
52 pub fn new(id: impl Into<ElementId>, code: impl Into<SharedString>) -> Self {
54 Self {
55 id: id.into(),
56 style: StyleRefinement::default(),
57 decoration: Decoration::default(),
58 code: code.into(),
59 language: None,
60 streaming: false,
61 failed: None,
62 copyable: true,
63 }
64 }
65
66 pub fn streamed(id: impl Into<ElementId>, content: &StreamedContent) -> Self {
69 let mut this = Self::new(id, content.text().to_string());
70 match content.state() {
71 ProgressState::Running => this.streaming = true,
72 ProgressState::Pending | ProgressState::Complete => {}
73 ProgressState::Failed(reason) => this.failed = Some(reason.clone()),
74 }
75 this
76 }
77
78 pub fn language(mut self, language: impl Into<SharedString>) -> Self {
80 self.language = Some(language.into());
81 self
82 }
83
84 pub fn not_copyable(mut self) -> Self {
86 self.copyable = false;
87 self
88 }
89}
90
91impl Styled for CodeBlock {
92 fn style(&mut self) -> &mut StyleRefinement {
93 &mut self.style
94 }
95}
96
97impl RenderOnce for CodeBlock {
98 fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
99 let tokens = cx.theme().semantic_tokens();
100 let mut decoration = std::mem::take(&mut self.decoration);
103 let language = self.language.unwrap_or_else(|| "text".into());
104
105 let longest_run = self
108 .code
109 .split(|c| c != '`')
110 .map(str::len)
111 .max()
112 .unwrap_or(0);
113 let fence = "`".repeat((longest_run + 1).max(3));
114 let cursor = if self.streaming { "▌" } else { "" };
115 let source = format!("{fence}{language}\n{}{cursor}\n{fence}", self.code);
116 let accessibility_label: SharedString = format!("{language} code").into();
117 let accessibility_description = self.failed.clone();
118
119 v_flex()
120 .id(self.id)
121 .role(Role::Code)
122 .aria_label(accessibility_label)
123 .when_some(accessibility_description, |this, description| {
124 this.aria_description(description)
125 })
126 .card_frame(cx)
127 .overflow_hidden()
128 .decoration_under(&mut decoration)
129 .child(
130 h_flex()
131 .items_center()
132 .justify_between()
133 .px(tokens.spacing.md)
134 .py(tokens.spacing.xs)
135 .bg(cx.theme().muted.opacity(0.35))
136 .border_b_1()
137 .border_color(cx.theme().border)
138 .child(
142 div()
143 .min_w_0()
144 .truncate()
145 .text_token(tokens.typography.xs)
146 .font_family(cx.theme().mono_font_family.clone())
147 .text_color(cx.theme().muted_foreground)
148 .child(language.clone()),
149 )
150 .when(self.copyable, |this| {
151 this.child(
152 div()
153 .flex_none()
154 .child(Clipboard::new("copy").value(self.code.clone())),
155 )
156 }),
157 )
158 .child(
159 div()
160 .px(tokens.spacing.md)
161 .py(tokens.spacing.sm)
162 .text_token(tokens.typography.sm)
163 .child(TextView::markdown("code", source).selectable(true)),
164 )
165 .when_some(self.failed, |this, reason| {
166 this.child(
167 div()
168 .px(tokens.spacing.md)
169 .py(tokens.spacing.xs)
170 .border_t_1()
171 .border_color(cx.theme().border)
172 .text_token(tokens.typography.xs)
173 .text_color(cx.theme().danger)
174 .child(reason),
175 )
176 })
177 .decoration_over(&mut decoration)
178 .refine_style(&self.style)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::stream::Progressive;
186
187 #[test]
188 fn maps_all_shared_lifecycle_states() {
189 assert!(
190 !CodeBlock::streamed("pending", &Progressive::pending("code".to_owned())).streaming
191 );
192 assert!(CodeBlock::streamed("running", &Progressive::running("code".to_owned())).streaming);
193 assert!(
194 CodeBlock::streamed("complete", &Progressive::complete("code".to_owned()))
195 .failed
196 .is_none()
197 );
198 assert_eq!(
199 CodeBlock::streamed("failed", &Progressive::failed("code".to_owned(), "stopped"))
200 .failed,
201 Some("stopped".into())
202 );
203 }
204}