Skip to main content

hjkl_buffer/
buffer.rs

1use std::sync::{Arc, Mutex, MutexGuard};
2
3use crate::content::Content;
4use crate::{Position, Viewport};
5
6/// Per-window view onto a [`Content`].
7///
8/// `Buffer` is the type the rest of `hjkl-buffer` — and all consumers —
9/// use directly. It owns exactly the state that is local to one editor
10/// window:
11///
12/// - `cursor` — the charwise caret for this window.
13///
14/// All document-level state (text rope, dirty generation, folds) lives on
15/// the inner [`Content`] and is accessed via `Arc<Mutex<Content>>`.
16/// Two `Buffer` instances that share the same `Arc` share text + folds
17/// but carry independent cursors — the Helix Document+View model.
18///
19/// ## `Send` + `Sync`
20///
21/// `Arc<Mutex<Content>>` is `Send + Sync`, so `Buffer` remains `Send`.
22/// The engine trait surface requires `Buffer: Send`; this constraint
23/// drove the choice of `Mutex` over `RefCell`. The mutex is never
24/// contended in normal operation (single-threaded app loop), so the
25/// lock cost is negligible (~5 ns uncontested).
26///
27/// ## 0.8.0 migration notes
28///
29/// The existing constructors ([`Buffer::new`], [`Buffer::from_str`],
30/// [`Buffer::replace_all`], etc.) keep the same external signatures.
31/// Callers that do not need multi-window sharing see no behaviour change.
32/// Use [`Buffer::new_view`] to create a second window onto the same
33/// [`Content`].
34///
35/// ## Viewport
36///
37/// The rope invariant — at least one line, never empty — is preserved by
38/// every mutation (ropey's empty rope already reports `len_lines() == 1`).
39/// The viewport itself (top_row, top_col, width, height, wrap, text_width)
40/// lives on the engine `Host` adapter; methods that need it take a
41/// `&Viewport` / `&mut Viewport` parameter so the rope-walking math stays
42/// here while runtime state lives there.
43pub struct Buffer {
44    /// Shared per-document state (text rope, dirty gen, folds).
45    pub(crate) content: Arc<Mutex<Content>>,
46    /// Charwise cursor. `col` is bound by the char count of `row` in
47    /// normal mode, one past it in operator-pending / insert.
48    cursor: Position,
49}
50
51impl Default for Buffer {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl Buffer {
58    // ── Constructors ──────────────────────────────────────────────
59
60    /// Construct an empty buffer with one empty row + cursor at `(0, 0)`.
61    pub fn new() -> Self {
62        Self {
63            content: Arc::new(Mutex::new(Content::new())),
64            cursor: Position::default(),
65        }
66    }
67
68    /// Build a buffer from a flat string. Splits on `\n`; a trailing
69    /// `\n` produces a trailing empty line (matches every text
70    /// editor's behaviour and keeps `from_text(buf.as_string())` an
71    /// identity round-trip in the common case).
72    #[allow(clippy::should_implement_trait)]
73    pub fn from_str(text: &str) -> Self {
74        Self {
75            content: Arc::new(Mutex::new(Content::from_str(text))),
76            cursor: Position::default(),
77        }
78    }
79
80    /// Create a second per-window view onto existing [`Content`].
81    ///
82    /// The new `Buffer` shares text + folds with every other view on the
83    /// same `Arc`. Its cursor starts at `(0, 0)` independently. This is
84    /// the primary entry point for split-window features.
85    ///
86    /// ```rust
87    /// # use hjkl_buffer::{Buffer, Content, Position};
88    /// # use std::sync::Arc;
89    /// # use std::sync::Mutex;
90    /// let a = Buffer::from_str("hello\nworld");
91    /// let content = a.content_arc();
92    /// let mut b = Buffer::new_view(Arc::clone(&content));
93    ///
94    /// // Cursors are independent.
95    /// let mut a = Buffer::new_view(Arc::clone(&content));
96    /// a.set_cursor(Position::new(1, 0));
97    /// assert_eq!(b.cursor(), Position::new(0, 0));
98    /// ```
99    pub fn new_view(content: Arc<Mutex<Content>>) -> Self {
100        Self {
101            content,
102            cursor: Position::default(),
103        }
104    }
105
106    /// Return a clone of the `Arc<Mutex<Content>>` so callers can
107    /// create additional views with [`Buffer::new_view`].
108    pub fn content_arc(&self) -> Arc<Mutex<Content>> {
109        Arc::clone(&self.content)
110    }
111
112    // ── Read-only accessors (delegate to Content) ─────────────────
113
114    pub fn cursor(&self) -> Position {
115        self.cursor
116    }
117
118    pub fn dirty_gen(&self) -> u64 {
119        self.content.lock().unwrap().dirty_gen
120    }
121
122    /// Number of rows in the buffer. Always `>= 1`.
123    pub fn row_count(&self) -> usize {
124        self.content.lock().unwrap().text.len_lines()
125    }
126
127    /// Concatenate the rows into a single `String` joined by `\n`.
128    ///
129    /// Equivalent to `rope.to_string()` — ropey's rope-to-string already
130    /// produces `\n`-joined content matching `split('\n').join("\n")`.
131    pub fn as_string(&self) -> String {
132        self.content.lock().unwrap().text.to_string()
133    }
134
135    // ── Cursor ops ────────────────────────────────────────────────
136
137    /// Set cursor without scrolling. Clamps to valid positions.
138    ///
139    /// The optional sticky column for `j`/`k` motions is **not** reset
140    /// by this call — it survives `set_cursor` intentionally.
141    pub fn set_cursor(&mut self, pos: Position) {
142        let c = self.content.lock().unwrap();
143        let n = c.text.len_lines();
144        let last_row = n.saturating_sub(1);
145        let row = pos.row.min(last_row);
146        let line_chars = rope_line_char_count(&c.text, row);
147        let col = pos.col.min(line_chars);
148        drop(c);
149        self.cursor = Position::new(row, col);
150    }
151
152    /// Bring the cursor into the visible [`Viewport`], scrolling by the
153    /// minimum amount needed.
154    pub fn ensure_cursor_visible(&mut self, viewport: &mut Viewport) {
155        let cursor = self.cursor;
156        let v = *viewport;
157        let wrap_active = !matches!(v.wrap, crate::Wrap::None) && v.text_width > 0;
158        if !wrap_active {
159            viewport.ensure_visible(cursor);
160            return;
161        }
162        if v.height == 0 {
163            return;
164        }
165        // Cursor above the visible region: snap top_row to it.
166        if cursor.row < v.top_row {
167            viewport.top_row = cursor.row;
168            viewport.top_col = 0;
169            return;
170        }
171        let height = v.height as usize;
172        // Push top_row forward until cursor lands inside [0, height).
173        loop {
174            let csr = self.cursor_screen_row_from(viewport, viewport.top_row);
175            match csr {
176                Some(row) if row < height => break,
177                _ => {}
178            }
179            let next = {
180                let c = self.content.lock().unwrap();
181                let mut n = viewport.top_row + 1;
182                while n <= cursor.row && c.folds.iter().any(|f| f.hides(n)) {
183                    n += 1;
184                }
185                n
186            };
187            if next > cursor.row {
188                viewport.top_row = cursor.row;
189                break;
190            }
191            viewport.top_row = next;
192        }
193        viewport.top_col = 0;
194    }
195
196    /// Cursor's screen row offset (0-based) from `viewport.top_row`.
197    pub fn cursor_screen_row(&self, viewport: &Viewport) -> Option<usize> {
198        if matches!(viewport.wrap, crate::Wrap::None) || viewport.text_width == 0 {
199            return None;
200        }
201        self.cursor_screen_row_from(viewport, viewport.top_row)
202    }
203
204    /// Number of screen rows the doc range `start..=end` occupies.
205    pub fn screen_rows_between(&self, viewport: &Viewport, start: usize, end: usize) -> usize {
206        if start > end {
207            return 0;
208        }
209        let c = self.content.lock().unwrap();
210        let n = c.text.len_lines();
211        let last = n.saturating_sub(1);
212        let end = end.min(last);
213        let v = *viewport;
214        let mut total = 0usize;
215        for r in start..=end {
216            if c.folds.iter().any(|f| f.hides(r)) {
217                continue;
218            }
219            if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
220                total += 1;
221            } else {
222                let line = rope_line_str(&c.text, r);
223                total += crate::wrap::wrap_segments(&line, v.text_width, v.wrap).len();
224            }
225        }
226        total
227    }
228
229    /// Earliest `top_row` such that `screen_rows_between(top, last)`
230    /// is at least `height`.
231    pub fn max_top_for_height(&self, viewport: &Viewport, height: usize) -> usize {
232        if height == 0 {
233            return 0;
234        }
235        let c = self.content.lock().unwrap();
236        let n = c.text.len_lines();
237        let last = n.saturating_sub(1);
238        let mut total = 0usize;
239        let mut row = last;
240        loop {
241            if !c.folds.iter().any(|f| f.hides(row)) {
242                let v = *viewport;
243                total += if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
244                    1
245                } else {
246                    let line = rope_line_str(&c.text, row);
247                    crate::wrap::wrap_segments(&line, v.text_width, v.wrap).len()
248                };
249            }
250            if total >= height {
251                return row;
252            }
253            if row == 0 {
254                return 0;
255            }
256            row -= 1;
257        }
258    }
259
260    /// Clamp `pos` to the buffer's content.
261    pub fn clamp_position(&self, pos: Position) -> Position {
262        let c = self.content.lock().unwrap();
263        let n = c.text.len_lines();
264        let last_row = n.saturating_sub(1);
265        let row = pos.row.min(last_row);
266        let line_chars = rope_line_char_count(&c.text, row);
267        let col = pos.col.min(line_chars);
268        Position::new(row, col)
269    }
270
271    /// Replace the buffer's full text in place. Cursor is clamped to
272    /// the new content.
273    pub fn replace_all(&mut self, text: &str) {
274        let new_cursor = {
275            let mut c = self.content.lock().unwrap();
276            c.text = ropey::Rope::from_str(text);
277            let n = c.text.len_lines();
278            let last_row = n.saturating_sub(1);
279            let row = self.cursor.row.min(last_row);
280            let line_chars = rope_line_char_count(&c.text, row);
281            let col = self.cursor.col.min(line_chars);
282            c.dirty_gen = c.dirty_gen.wrapping_add(1);
283            c.cached_joined = None;
284            c.cached_byte_len = None;
285            Position::new(row, col)
286        };
287        self.cursor = new_cursor;
288    }
289
290    // ── Crate-internal accessors (used by folds.rs) ───────────────
291
292    /// Bump the render-cache generation. Crate-internal.
293    pub(crate) fn dirty_gen_bump(&mut self) {
294        let mut c = self.content.lock().unwrap();
295        c.dirty_gen = c.dirty_gen.wrapping_add(1);
296        c.cached_joined = None;
297        c.cached_byte_len = None;
298    }
299
300    /// Canonical byte length of the document. `Rope::len_bytes()` is O(1)
301    /// and returns the same value as `to_string().len()` (i.e.
302    /// `sum(line_bytes) + (n_lines-1)` separators). Cached against
303    /// `dirty_gen` for API compatibility; the O(1) rope call makes the
304    /// cache essentially free but keeps the invalidation contract identical.
305    pub fn byte_len(&self) -> usize {
306        let mut c = self.content.lock().unwrap();
307        let dg = c.dirty_gen;
308        if let Some((cached_dg, len)) = c.cached_byte_len
309            && cached_dg == dg
310        {
311            return len;
312        }
313        let total = c.text.len_bytes();
314        c.cached_byte_len = Some((dg, total));
315        total
316    }
317
318    /// Return an `Arc<String>` of the full document, cached against
319    /// `dirty_gen`. Multiple per-tick consumers (syntax pipeline, LSP
320    /// notify, git signature, dirty hash) share the same `Arc` for the
321    /// same generation — first caller pays the `rope.to_string()` cost
322    /// (one alloc + one lock), the rest are O(1).
323    ///
324    /// Cache invalidates automatically on every `dirty_gen_bump` and on
325    /// `replace_all`, so callers never need to manage invalidation.
326    pub fn content_joined(&self) -> std::sync::Arc<String> {
327        let mut c = self.content.lock().unwrap();
328        let dg = c.dirty_gen;
329        if let Some((cached_dg, ref s)) = c.cached_joined
330            && cached_dg == dg
331        {
332            return std::sync::Arc::clone(s);
333        }
334        let joined = std::sync::Arc::new(c.text.to_string());
335        c.cached_joined = Some((dg, std::sync::Arc::clone(&joined)));
336        joined
337    }
338
339    /// Borrow the underlying rope. Hot-path consumers (tree-sitter
340    /// streaming parse, byte-range slicing) should use this instead of
341    /// `content_joined()` to avoid materializing the whole document as
342    /// a `String`.
343    ///
344    /// `ropey::Rope::clone` is O(1) — it Arc-clones the root node.
345    /// The clone gives the caller a snapshot they can read without
346    /// holding the content mutex.
347    pub fn rope(&self) -> ropey::Rope {
348        self.content.lock().unwrap().text.clone()
349    }
350
351    /// Shared access to the content guard. Crate-internal.
352    pub(crate) fn content_lock(&self) -> MutexGuard<'_, Content> {
353        self.content.lock().unwrap()
354    }
355
356    /// Exclusive access to Content. Crate-internal.
357    pub(crate) fn content_lock_mut(&mut self) -> MutexGuard<'_, Content> {
358        self.content.lock().unwrap()
359    }
360
361    // ── Screen-row helpers (private) ──────────────────────────────
362
363    fn cursor_screen_row_from(&self, viewport: &Viewport, top: usize) -> Option<usize> {
364        let cursor = self.cursor;
365        if cursor.row < top {
366            return None;
367        }
368        let c = self.content.lock().unwrap();
369        let v = *viewport;
370        let mut screen = 0usize;
371        for r in top..=cursor.row {
372            if c.folds.iter().any(|f| f.hides(r)) {
373                continue;
374            }
375            let line = rope_line_str(&c.text, r);
376            let segs = crate::wrap::wrap_segments(&line, v.text_width, v.wrap);
377            if r == cursor.row {
378                let seg_idx = crate::wrap::segment_for_col(&segs, cursor.col);
379                return Some(screen + seg_idx);
380            }
381            screen += segs.len();
382        }
383        None
384    }
385
386    // ── Per-buffer engine state accessors ─────────────────────────────────
387
388    pub fn undo_stack_is_empty(&self) -> bool {
389        self.content.lock().unwrap().undo_stack.is_empty()
390    }
391
392    pub fn redo_stack_is_empty(&self) -> bool {
393        self.content.lock().unwrap().redo_stack.is_empty()
394    }
395
396    pub fn undo_stack_len(&self) -> usize {
397        self.content.lock().unwrap().undo_stack.len()
398    }
399
400    pub fn push_undo_entry(&self, entry: crate::UndoEntry) {
401        self.content.lock().unwrap().undo_stack.push(entry);
402    }
403
404    pub fn push_redo_entry(&self, entry: crate::UndoEntry) {
405        self.content.lock().unwrap().redo_stack.push(entry);
406    }
407
408    pub fn pop_undo_entry(&self) -> Option<crate::UndoEntry> {
409        self.content.lock().unwrap().undo_stack.pop()
410    }
411
412    pub fn pop_redo_entry(&self) -> Option<crate::UndoEntry> {
413        self.content.lock().unwrap().redo_stack.pop()
414    }
415
416    pub fn peek_undo_timestamp(&self) -> Option<std::time::SystemTime> {
417        self.content
418            .lock()
419            .unwrap()
420            .undo_stack
421            .last()
422            .map(|e| e.timestamp)
423    }
424
425    pub fn peek_redo_timestamp(&self) -> Option<std::time::SystemTime> {
426        self.content
427            .lock()
428            .unwrap()
429            .redo_stack
430            .last()
431            .map(|e| e.timestamp)
432    }
433
434    pub fn clear_undo_redo(&self) {
435        let mut c = self.content.lock().unwrap();
436        c.undo_stack.clear();
437        c.redo_stack.clear();
438    }
439
440    pub fn clear_redo(&self) {
441        self.content.lock().unwrap().redo_stack.clear();
442    }
443
444    pub fn cap_undo(&self, cap: usize) {
445        if cap > 0 {
446            let mut c = self.content.lock().unwrap();
447            let len = c.undo_stack.len();
448            if len > cap {
449                c.undo_stack.drain(..len - cap);
450            }
451        }
452    }
453
454    pub fn content_dirty(&self) -> bool {
455        self.content.lock().unwrap().content_dirty
456    }
457
458    pub fn set_content_dirty(&self, v: bool) {
459        self.content.lock().unwrap().content_dirty = v;
460    }
461
462    pub fn mark_content_dirty(&self) {
463        let mut c = self.content.lock().unwrap();
464        c.content_dirty = true;
465        c.cached_editor_content = None;
466    }
467
468    pub fn take_dirty(&self) -> bool {
469        let mut c = self.content.lock().unwrap();
470        let v = c.content_dirty;
471        c.content_dirty = false;
472        v
473    }
474
475    pub fn cached_editor_content(&self) -> Option<std::sync::Arc<String>> {
476        self.content.lock().unwrap().cached_editor_content.clone()
477    }
478
479    pub fn set_cached_editor_content(&self, arc: std::sync::Arc<String>) {
480        self.content.lock().unwrap().cached_editor_content = Some(arc);
481    }
482
483    pub fn push_fold_op(&self, op: crate::FoldOp) {
484        self.content.lock().unwrap().pending_fold_ops.push(op);
485    }
486
487    pub fn take_fold_ops(&self) -> Vec<crate::FoldOp> {
488        std::mem::take(&mut self.content.lock().unwrap().pending_fold_ops)
489    }
490
491    pub fn extend_change_log(&self, edits: impl IntoIterator<Item = crate::EngineEdit>) {
492        self.content.lock().unwrap().change_log.extend(edits);
493    }
494
495    pub fn take_change_log(&self) -> Vec<crate::EngineEdit> {
496        std::mem::take(&mut self.content.lock().unwrap().change_log)
497    }
498
499    pub fn extend_pending_content_edits(
500        &self,
501        edits: impl IntoIterator<Item = crate::ContentEdit>,
502    ) {
503        self.content
504            .lock()
505            .unwrap()
506            .pending_content_edits
507            .extend(edits);
508    }
509
510    pub fn push_pending_content_edit(&self, edit: crate::ContentEdit) {
511        self.content
512            .lock()
513            .unwrap()
514            .pending_content_edits
515            .push(edit);
516    }
517
518    pub fn take_pending_content_edits(&self) -> Vec<crate::ContentEdit> {
519        std::mem::take(&mut self.content.lock().unwrap().pending_content_edits)
520    }
521
522    pub fn clear_pending_content_edits(&self) {
523        self.content.lock().unwrap().pending_content_edits.clear();
524    }
525
526    pub fn pending_content_reset(&self) -> bool {
527        self.content.lock().unwrap().pending_content_reset
528    }
529
530    pub fn set_pending_content_reset(&self, v: bool) {
531        self.content.lock().unwrap().pending_content_reset = v;
532    }
533
534    pub fn take_pending_content_reset(&self) -> bool {
535        let mut c = self.content.lock().unwrap();
536        let v = c.pending_content_reset;
537        c.pending_content_reset = false;
538        v
539    }
540
541    pub fn mark(&self, c: char) -> Option<(usize, usize)> {
542        self.content_lock().marks.get(&c).copied()
543    }
544    pub fn set_mark(&mut self, c: char, pos: (usize, usize)) {
545        self.content_lock_mut().marks.insert(c, pos);
546    }
547    pub fn clear_mark(&mut self, c: char) {
548        self.content_lock_mut().marks.remove(&c);
549    }
550    pub fn marks_cloned(&self) -> std::collections::BTreeMap<char, (usize, usize)> {
551        self.content_lock().marks.clone()
552    }
553    pub fn set_marks(&mut self, marks: std::collections::BTreeMap<char, (usize, usize)>) {
554        self.content_lock_mut().marks = marks;
555    }
556    /// Drop marks inside `[edit_start, drop_end)` and shift marks at/after
557    /// `shift_threshold` by `delta` rows (clamped to 0). Mirrors the engine's
558    /// edit-coherence pass for the per-buffer mark map (#154).
559    pub fn rebase_marks(
560        &mut self,
561        edit_start: usize,
562        drop_end: usize,
563        shift_threshold: usize,
564        delta: isize,
565    ) {
566        let mut c = self.content_lock_mut();
567        let mut to_drop: Vec<char> = Vec::new();
568        for (ch, (row, _col)) in c.marks.iter_mut() {
569            if (edit_start..drop_end).contains(row) {
570                to_drop.push(*ch);
571            } else if *row >= shift_threshold {
572                *row = ((*row as isize) + delta).max(0) as usize;
573            }
574        }
575        for ch in to_drop {
576            c.marks.remove(&ch);
577        }
578    }
579    pub fn syntax_fold_ranges_cloned(&self) -> Vec<(usize, usize)> {
580        self.content_lock().syntax_fold_ranges.clone()
581    }
582    pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
583        self.content_lock_mut().syntax_fold_ranges = ranges;
584    }
585}
586
587// ── Rope line helpers (free functions over &ropey::Rope) ─────────────
588
589/// Return logical line `row` as a `String`, stripping the trailing `\n`
590/// that ropey includes for non-final lines.
591pub fn rope_line_str(rope: &ropey::Rope, row: usize) -> String {
592    let mut s = rope.line(row).to_string();
593    // ropey includes the trailing '\n' for non-final lines; strip it.
594    if s.ends_with('\n') {
595        s.pop();
596    }
597    s
598}
599
600/// Byte length of logical line `row` (excluding the trailing `\n`).
601pub fn rope_line_bytes(rope: &ropey::Rope, row: usize) -> usize {
602    let slice = rope.line(row);
603    let bytes = slice.len_bytes();
604    // ropey includes the '\n' byte for non-final lines; subtract it.
605    if row + 1 < rope.len_lines() && bytes > 0 {
606        bytes - 1
607    } else {
608        bytes
609    }
610}
611
612/// Char count of logical line `row` (excluding the trailing `\n`).
613pub(crate) fn rope_line_char_count(rope: &ropey::Rope, row: usize) -> usize {
614    let slice = rope.line(row);
615    let chars = slice.len_chars();
616    // ropey includes the '\n' char for non-final lines; subtract it.
617    if row + 1 < rope.len_lines() && chars > 0 {
618        chars - 1
619    } else {
620        chars
621    }
622}
623
624/// Char index from `(row, col)` where `col` is a char index within the line.
625pub(crate) fn pos_to_char_idx(rope: &ropey::Rope, row: usize, col: usize) -> usize {
626    let line_start = rope.line_to_char(row);
627    let line_char_count = rope_line_char_count(rope, row);
628    line_start + col.min(line_char_count)
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn new_has_one_empty_row() {
637        let b = Buffer::new();
638        assert_eq!(b.row_count(), 1);
639        assert_eq!(rope_line_str(&b.rope(), 0), "");
640        assert_eq!(b.cursor(), Position::default());
641    }
642
643    #[test]
644    fn from_str_splits_on_newline() {
645        let b = Buffer::from_str("foo\nbar\nbaz");
646        assert_eq!(b.row_count(), 3);
647        assert_eq!(rope_line_str(&b.rope(), 0), "foo");
648        assert_eq!(rope_line_str(&b.rope(), 2), "baz");
649    }
650
651    #[test]
652    fn from_str_trailing_newline_keeps_empty_row() {
653        let b = Buffer::from_str("foo\n");
654        assert_eq!(b.row_count(), 2);
655        assert_eq!(rope_line_str(&b.rope(), 1), "");
656    }
657
658    #[test]
659    fn from_str_empty_input_keeps_one_row() {
660        let b = Buffer::from_str("");
661        assert_eq!(b.row_count(), 1);
662        assert_eq!(rope_line_str(&b.rope(), 0), "");
663    }
664
665    #[test]
666    fn as_string_round_trips() {
667        let b = Buffer::from_str("a\nb\nc");
668        assert_eq!(b.as_string(), "a\nb\nc");
669    }
670
671    #[test]
672    fn dirty_gen_starts_at_zero() {
673        assert_eq!(Buffer::new().dirty_gen(), 0);
674    }
675
676    fn vp_wrap(width: u16, height: u16) -> Viewport {
677        Viewport {
678            top_row: 0,
679            top_col: 0,
680            width,
681            height,
682            wrap: crate::Wrap::Char,
683            text_width: width,
684            tab_width: 0,
685        }
686    }
687
688    #[test]
689    fn ensure_cursor_visible_wrap_scrolls_when_cursor_below_screen() {
690        let mut b = Buffer::from_str("aaaaaaaaaa\nb\nc");
691        let mut v = vp_wrap(4, 3);
692        b.set_cursor(Position::new(2, 0));
693        b.ensure_cursor_visible(&mut v);
694        assert_eq!(v.top_row, 1);
695    }
696
697    #[test]
698    fn ensure_cursor_visible_wrap_no_scroll_when_visible() {
699        let mut b = Buffer::from_str("aaaaaaaaaa\nb");
700        let mut v = vp_wrap(4, 4);
701        b.set_cursor(Position::new(0, 5));
702        b.ensure_cursor_visible(&mut v);
703        assert_eq!(v.top_row, 0);
704    }
705
706    #[test]
707    fn ensure_cursor_visible_wrap_snaps_top_when_cursor_above() {
708        let mut b = Buffer::from_str("a\nb\nc\nd\ne");
709        let mut v = vp_wrap(4, 2);
710        v.top_row = 3;
711        b.set_cursor(Position::new(1, 0));
712        b.ensure_cursor_visible(&mut v);
713        assert_eq!(v.top_row, 1);
714    }
715
716    #[test]
717    fn screen_rows_between_sums_segments_under_wrap() {
718        let b = Buffer::from_str("aaaaaaaaa\nb\n");
719        let v = vp_wrap(4, 0);
720        assert_eq!(b.screen_rows_between(&v, 0, 0), 3);
721        assert_eq!(b.screen_rows_between(&v, 0, 1), 4);
722        assert_eq!(b.screen_rows_between(&v, 0, 2), 5);
723        assert_eq!(b.screen_rows_between(&v, 1, 2), 2);
724    }
725
726    #[test]
727    fn screen_rows_between_one_per_doc_row_when_wrap_off() {
728        let b = Buffer::from_str("aaaaa\nb\nc");
729        let v = Viewport::default();
730        assert_eq!(b.screen_rows_between(&v, 0, 2), 3);
731    }
732
733    #[test]
734    fn max_top_for_height_walks_back_until_height_reached() {
735        let b = Buffer::from_str("a\nb\nc\nd\neeeeeeee");
736        let v = vp_wrap(4, 0);
737        assert_eq!(b.max_top_for_height(&v, 4), 2);
738        assert_eq!(b.max_top_for_height(&v, 99), 0);
739    }
740
741    #[test]
742    fn cursor_screen_row_returns_none_when_wrap_off() {
743        let b = Buffer::from_str("a");
744        let v = Viewport::default();
745        assert!(b.cursor_screen_row(&v).is_none());
746    }
747
748    #[test]
749    fn cursor_screen_row_under_wrap() {
750        let mut b = Buffer::from_str("aaaaaaaaaa\nb");
751        let v = vp_wrap(4, 0);
752        b.set_cursor(Position::new(0, 5));
753        assert_eq!(b.cursor_screen_row(&v), Some(1));
754        b.set_cursor(Position::new(1, 0));
755        assert_eq!(b.cursor_screen_row(&v), Some(3));
756    }
757
758    #[test]
759    fn ensure_cursor_visible_falls_back_when_wrap_disabled() {
760        let mut b = Buffer::from_str("a\nb\nc\nd\ne");
761        let mut v = Viewport {
762            top_row: 0,
763            top_col: 0,
764            width: 4,
765            height: 2,
766            wrap: crate::Wrap::None,
767            text_width: 4,
768            tab_width: 0,
769        };
770        b.set_cursor(Position::new(4, 0));
771        b.ensure_cursor_visible(&mut v);
772        assert_eq!(v.top_row, 3);
773    }
774
775    // ── Per-buffer engine state tests (new in 0.33.0 / Phase B) ──────
776
777    /// Undo entries pushed via one `Buffer` view are visible via
778    /// another view sharing the same `Content` — proving that the
779    /// undo stack lives on `Content`, not on the per-window `Buffer`.
780    #[test]
781    fn undo_stack_shared_across_views() {
782        use crate::UndoEntry;
783        use std::time::SystemTime;
784
785        let a = Buffer::from_str("hello");
786        let arc = a.content_arc();
787        let view_a = Buffer::new_view(Arc::clone(&arc));
788        let view_b = Buffer::new_view(Arc::clone(&arc));
789
790        assert!(view_a.undo_stack_is_empty());
791        assert_eq!(view_a.undo_stack_len(), 0);
792
793        view_a.push_undo_entry(UndoEntry {
794            rope: view_a.rope(),
795            cursor: (0, 0),
796            timestamp: SystemTime::UNIX_EPOCH,
797        });
798
799        // Push via view_a is visible via view_b.
800        assert_eq!(view_b.undo_stack_len(), 1);
801        assert!(!view_b.undo_stack_is_empty());
802    }
803
804    /// Redo entries pushed via one view are visible via another.
805    #[test]
806    fn redo_stack_shared_across_views() {
807        use crate::UndoEntry;
808        use std::time::SystemTime;
809
810        let a = Buffer::from_str("world");
811        let arc = a.content_arc();
812        let view_a = Buffer::new_view(Arc::clone(&arc));
813        let view_b = Buffer::new_view(Arc::clone(&arc));
814
815        assert!(view_a.redo_stack_is_empty());
816
817        view_b.push_redo_entry(UndoEntry {
818            rope: view_b.rope(),
819            cursor: (0, 2),
820            timestamp: SystemTime::UNIX_EPOCH,
821        });
822
823        let entry = view_a.pop_redo_entry();
824        assert!(entry.is_some());
825        assert_eq!(entry.unwrap().cursor, (0, 2));
826    }
827
828    /// `clear_undo_redo` clears both stacks and the effect is shared.
829    #[test]
830    fn clear_undo_redo_shared_across_views() {
831        use crate::UndoEntry;
832        use std::time::SystemTime;
833
834        let a = Buffer::from_str("abc");
835        let arc = a.content_arc();
836        let view_a = Buffer::new_view(Arc::clone(&arc));
837        let view_b = Buffer::new_view(Arc::clone(&arc));
838
839        view_a.push_undo_entry(UndoEntry {
840            rope: view_a.rope(),
841            cursor: (0, 0),
842            timestamp: SystemTime::UNIX_EPOCH,
843        });
844        view_a.push_redo_entry(UndoEntry {
845            rope: view_a.rope(),
846            cursor: (0, 1),
847            timestamp: SystemTime::UNIX_EPOCH,
848        });
849
850        view_b.clear_undo_redo();
851        assert!(view_a.undo_stack_is_empty());
852        assert!(view_a.redo_stack_is_empty());
853    }
854
855    /// `content_dirty` flag is shared across views.
856    #[test]
857    fn content_dirty_shared_across_views() {
858        let a = Buffer::from_str("test");
859        let arc = a.content_arc();
860        let view_a = Buffer::new_view(Arc::clone(&arc));
861        let view_b = Buffer::new_view(Arc::clone(&arc));
862
863        assert!(!view_a.content_dirty());
864
865        view_b.mark_content_dirty();
866        assert!(view_a.content_dirty());
867
868        let taken = view_a.take_dirty();
869        assert!(taken);
870        assert!(!view_b.content_dirty());
871    }
872
873    /// `pending_fold_ops` push and take are shared across views.
874    #[test]
875    fn pending_fold_ops_shared_across_views() {
876        let a = Buffer::from_str("a\nb\nc");
877        let arc = a.content_arc();
878        let view_a = Buffer::new_view(Arc::clone(&arc));
879        let view_b = Buffer::new_view(Arc::clone(&arc));
880
881        view_a.push_fold_op(crate::FoldOp::Add {
882            start_row: 0,
883            end_row: 1,
884            closed: true,
885        });
886
887        let ops = view_b.take_fold_ops();
888        assert_eq!(ops.len(), 1);
889        assert!(matches!(
890            ops[0],
891            crate::FoldOp::Add {
892                start_row: 0,
893                end_row: 1,
894                closed: true
895            }
896        ));
897    }
898
899    /// `pending_content_reset` flag is shared across views.
900    #[test]
901    fn pending_content_reset_shared_across_views() {
902        let a = Buffer::from_str("x");
903        let arc = a.content_arc();
904        let view_a = Buffer::new_view(Arc::clone(&arc));
905        let view_b = Buffer::new_view(Arc::clone(&arc));
906
907        assert!(!view_a.pending_content_reset());
908        view_b.set_pending_content_reset(true);
909        assert!(view_a.pending_content_reset());
910        let taken = view_a.take_pending_content_reset();
911        assert!(taken);
912        assert!(!view_b.pending_content_reset());
913    }
914
915    // ── View-split tests (new in 0.8.0) ──────────────────────────
916
917    /// Two `Buffer` views sharing one `Content` must have independent
918    /// cursors.
919    #[test]
920    fn buffer_views_independent_cursors() {
921        let a = Buffer::from_str("hello\nworld");
922        let arc = a.content_arc();
923        let mut view_a = Buffer::new_view(Arc::clone(&arc));
924        let mut view_b = Buffer::new_view(Arc::clone(&arc));
925
926        view_a.set_cursor(Position::new(1, 3));
927        // view_b cursor must remain at (0, 0).
928        assert_eq!(view_b.cursor(), Position::new(0, 0));
929
930        view_b.set_cursor(Position::new(0, 2));
931        // view_a cursor must remain at (1, 3).
932        assert_eq!(view_a.cursor(), Position::new(1, 3));
933    }
934
935    /// An edit applied via one view must be visible via the other.
936    #[test]
937    fn buffer_views_share_content() {
938        use crate::edit::Edit;
939
940        let a = Buffer::from_str("foo");
941        let arc = a.content_arc();
942        let mut view_a = Buffer::new_view(Arc::clone(&arc));
943        let view_b = Buffer::new_view(Arc::clone(&arc));
944
945        view_a.apply_edit(Edit::InsertStr {
946            at: Position::new(0, 3),
947            text: "bar".into(),
948        });
949
950        assert_eq!(rope_line_str(&view_a.rope(), 0), "foobar");
951        assert_eq!(rope_line_str(&view_b.rope(), 0), "foobar");
952    }
953}
954
955#[cfg(test)]
956mod marks_shared_content_tests {
957    use super::*;
958
959    #[test]
960    fn marks_shared_across_views() {
961        // Two Buffer views on the same Content share marks (#154).
962        let a = Buffer::from_str("hello\nworld");
963        let content = a.content_arc();
964        let mut view_a = Buffer::new_view(std::sync::Arc::clone(&content));
965        let view_b = Buffer::new_view(std::sync::Arc::clone(&content));
966
967        // Set mark 'x' on view_a.
968        view_a.set_mark('x', (1, 3));
969
970        // view_b must see the same mark via shared Content.
971        assert_eq!(view_b.mark('x'), Some((1, 3)));
972    }
973
974    #[test]
975    fn syntax_fold_ranges_shared_across_views() {
976        let a = Buffer::from_str("fn foo() {\n  bar();\n}");
977        let content = a.content_arc();
978        let mut view_a = Buffer::new_view(std::sync::Arc::clone(&content));
979        let view_b = Buffer::new_view(std::sync::Arc::clone(&content));
980
981        view_a.set_syntax_fold_ranges(vec![(0, 2)]);
982
983        assert_eq!(view_b.syntax_fold_ranges_cloned(), vec![(0, 2)]);
984    }
985}