Skip to main content

ochrepad_core/
text.rs

1//! `Document`: the rope, addressed only by [`ByteOffset`].
2//!
3//! Every Ropey call in the crate lives in this module, so a later move to Ropey 2
4//! (byte-indexed) stays local. Ropey is built without `unicode_lines`/`cr_lines`,
5//! so only `\n` ends a line; line endings are normalized by `fileio`.
6
7use std::fmt;
8use std::ops::Range;
9
10use ropey::Rope;
11
12use crate::position::{ByteOffset, CharIdx};
13
14#[derive(Clone, Default)]
15pub struct Document {
16    rope: Rope,
17}
18
19impl Document {
20    pub fn new(text: &str) -> Self {
21        Self {
22            rope: Rope::from_str(text),
23        }
24    }
25
26    /// The text in order, in the rope's pieces (for hashing without a copy).
27    pub fn chunks(&self) -> impl Iterator<Item = &str> + '_ {
28        self.rope.chunks()
29    }
30
31    /// Length in bytes; also the offset of the end of the document.
32    pub fn len(&self) -> ByteOffset {
33        ByteOffset(self.rope.len_bytes())
34    }
35
36    pub fn is_empty(&self) -> bool {
37        self.rope.len_bytes() == 0
38    }
39
40    /// Number of lines. An empty document, or one ending in `\n`, still has a
41    /// (possibly empty) last line.
42    pub fn line_count(&self) -> usize {
43        self.rope.len_lines()
44    }
45
46    /// Line containing `pos`. A `\n` belongs to the line it ends.
47    pub fn line_of(&self, pos: ByteOffset) -> usize {
48        self.rope.byte_to_line(pos.0)
49    }
50
51    pub fn line_start(&self, line: usize) -> ByteOffset {
52        ByteOffset(self.rope.line_to_byte(line))
53    }
54
55    /// End of the line's content, excluding its `\n`.
56    pub fn line_end(&self, line: usize) -> ByteOffset {
57        if line + 1 < self.line_count() {
58            ByteOffset(self.rope.line_to_byte(line + 1) - 1)
59        } else {
60            self.len()
61        }
62    }
63
64    /// The line's content, excluding its `\n`.
65    pub fn line_text(&self, line: usize) -> String {
66        self.slice(self.line_start(line)..self.line_end(line))
67    }
68
69    pub fn slice(&self, range: Range<ByteOffset>) -> String {
70        self.rope.byte_slice(range.start.0..range.end.0).to_string()
71    }
72
73    pub fn is_char_boundary(&self, pos: ByteOffset) -> bool {
74        if pos.0 > self.rope.len_bytes() {
75            return false;
76        }
77        let (chunk, chunk_start, _, _) = self.rope.chunk_at_byte(pos.0);
78        chunk.is_char_boundary(pos.0 - chunk_start)
79    }
80
81    /// The chunk of text containing `pos` and the offset at which it starts.
82    /// At the end of the document this is the last chunk. Grapheme segmentation
83    /// in `position` walks the rope with this.
84    pub fn chunk_at(&self, pos: ByteOffset) -> (&str, ByteOffset) {
85        let (chunk, start, _, _) = self.rope.chunk_at_byte(pos.0);
86        (chunk, ByteOffset(start))
87    }
88
89    /// The char starting at `pos`, if any.
90    pub fn char_at(&self, pos: ByteOffset) -> Option<char> {
91        if pos >= self.len() {
92            return None;
93        }
94        let (chunk, start, _, _) = self.rope.chunk_at_byte(pos.0);
95        chunk[pos.0 - start..].chars().next()
96    }
97
98    pub fn to_char_idx(&self, pos: ByteOffset) -> CharIdx {
99        CharIdx(self.rope.byte_to_char(pos.0))
100    }
101
102    /// Replaces `range` with `text`. Both ends must be char boundaries.
103    pub fn replace(&mut self, range: Range<ByteOffset>, text: &str) {
104        debug_assert!(self.is_char_boundary(range.start) && self.is_char_boundary(range.end));
105        let start = self.to_char_idx(range.start);
106        if range.start < range.end {
107            let end = self.to_char_idx(range.end);
108            self.rope.remove(start.0..end.0);
109        }
110        if !text.is_empty() {
111            self.rope.insert(start.0, text);
112        }
113    }
114}
115
116impl fmt::Display for Document {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        for chunk in self.rope.chunks() {
119            f.write_str(chunk)?;
120        }
121        Ok(())
122    }
123}
124
125impl fmt::Debug for Document {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        write!(f, "Document({:?})", self.to_string())
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn lines_split_only_on_lf() {
137        let d = Document::new("a\r\nb\rc\u{2028}d\n");
138        assert_eq!(d.line_count(), 3);
139        assert_eq!(d.line_text(0), "a\r");
140        assert_eq!(d.line_text(1), "b\rc\u{2028}d");
141        assert_eq!(d.line_text(2), "");
142        assert_eq!(d.line_end(0), ByteOffset(2));
143    }
144
145    #[test]
146    fn replace_by_bytes() {
147        let mut d = Document::new("héllo wörld");
148        d.replace(ByteOffset(1)..ByteOffset(3), "e");
149        assert_eq!(d.to_string(), "hello wörld");
150        d.replace(ByteOffset(5)..ByteOffset(5), ",");
151        assert_eq!(d.to_string(), "hello, wörld");
152        assert!(!d.is_char_boundary(ByteOffset(9)));
153    }
154}