Rust · 5542 bytes Raw Blame History
1 use anyhow::Result;
2 use ropey::Rope;
3 use std::fs::File;
4 use std::io::{BufReader, BufWriter};
5 use std::path::Path;
6
7 /// Text buffer using rope data structure for efficient editing
8 #[derive(Debug)]
9 pub struct Buffer {
10 text: Rope,
11 pub modified: bool,
12 }
13
14 impl Default for Buffer {
15 fn default() -> Self {
16 Self::new()
17 }
18 }
19
20 impl Buffer {
21 pub fn new() -> Self {
22 Self {
23 text: Rope::new(),
24 modified: false,
25 }
26 }
27
28 pub fn from_str(s: &str) -> Self {
29 Self {
30 text: Rope::from_str(s),
31 modified: false,
32 }
33 }
34
35 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
36 let file = File::open(path)?;
37 let reader = BufReader::new(file);
38 let text = Rope::from_reader(reader)?;
39 Ok(Self {
40 text,
41 modified: false,
42 })
43 }
44
45 pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
46 let file = File::create(path)?;
47 let writer = BufWriter::new(file);
48 self.text.write_to(writer)?;
49 self.modified = false;
50 Ok(())
51 }
52
53 /// Insert text at character index
54 pub fn insert(&mut self, char_idx: usize, text: &str) {
55 let idx = char_idx.min(self.text.len_chars());
56 self.text.insert(idx, text);
57 self.modified = true;
58 }
59
60 /// Delete characters in range [start, end)
61 pub fn delete(&mut self, start: usize, end: usize) {
62 let start = start.min(self.text.len_chars());
63 let end = end.min(self.text.len_chars());
64 if start < end {
65 self.text.remove(start..end);
66 self.modified = true;
67 }
68 }
69
70 /// Get total line count
71 pub fn line_count(&self) -> usize {
72 self.text.len_lines()
73 }
74
75 /// Get total character count
76 pub fn char_count(&self) -> usize {
77 self.text.len_chars()
78 }
79
80 /// Get a line's content (0-indexed)
81 pub fn line(&self, line_idx: usize) -> Option<ropey::RopeSlice> {
82 if line_idx < self.text.len_lines() {
83 Some(self.text.line(line_idx))
84 } else {
85 None
86 }
87 }
88
89 /// Get line as String (without trailing newline)
90 pub fn line_str(&self, line_idx: usize) -> Option<String> {
91 self.line(line_idx).map(|l| {
92 let s: String = l.chars().collect();
93 s.trim_end_matches('\n').to_string()
94 })
95 }
96
97 /// Get character count for a line (excluding newline)
98 pub fn line_len(&self, line_idx: usize) -> usize {
99 self.line(line_idx)
100 .map(|l| {
101 let len = l.len_chars();
102 // Subtract 1 for newline if not last line
103 if line_idx + 1 < self.text.len_lines() && len > 0 {
104 len - 1
105 } else {
106 len
107 }
108 })
109 .unwrap_or(0)
110 }
111
112 /// Convert (line, col) to absolute char index
113 pub fn line_col_to_char(&self, line: usize, col: usize) -> usize {
114 if line >= self.text.len_lines() {
115 return self.text.len_chars();
116 }
117 let line_start = self.text.line_to_char(line);
118 let line_len = self.line_len(line);
119 line_start + col.min(line_len)
120 }
121
122 /// Convert absolute char index to (line, col)
123 pub fn char_to_line_col(&self, char_idx: usize) -> (usize, usize) {
124 let idx = char_idx.min(self.text.len_chars());
125 let line = self.text.char_to_line(idx);
126 let line_start = self.text.line_to_char(line);
127 let col = idx - line_start;
128 (line, col)
129 }
130
131 /// Get character at position
132 pub fn char_at(&self, char_idx: usize) -> Option<char> {
133 if char_idx < self.text.len_chars() {
134 Some(self.text.char(char_idx))
135 } else {
136 None
137 }
138 }
139
140 /// Check if buffer is empty
141 pub fn is_empty(&self) -> bool {
142 self.text.len_chars() == 0
143 }
144
145 /// Get rope slice for a range
146 pub fn slice(&self, start: usize, end: usize) -> ropey::RopeSlice {
147 let start = start.min(self.text.len_chars());
148 let end = end.min(self.text.len_chars());
149 self.text.slice(start..end)
150 }
151 }
152
153 #[cfg(test)]
154 mod tests {
155 use super::*;
156
157 #[test]
158 fn test_new_buffer() {
159 let buf = Buffer::new();
160 assert_eq!(buf.line_count(), 1);
161 assert_eq!(buf.char_count(), 0);
162 }
163
164 #[test]
165 fn test_insert() {
166 let mut buf = Buffer::new();
167 buf.insert(0, "Hello");
168 assert_eq!(buf.line_str(0), Some("Hello".to_string()));
169 assert!(buf.modified);
170 }
171
172 #[test]
173 fn test_multiline() {
174 let buf = Buffer::from_str("Hello\nWorld\n");
175 assert_eq!(buf.line_count(), 3);
176 assert_eq!(buf.line_str(0), Some("Hello".to_string()));
177 assert_eq!(buf.line_str(1), Some("World".to_string()));
178 }
179
180 #[test]
181 fn test_line_col_conversion() {
182 let buf = Buffer::from_str("Hello\nWorld");
183 assert_eq!(buf.line_col_to_char(0, 0), 0);
184 assert_eq!(buf.line_col_to_char(0, 5), 5);
185 assert_eq!(buf.line_col_to_char(1, 0), 6);
186 assert_eq!(buf.line_col_to_char(1, 3), 9);
187
188 assert_eq!(buf.char_to_line_col(0), (0, 0));
189 assert_eq!(buf.char_to_line_col(5), (0, 5));
190 assert_eq!(buf.char_to_line_col(6), (1, 0));
191 }
192
193 #[test]
194 fn test_delete() {
195 let mut buf = Buffer::from_str("Hello World");
196 buf.delete(5, 11);
197 assert_eq!(buf.line_str(0), Some("Hello".to_string()));
198 }
199 }
200