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