use std::fmt;
use std::ops::Range;
use ropey::Rope;
use crate::position::{ByteOffset, CharIdx};
#[derive(Clone, Default)]
pub struct Document {
rope: Rope,
}
impl Document {
pub fn new(text: &str) -> Self {
Self {
rope: Rope::from_str(text),
}
}
pub fn chunks(&self) -> impl Iterator<Item = &str> + '_ {
self.rope.chunks()
}
pub fn len(&self) -> ByteOffset {
ByteOffset(self.rope.len_bytes())
}
pub fn is_empty(&self) -> bool {
self.rope.len_bytes() == 0
}
pub fn line_count(&self) -> usize {
self.rope.len_lines()
}
pub fn line_of(&self, pos: ByteOffset) -> usize {
self.rope.byte_to_line(pos.0)
}
pub fn line_start(&self, line: usize) -> ByteOffset {
ByteOffset(self.rope.line_to_byte(line))
}
pub fn line_end(&self, line: usize) -> ByteOffset {
if line + 1 < self.line_count() {
ByteOffset(self.rope.line_to_byte(line + 1) - 1)
} else {
self.len()
}
}
pub fn line_text(&self, line: usize) -> String {
self.slice(self.line_start(line)..self.line_end(line))
}
pub fn slice(&self, range: Range<ByteOffset>) -> String {
self.rope.byte_slice(range.start.0..range.end.0).to_string()
}
pub fn is_char_boundary(&self, pos: ByteOffset) -> bool {
if pos.0 > self.rope.len_bytes() {
return false;
}
let (chunk, chunk_start, _, _) = self.rope.chunk_at_byte(pos.0);
chunk.is_char_boundary(pos.0 - chunk_start)
}
pub fn chunk_at(&self, pos: ByteOffset) -> (&str, ByteOffset) {
let (chunk, start, _, _) = self.rope.chunk_at_byte(pos.0);
(chunk, ByteOffset(start))
}
pub fn char_at(&self, pos: ByteOffset) -> Option<char> {
if pos >= self.len() {
return None;
}
let (chunk, start, _, _) = self.rope.chunk_at_byte(pos.0);
chunk[pos.0 - start..].chars().next()
}
pub fn to_char_idx(&self, pos: ByteOffset) -> CharIdx {
CharIdx(self.rope.byte_to_char(pos.0))
}
pub fn replace(&mut self, range: Range<ByteOffset>, text: &str) {
debug_assert!(self.is_char_boundary(range.start) && self.is_char_boundary(range.end));
let start = self.to_char_idx(range.start);
if range.start < range.end {
let end = self.to_char_idx(range.end);
self.rope.remove(start.0..end.0);
}
if !text.is_empty() {
self.rope.insert(start.0, text);
}
}
}
impl fmt::Display for Document {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for chunk in self.rope.chunks() {
f.write_str(chunk)?;
}
Ok(())
}
}
impl fmt::Debug for Document {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Document({:?})", self.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lines_split_only_on_lf() {
let d = Document::new("a\r\nb\rc\u{2028}d\n");
assert_eq!(d.line_count(), 3);
assert_eq!(d.line_text(0), "a\r");
assert_eq!(d.line_text(1), "b\rc\u{2028}d");
assert_eq!(d.line_text(2), "");
assert_eq!(d.line_end(0), ByteOffset(2));
}
#[test]
fn replace_by_bytes() {
let mut d = Document::new("héllo wörld");
d.replace(ByteOffset(1)..ByteOffset(3), "e");
assert_eq!(d.to_string(), "hello wörld");
d.replace(ByteOffset(5)..ByteOffset(5), ",");
assert_eq!(d.to_string(), "hello, wörld");
assert!(!d.is_char_boundary(ByteOffset(9)));
}
}