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 highlights: Vec<(usize, usize, LineTone)>,
186 reveal: Option<usize>,
187 on_copy: Option<Msg>,
188}
189
190impl<Msg: 'static> CodeView<Msg> {
191 #[must_use]
193 pub fn new(code: impl Into<String>, language: Language) -> Self {
194 Self {
195 code: code.into(),
196 language,
197 line_numbers: true,
198 marks: Vec::new(),
199 highlights: Vec::new(),
200 reveal: None,
201 on_copy: None,
202 }
203 }
204
205 #[must_use]
210 pub fn line_marks(mut self, marks: impl IntoIterator<Item = LineMark>) -> Self {
211 self.marks = marks.into_iter().collect();
212 self
213 }
214
215 #[must_use]
219 pub fn highlight_lines(mut self, lines: impl RangeBounds<usize>, tone: LineTone) -> Self {
220 let first = match lines.start_bound() {
221 Bound::Included(&n) => n,
222 Bound::Excluded(&n) => n.saturating_add(1),
223 Bound::Unbounded => 1,
224 };
225 let last = match lines.end_bound() {
226 Bound::Included(&n) => n,
227 Bound::Excluded(&n) => n.saturating_sub(1),
228 Bound::Unbounded => usize::MAX,
229 };
230 self.highlights.push((first.max(1), last, tone));
231 self
232 }
233
234 #[must_use]
239 pub fn reveal(mut self, line: usize) -> Self {
240 self.reveal = Some(line);
241 self
242 }
243
244 #[must_use]
246 pub fn line_numbers(mut self, show: bool) -> Self {
247 self.line_numbers = show;
248 self
249 }
250
251 #[must_use]
253 pub fn on_copy(mut self, message: Msg) -> Self {
254 self.on_copy = Some(message);
255 self
256 }
257
258 fn gutter(&self) -> u16 {
259 if self.line_numbers { gutter_width(&self.code) } else { 0 }
260 }
261
262 fn signs(&self) -> u16 {
264 if self.marks.is_empty() && self.highlights.is_empty() { 0 } else { 2 }
265 }
266
267 fn look(&self, line: usize) -> Option<(&'static str, Sign)> {
269 if let Some((_, _, tone)) =
270 self.highlights.iter().rev().find(|(first, last, _)| (*first..=*last).contains(&line))
271 {
272 let sign = match tone {
273 LineTone::Accent => Sign::Pillar,
274 LineTone::Warning => Sign::Icon("warning"),
275 };
276 return Some((tone.variant(), sign));
277 }
278 match self.marks.get(line.checked_sub(1)?) {
279 Some(LineMark::Added) => Some(("added", Sign::Icon("line-added"))),
280 Some(LineMark::Removed) => Some(("removed", Sign::Icon("line-removed"))),
281 Some(LineMark::Unchanged) | None => None,
282 }
283 }
284
285 fn paint_looks(&self, cx: &mut PaintCx<'_>, area: Rect, x: i32, top: i32, rows: &[CodeRow]) {
287 for (index, row) in rows.iter().enumerate() {
288 let Some((variant, sign)) = self.look(row.line) else { continue };
289 let y = top + i32::try_from(index).unwrap_or(i32::MAX);
290 let style = cx.style("code-line", Some(variant), &[]);
291 if let Some(bg) = style.color("bg") {
292 cx.fill(Rect::new(area.x, y, area.width, 1), bg);
293 }
294 let color = style.color("fg").unwrap_or_else(|| cx.color("text"));
295 match sign {
296 Sign::Pillar => cx.pillar(x, y, color),
297 Sign::Icon(icon) if row.number.is_some() => {
298 let glyph = cx.env().icons().glyph(icon).into_owned();
299 cx.text(x, y, &glyph, CellStyle::fg(color), 1);
300 }
301 Sign::Icon(_) => {}
302 }
303 }
304 }
305
306 fn request_reveal(&self, cx: &mut PaintCx<'_>, area: Rect, top: i32, rows: &[CodeRow]) {
308 let wanted = self.reveal.map(|line| line.clamp(1, rows.last().map_or(1, |row| row.line)));
309 let memory = cx.memory::<CodeMemory>();
310 if memory.revealed == wanted {
311 return;
312 }
313 memory.revealed = wanted;
314 let Some(line) = wanted else { return };
315 let Some(first) = rows.iter().position(|row| row.line == line) else { return };
316 let count = rows[first..].iter().take_while(|row| row.line == line).count();
317 let context = i32::from(REVEAL_CONTEXT);
318 let y = (top + i32::try_from(first).unwrap_or(i32::MAX) - context).max(area.y);
319 let bottom = (top + i32::try_from(first + count).unwrap_or(i32::MAX) + context).min(area.bottom());
320 cx.reveal(Rect::new(area.x, y, area.width, clamp_u16(bottom - y)));
321 }
322}
323
324impl<Msg: Clone + 'static> Widget<Msg> for CodeView<Msg> {
325 fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
326 let padding = cx.env().theme().style("code", None, &[]).pair("padding").unwrap_or((1, 2));
327 let content_width =
328 available.width.saturating_sub(cells::sum([padding.1.saturating_mul(2), self.signs(), self.gutter()]));
329 let rows = code_rows(&self.code, self.language, content_width.max(1));
330 let widest = rows
331 .iter()
332 .map(|row| cells::sum(row.pieces.iter().map(|(piece, _)| text::width(piece))))
333 .max()
334 .unwrap_or(0);
335 Size::new(
336 cells::sum([widest, self.signs(), self.gutter(), padding.1.saturating_mul(2)]),
337 clamp_u16(i32::try_from(rows.len()).unwrap_or(i32::MAX)).saturating_add(padding.0.saturating_mul(2)),
338 )
339 .min(available)
340 }
341
342 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
343 let mut states = cx.states();
344 states.retain(|state| *state != State::Hover);
345 let style = cx.style("code", None, &states);
346 if let Some(bg) = style.text().bg {
347 cx.clear(area, bg);
348 }
349 cx.register_hit(area);
350 let inner = area.inset(style.padding());
351 cx.selectable(inner);
353 let (signs, gutter) = (self.signs(), self.gutter());
354 let width = inner.width.saturating_sub(signs).saturating_sub(gutter).max(1);
355 let rows = code_rows(&self.code, self.language, width);
356 if signs > 0 {
357 cx.decoration(Rect::new(inner.x, inner.y, signs, inner.height));
359 self.paint_looks(cx, area, inner.x, inner.y, &rows);
360 }
361 paint_rows(
362 cx,
363 Rect::new(inner.x + i32::from(signs), inner.y, inner.width.saturating_sub(signs), inner.height),
364 &rows,
365 gutter,
366 );
367 self.request_reveal(cx, area, inner.y, &rows);
368 }
369
370 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
371 let Event::Key(key) = event else {
372 return false;
373 };
374 if !key.is_plain(Key::Char('c')) {
375 return false;
376 }
377 cx.copy(self.code.clone());
378 cx.flash();
379 if let Some(message) = &self.on_copy {
380 cx.emit(message.clone());
381 }
382 true
383 }
384
385 fn focusable(&self) -> bool {
386 true
387 }
388}
389
390#[cfg(test)]
391mod tests;