use super::*;
use ui::input::overdraw::Layers;
pub(super) fn code_lines(
language: Option<&str>,
code: &str,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
cx: &App,
) -> (AnyElement, AnyElement, Vec<AnyElement>) {
let ix = overlay.block;
let spans = crate::fence::highlight::spans(cx, language, code).or_else(|| {
language
.filter(|language| crate::source::is_markdown(language))
.map(|_| crate::source::spans(code))
});
let paint = RowPaint::new(&overlay, code, typography.code.size(), theme);
let mut rows: Vec<(Range<usize>, TextLayout, Vec<TextRun>)> = Vec::new();
let mut offset = 0usize;
let lines: Vec<AnyElement> = code
.split('\n')
.map(|line| {
let start = offset;
offset += line.len() + 1;
let (styled, runs) = code_line(line, start, spans.as_deref(), theme);
rows.push((start..start + line.len(), styled.layout().clone(), runs));
styled.into_any_element()
})
.collect();
let sink = overlay.layouts.cloned();
let over = {
let (rows, paint) = (rows.clone(), paint.clone());
canvas(
|_, _, _| (),
move |_, _, window, _| {
for (span, layout, runs) in &rows {
paint.paint_over(span, layout, runs, window);
}
},
)
.absolute()
.size_full()
};
let underlay = canvas(
|_, _, _| (),
move |_, _, window, _| {
for (span, layout, _) in &rows {
if let Some(sink) = &sink {
sink.record(
ix,
Part::Code,
span.clone(),
layout.clone(),
Shown::default(),
);
}
paint.paint_under(span, layout, window);
}
},
)
.absolute()
.size_full();
(underlay.into_any_element(), over.into_any_element(), lines)
}
pub(super) type Span = (Range<usize>, theme::HighlightKind);
pub(super) fn code_line(
line: &str,
start: usize,
spans: Option<&[Span]>,
theme: &Theme,
) -> (StyledText, Vec<TextRun>) {
let mono = font(theme.font_mono.clone());
let run = |len: usize, color: Hsla| TextRun {
len,
font: mono.clone(),
color,
background_color: None,
underline: None,
strikethrough: None,
};
let mut runs = Vec::new();
let mut pos = 0usize;
if let Some(spans) = spans {
let end = start + line.len();
for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
let s = range.start.clamp(start, end) - start;
let e = range.end.min(end) - start;
if s > pos {
runs.push(run(s - pos, theme.text));
}
runs.push(run(e - s, theme.syntax.color(*kind)));
pos = e;
}
}
if pos < line.len() {
runs.push(run(line.len() - pos, theme.text));
}
if runs.is_empty() {
runs.push(run(0, theme.text));
}
let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs.clone());
(styled, runs)
}
#[derive(Clone)]
pub(super) struct RowPaint {
caret: Option<usize>,
glyph: Option<Range<usize>>,
affinity: Affinity,
shape: ui::input::CaretShape,
height: ui::input::CaretHeight,
hollow: bool,
font: SharedString,
layers: Layers<()>,
caret_color: Hsla,
page: Hsla,
code_size: f32,
}
impl RowPaint {
pub(super) fn hollow(&self, hollow: bool) -> Self {
Self {
hollow,
..self.clone()
}
}
pub(super) fn new(overlay: &Overlay, code: &str, code_size: f32, theme: &Theme) -> Self {
let caret = overlay.caret_painted();
Self {
caret,
glyph: caret.and_then(|offset| overlay.covered(code, offset)),
affinity: overlay.affinity(),
shape: overlay.caret_shape,
height: overlay.caret_height,
hollow: overlay.caret_hollow(),
font: theme.font_mono.clone(),
layers: {
let mut layers = Layers::new();
for (range, color) in overlay.annotated(code.len(), theme) {
layers = layers.wash(range, color, ());
}
if let Some(range) = overlay.selected(code.len()) {
layers = layers.select(range, theme.selection, ());
}
layers
},
caret_color: theme.caret,
page: theme.bg,
code_size,
}
}
fn cut(&self) -> Option<&Range<usize>> {
self.glyph
.as_ref()
.filter(|_| self.shape.cuts_out(self.hollow))
}
pub(super) fn paint_under(
&self,
span: &Range<usize>,
layout: &TextLayout,
window: &mut Window,
) {
for (range, color, ()) in self.layers.clipped(span).painted() {
for rect in range_rects(layout, range, 0.0, 0.0) {
window.paint_quad(quad(
rect,
px(2.0),
color,
px(0.0),
gpui::transparent_black(),
BorderStyle::default(),
));
}
}
}
pub(super) fn paint_over(
&self,
span: &Range<usize>,
layout: &TextLayout,
runs: &[TextRun],
window: &mut Window,
) {
let layers = self.layers.clipped(span);
ui::input::overdraw::paint(
layout.line_layouts().iter().map(|line| &**line),
layout.bounds().origin,
layout.line_height(),
px(2.0),
&ui::input::overdraw::overdraws(runs, self.page, &layers),
window,
);
if let Some(offset) = self.caret.filter(|at| span.contains(at) || *at == span.end) {
let glyph = self
.cut()
.map(|glyph| glyph.start - span.start..glyph.end - span.start);
paint_caret(
layout,
offset - span.start,
self.affinity,
glyph.as_ref(),
CaretPaint {
shape: self.shape,
height: self.height,
hollow: self.hollow,
color: self.caret_color,
cover: ui::input::caret::cover(
runs,
offset - span.start,
self.page,
&layers,
self.caret_color,
),
size: self.code_size,
face: font(self.font.clone()),
},
window,
);
}
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn code_block(
language: Option<&str>,
code: &str,
note: Option<SharedString>,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let ix = overlay.block;
let (underlay, over, lines) = code_lines(language, code, overlay, typography, theme, cx);
let body = code_body(
ix,
underlay,
over,
lines,
typography,
cx.markdown_layout().wrap_code,
);
fence_panel(theme)
.child(
fence_band(theme)
.text_color(match language {
Some(_) => theme.text_muted,
None => theme.text_faint,
})
.child(
div()
.relative()
.children(overlay.layouts.map(|layouts| {
let layouts = layouts.clone();
canvas(
move |bounds, _, _| layouts.record_language(ix, bounds),
|_, _, _, _| (),
)
.absolute()
.size_full()
}))
.child(SharedString::from(
language.unwrap_or(PLAIN_LANGUAGE).to_string(),
)),
),
)
.child(body)
.children(note.map(|note| fence_note(note, theme)))
.children(
(overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
)
.into_any_element()
}
pub(super) fn code_body(
ix: usize,
underlay: AnyElement,
over: AnyElement,
lines: Vec<AnyElement>,
typography: &Typography,
wrap: bool,
) -> AnyElement {
let column = div()
.flex()
.flex_col()
.px(px(CODE_PADDING_X))
.children(lines);
let body = div()
.id(ElementId::named_usize("md-code", ix))
.relative()
.py(px(CODE_PADDING_Y))
.text_size(px(typography.code.size()))
.line_height(px(typography.code.line_height()))
.child(underlay);
if wrap {
body.child(column.w_full()).child(over).into_any_element()
} else {
ui::scroll::Viewport::new(
format!("md-code-scroll-{ix}"),
body.flex()
.flex_row()
.whitespace_nowrap()
.child(column.items_start())
.child(over),
gpui::Axis::Horizontal,
)
.into_any_element()
}
}