Rust · 12847 bytes Raw Blame History
1 use anyhow::Result;
2 use ropey::Rope;
3 use std::collections::hash_map::DefaultHasher;
4 use std::collections::HashSet;
5 use std::fs::File;
6 use std::hash::{Hash, Hasher};
7 use std::io::{BufReader, BufWriter};
8 use std::path::Path;
9
10 /// Text buffer using rope data structure for efficient editing
11 #[derive(Debug)]
12 pub struct Buffer {
13 text: Rope,
14 pub modified: bool,
15 /// Cached content hash (invalidated on modification)
16 cached_hash: Option<u64>,
17 }
18
19 impl Default for Buffer {
20 fn default() -> Self {
21 Self::new()
22 }
23 }
24
25 impl Buffer {
26 pub fn new() -> Self {
27 Self {
28 text: Rope::new(),
29 modified: false,
30 cached_hash: None,
31 }
32 }
33
34 #[allow(dead_code)]
35 pub fn from_str(s: &str) -> Self {
36 Self {
37 text: Rope::from_str(s),
38 modified: false,
39 cached_hash: None,
40 }
41 }
42
43 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
44 let file = File::open(path)?;
45 let reader = BufReader::new(file);
46 let text = Rope::from_reader(reader)?;
47 Ok(Self {
48 text,
49 modified: false,
50 cached_hash: None,
51 })
52 }
53
54 pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
55 let file = File::create(path)?;
56 let writer = BufWriter::new(file);
57 self.text.write_to(writer)?;
58 self.modified = false;
59 Ok(())
60 }
61
62 /// Insert text at character index
63 pub fn insert(&mut self, char_idx: usize, text: &str) {
64 let idx = char_idx.min(self.text.len_chars());
65 self.text.insert(idx, text);
66 self.modified = true;
67 self.cached_hash = None; // Invalidate hash cache
68 }
69
70 /// Delete characters in range [start, end)
71 pub fn delete(&mut self, start: usize, end: usize) {
72 let start = start.min(self.text.len_chars());
73 let end = end.min(self.text.len_chars());
74 if start < end {
75 self.text.remove(start..end);
76 self.modified = true;
77 self.cached_hash = None; // Invalidate hash cache
78 }
79 }
80
81 /// Get total line count
82 pub fn line_count(&self) -> usize {
83 self.text.len_lines()
84 }
85
86 /// Get total character count
87 #[allow(dead_code)]
88 pub fn char_count(&self) -> usize {
89 self.text.len_chars()
90 }
91
92 /// Get a line's content (0-indexed)
93 pub fn line(&self, line_idx: usize) -> Option<ropey::RopeSlice<'_>> {
94 if line_idx < self.text.len_lines() {
95 Some(self.text.line(line_idx))
96 } else {
97 None
98 }
99 }
100
101 /// Get line as String (without trailing newline)
102 pub fn line_str(&self, line_idx: usize) -> Option<String> {
103 self.line(line_idx).map(|l| {
104 let s: String = l.chars().collect();
105 s.trim_end_matches('\n').to_string()
106 })
107 }
108
109 /// Get character count for a line (excluding newline)
110 pub fn line_len(&self, line_idx: usize) -> usize {
111 self.line(line_idx)
112 .map(|l| {
113 let len = l.len_chars();
114 // Subtract 1 for newline if not last line
115 if line_idx + 1 < self.text.len_lines() && len > 0 {
116 len - 1
117 } else {
118 len
119 }
120 })
121 .unwrap_or(0)
122 }
123
124 /// Convert (line, col) to absolute char index
125 pub fn line_col_to_char(&self, line: usize, col: usize) -> usize {
126 if line >= self.text.len_lines() {
127 return self.text.len_chars();
128 }
129 let line_start = self.text.line_to_char(line);
130 let line_len = self.line_len(line);
131 line_start + col.min(line_len)
132 }
133
134 /// Convert absolute char index to (line, col)
135 #[allow(dead_code)]
136 pub fn char_to_line_col(&self, char_idx: usize) -> (usize, usize) {
137 let idx = char_idx.min(self.text.len_chars());
138 let line = self.text.char_to_line(idx);
139 let line_start = self.text.line_to_char(line);
140 let col = idx - line_start;
141 (line, col)
142 }
143
144 /// Get character at position
145 #[allow(dead_code)]
146 pub fn char_at(&self, char_idx: usize) -> Option<char> {
147 if char_idx < self.text.len_chars() {
148 Some(self.text.char(char_idx))
149 } else {
150 None
151 }
152 }
153
154 /// Check if buffer is empty
155 #[allow(dead_code)]
156 pub fn is_empty(&self) -> bool {
157 self.text.len_chars() == 0
158 }
159
160 /// Get total character count
161 pub fn len_chars(&self) -> usize {
162 self.text.len_chars()
163 }
164
165 /// Get rope slice for a range
166 #[allow(dead_code)]
167 pub fn slice(&self, start: usize, end: usize) -> ropey::RopeSlice<'_> {
168 let start = start.min(self.text.len_chars());
169 let end = end.min(self.text.len_chars());
170 self.text.slice(start..end)
171 }
172
173 /// Get entire buffer content as a String
174 pub fn contents(&self) -> String {
175 self.text.to_string()
176 }
177
178 /// Extract all unique words from the buffer for autocomplete.
179 /// Words are alphanumeric sequences with underscores, minimum 3 characters.
180 pub fn extract_words(&self) -> Vec<String> {
181 let mut words = HashSet::new();
182 let mut current_word = String::new();
183
184 for ch in self.text.chars() {
185 if ch.is_alphanumeric() || ch == '_' {
186 current_word.push(ch);
187 } else {
188 if current_word.len() >= 3 {
189 words.insert(current_word.clone());
190 }
191 current_word.clear();
192 }
193 }
194 // Don't forget the last word
195 if current_word.len() >= 3 {
196 words.insert(current_word);
197 }
198
199 words.into_iter().collect()
200 }
201
202 /// Compute a hash of the buffer contents for change detection.
203 /// Uses cached value if available, otherwise computes and caches.
204 pub fn content_hash(&mut self) -> u64 {
205 if let Some(hash) = self.cached_hash {
206 return hash;
207 }
208
209 let mut hasher = DefaultHasher::new();
210 // Hash character by character to ensure consistent hashing
211 // regardless of rope's internal chunk structure
212 for ch in self.text.chars() {
213 ch.hash(&mut hasher);
214 }
215 let hash = hasher.finish();
216 self.cached_hash = Some(hash);
217 hash
218 }
219
220 /// Replace entire buffer content (used for backup restoration)
221 pub fn set_contents(&mut self, content: &str) {
222 self.text = Rope::from_str(content);
223 self.modified = true;
224 self.cached_hash = None; // Invalidate hash cache
225 }
226
227 /// Find matching bracket for the character at the given position
228 /// Returns (line, col) of matching bracket, or None if not found
229 pub fn find_matching_bracket(&self, line: usize, col: usize) -> Option<(usize, usize)> {
230 let char_idx = self.line_col_to_char(line, col);
231 let ch = self.char_at(char_idx)?;
232
233 let (target, direction) = match ch {
234 '(' => (')', 1i32),
235 ')' => ('(', -1),
236 '[' => (']', 1),
237 ']' => ('[', -1),
238 '{' => ('}', 1),
239 '}' => ('{', -1),
240 '<' => ('>', 1),
241 '>' => ('<', -1),
242 _ => return None,
243 };
244
245 let mut depth = 1;
246 let mut pos = char_idx as i32 + direction;
247 let len = self.text.len_chars() as i32;
248
249 while pos >= 0 && pos < len {
250 if let Some(c) = self.char_at(pos as usize) {
251 if c == target {
252 depth -= 1;
253 if depth == 0 {
254 return Some(self.char_to_line_col(pos as usize));
255 }
256 } else if c == ch {
257 depth += 1;
258 }
259 }
260 pos += direction;
261 }
262
263 None
264 }
265
266 /// Find surrounding brackets containing the cursor position (across lines)
267 /// Returns (open_char_idx, close_char_idx, open_char, close_char)
268 pub fn find_surrounding_brackets(&self, line: usize, col: usize) -> Option<(usize, usize, char, char)> {
269 let cursor_idx = self.line_col_to_char(line, col);
270
271 // Search backward for an opening bracket that contains cursor
272 for search_pos in (0..cursor_idx).rev() {
273 let ch = self.char_at(search_pos)?;
274 let (open, close) = match ch {
275 '(' => ('(', ')'),
276 '{' => ('{', '}'),
277 '[' => ('[', ']'),
278 _ => continue,
279 };
280
281 // Find matching close
282 let mut depth = 1;
283 let mut pos = search_pos + 1;
284 let len = self.text.len_chars();
285
286 while pos < len {
287 if let Some(c) = self.char_at(pos) {
288 if c == close {
289 depth -= 1;
290 if depth == 0 {
291 // Check if cursor is inside this pair
292 if cursor_idx > search_pos && cursor_idx <= pos {
293 return Some((search_pos, pos, open, close));
294 }
295 break;
296 }
297 } else if c == open {
298 depth += 1;
299 }
300 }
301 pos += 1;
302 }
303 }
304 None
305 }
306
307 /// Find surrounding quotes containing the cursor position (across lines)
308 /// Returns (open_char_idx, close_char_idx, quote_char)
309 pub fn find_surrounding_quotes(&self, line: usize, col: usize) -> Option<(usize, usize, char)> {
310 let cursor_idx = self.line_col_to_char(line, col);
311
312 // Search backward for an opening quote
313 for search_pos in (0..cursor_idx).rev() {
314 let ch = self.char_at(search_pos)?;
315 if ch != '"' && ch != '\'' && ch != '`' {
316 continue;
317 }
318
319 // Find matching close (same quote char)
320 let mut pos = search_pos + 1;
321 let len = self.text.len_chars();
322
323 while pos < len {
324 if let Some(c) = self.char_at(pos) {
325 if c == ch {
326 // Check if cursor is inside this pair
327 if cursor_idx > search_pos && cursor_idx <= pos {
328 return Some((search_pos, pos, ch));
329 }
330 break;
331 }
332 }
333 pos += 1;
334 }
335 }
336 None
337 }
338 }
339
340 #[cfg(test)]
341 mod tests {
342 use super::*;
343
344 #[test]
345 fn test_new_buffer() {
346 let buf = Buffer::new();
347 assert_eq!(buf.line_count(), 1);
348 assert_eq!(buf.char_count(), 0);
349 }
350
351 #[test]
352 fn test_insert() {
353 let mut buf = Buffer::new();
354 buf.insert(0, "Hello");
355 assert_eq!(buf.line_str(0), Some("Hello".to_string()));
356 assert!(buf.modified);
357 }
358
359 #[test]
360 fn test_multiline() {
361 let buf = Buffer::from_str("Hello\nWorld\n");
362 assert_eq!(buf.line_count(), 3);
363 assert_eq!(buf.line_str(0), Some("Hello".to_string()));
364 assert_eq!(buf.line_str(1), Some("World".to_string()));
365 }
366
367 #[test]
368 fn test_line_col_conversion() {
369 let buf = Buffer::from_str("Hello\nWorld");
370 assert_eq!(buf.line_col_to_char(0, 0), 0);
371 assert_eq!(buf.line_col_to_char(0, 5), 5);
372 assert_eq!(buf.line_col_to_char(1, 0), 6);
373 assert_eq!(buf.line_col_to_char(1, 3), 9);
374
375 assert_eq!(buf.char_to_line_col(0), (0, 0));
376 assert_eq!(buf.char_to_line_col(5), (0, 5));
377 assert_eq!(buf.char_to_line_col(6), (1, 0));
378 }
379
380 #[test]
381 fn test_delete() {
382 let mut buf = Buffer::from_str("Hello World");
383 buf.delete(5, 11);
384 assert_eq!(buf.line_str(0), Some("Hello".to_string()));
385 }
386
387 #[test]
388 fn test_content_hash_caching() {
389 let mut buf = Buffer::from_str("Hello World");
390
391 // First call computes the hash
392 let hash1 = buf.content_hash();
393 assert!(buf.cached_hash.is_some());
394
395 // Second call returns cached value (same hash)
396 let hash2 = buf.content_hash();
397 assert_eq!(hash1, hash2);
398
399 // After modification, cache should be invalidated
400 buf.insert(5, "!");
401 assert!(buf.cached_hash.is_none());
402
403 // New hash should be different
404 let hash3 = buf.content_hash();
405 assert_ne!(hash1, hash3);
406 assert!(buf.cached_hash.is_some());
407
408 // Delete also invalidates cache
409 buf.delete(5, 6);
410 assert!(buf.cached_hash.is_none());
411
412 // After delete, should be back to original content and hash
413 let hash4 = buf.content_hash();
414 assert_eq!(hash1, hash4);
415 }
416 }
417