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