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