use crate::primitives::display_width::{char_width, str_width};
use crate::primitives::highlighter::HighlightSpan;
use crate::view::overlay::{Overlay, OverlayFace};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;
use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;
pub(super) fn compute_inline_diff(
old_text: &str,
new_text: &str,
) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
let old_chars: Vec<char> = old_text.chars().collect();
let new_chars: Vec<char> = new_text.chars().collect();
let mut old_ranges = Vec::new();
let mut new_ranges = Vec::new();
let prefix_len = old_chars
.iter()
.zip(new_chars.iter())
.take_while(|(a, b)| a == b)
.count();
let old_remaining = old_chars.len() - prefix_len;
let new_remaining = new_chars.len() - prefix_len;
let suffix_len = old_chars
.iter()
.rev()
.zip(new_chars.iter().rev())
.take(old_remaining.min(new_remaining))
.take_while(|(a, b)| a == b)
.count();
let old_start = prefix_len;
let old_end = old_chars.len().saturating_sub(suffix_len);
let new_start = prefix_len;
let new_end = new_chars.len().saturating_sub(suffix_len);
if old_start < old_end {
old_ranges.push(old_start..old_end);
}
if new_start < new_end {
new_ranges.push(new_start..new_end);
}
(old_ranges, new_ranges)
}
pub(super) fn push_span_with_map(
spans: &mut Vec<Span<'static>>,
map: &mut Vec<Option<usize>>,
text: String,
style: Style,
source: Option<usize>,
) {
if text.is_empty() {
return;
}
for grapheme in text.graphemes(true) {
let width = str_width(grapheme);
for _ in 0..width {
map.push(source);
}
}
spans.push(Span::styled(text, style));
}
pub(super) fn debug_tag_style() -> Style {
Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::DIM)
}
pub(super) fn push_debug_tag(
spans: &mut Vec<Span<'static>>,
map: &mut Vec<Option<usize>>,
text: String,
) {
if text.is_empty() {
return;
}
for ch in text.chars() {
let width = char_width(ch);
for _ in 0..width {
map.push(None);
}
}
spans.push(Span::styled(text, debug_tag_style()));
}
pub(super) struct SpanAccumulator {
text: String,
style: Style,
first_source: Option<usize>,
}
impl SpanAccumulator {
pub(super) fn new() -> Self {
Self {
text: String::new(),
style: Style::default(),
first_source: None,
}
}
pub(super) fn push(
&mut self,
ch: char,
style: Style,
source: Option<usize>,
spans: &mut Vec<Span<'static>>,
map: &mut Vec<Option<usize>>,
) {
if !self.text.is_empty() && style != self.style {
self.flush(spans, map);
}
if self.text.is_empty() {
self.style = style;
self.first_source = source;
}
let width_before = str_width(&self.text);
self.text.push(ch);
let width_after = str_width(&self.text);
let delta = width_after.saturating_sub(width_before);
for _ in 0..delta {
map.push(source);
}
}
pub(super) fn flush(&mut self, spans: &mut Vec<Span<'static>>, _map: &mut Vec<Option<usize>>) {
if !self.text.is_empty() {
spans.push(Span::styled(std::mem::take(&mut self.text), self.style));
self.first_source = None;
}
}
}
#[derive(Default)]
pub(super) struct DebugSpanTracker {
active_highlight: Option<Range<usize>>,
active_overlays: Vec<Range<usize>>,
}
impl DebugSpanTracker {
pub(super) fn get_opening_tags(
&mut self,
byte_pos: Option<usize>,
highlight_spans: &[HighlightSpan],
viewport_overlays: &[(Overlay, Range<usize>)],
) -> Vec<String> {
let mut tags = Vec::new();
if let Some(bp) = byte_pos {
if let Some(span) = highlight_spans.iter().find(|s| s.range.start == bp) {
tags.push(format!("<hl:{}-{}>", span.range.start, span.range.end));
self.active_highlight = Some(span.range.clone());
}
for (overlay, range) in viewport_overlays.iter() {
if range.start == bp {
let overlay_type = match &overlay.face {
OverlayFace::Underline { .. } => "ul",
OverlayFace::Background { .. } => "bg",
OverlayFace::Foreground { .. } => "fg",
OverlayFace::Style { .. } => "st",
OverlayFace::ThemedStyle { .. } => "ts",
};
tags.push(format!("<{}:{}-{}>", overlay_type, range.start, range.end));
self.active_overlays.push(range.clone());
}
}
}
tags
}
pub(super) fn get_closing_tags(&mut self, byte_pos: Option<usize>) -> Vec<String> {
let mut tags = Vec::new();
if let Some(bp) = byte_pos {
if let Some(ref range) = self.active_highlight {
if bp >= range.end {
tags.push("</hl>".to_string());
self.active_highlight = None;
}
}
let mut closed_indices = Vec::new();
for (i, range) in self.active_overlays.iter().enumerate() {
if bp >= range.end {
tags.push("</ov>".to_string());
closed_indices.push(i);
}
}
for i in closed_indices.into_iter().rev() {
self.active_overlays.remove(i);
}
}
tags
}
}
#[inline]
pub(super) fn span_color_at(
spans: &[HighlightSpan],
cursor: &mut usize,
byte_pos: usize,
) -> Option<Color> {
while *cursor < spans.len() {
let span = &spans[*cursor];
if span.range.end <= byte_pos {
*cursor += 1;
} else if span.range.start > byte_pos {
return None;
} else {
return Some(span.color);
}
}
None
}
pub(super) fn span_info_at(
spans: &[HighlightSpan],
cursor: &mut usize,
byte_pos: usize,
) -> (Option<Color>, Option<&'static str>, Option<&'static str>) {
while *cursor < spans.len() {
let span = &spans[*cursor];
if span.range.end <= byte_pos {
*cursor += 1;
} else if span.range.start > byte_pos {
return (None, None, None);
} else {
let theme_key = span.category.as_ref().map(|c| c.theme_key());
let display_name = span.category.as_ref().map(|c| c.display_name());
return (Some(span.color), theme_key, display_name);
}
}
(None, None, None)
}
pub(super) fn compress_chars(chars: Vec<(char, Style)>) -> Vec<Span<'static>> {
if chars.is_empty() {
return vec![];
}
let mut spans = Vec::new();
let mut current_style = chars[0].1;
let mut current_text = String::new();
current_text.push(chars[0].0);
for (ch, style) in chars.into_iter().skip(1) {
if style == current_style {
current_text.push(ch);
} else {
spans.push(Span::styled(current_text.clone(), current_style));
current_text.clear();
current_text.push(ch);
current_style = style;
}
}
spans.push(Span::styled(current_text, current_style));
spans
}