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(|| cx.typography());
25 let overlay = Overlay {
26 block: 0,
27 part: Part::Code,
28 selection,
29 caret_on,
30 caret_shape: cx.caret_shape(),
31 window_active: cx.active_window().is_some(),
33 layouts,
34 annotations,
35 placeholder: None,
36 caption: Caption::default(),
37 toggle: None,
39 image: None,
40 image_overlay: None,
41 image_overlay_corner: ImageOverlayCorner::BottomRight,
42 table_controls: false,
43 copy: CopyButton::Hidden,
45 base: None,
46 highlight: crate::marks::highlight_paint_of(cx),
47 find: crate::find::find_paint_of(cx),
48 };
49 let style = cx.source_style();
50 let count = code.split('\n').count();
51 let gutter = Gutter::new(&style, count, &typography, &theme);
52
53 let Some(layouts) = layouts else {
54 let (underlay, lines) = code_lines(
55 Some(crate::source::LANGUAGES[0]),
56 code,
57 overlay,
58 &typography,
59 &theme,
60 cx,
61 );
62 let lines = lines
63 .into_iter()
64 .enumerate()
65 .map(|(index, line)| gutter.row(index, line))
66 .collect();
67 return div()
68 .flex()
69 .flex_col()
70 .child(code_body(0, underlay, lines, &typography, true))
71 .into_any_element();
72 };
73
74 let language = crate::source::LANGUAGES[0];
77 let spans: Option<Rc<[Span]>> = crate::highlight::spans(cx, Some(language), code)
78 .or_else(|| crate::source::is_markdown(language).then(|| crate::source::spans(code)))
79 .map(Into::into);
80 let mut ranges = Vec::with_capacity(count);
81 let mut offset = 0usize;
82 for line in code.split('\n') {
83 ranges.push(offset..offset + line.len());
84 offset += line.len() + 1;
85 }
86 let line_of = |at: usize| {
87 ranges
88 .partition_point(|range| range.end < at)
89 .min(count - 1)
90 };
91 let mut kept: Vec<usize> = keep.to_vec();
92 if let Some(selection) = selection {
93 kept.push(line_of(selection.head.offset));
94 kept.push(line_of(selection.anchor.offset));
95 }
96 let line = px(typography.code.line_height());
97 let keys: Vec<u64> = ranges
98 .iter()
99 .map(|range| {
100 let mut hasher = DefaultHasher::new();
101 "source".hash(&mut hasher);
102 code[range.clone()].hash(&mut hasher);
103 typography.code.size().to_bits().hash(&mut hasher);
104 hasher.finish()
105 })
106 .collect();
107 layouts.prune(&keys);
108 let indent = px(gutter.width() + 2.0 * CODE_PADDING_X);
109 let guesses: Vec<Guess> = ranges
110 .iter()
111 .map(|range| Guess {
112 chars: range.len(),
113 line,
114 rows: 0,
115 extra: px(0.0),
116 indent,
117 })
118 .collect();
119 let paint = RowPaint::new(&overlay, code, typography.code.size(), &theme);
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, window, _| {
135 let span = ranges[index].clone();
136 let paint = RowPaint {
137 hollow: !window.is_window_active(),
138 ..paint.clone()
139 };
140 let styled = code_line(
141 &code[span.clone()],
142 span.start,
143 spans.as_deref(),
144 paint.cut(),
145 &theme,
146 );
147 let layout = styled.layout().clone();
148 let sink = sink.clone();
149 let underlay = canvas(
150 |_, _, _| (),
151 move |_, _, window, _| {
152 sink.record(
153 0,
154 Part::Code,
155 span.clone(),
156 layout.clone(),
157 Shown::default(),
158 );
159 paint.paint(&span, &layout, window);
160 },
161 )
162 .absolute()
163 .size_full();
164 let text = div().relative().child(underlay).child(styled);
165 div()
166 .px(px(CODE_PADDING_X))
167 .child(gutter.row(index, text.into_any_element()))
168 .into_any_element()
169 }),
170 };
171 div()
172 .flex()
173 .flex_col()
174 .py(px(CODE_PADDING_Y))
175 .text_size(px(typography.code.size()))
176 .line_height(line)
177 .child(column)
178 .into_any_element()
179}
180
181#[derive(Clone)]
183struct Gutter {
184 shown: bool,
185 width: f32,
186 gap: f32,
187 color: Hsla,
188 font: SharedString,
189}
190
191impl Gutter {
192 fn new(
193 style: &crate::SourceStyle,
194 lines: usize,
195 typography: &Typography,
196 theme: &Theme,
197 ) -> Self {
198 let digits = lines.to_string().len().max(style.gutter_min_digits);
199 let gap = style.gutter_gap.max(0.0) * typography.code.size();
200 Self {
201 shown: style.line_numbers,
202 width: digits as f32 * typography.code.size() + gap,
203 gap,
204 color: style.gutter_color.unwrap_or(theme.text_faint),
205 font: theme.font_mono.clone(),
206 }
207 }
208
209 fn width(&self) -> f32 {
210 if self.shown { self.width } else { 0.0 }
211 }
212
213 fn row(&self, index: usize, line: AnyElement) -> AnyElement {
216 if !self.shown {
217 return line;
218 }
219 div()
220 .flex()
221 .items_start()
222 .child(
223 div()
224 .w(px(self.width))
225 .flex_shrink_0()
226 .pr(px(self.gap))
227 .font_family(self.font.clone())
228 .text_color(self.color)
229 .text_right()
230 .child((index + 1).to_string()),
231 )
232 .child(div().flex_1().min_w_0().child(line))
233 .into_any_element()
234 }
235}
236
237pub(super) fn code_lines(
240 language: Option<&str>,
241 code: &str,
242 overlay: Overlay,
243 typography: &Typography,
244 theme: &Theme,
245 cx: &App,
246) -> (AnyElement, Vec<AnyElement>) {
247 let ix = overlay.block;
248 let spans = crate::highlight::spans(cx, language, code).or_else(|| {
253 language
254 .filter(|language| crate::source::is_markdown(language))
255 .map(|_| crate::source::spans(code))
256 });
257 let paint = RowPaint::new(&overlay, code, typography.code.size(), theme);
262 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
263 let mut offset = 0usize;
264 let lines: Vec<AnyElement> = code
265 .split('\n')
266 .map(|line| {
267 let start = offset;
268 offset += line.len() + 1;
269 let styled = code_line(line, start, spans.as_deref(), paint.cut(), theme);
270 rows.push((start..start + line.len(), styled.layout().clone()));
271 styled.into_any_element()
272 })
273 .collect();
274
275 let sink = overlay.layouts.cloned();
276 let underlay = canvas(
277 |_, _, _| (),
278 move |_, _, window, _| {
279 for (span, layout) in &rows {
280 if let Some(sink) = &sink {
281 sink.record(
282 ix,
283 Part::Code,
284 span.clone(),
285 layout.clone(),
286 Shown::default(),
287 );
288 }
289 paint.paint(span, layout, window);
290 }
291 },
292 )
293 .absolute()
294 .size_full();
295
296 (underlay.into_any_element(), lines)
297}
298
299type Span = (Range<usize>, theme::HighlightKind);
301
302fn code_line(
309 line: &str,
310 start: usize,
311 spans: Option<&[Span]>,
312 glyph: Option<&Range<usize>>,
313 theme: &Theme,
314) -> StyledText {
315 let mono = font(theme.font_mono.clone());
316 let run = |len: usize, color: Hsla| TextRun {
317 len,
318 font: mono.clone(),
319 color,
320 background_color: None,
321 underline: None,
322 strikethrough: None,
323 };
324 let mut runs = Vec::new();
325 let mut pos = 0usize;
326 if let Some(spans) = spans {
327 let end = start + line.len();
328 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
329 let s = range.start.clamp(start, end) - start;
330 let e = range.end.min(end) - start;
331 if s > pos {
332 runs.push(run(s - pos, theme.text));
333 }
334 runs.push(run(e - s, theme.syntax.color(*kind)));
335 pos = e;
336 }
337 }
338 if pos < line.len() {
339 runs.push(run(line.len() - pos, theme.text));
340 }
341 if runs.is_empty() {
342 runs.push(run(0, theme.text));
343 }
344 if let Some(glyph) =
345 glyph.filter(|glyph| glyph.start >= start && glyph.end <= start + line.len())
346 {
347 runs =
348 ui::input::caret::recoloured(runs, &(glyph.start - start..glyph.end - start), theme.bg);
349 }
350 StyledText::new(SharedString::from(line.to_string())).with_runs(runs)
351}
352
353#[derive(Clone)]
356struct RowPaint {
357 caret: Option<usize>,
358 glyph: Option<Range<usize>>,
360 shape: ui::input::CaretShape,
361 hollow: bool,
362 font: SharedString,
363 selected: Option<Range<usize>>,
364 annotated: Vec<(Range<usize>, Hsla)>,
365 caret_color: Hsla,
366 selection_color: Hsla,
367 code_size: f32,
368}
369
370impl RowPaint {
371 fn new(overlay: &Overlay, code: &str, code_size: f32, theme: &Theme) -> Self {
372 let caret = overlay.caret_painted();
373 Self {
374 caret,
375 glyph: caret.and_then(|offset| glyph_at(code, offset)),
376 shape: overlay.caret_shape,
377 hollow: overlay.caret_hollow(),
378 font: theme.font_mono.clone(),
379 selected: overlay.selected(code.len()),
380 annotated: overlay.annotated(code.len(), theme),
381 caret_color: theme.caret,
382 selection_color: theme.selection,
383 code_size,
384 }
385 }
386
387 fn cut(&self) -> Option<&Range<usize>> {
389 self.glyph
390 .as_ref()
391 .filter(|_| self.shape.cuts_out(self.hollow))
392 }
393
394 fn paint(&self, span: &Range<usize>, layout: &TextLayout, window: &mut Window) {
395 let wash = |range: &Range<usize>, color: Hsla, window: &mut Window| {
396 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
397 if from < to {
398 for rect in range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0) {
399 window.paint_quad(quad(
400 rect,
401 px(2.0),
402 color,
403 px(0.0),
404 gpui::transparent_black(),
405 BorderStyle::default(),
406 ));
407 }
408 }
409 };
410 for (range, color) in &self.annotated {
411 wash(range, *color, window);
412 }
413 if let Some(range) = &self.selected {
414 wash(range, self.selection_color, window);
415 }
416 if let Some(offset) = self.caret.filter(|at| span.contains(at) || *at == span.end) {
417 let glyph = self
418 .cut()
419 .map(|glyph| glyph.start - span.start..glyph.end - span.start);
420 paint_caret(
421 layout,
422 offset - span.start,
423 glyph.as_ref(),
424 CaretPaint {
425 shape: self.shape,
426 hollow: self.hollow,
427 color: self.caret_color,
428 size: self.code_size,
429 face: font(self.font.clone()),
430 },
431 window,
432 );
433 }
434 }
435}
436
437pub(super) fn code_block(
438 language: Option<&str>,
439 code: &str,
440 overlay: Overlay,
441 typography: &Typography,
442 theme: &Theme,
443 window: &mut Window,
444 cx: &mut App,
445) -> AnyElement {
446 let ix = overlay.block;
447 let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
448 let body = code_body(
449 ix,
450 underlay,
451 lines,
452 typography,
453 cx.markdown_layout().wrap_code,
454 );
455
456 div()
457 .rounded(px(Theme::panel_radius()))
458 .bg(theme.ink(0.035))
459 .border_1()
460 .border_color(theme.border)
461 .overflow_hidden()
462 .relative()
463 .child(
467 div()
468 .relative()
469 .flex()
470 .flex_row()
471 .items_center()
472 .px(px(CODE_PADDING_X))
473 .py(px(5.0))
474 .border_b_1()
475 .border_color(theme.border)
476 .bg(theme.ink(0.02))
477 .text_style(TextStyle::Subheadline)
478 .text_color(match language {
479 Some(_) => theme.text_muted,
480 None => theme.text_faint,
481 })
482 .child(
486 div()
487 .relative()
488 .children(overlay.layouts.map(|layouts| {
489 let layouts = layouts.clone();
490 canvas(
491 move |bounds, _, _| layouts.record_language(ix, bounds),
492 |_, _, _, _| (),
493 )
494 .absolute()
495 .size_full()
496 }))
497 .child(SharedString::from(
498 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
499 )),
500 ),
501 )
502 .child(body)
503 .children(
504 (overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
505 )
506 .into_any_element()
507}
508
509pub(super) fn code_body(
511 ix: usize,
512 underlay: AnyElement,
513 lines: Vec<AnyElement>,
514 typography: &Typography,
515 wrap: bool,
516) -> AnyElement {
517 let column = div()
518 .flex()
519 .flex_col()
520 .px(px(CODE_PADDING_X))
521 .children(lines);
522 let body = div()
523 .id(ElementId::named_usize("md-code", ix))
524 .relative()
525 .py(px(CODE_PADDING_Y))
526 .text_size(px(typography.code.size()))
527 .line_height(px(typography.code.line_height()))
528 .child(underlay);
529 if wrap {
530 body.child(column.w_full()).into_any_element()
533 } else {
534 ui::scroll::Viewport::new(
535 format!("md-code-scroll-{ix}"),
536 body.flex()
537 .flex_row()
538 .whitespace_nowrap()
539 .child(column.items_start()),
546 gpui::Axis::Horizontal,
547 )
548 .into_any_element()
549 }
550}
551
552pub(super) fn copy_button(
559 code: &str,
560 ix: usize,
561 theme: &Theme,
562 window: &mut Window,
563 cx: &mut App,
564) -> AnyElement {
565 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
566 let showing = *copied.read(cx);
567 let text: SharedString = code.to_string().into();
568
569 div()
570 .id(ElementId::named_usize("md-copy", ix))
571 .absolute()
572 .top(px(3.0))
573 .right(px(5.0))
574 .h(px(20.0))
575 .px(px(6.0))
576 .rounded(px(5.0))
577 .flex()
578 .items_center()
579 .cursor_pointer()
580 .text_style(TextStyle::Caption)
581 .text_color(theme.text_muted)
582 .hover(|el| el.bg(theme.element_hover))
583 .child(if showing { "Copied" } else { "Copy" })
584 .on_click({
585 let copied = copied.clone();
586 move |_, _, cx| {
587 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
588 copied.update(cx, |state, cx| {
589 *state = true;
590 cx.notify();
591 });
592 }
593 })
594 .on_hover(move |hovering, _, cx| {
595 if !*hovering && *copied.read(cx) {
596 copied.update(cx, |state, cx| {
597 *state = false;
598 cx.notify();
599 });
600 }
601 })
602 .into_any_element()
603}