Skip to main content

dscode_core/
text_buffer.rs

1/// A rope-based text buffer for efficient editing operations.
2///
3/// `TextBuffer` wraps a [`ropey::Rope`] to provide fast insertions, deletions,
4/// and character counting on texts of any size — from short snippets to files
5/// with millions of lines.
6///
7/// # Invariants
8///
9/// - The internal rope is always well-formed (never `None` or in an undefined
10///   state) after construction via [`TextBuffer::new`].
11/// - Character counts returned by [`TextBuffer::len_chars`] count Unicode
12///   scalar values, so multi-byte characters like emoji are counted as a
13///   single character.
14///
15/// # Example
16///
17/// ```rust
18/// use dscode_core::TextBuffer;
19///
20/// let buffer = TextBuffer::new("Hello, world!");
21/// assert_eq!(buffer.len_chars(), 13);
22/// assert_eq!(buffer.get_text(), "Hello, world!");
23/// assert!(!buffer.is_empty());
24/// ```
25pub struct TextBuffer {
26    rope: ropey::Rope,
27}
28
29impl TextBuffer {
30    /// Create a new text buffer initialised with the given content.
31    ///
32    /// The entire `content` string is copied into an internal rope data
33    /// structure. For an empty buffer, pass `""`.
34    ///
35    /// # Arguments
36    ///
37    /// * `content` — The initial text content of the buffer. An empty string
38    ///   produces an empty buffer.
39    ///
40    /// # Example
41    ///
42    /// ```rust
43    /// use dscode_core::TextBuffer;
44    ///
45    /// let buffer = TextBuffer::new("some text");
46    /// ```
47    pub fn new(content: &str) -> Self {
48        Self { rope: ropey::Rope::from_str(content) }
49    }
50
51    /// Get the full text content of the buffer as a [`String`].
52    ///
53    /// This copies the entire rope contents into a new string. For very large
54    /// buffers, consider whether you truly need the whole text at once.
55    ///
56    /// # Example
57    ///
58    /// ```rust
59    /// use dscode_core::TextBuffer;
60    ///
61    /// let buffer = TextBuffer::new("abc");
62    /// assert_eq!(buffer.get_text(), "abc");
63    /// ```
64    pub fn get_text(&self) -> String {
65        self.rope.to_string()
66    }
67
68    /// Get the number of Unicode scalar values in the buffer.
69    ///
70    /// This counts characters, not bytes. For example, `"café"` returns `4`,
71    /// and `"🦀"` returns `1`.
72    ///
73    /// # Example
74    ///
75    /// ```rust
76    /// use dscode_core::TextBuffer;
77    ///
78    /// let buffer = TextBuffer::new("café");
79    /// assert_eq!(buffer.len_chars(), 4);
80    /// ```
81    pub fn len_chars(&self) -> usize {
82        self.rope.len_chars()
83    }
84
85    /// Check whether the buffer contains no characters.
86    ///
87    /// Returns `true` when [`TextBuffer::len_chars`] is zero.
88    ///
89    /// # Example
90    ///
91    /// ```rust
92    /// use dscode_core::TextBuffer;
93    ///
94    /// let buffer = TextBuffer::new("");
95    /// assert!(buffer.is_empty());
96    /// ```
97    pub fn is_empty(&self) -> bool {
98        self.rope.len_chars() == 0
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_text_buffer_new_empty() {
108        let buffer = TextBuffer::new("");
109        assert!(buffer.is_empty());
110        assert_eq!(buffer.len_chars(), 0);
111        assert_eq!(buffer.get_text(), "");
112    }
113
114    #[test]
115    fn test_text_buffer_with_content() {
116        let content = "Hello, world!";
117        let buffer = TextBuffer::new(content);
118        assert!(!buffer.is_empty());
119        assert_eq!(buffer.len_chars(), 13);
120        assert_eq!(buffer.get_text(), content);
121    }
122
123    #[test]
124    fn test_text_buffer_len_chars() {
125        // Single line
126        let buffer = TextBuffer::new("abcdef");
127        assert_eq!(buffer.len_chars(), 6);
128
129        // Multi-line with newlines
130        let buffer = TextBuffer::new("line1\nline2\nline3");
131        assert_eq!(buffer.len_chars(), 17);
132
133        // Unicode content
134        let buffer = TextBuffer::new("café");
135        assert_eq!(buffer.len_chars(), 4);
136
137        // Emoji
138        let buffer = TextBuffer::new("🦀");
139        assert_eq!(buffer.len_chars(), 1);
140    }
141
142    #[test]
143    fn test_text_buffer_large_content() {
144        // Create a large buffer with 10,000 lines
145        let content: String = (0..10_000)
146            .map(|i| format!("Line number {}", i))
147            .collect::<Vec<_>>()
148            .join("\n");
149
150        let buffer = TextBuffer::new(&content);
151        assert!(!buffer.is_empty());
152        assert_eq!(buffer.len_chars(), content.len());
153
154        // Verify the content is preserved
155        let text = buffer.get_text();
156        assert_eq!(text, content);
157        assert!(text.starts_with("Line number 0"));
158        assert!(text.contains("Line number 5000"));
159        assert!(text.ends_with("Line number 9999"));
160    }
161
162    #[test]
163    fn test_text_buffer_new_returns_valid_buffer() {
164        // Verify new() constructs a buffer whose get_text() matches the input
165        let content = "Hello, DSCode!";
166        let buffer = TextBuffer::new(content);
167        assert_eq!(buffer.get_text(), content);
168    }
169
170    #[test]
171    fn test_text_buffer_get_text_preserves_unicode() {
172        // get_text() should faithfully reproduce multi-byte content
173        let content = "cafe\u{301} 🦀 Rust"; // café with combining accent + emoji
174        let buffer = TextBuffer::new(content);
175        assert_eq!(buffer.get_text(), content);
176    }
177
178    #[test]
179    fn test_text_buffer_is_empty_variations() {
180        // Only truly empty string should report is_empty
181        assert!(TextBuffer::new("").is_empty());
182        assert!(!TextBuffer::new(" ").is_empty()); // space is a character
183        assert!(!TextBuffer::new("\n").is_empty()); // newline is a character
184        assert!(!TextBuffer::new("\t").is_empty()); // tab is a character
185    }
186
187    #[test]
188    fn test_text_buffer_len_chars_multibyte() {
189        // Emoji and CJK characters should count as single chars
190        let buffer = TextBuffer::new("🦀🦀🦀");
191        assert_eq!(buffer.len_chars(), 3);
192
193        let buffer = TextBuffer::new("日本語");
194        assert_eq!(buffer.len_chars(), 3);
195    }
196
197    #[test]
198    fn test_text_buffer_large_multiline() {
199        // Verify line-count consistency for a large multi-line buffer
200        let line_count = 50_000;
201        let content: String = (0..line_count)
202            .map(|i| format!("line {}", i))
203            .collect::<Vec<_>>()
204            .join("\n");
205        let buffer = TextBuffer::new(&content);
206        assert!(!buffer.is_empty());
207        assert_eq!(buffer.get_text(), content);
208    }
209}