use std::hash::{Hash, Hasher};
use super::{BufferId, Position, rope::Rope};
#[derive(Debug, Clone)]
pub struct Buffer {
id: BufferId,
text: Rope,
modified: bool,
file_path: Option<String>,
}
impl Buffer {
#[must_use]
pub fn new() -> Self {
Self {
id: BufferId::new(),
text: Rope::new(),
modified: false,
file_path: None,
}
}
#[must_use]
pub fn with_id(id: BufferId) -> Self {
Self {
id,
text: Rope::new(),
modified: false,
file_path: None,
}
}
#[must_use]
pub fn from_string(content: &str) -> Self {
Self {
id: BufferId::new(),
text: normalize_to_rope(content),
modified: false,
file_path: None,
}
}
#[must_use]
pub const fn id(&self) -> BufferId {
self.id
}
#[must_use]
pub const fn is_modified(&self) -> bool {
self.modified
}
pub const fn set_modified(&mut self, modified: bool) {
self.modified = modified;
}
#[must_use]
pub fn file_path(&self) -> Option<&str> {
self.file_path.as_deref()
}
pub fn set_file_path(&mut self, path: Option<String>) {
self.file_path = path;
}
#[must_use]
pub fn line_hash(&self, line_idx: usize) -> Option<u64> {
use std::collections::hash_map::DefaultHasher;
self.line(line_idx).map(|line| {
let mut hasher = DefaultHasher::new();
line.hash(&mut hasher);
hasher.finish()
})
}
#[must_use]
pub fn line_hashes(&self) -> Vec<u64> {
(0..self.line_count())
.filter_map(|idx| self.line_hash(idx))
.collect()
}
#[must_use]
pub fn line_count(&self) -> usize {
self.text.line_count()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
#[must_use]
pub fn line(&self, index: usize) -> Option<&str> {
self.text.line(index)
}
#[must_use]
pub fn line_len(&self, index: usize) -> Option<usize> {
self.text.line_len(index)
}
#[must_use]
pub(crate) fn clone_rope(&self) -> Rope {
self.text.clone()
}
pub(crate) fn set_rope(&mut self, rope: Rope) {
self.text = rope;
self.modified = true;
}
#[must_use]
pub fn content(&self) -> String {
self.text.content()
}
pub fn set_content(&mut self, content: &str) {
self.text = normalize_to_rope(content);
self.modified = true;
}
pub fn insert_at(&mut self, pos: Position, text: &str) {
if text.is_empty() {
return;
}
if self.text.is_empty() {
self.text = Rope::from_str(text);
self.modified = true;
return;
}
let pos = self.clamp_position(pos);
let byte_offset = self.text.position_to_byte(pos.line, pos.column);
self.text = self.text.insert(byte_offset, text);
self.modified = true;
}
pub fn delete_at(&mut self, pos: Position, count: usize) -> String {
if count == 0 || self.text.is_empty() {
return String::new();
}
let pos = self.clamp_position(pos);
let byte_start = self.text.position_to_byte(pos.line, pos.column);
let char_start = self.text.byte_to_char(byte_start);
let char_end = (char_start + count).min(self.text.char_len());
let byte_end = self.text.char_to_byte(char_end);
if byte_start >= byte_end {
return String::new();
}
let deleted = extract_byte_range(&self.text, byte_start, byte_end);
self.text = self.text.remove(byte_start..byte_end);
self.modified = true;
deleted
}
pub fn delete_range(&mut self, start: Position, end: Position) -> String {
let (start, end) = if start <= end {
(start, end)
} else {
(end, start)
};
let start = self.clamp_position(start);
let end = self.clamp_position(end);
let byte_start = self.text.position_to_byte(start.line, start.column);
let byte_end = self.text.position_to_byte(end.line, end.column);
if byte_start >= byte_end {
return String::new();
}
let deleted = extract_byte_range(&self.text, byte_start, byte_end);
self.text = self.text.remove(byte_start..byte_end);
self.modified = true;
deleted
}
#[must_use]
pub fn position_to_byte(&self, pos: Position) -> usize {
if self.text.is_empty() {
return 0;
}
let pos = self.clamp_position(pos);
self.text.position_to_byte(pos.line, pos.column)
}
#[must_use]
pub fn byte_to_position(&self, byte_offset: usize) -> Position {
if self.text.is_empty() {
return Position::new(0, 0);
}
let (line, col) = self.text.byte_to_position(byte_offset);
Position::new(line, col)
}
#[must_use]
fn clamp_position(&self, pos: Position) -> Position {
if self.text.is_empty() {
return Position::origin();
}
let line = pos.line.min(self.text.line_count() - 1);
let max_col = self.text.line_len(line).unwrap_or(0);
let column = pos.column.min(max_col);
Position::new(line, column)
}
}
impl Default for Buffer {
fn default() -> Self {
Self::new()
}
}
fn normalize_to_rope(content: &str) -> Rope {
if content.is_empty() {
return Rope::new();
}
let joined: String = content.lines().collect::<Vec<_>>().join("\n");
if joined.is_empty() {
Rope::new()
} else {
Rope::from_str(&joined)
}
}
fn extract_byte_range(text: &Rope, start: usize, end: usize) -> String {
let mut result = String::with_capacity(end - start);
let mut pos = 0;
for chunk in text.chunks() {
let chunk_end = pos + chunk.len();
if chunk_end <= start {
pos = chunk_end;
continue;
}
if pos >= end {
break;
}
let s = start.saturating_sub(pos);
let e = (end - pos).min(chunk.len());
result.push_str(&chunk[s..e]);
pos = chunk_end;
}
result
}
#[cfg(test)]
#[path = "tests/buffer.rs"]
mod tests;