use ropey::{LineType, Rope};
use std::sync::Arc;
const LINE_TYPE: LineType = LineType::LF_CR;
#[derive(Debug, Clone)]
pub struct TruthTextBuffer {
rope: Arc<Rope>,
}
impl Default for TruthTextBuffer {
fn default() -> Self {
Self::new()
}
}
impl TruthTextBuffer {
pub fn new() -> Self {
Self {
rope: Arc::new(Rope::new()),
}
}
pub fn from_str(content: &str) -> Self {
Self {
rope: Arc::new(Rope::from_str(content)),
}
}
pub fn text(&self) -> String {
self.rope.to_string()
}
pub fn slice(&self, start: usize, end: usize) -> String {
self.rope.slice(start..end).to_string()
}
pub fn len(&self) -> usize {
self.rope.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn insert(&mut self, pos: usize, text: &str) {
let rope = Arc::make_mut(&mut self.rope);
rope.insert(pos, text);
}
pub fn remove(&mut self, start: usize, end: usize) {
let rope = Arc::make_mut(&mut self.rope);
rope.remove(start..end);
}
pub fn replace(&mut self, start: usize, end: usize, text: &str) {
let rope = Arc::make_mut(&mut self.rope);
rope.remove(start..end);
rope.insert(start, text);
}
pub fn line_count(&self) -> usize {
self.rope.len_lines(LINE_TYPE)
}
pub fn line_start(&self, line: usize) -> usize {
self.rope.line_to_byte_idx(line, LINE_TYPE)
}
pub fn get_line(&self, line: usize) -> String {
strip_line_ending(self.rope.line(line, LINE_TYPE).to_string())
}
pub fn line_range(&self, line: usize) -> (usize, usize) {
let start = self.rope.line_to_byte_idx(line, LINE_TYPE);
let end_with_line_ending = if line + 1 < self.line_count() {
self.rope.line_to_byte_idx(line + 1, LINE_TYPE)
} else {
self.len()
};
let line_text = self.rope.line(line, LINE_TYPE).to_string();
let end = end_with_line_ending.saturating_sub(line_ending_len(&line_text));
(start, end)
}
pub fn byte_to_line_col(&self, byte: usize) -> (usize, usize) {
let line = self.rope.byte_to_line_idx(byte.min(self.len()), LINE_TYPE);
let col = byte.min(self.len()) - self.rope.line_to_byte_idx(line, LINE_TYPE);
(line, col)
}
pub fn line_col_to_byte(&self, line: usize, col: usize) -> usize {
let (start, end) = self.line_range(line);
start.saturating_add(col).min(end)
}
}
#[derive(Debug, Clone)]
pub struct BufferSnapshot {
rope: Arc<Rope>,
}
impl BufferSnapshot {
pub fn from_buffer(buffer: &TruthTextBuffer) -> Self {
Self {
rope: buffer.rope.clone(),
}
}
pub fn text(&self) -> String {
self.rope.to_string()
}
pub fn slice(&self, start: usize, end: usize) -> String {
self.rope.slice(start..end).to_string()
}
pub fn len(&self) -> usize {
self.rope.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn line_count(&self) -> usize {
self.rope.len_lines(LINE_TYPE)
}
pub fn get_line(&self, line: usize) -> String {
strip_line_ending(self.rope.line(line, LINE_TYPE).to_string())
}
pub fn byte_to_line_col(&self, byte: usize) -> (usize, usize) {
let line = self.rope.byte_to_line_idx(byte.min(self.len()), LINE_TYPE);
let col = byte.min(self.len()) - self.rope.line_to_byte_idx(line, LINE_TYPE);
(line, col)
}
}
fn strip_line_ending(mut line: String) -> String {
if line.ends_with('\n') {
line.pop();
if line.ends_with('\r') {
line.pop();
}
} else if line.ends_with('\r') {
line.pop();
}
line
}
fn line_ending_len(line: &str) -> usize {
if line.ends_with("\r\n") {
2
} else {
usize::from(line.ends_with('\n') || line.ends_with('\r'))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_buffer() {
let buffer = TruthTextBuffer::new();
assert!(buffer.is_empty());
assert_eq!(buffer.len(), 0);
assert_eq!(buffer.line_count(), 1); }
#[test]
fn test_buffer_with_content() {
let content = "Truth: Test\n Scenario: Test\n Given a test";
let buffer = TruthTextBuffer::from_str(content);
assert!(!buffer.is_empty());
assert_eq!(buffer.len(), content.len());
assert_eq!(buffer.line_count(), 3);
assert_eq!(buffer.get_line(0), "Truth: Test");
assert_eq!(buffer.get_line(1), " Scenario: Test");
assert_eq!(buffer.get_line(2), " Given a test");
}
#[test]
fn test_insert_and_remove() {
let original = "Feature: Test";
let mut buffer = TruthTextBuffer::from_str(original);
buffer.insert(original.len(), "\nScenario: Test");
assert_eq!(buffer.text(), "Feature: Test\nScenario: Test");
buffer.remove(original.len(), buffer.len());
assert_eq!(buffer.text(), "Feature: Test");
}
#[test]
fn test_line_col_conversion() {
let buffer = TruthTextBuffer::from_str("Line 1\nLine 2\nLine 3");
let (line, col) = buffer.byte_to_line_col(7);
assert_eq!(line, 1);
assert_eq!(col, 0);
let byte = buffer.line_col_to_byte(1, 0);
assert_eq!(byte, 7);
}
#[test]
fn test_line_range_excludes_line_ending() {
let buffer = TruthTextBuffer::from_str("Line 1\r\nLine 2\n");
assert_eq!(buffer.line_range(0), (0, 6));
assert_eq!(buffer.get_line(0), "Line 1");
assert_eq!(buffer.get_line(1), "Line 2");
}
}