use std::cell::RefCell;
use std::ops::{Bound, Range, RangeBounds};
use std::sync::{Arc, Mutex, PoisonError};
use unicode_segmentation::UnicodeSegmentation;
use super::cells;
use super::highlight::{Language, Token, highlight};
use crate::event::Event;
use crate::geometry::{Rect, Size, clamp_u16};
use crate::keymap::Key;
use crate::style::CellStyle;
use crate::text;
use crate::theme::State;
use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CodeRow {
pub(crate) line: usize,
pub(crate) number: Option<usize>,
pub(crate) pieces: Vec<(String, Token)>,
}
pub(crate) fn code_rows(code: &str, language: Language, width: u16) -> Vec<CodeRow> {
let tokens = highlight(code, language);
let mut rows = Vec::new();
let mut line_start = 0;
let mut first = 0;
for (index, line) in code.split('\n').enumerate() {
let line_end = line_start + line.len();
while first < tokens.len() && tokens[first].0.end <= line_start {
first += 1;
}
let mut row = CodeRow { line: index + 1, number: Some(index + 1), pieces: Vec::new() };
let mut used = 0u16;
for (range, token) in tokens[first..].iter().take_while(|(range, _)| range.start < line_end) {
let start = range.start.max(line_start);
let end = range.end.min(line_end);
if start >= end {
continue;
}
for grapheme in code[start..end].graphemes(true) {
let cell = if grapheme == "\t" { " " } else { grapheme };
let w = text::width(cell);
if used.saturating_add(w) > width && used > 0 {
rows.push(std::mem::replace(
&mut row,
CodeRow { line: index + 1, number: None, pieces: vec![(" ".to_owned(), Token::Plain)] },
));
used = 2;
}
match row.pieces.last_mut() {
Some((piece, last)) if last == token => piece.push_str(cell),
_ => row.pieces.push((cell.to_owned(), *token)),
}
used = used.saturating_add(w);
}
}
rows.push(row);
line_start = line_end + 1;
}
if code.ends_with('\n') {
rows.pop();
}
rows
}
pub(crate) fn paint_rows(cx: &mut PaintCx<'_>, area: Rect, rows: &[CodeRow], gutter: u16) {
let visible = visible_rows(cx, area, rows.len());
for (y, row) in rows.iter().enumerate().skip(visible.start).take(visible.len()) {
let Ok(y) = u16::try_from(y) else { break };
if y >= area.height {
break;
}
let row_y = area.y + i32::from(y);
if gutter > 0 {
cx.decoration(Rect::new(area.x, row_y, gutter, 1));
}
if gutter > 0
&& let Some(number) = row.number
{
let style = cx.style("code-line-number", None, &[]).text();
let label = format!("{number:>width$}", width = usize::from(gutter - 2));
cx.text(area.x, row_y, &label, style, gutter);
}
let mut x = area.x + i32::from(gutter);
for (piece, token) in &row.pieces {
let style = cx.style("code-token", Some(token.variant()), &[]).text();
x += i32::from(cx.text(x, row_y, piece, style, area.right().saturating_sub(x).try_into().unwrap_or(0)));
}
}
}
fn visible_rows(cx: &PaintCx<'_>, area: Rect, count: usize) -> Range<usize> {
let clip = cx.clip();
let top = clip.y.max(area.y);
let bottom = clip.bottom().min(area.bottom()).max(top);
let row = |y: i32| usize::try_from(y - area.y).unwrap_or(0).min(count);
row(top)..row(bottom)
}
pub(crate) fn padding_decoration(cx: &mut PaintCx<'_>, rect: Rect, inner: Rect) {
cx.decoration(Rect::new(rect.x, rect.y, rect.width, clamp_u16(inner.y - rect.y)));
cx.decoration(Rect::new(rect.x, inner.bottom(), rect.width, clamp_u16(rect.bottom() - inner.bottom())));
cx.decoration(Rect::new(rect.x, inner.y, clamp_u16(inner.x - rect.x), inner.height));
cx.decoration(Rect::new(inner.right(), inner.y, clamp_u16(rect.right() - inner.right()), inner.height));
}
pub(crate) fn gutter_width(code: &str) -> u16 {
gutter_for(code.split('\n').count())
}
fn gutter_for(lines: usize) -> u16 {
text::width(&lines.to_string()).saturating_add(2)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineMark {
#[default]
Unchanged,
Added,
Removed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineTone {
#[default]
Accent,
Warning,
}
impl LineTone {
fn variant(self) -> &'static str {
match self {
Self::Accent => "accent",
Self::Warning => "warning",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Sign {
Icon(&'static str),
Pillar,
}
const REVEAL_CONTEXT: u16 = 2;
#[derive(Debug, Default)]
struct CodeMemory {
revealed: Option<usize>,
}
const CACHED_SOURCES: usize = 16;
const CACHED_LAYOUTS: usize = 4;
thread_local! {
static SOURCES: RefCell<Vec<Arc<Source>>> = const { RefCell::new(Vec::new()) };
}
struct Source {
code: String,
language: Language,
lines: usize,
layouts: Mutex<Vec<Arc<Layout>>>,
}
struct Layout {
width: u16,
rows: Vec<CodeRow>,
widest: u16,
}
impl Source {
fn cached(code: String, language: Language) -> Arc<Self> {
SOURCES.with_borrow_mut(|sources| {
let source = match sources.iter().position(|source| source.language == language && source.code == code) {
Some(index) => sources.remove(index),
None => {
let lines = code.split('\n').count();
Arc::new(Self { code, language, lines, layouts: Mutex::new(Vec::new()) })
}
};
sources.insert(0, Arc::clone(&source));
sources.truncate(CACHED_SOURCES);
source
})
}
fn layout(&self, width: u16) -> Arc<Layout> {
let mut layouts = self.layouts.lock().unwrap_or_else(PoisonError::into_inner);
let layout = match layouts.iter().position(|layout| layout.width == width) {
Some(index) => layouts.remove(index),
None => {
let rows = code_rows(&self.code, self.language, width);
let widest = rows
.iter()
.map(|row| cells::sum(row.pieces.iter().map(|(piece, _)| text::width(piece))))
.max()
.unwrap_or(0);
Arc::new(Layout { width, rows, widest })
}
};
layouts.insert(0, Arc::clone(&layout));
layouts.truncate(CACHED_LAYOUTS);
layout
}
}
pub struct CodeView<Msg> {
source: Arc<Source>,
line_numbers: bool,
marks: Vec<LineMark>,
numbers: Option<Vec<Option<usize>>>,
highlights: Vec<(usize, usize, LineTone)>,
reveal: Option<usize>,
reveal_number: Option<usize>,
on_copy: Option<Msg>,
}
impl<Msg: 'static> CodeView<Msg> {
#[must_use]
pub fn new(code: impl Into<String>, language: Language) -> Self {
Self {
source: Source::cached(code.into(), language),
line_numbers: true,
marks: Vec::new(),
numbers: None,
highlights: Vec::new(),
reveal: None,
reveal_number: None,
on_copy: None,
}
}
#[must_use]
pub fn line_marks(mut self, marks: impl IntoIterator<Item = LineMark>) -> Self {
self.marks = marks.into_iter().collect();
self
}
#[must_use]
pub fn highlight_lines(mut self, lines: impl RangeBounds<usize>, tone: LineTone) -> Self {
let first = match lines.start_bound() {
Bound::Included(&n) => n,
Bound::Excluded(&n) => n.saturating_add(1),
Bound::Unbounded => 1,
};
let last = match lines.end_bound() {
Bound::Included(&n) => n,
Bound::Excluded(&n) => n.saturating_sub(1),
Bound::Unbounded => usize::MAX,
};
self.highlights.push((first.max(1), last, tone));
self
}
#[must_use]
pub fn reveal(mut self, line: usize) -> Self {
self.reveal = Some(line);
self
}
#[must_use]
pub fn line_numbers(mut self, show: bool) -> Self {
self.line_numbers = show;
self
}
#[must_use]
pub fn line_numbers_from(mut self, numbers: impl IntoIterator<Item = Option<usize>>) -> Self {
self.numbers = Some(numbers.into_iter().collect());
self
}
#[must_use]
pub fn reveal_number(mut self, number: usize) -> Self {
self.reveal_number = Some(number);
self
}
fn numbers(&self) -> Vec<Option<usize>> {
let lines = self.source.lines;
if let Some(given) = &self.numbers {
return (0..lines).map(|index| given.get(index).copied().flatten()).collect();
}
if self.marks.is_empty() {
return (1..=lines).map(Some).collect();
}
let (mut old, mut new) = (0, 0);
(0..lines)
.map(|index| match self.marks.get(index) {
Some(LineMark::Removed) => {
old += 1;
Some(old)
}
Some(LineMark::Added) => {
new += 1;
Some(new)
}
Some(LineMark::Unchanged) | None => {
old += 1;
new += 1;
Some(new)
}
})
.collect()
}
fn line_of_number(&self, number: usize) -> Option<usize> {
let numbers = self.numbers();
let carries = |index: &usize| numbers.get(*index).copied().flatten() == Some(number);
let kept = |index: &usize| !matches!(self.marks.get(*index), Some(LineMark::Removed));
let index = (0..numbers.len())
.find(|index| carries(index) && kept(index))
.or_else(|| (0..numbers.len()).find(carries))?;
Some(index + 1)
}
#[must_use]
pub fn on_copy(mut self, message: Msg) -> Self {
self.on_copy = Some(message);
self
}
fn gutter(&self) -> u16 {
if !self.line_numbers {
return 0;
}
if self.numbers.is_none() && self.marks.is_empty() {
return gutter_for(self.source.lines);
}
let widest = self.numbers().into_iter().flatten().max().unwrap_or(1);
text::width(&widest.to_string()).saturating_add(2)
}
fn signs(&self) -> u16 {
if self.marks.is_empty() && self.highlights.is_empty() { 0 } else { 2 }
}
fn look(&self, line: usize) -> Option<(&'static str, Sign)> {
if let Some((_, _, tone)) =
self.highlights.iter().rev().find(|(first, last, _)| (*first..=*last).contains(&line))
{
let sign = match tone {
LineTone::Accent => Sign::Pillar,
LineTone::Warning => Sign::Icon("warning"),
};
return Some((tone.variant(), sign));
}
match self.marks.get(line.checked_sub(1)?) {
Some(LineMark::Added) => Some(("added", Sign::Icon("line-added"))),
Some(LineMark::Removed) => Some(("removed", Sign::Icon("line-removed"))),
Some(LineMark::Unchanged) | None => None,
}
}
fn renumbered(&self, rows: &[CodeRow]) -> Option<Vec<CodeRow>> {
if self.numbers.is_none() && self.marks.is_empty() {
return None;
}
let numbers = self.numbers();
let mut rows = rows.to_vec();
for row in &mut rows {
if row.number.is_some() {
row.number = row.line.checked_sub(1).and_then(|index| numbers.get(index).copied().flatten());
}
}
Some(rows)
}
fn paint_looks(&self, cx: &mut PaintCx<'_>, area: Rect, x: i32, top: i32, rows: &[CodeRow]) {
let visible = visible_rows(cx, Rect::new(area.x, top, area.width, clamp_u16(area.bottom() - top)), rows.len());
for (index, row) in rows.iter().enumerate().skip(visible.start).take(visible.len()) {
let Some((variant, sign)) = self.look(row.line) else { continue };
let first_row = index == 0 || rows[index - 1].line != row.line;
let y = top + i32::try_from(index).unwrap_or(i32::MAX);
let style = cx.style("code-line", Some(variant), &[]);
if let Some(bg) = style.color("bg") {
cx.fill(Rect::new(area.x, y, area.width, 1), bg);
}
let color = style.color("fg").unwrap_or_else(|| cx.color("text"));
match sign {
Sign::Pillar => cx.pillar(x, y, color),
Sign::Icon(icon) if first_row => {
let glyph = cx.env().icons().glyph(icon).into_owned();
cx.text(x, y, &glyph, CellStyle::fg(color), 1);
}
Sign::Icon(_) => {}
}
}
}
fn request_reveal(&self, cx: &mut PaintCx<'_>, area: Rect, top: i32, rows: &[CodeRow]) {
let asked = match self.reveal_number {
Some(number) => self.line_of_number(number),
None => self.reveal,
};
let wanted = asked.map(|line| line.clamp(1, rows.last().map_or(1, |row| row.line)));
let memory = cx.memory::<CodeMemory>();
if memory.revealed == wanted {
return;
}
memory.revealed = wanted;
let Some(line) = wanted else { return };
let Some(first) = rows.iter().position(|row| row.line == line) else { return };
let count = rows[first..].iter().take_while(|row| row.line == line).count();
let context = i32::from(REVEAL_CONTEXT);
let y = (top + i32::try_from(first).unwrap_or(i32::MAX) - context).max(area.y);
let bottom = (top + i32::try_from(first + count).unwrap_or(i32::MAX) + context).min(area.bottom());
cx.reveal(Rect::new(area.x, y, area.width, clamp_u16(bottom - y)));
}
}
impl<Msg: Clone + 'static> Widget<Msg> for CodeView<Msg> {
fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
let padding = cx.env().theme().style("code", None, &[]).pair("padding").unwrap_or((1, 2));
let content_width =
available.width.saturating_sub(cells::sum([padding.1.saturating_mul(2), self.signs(), self.gutter()]));
let layout = self.source.layout(content_width.max(1));
Size::new(
cells::sum([layout.widest, self.signs(), self.gutter(), padding.1.saturating_mul(2)]),
clamp_u16(i32::try_from(layout.rows.len()).unwrap_or(i32::MAX)).saturating_add(padding.0.saturating_mul(2)),
)
.min(available)
}
fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
let mut states = cx.states();
states.retain(|state| *state != State::Hover);
let style = cx.style("code", None, &states);
if let Some(bg) = style.text().bg {
cx.clear(area, bg);
}
cx.register_hit(area);
let inner = area.inset(style.padding());
cx.selectable(inner);
let (signs, gutter) = (self.signs(), self.gutter());
let width = inner.width.saturating_sub(signs).saturating_sub(gutter).max(1);
let layout = self.source.layout(width);
let renumbered = self.renumbered(&layout.rows);
let rows = renumbered.as_deref().unwrap_or(&layout.rows);
if signs > 0 {
cx.decoration(Rect::new(inner.x, inner.y, signs, inner.height));
self.paint_looks(cx, area, inner.x, inner.y, rows);
}
paint_rows(
cx,
Rect::new(inner.x + i32::from(signs), inner.y, inner.width.saturating_sub(signs), inner.height),
rows,
gutter,
);
self.request_reveal(cx, area, inner.y, rows);
}
fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
let Event::Key(key) = event else {
return false;
};
if !key.is_plain(Key::Char('c')) {
return false;
}
cx.copy(self.source.code.clone());
cx.flash();
if let Some(message) = &self.on_copy {
cx.emit(message.clone());
}
true
}
fn focusable(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests;