Skip to main content

markdown/render/
code.rs

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