endbasic_std/console/
linebuffer.rs

1// EndBASIC
2// Copyright 2022 Philippe GASSMANN
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! String buffer used to manipulate strings at the UTF-8 code point level instead of at the byte
17//! level.
18
19use std::fmt::Display;
20use std::iter;
21use std::str::Chars;
22
23#[derive(Default, Debug)]
24/// Abstraction over a string to handle manipulation operations at char boundaries.
25///
26/// This exists because Rust strings are indexed by bytes and manipulating string bytes is
27/// complicated.
28///
29/// TODO(zenria): The current implementation of the buffer is using `String::chars`.  Should be
30/// converted to using graphemes instead.
31pub struct LineBuffer {
32    line: String,
33}
34
35impl LineBuffer {
36    /// Creates an empty `LineBuffer` with an allocated `capacity`.
37    pub fn with_capacity(capacity: usize) -> Self {
38        Self { line: String::with_capacity(capacity) }
39    }
40
41    /// Returns the logical `LineBuffer` length (in UTF-8 code point count and not in bytes).
42    pub fn len(&self) -> usize {
43        self.line.chars().count()
44    }
45
46    /// Gets and iterator over buffer chars.
47    pub fn chars(&self) -> Chars {
48        self.line.chars()
49    }
50
51    /// Returns the end of the string starting at `start_pos`, or an empty string if `start_pos` is
52    /// after the string's end.
53    pub fn end(&self, start_pos: usize) -> String {
54        self.chars().skip(start_pos).collect()
55    }
56
57    /// Returns the characters from the start of the string to `end_pos`, excluding `end_pos`.
58    ///
59    /// Calling this with `end_pos` set to 0 will return an empty string, and a 1-char string if
60    /// `end_pos` is 1 (if the string contains at least 1 character).
61    pub fn start(&self, end_pos: usize) -> String {
62        self.chars().take(end_pos).collect()
63    }
64
65    /// Extracts a range of characters from this buffer.
66    pub fn range(&self, start_pos: usize, end_pos: usize) -> String {
67        let count = if start_pos > end_pos { 0 } else { end_pos - start_pos };
68        self.chars().skip(start_pos).take(count).collect()
69    }
70
71    /// Checks if this buffer is empty or not.
72    pub fn is_empty(&self) -> bool {
73        self.line.is_empty()
74    }
75
76    /// Gets a view on the underlying bytes held by this buffer.
77    ///
78    /// Warning: direct bytes manipulation may lead to undefined behavior.
79    pub fn as_bytes(&self) -> &[u8] {
80        self.line.as_bytes()
81    }
82
83    /// Removes a character from this buffer.
84    ///
85    /// If the given position if greater than the length of the buffer, this function does nothing.
86    pub fn remove(&mut self, pos: usize) {
87        self.line = self.line.chars().take(pos).chain(self.line.chars().skip(pos + 1)).collect();
88    }
89
90    /// Inserts a char at the given position.
91    ///
92    /// If the position is greater than the buffer length, the character will be appended at the
93    /// end ot it.
94    pub fn insert(&mut self, pos: usize, ch: char) {
95        self.line = self
96            .line
97            .chars()
98            .take(pos)
99            .chain(iter::once(ch))
100            .chain(self.line.chars().skip(pos))
101            .collect();
102    }
103
104    /// Returns the underlying string.
105    pub fn into_inner(self) -> String {
106        self.line
107    }
108
109    /// Inserts the given string `s` into the buffer at `pos`.
110    ///
111    /// If `pos` is greater than the length of the buffer, `s` will be appended at the end of it.
112    pub fn insert_str(&mut self, pos: usize, s: &str) {
113        self.line = self
114            .line
115            .chars()
116            .take(pos)
117            .chain(s.chars())
118            .chain(self.line.chars().skip(pos))
119            .collect();
120    }
121
122    /// Appends the given string `s` to the buffer.
123    pub fn push_str(&mut self, s: &LineBuffer) {
124        self.line.push_str(&s.line);
125    }
126
127    /// Splits the buffer in two parts at the position `at`.
128    ///
129    /// Returns the remaining part of the buffer (same behavior as `String::split_off`).
130    pub fn split_off(&mut self, at: usize) -> LineBuffer {
131        let ret = LineBuffer::from(self.line.chars().skip(at).collect::<String>());
132        self.line = self.line.chars().take(at).collect();
133        ret
134    }
135}
136
137impl From<&str> for LineBuffer {
138    fn from(s: &str) -> Self {
139        Self { line: String::from(s) }
140    }
141}
142
143impl From<&String> for LineBuffer {
144    fn from(s: &String) -> Self {
145        Self::from(s.as_str())
146    }
147}
148impl From<String> for LineBuffer {
149    fn from(line: String) -> Self {
150        Self { line }
151    }
152}
153
154impl Display for LineBuffer {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        self.line.fmt(f)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::LineBuffer;
163
164    #[test]
165    fn test_end() {
166        let empty = LineBuffer::default();
167        assert_eq!(empty.end(0), "");
168        assert_eq!(empty.end(1), ""); // Should not panic even if the given position is invalid.
169        let hw = LineBuffer::from("Hello, World");
170
171        assert_eq!(hw.end(0), "Hello, World");
172        assert_eq!(hw.end(7), "World");
173        assert_eq!(hw.end(100), "");
174    }
175
176    #[test]
177    fn test_start() {
178        let empty = LineBuffer::default();
179        assert_eq!(empty.start(0), "");
180        assert_eq!(empty.start(1), ""); // Should not panic even if the given position is invalid.
181        let hw = LineBuffer::from("Hello, World");
182
183        assert_eq!(hw.start(0), "");
184        assert_eq!(hw.start(7), "Hello, ");
185        assert_eq!(hw.start(100), "Hello, World");
186    }
187
188    #[test]
189    fn test_insert() {
190        let mut buffer = LineBuffer::default();
191        buffer.insert(0, 'c'); // c
192        buffer.insert(0, 'é'); // éc
193
194        // Insertion must happen after the 2-byte UTF-8 code point and not at byte 1.
195        buffer.insert(1, 'à'); // éàc
196        buffer.insert(100, 'd'); // Should not panic even if the given position is invalid.
197
198        assert_eq!(buffer.into_inner(), "éàcd");
199    }
200
201    #[test]
202    fn test_remove() {
203        let mut empty = LineBuffer::default();
204        empty.remove(0);
205        empty.remove(5);
206        assert_eq!(empty.into_inner(), "");
207
208        let mut hello = LineBuffer::from("Hello");
209        hello.remove(100); // Do nothing.
210        hello.remove(1); // Remove the 'e'
211        assert_eq!(hello.into_inner(), "Hllo");
212    }
213
214    #[test]
215    fn test_insert_str() {
216        let mut buffer = LineBuffer::default();
217        buffer.insert_str(0, "Hello");
218        assert_eq!(buffer.end(0), "Hello");
219        buffer.insert_str(100, "World"); // pos > len will insert at the end.
220        assert_eq!(buffer.end(0), "HelloWorld");
221        buffer.insert_str(5, ", ");
222        assert_eq!(buffer.end(0), "Hello, World");
223    }
224
225    #[test]
226    fn test_range() {
227        let mut buffer = LineBuffer::default();
228
229        assert_eq!(buffer.range(0, 0), "");
230        assert_eq!(buffer.range(0, 10), "");
231        assert_eq!(buffer.range(0, 10), "");
232        assert_eq!(buffer.range(10, 0), ""); // Should not panic even with bad indexes.
233
234        buffer.insert_str(0, "Hello, World");
235        assert_eq!(buffer.range(0, 5), "Hello");
236        assert_eq!(buffer.range(7, 10), "Wor");
237        assert_eq!(buffer.range(7, 100), "World");
238        assert_eq!(buffer.range(10, 0), ""); // Should not panic even with bad indexes.
239    }
240
241    #[test]
242    fn test_is_empty() {
243        assert!(LineBuffer::default().is_empty());
244        assert!(LineBuffer::from("").is_empty());
245        assert!(!LineBuffer::from("This is not empty").is_empty());
246    }
247
248    #[test]
249    fn test_split_off() {
250        let mut buffer = LineBuffer::from("Hello, World");
251        let world = buffer.split_off(7);
252        assert_eq!(buffer.into_inner(), "Hello, ");
253        assert_eq!(world.into_inner(), "World");
254    }
255}