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