#[cfg(feature = "syntax-highlighting")]
use crate::SyntaxHighlighter;
use crate::{
helper::{char_width, span_width, split_str_at},
state::highlight::Highlight,
state::selection::Selection,
};
use jagged::Index2;
use ratatui_core::{style::Style, text::Span};
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct InternalSpan {
pub(crate) content: String,
pub(crate) style: Style,
}
impl InternalSpan {
pub(crate) fn new<T: Into<String>>(content: T, style: &Style) -> Self {
Self {
content: content.into(),
style: *style,
}
}
pub(crate) fn spans_len(spans: &[Self]) -> usize {
spans.iter().fold(0, |sum, span| sum + span.content.len())
}
fn split_at_selection(
spans: &[Self],
row_index: usize,
selection: &Selection,
style: &Style,
) -> Option<Vec<InternalSpan>> {
let spans_len = InternalSpan::spans_len(spans);
let (start_col, end_col) = selection.get_selected_columns_in_row(row_index, spans_len)?;
debug_assert!(end_col >= start_col, "{start_col} {end_col}");
Some(Self::split_spans(spans, start_col, end_col, style))
}
fn split_at_highlight(
spans: &[Self],
row_index: usize,
highlight: &Highlight,
) -> Option<Vec<InternalSpan>> {
let spans_len = InternalSpan::spans_len(spans);
let (start_col, end_col) = highlight.get_columns_in_row(row_index, spans_len)?;
if end_col < start_col {
return None;
}
Some(Self::split_spans(
spans,
start_col,
end_col,
&highlight.style,
))
}
fn crop_spans(spans: &mut Vec<Self>, crop_at: usize) {
if crop_at == 0 {
return;
}
let mut span_offset = 0;
let mut first_visible_span = 0;
let mut split_span_at = 0;
for (i, span) in spans.iter().enumerate() {
let span_width = span.content.len();
let span_start = span_offset;
let span_end = span_offset + span_width;
if span_start >= crop_at {
break;
}
if span_end > crop_at {
split_span_at = crop_at - span_start;
break;
}
first_visible_span = i + 1;
span_offset += span_width;
}
if first_visible_span > 0 {
spans.drain(0..first_visible_span);
}
if split_span_at > 0 {
let first_span = spans.remove(0);
let (_, right) = split_str_at(&first_span.content, split_span_at);
spans.insert(0, InternalSpan::new(right, &first_span.style));
}
}
fn split_spans(
spans: &[Self],
split_start: usize,
split_end: usize,
style: &Style,
) -> Vec<Self> {
let mut new_spans: Vec<InternalSpan> = Vec::new();
let mut offset = 0;
for span in spans {
let span_len = span.content.chars().count();
let span_start = offset;
let span_end = offset + span_len.saturating_sub(1);
if span_end < split_start || span_start > split_end {
new_spans.push(span.clone());
}
else if split_start <= span_start && split_end < span_end {
let split_point = split_end - span_start + 1;
let (left, right) = split_str_at(&span.content, split_point);
new_spans.push(InternalSpan::new(left, style));
new_spans.push(InternalSpan::new(right, &span.style));
}
else if split_start > span_start && split_end >= span_end {
let split_point = split_start - span_start;
let (left, right) = split_str_at(&span.content, split_point);
new_spans.push(InternalSpan::new(left, &span.style));
new_spans.push(InternalSpan::new(right, style));
}
else if split_start > span_start && split_end < span_end {
let split_front = split_start - span_start;
let split_back = split_end - span_start + 1;
let (left, rest) = split_str_at(&span.content, split_front);
let (middle, right) = split_str_at(&rest, split_back - split_front);
new_spans.push(InternalSpan::new(left, &span.style));
new_spans.push(InternalSpan::new(middle, style));
new_spans.push(InternalSpan::new(right, &span.style));
}
else if split_start <= span_start && split_end >= span_end {
new_spans.push(InternalSpan::new(span.content.clone(), style));
}
offset += span_len;
}
new_spans
}
}
impl From<Span<'_>> for InternalSpan {
fn from(value: Span) -> Self {
Self::new(value.content, &value.style)
}
}
impl From<InternalSpan> for Span<'_> {
fn from(value: InternalSpan) -> Self {
Self::styled(value.content, value.style)
}
}
pub(crate) fn line_into_spans_with_selections<'a>(
line: &[char],
selections: &[&Option<Selection>],
highlights: &[Highlight],
row_index: usize,
col_skips: usize,
base_style: &Style,
selection_style: &Style,
) -> Vec<Span<'a>> {
let mut spans = Vec::new();
let mut current_span = String::new();
let mut current_style = *base_style;
for (i, &ch) in line.iter().skip(col_skips).enumerate() {
let position = Index2::new(row_index, col_skips + i);
let new_style = if selections
.iter()
.filter_map(|s| s.as_ref())
.any(|s| s.contains(&position))
{
*selection_style
} else if let Some(h) = highlights.iter().find(|h| h.contains(&position)) {
h.style
} else {
*base_style
};
if i != 0 && new_style != current_style {
spans.push(Span::styled(
std::mem::take(&mut current_span),
current_style,
));
}
current_style = new_style;
current_span.push(ch);
}
if !current_span.is_empty() {
spans.push(Span::styled(current_span, current_style));
}
spans
}
#[allow(clippy::too_many_arguments)]
#[cfg(feature = "syntax-highlighting")]
pub(crate) fn line_into_highlighted_spans_with_selections<'a>(
line: &[char],
selections: &[&Option<Selection>],
highlights: &[Highlight],
syntax_highligher: &SyntaxHighlighter,
row_index: usize,
col_skips: usize,
base_style: &Style,
selection_style: &Style,
) -> Vec<Span<'a>> {
let line: String = line.iter().collect();
let mut internal_spans = syntax_highligher.highlight_line(&line, base_style);
for highlight in highlights.iter().filter(|h| h.contains_row(row_index)) {
if let Some(new_spans) =
InternalSpan::split_at_highlight(&internal_spans, row_index, highlight)
{
internal_spans = new_spans;
}
}
let selections = selections
.iter()
.filter_map(|selection| selection.as_ref().filter(|s| s.contains_row(row_index)));
for selection in selections {
if let Some(new_span) =
InternalSpan::split_at_selection(&internal_spans, row_index, selection, selection_style)
{
internal_spans = new_span;
}
}
if col_skips > 0 {
InternalSpan::crop_spans(&mut internal_spans, col_skips);
}
internal_spans.into_iter().map(Span::from).collect()
}
pub(super) fn find_position_in_wrapped_spans(
wrapped_spans: &[Vec<Span>],
col_index: usize,
max_width: usize,
tab_width: usize,
) -> Index2 {
if wrapped_spans.is_empty() {
return Index2::new(0, col_index);
}
let mut char_pos = col_index;
for (row, spans) in wrapped_spans.iter().enumerate() {
let row_char_count = count_characters_in_spans(spans);
let max_char_pos = row_char_count.saturating_sub(1);
if char_pos <= max_char_pos {
let col = unicode_width_position_in_spans(spans, char_pos, tab_width);
return Index2::new(row, col);
}
if row + 1 < wrapped_spans.len() {
char_pos -= row_char_count;
}
}
let last_span_width = match wrapped_spans.last() {
Some(span) => spans_width(span, tab_width),
None => 0,
};
if last_span_width >= max_width {
Index2::new(wrapped_spans.len(), 0)
} else {
Index2::new(wrapped_spans.len().saturating_sub(1), last_span_width)
}
}
pub(super) fn unicode_width_position_in_spans(spans: &[Span], n: usize, tab_width: usize) -> usize {
let mut total_width = 0;
let mut chars_counted = 0;
for span in spans {
for ch in span.content.chars() {
if chars_counted >= n {
return total_width;
}
total_width += char_width(ch, tab_width);
chars_counted += 1;
}
}
total_width
}
pub(crate) fn find_position_in_spans(spans: &[Span], char_pos: usize, tab_width: usize) -> Index2 {
if spans.is_empty() {
return Index2::new(0, char_pos);
}
Index2::new(
0,
unicode_width_position_in_spans(spans, char_pos, tab_width),
)
}
fn count_characters_in_spans(spans: &[Span]) -> usize {
spans.iter().map(|span| span.content.chars().count()).sum()
}
fn spans_width(spans: &[Span], tab_width: usize) -> usize {
spans
.iter()
.fold(0, |sum, span| sum + span_width(span, tab_width))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_internal_line_into_spans() {
let base = Style::default();
let hightlighted = Style::default().red();
let line = "Hello".chars().into_iter().collect::<Vec<char>>();
let selection = Some(Selection::new(Index2::new(0, 0), Index2::new(0, 2)));
let selections = vec![&selection];
let spans =
line_into_spans_with_selections(&line, &selections, &[], 0, 0, &base, &hightlighted);
assert_eq!(spans[0], Span::styled("Hel", hightlighted));
assert_eq!(spans[1], Span::styled("lo", base));
}
#[test]
fn test_internal_span_split_spans() {
let base = &Style::default();
let hightlighted = &Style::default().red();
let spans = vec![
InternalSpan::new("Hel", base),
InternalSpan::new("lo!", base),
];
let new_spans = InternalSpan::split_spans(&spans, 1, 1, &hightlighted);
assert_eq!(new_spans[0], InternalSpan::new("H", base));
assert_eq!(new_spans[1], InternalSpan::new("e", hightlighted));
assert_eq!(new_spans[2], InternalSpan::new("l", base));
assert_eq!(new_spans[3], InternalSpan::new("lo!", base));
let new_spans = InternalSpan::split_spans(&spans, 1, 2, &hightlighted);
assert_eq!(new_spans[0], InternalSpan::new("H", base));
assert_eq!(new_spans[1], InternalSpan::new("el", hightlighted));
assert_eq!(new_spans[2], InternalSpan::new("lo!", base));
let new_spans = InternalSpan::split_spans(&spans, 1, 3, &hightlighted);
assert_eq!(new_spans[0], InternalSpan::new("H", base));
assert_eq!(new_spans[1], InternalSpan::new("el", hightlighted));
assert_eq!(new_spans[2], InternalSpan::new("l", hightlighted));
assert_eq!(new_spans[3], InternalSpan::new("o!", base));
let new_spans = InternalSpan::split_spans(&spans, 1, 10, &hightlighted);
assert_eq!(new_spans[0], InternalSpan::new("H", base));
assert_eq!(new_spans[1], InternalSpan::new("el", hightlighted));
assert_eq!(new_spans[2], InternalSpan::new("lo!", hightlighted));
}
#[test]
fn test_split_spans_with_emoji() {
let base = &Style::default();
let hightlighted = &Style::default().red();
let spans = vec![InternalSpan::new("Hell๐!", base)];
let new_spans = InternalSpan::split_spans(&spans, 2, 4, &hightlighted);
assert_eq!(new_spans[0], InternalSpan::new("He", base));
assert_eq!(new_spans[1], InternalSpan::new("ll๐", hightlighted));
assert_eq!(new_spans[2], InternalSpan::new("!", base));
}
#[test]
fn test_internal_span_crop_spans() {
let base = &Style::default();
let original_spans = vec![
InternalSpan::new("Hel", base),
InternalSpan::new("lo", base),
InternalSpan::new("World!", base),
];
let mut spans = original_spans.clone();
InternalSpan::crop_spans(&mut spans, 1);
assert_eq!(spans[0], InternalSpan::new("el", base));
assert_eq!(spans[1], InternalSpan::new("lo", base));
assert_eq!(spans[2], InternalSpan::new("World!", base));
let mut spans = original_spans.clone();
InternalSpan::crop_spans(&mut spans, 2);
assert_eq!(spans[0], InternalSpan::new("l", base));
assert_eq!(spans[1], InternalSpan::new("lo", base));
assert_eq!(spans[2], InternalSpan::new("World!", base));
let mut spans = original_spans.clone();
InternalSpan::crop_spans(&mut spans, 3);
assert_eq!(spans[0], InternalSpan::new("lo", base));
assert_eq!(spans[1], InternalSpan::new("World!", base));
}
#[test]
fn test_internal_span_apply_selection() {
let base = &Style::default();
let hightlighted = &Style::default().red();
let spans = vec![
InternalSpan::new("Hel", base),
InternalSpan::new("lo!", base),
];
let selection = Selection::new(Index2::new(0, 1), Index2::new(0, 3));
let new_spans =
InternalSpan::split_at_selection(&spans, 0, &selection, &hightlighted).unwrap();
assert_eq!(new_spans[0], InternalSpan::new("H", base));
assert_eq!(new_spans[1], InternalSpan::new("el", hightlighted));
assert_eq!(new_spans[2], InternalSpan::new("l", hightlighted));
assert_eq!(new_spans[3], InternalSpan::new("o!", base));
}
#[test]
fn test_internal_span_apply_selection_with_emoji() {
let base = &Style::default();
let hightlighted = &Style::default().red();
let spans = vec![
InternalSpan::new("Hell๐", base),
InternalSpan::new("!", base),
];
let selection = Selection::new(Index2::new(0, 3), Index2::new(0, 5));
let new_spans =
InternalSpan::split_at_selection(&spans, 0, &selection, &hightlighted).unwrap();
assert_eq!(new_spans[0], InternalSpan::new("Hel", base));
assert_eq!(new_spans[1], InternalSpan::new("l๐", hightlighted));
assert_eq!(new_spans[2], InternalSpan::new("!", hightlighted));
}
#[test]
fn test_unicode_width_position_in_spans() {
let spans = vec![Span::from("a๐b"), Span::from("c๐d")];
let index = unicode_width_position_in_spans(&spans, 2, 0);
assert_eq!(index, 3);
let index = unicode_width_position_in_spans(&spans, 5, 0);
assert_eq!(index, 7);
let index = unicode_width_position_in_spans(&spans, 99, 0);
assert_eq!(index, 8);
}
#[test]
fn test_find_position_in_wrapped_spans() {
let line_1 = vec![Span::from("abc")];
let line_2 = vec![Span::from("def")];
let spans = vec![line_1, line_2];
let position = find_position_in_wrapped_spans(&spans, 2, 3, 0);
assert_eq!(position, Index2::new(0, 2));
let position = find_position_in_wrapped_spans(&spans, 3, 3, 0);
assert_eq!(position, Index2::new(1, 0));
let position = find_position_in_wrapped_spans(&spans, 5, 3, 0);
assert_eq!(position, Index2::new(1, 2));
let position = find_position_in_wrapped_spans(&spans, 6, 3, 0);
assert_eq!(position, Index2::new(2, 0));
}
#[test]
fn test_find_position_in_wrapped_spans_with_emoji() {
let line_1 = vec![Span::from("a๐b")];
let line_2 = vec![Span::from("c๐")];
let spans = vec![line_1, line_2];
let position = find_position_in_wrapped_spans(&spans, 2, 4, 0);
assert_eq!(position, Index2::new(0, 3));
let position = find_position_in_wrapped_spans(&spans, 3, 4, 0);
assert_eq!(position, Index2::new(1, 0));
let position = find_position_in_wrapped_spans(&spans, 4, 4, 0);
assert_eq!(position, Index2::new(1, 1));
let position = find_position_in_wrapped_spans(&spans, 5, 4, 0);
assert_eq!(position, Index2::new(1, 3));
}
}