1use std::collections::BTreeMap;
2
3use crate::search::SearchState;
4use crate::{Position, Span, Viewport};
5
6pub struct Buffer {
14 lines: Vec<String>,
18 cursor: Position,
21 sticky_col: Option<usize>,
25 viewport: Viewport,
28 spans: Vec<Vec<Span>>,
32 marks: BTreeMap<char, Position>,
35 dirty_gen: u64,
38 search: SearchState,
42 pub(crate) folds: Vec<crate::folds::Fold>,
45}
46
47impl Default for Buffer {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl Buffer {
54 pub fn new() -> Self {
57 Self {
58 lines: vec![String::new()],
59 cursor: Position::default(),
60 sticky_col: None,
61 viewport: Viewport::default(),
62 spans: Vec::new(),
63 marks: BTreeMap::new(),
64 dirty_gen: 0,
65 search: SearchState::new(),
66 folds: Vec::new(),
67 }
68 }
69
70 #[allow(clippy::should_implement_trait)]
75 pub fn from_str(text: &str) -> Self {
76 let mut lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
77 if lines.is_empty() {
78 lines.push(String::new());
79 }
80 Self {
81 lines,
82 cursor: Position::default(),
83 sticky_col: None,
84 viewport: Viewport::default(),
85 spans: Vec::new(),
86 marks: BTreeMap::new(),
87 dirty_gen: 0,
88 search: SearchState::new(),
89 folds: Vec::new(),
90 }
91 }
92
93 pub fn lines(&self) -> &[String] {
94 &self.lines
95 }
96
97 pub fn line(&self, row: usize) -> Option<&str> {
98 self.lines.get(row).map(String::as_str)
99 }
100
101 pub fn cursor(&self) -> Position {
102 self.cursor
103 }
104
105 pub fn sticky_col(&self) -> Option<usize> {
106 self.sticky_col
107 }
108
109 pub fn viewport(&self) -> Viewport {
110 self.viewport
111 }
112
113 pub fn viewport_mut(&mut self) -> &mut Viewport {
114 &mut self.viewport
117 }
118
119 pub fn dirty_gen(&self) -> u64 {
120 self.dirty_gen
121 }
122
123 pub fn set_cursor(&mut self, pos: Position) {
128 let last_row = self.lines.len().saturating_sub(1);
129 let row = pos.row.min(last_row);
130 let line_chars = self.lines[row].chars().count();
131 let col = pos.col.min(line_chars);
132 self.cursor = Position::new(row, col);
133 }
134
135 pub fn set_sticky_col(&mut self, col: Option<usize>) {
139 self.sticky_col = col;
140 }
141
142 pub fn ensure_cursor_visible(&mut self) {
148 let cursor = self.cursor;
149 let v = self.viewport;
150 let wrap_active = !matches!(v.wrap, crate::Wrap::None) && v.text_width > 0;
151 if !wrap_active {
152 self.viewport.ensure_visible(cursor);
153 return;
154 }
155 if v.height == 0 {
156 return;
157 }
158 if cursor.row < v.top_row {
160 self.viewport.top_row = cursor.row;
161 self.viewport.top_col = 0;
162 return;
163 }
164 let height = v.height as usize;
165 loop {
168 let csr = self.cursor_screen_row_from(self.viewport.top_row);
169 match csr {
170 Some(row) if row < height => break,
171 _ => {}
172 }
173 let mut next = self.viewport.top_row + 1;
176 while next <= cursor.row && self.folds.iter().any(|f| f.hides(next)) {
177 next += 1;
178 }
179 if next > cursor.row {
180 self.viewport.top_row = cursor.row;
183 break;
184 }
185 self.viewport.top_row = next;
186 }
187 self.viewport.top_col = 0;
188 }
189
190 pub fn cursor_screen_row(&self) -> Option<usize> {
195 if matches!(self.viewport.wrap, crate::Wrap::None) || self.viewport.text_width == 0 {
196 return None;
197 }
198 self.cursor_screen_row_from(self.viewport.top_row)
199 }
200
201 pub fn screen_rows_between(&self, start: usize, end: usize) -> usize {
206 if start > end {
207 return 0;
208 }
209 let last = self.lines.len().saturating_sub(1);
210 let end = end.min(last);
211 let v = self.viewport;
212 let mut total = 0usize;
213 for r in start..=end {
214 if self.folds.iter().any(|f| f.hides(r)) {
215 continue;
216 }
217 if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
218 total += 1;
219 } else {
220 let line = self.lines.get(r).map(String::as_str).unwrap_or("");
221 total += crate::wrap::wrap_segments(line, v.text_width, v.wrap).len();
222 }
223 }
224 total
225 }
226
227 pub fn max_top_for_height(&self, height: usize) -> usize {
233 if height == 0 {
234 return 0;
235 }
236 let last = self.lines.len().saturating_sub(1);
237 let mut total = 0usize;
238 let mut row = last;
239 loop {
240 if !self.folds.iter().any(|f| f.hides(row)) {
241 let v = self.viewport;
242 total += if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
243 1
244 } else {
245 let line = self.lines.get(row).map(String::as_str).unwrap_or("");
246 crate::wrap::wrap_segments(line, v.text_width, v.wrap).len()
247 };
248 }
249 if total >= height {
250 return row;
251 }
252 if row == 0 {
253 return 0;
254 }
255 row -= 1;
256 }
257 }
258
259 fn cursor_screen_row_from(&self, top: usize) -> Option<usize> {
263 let cursor = self.cursor;
264 if cursor.row < top {
265 return None;
266 }
267 let v = self.viewport;
268 let mut screen = 0usize;
269 for r in top..=cursor.row {
270 if self.folds.iter().any(|f| f.hides(r)) {
271 continue;
272 }
273 let line = self.lines.get(r).map(String::as_str).unwrap_or("");
274 let segs = crate::wrap::wrap_segments(line, v.text_width, v.wrap);
275 if r == cursor.row {
276 let seg_idx = crate::wrap::segment_for_col(&segs, cursor.col);
277 return Some(screen + seg_idx);
278 }
279 screen += segs.len();
280 }
281 None
282 }
283
284 pub fn clamp_position(&self, pos: Position) -> Position {
288 let last_row = self.lines.len().saturating_sub(1);
289 let row = pos.row.min(last_row);
290 let line_chars = self.lines[row].chars().count();
291 let col = pos.col.min(line_chars);
292 Position::new(row, col)
293 }
294
295 pub(crate) fn lines_mut(&mut self) -> &mut Vec<String> {
298 &mut self.lines
299 }
300
301 pub(crate) fn search_state(&self) -> &SearchState {
305 &self.search
306 }
307 pub(crate) fn search_state_mut(&mut self) -> &mut SearchState {
308 &mut self.search
309 }
310
311 pub(crate) fn dirty_gen_bump(&mut self) {
314 self.dirty_gen = self.dirty_gen.wrapping_add(1);
315 }
316
317 pub fn set_spans(&mut self, spans: Vec<Vec<crate::Span>>) {
322 self.spans = spans;
323 self.dirty_gen_bump();
324 }
325
326 pub fn replace_all(&mut self, text: &str) {
332 let mut lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
333 if lines.is_empty() {
334 lines.push(String::new());
335 }
336 self.lines = lines;
337 let cursor = self.clamp_position(self.cursor);
339 self.cursor = cursor;
340 self.dirty_gen_bump();
341 }
342
343 #[cfg(test)]
347 pub(crate) fn set_spans_for_test(&mut self, spans: Vec<Vec<crate::Span>>) {
348 self.spans = spans;
349 }
350
351 pub fn marks(&self) -> &BTreeMap<char, Position> {
352 &self.marks
353 }
354
355 pub fn spans(&self) -> &[Vec<Span>] {
356 &self.spans
357 }
358
359 pub fn as_string(&self) -> String {
363 self.lines.join("\n")
364 }
365
366 pub fn row_count(&self) -> usize {
368 self.lines.len()
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn new_has_one_empty_row() {
378 let b = Buffer::new();
379 assert_eq!(b.row_count(), 1);
380 assert_eq!(b.line(0), Some(""));
381 assert_eq!(b.cursor(), Position::default());
382 }
383
384 #[test]
385 fn from_str_splits_on_newline() {
386 let b = Buffer::from_str("foo\nbar\nbaz");
387 assert_eq!(b.row_count(), 3);
388 assert_eq!(b.line(0), Some("foo"));
389 assert_eq!(b.line(2), Some("baz"));
390 }
391
392 #[test]
393 fn from_str_trailing_newline_keeps_empty_row() {
394 let b = Buffer::from_str("foo\n");
395 assert_eq!(b.row_count(), 2);
396 assert_eq!(b.line(1), Some(""));
397 }
398
399 #[test]
400 fn from_str_empty_input_keeps_one_row() {
401 let b = Buffer::from_str("");
402 assert_eq!(b.row_count(), 1);
403 assert_eq!(b.line(0), Some(""));
404 }
405
406 #[test]
407 fn as_string_round_trips() {
408 let b = Buffer::from_str("a\nb\nc");
409 assert_eq!(b.as_string(), "a\nb\nc");
410 }
411
412 #[test]
413 fn dirty_gen_starts_at_zero() {
414 assert_eq!(Buffer::new().dirty_gen(), 0);
415 }
416
417 #[test]
418 fn ensure_cursor_visible_wrap_scrolls_when_cursor_below_screen() {
419 let mut b = Buffer::from_str("aaaaaaaaaa\nb\nc");
420 {
421 let v = b.viewport_mut();
422 v.height = 3;
423 v.width = 4;
424 v.text_width = 4;
425 v.wrap = crate::Wrap::Char;
426 }
427 b.set_cursor(Position::new(2, 0));
431 b.ensure_cursor_visible();
432 assert_eq!(b.viewport().top_row, 1);
433 }
434
435 #[test]
436 fn ensure_cursor_visible_wrap_no_scroll_when_visible() {
437 let mut b = Buffer::from_str("aaaaaaaaaa\nb");
438 {
439 let v = b.viewport_mut();
440 v.height = 4;
441 v.width = 4;
442 v.text_width = 4;
443 v.wrap = crate::Wrap::Char;
444 }
445 b.set_cursor(Position::new(0, 5));
448 b.ensure_cursor_visible();
449 assert_eq!(b.viewport().top_row, 0);
450 }
451
452 #[test]
453 fn ensure_cursor_visible_wrap_snaps_top_when_cursor_above() {
454 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
455 {
456 let v = b.viewport_mut();
457 v.height = 2;
458 v.width = 4;
459 v.text_width = 4;
460 v.wrap = crate::Wrap::Char;
461 v.top_row = 3;
462 }
463 b.set_cursor(Position::new(1, 0));
464 b.ensure_cursor_visible();
465 assert_eq!(b.viewport().top_row, 1);
466 }
467
468 #[test]
469 fn screen_rows_between_sums_segments_under_wrap() {
470 let mut b = Buffer::from_str("aaaaaaaaa\nb\n");
472 {
473 let v = b.viewport_mut();
474 v.wrap = crate::Wrap::Char;
475 v.text_width = 4;
476 }
477 assert_eq!(b.screen_rows_between(0, 0), 3);
479 assert_eq!(b.screen_rows_between(0, 1), 4);
480 assert_eq!(b.screen_rows_between(0, 2), 5);
481 assert_eq!(b.screen_rows_between(1, 2), 2);
482 }
483
484 #[test]
485 fn screen_rows_between_one_per_doc_row_when_wrap_off() {
486 let b = Buffer::from_str("aaaaa\nb\nc");
487 assert_eq!(b.screen_rows_between(0, 2), 3);
488 }
489
490 #[test]
491 fn max_top_for_height_walks_back_until_height_reached() {
492 let mut b = Buffer::from_str("a\nb\nc\nd\neeeeeeee");
494 {
495 let v = b.viewport_mut();
496 v.wrap = crate::Wrap::Char;
497 v.text_width = 4;
498 }
499 assert_eq!(b.max_top_for_height(4), 2);
502 assert_eq!(b.max_top_for_height(99), 0);
504 }
505
506 #[test]
507 fn cursor_screen_row_returns_none_when_wrap_off() {
508 let b = Buffer::from_str("a");
509 assert!(b.cursor_screen_row().is_none());
510 }
511
512 #[test]
513 fn cursor_screen_row_under_wrap() {
514 let mut b = Buffer::from_str("aaaaaaaaaa\nb");
515 {
516 let v = b.viewport_mut();
517 v.wrap = crate::Wrap::Char;
518 v.text_width = 4;
519 }
520 b.set_cursor(Position::new(0, 5));
521 assert_eq!(b.cursor_screen_row(), Some(1));
523 b.set_cursor(Position::new(1, 0));
524 assert_eq!(b.cursor_screen_row(), Some(3));
526 }
527
528 #[test]
529 fn ensure_cursor_visible_falls_back_when_wrap_disabled() {
530 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
531 {
532 let v = b.viewport_mut();
533 v.height = 2;
534 v.width = 4;
535 v.text_width = 4;
536 v.wrap = crate::Wrap::None;
537 }
538 b.set_cursor(Position::new(4, 0));
539 b.ensure_cursor_visible();
540 assert_eq!(b.viewport().top_row, 3);
543 }
544}