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