Skip to main content

hjkl_buffer/
buffer.rs

1use std::sync::{Arc, Mutex, MutexGuard};
2
3use crate::content::Buffer;
4use crate::{Position, Viewport};
5
6/// Per-window view onto a [`Buffer`].
7///
8/// `View` 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 [`Buffer`] and is accessed via `Arc<Mutex<Buffer>>`.
16/// Two `View` 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<Buffer>>` is `Send + Sync`, so `View` remains `Send`.
22/// The engine trait surface requires `View: 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 ([`View::new`], [`View::from_str`],
30/// [`View::replace_all`], etc.) keep the same external signatures.
31/// Callers that do not need multi-window sharing see no behaviour change.
32/// Use [`View::new_view`] to create a second window onto the same
33/// [`Buffer`].
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 View {
44    /// Shared per-document state (text rope, dirty gen, folds).
45    pub(crate) content: Arc<Mutex<Buffer>>,
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 View {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl View {
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(Buffer::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(Buffer::from_str(text))),
76            cursor: Position::default(),
77        }
78    }
79
80    /// Create a second per-window view onto existing [`Buffer`].
81    ///
82    /// The new `View` 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::{View, Buffer, Position};
88    /// # use std::sync::Arc;
89    /// # use std::sync::Mutex;
90    /// let a = View::from_str("hello\nworld");
91    /// let content = a.content_arc();
92    /// let mut b = View::new_view(Arc::clone(&content));
93    ///
94    /// // Cursors are independent.
95    /// let mut a = View::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<Buffer>>) -> Self {
100        Self {
101            content,
102            cursor: Position::default(),
103        }
104    }
105
106    /// Return a clone of the `Arc<Mutex<Buffer>>` so callers can
107    /// create additional views with [`View::new_view`].
108    pub fn content_arc(&self) -> Arc<Mutex<Buffer>> {
109        Arc::clone(&self.content)
110    }
111
112    // ── Read-only accessors (delegate to Buffer) ─────────────────
113
114    pub fn cursor(&self) -> Position {
115        self.cursor
116    }
117
118    /// The last cursor `(row, col)` committed on the shared [`Buffer`] by any
119    /// view (see [`View::set_cursor`]). This is the "last-moved cursor across
120    /// all windows" the cross-session cursor store persists — not this view's
121    /// own live cursor. Best-effort; read at write/close/exit.
122    pub fn last_cursor(&self) -> (usize, usize) {
123        self.content.lock().unwrap().last_cursor
124    }
125
126    pub fn dirty_gen(&self) -> u64 {
127        self.content.lock().unwrap().dirty_gen
128    }
129
130    /// Number of rows in the buffer. Always `>= 1`.
131    pub fn row_count(&self) -> usize {
132        self.content.lock().unwrap().text.len_lines()
133    }
134
135    /// Concatenate the rows into a single `String` joined by `\n`.
136    ///
137    /// Equivalent to `rope.to_string()` — ropey's rope-to-string already
138    /// produces `\n`-joined content matching `split('\n').join("\n")`.
139    pub fn as_string(&self) -> String {
140        self.content.lock().unwrap().text.to_string()
141    }
142
143    // ── Cursor ops ────────────────────────────────────────────────
144
145    /// Set cursor without scrolling. Clamps to valid positions.
146    ///
147    /// The optional sticky column for `j`/`k` motions is **not** reset
148    /// by this call — it survives `set_cursor` intentionally.
149    pub fn set_cursor(&mut self, pos: Position) {
150        let mut c = self.content.lock().unwrap();
151        let n = c.text.len_lines();
152        let last_row = n.saturating_sub(1);
153        let row = pos.row.min(last_row);
154        let line_chars = rope_line_char_count(&c.text, row);
155        let col = pos.col.min(line_chars);
156        // Single choke point for cursor moves: record the last-moved cursor on
157        // the shared `Buffer` so the most-recent move across ALL views onto
158        // this document wins. Cheap — no I/O.
159        c.last_cursor = (row, col);
160        drop(c);
161        self.cursor = Position::new(row, col);
162    }
163
164    /// Bring the cursor into the visible [`Viewport`], scrolling by the
165    /// minimum amount needed.
166    pub fn ensure_cursor_visible(&mut self, viewport: &mut Viewport) {
167        let cursor = self.cursor;
168        let v = *viewport;
169        let wrap_active = !matches!(v.wrap, crate::Wrap::None) && v.text_width > 0;
170        if !wrap_active {
171            viewport.ensure_visible(cursor);
172            return;
173        }
174        if v.height == 0 {
175            return;
176        }
177        // Cursor above the visible region: snap top_row to it.
178        if cursor.row < v.top_row {
179            viewport.top_row = cursor.row;
180            viewport.top_col = 0;
181            return;
182        }
183        let height = v.height as usize;
184        // Compute the cursor's screen row once, then push `top_row` down
185        // incrementally: each dropped row reduces the screen row by its own
186        // visible height. O(distance) instead of recomputing
187        // `cursor_screen_row_from` every step (which was O(distance^2) on a
188        // large soft-wrapped jump).
189        let Some(mut screen) = self.cursor_screen_row_from(viewport, viewport.top_row) else {
190            // Two ways to land here, both repaired by snapping `top_row` to the
191            // cursor's row clamped against the live rope:
192            //   - a concurrent view shrink dropped rows, so this view's stale
193            //     cursor row *and* `top_row` are both past the last line;
194            //   - the cursor's row is hidden by a fold, so the walk from
195            //     `top_row` never reaches it.
196            // The clamp is what does the work in the shrink case: `cursor.row
197            // >= top_row` is already known here, so assigning the raw
198            // `cursor.row` would push `top_row` further past the end rather
199            // than pulling it back into the rope.
200            let last_row = {
201                let c = self.content.lock().unwrap();
202                c.text.len_lines().saturating_sub(1)
203            };
204            viewport.top_row = cursor.row.min(last_row);
205            viewport.top_col = 0;
206            return;
207        };
208        while screen >= height {
209            let c = self.content.lock().unwrap();
210            let mut next = viewport.top_row + 1;
211            while next <= cursor.row && c.folds.iter().any(|f| f.hides(next)) {
212                next += 1;
213            }
214            if next > cursor.row {
215                drop(c);
216                viewport.top_row = cursor.row;
217                break;
218            }
219            // Removing rows [top_row, next) drops their visible heights (hidden
220            // rows contribute 0). After this, `screen` equals
221            // `cursor_screen_row_from(next)`.
222            for r in viewport.top_row..next {
223                if c.folds.iter().any(|f| f.hides(r)) {
224                    continue;
225                }
226                let line = rope_line_str(&c.text, r);
227                screen -= crate::wrap::wrap_segments(&line, v.text_width, v.wrap).len();
228            }
229            drop(c);
230            viewport.top_row = next;
231        }
232        viewport.top_col = 0;
233    }
234
235    /// Cursor's screen row offset (0-based) from `viewport.top_row`.
236    pub fn cursor_screen_row(&self, viewport: &Viewport) -> Option<usize> {
237        if matches!(viewport.wrap, crate::Wrap::None) || viewport.text_width == 0 {
238            return None;
239        }
240        self.cursor_screen_row_from(viewport, viewport.top_row)
241    }
242
243    /// Number of screen rows the doc range `start..=end` occupies.
244    pub fn screen_rows_between(&self, viewport: &Viewport, start: usize, end: usize) -> usize {
245        if start > end {
246            return 0;
247        }
248        let c = self.content.lock().unwrap();
249        let n = c.text.len_lines();
250        let last = n.saturating_sub(1);
251        let end = end.min(last);
252        let v = *viewport;
253        let mut total = 0usize;
254        for r in start..=end {
255            if c.folds.iter().any(|f| f.hides(r)) {
256                continue;
257            }
258            if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
259                total += 1;
260            } else {
261                let line = rope_line_str(&c.text, r);
262                total += crate::wrap::wrap_segments(&line, v.text_width, v.wrap).len();
263            }
264        }
265        total
266    }
267
268    /// Earliest `top_row` such that `screen_rows_between(top, last)`
269    /// is at least `height`.
270    pub fn max_top_for_height(&self, viewport: &Viewport, height: usize) -> usize {
271        if height == 0 {
272            return 0;
273        }
274        let c = self.content.lock().unwrap();
275        let n = c.text.len_lines();
276        let last = n.saturating_sub(1);
277        let mut total = 0usize;
278        let mut row = last;
279        loop {
280            if !c.folds.iter().any(|f| f.hides(row)) {
281                let v = *viewport;
282                total += if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
283                    1
284                } else {
285                    let line = rope_line_str(&c.text, row);
286                    crate::wrap::wrap_segments(&line, v.text_width, v.wrap).len()
287                };
288            }
289            if total >= height {
290                return row;
291            }
292            if row == 0 {
293                return 0;
294            }
295            row -= 1;
296        }
297    }
298
299    /// Clamp `pos` to the buffer's content.
300    pub fn clamp_position(&self, pos: Position) -> Position {
301        let c = self.content.lock().unwrap();
302        let n = c.text.len_lines();
303        let last_row = n.saturating_sub(1);
304        let row = pos.row.min(last_row);
305        let line_chars = rope_line_char_count(&c.text, row);
306        let col = pos.col.min(line_chars);
307        Position::new(row, col)
308    }
309
310    /// Replace the buffer's full text in place. Cursor is clamped to
311    /// the new content.
312    pub fn replace_all(&mut self, text: &str) {
313        let new_cursor = {
314            let mut c = self.content.lock().unwrap();
315            c.text = ropey::Rope::from_str(text);
316            let n = c.text.len_lines();
317            let last_row = n.saturating_sub(1);
318            let row = self.cursor.row.min(last_row);
319            let line_chars = rope_line_char_count(&c.text, row);
320            let col = self.cursor.col.min(line_chars);
321            c.dirty_gen = c.dirty_gen.wrapping_add(1);
322            c.cached_joined = None;
323            c.cached_byte_len = None;
324            Position::new(row, col)
325        };
326        self.cursor = new_cursor;
327    }
328
329    // ── Crate-internal accessors (used by folds.rs) ───────────────
330
331    /// Bump the fold-mutation generation. Crate-internal — every fold
332    /// mutator in [`crate::folds`] calls it (via `folds_changed`) after it
333    /// actually changes the fold set. Deliberately separate from
334    /// [`Self::dirty_gen_bump`]: text edits bump `dirty_gen` on every
335    /// keystroke, and a fold-snapshot cache keyed off that would never hit.
336    pub(crate) fn fold_gen_bump(&mut self) {
337        let mut c = self.content.lock().unwrap();
338        c.fold_gen = c.fold_gen.wrapping_add(1);
339    }
340
341    /// Bump the render-cache generation. Crate-internal.
342    pub(crate) fn dirty_gen_bump(&mut self) {
343        let mut c = self.content.lock().unwrap();
344        c.dirty_gen = c.dirty_gen.wrapping_add(1);
345        c.cached_joined = None;
346        c.cached_byte_len = None;
347    }
348
349    /// Canonical byte length of the document. `Rope::len_bytes()` is O(1)
350    /// and returns the same value as `to_string().len()` (i.e.
351    /// `sum(line_bytes) + (n_lines-1)` separators). Cached against
352    /// `dirty_gen` for API compatibility; the O(1) rope call makes the
353    /// cache essentially free but keeps the invalidation contract identical.
354    pub fn byte_len(&self) -> usize {
355        let mut c = self.content.lock().unwrap();
356        let dg = c.dirty_gen;
357        if let Some((cached_dg, len)) = c.cached_byte_len
358            && cached_dg == dg
359        {
360            return len;
361        }
362        let total = c.text.len_bytes();
363        c.cached_byte_len = Some((dg, total));
364        total
365    }
366
367    /// Return an `Arc<String>` of the full document, cached against
368    /// `dirty_gen`. Multiple per-tick consumers (syntax pipeline, LSP
369    /// notify, git signature, dirty hash) share the same `Arc` for the
370    /// same generation — first caller pays the `rope.to_string()` cost
371    /// (one alloc + one lock), the rest are O(1).
372    ///
373    /// Cache invalidates automatically on every `dirty_gen_bump` and on
374    /// `replace_all`, so callers never need to manage invalidation.
375    pub fn content_joined(&self) -> std::sync::Arc<String> {
376        let mut c = self.content.lock().unwrap();
377        let dg = c.dirty_gen;
378        if let Some((cached_dg, ref s)) = c.cached_joined
379            && cached_dg == dg
380        {
381            return std::sync::Arc::clone(s);
382        }
383        let joined = std::sync::Arc::new(c.text.to_string());
384        c.cached_joined = Some((dg, std::sync::Arc::clone(&joined)));
385        joined
386    }
387
388    /// Borrow the underlying rope. Hot-path consumers (tree-sitter
389    /// streaming parse, byte-range slicing) should use this instead of
390    /// `content_joined()` to avoid materializing the whole document as
391    /// a `String`.
392    ///
393    /// `ropey::Rope::clone` is O(1) — it Arc-clones the root node.
394    /// The clone gives the caller a snapshot they can read without
395    /// holding the content mutex.
396    pub fn rope(&self) -> ropey::Rope {
397        self.content.lock().unwrap().text.clone()
398    }
399
400    /// Shared access to the content guard. Crate-internal.
401    pub(crate) fn content_lock(&self) -> MutexGuard<'_, Buffer> {
402        self.content.lock().unwrap()
403    }
404
405    /// Exclusive access to Buffer. Crate-internal.
406    pub(crate) fn content_lock_mut(&mut self) -> MutexGuard<'_, Buffer> {
407        self.content.lock().unwrap()
408    }
409
410    // ── Screen-row helpers (private) ──────────────────────────────
411
412    fn cursor_screen_row_from(&self, viewport: &Viewport, top: usize) -> Option<usize> {
413        let cursor = self.cursor;
414        if cursor.row < top {
415            return None;
416        }
417        let c = self.content.lock().unwrap();
418        // Clamp against the live rope: another view sharing this Buffer may
419        // have removed rows since this view's cursor was last clamped, and
420        // `rope.line(r)` panics past the last line.
421        let cursor_row = cursor.row.min(c.text.len_lines().saturating_sub(1));
422        if cursor_row < top {
423            return None;
424        }
425        let v = *viewport;
426        let mut screen = 0usize;
427        for r in top..=cursor_row {
428            if c.folds.iter().any(|f| f.hides(r)) {
429                continue;
430            }
431            let line = rope_line_str(&c.text, r);
432            let segs = crate::wrap::wrap_segments(&line, v.text_width, v.wrap);
433            if r == cursor_row {
434                let seg_idx = crate::wrap::segment_for_col(&segs, cursor.col);
435                return Some(screen + seg_idx);
436            }
437            screen += segs.len();
438        }
439        None
440    }
441
442    // ── Per-buffer engine state accessors ─────────────────────────────────
443
444    // ── Undo arena tree accessors (Phase 2a) ─────────────────────────────
445    //
446    // These delegate to the per-document [`crate::UndoTree`]. Their names and
447    // semantics mirror the two-stack API they replace (`undo_stack` /
448    // `redo_stack`), so the engine's undo/redo drivers are untouched apart
449    // from the two `*_step` moves below.
450
451    pub fn undo_stack_is_empty(&self) -> bool {
452        self.content.lock().unwrap().undo.is_at_root()
453    }
454
455    pub fn redo_stack_is_empty(&self) -> bool {
456        !self.content.lock().unwrap().undo.has_redo()
457    }
458
459    pub fn undo_stack_len(&self) -> usize {
460        self.content.lock().unwrap().undo.depth()
461    }
462
463    /// Commit the pre-edit LIVE state `entry` as an undo boundary: the current
464    /// node takes `entry` and a fresh child becomes current, with the forward
465    /// (redo) branch dropped — the tree form of `undo_stack.push` + `clear_redo`.
466    pub fn push_undo_entry(&self, entry: crate::UndoEntry) {
467        self.content.lock().unwrap().undo.push(entry);
468    }
469
470    /// One undo move. `live` is the current buffer state (rope/cursor/marks) of
471    /// the node being left; returns the parent snapshot to restore, or `None`
472    /// at the root. See [`crate::UndoTree::undo_step`].
473    pub fn undo_step(
474        &self,
475        rope: ropey::Rope,
476        cursor: (usize, usize),
477        marks: crate::MarkSnapshot,
478    ) -> Option<crate::UndoEntry> {
479        self.content
480            .lock()
481            .unwrap()
482            .undo
483            .undo_step(rope, cursor, marks)
484    }
485
486    /// One redo move. Symmetric to [`Self::undo_step`]; returns the child
487    /// snapshot to restore, or `None` when there is no forward branch.
488    pub fn redo_step(
489        &self,
490        rope: ropey::Rope,
491        cursor: (usize, usize),
492        marks: crate::MarkSnapshot,
493    ) -> Option<crate::UndoEntry> {
494        self.content
495            .lock()
496            .unwrap()
497            .undo
498            .redo_step(rope, cursor, marks)
499    }
500
501    /// Discard the most-recent undo boundary without moving the live state
502    /// (`undo_stack.pop()`); `false` at the root. See
503    /// [`crate::UndoTree::pop_committed`].
504    pub fn pop_committed(&self) -> bool {
505        self.content.lock().unwrap().undo.pop_committed()
506    }
507
508    /// One `g-` / `:earlier` step: move to the next-lower-`seq` state tree-wide,
509    /// returning its snapshot to restore, or `None` at the lowest state. `live`
510    /// is the current buffer state, stashed into the node being left. See
511    /// [`crate::UndoTree::seq_earlier_step`].
512    pub fn seq_earlier_step(
513        &self,
514        rope: ropey::Rope,
515        cursor: (usize, usize),
516        marks: crate::MarkSnapshot,
517    ) -> Option<crate::UndoEntry> {
518        self.content
519            .lock()
520            .unwrap()
521            .undo
522            .seq_earlier_step(rope, cursor, marks)
523    }
524
525    /// One `g+` / `:later` step: move to the next-higher-`seq` state tree-wide.
526    /// Symmetric to [`Self::seq_earlier_step`].
527    pub fn seq_later_step(
528        &self,
529        rope: ropey::Rope,
530        cursor: (usize, usize),
531        marks: crate::MarkSnapshot,
532    ) -> Option<crate::UndoEntry> {
533        self.content
534            .lock()
535            .unwrap()
536            .undo
537            .seq_later_step(rope, cursor, marks)
538    }
539
540    /// Timestamp of the next-lower-`seq` state (`:earlier Ns` predicate).
541    pub fn seq_earlier_timestamp(&self) -> Option<std::time::SystemTime> {
542        self.content.lock().unwrap().undo.seq_earlier_timestamp()
543    }
544
545    /// Timestamp of the next-higher-`seq` state (`:later Ns` predicate).
546    pub fn seq_later_timestamp(&self) -> Option<std::time::SystemTime> {
547        self.content.lock().unwrap().undo.seq_later_timestamp()
548    }
549
550    /// Undo-tree leaves for `:undolist`, each `(seq, changes/depth, timestamp,
551    /// is_current)`, sorted by `seq`. See [`crate::UndoTree::leaves`].
552    pub fn undo_leaves(&self) -> Vec<(u64, usize, std::time::SystemTime, bool)> {
553        self.content.lock().unwrap().undo.leaves()
554    }
555
556    pub fn peek_undo_timestamp(&self) -> Option<std::time::SystemTime> {
557        self.content.lock().unwrap().undo.parent_timestamp()
558    }
559
560    pub fn peek_redo_timestamp(&self) -> Option<std::time::SystemTime> {
561        self.content.lock().unwrap().undo.child_timestamp()
562    }
563
564    pub fn clear_undo_redo(&self) {
565        self.content.lock().unwrap().undo.clear_all();
566    }
567
568    // ── Undofile persistence (Phase 3b) ───────────────────────────────────
569
570    /// Project this buffer's undo tree into its serializable form for the
571    /// undofile, plus the current node's `seq`. Syncs the current node to the
572    /// live buffer text first, so the on-disk tree's `current` edge is exact
573    /// even when `current` is a fresh (still-stale) leaf.
574    pub fn undo_to_serializable(&self) -> (crate::SerTree, u64) {
575        let mut c = self.content.lock().unwrap();
576        let rope = c.text.clone();
577        c.undo.sync_current(rope);
578        let seq = c.undo.current_node_seq();
579        (c.undo.to_serializable(), seq)
580    }
581
582    /// Replace this buffer's fresh single-node undo tree with one deserialized
583    /// from an undofile. Must run BEFORE the first user edit and after the
584    /// buffer text is populated (the tree's `current` materializes to the loaded
585    /// content). Returns `false` — leaving the fresh tree untouched — if the
586    /// projection is structurally inconsistent.
587    pub fn install_undo_tree(&self, ser: &crate::SerTree) -> bool {
588        match crate::UndoTree::from_serializable(ser) {
589            Some(tree) => {
590                self.content.lock().unwrap().undo = tree;
591                true
592            }
593            None => false,
594        }
595    }
596
597    /// Install an undo tree recovered from a **swap** file (crash path),
598    /// verifying it against the just-recovered
599    /// buffer text before committing. Unlike [`Self::install_undo_tree`] (the
600    /// undofile / clean-close path, whose caller gates on a content hash), the
601    /// swap tail rides an unsaved buffer, so this re-checks consistency itself.
602    ///
603    /// Returns `false` — leaving the fresh single-node tree seeded from the
604    /// recovered content untouched — when the projection is structurally
605    /// invalid, its current node's `seq` differs from `current_seq`, or its
606    /// current node doesn't materialize to the live buffer text. Must run AFTER
607    /// the recovered content is installed (which resets undo to a single node).
608    /// A trailing-newline difference is normalized away, matching how recovery
609    /// installs `body.strip_suffix('\n')`.
610    pub fn install_recovered_undo_tree(&self, ser: &crate::SerTree, current_seq: u64) -> bool {
611        let Some(mut tree) = crate::UndoTree::from_serializable(ser) else {
612            return false;
613        };
614        if tree.current_node_seq() != current_seq {
615            return false;
616        }
617        let mut c = self.content.lock().unwrap();
618        let materialized = tree.current_content();
619        if materialized.to_string().trim_end_matches('\n')
620            != c.text.to_string().trim_end_matches('\n')
621        {
622            return false;
623        }
624        c.undo = tree;
625        true
626    }
627
628    pub fn clear_redo(&self) {
629        self.content.lock().unwrap().undo.clear_redo();
630    }
631
632    /// Whether an undo group is currently open on this content (depth `> 0`).
633    /// Used by the engine's `push_undo` to decide whether to coalesce.
634    pub fn undo_group_active(&self) -> bool {
635        self.content.lock().unwrap().undo_group_active()
636    }
637
638    /// Arm the open group's single snapshot. See [`Buffer::undo_group_arm`].
639    pub fn undo_group_arm(&self) -> bool {
640        self.content.lock().unwrap().undo_group_arm()
641    }
642
643    pub fn cap_undo(&self, cap: usize) {
644        self.content.lock().unwrap().undo.cap(cap);
645    }
646
647    pub fn content_dirty(&self) -> bool {
648        self.content.lock().unwrap().content_dirty
649    }
650
651    pub fn set_content_dirty(&self, v: bool) {
652        self.content.lock().unwrap().content_dirty = v;
653    }
654
655    pub fn mark_content_dirty(&self) {
656        let mut c = self.content.lock().unwrap();
657        c.content_dirty = true;
658        c.cached_editor_content = None;
659    }
660
661    pub fn take_dirty(&self) -> bool {
662        let mut c = self.content.lock().unwrap();
663        let v = c.content_dirty;
664        c.content_dirty = false;
665        v
666    }
667
668    pub fn cached_editor_content(&self) -> Option<std::sync::Arc<String>> {
669        self.content.lock().unwrap().cached_editor_content.clone()
670    }
671
672    pub fn set_cached_editor_content(&self, arc: std::sync::Arc<String>) {
673        self.content.lock().unwrap().cached_editor_content = Some(arc);
674    }
675
676    pub fn push_fold_op(&self, op: crate::FoldOp) {
677        self.content.lock().unwrap().pending_fold_ops.push(op);
678    }
679
680    pub fn take_fold_ops(&self) -> Vec<crate::FoldOp> {
681        std::mem::take(&mut self.content.lock().unwrap().pending_fold_ops)
682    }
683
684    pub fn extend_change_log(&self, edits: impl IntoIterator<Item = crate::EngineEdit>) {
685        self.content.lock().unwrap().change_log.extend(edits);
686    }
687
688    pub fn take_change_log(&self) -> Vec<crate::EngineEdit> {
689        std::mem::take(&mut self.content.lock().unwrap().change_log)
690    }
691
692    pub fn extend_pending_content_edits(
693        &self,
694        edits: impl IntoIterator<Item = crate::ContentEdit>,
695    ) {
696        self.content
697            .lock()
698            .unwrap()
699            .pending_content_edits
700            .extend(edits);
701    }
702
703    pub fn push_pending_content_edit(&self, edit: crate::ContentEdit) {
704        self.content
705            .lock()
706            .unwrap()
707            .pending_content_edits
708            .push(edit);
709    }
710
711    pub fn take_pending_content_edits(&self) -> Vec<crate::ContentEdit> {
712        std::mem::take(&mut self.content.lock().unwrap().pending_content_edits)
713    }
714
715    pub fn clear_pending_content_edits(&self) {
716        self.content.lock().unwrap().pending_content_edits.clear();
717    }
718
719    pub fn pending_content_reset(&self) -> bool {
720        self.content.lock().unwrap().pending_content_reset
721    }
722
723    pub fn set_pending_content_reset(&self, v: bool) {
724        self.content.lock().unwrap().pending_content_reset = v;
725    }
726
727    pub fn take_pending_content_reset(&self) -> bool {
728        let mut c = self.content.lock().unwrap();
729        let v = c.pending_content_reset;
730        c.pending_content_reset = false;
731        v
732    }
733
734    pub fn mark(&self, c: char) -> Option<(usize, usize)> {
735        self.content_lock().marks.get(&c).copied()
736    }
737    pub fn set_mark(&mut self, c: char, pos: (usize, usize)) {
738        self.content_lock_mut().marks.insert(c, pos);
739    }
740    pub fn clear_mark(&mut self, c: char) {
741        self.content_lock_mut().marks.remove(&c);
742    }
743    pub fn marks_cloned(&self) -> std::collections::BTreeMap<char, (usize, usize)> {
744        self.content_lock().marks.clone()
745    }
746    pub fn set_marks(&mut self, marks: std::collections::BTreeMap<char, (usize, usize)>) {
747        self.content_lock_mut().marks = marks;
748    }
749    /// Drop marks inside `[edit_start, drop_end)` and shift marks at/after
750    /// `shift_threshold` by `delta` rows (clamped to 0). Mirrors the engine's
751    /// edit-coherence pass for the per-buffer mark map (#154).
752    pub fn rebase_marks(
753        &mut self,
754        edit_start: usize,
755        drop_end: usize,
756        shift_threshold: usize,
757        delta: isize,
758    ) {
759        let mut c = self.content_lock_mut();
760        let mut to_drop: Vec<char> = Vec::new();
761        for (ch, (row, _col)) in c.marks.iter_mut() {
762            if (edit_start..drop_end).contains(row) {
763                to_drop.push(*ch);
764            } else if *row >= shift_threshold {
765                *row = ((*row as isize) + delta).max(0) as usize;
766            }
767        }
768        for ch in to_drop {
769            c.marks.remove(&ch);
770        }
771    }
772    pub fn syntax_fold_ranges_cloned(&self) -> Vec<(usize, usize)> {
773        self.content_lock().syntax_fold_ranges.clone()
774    }
775    pub fn set_syntax_fold_ranges(&mut self, ranges: Vec<(usize, usize)>) {
776        self.content_lock_mut().syntax_fold_ranges = ranges;
777    }
778}
779
780// ── Rope line helpers (free functions over &ropey::Rope) ─────────────
781
782/// Largest char-boundary byte index `<= byte_idx` (clamped to rope length).
783///
784/// Byte offsets that arrive from outside the rope — LSP positions clamped to
785/// `len_bytes`, a stale tree-sitter node range — can land in the middle of a
786/// multi-byte char (e.g. a 4-byte emoji whose last byte sits at `N-1` while
787/// the clamp produced `N-3`). `ropey::Rope::byte_slice` panics on a
788/// non-aligned index, so callers must floor first. Uses ropey's own
789/// conversion, which is safe for any byte value `<= len_bytes`:
790/// `byte_to_char` returns the index of the char *containing* a non-boundary
791/// byte. For an already char-aligned, in-bounds index this is the identity.
792pub fn floor_char_boundary(rope: &ropey::Rope, byte_idx: usize) -> usize {
793    let byte_idx = byte_idx.min(rope.len_bytes());
794    rope.char_to_byte(rope.byte_to_char(byte_idx))
795}
796
797/// Return logical line `row` as a `String`, stripping the trailing `\n`
798/// that ropey includes for non-final lines.
799pub fn rope_line_str(rope: &ropey::Rope, row: usize) -> String {
800    let mut s = rope.line(row).to_string();
801    // ropey includes the trailing '\n' for non-final lines; strip it.
802    if s.ends_with('\n') {
803        s.pop();
804    }
805    s
806}
807
808/// Byte length of logical line `row` (excluding the trailing `\n`).
809pub fn rope_line_bytes(rope: &ropey::Rope, row: usize) -> usize {
810    let slice = rope.line(row);
811    let bytes = slice.len_bytes();
812    // ropey includes the '\n' byte for non-final lines; subtract it.
813    if row + 1 < rope.len_lines() && bytes > 0 {
814        bytes - 1
815    } else {
816        bytes
817    }
818}
819
820/// Char count of logical line `row` (excluding the trailing `\n`).
821pub fn rope_line_char_count(rope: &ropey::Rope, row: usize) -> usize {
822    let slice = rope.line(row);
823    let chars = slice.len_chars();
824    // ropey includes the '\n' char for non-final lines; subtract it.
825    if row + 1 < rope.len_lines() && chars > 0 {
826        chars - 1
827    } else {
828        chars
829    }
830}
831
832/// Char index from `(row, col)` where `col` is a char index within the line.
833/// Both coordinates are clamped to the rope's bounds so a position that went
834/// stale (e.g. another view shrank the shared `Buffer` between the caller's
835/// clamp and this call) can never panic `line_to_char`.
836pub fn pos_to_char_idx(rope: &ropey::Rope, row: usize, col: usize) -> usize {
837    let row = row.min(rope.len_lines().saturating_sub(1));
838    let line_start = rope.line_to_char(row);
839    let line_char_count = rope_line_char_count(rope, row);
840    line_start + col.min(line_char_count)
841}
842
843#[cfg(test)]
844mod tests {
845    use super::*;
846
847    #[test]
848    fn new_has_one_empty_row() {
849        let b = View::new();
850        assert_eq!(b.row_count(), 1);
851        assert_eq!(rope_line_str(&b.rope(), 0), "");
852        assert_eq!(b.cursor(), Position::default());
853    }
854
855    #[test]
856    fn from_str_splits_on_newline() {
857        let b = View::from_str("foo\nbar\nbaz");
858        assert_eq!(b.row_count(), 3);
859        assert_eq!(rope_line_str(&b.rope(), 0), "foo");
860        assert_eq!(rope_line_str(&b.rope(), 2), "baz");
861    }
862
863    #[test]
864    fn from_str_trailing_newline_keeps_empty_row() {
865        let b = View::from_str("foo\n");
866        assert_eq!(b.row_count(), 2);
867        assert_eq!(rope_line_str(&b.rope(), 1), "");
868    }
869
870    #[test]
871    fn from_str_empty_input_keeps_one_row() {
872        let b = View::from_str("");
873        assert_eq!(b.row_count(), 1);
874        assert_eq!(rope_line_str(&b.rope(), 0), "");
875    }
876
877    #[test]
878    fn as_string_round_trips() {
879        let b = View::from_str("a\nb\nc");
880        assert_eq!(b.as_string(), "a\nb\nc");
881    }
882
883    #[test]
884    fn dirty_gen_starts_at_zero() {
885        assert_eq!(View::new().dirty_gen(), 0);
886    }
887
888    /// A minimal single-node [`crate::SerTree`] whose root materializes to
889    /// `base`, tagged with `seq`.
890    fn single_node_tree(base: &str, seq: u64) -> crate::SerTree {
891        crate::SerTree {
892            base: base.to_string(),
893            nodes: vec![crate::SerNode {
894                parent: None,
895                children: Vec::new(),
896                last_child: None,
897                delta: None,
898                cursor: (0, 0),
899                timestamp_unix_ms: 0,
900                marks: crate::MarkSnapshot::default(),
901                seq,
902            }],
903            root: 0,
904            current: 0,
905            next_seq: seq + 1,
906        }
907    }
908
909    /// `install_recovered_undo_tree` installs a tree whose current node matches
910    /// the live buffer content and whose `seq` matches — and rejects (leaves the
911    /// fresh tree) on a `seq` or content mismatch (the swap consistency guard).
912    #[test]
913    fn install_recovered_undo_tree_guards_seq_and_content() {
914        // Match: content + seq agree → installs.
915        let v = View::from_str("alpha\nhello");
916        assert!(v.install_recovered_undo_tree(&single_node_tree("alpha\nhello", 3), 3));
917
918        // Seq mismatch → rejected.
919        let v = View::from_str("alpha\nhello");
920        assert!(!v.install_recovered_undo_tree(&single_node_tree("alpha\nhello", 3), 4));
921
922        // Content mismatch → rejected.
923        let v = View::from_str("alpha\nhello");
924        assert!(!v.install_recovered_undo_tree(&single_node_tree("something else", 3), 3));
925
926        // A trailing-newline-only difference is normalized away → still installs.
927        let v = View::from_str("alpha\nhello");
928        assert!(v.install_recovered_undo_tree(&single_node_tree("alpha\nhello\n", 3), 3));
929    }
930
931    fn vp_wrap(width: u16, height: u16) -> Viewport {
932        Viewport {
933            top_row: 0,
934            top_col: 0,
935            width,
936            height,
937            wrap: crate::Wrap::Char,
938            text_width: width,
939            tab_width: 0,
940        }
941    }
942
943    #[test]
944    fn ensure_cursor_visible_wrap_scrolls_when_cursor_below_screen() {
945        let mut b = View::from_str("aaaaaaaaaa\nb\nc");
946        let mut v = vp_wrap(4, 3);
947        b.set_cursor(Position::new(2, 0));
948        b.ensure_cursor_visible(&mut v);
949        assert_eq!(v.top_row, 1);
950    }
951
952    #[test]
953    fn ensure_cursor_visible_wrap_no_scroll_when_visible() {
954        let mut b = View::from_str("aaaaaaaaaa\nb");
955        let mut v = vp_wrap(4, 4);
956        b.set_cursor(Position::new(0, 5));
957        b.ensure_cursor_visible(&mut v);
958        assert_eq!(v.top_row, 0);
959    }
960
961    #[test]
962    fn ensure_cursor_visible_wrap_snaps_top_when_cursor_above() {
963        let mut b = View::from_str("a\nb\nc\nd\ne");
964        let mut v = vp_wrap(4, 2);
965        v.top_row = 3;
966        b.set_cursor(Position::new(1, 0));
967        b.ensure_cursor_visible(&mut v);
968        assert_eq!(v.top_row, 1);
969    }
970
971    #[test]
972    fn screen_rows_between_sums_segments_under_wrap() {
973        let b = View::from_str("aaaaaaaaa\nb\n");
974        let v = vp_wrap(4, 0);
975        assert_eq!(b.screen_rows_between(&v, 0, 0), 3);
976        assert_eq!(b.screen_rows_between(&v, 0, 1), 4);
977        assert_eq!(b.screen_rows_between(&v, 0, 2), 5);
978        assert_eq!(b.screen_rows_between(&v, 1, 2), 2);
979    }
980
981    #[test]
982    fn screen_rows_between_one_per_doc_row_when_wrap_off() {
983        let b = View::from_str("aaaaa\nb\nc");
984        let v = Viewport::default();
985        assert_eq!(b.screen_rows_between(&v, 0, 2), 3);
986    }
987
988    #[test]
989    fn max_top_for_height_walks_back_until_height_reached() {
990        let b = View::from_str("a\nb\nc\nd\neeeeeeee");
991        let v = vp_wrap(4, 0);
992        assert_eq!(b.max_top_for_height(&v, 4), 2);
993        assert_eq!(b.max_top_for_height(&v, 99), 0);
994    }
995
996    #[test]
997    fn cursor_screen_row_returns_none_when_wrap_off() {
998        let b = View::from_str("a");
999        let v = Viewport::default();
1000        assert!(b.cursor_screen_row(&v).is_none());
1001    }
1002
1003    #[test]
1004    fn cursor_screen_row_under_wrap() {
1005        let mut b = View::from_str("aaaaaaaaaa\nb");
1006        let v = vp_wrap(4, 0);
1007        b.set_cursor(Position::new(0, 5));
1008        assert_eq!(b.cursor_screen_row(&v), Some(1));
1009        b.set_cursor(Position::new(1, 0));
1010        assert_eq!(b.cursor_screen_row(&v), Some(3));
1011    }
1012
1013    /// Regression: a view whose cursor went stale after another view shrank
1014    /// the shared Buffer used to panic `rope.line()` inside
1015    /// `cursor_screen_row_from`. The row must clamp to the live rope.
1016    #[test]
1017    fn cursor_screen_row_survives_shrink_from_other_view() {
1018        let a = View::from_str("a\nb\nc\nd\ne");
1019        let arc = a.content_arc();
1020        let mut view_a = View::new_view(Arc::clone(&arc));
1021        let mut view_b = View::new_view(Arc::clone(&arc));
1022        view_b.set_cursor(Position::new(4, 0));
1023        // view_a truncates the document; view_b's cursor row 4 is now stale.
1024        view_a.replace_all("a");
1025        let v = vp_wrap(4, 3);
1026        assert_eq!(view_b.cursor_screen_row(&v), Some(0));
1027        let mut v2 = vp_wrap(4, 3);
1028        view_b.ensure_cursor_visible(&mut v2); // must not panic
1029    }
1030
1031    /// Regression: after another view shrank the shared Buffer, a `top_row`
1032    /// left past the rope's end must be pulled *back* to the clamped cursor
1033    /// row. Assigning the raw stale `cursor.row` would push `top_row` further
1034    /// past the end, leaving the cursor off-screen.
1035    #[test]
1036    fn ensure_cursor_visible_clamps_stale_top_row_after_shrink() {
1037        let a = View::from_str("a\nb\nc\nd\ne");
1038        let arc = a.content_arc();
1039        let mut view_a = View::new_view(Arc::clone(&arc));
1040        let mut view_b = View::new_view(Arc::clone(&arc));
1041        view_b.set_cursor(Position::new(4, 0));
1042        view_a.replace_all("a");
1043        let mut v = vp_wrap(4, 3);
1044        // Stale scroll position: below the cursor's clamped row, above the
1045        // stale cursor row, and past the (now single-line) rope.
1046        v.top_row = 3;
1047        view_b.ensure_cursor_visible(&mut v);
1048        assert_eq!(v.top_row, 0, "top_row must clamp into the live rope");
1049        assert_eq!(v.top_col, 0);
1050    }
1051
1052    /// The other `cursor_screen_row_from` `None` path: the cursor's row is
1053    /// hidden inside a closed fold, so the walk never reaches it. `top_row`
1054    /// still snaps to the cursor row (in-range, so no clamping applies).
1055    #[test]
1056    fn ensure_cursor_visible_snaps_when_cursor_row_folded() {
1057        let mut b = View::from_str("a\nb\nc\nd\ne");
1058        b.set_folds(&[crate::Fold {
1059            start_row: 2,
1060            end_row: 4,
1061            closed: true,
1062            auto_generated: false,
1063        }]);
1064        let mut v = vp_wrap(4, 2);
1065        v.top_row = 1;
1066        b.set_cursor(Position::new(3, 0));
1067        b.ensure_cursor_visible(&mut v);
1068        assert_eq!(v.top_row, 3);
1069    }
1070
1071    #[test]
1072    fn ensure_cursor_visible_falls_back_when_wrap_disabled() {
1073        let mut b = View::from_str("a\nb\nc\nd\ne");
1074        let mut v = Viewport {
1075            top_row: 0,
1076            top_col: 0,
1077            width: 4,
1078            height: 2,
1079            wrap: crate::Wrap::None,
1080            text_width: 4,
1081            tab_width: 0,
1082        };
1083        b.set_cursor(Position::new(4, 0));
1084        b.ensure_cursor_visible(&mut v);
1085        assert_eq!(v.top_row, 3);
1086    }
1087
1088    // ── Per-buffer engine state tests (new in 0.33.0 / Phase B) ──────
1089
1090    /// Undo entries pushed via one `View` view are visible via
1091    /// another view sharing the same `Buffer` — proving that the
1092    /// undo stack lives on `Buffer`, not on the per-window `View`.
1093    #[test]
1094    fn undo_stack_shared_across_views() {
1095        use crate::UndoEntry;
1096        use std::time::SystemTime;
1097
1098        let a = View::from_str("hello");
1099        let arc = a.content_arc();
1100        let view_a = View::new_view(Arc::clone(&arc));
1101        let view_b = View::new_view(Arc::clone(&arc));
1102
1103        assert!(view_a.undo_stack_is_empty());
1104        assert_eq!(view_a.undo_stack_len(), 0);
1105
1106        view_a.push_undo_entry(UndoEntry {
1107            rope: view_a.rope(),
1108            cursor: (0, 0),
1109            timestamp: SystemTime::UNIX_EPOCH,
1110            marks: Default::default(),
1111        });
1112
1113        // Push via view_a is visible via view_b.
1114        assert_eq!(view_b.undo_stack_len(), 1);
1115        assert!(!view_b.undo_stack_is_empty());
1116    }
1117
1118    /// A redo branch created via one view is visible via another — the undo
1119    /// tree lives on the shared `Buffer`, not the per-window `View`. (Phase 2a:
1120    /// re-expressed against the arena-tree API — a redo entry is now a forward
1121    /// child left behind by an undo move, not a `push_redo_entry` onto a Vec.)
1122    #[test]
1123    fn redo_stack_shared_across_views() {
1124        use crate::UndoEntry;
1125        use std::time::SystemTime;
1126
1127        let a = View::from_str("world");
1128        let arc = a.content_arc();
1129        let view_a = View::new_view(Arc::clone(&arc));
1130        let view_b = View::new_view(Arc::clone(&arc));
1131
1132        assert!(view_a.redo_stack_is_empty());
1133
1134        // Commit an undo boundary via view_b, then undo it — that leaves the
1135        // node we left (cursor (0, 2)) as a forward/redo branch on the shared
1136        // Content.
1137        view_b.push_undo_entry(UndoEntry {
1138            rope: view_b.rope(),
1139            cursor: (0, 0),
1140            timestamp: SystemTime::UNIX_EPOCH,
1141            marks: Default::default(),
1142        });
1143        view_b.undo_step(view_b.rope(), (0, 2), Default::default());
1144
1145        // The redo branch is visible + walkable via view_a.
1146        assert!(!view_a.redo_stack_is_empty());
1147        let entry = view_a.redo_step(view_a.rope(), (0, 0), Default::default());
1148        assert!(entry.is_some());
1149        assert_eq!(entry.unwrap().cursor, (0, 2));
1150    }
1151
1152    /// `clear_undo_redo` collapses the shared undo tree to a single node, wiping
1153    /// both directions, and the effect is visible from every view. (Phase 2a:
1154    /// the redo side is now seeded by an undo move rather than a raw
1155    /// `push_redo_entry`.)
1156    #[test]
1157    fn clear_undo_redo_shared_across_views() {
1158        use crate::UndoEntry;
1159        use std::time::SystemTime;
1160
1161        let a = View::from_str("abc");
1162        let arc = a.content_arc();
1163        let view_a = View::new_view(Arc::clone(&arc));
1164        let view_b = View::new_view(Arc::clone(&arc));
1165
1166        // Two boundaries then one undo → undo side AND redo side both populated.
1167        for _ in 0..2 {
1168            view_a.push_undo_entry(UndoEntry {
1169                rope: view_a.rope(),
1170                cursor: (0, 0),
1171                timestamp: SystemTime::UNIX_EPOCH,
1172                marks: Default::default(),
1173            });
1174        }
1175        view_a.undo_step(view_a.rope(), (0, 1), Default::default());
1176        assert!(!view_a.undo_stack_is_empty());
1177        assert!(!view_a.redo_stack_is_empty());
1178
1179        view_b.clear_undo_redo();
1180        assert!(view_a.undo_stack_is_empty());
1181        assert!(view_a.redo_stack_is_empty());
1182    }
1183
1184    /// `content_dirty` flag is shared across views.
1185    #[test]
1186    fn content_dirty_shared_across_views() {
1187        let a = View::from_str("test");
1188        let arc = a.content_arc();
1189        let view_a = View::new_view(Arc::clone(&arc));
1190        let view_b = View::new_view(Arc::clone(&arc));
1191
1192        assert!(!view_a.content_dirty());
1193
1194        view_b.mark_content_dirty();
1195        assert!(view_a.content_dirty());
1196
1197        let taken = view_a.take_dirty();
1198        assert!(taken);
1199        assert!(!view_b.content_dirty());
1200    }
1201
1202    /// `pending_fold_ops` push and take are shared across views.
1203    #[test]
1204    fn pending_fold_ops_shared_across_views() {
1205        let a = View::from_str("a\nb\nc");
1206        let arc = a.content_arc();
1207        let view_a = View::new_view(Arc::clone(&arc));
1208        let view_b = View::new_view(Arc::clone(&arc));
1209
1210        view_a.push_fold_op(crate::FoldOp::Add {
1211            start_row: 0,
1212            end_row: 1,
1213            closed: true,
1214        });
1215
1216        let ops = view_b.take_fold_ops();
1217        assert_eq!(ops.len(), 1);
1218        assert!(matches!(
1219            ops[0],
1220            crate::FoldOp::Add {
1221                start_row: 0,
1222                end_row: 1,
1223                closed: true
1224            }
1225        ));
1226    }
1227
1228    /// `pending_content_reset` flag is shared across views.
1229    #[test]
1230    fn pending_content_reset_shared_across_views() {
1231        let a = View::from_str("x");
1232        let arc = a.content_arc();
1233        let view_a = View::new_view(Arc::clone(&arc));
1234        let view_b = View::new_view(Arc::clone(&arc));
1235
1236        assert!(!view_a.pending_content_reset());
1237        view_b.set_pending_content_reset(true);
1238        assert!(view_a.pending_content_reset());
1239        let taken = view_a.take_pending_content_reset();
1240        assert!(taken);
1241        assert!(!view_b.pending_content_reset());
1242    }
1243
1244    // ── View-split tests (new in 0.8.0) ──────────────────────────
1245
1246    /// Two `View` views sharing one `Buffer` must have independent
1247    /// cursors.
1248    #[test]
1249    fn buffer_views_independent_cursors() {
1250        let a = View::from_str("hello\nworld");
1251        let arc = a.content_arc();
1252        let mut view_a = View::new_view(Arc::clone(&arc));
1253        let mut view_b = View::new_view(Arc::clone(&arc));
1254
1255        view_a.set_cursor(Position::new(1, 3));
1256        // view_b cursor must remain at (0, 0).
1257        assert_eq!(view_b.cursor(), Position::new(0, 0));
1258
1259        view_b.set_cursor(Position::new(0, 2));
1260        // view_a cursor must remain at (1, 3).
1261        assert_eq!(view_a.cursor(), Position::new(1, 3));
1262    }
1263
1264    /// `last_cursor` on the shared `Buffer` reflects the most recent move
1265    /// across two independent `View`s — the "last-moved window wins" contract
1266    /// the cross-session cursor store depends on (docs §6b).
1267    #[test]
1268    fn last_cursor_reflects_most_recent_move_across_views() {
1269        let a = View::from_str("aaaa\nbbbb\ncccc\ndddd");
1270        let arc = a.content_arc();
1271        let mut view_a = View::new_view(Arc::clone(&arc));
1272        let mut view_b = View::new_view(Arc::clone(&arc));
1273
1274        view_a.set_cursor(Position::new(1, 2));
1275        assert_eq!(view_a.last_cursor(), (1, 2));
1276        // Both views see the same shared last_cursor.
1277        assert_eq!(view_b.last_cursor(), (1, 2));
1278
1279        // A later move on view_b wins.
1280        view_b.set_cursor(Position::new(3, 1));
1281        assert_eq!(view_a.last_cursor(), (3, 1));
1282        assert_eq!(view_b.last_cursor(), (3, 1));
1283
1284        // last_cursor stores the CLAMPED landing position, not the request.
1285        view_a.set_cursor(Position::new(99, 99));
1286        assert_eq!(view_a.last_cursor(), (3, 4));
1287    }
1288
1289    /// Cursor-restore clamp contract (docs §6b): a stored row past EOF clamps
1290    /// to the last line, and a col past the line's char count clamps to its
1291    /// length. `clamp_position` is what `build_slot` runs on the stored cursor.
1292    #[test]
1293    fn clamp_position_restore_semantics() {
1294        let b = View::from_str("ab\ncdef\ng");
1295        // Row past the last line (2) → clamped to last line, col to its length.
1296        assert_eq!(b.clamp_position(Position::new(99, 99)), Position::new(2, 1));
1297        // Col past the line's char count → clamped to the char count (4).
1298        assert_eq!(b.clamp_position(Position::new(1, 99)), Position::new(1, 4));
1299        // In-bounds position is unchanged (exact restore on a content match).
1300        assert_eq!(b.clamp_position(Position::new(1, 2)), Position::new(1, 2));
1301    }
1302
1303    /// An edit applied via one view must be visible via the other.
1304    #[test]
1305    fn buffer_views_share_content() {
1306        use crate::edit::Edit;
1307
1308        let a = View::from_str("foo");
1309        let arc = a.content_arc();
1310        let mut view_a = View::new_view(Arc::clone(&arc));
1311        let view_b = View::new_view(Arc::clone(&arc));
1312
1313        view_a.apply_edit(Edit::InsertStr {
1314            at: Position::new(0, 3),
1315            text: "bar".into(),
1316        });
1317
1318        assert_eq!(rope_line_str(&view_a.rope(), 0), "foobar");
1319        assert_eq!(rope_line_str(&view_b.rope(), 0), "foobar");
1320    }
1321}
1322
1323#[cfg(test)]
1324mod marks_shared_content_tests {
1325    use super::*;
1326
1327    #[test]
1328    fn marks_shared_across_views() {
1329        // Two View views on the same Buffer share marks (#154).
1330        let a = View::from_str("hello\nworld");
1331        let content = a.content_arc();
1332        let mut view_a = View::new_view(std::sync::Arc::clone(&content));
1333        let view_b = View::new_view(std::sync::Arc::clone(&content));
1334
1335        // Set mark 'x' on view_a.
1336        view_a.set_mark('x', (1, 3));
1337
1338        // view_b must see the same mark via shared Buffer.
1339        assert_eq!(view_b.mark('x'), Some((1, 3)));
1340    }
1341
1342    #[test]
1343    fn syntax_fold_ranges_shared_across_views() {
1344        let a = View::from_str("fn foo() {\n  bar();\n}");
1345        let content = a.content_arc();
1346        let mut view_a = View::new_view(std::sync::Arc::clone(&content));
1347        let view_b = View::new_view(std::sync::Arc::clone(&content));
1348
1349        view_a.set_syntax_fold_ranges(vec![(0, 2)]);
1350
1351        assert_eq!(view_b.syntax_fold_ranges_cloned(), vec![(0, 2)]);
1352    }
1353}