1use std::ops::{Bound, RangeBounds};
4
5use unicode_segmentation::UnicodeSegmentation;
6
7use super::cells;
8use super::highlight::{Language, Token, highlight};
9use crate::event::Event;
10use crate::geometry::{Rect, Size, clamp_u16};
11use crate::keymap::Key;
12use crate::style::CellStyle;
13use crate::text;
14use crate::theme::State;
15use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub(crate) struct CodeRow {
20 pub(crate) line: usize,
22 pub(crate) number: Option<usize>,
23 pub(crate) pieces: Vec<(String, Token)>,
24}
25
26pub(crate) fn code_rows(code: &str, language: Language, width: u16) -> Vec<CodeRow> {
29 let tokens = highlight(code, language);
30 let mut rows = Vec::new();
31 let mut line_start = 0;
32 for (index, line) in code.split('\n').enumerate() {
33 let line_end = line_start + line.len();
34 let mut row = CodeRow { line: index + 1, number: Some(index + 1), pieces: Vec::new() };
35 let mut used = 0u16;
36 for (range, token) in &tokens {
37 let start = range.start.max(line_start);
38 let end = range.end.min(line_end);
39 if start >= end {
40 continue;
41 }
42 for grapheme in code[start..end].graphemes(true) {
43 let cell = if grapheme == "\t" { " " } else { grapheme };
44 let w = text::width(cell);
45 if used.saturating_add(w) > width && used > 0 {
46 rows.push(std::mem::replace(
47 &mut row,
48 CodeRow { line: index + 1, number: None, pieces: vec![(" ".to_owned(), Token::Plain)] },
49 ));
50 used = 2;
51 }
52 match row.pieces.last_mut() {
53 Some((piece, last)) if last == token => piece.push_str(cell),
54 _ => row.pieces.push((cell.to_owned(), *token)),
55 }
56 used = used.saturating_add(w);
57 }
58 }
59 rows.push(row);
60 line_start = line_end + 1;
61 }
62 if code.ends_with('\n') {
63 rows.pop();
64 }
65 rows
66}
67
68pub(crate) fn paint_rows(cx: &mut PaintCx<'_>, area: Rect, rows: &[CodeRow], gutter: u16) {
70 for (y, row) in rows.iter().enumerate() {
71 let Ok(y) = u16::try_from(y) else { break };
72 if y >= area.height {
73 break;
74 }
75 let row_y = area.y + i32::from(y);
76 if gutter > 0 {
78 cx.decoration(Rect::new(area.x, row_y, gutter, 1));
79 }
80 if gutter > 0
81 && let Some(number) = row.number
82 {
83 let style = cx.style("code-line-number", None, &[]).text();
84 let label = format!("{number:>width$}", width = usize::from(gutter - 2));
85 cx.text(area.x, row_y, &label, style, gutter);
86 }
87 let mut x = area.x + i32::from(gutter);
88 for (piece, token) in &row.pieces {
89 let style = cx.style("code-token", Some(token.variant()), &[]).text();
90 x += i32::from(cx.text(x, row_y, piece, style, area.right().saturating_sub(x).try_into().unwrap_or(0)));
91 }
92 }
93}
94
95pub(crate) fn padding_decoration(cx: &mut PaintCx<'_>, rect: Rect, inner: Rect) {
98 cx.decoration(Rect::new(rect.x, rect.y, rect.width, clamp_u16(inner.y - rect.y)));
99 cx.decoration(Rect::new(rect.x, inner.bottom(), rect.width, clamp_u16(rect.bottom() - inner.bottom())));
100 cx.decoration(Rect::new(rect.x, inner.y, clamp_u16(inner.x - rect.x), inner.height));
101 cx.decoration(Rect::new(inner.right(), inner.y, clamp_u16(rect.right() - inner.right()), inner.height));
102}
103
104pub(crate) fn gutter_width(code: &str) -> u16 {
106 let lines = code.split('\n').count();
107 text::width(&lines.to_string()).saturating_add(2)
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum LineMark {
113 #[default]
115 Unchanged,
116 Added,
118 Removed,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum LineTone {
125 #[default]
127 Accent,
128 Warning,
131}
132
133impl LineTone {
134 fn variant(self) -> &'static str {
135 match self {
136 Self::Accent => "accent",
137 Self::Warning => "warning",
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144enum Sign {
145 Icon(&'static str),
147 Pillar,
149}
150
151const REVEAL_CONTEXT: u16 = 2;
154
155#[derive(Debug, Default)]
157struct CodeMemory {
158 revealed: Option<usize>,
160}
161
162pub struct CodeView<Msg> {
181 code: String,
182 language: Language,
183 line_numbers: bool,
184 marks: Vec<LineMark>,
185 numbers: Option<Vec<Option<usize>>>,
187 highlights: Vec<(usize, usize, LineTone)>,
188 reveal: Option<usize>,
189 reveal_number: Option<usize>,
190 on_copy: Option<Msg>,
191}
192
193impl<Msg: 'static> CodeView<Msg> {
194 #[must_use]
196 pub fn new(code: impl Into<String>, language: Language) -> Self {
197 Self {
198 code: code.into(),
199 language,
200 line_numbers: true,
201 marks: Vec::new(),
202 numbers: None,
203 highlights: Vec::new(),
204 reveal: None,
205 reveal_number: None,
206 on_copy: None,
207 }
208 }
209
210 #[must_use]
222 pub fn line_marks(mut self, marks: impl IntoIterator<Item = LineMark>) -> Self {
223 self.marks = marks.into_iter().collect();
224 self
225 }
226
227 #[must_use]
231 pub fn highlight_lines(mut self, lines: impl RangeBounds<usize>, tone: LineTone) -> Self {
232 let first = match lines.start_bound() {
233 Bound::Included(&n) => n,
234 Bound::Excluded(&n) => n.saturating_add(1),
235 Bound::Unbounded => 1,
236 };
237 let last = match lines.end_bound() {
238 Bound::Included(&n) => n,
239 Bound::Excluded(&n) => n.saturating_sub(1),
240 Bound::Unbounded => usize::MAX,
241 };
242 self.highlights.push((first.max(1), last, tone));
243 self
244 }
245
246 #[must_use]
251 pub fn reveal(mut self, line: usize) -> Self {
252 self.reveal = Some(line);
253 self
254 }
255
256 #[must_use]
258 pub fn line_numbers(mut self, show: bool) -> Self {
259 self.line_numbers = show;
260 self
261 }
262
263 #[must_use]
271 pub fn line_numbers_from(mut self, numbers: impl IntoIterator<Item = Option<usize>>) -> Self {
272 self.numbers = Some(numbers.into_iter().collect());
273 self
274 }
275
276 #[must_use]
285 pub fn reveal_number(mut self, number: usize) -> Self {
286 self.reveal_number = Some(number);
287 self
288 }
289
290 fn numbers(&self) -> Vec<Option<usize>> {
293 let lines = self.code.split('\n').count();
294 if let Some(given) = &self.numbers {
295 return (0..lines).map(|index| given.get(index).copied().flatten()).collect();
296 }
297 if self.marks.is_empty() {
298 return (1..=lines).map(Some).collect();
299 }
300 let (mut old, mut new) = (0, 0);
301 (0..lines)
302 .map(|index| match self.marks.get(index) {
303 Some(LineMark::Removed) => {
304 old += 1;
305 Some(old)
306 }
307 Some(LineMark::Added) => {
308 new += 1;
309 Some(new)
310 }
311 Some(LineMark::Unchanged) | None => {
312 old += 1;
313 new += 1;
314 Some(new)
315 }
316 })
317 .collect()
318 }
319
320 fn line_of_number(&self, number: usize) -> Option<usize> {
323 let numbers = self.numbers();
324 let carries = |index: &usize| numbers.get(*index).copied().flatten() == Some(number);
325 let kept = |index: &usize| !matches!(self.marks.get(*index), Some(LineMark::Removed));
326 let index = (0..numbers.len())
327 .find(|index| carries(index) && kept(index))
328 .or_else(|| (0..numbers.len()).find(carries))?;
329 Some(index + 1)
330 }
331
332 #[must_use]
334 pub fn on_copy(mut self, message: Msg) -> Self {
335 self.on_copy = Some(message);
336 self
337 }
338
339 fn gutter(&self) -> u16 {
340 if !self.line_numbers {
341 return 0;
342 }
343 if self.numbers.is_none() && self.marks.is_empty() {
344 return gutter_width(&self.code);
345 }
346 let widest = self.numbers().into_iter().flatten().max().unwrap_or(1);
348 text::width(&widest.to_string()).saturating_add(2)
349 }
350
351 fn signs(&self) -> u16 {
353 if self.marks.is_empty() && self.highlights.is_empty() { 0 } else { 2 }
354 }
355
356 fn look(&self, line: usize) -> Option<(&'static str, Sign)> {
358 if let Some((_, _, tone)) =
359 self.highlights.iter().rev().find(|(first, last, _)| (*first..=*last).contains(&line))
360 {
361 let sign = match tone {
362 LineTone::Accent => Sign::Pillar,
363 LineTone::Warning => Sign::Icon("warning"),
364 };
365 return Some((tone.variant(), sign));
366 }
367 match self.marks.get(line.checked_sub(1)?) {
368 Some(LineMark::Added) => Some(("added", Sign::Icon("line-added"))),
369 Some(LineMark::Removed) => Some(("removed", Sign::Icon("line-removed"))),
370 Some(LineMark::Unchanged) | None => None,
371 }
372 }
373
374 fn numbered_rows(&self, width: u16) -> Vec<CodeRow> {
377 let mut rows = code_rows(&self.code, self.language, width);
378 if self.numbers.is_none() && self.marks.is_empty() {
379 return rows;
380 }
381 let numbers = self.numbers();
382 for row in &mut rows {
383 if row.number.is_some() {
384 row.number = row.line.checked_sub(1).and_then(|index| numbers.get(index).copied().flatten());
385 }
386 }
387 rows
388 }
389
390 fn paint_looks(&self, cx: &mut PaintCx<'_>, area: Rect, x: i32, top: i32, rows: &[CodeRow]) {
392 for (index, row) in rows.iter().enumerate() {
393 let Some((variant, sign)) = self.look(row.line) else { continue };
394 let first_row = index == 0 || rows[index - 1].line != row.line;
397 let y = top + i32::try_from(index).unwrap_or(i32::MAX);
398 let style = cx.style("code-line", Some(variant), &[]);
399 if let Some(bg) = style.color("bg") {
400 cx.fill(Rect::new(area.x, y, area.width, 1), bg);
401 }
402 let color = style.color("fg").unwrap_or_else(|| cx.color("text"));
403 match sign {
404 Sign::Pillar => cx.pillar(x, y, color),
405 Sign::Icon(icon) if first_row => {
406 let glyph = cx.env().icons().glyph(icon).into_owned();
407 cx.text(x, y, &glyph, CellStyle::fg(color), 1);
408 }
409 Sign::Icon(_) => {}
410 }
411 }
412 }
413
414 fn request_reveal(&self, cx: &mut PaintCx<'_>, area: Rect, top: i32, rows: &[CodeRow]) {
416 let asked = match self.reveal_number {
417 Some(number) => self.line_of_number(number),
418 None => self.reveal,
419 };
420 let wanted = asked.map(|line| line.clamp(1, rows.last().map_or(1, |row| row.line)));
421 let memory = cx.memory::<CodeMemory>();
422 if memory.revealed == wanted {
423 return;
424 }
425 memory.revealed = wanted;
426 let Some(line) = wanted else { return };
427 let Some(first) = rows.iter().position(|row| row.line == line) else { return };
428 let count = rows[first..].iter().take_while(|row| row.line == line).count();
429 let context = i32::from(REVEAL_CONTEXT);
430 let y = (top + i32::try_from(first).unwrap_or(i32::MAX) - context).max(area.y);
431 let bottom = (top + i32::try_from(first + count).unwrap_or(i32::MAX) + context).min(area.bottom());
432 cx.reveal(Rect::new(area.x, y, area.width, clamp_u16(bottom - y)));
433 }
434}
435
436impl<Msg: Clone + 'static> Widget<Msg> for CodeView<Msg> {
437 fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
438 let padding = cx.env().theme().style("code", None, &[]).pair("padding").unwrap_or((1, 2));
439 let content_width =
440 available.width.saturating_sub(cells::sum([padding.1.saturating_mul(2), self.signs(), self.gutter()]));
441 let rows = code_rows(&self.code, self.language, content_width.max(1));
442 let widest = rows
443 .iter()
444 .map(|row| cells::sum(row.pieces.iter().map(|(piece, _)| text::width(piece))))
445 .max()
446 .unwrap_or(0);
447 Size::new(
448 cells::sum([widest, self.signs(), self.gutter(), padding.1.saturating_mul(2)]),
449 clamp_u16(i32::try_from(rows.len()).unwrap_or(i32::MAX)).saturating_add(padding.0.saturating_mul(2)),
450 )
451 .min(available)
452 }
453
454 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
455 let mut states = cx.states();
456 states.retain(|state| *state != State::Hover);
457 let style = cx.style("code", None, &states);
458 if let Some(bg) = style.text().bg {
459 cx.clear(area, bg);
460 }
461 cx.register_hit(area);
462 let inner = area.inset(style.padding());
463 cx.selectable(inner);
465 let (signs, gutter) = (self.signs(), self.gutter());
466 let width = inner.width.saturating_sub(signs).saturating_sub(gutter).max(1);
467 let rows = self.numbered_rows(width);
468 if signs > 0 {
469 cx.decoration(Rect::new(inner.x, inner.y, signs, inner.height));
471 self.paint_looks(cx, area, inner.x, inner.y, &rows);
472 }
473 paint_rows(
474 cx,
475 Rect::new(inner.x + i32::from(signs), inner.y, inner.width.saturating_sub(signs), inner.height),
476 &rows,
477 gutter,
478 );
479 self.request_reveal(cx, area, inner.y, &rows);
480 }
481
482 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
483 let Event::Key(key) = event else {
484 return false;
485 };
486 if !key.is_plain(Key::Char('c')) {
487 return false;
488 }
489 cx.copy(self.code.clone());
490 cx.flash();
491 if let Some(message) = &self.on_copy {
492 cx.emit(message.clone());
493 }
494 true
495 }
496
497 fn focusable(&self) -> bool {
498 true
499 }
500}
501
502#[cfg(test)]
503mod tests;