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