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