use std::hash::{Hash, Hasher};
use super::{BufferId, Position};
#[derive(Debug, Clone)]
pub struct Buffer {
id: BufferId,
lines: Vec<String>,
modified: bool,
file_path: Option<String>,
}
impl Buffer {
#[must_use]
pub fn new() -> Self {
Self {
id: BufferId::new(),
lines: Vec::new(),
modified: false,
file_path: None,
}
}
#[must_use]
pub const fn with_id(id: BufferId) -> Self {
Self {
id,
lines: Vec::new(),
modified: false,
file_path: None,
}
}
#[must_use]
pub fn from_string(content: &str) -> Self {
let lines = if content.is_empty() {
Vec::new()
} else {
content.lines().map(String::from).collect()
};
Self {
id: BufferId::new(),
lines,
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 const fn line_count(&self) -> usize {
self.lines.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.lines.is_empty()
}
#[must_use]
pub fn line(&self, index: usize) -> Option<&str> {
self.lines.get(index).map(String::as_str)
}
#[must_use]
pub fn line_len(&self, index: usize) -> Option<usize> {
self.lines.get(index).map(|l| l.chars().count())
}
#[must_use]
pub fn lines(&self) -> &[String] {
&self.lines
}
#[must_use]
pub fn content(&self) -> String {
self.lines.join("\n")
}
pub fn set_content(&mut self, content: &str) {
self.lines = if content.is_empty() {
Vec::new()
} else {
content.lines().map(String::from).collect()
};
self.modified = true;
}
pub fn insert_at(&mut self, pos: Position, text: &str) {
if text.is_empty() {
return;
}
if self.lines.is_empty() {
self.lines.push(String::new());
}
let pos = self.clamp_position(pos);
let line_idx = pos.line;
let col = pos.column;
let current_line = &self.lines[line_idx];
let byte_offset = char_to_byte_offset(current_line, col);
let (before, after) = current_line.split_at(byte_offset);
let before = before.to_string();
let after = after.to_string();
let insert_lines: Vec<&str> = text.split('\n').collect();
if insert_lines.len() == 1 {
self.lines[line_idx] = format!("{before}{text}{after}");
} else {
let first_insert = insert_lines[0];
self.lines[line_idx] = format!("{before}{first_insert}");
let last_insert = insert_lines[insert_lines.len() - 1];
let last_line = format!("{last_insert}{after}");
let insert_pos = line_idx + 1;
self.lines.splice(
insert_pos..insert_pos,
insert_lines[1..insert_lines.len() - 1]
.iter()
.map(|s| (*s).to_string())
.chain(std::iter::once(last_line)),
);
}
self.modified = true;
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn delete_at(&mut self, pos: Position, count: usize) -> String {
if count == 0 || self.lines.is_empty() {
return String::new();
}
let pos = self.clamp_position(pos);
let mut deleted = String::new();
let mut remaining = count;
let current_line = pos.line;
let current_col = pos.column;
while remaining > 0 && current_line < self.lines.len() {
let line = &self.lines[current_line];
let chars: Vec<char> = line.chars().collect();
let chars_in_line = chars.len();
if current_col >= chars_in_line {
if current_line + 1 < self.lines.len() {
deleted.push('\n');
let next_line = self.lines.remove(current_line + 1);
self.lines[current_line].push_str(&next_line);
remaining -= 1;
} else {
break;
}
} else {
let chars_to_delete = remaining.min(chars_in_line - current_col);
let delete_chars: String = chars[current_col..current_col + chars_to_delete]
.iter()
.collect();
deleted.push_str(&delete_chars);
let new_line: String = chars[..current_col]
.iter()
.chain(chars[current_col + chars_to_delete..].iter())
.collect();
self.lines[current_line] = new_line;
remaining -= chars_to_delete;
}
}
if !deleted.is_empty() {
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 count = self.char_count_between(start, end);
self.delete_at(start, count)
}
#[must_use]
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn position_to_byte(&self, pos: Position) -> usize {
let pos = self.clamp_position(pos);
let mut offset = 0;
for (i, line) in self.lines.iter().enumerate() {
if i < pos.line {
offset += line.len() + 1; } else if i == pos.line {
offset += char_to_byte_offset(line, pos.column);
break;
}
}
offset
}
#[must_use]
pub fn byte_to_position(&self, byte_offset: usize) -> Position {
let mut remaining = byte_offset;
for (line_idx, line) in self.lines.iter().enumerate() {
let line_bytes = line.len();
let line_total = line_bytes + 1;
if remaining <= line_bytes {
let col = byte_to_char_offset(line, remaining);
return Position::new(line_idx, col);
}
remaining -= line_total;
}
let last_line = self.lines.len().saturating_sub(1);
let last_col = self.lines.last().map_or(0, |l| l.chars().count());
Position::new(last_line, last_col)
}
#[must_use]
fn clamp_position(&self, pos: Position) -> Position {
if self.lines.is_empty() {
return Position::origin();
}
let line = pos.line.min(self.lines.len() - 1);
let max_col = self.lines[line].chars().count();
let column = pos.column.min(max_col);
Position::new(line, column)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn char_count_between(&self, start: Position, end: Position) -> usize {
if start >= end {
return 0;
}
if start.line == end.line {
return end.column.saturating_sub(start.column);
}
let mut count = 0;
if let Some(first_line) = self.lines.get(start.line) {
count += first_line.chars().count() - start.column + 1; }
for line_idx in start.line + 1..end.line {
if let Some(line) = self.lines.get(line_idx) {
count += line.chars().count() + 1; }
}
count += end.column;
count
}
}
impl Default for Buffer {
fn default() -> Self {
Self::new()
}
}
fn char_to_byte_offset(line: &str, char_offset: usize) -> usize {
line.char_indices()
.nth(char_offset)
.map_or(line.len(), |(byte_idx, _)| byte_idx)
}
fn byte_to_char_offset(line: &str, byte_offset: usize) -> usize {
line.char_indices()
.take_while(|(byte_idx, _)| *byte_idx < byte_offset)
.count()
}
#[cfg(test)]
#[path = "tests/buffer.rs"]
mod tests;