251 lines
8.2 KiB
Rust
251 lines
8.2 KiB
Rust
// src/textarea/provider.rs
|
||
use crate::DataProvider;
|
||
use once_cell::unsync::OnceCell;
|
||
use ropey::Rope;
|
||
use std::io::{self, BufReader, Read};
|
||
use std::path::Path;
|
||
|
||
#[derive(Debug)] // Clone removed: OnceCell<String> is not Clone
|
||
pub struct TextAreaProvider {
|
||
rope: Rope,
|
||
name: String,
|
||
// Lazy per-line cache; only lines that are actually used get materialized.
|
||
// This keeps memory low even for very large files.
|
||
line_cache: Vec<OnceCell<String>>,
|
||
}
|
||
|
||
impl Default for TextAreaProvider {
|
||
fn default() -> Self {
|
||
let rope = Rope::from_str("");
|
||
Self {
|
||
rope,
|
||
name: "Text".to_string(),
|
||
line_cache: vec![OnceCell::new()], // at least 1 logical line
|
||
}
|
||
}
|
||
}
|
||
|
||
impl TextAreaProvider {
|
||
pub fn from_text<S: Into<String>>(text: S) -> Self {
|
||
let s = text.into();
|
||
let rope = Rope::from_str(&s);
|
||
let lines = rope.len_lines().max(1);
|
||
Self {
|
||
rope,
|
||
name: "Text".to_string(),
|
||
line_cache: vec![(); lines].into_iter().map(|_| OnceCell::new()).collect(),
|
||
}
|
||
}
|
||
|
||
pub fn to_text(&self) -> String {
|
||
self.rope.to_string()
|
||
}
|
||
|
||
pub fn from_file<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||
let f = std::fs::File::open(path)?;
|
||
let mut reader = BufReader::new(f);
|
||
Self::from_reader(&mut reader)
|
||
}
|
||
|
||
pub fn from_reader<R: Read>(reader: &mut R) -> io::Result<Self> {
|
||
let rope = Rope::from_reader(reader)?;
|
||
let lines = rope.len_lines().max(1);
|
||
Ok(Self {
|
||
rope,
|
||
name: "Text".to_string(),
|
||
line_cache: vec![(); lines].into_iter().map(|_| OnceCell::new()).collect(),
|
||
})
|
||
}
|
||
|
||
pub fn set_text<S: Into<String>>(&mut self, text: S) {
|
||
let s = text.into();
|
||
self.rope = Rope::from_str(&s);
|
||
self.resize_cache();
|
||
self.invalidate_cache_from(0);
|
||
}
|
||
|
||
pub fn line_count(&self) -> usize {
|
||
self.rope.len_lines().max(1)
|
||
}
|
||
|
||
fn resize_cache(&mut self) {
|
||
let want = self.line_count();
|
||
if self.line_cache.len() < want {
|
||
self.line_cache
|
||
.extend((0..(want - self.line_cache.len())).map(|_| OnceCell::new()));
|
||
} else if self.line_cache.len() > want {
|
||
self.line_cache.truncate(want);
|
||
}
|
||
}
|
||
|
||
fn invalidate_cache_from(&mut self, line_idx: usize) {
|
||
self.resize_cache();
|
||
if line_idx < self.line_cache.len() {
|
||
for cell in &mut self.line_cache[line_idx..] {
|
||
let _ = cell.take();
|
||
}
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
fn line_bounds_chars(&self, line_idx: usize) -> (usize, usize) {
|
||
// Returns [start, end) in char indices for content only (excluding newline).
|
||
let total_lines = self.line_count();
|
||
let start = self.rope.line_to_char(line_idx);
|
||
let end_exclusive = if line_idx + 1 < total_lines {
|
||
// Next line start is at the char index right after the newline.
|
||
// Exclude the newline itself by not including it in the range.
|
||
self.rope.line_to_char(line_idx + 1) - 1
|
||
} else {
|
||
self.rope.len_chars()
|
||
};
|
||
(start, end_exclusive)
|
||
}
|
||
|
||
fn line_content_len_chars(&self, line_idx: usize) -> usize {
|
||
let slice = self.rope.line(line_idx);
|
||
let mut len = slice.len_chars();
|
||
if line_idx + 1 < self.line_count() && len > 0 {
|
||
// Non-final lines include a trailing '\n' char in rope; exclude it.
|
||
len -= 1;
|
||
}
|
||
len
|
||
}
|
||
|
||
fn compute_line_string(&self, index: usize) -> String {
|
||
let mut s = self.rope.line(index).to_string();
|
||
// Trim trailing newline/CR if present (for non-final lines)
|
||
if s.ends_with('\n') {
|
||
s.pop();
|
||
if s.ends_with('\r') {
|
||
s.pop();
|
||
}
|
||
}
|
||
s
|
||
}
|
||
|
||
// --------------------------
|
||
// Editing helpers for TextAreaState (unchanged API)
|
||
// --------------------------
|
||
|
||
/// Split line at a character offset (within that line).
|
||
/// Returns the index of the newly created line (line_idx + 1).
|
||
pub fn split_line_at(&mut self, line_idx: usize, at_char: usize) -> usize {
|
||
let lines = self.line_count();
|
||
let clamped_line = line_idx.min(lines.saturating_sub(1));
|
||
let (start, end) = self.line_bounds_chars(clamped_line);
|
||
let line_len = end.saturating_sub(start);
|
||
let at = at_char.min(line_len);
|
||
|
||
let insert_at = start + at;
|
||
self.rope.insert(insert_at, "\n"); // rope insert at char index
|
||
|
||
self.resize_cache();
|
||
self.invalidate_cache_from(clamped_line);
|
||
clamped_line + 1
|
||
}
|
||
|
||
/// Join current line with the next by removing the newline.
|
||
/// Returns Some(new_cursor_col_on_merged_line) or None if no next line.
|
||
pub fn join_with_next(&mut self, line_idx: usize) -> Option<usize> {
|
||
if line_idx + 1 >= self.line_count() {
|
||
return None;
|
||
}
|
||
let newline_pos = self.rope.line_to_char(line_idx + 1) - 1; // index of '\n'
|
||
let left_len = self.line_content_len_chars(line_idx);
|
||
self.rope.remove(newline_pos..newline_pos + 1); // remove the newline
|
||
|
||
self.resize_cache();
|
||
self.invalidate_cache_from(line_idx);
|
||
Some(left_len)
|
||
}
|
||
|
||
/// Join current line with the previous by removing the previous newline.
|
||
/// Returns Some((new_prev_index, cursor_col)) or None if at line 0.
|
||
pub fn join_with_prev(&mut self, line_idx: usize) -> Option<(usize, usize)> {
|
||
if line_idx == 0 || line_idx >= self.line_count() {
|
||
return None;
|
||
}
|
||
let prev_idx = line_idx - 1;
|
||
let prev_len = self.line_content_len_chars(prev_idx);
|
||
let newline_pos = self.rope.line_to_char(line_idx) - 1; // index of '\n' before current line
|
||
self.rope.remove(newline_pos..newline_pos + 1);
|
||
|
||
self.resize_cache();
|
||
self.invalidate_cache_from(prev_idx);
|
||
Some((prev_idx, prev_len))
|
||
}
|
||
|
||
/// Insert an empty line after given index.
|
||
/// Returns the index of the inserted blank line (line_idx + 1).
|
||
pub fn insert_blank_line_after(&mut self, line_idx: usize) -> usize {
|
||
let lines = self.line_count();
|
||
let clamped = line_idx.min(lines.saturating_sub(1));
|
||
let pos = if clamped + 1 < lines {
|
||
self.rope.line_to_char(clamped + 1)
|
||
} else {
|
||
self.rope.len_chars()
|
||
};
|
||
self.rope.insert(pos, "\n");
|
||
|
||
self.resize_cache();
|
||
self.invalidate_cache_from(clamped);
|
||
clamped + 1
|
||
}
|
||
|
||
/// Insert an empty line before given index.
|
||
/// Returns the index of the inserted blank line (line_idx).
|
||
pub fn insert_blank_line_before(&mut self, line_idx: usize) -> usize {
|
||
let clamped = line_idx.min(self.line_count());
|
||
let pos = if clamped < self.line_count() {
|
||
self.rope.line_to_char(clamped)
|
||
} else {
|
||
self.rope.len_chars()
|
||
};
|
||
self.rope.insert(pos, "\n");
|
||
|
||
self.resize_cache();
|
||
self.invalidate_cache_from(clamped);
|
||
clamped
|
||
}
|
||
}
|
||
|
||
impl DataProvider for TextAreaProvider {
|
||
fn field_count(&self) -> usize {
|
||
self.line_count()
|
||
}
|
||
|
||
fn field_name(&self, _index: usize) -> &str {
|
||
&self.name
|
||
}
|
||
|
||
fn field_value(&self, index: usize) -> &str {
|
||
if index >= self.line_cache.len() {
|
||
return "";
|
||
}
|
||
let cell = &self.line_cache[index];
|
||
// Fill lazily on first read, from &self (no &mut needed).
|
||
let s_ref = cell.get_or_init(|| self.compute_line_string(index));
|
||
s_ref.as_str()
|
||
}
|
||
|
||
fn set_field_value(&mut self, index: usize, value: String) {
|
||
if index >= self.line_count() {
|
||
return;
|
||
}
|
||
// Enforce single-line invariant: strip embedded newlines
|
||
let clean = value.replace('\n', "");
|
||
|
||
let (start, end) = self.line_bounds_chars(index);
|
||
self.rope.remove(start..end);
|
||
self.rope.insert(start, &clean);
|
||
|
||
self.resize_cache();
|
||
if index < self.line_cache.len() {
|
||
// Replace this line’s cached string only; other lines unchanged
|
||
let _ = self.line_cache[index].take();
|
||
let _ = self.line_cache[index].set(clean);
|
||
}
|
||
}
|
||
}
|