Files
komp_ac/canvas/src/textarea/provider.rs
2025-08-19 00:03:04 +02:00

251 lines
8.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 lines cached string only; other lines unchanged
let _ = self.line_cache[index].take();
let _ = self.line_cache[index].set(clean);
}
}
}