1use super::*;
4
5pub fn render_source(code: &str, editing: Editing, cx: &mut App) -> AnyElement {
13 let Editing {
14 selection,
15 caret_on,
16 layouts,
17 annotations,
18 typography,
19 keep,
20 scroll,
21 ..
22 } = editing;
23 let theme = Theme::of(cx).clone();
24 let typography = typography.unwrap_or_else(|| Typography::of(cx));
25 let overlay = Overlay {
26 block: 0,
27 part: Part::Code,
28 selection,
29 caret_on,
30 layouts,
31 annotations,
32 placeholder: None,
33 caption: Caption::default(),
34 toggle: None,
36 copy: CopyButton::Hidden,
38 base: None,
39 highlight: crate::marks::highlight_paint_of(cx),
40 };
41 let style = crate::SourceStyle::of(cx);
42 let count = code.split('\n').count();
43 let gutter = Gutter::new(&style, count, &typography, &theme);
44
45 let Some(layouts) = layouts else {
46 let (underlay, lines) = code_lines(
47 Some(crate::source::LANGUAGES[0]),
48 code,
49 overlay,
50 &typography,
51 &theme,
52 cx,
53 );
54 let lines = lines
55 .into_iter()
56 .enumerate()
57 .map(|(index, line)| gutter.row(index, line))
58 .collect();
59 return div()
60 .flex()
61 .flex_col()
62 .child(code_body(0, underlay, lines, &typography, true))
63 .into_any_element();
64 };
65
66 let language = crate::source::LANGUAGES[0];
69 let spans: Option<Rc<[Span]>> = crate::highlight::spans(cx, Some(language), code)
70 .or_else(|| crate::source::is_markdown(language).then(|| crate::source::spans(code)))
71 .map(Into::into);
72 let mut ranges = Vec::with_capacity(count);
73 let mut offset = 0usize;
74 for line in code.split('\n') {
75 ranges.push(offset..offset + line.len());
76 offset += line.len() + 1;
77 }
78 let line_of = |at: usize| {
79 ranges
80 .partition_point(|range| range.end < at)
81 .min(count - 1)
82 };
83 let mut kept: Vec<usize> = keep.to_vec();
84 if let Some(selection) = selection {
85 kept.push(line_of(selection.head.offset));
86 kept.push(line_of(selection.anchor.offset));
87 }
88 let line = px(typography.code.line_height());
89 let keys: Vec<u64> = ranges
90 .iter()
91 .map(|range| {
92 let mut hasher = DefaultHasher::new();
93 "source".hash(&mut hasher);
94 code[range.clone()].hash(&mut hasher);
95 typography.code.size().to_bits().hash(&mut hasher);
96 hasher.finish()
97 })
98 .collect();
99 layouts.prune(&keys);
100 let indent = px(gutter.width() + 2.0 * CODE_PADDING_X);
101 let guesses: Vec<Guess> = ranges
102 .iter()
103 .map(|range| Guess {
104 chars: range.len(),
105 line,
106 rows: 0,
107 extra: px(0.0),
108 indent,
109 })
110 .collect();
111 let paint = RowPaint {
112 caret: overlay.caret_painted(),
113 selected: overlay.selected(code.len()),
114 annotated: overlay.annotated(code.len(), &theme),
115 caret_color: theme.caret,
116 selection_color: theme.selection,
117 code_size: typography.code.size(),
118 };
119 let code: Rc<str> = code.into();
120 let sink = layouts.clone();
121 let ranges: Rc<[Range<usize>]> = ranges.into();
122 let column = Column {
123 layouts: layouts.clone(),
124 keys: keys.into(),
125 gaps: vec![px(0.0); count].into(),
126 guesses: guesses.into(),
127 keep: kept,
128 scroll: scroll.cloned(),
129 build: Box::new(move |index, _, _| {
130 let span = ranges[index].clone();
131 let styled = code_line(&code[span.clone()], span.start, spans.as_deref(), &theme);
132 let layout = styled.layout().clone();
133 let (sink, paint) = (sink.clone(), paint.clone());
134 let underlay = canvas(
135 |_, _, _| (),
136 move |_, _, window, _| {
137 sink.record(0, Part::Code, span.clone(), layout.clone());
138 paint.paint(&span, &layout, window);
139 },
140 )
141 .absolute()
142 .size_full();
143 let text = div().relative().child(underlay).child(styled);
144 div()
145 .px(px(CODE_PADDING_X))
146 .child(gutter.row(index, text.into_any_element()))
147 .into_any_element()
148 }),
149 };
150 div()
151 .flex()
152 .flex_col()
153 .py(px(CODE_PADDING_Y))
154 .text_size(px(typography.code.size()))
155 .line_height(line)
156 .child(column)
157 .into_any_element()
158}
159
160#[derive(Clone)]
162struct Gutter {
163 shown: bool,
164 width: f32,
165 gap: f32,
166 color: Hsla,
167 font: SharedString,
168}
169
170impl Gutter {
171 fn new(
172 style: &crate::SourceStyle,
173 lines: usize,
174 typography: &Typography,
175 theme: &Theme,
176 ) -> Self {
177 let digits = lines.to_string().len().max(style.gutter_min_digits);
178 let gap = style.gutter_gap.max(0.0) * typography.code.size();
179 Self {
180 shown: style.line_numbers,
181 width: digits as f32 * typography.code.size() + gap,
182 gap,
183 color: style.gutter_color.unwrap_or(theme.text_faint),
184 font: theme.font_mono.clone(),
185 }
186 }
187
188 fn width(&self) -> f32 {
189 if self.shown { self.width } else { 0.0 }
190 }
191
192 fn row(&self, index: usize, line: AnyElement) -> AnyElement {
195 if !self.shown {
196 return line;
197 }
198 div()
199 .flex()
200 .items_start()
201 .child(
202 div()
203 .w(px(self.width))
204 .flex_shrink_0()
205 .pr(px(self.gap))
206 .font_family(self.font.clone())
207 .text_color(self.color)
208 .text_right()
209 .child((index + 1).to_string()),
210 )
211 .child(div().flex_1().min_w_0().child(line))
212 .into_any_element()
213 }
214}
215
216pub(super) fn code_lines(
219 language: Option<&str>,
220 code: &str,
221 overlay: Overlay,
222 typography: &Typography,
223 theme: &Theme,
224 cx: &App,
225) -> (AnyElement, Vec<AnyElement>) {
226 let ix = overlay.block;
227 let spans = crate::highlight::spans(cx, language, code).or_else(|| {
232 language
233 .filter(|language| crate::source::is_markdown(language))
234 .map(|_| crate::source::spans(code))
235 });
236 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
241 let mut offset = 0usize;
242 let lines: Vec<AnyElement> = code
243 .split('\n')
244 .map(|line| {
245 let start = offset;
246 offset += line.len() + 1;
247 let styled = code_line(line, start, spans.as_deref(), theme);
248 rows.push((start..start + line.len(), styled.layout().clone()));
249 styled.into_any_element()
250 })
251 .collect();
252
253 let caret = overlay.caret_painted();
254 let selected = overlay.selected(code.len());
255 let sink = overlay.layouts.cloned();
256 let code_size = typography.code.size();
257 let annotated = overlay.annotated(code.len(), theme);
258 let (caret_color, selection_color) = (theme.caret, theme.selection);
259 let paint = RowPaint {
260 caret,
261 selected,
262 annotated,
263 caret_color,
264 selection_color,
265 code_size,
266 };
267 let underlay = canvas(
268 |_, _, _| (),
269 move |_, _, window, _| {
270 for (span, layout) in &rows {
271 if let Some(sink) = &sink {
272 sink.record(ix, Part::Code, span.clone(), layout.clone());
273 }
274 paint.paint(span, layout, window);
275 }
276 },
277 )
278 .absolute()
279 .size_full();
280
281 (underlay.into_any_element(), lines)
282}
283
284type Span = (Range<usize>, theme::HighlightKind);
286
287fn code_line(line: &str, start: usize, spans: Option<&[Span]>, theme: &Theme) -> StyledText {
292 let mono = font(theme.font_mono.clone());
293 let run = |len: usize, color: Hsla| TextRun {
294 len,
295 font: mono.clone(),
296 color,
297 background_color: None,
298 underline: None,
299 strikethrough: None,
300 };
301 let mut runs = Vec::new();
302 let mut pos = 0usize;
303 if let Some(spans) = spans {
304 let end = start + line.len();
305 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
306 let s = range.start.clamp(start, end) - start;
307 let e = range.end.min(end) - start;
308 if s > pos {
309 runs.push(run(s - pos, theme.text));
310 }
311 runs.push(run(e - s, theme.syntax.color(*kind)));
312 pos = e;
313 }
314 }
315 if pos < line.len() {
316 runs.push(run(line.len() - pos, theme.text));
317 }
318 if runs.is_empty() {
319 runs.push(run(0, theme.text));
320 }
321 StyledText::new(SharedString::from(line.to_string())).with_runs(runs)
322}
323
324#[derive(Clone)]
327struct RowPaint {
328 caret: Option<usize>,
329 selected: Option<Range<usize>>,
330 annotated: Vec<(Range<usize>, Hsla)>,
331 caret_color: Hsla,
332 selection_color: Hsla,
333 code_size: f32,
334}
335
336impl RowPaint {
337 fn paint(&self, span: &Range<usize>, layout: &TextLayout, window: &mut Window) {
338 let wash = |range: &Range<usize>, color: Hsla, window: &mut Window| {
339 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
340 if from < to {
341 for rect in range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0) {
342 window.paint_quad(quad(
343 rect,
344 px(2.0),
345 color,
346 px(0.0),
347 gpui::transparent_black(),
348 BorderStyle::default(),
349 ));
350 }
351 }
352 };
353 for (range, color) in &self.annotated {
354 wash(range, *color, window);
355 }
356 if let Some(range) = &self.selected {
357 wash(range, self.selection_color, window);
358 }
359 if let Some(offset) = self.caret.filter(|at| span.contains(at) || *at == span.end)
360 && let Some(head) = layout.position_for_index(offset - span.start)
361 {
362 window.paint_quad(quad(
363 caret_quad(head, self.code_size, layout.line_height()),
364 px(0.0),
365 self.caret_color,
366 px(0.0),
367 gpui::transparent_black(),
368 BorderStyle::default(),
369 ));
370 }
371 }
372}
373
374pub(super) fn code_block(
375 language: Option<&str>,
376 code: &str,
377 overlay: Overlay,
378 typography: &Typography,
379 theme: &Theme,
380 window: &mut Window,
381 cx: &mut App,
382) -> AnyElement {
383 let ix = overlay.block;
384 let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
385 let body = code_body(ix, underlay, lines, typography, Layout::of(cx).wrap_code);
386
387 div()
388 .rounded(px(Theme::panel_radius()))
389 .bg(theme.ink(0.035))
390 .border_1()
391 .border_color(theme.border)
392 .overflow_hidden()
393 .relative()
394 .child(
398 div()
399 .relative()
400 .flex()
401 .flex_row()
402 .items_center()
403 .px(px(CODE_PADDING_X))
404 .py(px(5.0))
405 .border_b_1()
406 .border_color(theme.border)
407 .bg(theme.ink(0.02))
408 .text_style(TextStyle::Subheadline)
409 .text_color(match language {
410 Some(_) => theme.text_muted,
411 None => theme.text_faint,
412 })
413 .child(
417 div()
418 .relative()
419 .children(overlay.layouts.map(|layouts| {
420 let layouts = layouts.clone();
421 canvas(
422 move |bounds, _, _| layouts.record_language(ix, bounds),
423 |_, _, _, _| (),
424 )
425 .absolute()
426 .size_full()
427 }))
428 .child(SharedString::from(
429 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
430 )),
431 ),
432 )
433 .child(body)
434 .children(
435 (overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
436 )
437 .into_any_element()
438}
439
440pub(super) fn code_body(
442 ix: usize,
443 underlay: AnyElement,
444 lines: Vec<AnyElement>,
445 typography: &Typography,
446 wrap: bool,
447) -> AnyElement {
448 let column = div()
449 .flex()
450 .flex_col()
451 .px(px(CODE_PADDING_X))
452 .children(lines);
453 let body = div()
454 .id(ElementId::named_usize("md-code", ix))
455 .relative()
456 .py(px(CODE_PADDING_Y))
457 .text_size(px(typography.code.size()))
458 .line_height(px(typography.code.line_height()))
459 .child(underlay);
460 if wrap {
461 body.child(column.w_full()).into_any_element()
464 } else {
465 ui::scroll::Viewport::new(
466 format!("md-code-scroll-{ix}"),
467 body.flex()
468 .flex_row()
469 .whitespace_nowrap()
470 .child(column.items_start()),
477 gpui::Axis::Horizontal,
478 )
479 .into_any_element()
480 }
481}
482
483pub(super) fn copy_button(
490 code: &str,
491 ix: usize,
492 theme: &Theme,
493 window: &mut Window,
494 cx: &mut App,
495) -> AnyElement {
496 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
497 let showing = *copied.read(cx);
498 let text: SharedString = code.to_string().into();
499
500 div()
501 .id(ElementId::named_usize("md-copy", ix))
502 .absolute()
503 .top(px(3.0))
504 .right(px(5.0))
505 .h(px(20.0))
506 .px(px(6.0))
507 .rounded(px(5.0))
508 .flex()
509 .items_center()
510 .cursor_pointer()
511 .text_style(TextStyle::Caption)
512 .text_color(theme.text_muted)
513 .hover(|el| el.bg(theme.element_hover))
514 .child(if showing { "Copied" } else { "Copy" })
515 .on_click({
516 let copied = copied.clone();
517 move |_, _, cx| {
518 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
519 copied.update(cx, |state, cx| {
520 *state = true;
521 cx.notify();
522 });
523 }
524 })
525 .on_hover(move |hovering, _, cx| {
526 if !*hovering && *copied.read(cx) {
527 copied.update(cx, |state, cx| {
528 *state = false;
529 cx.notify();
530 });
531 }
532 })
533 .into_any_element()
534}