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}