Skip to main content

markdown/render/
code.rs

1//! Code blocks, source mode, and the copy button.
2
3use super::*;
4
5/// A source text and what [`render_source`] reads off it every frame: its
6/// lines, a hash of each, and its highlight spans.
7///
8/// [`SourceText::new`] walks and highlights the whole text.
9/// [`SourceText::edit`] redoes only the lines an edit touched and shifts the
10/// spans around it; the [`SpanJob`] it answers highlights the new text in full,
11/// on any thread, for [`SourceText::set_spans`].
12pub struct SourceText {
13    code: Arc<str>,
14    ranges: Rc<[Range<usize>]>,
15    hashes: Rc<[u64]>,
16    spans: Option<Rc<[Span]>>,
17    /// Moved by every edit. A [`SpanJob`] carries the one it was taken at.
18    generation: u64,
19    sized: RefCell<Option<Sized>>,
20}
21
22/// The per-line tables that also depend on what the text is set at.
23#[derive(Clone)]
24struct Sized {
25    /// The text size, line height and indent these were built for.
26    at: (u32, Pixels, Pixels),
27    keys: Rc<[u64]>,
28    gaps: Rc<[Pixels]>,
29    guesses: Rc<[Guess]>,
30}
31
32/// Highlighting a [`SourceText`] in full. `Send`, so it can run off the thread
33/// that paints.
34pub struct SpanJob {
35    code: Arc<str>,
36    highlighter: Option<crate::Highlighter>,
37    generation: u64,
38}
39
40/// What a [`SpanJob`] found, for [`SourceText::set_spans`].
41pub struct Spans {
42    spans: Option<Vec<Span>>,
43    generation: u64,
44}
45
46impl SpanJob {
47    pub fn run(self) -> Spans {
48        Spans {
49            spans: highlight_source(self.highlighter, &self.code),
50            generation: self.generation,
51        }
52    }
53}
54
55fn highlight_source(highlighter: Option<crate::Highlighter>, code: &str) -> Option<Vec<Span>> {
56    let language = crate::source::LANGUAGES[0];
57    highlighter
58        .and_then(|highlighter| highlighter(language, code))
59        .or_else(|| crate::source::is_markdown(language).then(|| crate::source::spans(code)))
60}
61
62impl SourceText {
63    pub fn new(code: impl Into<Arc<str>>, cx: &App) -> Self {
64        let code: Arc<str> = code.into();
65        let spans = highlight_source(crate::highlight::installed(cx), &code).map(Into::into);
66        let ranges = split_lines(&code, 0..code.len());
67        let hashes = ranges.iter().map(|range| hash_line(&code, range)).collect();
68        Self {
69            code,
70            ranges: ranges.into(),
71            hashes,
72            spans,
73            generation: 0,
74            sized: RefCell::new(None),
75        }
76    }
77
78    pub fn text(&self) -> &str {
79        &self.code
80    }
81
82    /// Lines, counting the empty one after a trailing newline.
83    pub fn lines(&self) -> usize {
84        self.ranges.len()
85    }
86
87    /// The line holding byte `offset`; past the end, the last line.
88    pub fn line_of(&self, offset: usize) -> usize {
89        self.ranges
90            .partition_point(|range| range.end < offset)
91            .min(self.ranges.len() - 1)
92    }
93
94    /// The highlight spans, in bytes and in order. Between an [`Self::edit`]
95    /// and the [`Self::set_spans`] that follows it, the spans from before the
96    /// edit, shifted past it, and none over the bytes it replaced.
97    pub fn spans(&self) -> Option<&[(Range<usize>, theme::HighlightKind)]> {
98        self.spans.as_deref()
99    }
100
101    /// Make the text `code`. The change is taken to be the one run of bytes
102    /// between the longest prefix and suffix the two texts share. `None` when
103    /// the text is unchanged.
104    pub fn edit(&mut self, code: &str, cx: &App) -> Option<SpanJob> {
105        let old = self.code.as_bytes();
106        let new = code.as_bytes();
107        let prefix = old.iter().zip(new).take_while(|(a, b)| a == b).count();
108        if prefix == old.len() && prefix == new.len() {
109            return None;
110        }
111        let room = old.len().min(new.len()) - prefix;
112        let suffix = old
113            .iter()
114            .rev()
115            .zip(new.iter().rev())
116            .take(room)
117            .take_while(|(a, b)| a == b)
118            .count();
119        let old_end = old.len() - suffix;
120        let delta = (new.len() - suffix) as isize - old_end as isize;
121        let shift = |at: usize| at.wrapping_add_signed(delta);
122
123        let first = self.line_of(prefix);
124        let last = self.line_of(old_end);
125        let code: Arc<str> = code.into();
126        let lines = split_lines(
127            &code,
128            self.ranges[first].start..shift(self.ranges[last].end),
129        );
130        let hashes: Vec<u64> = lines.iter().map(|range| hash_line(&code, range)).collect();
131        let cut = first..last + 1;
132
133        if let Some(sized) = self.sized.get_mut() {
134            let (size, line, indent) = sized.at;
135            sized.keys = splice(
136                &sized.keys,
137                cut.clone(),
138                hashes.iter().map(|hash| key(*hash, size)),
139            );
140            sized.guesses = splice(
141                &sized.guesses,
142                cut.clone(),
143                lines.iter().map(|range| guess(range, line, indent)),
144            );
145            sized.gaps = vec![px(0.0); sized.keys.len()].into();
146        }
147        self.hashes = splice(&self.hashes, cut.clone(), hashes);
148        self.ranges = self.ranges[..first]
149            .iter()
150            .cloned()
151            .chain(lines)
152            .chain(
153                self.ranges[last + 1..]
154                    .iter()
155                    .map(|range| shift(range.start)..shift(range.end)),
156            )
157            .collect();
158        self.spans = self.spans.as_ref().map(|spans| {
159            let mut kept = Vec::with_capacity(spans.len());
160            for (range, kind) in spans.iter() {
161                if range.start < prefix {
162                    kept.push((range.start..range.end.min(prefix), *kind));
163                }
164                if range.end > old_end {
165                    kept.push((shift(range.start.max(old_end))..shift(range.end), *kind));
166                }
167            }
168            kept.into()
169        });
170        self.code = code;
171        self.generation += 1;
172        Some(SpanJob {
173            code: self.code.clone(),
174            highlighter: crate::highlight::installed(cx),
175            generation: self.generation,
176        })
177    }
178
179    /// Take what a [`SpanJob`] found. `false`, and nothing taken, when the text
180    /// has been edited since the job was made.
181    pub fn set_spans(&mut self, spans: Spans) -> bool {
182        if spans.generation != self.generation {
183            return false;
184        }
185        self.spans = spans.spans.map(Into::into);
186        true
187    }
188
189    fn sized(&self, size: f32, line: Pixels, indent: Pixels) -> Sized {
190        let at = (size.to_bits(), line, indent);
191        let mut held = self.sized.borrow_mut();
192        if let Some(sized) = held.as_ref()
193            && sized.at == at
194        {
195            return sized.clone();
196        }
197        let sized = Sized {
198            at,
199            keys: self.hashes.iter().map(|hash| key(*hash, at.0)).collect(),
200            gaps: vec![px(0.0); self.ranges.len()].into(),
201            guesses: self
202                .ranges
203                .iter()
204                .map(|range| guess(range, line, indent))
205                .collect(),
206        };
207        *held = Some(sized.clone());
208        sized
209    }
210}
211
212/// The lines of `code` within `within`, which starts at a line's start and
213/// ends at a line's end.
214fn split_lines(code: &str, within: Range<usize>) -> Vec<Range<usize>> {
215    let mut ranges = Vec::new();
216    let mut at = within.start;
217    for line in code[within].split('\n') {
218        ranges.push(at..at + line.len());
219        at += line.len() + 1;
220    }
221    ranges
222}
223
224fn hash_line(code: &str, range: &Range<usize>) -> u64 {
225    let mut hasher = DefaultHasher::new();
226    "source".hash(&mut hasher);
227    code[range.clone()].hash(&mut hasher);
228    hasher.finish()
229}
230
231fn key(hash: u64, size: u32) -> u64 {
232    let mut hasher = DefaultHasher::new();
233    hash.hash(&mut hasher);
234    size.hash(&mut hasher);
235    hasher.finish()
236}
237
238fn guess(range: &Range<usize>, line: Pixels, indent: Pixels) -> Guess {
239    Guess {
240        chars: range.len(),
241        line,
242        rows: 0,
243        extra: px(0.0),
244        indent,
245    }
246}
247
248/// `old` with `cut` replaced by `new`.
249fn splice<T: Clone>(old: &[T], cut: Range<usize>, new: impl IntoIterator<Item = T>) -> Rc<[T]> {
250    old[..cut.start]
251        .iter()
252        .cloned()
253        .chain(new)
254        .chain(old[cut.end..].iter().cloned())
255        .collect()
256}
257
258/// Paint a document's own markdown source: a fence's caret, selection and hit
259/// testing, without a fence's box, band or copy button.
260///
261/// The caret is a [`Cursor`] at block 0 in [`Part::Code`] — what a document
262/// held as one fence answers to, which is how an editor holds its source.
263/// Wrapping is not optional here: a paragraph is one line of markdown, and a
264/// source view that scrolled sideways would hide most of it.
265///
266/// Set at [`Typography::body`]'s size and leading, in [`Theme::font_body`].
267pub fn render_source(source: &SourceText, editing: Editing, cx: &mut App) -> AnyElement {
268    let code: &str = &source.code;
269    let Editing {
270        selection,
271        caret_on,
272        layouts,
273        annotations,
274        typography,
275        keep,
276        scroll,
277        ..
278    } = editing;
279    let mut theme = Theme::of(cx).clone();
280    theme.font_mono = theme.font_body.clone();
281    let mut typography = typography.unwrap_or_else(|| cx.typography());
282    typography.code = typography.body;
283    let overlay = Overlay {
284        block: 0,
285        part: Part::Code,
286        selection,
287        caret_on,
288        caret_shape: cx.caret_shape(),
289        caret_height: cx.caret_height(),
290        caret_inactive: cx.inactive_caret(),
291        // No window reaches here; a source view's rows read their own below.
292        window_active: cx.active_window().is_some(),
293        layouts,
294        annotations,
295        placeholder: None,
296        caption: Caption::default(),
297        // The source view is one fence and holds no task block.
298        toggle: None,
299        image: None,
300        image_overlay: None,
301        image_overlay_corner: ImageOverlayCorner::BottomRight,
302        fence: None,
303        sizing: None,
304        table_controls: false,
305        // It paints no band, so there is nowhere for the button to float.
306        copy: CopyButton::Hidden,
307        base: None,
308        highlight: crate::marks::highlight_paint_of(cx),
309        find: crate::find::find_paint_of(cx),
310        jump: None,
311    };
312    let style = cx.source_style();
313    let count = source.ranges.len();
314    let gutter = Gutter::new(&style, count, &typography, &theme);
315
316    let Some(layouts) = layouts else {
317        let (underlay, lines) = code_lines(
318            Some(crate::source::LANGUAGES[0]),
319            code,
320            overlay,
321            &typography,
322            &theme,
323            cx,
324        );
325        let lines = lines
326            .into_iter()
327            .enumerate()
328            .map(|(index, line)| gutter.row(index, line))
329            .collect();
330        return div()
331            .flex()
332            .flex_col()
333            .child(code_body(0, underlay, lines, &typography, true))
334            .into_any_element();
335    };
336
337    // Built a line at a time by the same column the blocks are: a line is to
338    // this text what a block is to a document.
339    let spans = source.spans.clone();
340    let ranges = source.ranges.clone();
341    let mut kept: Vec<usize> = keep.to_vec();
342    if let Some(selection) = selection {
343        kept.push(source.line_of(selection.head.offset));
344        kept.push(source.line_of(selection.anchor.offset));
345    }
346    let line = px(typography.code.line_height());
347    let indent = px(gutter.width() + 2.0 * CODE_PADDING_X);
348    let sized = source.sized(typography.code.size(), line, indent);
349    layouts.prune(&sized.keys);
350    let paint = RowPaint::new(&overlay, code, typography.code.size(), &theme);
351    let code = source.code.clone();
352    let sink = layouts.clone();
353    let column = Column {
354        layouts: layouts.clone(),
355        keys: sized.keys,
356        gaps: sized.gaps,
357        guesses: sized.guesses,
358        keep: kept,
359        scroll: scroll.cloned(),
360        item_of: {
361            let ranges = ranges.clone();
362            Rc::new(move |at| ranges.partition_point(|line| line.end < at.offset))
363        },
364        build: Box::new(move |index, window, _| {
365            let span = ranges[index].clone();
366            let paint = RowPaint {
367                hollow: !window.is_window_active(),
368                ..paint.clone()
369            };
370            let styled = code_line(
371                &code[span.clone()],
372                span.start,
373                spans.as_deref(),
374                paint.cut(),
375                &theme,
376            );
377            let layout = styled.layout().clone();
378            let sink = sink.clone();
379            let underlay = canvas(
380                |_, _, _| (),
381                move |_, _, window, _| {
382                    sink.record(
383                        0,
384                        Part::Code,
385                        span.clone(),
386                        layout.clone(),
387                        Shown::default(),
388                    );
389                    paint.paint(&span, &layout, window);
390                },
391            )
392            .absolute()
393            .size_full();
394            let text = div().relative().child(underlay).child(styled);
395            div()
396                .px(px(CODE_PADDING_X))
397                .child(gutter.row(index, text.into_any_element()))
398                .into_any_element()
399        }),
400    };
401    div()
402        .flex()
403        .flex_col()
404        .py(px(CODE_PADDING_Y))
405        .text_size(px(typography.code.size()))
406        .line_height(line)
407        .child(column)
408        .into_any_element()
409}
410
411/// The line numbers beside a source view, when it shows them.
412#[derive(Clone)]
413struct Gutter {
414    shown: bool,
415    width: f32,
416    gap: f32,
417    color: Hsla,
418    font: SharedString,
419}
420
421impl Gutter {
422    fn new(
423        style: &crate::SourceStyle,
424        lines: usize,
425        typography: &Typography,
426        theme: &Theme,
427    ) -> Self {
428        let digits = lines.to_string().len().max(style.gutter_min_digits);
429        let gap = style.gutter_gap.max(0.0) * typography.code.size();
430        Self {
431            shown: style.line_numbers,
432            width: digits as f32 * typography.code.size() + gap,
433            gap,
434            color: style.gutter_color.unwrap_or(theme.text_faint),
435            font: theme.font_mono.clone(),
436        }
437    }
438
439    fn width(&self) -> f32 {
440        if self.shown { self.width } else { 0.0 }
441    }
442
443    /// Keeps each number beside its source line, including wrapped and empty
444    /// lines.
445    fn row(&self, index: usize, line: AnyElement) -> AnyElement {
446        if !self.shown {
447            return line;
448        }
449        div()
450            .flex()
451            .items_start()
452            .child(
453                div()
454                    .w(px(self.width))
455                    .flex_shrink_0()
456                    .pr(px(self.gap))
457                    .font_family(self.font.clone())
458                    .text_color(self.color)
459                    .text_right()
460                    .child((index + 1).to_string()),
461            )
462            .child(div().flex_1().min_w_0().child(line))
463            .into_any_element()
464    }
465}
466
467/// The shaped lines of a fence, and the canvas that paints the caret, the
468/// selection and the annotations over them.
469pub(super) fn code_lines(
470    language: Option<&str>,
471    code: &str,
472    overlay: Overlay,
473    typography: &Typography,
474    theme: &Theme,
475    cx: &App,
476) -> (AnyElement, Vec<AnyElement>) {
477    let ix = overlay.block;
478    // Highlighting recolors runs only — layout does not move, so a build with
479    // no highlighter installed paints the same block in one plain run.
480    // Markdown is the one language this crate can colour on its own, which is
481    // what a source view is painted with where no highlighter reaches.
482    let spans = crate::highlight::spans(cx, language, code).or_else(|| {
483        language
484            .filter(|language| crate::source::is_markdown(language))
485            .map(|_| crate::source::spans(code))
486    });
487    // Each source line's own layout, with the slice of the code it covers —
488    // the caret and a click both resolve through these. A wrapped line is
489    // several rows of one layout, which is the case `range_rects` already
490    // walks for a paragraph.
491    let paint = RowPaint::new(&overlay, code, typography.code.size(), theme);
492    let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
493    let mut offset = 0usize;
494    let lines: Vec<AnyElement> = code
495        .split('\n')
496        .map(|line| {
497            let start = offset;
498            offset += line.len() + 1;
499            let styled = code_line(line, start, spans.as_deref(), paint.cut(), theme);
500            rows.push((start..start + line.len(), styled.layout().clone()));
501            styled.into_any_element()
502        })
503        .collect();
504
505    let sink = overlay.layouts.cloned();
506    let underlay = canvas(
507        |_, _, _| (),
508        move |_, _, window, _| {
509            for (span, layout) in &rows {
510                if let Some(sink) = &sink {
511                    sink.record(
512                        ix,
513                        Part::Code,
514                        span.clone(),
515                        layout.clone(),
516                        Shown::default(),
517                    );
518                }
519                paint.paint(span, layout, window);
520            }
521        },
522    )
523    .absolute()
524    .size_full();
525
526    (underlay.into_any_element(), lines)
527}
528
529/// A highlighted byte range of a text.
530type Span = (Range<usize>, theme::HighlightKind);
531
532/// One line of code, coloured by the spans over the text it came from.
533///
534/// Runs are measured within the line; spans are byte ranges over the whole
535/// text, so every span is clipped to the line and rebased.
536/// `glyph`, over the whole text, is recoloured to the background where it
537/// falls in the line.
538fn code_line(
539    line: &str,
540    start: usize,
541    spans: Option<&[Span]>,
542    glyph: Option<&Range<usize>>,
543    theme: &Theme,
544) -> StyledText {
545    let mono = font(theme.font_mono.clone());
546    let run = |len: usize, color: Hsla| TextRun {
547        len,
548        font: mono.clone(),
549        color,
550        background_color: None,
551        underline: None,
552        strikethrough: None,
553    };
554    let mut runs = Vec::new();
555    let mut pos = 0usize;
556    if let Some(spans) = spans {
557        let end = start + line.len();
558        for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
559            let s = range.start.clamp(start, end) - start;
560            let e = range.end.min(end) - start;
561            if s > pos {
562                runs.push(run(s - pos, theme.text));
563            }
564            runs.push(run(e - s, theme.syntax.color(*kind)));
565            pos = e;
566        }
567    }
568    if pos < line.len() {
569        runs.push(run(line.len() - pos, theme.text));
570    }
571    if runs.is_empty() {
572        runs.push(run(0, theme.text));
573    }
574    if let Some(glyph) =
575        glyph.filter(|glyph| glyph.start >= start && glyph.end <= start + line.len())
576    {
577        runs =
578            ui::input::caret::recoloured(runs, &(glyph.start - start..glyph.end - start), theme.bg);
579    }
580    StyledText::new(SharedString::from(line.to_string())).with_runs(runs)
581}
582
583/// What paints under a line of code: the annotations, the selection and the
584/// caret, each clipped to the slice of the text the line covers.
585#[derive(Clone)]
586struct RowPaint {
587    caret: Option<usize>,
588    /// The grapheme after the caret, over the whole text.
589    glyph: Option<Range<usize>>,
590    affinity: Affinity,
591    shape: ui::input::CaretShape,
592    height: ui::input::CaretHeight,
593    hollow: bool,
594    font: SharedString,
595    selected: Option<Range<usize>>,
596    annotated: Vec<(Range<usize>, Hsla)>,
597    caret_color: Hsla,
598    selection_color: Hsla,
599    code_size: f32,
600}
601
602impl RowPaint {
603    fn new(overlay: &Overlay, code: &str, code_size: f32, theme: &Theme) -> Self {
604        let caret = overlay.caret_painted();
605        Self {
606            caret,
607            glyph: caret.and_then(|offset| overlay.covered(code, offset)),
608            affinity: overlay.affinity(),
609            shape: overlay.caret_shape,
610            height: overlay.caret_height,
611            hollow: overlay.caret_hollow(),
612            font: theme.font_mono.clone(),
613            selected: overlay.selected(code.len()),
614            annotated: overlay.annotated(code.len(), theme),
615            caret_color: theme.caret,
616            selection_color: theme.selection,
617            code_size,
618        }
619    }
620
621    /// The grapheme a solid block covers, recoloured to the background.
622    fn cut(&self) -> Option<&Range<usize>> {
623        self.glyph
624            .as_ref()
625            .filter(|_| self.shape.cuts_out(self.hollow))
626    }
627
628    fn paint(&self, span: &Range<usize>, layout: &TextLayout, window: &mut Window) {
629        let wash = |range: &Range<usize>, color: Hsla, window: &mut Window| {
630            let (from, to) = (range.start.max(span.start), range.end.min(span.end));
631            if from < to {
632                for rect in range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0) {
633                    window.paint_quad(quad(
634                        rect,
635                        px(2.0),
636                        color,
637                        px(0.0),
638                        gpui::transparent_black(),
639                        BorderStyle::default(),
640                    ));
641                }
642            }
643        };
644        for (range, color) in &self.annotated {
645            wash(range, *color, window);
646        }
647        if let Some(range) = &self.selected {
648            wash(range, self.selection_color, window);
649        }
650        if let Some(offset) = self.caret.filter(|at| span.contains(at) || *at == span.end) {
651            let glyph = self
652                .cut()
653                .map(|glyph| glyph.start - span.start..glyph.end - span.start);
654            paint_caret(
655                layout,
656                offset - span.start,
657                self.affinity,
658                glyph.as_ref(),
659                CaretPaint {
660                    shape: self.shape,
661                    height: self.height,
662                    hollow: self.hollow,
663                    color: self.caret_color,
664                    size: self.code_size,
665                    face: font(self.font.clone()),
666                },
667                window,
668            );
669        }
670    }
671}
672
673pub(super) fn code_block(
674    language: Option<&str>,
675    code: &str,
676    overlay: Overlay,
677    typography: &Typography,
678    theme: &Theme,
679    window: &mut Window,
680    cx: &mut App,
681) -> AnyElement {
682    let ix = overlay.block;
683    let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
684    let body = code_body(
685        ix,
686        underlay,
687        lines,
688        typography,
689        cx.markdown_layout().wrap_code,
690    );
691
692    fence_panel(theme)
693        // The band is unconditional: it is where the copy button already floats,
694        // and where a host puts its language control — which needs somewhere to
695        // sit on a block that has no language yet.
696        .child(
697            fence_band(theme)
698                .text_color(match language {
699                    Some(_) => theme.text_muted,
700                    None => theme.text_faint,
701                })
702                // The label's own box, not the band's: a host hanging a picker
703                // here wants it around the word, and only the word knows how
704                // wide the word is.
705                .child(
706                    div()
707                        .relative()
708                        .children(overlay.layouts.map(|layouts| {
709                            let layouts = layouts.clone();
710                            canvas(
711                                move |bounds, _, _| layouts.record_language(ix, bounds),
712                                |_, _, _, _| (),
713                            )
714                            .absolute()
715                            .size_full()
716                        }))
717                        .child(SharedString::from(
718                            language.unwrap_or(PLAIN_LANGUAGE).to_string(),
719                        )),
720                ),
721        )
722        .child(body)
723        .children(
724            (overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
725        )
726        .into_any_element()
727}
728
729/// The box a fence is drawn in, painted or as code.
730pub fn fence_panel(theme: &Theme) -> gpui::Div {
731    div()
732        .rounded(px(Theme::panel_radius()))
733        .bg(theme.ink(0.035))
734        .border_1()
735        .border_color(theme.border)
736        .overflow_hidden()
737        .relative()
738}
739
740/// The band across the top of a [`fence_panel`], where its label and copy
741/// button sit.
742pub fn fence_band(theme: &Theme) -> gpui::Div {
743    div()
744        .relative()
745        .flex()
746        .flex_row()
747        .items_center()
748        .px(px(CODE_PADDING_X))
749        .py(px(5.0))
750        .border_b_1()
751        .border_color(theme.border)
752        .bg(theme.ink(0.02))
753        .text_style(TextStyle::Subheadline)
754}
755
756/// The lines of a fence, wrapped to the block or scrolling sideways under it.
757pub(super) fn code_body(
758    ix: usize,
759    underlay: AnyElement,
760    lines: Vec<AnyElement>,
761    typography: &Typography,
762    wrap: bool,
763) -> AnyElement {
764    let column = div()
765        .flex()
766        .flex_col()
767        .px(px(CODE_PADDING_X))
768        .children(lines);
769    let body = div()
770        .id(ElementId::named_usize("md-code", ix))
771        .relative()
772        .py(px(CODE_PADDING_Y))
773        .text_size(px(typography.code.size()))
774        .line_height(px(typography.code.line_height()))
775        .child(underlay);
776    if wrap {
777        // The column is the block's width here rather than its widest line's,
778        // which is what gives the text something to wrap against.
779        body.child(column.w_full()).into_any_element()
780    } else {
781        ui::scroll::Viewport::new(
782            format!("md-code-scroll-{ix}"),
783            body.flex()
784                .flex_row()
785                .whitespace_nowrap()
786                // The padding belongs to the lines, not to the scroller: a scroll
787                // container's trailing padding is not part of what it will scroll
788                // to, so the last characters of a long line sit behind the right
789                // edge with nowhere left to go. As a row's only item this column is
790                // sized by its widest line, and the padding rides along inside that
791                // width.
792                .child(column.items_start()),
793            gpui::Axis::Horizontal,
794        )
795        .into_any_element()
796    }
797}
798
799/// A copy button that owns its own feedback.
800///
801/// The state is the element's, not the caller's: a component library cannot ask
802/// every host to thread a handler and a "which block is showing Copied" index
803/// through its render tree just to put a button on a code block. It resets when
804/// the pointer leaves, which needs no clock.
805pub(crate) fn copy_button(
806    code: &str,
807    ix: usize,
808    theme: &Theme,
809    window: &mut Window,
810    cx: &mut App,
811) -> AnyElement {
812    let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
813    let showing = *copied.read(cx);
814    let text: SharedString = code.to_string().into();
815
816    div()
817        .id(ElementId::named_usize("md-copy", ix))
818        .absolute()
819        .top(px(3.0))
820        .right(px(5.0))
821        .h(px(20.0))
822        .px(px(6.0))
823        .rounded(px(5.0))
824        .flex()
825        .items_center()
826        .cursor_pointer()
827        .text_style(TextStyle::Caption)
828        .text_color(theme.text_muted)
829        .hover(|el| el.bg(theme.element_hover))
830        .child(if showing { "Copied" } else { "Copy" })
831        .on_click({
832            let copied = copied.clone();
833            move |_, _, cx| {
834                cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
835                copied.update(cx, |state, cx| {
836                    *state = true;
837                    cx.notify();
838                });
839            }
840        })
841        .on_hover(move |hovering, _, cx| {
842            if !*hovering && *copied.read(cx) {
843                copied.update(cx, |state, cx| {
844                    *state = false;
845                    cx.notify();
846                });
847            }
848        })
849        .into_any_element()
850}