1use super::*;
4
5pub fn render_source(code: &str, editing: Editing, cx: &mut App) -> AnyElement {
13 let Editing {
14 selection,
15 caret_on,
16 layouts,
17 annotations,
18 typography,
19 ..
20 } = editing;
21 let reset = layouts.map(|layouts| {
25 let layouts = layouts.clone();
26 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
27 .absolute()
28 .size(px(0.0))
29 });
30 let theme = Theme::of(cx).clone();
31 let typography = typography.unwrap_or_else(|| Typography::of(cx));
32 let overlay = Overlay {
33 block: 0,
34 part: Part::Code,
35 selection,
36 caret_on,
37 layouts,
38 annotations,
39 placeholder: None,
40 caption: Caption::default(),
41 toggle: None,
43 copy: CopyButton::Hidden,
45 base: None,
46 highlight: crate::marks::highlight_paint_of(cx),
47 };
48 let (underlay, lines) = code_lines(
49 Some(crate::source::LANGUAGES[0]),
50 code,
51 overlay,
52 &typography,
53 &theme,
54 cx,
55 );
56 let style = crate::SourceStyle::of(cx);
58 let digits = lines.len().to_string().len().max(style.gutter_min_digits);
59 let gap = style.gutter_gap.max(0.0) * typography.code.size();
60 let gutter_width = digits as f32 * typography.code.size() + gap;
61 let lines = lines
62 .into_iter()
63 .enumerate()
64 .map(|(index, line)| {
65 if !style.line_numbers {
66 return line;
67 }
68 div()
69 .flex()
70 .items_start()
71 .child(
72 div()
73 .w(px(gutter_width))
74 .flex_shrink_0()
75 .pr(px(gap))
76 .font_family(theme.font_mono.clone())
77 .text_color(style.gutter_color.unwrap_or(theme.text_faint))
78 .text_right()
79 .child((index + 1).to_string()),
80 )
81 .child(div().flex_1().min_w_0().child(line))
82 .into_any_element()
83 })
84 .collect();
85 div()
86 .flex()
87 .flex_col()
88 .children(reset)
89 .child(code_body(0, underlay, lines, &typography, true))
90 .into_any_element()
91}
92
93pub(super) fn code_lines(
96 language: Option<&str>,
97 code: &str,
98 overlay: Overlay,
99 typography: &Typography,
100 theme: &Theme,
101 cx: &App,
102) -> (AnyElement, Vec<AnyElement>) {
103 let ix = overlay.block;
104 let spans = crate::highlight::spans(cx, language, code).or_else(|| {
109 language
110 .filter(|language| crate::source::is_markdown(language))
111 .map(|_| crate::source::spans(code))
112 });
113 let mono = font(theme.font_mono.clone());
114 let run = |len: usize, color: Hsla| TextRun {
115 len,
116 font: mono.clone(),
117 color,
118 background_color: None,
119 underline: None,
120 strikethrough: None,
121 };
122 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
127 let mut offset = 0usize;
128 let lines: Vec<AnyElement> = code
129 .split('\n')
130 .map(|line| {
131 let start = offset;
132 offset += line.len() + 1;
133 let mut runs = Vec::new();
134 let mut pos = 0usize;
137 if let Some(spans) = &spans {
138 let end = start + line.len();
139 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
140 let s = range.start.clamp(start, end) - start;
141 let e = range.end.min(end) - start;
142 if s > pos {
143 runs.push(run(s - pos, theme.text));
144 }
145 runs.push(run(e - s, theme.syntax.color(*kind)));
146 pos = e;
147 }
148 }
149 if pos < line.len() {
150 runs.push(run(line.len() - pos, theme.text));
151 }
152 if runs.is_empty() {
153 runs.push(run(0, theme.text));
154 }
155 let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
156 rows.push((start..start + line.len(), styled.layout().clone()));
157 styled.into_any_element()
158 })
159 .collect();
160
161 let caret = overlay.caret_painted();
162 let selected = overlay.selected(code.len());
163 let sink = overlay.layouts.cloned();
164 let code_size = typography.code.size();
165 let annotated = overlay.annotated(code.len(), theme);
166 let (caret_color, selection_color) = (theme.caret, theme.selection);
167 let underlay = canvas(
168 |_, _, _| (),
169 move |_, _, window, _| {
170 for (span, layout) in &rows {
171 if let Some(sink) = &sink {
172 sink.record(ix, Part::Code, span.clone(), layout.clone());
173 }
174 for (range, wash) in &annotated {
175 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
176 if from < to {
177 for rect in
178 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
179 {
180 window.paint_quad(quad(
181 rect,
182 px(2.0),
183 *wash,
184 px(0.0),
185 gpui::transparent_black(),
186 BorderStyle::default(),
187 ));
188 }
189 }
190 }
191 if let Some(range) = &selected {
192 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
193 if from < to {
194 for rect in
195 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
196 {
197 window.paint_quad(quad(
198 rect,
199 px(2.0),
200 selection_color,
201 px(0.0),
202 gpui::transparent_black(),
203 BorderStyle::default(),
204 ));
205 }
206 }
207 }
208 if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
209 && let Some(head) = layout.position_for_index(offset - span.start)
210 {
211 window.paint_quad(quad(
212 caret_quad(head, code_size, layout.line_height()),
213 px(0.0),
214 caret_color,
215 px(0.0),
216 gpui::transparent_black(),
217 BorderStyle::default(),
218 ));
219 }
220 }
221 },
222 )
223 .absolute()
224 .size_full();
225
226 (underlay.into_any_element(), lines)
227}
228
229pub(super) fn code_block(
230 language: Option<&str>,
231 code: &str,
232 overlay: Overlay,
233 typography: &Typography,
234 theme: &Theme,
235 window: &mut Window,
236 cx: &mut App,
237) -> AnyElement {
238 let ix = overlay.block;
239 let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
240 let body = code_body(ix, underlay, lines, typography, Layout::of(cx).wrap_code);
241
242 div()
243 .rounded(px(Theme::panel_radius()))
244 .bg(theme.ink(0.035))
245 .border_1()
246 .border_color(theme.border)
247 .overflow_hidden()
248 .relative()
249 .child(
253 div()
254 .relative()
255 .flex()
256 .flex_row()
257 .items_center()
258 .px(px(CODE_PADDING_X))
259 .py(px(5.0))
260 .border_b_1()
261 .border_color(theme.border)
262 .bg(theme.ink(0.02))
263 .text_style(TextStyle::Subheadline)
264 .text_color(match language {
265 Some(_) => theme.text_muted,
266 None => theme.text_faint,
267 })
268 .child(
272 div()
273 .relative()
274 .children(overlay.layouts.map(|layouts| {
275 let layouts = layouts.clone();
276 canvas(
277 move |bounds, _, _| layouts.record_language(ix, bounds),
278 |_, _, _, _| (),
279 )
280 .absolute()
281 .size_full()
282 }))
283 .child(SharedString::from(
284 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
285 )),
286 ),
287 )
288 .child(body)
289 .children(
290 (overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
291 )
292 .into_any_element()
293}
294
295pub(super) fn code_body(
297 ix: usize,
298 underlay: AnyElement,
299 lines: Vec<AnyElement>,
300 typography: &Typography,
301 wrap: bool,
302) -> AnyElement {
303 let column = div()
304 .flex()
305 .flex_col()
306 .px(px(CODE_PADDING_X))
307 .children(lines);
308 let body = div()
309 .id(ElementId::named_usize("md-code", ix))
310 .relative()
311 .py(px(CODE_PADDING_Y))
312 .text_size(px(typography.code.size()))
313 .line_height(px(typography.code.line_height()))
314 .child(underlay);
315 if wrap {
316 body.child(column.w_full()).into_any_element()
319 } else {
320 ui::scroll::Viewport::new(
321 format!("md-code-scroll-{ix}"),
322 body.flex()
323 .flex_row()
324 .whitespace_nowrap()
325 .child(column.items_start()),
332 gpui::Axis::Horizontal,
333 )
334 .into_any_element()
335 }
336}
337
338pub(super) fn copy_button(
345 code: &str,
346 ix: usize,
347 theme: &Theme,
348 window: &mut Window,
349 cx: &mut App,
350) -> AnyElement {
351 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
352 let showing = *copied.read(cx);
353 let text: SharedString = code.to_string().into();
354
355 div()
356 .id(ElementId::named_usize("md-copy", ix))
357 .absolute()
358 .top(px(3.0))
359 .right(px(5.0))
360 .h(px(20.0))
361 .px(px(6.0))
362 .rounded(px(5.0))
363 .flex()
364 .items_center()
365 .cursor_pointer()
366 .text_style(TextStyle::Caption)
367 .text_color(theme.text_muted)
368 .hover(|el| el.bg(theme.element_hover))
369 .child(if showing { "Copied" } else { "Copy" })
370 .on_click({
371 let copied = copied.clone();
372 move |_, _, cx| {
373 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
374 copied.update(cx, |state, cx| {
375 *state = true;
376 cx.notify();
377 });
378 }
379 })
380 .on_hover(move |hovering, _, cx| {
381 if !*hovering && *copied.read(cx) {
382 copied.update(cx, |state, cx| {
383 *state = false;
384 cx.notify();
385 });
386 }
387 })
388 .into_any_element()
389}