1use std::cell::RefCell;
4use std::ops::{Bound, Range, RangeBounds};
5use std::sync::{Arc, Mutex, PoisonError};
6
7use unicode_segmentation::UnicodeSegmentation;
8
9use super::cells;
10use super::highlight::{Language, Token, highlight};
11use crate::event::Event;
12use crate::geometry::{Rect, Size, clamp_u16};
13use crate::keymap::Key;
14use crate::style::CellStyle;
15use crate::text;
16use crate::theme::State;
17use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub(crate) struct CodeRow {
22 pub(crate) line: usize,
24 pub(crate) number: Option<usize>,
25 pub(crate) pieces: Vec<(String, Token)>,
26}
27
28pub(crate) fn code_rows(code: &str, language: Language, width: u16) -> Vec<CodeRow> {
31 let tokens = highlight(code, language);
32 let mut rows = Vec::new();
33 let mut line_start = 0;
34 let mut first = 0;
40 for (index, line) in code.split('\n').enumerate() {
41 let line_end = line_start + line.len();
42 while first < tokens.len() && tokens[first].0.end <= line_start {
43 first += 1;
44 }
45 let mut row = CodeRow { line: index + 1, number: Some(index + 1), pieces: Vec::new() };
46 let mut used = 0u16;
47 for (range, token) in tokens[first..].iter().take_while(|(range, _)| range.start < line_end) {
48 let start = range.start.max(line_start);
49 let end = range.end.min(line_end);
50 if start >= end {
51 continue;
52 }
53 for grapheme in code[start..end].graphemes(true) {
54 let cell = if grapheme == "\t" { " " } else { grapheme };
55 let w = text::width(cell);
56 if used.saturating_add(w) > width && used > 0 {
57 rows.push(std::mem::replace(
58 &mut row,
59 CodeRow { line: index + 1, number: None, pieces: vec![(" ".to_owned(), Token::Plain)] },
60 ));
61 used = 2;
62 }
63 match row.pieces.last_mut() {
64 Some((piece, last)) if last == token => piece.push_str(cell),
65 _ => row.pieces.push((cell.to_owned(), *token)),
66 }
67 used = used.saturating_add(w);
68 }
69 }
70 rows.push(row);
71 line_start = line_end + 1;
72 }
73 if code.ends_with('\n') {
74 rows.pop();
75 }
76 rows
77}
78
79pub(crate) fn paint_rows(cx: &mut PaintCx<'_>, area: Rect, rows: &[CodeRow], gutter: u16) {
81 let visible = visible_rows(cx, area, rows.len());
82 for (y, row) in rows.iter().enumerate().skip(visible.start).take(visible.len()) {
83 let Ok(y) = u16::try_from(y) else { break };
84 if y >= area.height {
85 break;
86 }
87 let row_y = area.y + i32::from(y);
88 if gutter > 0 {
90 cx.decoration(Rect::new(area.x, row_y, gutter, 1));
91 }
92 if gutter > 0
93 && let Some(number) = row.number
94 {
95 let style = cx.style("code-line-number", None, &[]).text();
96 let label = format!("{number:>width$}", width = usize::from(gutter - 2));
97 cx.text(area.x, row_y, &label, style, gutter);
98 }
99 let mut x = area.x + i32::from(gutter);
100 for (piece, token) in &row.pieces {
101 let style = cx.style("code-token", Some(token.variant()), &[]).text();
102 x += i32::from(cx.text(x, row_y, piece, style, area.right().saturating_sub(x).try_into().unwrap_or(0)));
103 }
104 }
105}
106
107fn visible_rows(cx: &PaintCx<'_>, area: Rect, count: usize) -> Range<usize> {
112 let clip = cx.clip();
113 let top = clip.y.max(area.y);
114 let bottom = clip.bottom().min(area.bottom()).max(top);
115 let row = |y: i32| usize::try_from(y - area.y).unwrap_or(0).min(count);
116 row(top)..row(bottom)
117}
118
119pub(crate) fn padding_decoration(cx: &mut PaintCx<'_>, rect: Rect, inner: Rect) {
122 cx.decoration(Rect::new(rect.x, rect.y, rect.width, clamp_u16(inner.y - rect.y)));
123 cx.decoration(Rect::new(rect.x, inner.bottom(), rect.width, clamp_u16(rect.bottom() - inner.bottom())));
124 cx.decoration(Rect::new(rect.x, inner.y, clamp_u16(inner.x - rect.x), inner.height));
125 cx.decoration(Rect::new(inner.right(), inner.y, clamp_u16(rect.right() - inner.right()), inner.height));
126}
127
128pub(crate) fn gutter_width(code: &str) -> u16 {
130 gutter_for(code.split('\n').count())
131}
132
133fn gutter_for(lines: usize) -> u16 {
135 text::width(&lines.to_string()).saturating_add(2)
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
140pub enum LineMark {
141 #[default]
143 Unchanged,
144 Added,
146 Removed,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum LineTone {
153 #[default]
155 Accent,
156 Warning,
159}
160
161impl LineTone {
162 fn variant(self) -> &'static str {
163 match self {
164 Self::Accent => "accent",
165 Self::Warning => "warning",
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172enum Sign {
173 Icon(&'static str),
175 Pillar,
177}
178
179const REVEAL_CONTEXT: u16 = 2;
182
183#[derive(Debug, Default)]
185struct CodeMemory {
186 revealed: Option<usize>,
188}
189
190const CACHED_SOURCES: usize = 16;
193
194const CACHED_LAYOUTS: usize = 4;
197
198thread_local! {
199 static SOURCES: RefCell<Vec<Arc<Source>>> = const { RefCell::new(Vec::new()) };
201}
202
203struct Source {
205 code: String,
206 language: Language,
207 lines: usize,
209 layouts: Mutex<Vec<Arc<Layout>>>,
211}
212
213struct Layout {
215 width: u16,
216 rows: Vec<CodeRow>,
217 widest: u16,
219}
220
221impl Source {
222 fn cached(code: String, language: Language) -> Arc<Self> {
228 SOURCES.with_borrow_mut(|sources| {
229 let source = match sources.iter().position(|source| source.language == language && source.code == code) {
230 Some(index) => sources.remove(index),
231 None => {
232 let lines = code.split('\n').count();
233 Arc::new(Self { code, language, lines, layouts: Mutex::new(Vec::new()) })
234 }
235 };
236 sources.insert(0, Arc::clone(&source));
237 sources.truncate(CACHED_SOURCES);
238 source
239 })
240 }
241
242 fn layout(&self, width: u16) -> Arc<Layout> {
244 let mut layouts = self.layouts.lock().unwrap_or_else(PoisonError::into_inner);
245 let layout = match layouts.iter().position(|layout| layout.width == width) {
246 Some(index) => layouts.remove(index),
247 None => {
248 let rows = code_rows(&self.code, self.language, width);
249 let widest = rows
250 .iter()
251 .map(|row| cells::sum(row.pieces.iter().map(|(piece, _)| text::width(piece))))
252 .max()
253 .unwrap_or(0);
254 Arc::new(Layout { width, rows, widest })
255 }
256 };
257 layouts.insert(0, Arc::clone(&layout));
258 layouts.truncate(CACHED_LAYOUTS);
259 layout
260 }
261}
262
263pub struct CodeView<Msg> {
286 source: Arc<Source>,
287 line_numbers: bool,
288 marks: Vec<LineMark>,
289 numbers: Option<Vec<Option<usize>>>,
291 highlights: Vec<(usize, usize, LineTone)>,
292 reveal: Option<usize>,
293 reveal_number: Option<usize>,
294 on_copy: Option<Msg>,
295}
296
297impl<Msg: 'static> CodeView<Msg> {
298 #[must_use]
301 pub fn new(code: impl Into<String>, language: Language) -> Self {
302 Self {
303 source: Source::cached(code.into(), language),
304 line_numbers: true,
305 marks: Vec::new(),
306 numbers: None,
307 highlights: Vec::new(),
308 reveal: None,
309 reveal_number: None,
310 on_copy: None,
311 }
312 }
313
314 #[must_use]
326 pub fn line_marks(mut self, marks: impl IntoIterator<Item = LineMark>) -> Self {
327 self.marks = marks.into_iter().collect();
328 self
329 }
330
331 #[must_use]
335 pub fn highlight_lines(mut self, lines: impl RangeBounds<usize>, tone: LineTone) -> Self {
336 let first = match lines.start_bound() {
337 Bound::Included(&n) => n,
338 Bound::Excluded(&n) => n.saturating_add(1),
339 Bound::Unbounded => 1,
340 };
341 let last = match lines.end_bound() {
342 Bound::Included(&n) => n,
343 Bound::Excluded(&n) => n.saturating_sub(1),
344 Bound::Unbounded => usize::MAX,
345 };
346 self.highlights.push((first.max(1), last, tone));
347 self
348 }
349
350 #[must_use]
355 pub fn reveal(mut self, line: usize) -> Self {
356 self.reveal = Some(line);
357 self
358 }
359
360 #[must_use]
362 pub fn line_numbers(mut self, show: bool) -> Self {
363 self.line_numbers = show;
364 self
365 }
366
367 #[must_use]
375 pub fn line_numbers_from(mut self, numbers: impl IntoIterator<Item = Option<usize>>) -> Self {
376 self.numbers = Some(numbers.into_iter().collect());
377 self
378 }
379
380 #[must_use]
389 pub fn reveal_number(mut self, number: usize) -> Self {
390 self.reveal_number = Some(number);
391 self
392 }
393
394 fn numbers(&self) -> Vec<Option<usize>> {
397 let lines = self.source.lines;
398 if let Some(given) = &self.numbers {
399 return (0..lines).map(|index| given.get(index).copied().flatten()).collect();
400 }
401 if self.marks.is_empty() {
402 return (1..=lines).map(Some).collect();
403 }
404 let (mut old, mut new) = (0, 0);
405 (0..lines)
406 .map(|index| match self.marks.get(index) {
407 Some(LineMark::Removed) => {
408 old += 1;
409 Some(old)
410 }
411 Some(LineMark::Added) => {
412 new += 1;
413 Some(new)
414 }
415 Some(LineMark::Unchanged) | None => {
416 old += 1;
417 new += 1;
418 Some(new)
419 }
420 })
421 .collect()
422 }
423
424 fn line_of_number(&self, number: usize) -> Option<usize> {
427 let numbers = self.numbers();
428 let carries = |index: &usize| numbers.get(*index).copied().flatten() == Some(number);
429 let kept = |index: &usize| !matches!(self.marks.get(*index), Some(LineMark::Removed));
430 let index = (0..numbers.len())
431 .find(|index| carries(index) && kept(index))
432 .or_else(|| (0..numbers.len()).find(carries))?;
433 Some(index + 1)
434 }
435
436 #[must_use]
438 pub fn on_copy(mut self, message: Msg) -> Self {
439 self.on_copy = Some(message);
440 self
441 }
442
443 fn gutter(&self) -> u16 {
444 if !self.line_numbers {
445 return 0;
446 }
447 if self.numbers.is_none() && self.marks.is_empty() {
448 return gutter_for(self.source.lines);
449 }
450 let widest = self.numbers().into_iter().flatten().max().unwrap_or(1);
452 text::width(&widest.to_string()).saturating_add(2)
453 }
454
455 fn signs(&self) -> u16 {
457 if self.marks.is_empty() && self.highlights.is_empty() { 0 } else { 2 }
458 }
459
460 fn look(&self, line: usize) -> Option<(&'static str, Sign)> {
462 if let Some((_, _, tone)) =
463 self.highlights.iter().rev().find(|(first, last, _)| (*first..=*last).contains(&line))
464 {
465 let sign = match tone {
466 LineTone::Accent => Sign::Pillar,
467 LineTone::Warning => Sign::Icon("warning"),
468 };
469 return Some((tone.variant(), sign));
470 }
471 match self.marks.get(line.checked_sub(1)?) {
472 Some(LineMark::Added) => Some(("added", Sign::Icon("line-added"))),
473 Some(LineMark::Removed) => Some(("removed", Sign::Icon("line-removed"))),
474 Some(LineMark::Unchanged) | None => None,
475 }
476 }
477
478 fn renumbered(&self, rows: &[CodeRow]) -> Option<Vec<CodeRow>> {
481 if self.numbers.is_none() && self.marks.is_empty() {
482 return None;
483 }
484 let numbers = self.numbers();
485 let mut rows = rows.to_vec();
486 for row in &mut rows {
487 if row.number.is_some() {
488 row.number = row.line.checked_sub(1).and_then(|index| numbers.get(index).copied().flatten());
489 }
490 }
491 Some(rows)
492 }
493
494 fn paint_looks(&self, cx: &mut PaintCx<'_>, area: Rect, x: i32, top: i32, rows: &[CodeRow]) {
496 let visible = visible_rows(cx, Rect::new(area.x, top, area.width, clamp_u16(area.bottom() - top)), rows.len());
497 for (index, row) in rows.iter().enumerate().skip(visible.start).take(visible.len()) {
498 let Some((variant, sign)) = self.look(row.line) else { continue };
499 let first_row = index == 0 || rows[index - 1].line != row.line;
502 let y = top + i32::try_from(index).unwrap_or(i32::MAX);
503 let style = cx.style("code-line", Some(variant), &[]);
504 if let Some(bg) = style.color("bg") {
505 cx.fill(Rect::new(area.x, y, area.width, 1), bg);
506 }
507 let color = style.color("fg").unwrap_or_else(|| cx.color("text"));
508 match sign {
509 Sign::Pillar => cx.pillar(x, y, color),
510 Sign::Icon(icon) if first_row => {
511 let glyph = cx.env().icons().glyph(icon).into_owned();
512 cx.text(x, y, &glyph, CellStyle::fg(color), 1);
513 }
514 Sign::Icon(_) => {}
515 }
516 }
517 }
518
519 fn request_reveal(&self, cx: &mut PaintCx<'_>, area: Rect, top: i32, rows: &[CodeRow]) {
521 let asked = match self.reveal_number {
522 Some(number) => self.line_of_number(number),
523 None => self.reveal,
524 };
525 let wanted = asked.map(|line| line.clamp(1, rows.last().map_or(1, |row| row.line)));
526 let memory = cx.memory::<CodeMemory>();
527 if memory.revealed == wanted {
528 return;
529 }
530 memory.revealed = wanted;
531 let Some(line) = wanted else { return };
532 let Some(first) = rows.iter().position(|row| row.line == line) else { return };
533 let count = rows[first..].iter().take_while(|row| row.line == line).count();
534 let context = i32::from(REVEAL_CONTEXT);
535 let y = (top + i32::try_from(first).unwrap_or(i32::MAX) - context).max(area.y);
536 let bottom = (top + i32::try_from(first + count).unwrap_or(i32::MAX) + context).min(area.bottom());
537 cx.reveal(Rect::new(area.x, y, area.width, clamp_u16(bottom - y)));
538 }
539}
540
541impl<Msg: Clone + 'static> Widget<Msg> for CodeView<Msg> {
542 fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
543 let padding = cx.env().theme().style("code", None, &[]).pair("padding").unwrap_or((1, 2));
544 let content_width =
545 available.width.saturating_sub(cells::sum([padding.1.saturating_mul(2), self.signs(), self.gutter()]));
546 let layout = self.source.layout(content_width.max(1));
547 Size::new(
548 cells::sum([layout.widest, self.signs(), self.gutter(), padding.1.saturating_mul(2)]),
549 clamp_u16(i32::try_from(layout.rows.len()).unwrap_or(i32::MAX)).saturating_add(padding.0.saturating_mul(2)),
550 )
551 .min(available)
552 }
553
554 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
555 let mut states = cx.states();
556 states.retain(|state| *state != State::Hover);
557 let style = cx.style("code", None, &states);
558 if let Some(bg) = style.text().bg {
559 cx.clear(area, bg);
560 }
561 cx.register_hit(area);
562 let inner = area.inset(style.padding());
563 cx.selectable(inner);
565 let (signs, gutter) = (self.signs(), self.gutter());
566 let width = inner.width.saturating_sub(signs).saturating_sub(gutter).max(1);
567 let layout = self.source.layout(width);
568 let renumbered = self.renumbered(&layout.rows);
569 let rows = renumbered.as_deref().unwrap_or(&layout.rows);
570 if signs > 0 {
571 cx.decoration(Rect::new(inner.x, inner.y, signs, inner.height));
573 self.paint_looks(cx, area, inner.x, inner.y, rows);
574 }
575 paint_rows(
576 cx,
577 Rect::new(inner.x + i32::from(signs), inner.y, inner.width.saturating_sub(signs), inner.height),
578 rows,
579 gutter,
580 );
581 self.request_reveal(cx, area, inner.y, rows);
582 }
583
584 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
585 let Event::Key(key) = event else {
586 return false;
587 };
588 if !key.is_plain(Key::Char('c')) {
589 return false;
590 }
591 cx.copy(self.source.code.clone());
592 cx.flash();
593 if let Some(message) = &self.on_copy {
594 cx.emit(message.clone());
595 }
596 true
597 }
598
599 fn focusable(&self) -> bool {
600 true
601 }
602}
603
604#[cfg(test)]
605mod tests;