Skip to main content

markdown/render/
code.rs

1//! Code blocks.
2
3use super::*;
4use ui::input::overdraw::Layers;
5
6/// The shaped lines of a fence, the canvas that paints the selection and the
7/// annotations under them, and the one that paints the caret over them.
8pub(super) fn code_lines(
9    language: Option<&str>,
10    code: &str,
11    overlay: Overlay,
12    typography: &Typography,
13    theme: &Theme,
14    cx: &App,
15) -> (AnyElement, AnyElement, Vec<AnyElement>) {
16    let ix = overlay.block;
17    // Highlighting recolors runs only — layout does not move, so a build with
18    // no highlighter installed paints the same block in one plain run.
19    // Markdown is the one language this crate can colour on its own, which is
20    // what a source view is painted with where no highlighter reaches.
21    let spans = crate::fence::highlight::spans(cx, language, code).or_else(|| {
22        language
23            .filter(|language| crate::source::is_markdown(language))
24            .map(|_| crate::source::spans(code))
25    });
26    // Each source line's own layout, with the slice of the code it covers —
27    // the caret and a click both resolve through these. A wrapped line is
28    // several rows of one layout, which is the case `range_rects` already
29    // walks for a paragraph.
30    let paint = RowPaint::new(&overlay, code, typography.code.size(), theme);
31    let mut rows: Vec<(Range<usize>, TextLayout, Vec<TextRun>)> = Vec::new();
32    let mut offset = 0usize;
33    let lines: Vec<AnyElement> = code
34        .split('\n')
35        .map(|line| {
36            let start = offset;
37            offset += line.len() + 1;
38            let (styled, runs) = code_line(line, start, spans.as_deref(), theme);
39            rows.push((start..start + line.len(), styled.layout().clone(), runs));
40            styled.into_any_element()
41        })
42        .collect();
43
44    let sink = overlay.layouts.cloned();
45    let over = {
46        let (rows, paint) = (rows.clone(), paint.clone());
47        canvas(
48            |_, _, _| (),
49            move |_, _, window, _| {
50                for (span, layout, runs) in &rows {
51                    paint.paint_over(span, layout, runs, window);
52                }
53            },
54        )
55        .absolute()
56        .size_full()
57    };
58    let underlay = canvas(
59        |_, _, _| (),
60        move |_, _, window, _| {
61            for (span, layout, _) in &rows {
62                if let Some(sink) = &sink {
63                    sink.record(
64                        ix,
65                        Part::Code,
66                        span.clone(),
67                        layout.clone(),
68                        Shown::default(),
69                    );
70                }
71                paint.paint_under(span, layout, window);
72            }
73        },
74    )
75    .absolute()
76    .size_full();
77
78    (underlay.into_any_element(), over.into_any_element(), lines)
79}
80
81/// A highlighted byte range of a text.
82pub(super) type Span = (Range<usize>, theme::HighlightKind);
83
84/// One line of code, coloured by the spans over the text it came from, and
85/// its runs.
86///
87/// Runs are measured within the line; spans are byte ranges over the whole
88/// text, so every span is clipped to the line and rebased.
89pub(super) fn code_line(
90    line: &str,
91    start: usize,
92    spans: Option<&[Span]>,
93    theme: &Theme,
94) -> (StyledText, Vec<TextRun>) {
95    let mono = font(theme.font_mono.clone());
96    let run = |len: usize, color: Hsla| TextRun {
97        len,
98        font: mono.clone(),
99        color,
100        background_color: None,
101        underline: None,
102        strikethrough: None,
103    };
104    let mut runs = Vec::new();
105    let mut pos = 0usize;
106    if let Some(spans) = spans {
107        let end = start + line.len();
108        for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
109            let s = range.start.clamp(start, end) - start;
110            let e = range.end.min(end) - start;
111            if s > pos {
112                runs.push(run(s - pos, theme.text));
113            }
114            runs.push(run(e - s, theme.syntax.color(*kind)));
115            pos = e;
116        }
117    }
118    if pos < line.len() {
119        runs.push(run(line.len() - pos, theme.text));
120    }
121    if runs.is_empty() {
122        runs.push(run(0, theme.text));
123    }
124    let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs.clone());
125    (styled, runs)
126}
127
128/// What paints with a line of code: the selection and the annotations under
129/// it, the caret and the text they leave unreadable over it, each clipped to the slice of the text the line
130/// covers.
131#[derive(Clone)]
132pub(super) struct RowPaint {
133    caret: Option<usize>,
134    /// The grapheme after the caret, over the whole text.
135    glyph: Option<Range<usize>>,
136    affinity: Affinity,
137    shape: ui::input::CaretShape,
138    height: ui::input::CaretHeight,
139    hollow: bool,
140    font: SharedString,
141    /// What is painted under the code, over the whole text.
142    layers: Layers<()>,
143    caret_color: Hsla,
144    /// What the code is painted on.
145    page: Hsla,
146    code_size: f32,
147}
148
149impl RowPaint {
150    /// The same paint, its caret hollow or not.
151    pub(super) fn hollow(&self, hollow: bool) -> Self {
152        Self {
153            hollow,
154            ..self.clone()
155        }
156    }
157
158    pub(super) fn new(overlay: &Overlay, code: &str, code_size: f32, theme: &Theme) -> Self {
159        let caret = overlay.caret_painted();
160        Self {
161            caret,
162            glyph: caret.and_then(|offset| overlay.covered(code, offset)),
163            affinity: overlay.affinity(),
164            shape: overlay.caret_shape,
165            height: overlay.caret_height,
166            hollow: overlay.caret_hollow(),
167            font: theme.font_mono.clone(),
168            layers: {
169                let mut layers = Layers::new();
170                for (range, color) in overlay.annotated(code.len(), theme) {
171                    layers = layers.wash(range, color, ());
172                }
173                if let Some(range) = overlay.selected(code.len()) {
174                    layers = layers.select(range, theme.selection, ());
175                }
176                layers
177            },
178            caret_color: theme.caret,
179            page: theme.bg,
180            code_size,
181        }
182    }
183
184    /// The grapheme a solid block covers.
185    fn cut(&self) -> Option<&Range<usize>> {
186        self.glyph
187            .as_ref()
188            .filter(|_| self.shape.cuts_out(self.hollow))
189    }
190
191    pub(super) fn paint_under(
192        &self,
193        span: &Range<usize>,
194        layout: &TextLayout,
195        window: &mut Window,
196    ) {
197        for (range, color, ()) in self.layers.clipped(span).painted() {
198            for rect in range_rects(layout, range, 0.0, 0.0) {
199                window.paint_quad(quad(
200                    rect,
201                    px(2.0),
202                    color,
203                    px(0.0),
204                    gpui::transparent_black(),
205                    BorderStyle::default(),
206                ));
207            }
208        }
209    }
210
211    /// `runs` are the line's, `span` its slice of the whole text.
212    pub(super) fn paint_over(
213        &self,
214        span: &Range<usize>,
215        layout: &TextLayout,
216        runs: &[TextRun],
217        window: &mut Window,
218    ) {
219        let layers = self.layers.clipped(span);
220        ui::input::overdraw::paint(
221            layout.line_layouts().iter().map(|line| &**line),
222            layout.bounds().origin,
223            layout.line_height(),
224            px(2.0),
225            &ui::input::overdraw::overdraws(runs, self.page, &layers),
226            window,
227        );
228        if let Some(offset) = self.caret.filter(|at| span.contains(at) || *at == span.end) {
229            let glyph = self
230                .cut()
231                .map(|glyph| glyph.start - span.start..glyph.end - span.start);
232            paint_caret(
233                layout,
234                offset - span.start,
235                self.affinity,
236                glyph.as_ref(),
237                CaretPaint {
238                    shape: self.shape,
239                    height: self.height,
240                    hollow: self.hollow,
241                    color: self.caret_color,
242                    cover: ui::input::caret::cover(
243                        runs,
244                        offset - span.start,
245                        self.page,
246                        &layers,
247                        self.caret_color,
248                    ),
249                    size: self.code_size,
250                    face: font(self.font.clone()),
251                },
252                window,
253            );
254        }
255    }
256}
257
258#[allow(clippy::too_many_arguments)]
259pub(super) fn code_block(
260    language: Option<&str>,
261    code: &str,
262    note: Option<SharedString>,
263    overlay: Overlay,
264    typography: &Typography,
265    theme: &Theme,
266    window: &mut Window,
267    cx: &mut App,
268) -> AnyElement {
269    let ix = overlay.block;
270    let (underlay, over, lines) = code_lines(language, code, overlay, typography, theme, cx);
271    let body = code_body(
272        ix,
273        underlay,
274        over,
275        lines,
276        typography,
277        cx.markdown_layout().wrap_code,
278    );
279
280    fence_panel(theme)
281        // The band is unconditional: it is where the copy button already floats,
282        // and where a host puts its language control — which needs somewhere to
283        // sit on a block that has no language yet.
284        .child(
285            fence_band(theme)
286                .text_color(match language {
287                    Some(_) => theme.text_muted,
288                    None => theme.text_faint,
289                })
290                // The label's own box, not the band's: a host hanging a picker
291                // here wants it around the word, and only the word knows how
292                // wide the word is.
293                .child(
294                    div()
295                        .relative()
296                        .children(overlay.layouts.map(|layouts| {
297                            let layouts = layouts.clone();
298                            canvas(
299                                move |bounds, _, _| layouts.record_language(ix, bounds),
300                                |_, _, _, _| (),
301                            )
302                            .absolute()
303                            .size_full()
304                        }))
305                        .child(SharedString::from(
306                            language.unwrap_or(PLAIN_LANGUAGE).to_string(),
307                        )),
308                ),
309        )
310        .child(body)
311        .children(note.map(|note| fence_note(note, theme)))
312        .children(
313            (overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
314        )
315        .into_any_element()
316}
317
318/// The lines of a fence, wrapped to the block or scrolling sideways under it.
319pub(super) fn code_body(
320    ix: usize,
321    underlay: AnyElement,
322    over: AnyElement,
323    lines: Vec<AnyElement>,
324    typography: &Typography,
325    wrap: bool,
326) -> AnyElement {
327    let column = div()
328        .flex()
329        .flex_col()
330        .px(px(CODE_PADDING_X))
331        .children(lines);
332    let body = div()
333        .id(ElementId::named_usize("md-code", ix))
334        .relative()
335        .py(px(CODE_PADDING_Y))
336        .text_size(px(typography.code.size()))
337        .line_height(px(typography.code.line_height()))
338        .child(underlay);
339    if wrap {
340        // The column is the block's width here rather than its widest line's,
341        // which is what gives the text something to wrap against.
342        body.child(column.w_full()).child(over).into_any_element()
343    } else {
344        ui::scroll::Viewport::new(
345            format!("md-code-scroll-{ix}"),
346            body.flex()
347                .flex_row()
348                .whitespace_nowrap()
349                // The padding belongs to the lines, not to the scroller: a scroll
350                // container's trailing padding is not part of what it will scroll
351                // to, so the last characters of a long line sit behind the right
352                // edge with nowhere left to go. As a row's only item this column is
353                // sized by its widest line, and the padding rides along inside that
354                // width.
355                .child(column.items_start())
356                .child(over),
357            gpui::Axis::Horizontal,
358        )
359        .into_any_element()
360    }
361}