Skip to main content

markdown/render/
code.rs

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