Skip to main content

markdown/render/
code.rs

1//! Code blocks, source mode, and the copy button.
2
3use super::*;
4
5/// Paint a document's own markdown source: a fence's caret, selection and hit
6/// testing, without a fence's box, band or copy button.
7///
8/// The caret is a [`Cursor`] at block 0 in [`Part::Code`] — what a document
9/// held as one fence answers to, which is how an editor holds its source.
10/// Wrapping is not optional here: a paragraph is one line of markdown, and a
11/// source view that scrolled sideways would hide most of it.
12pub 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        keep,
20        scroll,
21        ..
22    } = editing;
23    let theme = Theme::of(cx).clone();
24    let typography = typography.unwrap_or_else(|| Typography::of(cx));
25    let overlay = Overlay {
26        block: 0,
27        part: Part::Code,
28        selection,
29        caret_on,
30        layouts,
31        annotations,
32        placeholder: None,
33        caption: Caption::default(),
34        // The source view is one fence and holds no task block.
35        toggle: None,
36        // It paints no band, so there is nowhere for the button to float.
37        copy: CopyButton::Hidden,
38        base: None,
39        highlight: crate::marks::highlight_paint_of(cx),
40    };
41    let style = crate::SourceStyle::of(cx);
42    let count = code.split('\n').count();
43    let gutter = Gutter::new(&style, count, &typography, &theme);
44
45    let Some(layouts) = layouts else {
46        let (underlay, lines) = code_lines(
47            Some(crate::source::LANGUAGES[0]),
48            code,
49            overlay,
50            &typography,
51            &theme,
52            cx,
53        );
54        let lines = lines
55            .into_iter()
56            .enumerate()
57            .map(|(index, line)| gutter.row(index, line))
58            .collect();
59        return div()
60            .flex()
61            .flex_col()
62            .child(code_body(0, underlay, lines, &typography, true))
63            .into_any_element();
64    };
65
66    // Built a line at a time by the same column the blocks are: a line is to
67    // this text what a block is to a document.
68    let language = crate::source::LANGUAGES[0];
69    let spans: Option<Rc<[Span]>> = crate::highlight::spans(cx, Some(language), code)
70        .or_else(|| crate::source::is_markdown(language).then(|| crate::source::spans(code)))
71        .map(Into::into);
72    let mut ranges = Vec::with_capacity(count);
73    let mut offset = 0usize;
74    for line in code.split('\n') {
75        ranges.push(offset..offset + line.len());
76        offset += line.len() + 1;
77    }
78    let line_of = |at: usize| {
79        ranges
80            .partition_point(|range| range.end < at)
81            .min(count - 1)
82    };
83    let mut kept: Vec<usize> = keep.to_vec();
84    if let Some(selection) = selection {
85        kept.push(line_of(selection.head.offset));
86        kept.push(line_of(selection.anchor.offset));
87    }
88    let line = px(typography.code.line_height());
89    let keys: Vec<u64> = ranges
90        .iter()
91        .map(|range| {
92            let mut hasher = DefaultHasher::new();
93            "source".hash(&mut hasher);
94            code[range.clone()].hash(&mut hasher);
95            typography.code.size().to_bits().hash(&mut hasher);
96            hasher.finish()
97        })
98        .collect();
99    layouts.prune(&keys);
100    let indent = px(gutter.width() + 2.0 * CODE_PADDING_X);
101    let guesses: Vec<Guess> = ranges
102        .iter()
103        .map(|range| Guess {
104            chars: range.len(),
105            line,
106            rows: 0,
107            extra: px(0.0),
108            indent,
109        })
110        .collect();
111    let paint = RowPaint {
112        caret: overlay.caret_painted(),
113        selected: overlay.selected(code.len()),
114        annotated: overlay.annotated(code.len(), &theme),
115        caret_color: theme.caret,
116        selection_color: theme.selection,
117        code_size: typography.code.size(),
118    };
119    let code: Rc<str> = code.into();
120    let sink = layouts.clone();
121    let ranges: Rc<[Range<usize>]> = ranges.into();
122    let column = Column {
123        layouts: layouts.clone(),
124        keys: keys.into(),
125        gaps: vec![px(0.0); count].into(),
126        guesses: guesses.into(),
127        keep: kept,
128        scroll: scroll.cloned(),
129        build: Box::new(move |index, _, _| {
130            let span = ranges[index].clone();
131            let styled = code_line(&code[span.clone()], span.start, spans.as_deref(), &theme);
132            let layout = styled.layout().clone();
133            let (sink, paint) = (sink.clone(), paint.clone());
134            let underlay = canvas(
135                |_, _, _| (),
136                move |_, _, window, _| {
137                    sink.record(0, Part::Code, span.clone(), layout.clone());
138                    paint.paint(&span, &layout, window);
139                },
140            )
141            .absolute()
142            .size_full();
143            let text = div().relative().child(underlay).child(styled);
144            div()
145                .px(px(CODE_PADDING_X))
146                .child(gutter.row(index, text.into_any_element()))
147                .into_any_element()
148        }),
149    };
150    div()
151        .flex()
152        .flex_col()
153        .py(px(CODE_PADDING_Y))
154        .text_size(px(typography.code.size()))
155        .line_height(line)
156        .child(column)
157        .into_any_element()
158}
159
160/// The line numbers beside a source view, when it shows them.
161#[derive(Clone)]
162struct Gutter {
163    shown: bool,
164    width: f32,
165    gap: f32,
166    color: Hsla,
167    font: SharedString,
168}
169
170impl Gutter {
171    fn new(
172        style: &crate::SourceStyle,
173        lines: usize,
174        typography: &Typography,
175        theme: &Theme,
176    ) -> Self {
177        let digits = lines.to_string().len().max(style.gutter_min_digits);
178        let gap = style.gutter_gap.max(0.0) * typography.code.size();
179        Self {
180            shown: style.line_numbers,
181            width: digits as f32 * typography.code.size() + gap,
182            gap,
183            color: style.gutter_color.unwrap_or(theme.text_faint),
184            font: theme.font_mono.clone(),
185        }
186    }
187
188    fn width(&self) -> f32 {
189        if self.shown { self.width } else { 0.0 }
190    }
191
192    /// Keeps each number beside its source line, including wrapped and empty
193    /// lines.
194    fn row(&self, index: usize, line: AnyElement) -> AnyElement {
195        if !self.shown {
196            return line;
197        }
198        div()
199            .flex()
200            .items_start()
201            .child(
202                div()
203                    .w(px(self.width))
204                    .flex_shrink_0()
205                    .pr(px(self.gap))
206                    .font_family(self.font.clone())
207                    .text_color(self.color)
208                    .text_right()
209                    .child((index + 1).to_string()),
210            )
211            .child(div().flex_1().min_w_0().child(line))
212            .into_any_element()
213    }
214}
215
216/// The shaped lines of a fence, and the canvas that paints the caret, the
217/// selection and the annotations over them.
218pub(super) fn code_lines(
219    language: Option<&str>,
220    code: &str,
221    overlay: Overlay,
222    typography: &Typography,
223    theme: &Theme,
224    cx: &App,
225) -> (AnyElement, Vec<AnyElement>) {
226    let ix = overlay.block;
227    // Highlighting recolors runs only — layout does not move, so a build with
228    // no highlighter installed paints the same block in one plain run.
229    // Markdown is the one language this crate can colour on its own, which is
230    // what a source view is painted with where no highlighter reaches.
231    let spans = crate::highlight::spans(cx, language, code).or_else(|| {
232        language
233            .filter(|language| crate::source::is_markdown(language))
234            .map(|_| crate::source::spans(code))
235    });
236    // Each source line's own layout, with the slice of the code it covers —
237    // the caret and a click both resolve through these. A wrapped line is
238    // several rows of one layout, which is the case `range_rects` already
239    // walks for a paragraph.
240    let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
241    let mut offset = 0usize;
242    let lines: Vec<AnyElement> = code
243        .split('\n')
244        .map(|line| {
245            let start = offset;
246            offset += line.len() + 1;
247            let styled = code_line(line, start, spans.as_deref(), theme);
248            rows.push((start..start + line.len(), styled.layout().clone()));
249            styled.into_any_element()
250        })
251        .collect();
252
253    let caret = overlay.caret_painted();
254    let selected = overlay.selected(code.len());
255    let sink = overlay.layouts.cloned();
256    let code_size = typography.code.size();
257    let annotated = overlay.annotated(code.len(), theme);
258    let (caret_color, selection_color) = (theme.caret, theme.selection);
259    let paint = RowPaint {
260        caret,
261        selected,
262        annotated,
263        caret_color,
264        selection_color,
265        code_size,
266    };
267    let underlay = canvas(
268        |_, _, _| (),
269        move |_, _, window, _| {
270            for (span, layout) in &rows {
271                if let Some(sink) = &sink {
272                    sink.record(ix, Part::Code, span.clone(), layout.clone());
273                }
274                paint.paint(span, layout, window);
275            }
276        },
277    )
278    .absolute()
279    .size_full();
280
281    (underlay.into_any_element(), lines)
282}
283
284/// A highlighted byte range of a text.
285type Span = (Range<usize>, theme::HighlightKind);
286
287/// One line of code, coloured by the spans over the text it came from.
288///
289/// Runs are measured within the line; spans are byte ranges over the whole
290/// text, so every span is clipped to the line and rebased.
291fn code_line(line: &str, start: usize, spans: Option<&[Span]>, theme: &Theme) -> StyledText {
292    let mono = font(theme.font_mono.clone());
293    let run = |len: usize, color: Hsla| TextRun {
294        len,
295        font: mono.clone(),
296        color,
297        background_color: None,
298        underline: None,
299        strikethrough: None,
300    };
301    let mut runs = Vec::new();
302    let mut pos = 0usize;
303    if let Some(spans) = spans {
304        let end = start + line.len();
305        for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
306            let s = range.start.clamp(start, end) - start;
307            let e = range.end.min(end) - start;
308            if s > pos {
309                runs.push(run(s - pos, theme.text));
310            }
311            runs.push(run(e - s, theme.syntax.color(*kind)));
312            pos = e;
313        }
314    }
315    if pos < line.len() {
316        runs.push(run(line.len() - pos, theme.text));
317    }
318    if runs.is_empty() {
319        runs.push(run(0, theme.text));
320    }
321    StyledText::new(SharedString::from(line.to_string())).with_runs(runs)
322}
323
324/// What paints under a line of code: the annotations, the selection and the
325/// caret, each clipped to the slice of the text the line covers.
326#[derive(Clone)]
327struct RowPaint {
328    caret: Option<usize>,
329    selected: Option<Range<usize>>,
330    annotated: Vec<(Range<usize>, Hsla)>,
331    caret_color: Hsla,
332    selection_color: Hsla,
333    code_size: f32,
334}
335
336impl RowPaint {
337    fn paint(&self, span: &Range<usize>, layout: &TextLayout, window: &mut Window) {
338        let wash = |range: &Range<usize>, color: Hsla, window: &mut Window| {
339            let (from, to) = (range.start.max(span.start), range.end.min(span.end));
340            if from < to {
341                for rect in range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0) {
342                    window.paint_quad(quad(
343                        rect,
344                        px(2.0),
345                        color,
346                        px(0.0),
347                        gpui::transparent_black(),
348                        BorderStyle::default(),
349                    ));
350                }
351            }
352        };
353        for (range, color) in &self.annotated {
354            wash(range, *color, window);
355        }
356        if let Some(range) = &self.selected {
357            wash(range, self.selection_color, window);
358        }
359        if let Some(offset) = self.caret.filter(|at| span.contains(at) || *at == span.end)
360            && let Some(head) = layout.position_for_index(offset - span.start)
361        {
362            window.paint_quad(quad(
363                caret_quad(head, self.code_size, layout.line_height()),
364                px(0.0),
365                self.caret_color,
366                px(0.0),
367                gpui::transparent_black(),
368                BorderStyle::default(),
369            ));
370        }
371    }
372}
373
374pub(super) fn code_block(
375    language: Option<&str>,
376    code: &str,
377    overlay: Overlay,
378    typography: &Typography,
379    theme: &Theme,
380    window: &mut Window,
381    cx: &mut App,
382) -> AnyElement {
383    let ix = overlay.block;
384    let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
385    let body = code_body(ix, underlay, lines, typography, Layout::of(cx).wrap_code);
386
387    div()
388        .rounded(px(Theme::panel_radius()))
389        .bg(theme.ink(0.035))
390        .border_1()
391        .border_color(theme.border)
392        .overflow_hidden()
393        .relative()
394        // The band is unconditional: it is where the copy button already floats,
395        // and where a host puts its language control — which needs somewhere to
396        // sit on a block that has no language yet.
397        .child(
398            div()
399                .relative()
400                .flex()
401                .flex_row()
402                .items_center()
403                .px(px(CODE_PADDING_X))
404                .py(px(5.0))
405                .border_b_1()
406                .border_color(theme.border)
407                .bg(theme.ink(0.02))
408                .text_style(TextStyle::Subheadline)
409                .text_color(match language {
410                    Some(_) => theme.text_muted,
411                    None => theme.text_faint,
412                })
413                // The label's own box, not the band's: a host hanging a picker
414                // here wants it around the word, and only the word knows how
415                // wide the word is.
416                .child(
417                    div()
418                        .relative()
419                        .children(overlay.layouts.map(|layouts| {
420                            let layouts = layouts.clone();
421                            canvas(
422                                move |bounds, _, _| layouts.record_language(ix, bounds),
423                                |_, _, _, _| (),
424                            )
425                            .absolute()
426                            .size_full()
427                        }))
428                        .child(SharedString::from(
429                            language.unwrap_or(PLAIN_LANGUAGE).to_string(),
430                        )),
431                ),
432        )
433        .child(body)
434        .children(
435            (overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
436        )
437        .into_any_element()
438}
439
440/// The lines of a fence, wrapped to the block or scrolling sideways under it.
441pub(super) fn code_body(
442    ix: usize,
443    underlay: AnyElement,
444    lines: Vec<AnyElement>,
445    typography: &Typography,
446    wrap: bool,
447) -> AnyElement {
448    let column = div()
449        .flex()
450        .flex_col()
451        .px(px(CODE_PADDING_X))
452        .children(lines);
453    let body = div()
454        .id(ElementId::named_usize("md-code", ix))
455        .relative()
456        .py(px(CODE_PADDING_Y))
457        .text_size(px(typography.code.size()))
458        .line_height(px(typography.code.line_height()))
459        .child(underlay);
460    if wrap {
461        // The column is the block's width here rather than its widest line's,
462        // which is what gives the text something to wrap against.
463        body.child(column.w_full()).into_any_element()
464    } else {
465        ui::scroll::Viewport::new(
466            format!("md-code-scroll-{ix}"),
467            body.flex()
468                .flex_row()
469                .whitespace_nowrap()
470                // The padding belongs to the lines, not to the scroller: a scroll
471                // container's trailing padding is not part of what it will scroll
472                // to, so the last characters of a long line sit behind the right
473                // edge with nowhere left to go. As a row's only item this column is
474                // sized by its widest line, and the padding rides along inside that
475                // width.
476                .child(column.items_start()),
477            gpui::Axis::Horizontal,
478        )
479        .into_any_element()
480    }
481}
482
483/// A copy button that owns its own feedback.
484///
485/// The state is the element's, not the caller's: a component library cannot ask
486/// every host to thread a handler and a "which block is showing Copied" index
487/// through its render tree just to put a button on a code block. It resets when
488/// the pointer leaves, which needs no clock.
489pub(super) fn copy_button(
490    code: &str,
491    ix: usize,
492    theme: &Theme,
493    window: &mut Window,
494    cx: &mut App,
495) -> AnyElement {
496    let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
497    let showing = *copied.read(cx);
498    let text: SharedString = code.to_string().into();
499
500    div()
501        .id(ElementId::named_usize("md-copy", ix))
502        .absolute()
503        .top(px(3.0))
504        .right(px(5.0))
505        .h(px(20.0))
506        .px(px(6.0))
507        .rounded(px(5.0))
508        .flex()
509        .items_center()
510        .cursor_pointer()
511        .text_style(TextStyle::Caption)
512        .text_color(theme.text_muted)
513        .hover(|el| el.bg(theme.element_hover))
514        .child(if showing { "Copied" } else { "Copy" })
515        .on_click({
516            let copied = copied.clone();
517            move |_, _, cx| {
518                cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
519                copied.update(cx, |state, cx| {
520                    *state = true;
521                    cx.notify();
522                });
523            }
524        })
525        .on_hover(move |hovering, _, cx| {
526            if !*hovering && *copied.read(cx) {
527                copied.update(cx, |state, cx| {
528                    *state = false;
529                    cx.notify();
530                });
531            }
532        })
533        .into_any_element()
534}