use crate::util::text::{char_width, display_width};
#[derive(Debug, Clone, PartialEq)]
pub struct VisualLine {
pub logical_line: usize,
pub start_col: usize,
pub end_col: usize,
pub text: String,
pub display_width: usize,
}
impl VisualLine {
pub fn from_line(line: &str, line_num: usize) -> Self {
Self {
logical_line: line_num,
start_col: 0,
end_col: line.chars().count(),
text: line.to_string(),
display_width: display_width(line),
}
}
}
#[derive(Debug, Clone)]
pub struct WrapEngine {
enabled: bool,
width: usize,
line_cache: std::collections::HashMap<usize, Vec<VisualLine>>,
line_visual_counts: Vec<usize>,
prefix_sums: Vec<usize>,
dirty: bool,
}
impl Default for WrapEngine {
fn default() -> Self {
Self::new()
}
}
impl WrapEngine {
pub fn new() -> Self {
Self {
enabled: true,
width: 80,
line_cache: std::collections::HashMap::new(),
line_visual_counts: Vec::new(),
prefix_sums: vec![0],
dirty: true,
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
if self.enabled != enabled {
self.enabled = enabled;
self.dirty = true;
}
}
pub fn set_width(&mut self, width: usize) {
let width = width.max(10);
if self.width != width {
self.width = width;
self.dirty = true;
}
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn visual_line_count(&self) -> usize {
self.prefix_sums.last().copied().unwrap_or(0)
}
pub fn rebuild_cache(&mut self, lines: &[String]) {
self.line_cache.clear();
self.line_visual_counts.clear();
self.prefix_sums.clear();
self.line_visual_counts.reserve(lines.len());
self.prefix_sums.reserve(lines.len() + 1);
self.prefix_sums.push(0);
let mut sum: usize = 0;
for line in lines {
let count = self.compute_visual_line_count(line);
self.line_visual_counts.push(count);
sum += count;
self.prefix_sums.push(sum);
}
self.dirty = false;
}
fn compute_visual_line_count(&self, line: &str) -> usize {
if !self.enabled {
return 1;
}
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() {
return 1;
}
let mut count: usize = 1;
let mut current_width: usize = 0;
for ch in &chars {
let ch_width = char_width(*ch);
if current_width + ch_width > self.width && current_width > 0 {
count += 1;
current_width = 0;
}
current_width += ch_width;
}
count
}
pub fn build_range(&mut self, lines: &[String], start: usize, end: usize) {
let end = end.min(lines.len());
for (i, line) in lines.iter().enumerate().skip(start).take(end - start) {
if !self.line_cache.contains_key(&i) {
let vlines = self.wrap_line(line, i);
self.line_cache.insert(i, vlines);
}
}
}
pub fn wrap_line(&self, line: &str, line_num: usize) -> Vec<VisualLine> {
if !self.enabled {
return vec![VisualLine::from_line(line, line_num)];
}
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() {
return vec![VisualLine {
logical_line: line_num,
start_col: 0,
end_col: 0,
text: String::new(),
display_width: 0,
}];
}
let mut result = Vec::new();
let mut current = String::new();
let mut current_width = 0;
let mut start_col = 0;
let mut col = 0;
for ch in chars {
let ch_width = char_width(ch);
if current_width + ch_width > self.width && !current.is_empty() {
result.push(VisualLine {
logical_line: line_num,
start_col,
end_col: col,
text: current.clone(),
display_width: current_width,
});
start_col = col;
current.clear();
current_width = 0;
}
current.push(ch);
current_width += ch_width;
col += 1;
}
if !current.is_empty() || result.is_empty() {
result.push(VisualLine {
logical_line: line_num,
start_col,
end_col: col,
text: current,
display_width: current_width,
});
}
result
}
fn visual_to_logical_line(&self, visual_row: usize) -> usize {
if self.prefix_sums.len() <= 1 {
return 0;
}
let max_logical = self.line_visual_counts.len().saturating_sub(1);
match self.prefix_sums.binary_search(&visual_row) {
Ok(i) => i.min(max_logical),
Err(i) => i.saturating_sub(1).min(max_logical),
}
}
pub fn logical_to_visual(&self, logical_line: usize, logical_col: usize) -> usize {
if logical_line >= self.line_visual_counts.len() {
return self.visual_line_count().saturating_sub(1);
}
let base = self.prefix_sums[logical_line];
if !self.enabled || self.width == 0 {
return base;
}
let count = self.line_visual_counts[logical_line];
if count <= 1 {
return base;
}
if let Some(vlines) = self.line_cache.get(&logical_line) {
for (i, vl) in vlines.iter().enumerate() {
if logical_col < vl.end_col || i == vlines.len() - 1 {
return base + i;
}
}
return base + vlines.len().saturating_sub(1);
}
let sub = logical_col / self.width.max(1);
base + sub.min(count.saturating_sub(1))
}
pub fn visual_to_logical(&self, visual_row: usize) -> (usize, usize) {
let logical = self.visual_to_logical_line(visual_row);
let base = self.prefix_sums.get(logical).copied().unwrap_or(0);
let sub = visual_row.saturating_sub(base);
if let Some(vlines) = self.line_cache.get(&logical)
&& let Some(vl) = vlines.get(sub)
{
return (logical, vl.start_col);
}
let start_col = sub * self.width;
(logical, start_col)
}
pub fn get_visual_line(&self, visual_row: usize) -> Option<&VisualLine> {
let logical = self.visual_to_logical_line(visual_row);
let base = self.prefix_sums.get(logical).copied().unwrap_or(0);
let sub = visual_row.saturating_sub(base);
self.line_cache.get(&logical)?.get(sub)
}
pub fn get_cached_lines(&self, logical_line: usize) -> &[VisualLine] {
self.line_cache
.get(&logical_line)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn visual_offset_of(&self, logical_line: usize) -> usize {
self.prefix_sums.get(logical_line).copied().unwrap_or(0)
}
}
#[cfg(test)]
mod tests;