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