use super::*;
pub struct SourceText {
code: Arc<str>,
ranges: Rc<[Range<usize>]>,
hashes: Rc<[u64]>,
spans: Option<Rc<[Span]>>,
generation: u64,
sized: RefCell<Option<Sized>>,
}
#[derive(Clone)]
struct Sized {
at: (u32, Pixels, Pixels),
keys: Rc<[u64]>,
gaps: Rc<[Pixels]>,
guesses: Rc<[Guess]>,
}
pub struct SpanJob {
code: Arc<str>,
highlighter: Option<crate::Highlighter>,
generation: u64,
}
pub struct Spans {
spans: Option<Vec<Span>>,
generation: u64,
}
impl SpanJob {
pub fn run(self) -> Spans {
Spans {
spans: highlight_source(self.highlighter, &self.code),
generation: self.generation,
}
}
}
fn highlight_source(highlighter: Option<crate::Highlighter>, code: &str) -> Option<Vec<Span>> {
let language = crate::source::LANGUAGES[0];
highlighter
.and_then(|highlighter| highlighter(language, code))
.or_else(|| crate::source::is_markdown(language).then(|| crate::source::spans(code)))
}
impl SourceText {
pub fn new(code: impl Into<Arc<str>>, cx: &App) -> Self {
let code: Arc<str> = code.into();
let spans = highlight_source(crate::highlight::installed(cx), &code).map(Into::into);
let ranges = split_lines(&code, 0..code.len());
let hashes = ranges.iter().map(|range| hash_line(&code, range)).collect();
Self {
code,
ranges: ranges.into(),
hashes,
spans,
generation: 0,
sized: RefCell::new(None),
}
}
pub fn text(&self) -> &str {
&self.code
}
pub fn lines(&self) -> usize {
self.ranges.len()
}
pub fn line_of(&self, offset: usize) -> usize {
self.ranges
.partition_point(|range| range.end < offset)
.min(self.ranges.len() - 1)
}
pub fn spans(&self) -> Option<&[(Range<usize>, theme::HighlightKind)]> {
self.spans.as_deref()
}
pub fn edit(&mut self, code: &str, cx: &App) -> Option<SpanJob> {
let old = self.code.as_bytes();
let new = code.as_bytes();
let prefix = old.iter().zip(new).take_while(|(a, b)| a == b).count();
if prefix == old.len() && prefix == new.len() {
return None;
}
let room = old.len().min(new.len()) - prefix;
let suffix = old
.iter()
.rev()
.zip(new.iter().rev())
.take(room)
.take_while(|(a, b)| a == b)
.count();
let old_end = old.len() - suffix;
let delta = (new.len() - suffix) as isize - old_end as isize;
let shift = |at: usize| at.wrapping_add_signed(delta);
let first = self.line_of(prefix);
let last = self.line_of(old_end);
let code: Arc<str> = code.into();
let lines = split_lines(
&code,
self.ranges[first].start..shift(self.ranges[last].end),
);
let hashes: Vec<u64> = lines.iter().map(|range| hash_line(&code, range)).collect();
let cut = first..last + 1;
if let Some(sized) = self.sized.get_mut() {
let (size, line, indent) = sized.at;
sized.keys = splice(
&sized.keys,
cut.clone(),
hashes.iter().map(|hash| key(*hash, size)),
);
sized.guesses = splice(
&sized.guesses,
cut.clone(),
lines.iter().map(|range| guess(range, line, indent)),
);
sized.gaps = vec![px(0.0); sized.keys.len()].into();
}
self.hashes = splice(&self.hashes, cut.clone(), hashes);
self.ranges = self.ranges[..first]
.iter()
.cloned()
.chain(lines)
.chain(
self.ranges[last + 1..]
.iter()
.map(|range| shift(range.start)..shift(range.end)),
)
.collect();
self.spans = self.spans.as_ref().map(|spans| {
let mut kept = Vec::with_capacity(spans.len());
for (range, kind) in spans.iter() {
if range.start < prefix {
kept.push((range.start..range.end.min(prefix), *kind));
}
if range.end > old_end {
kept.push((shift(range.start.max(old_end))..shift(range.end), *kind));
}
}
kept.into()
});
self.code = code;
self.generation += 1;
Some(SpanJob {
code: self.code.clone(),
highlighter: crate::highlight::installed(cx),
generation: self.generation,
})
}
pub fn set_spans(&mut self, spans: Spans) -> bool {
if spans.generation != self.generation {
return false;
}
self.spans = spans.spans.map(Into::into);
true
}
fn sized(&self, size: f32, line: Pixels, indent: Pixels) -> Sized {
let at = (size.to_bits(), line, indent);
let mut held = self.sized.borrow_mut();
if let Some(sized) = held.as_ref()
&& sized.at == at
{
return sized.clone();
}
let sized = Sized {
at,
keys: self.hashes.iter().map(|hash| key(*hash, at.0)).collect(),
gaps: vec![px(0.0); self.ranges.len()].into(),
guesses: self
.ranges
.iter()
.map(|range| guess(range, line, indent))
.collect(),
};
*held = Some(sized.clone());
sized
}
}
fn split_lines(code: &str, within: Range<usize>) -> Vec<Range<usize>> {
let mut ranges = Vec::new();
let mut at = within.start;
for line in code[within].split('\n') {
ranges.push(at..at + line.len());
at += line.len() + 1;
}
ranges
}
fn hash_line(code: &str, range: &Range<usize>) -> u64 {
let mut hasher = DefaultHasher::new();
"source".hash(&mut hasher);
code[range.clone()].hash(&mut hasher);
hasher.finish()
}
fn key(hash: u64, size: u32) -> u64 {
let mut hasher = DefaultHasher::new();
hash.hash(&mut hasher);
size.hash(&mut hasher);
hasher.finish()
}
fn guess(range: &Range<usize>, line: Pixels, indent: Pixels) -> Guess {
Guess {
chars: range.len(),
line,
rows: 0,
extra: px(0.0),
indent,
}
}
fn splice<T: Clone>(old: &[T], cut: Range<usize>, new: impl IntoIterator<Item = T>) -> Rc<[T]> {
old[..cut.start]
.iter()
.cloned()
.chain(new)
.chain(old[cut.end..].iter().cloned())
.collect()
}
pub fn render_source(source: &SourceText, editing: Editing, cx: &mut App) -> AnyElement {
let code: &str = &source.code;
let Editing {
selection,
caret_on,
layouts,
annotations,
typography,
keep,
scroll,
..
} = editing;
let mut theme = Theme::of(cx).clone();
theme.font_mono = theme.font_body.clone();
let mut typography = typography.unwrap_or_else(|| cx.typography());
typography.code = typography.body;
let overlay = Overlay {
block: 0,
part: Part::Code,
selection,
caret_on,
caret_shape: cx.caret_shape(),
caret_height: cx.caret_height(),
caret_inactive: cx.inactive_caret(),
window_active: cx.active_window().is_some(),
layouts,
annotations,
placeholder: None,
caption: Caption::default(),
toggle: None,
image: None,
image_overlay: None,
image_overlay_corner: ImageOverlayCorner::BottomRight,
fence: None,
sizing: None,
table_controls: false,
copy: CopyButton::Hidden,
base: None,
highlight: crate::marks::highlight_paint_of(cx),
find: crate::find::find_paint_of(cx),
jump: None,
};
let style = cx.source_style();
let count = source.ranges.len();
let gutter = Gutter::new(&style, count, &typography, &theme);
let Some(layouts) = layouts else {
let (underlay, lines) = code_lines(
Some(crate::source::LANGUAGES[0]),
code,
overlay,
&typography,
&theme,
cx,
);
let lines = lines
.into_iter()
.enumerate()
.map(|(index, line)| gutter.row(index, line))
.collect();
return div()
.flex()
.flex_col()
.child(code_body(0, underlay, lines, &typography, true))
.into_any_element();
};
let spans = source.spans.clone();
let ranges = source.ranges.clone();
let mut kept: Vec<usize> = keep.to_vec();
if let Some(selection) = selection {
kept.push(source.line_of(selection.head.offset));
kept.push(source.line_of(selection.anchor.offset));
}
let line = px(typography.code.line_height());
let indent = px(gutter.width() + 2.0 * CODE_PADDING_X);
let sized = source.sized(typography.code.size(), line, indent);
layouts.prune(&sized.keys);
let paint = RowPaint::new(&overlay, code, typography.code.size(), &theme);
let code = source.code.clone();
let sink = layouts.clone();
let column = Column {
layouts: layouts.clone(),
keys: sized.keys,
gaps: sized.gaps,
guesses: sized.guesses,
keep: kept,
scroll: scroll.cloned(),
item_of: {
let ranges = ranges.clone();
Rc::new(move |at| ranges.partition_point(|line| line.end < at.offset))
},
build: Box::new(move |index, window, _| {
let span = ranges[index].clone();
let paint = RowPaint {
hollow: !window.is_window_active(),
..paint.clone()
};
let styled = code_line(
&code[span.clone()],
span.start,
spans.as_deref(),
paint.cut(),
&theme,
);
let layout = styled.layout().clone();
let sink = sink.clone();
let underlay = canvas(
|_, _, _| (),
move |_, _, window, _| {
sink.record(
0,
Part::Code,
span.clone(),
layout.clone(),
Shown::default(),
);
paint.paint(&span, &layout, window);
},
)
.absolute()
.size_full();
let text = div().relative().child(underlay).child(styled);
div()
.px(px(CODE_PADDING_X))
.child(gutter.row(index, text.into_any_element()))
.into_any_element()
}),
};
div()
.flex()
.flex_col()
.py(px(CODE_PADDING_Y))
.text_size(px(typography.code.size()))
.line_height(line)
.child(column)
.into_any_element()
}
#[derive(Clone)]
struct Gutter {
shown: bool,
width: f32,
gap: f32,
color: Hsla,
font: SharedString,
}
impl Gutter {
fn new(
style: &crate::SourceStyle,
lines: usize,
typography: &Typography,
theme: &Theme,
) -> Self {
let digits = lines.to_string().len().max(style.gutter_min_digits);
let gap = style.gutter_gap.max(0.0) * typography.code.size();
Self {
shown: style.line_numbers,
width: digits as f32 * typography.code.size() + gap,
gap,
color: style.gutter_color.unwrap_or(theme.text_faint),
font: theme.font_mono.clone(),
}
}
fn width(&self) -> f32 {
if self.shown { self.width } else { 0.0 }
}
fn row(&self, index: usize, line: AnyElement) -> AnyElement {
if !self.shown {
return line;
}
div()
.flex()
.items_start()
.child(
div()
.w(px(self.width))
.flex_shrink_0()
.pr(px(self.gap))
.font_family(self.font.clone())
.text_color(self.color)
.text_right()
.child((index + 1).to_string()),
)
.child(div().flex_1().min_w_0().child(line))
.into_any_element()
}
}
pub(super) fn code_lines(
language: Option<&str>,
code: &str,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
cx: &App,
) -> (AnyElement, Vec<AnyElement>) {
let ix = overlay.block;
let spans = crate::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::new();
let mut offset = 0usize;
let lines: Vec<AnyElement> = code
.split('\n')
.map(|line| {
let start = offset;
offset += line.len() + 1;
let styled = code_line(line, start, spans.as_deref(), paint.cut(), theme);
rows.push((start..start + line.len(), styled.layout().clone()));
styled.into_any_element()
})
.collect();
let sink = overlay.layouts.cloned();
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(span, layout, window);
}
},
)
.absolute()
.size_full();
(underlay.into_any_element(), lines)
}
type Span = (Range<usize>, theme::HighlightKind);
fn code_line(
line: &str,
start: usize,
spans: Option<&[Span]>,
glyph: Option<&Range<usize>>,
theme: &Theme,
) -> StyledText {
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));
}
if let Some(glyph) =
glyph.filter(|glyph| glyph.start >= start && glyph.end <= start + line.len())
{
runs =
ui::input::caret::recoloured(runs, &(glyph.start - start..glyph.end - start), theme.bg);
}
StyledText::new(SharedString::from(line.to_string())).with_runs(runs)
}
#[derive(Clone)]
struct RowPaint {
caret: Option<usize>,
glyph: Option<Range<usize>>,
affinity: Affinity,
shape: ui::input::CaretShape,
height: ui::input::CaretHeight,
hollow: bool,
font: SharedString,
selected: Option<Range<usize>>,
annotated: Vec<(Range<usize>, Hsla)>,
caret_color: Hsla,
selection_color: Hsla,
code_size: f32,
}
impl RowPaint {
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(),
selected: overlay.selected(code.len()),
annotated: overlay.annotated(code.len(), theme),
caret_color: theme.caret,
selection_color: theme.selection,
code_size,
}
}
fn cut(&self) -> Option<&Range<usize>> {
self.glyph
.as_ref()
.filter(|_| self.shape.cuts_out(self.hollow))
}
fn paint(&self, span: &Range<usize>, layout: &TextLayout, window: &mut Window) {
let wash = |range: &Range<usize>, color: Hsla, window: &mut Window| {
let (from, to) = (range.start.max(span.start), range.end.min(span.end));
if from < to {
for rect in range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0) {
window.paint_quad(quad(
rect,
px(2.0),
color,
px(0.0),
gpui::transparent_black(),
BorderStyle::default(),
));
}
}
};
for (range, color) in &self.annotated {
wash(range, *color, window);
}
if let Some(range) = &self.selected {
wash(range, self.selection_color, 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,
size: self.code_size,
face: font(self.font.clone()),
},
window,
);
}
}
}
pub(super) fn code_block(
language: Option<&str>,
code: &str,
overlay: Overlay,
typography: &Typography,
theme: &Theme,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let ix = overlay.block;
let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
let body = code_body(
ix,
underlay,
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(
(overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
)
.into_any_element()
}
pub fn fence_panel(theme: &Theme) -> gpui::Div {
div()
.rounded(px(Theme::panel_radius()))
.bg(theme.ink(0.035))
.border_1()
.border_color(theme.border)
.overflow_hidden()
.relative()
}
pub fn fence_band(theme: &Theme) -> gpui::Div {
div()
.relative()
.flex()
.flex_row()
.items_center()
.px(px(CODE_PADDING_X))
.py(px(5.0))
.border_b_1()
.border_color(theme.border)
.bg(theme.ink(0.02))
.text_style(TextStyle::Subheadline)
}
pub(super) fn code_body(
ix: usize,
underlay: 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()).into_any_element()
} else {
ui::scroll::Viewport::new(
format!("md-code-scroll-{ix}"),
body.flex()
.flex_row()
.whitespace_nowrap()
.child(column.items_start()),
gpui::Axis::Horizontal,
)
.into_any_element()
}
}
pub(crate) fn copy_button(
code: &str,
ix: usize,
theme: &Theme,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
let showing = *copied.read(cx);
let text: SharedString = code.to_string().into();
div()
.id(ElementId::named_usize("md-copy", ix))
.absolute()
.top(px(3.0))
.right(px(5.0))
.h(px(20.0))
.px(px(6.0))
.rounded(px(5.0))
.flex()
.items_center()
.cursor_pointer()
.text_style(TextStyle::Caption)
.text_color(theme.text_muted)
.hover(|el| el.bg(theme.element_hover))
.child(if showing { "Copied" } else { "Copy" })
.on_click({
let copied = copied.clone();
move |_, _, cx| {
cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
copied.update(cx, |state, cx| {
*state = true;
cx.notify();
});
}
})
.on_hover(move |hovering, _, cx| {
if !*hovering && *copied.read(cx) {
copied.update(cx, |state, cx| {
*state = false;
cx.notify();
});
}
})
.into_any_element()
}