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