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