Skip to main content

hjkl_buffer/
buffer.rs

1use std::sync::{Arc, Mutex, MutexGuard};
2
3use crate::content::Content;
4use crate::{Position, Viewport};
5
6/// Per-window view onto a [`Content`].
7///
8/// `Buffer` is the type the rest of `hjkl-buffer` — and all consumers —
9/// use directly. It owns exactly the state that is local to one editor
10/// window:
11///
12/// - `cursor` — the charwise caret for this window.
13///
14/// All document-level state (text rows, dirty generation, folds) lives on
15/// the inner [`Content`] and is accessed via `Arc<Mutex<Content>>`.
16/// Two `Buffer` instances that share the same `Arc` share text + folds
17/// but carry independent cursors — the Helix Document+View model.
18///
19/// ## `Send` + `Sync`
20///
21/// `Arc<Mutex<Content>>` is `Send + Sync`, so `Buffer` remains `Send`.
22/// The engine trait surface requires `Buffer: Send`; this constraint
23/// drove the choice of `Mutex` over `RefCell`. The mutex is never
24/// contended in normal operation (single-threaded app loop), so the
25/// lock cost is negligible (~5 ns uncontested).
26///
27/// ## 0.8.0 migration notes
28///
29/// The existing constructors ([`Buffer::new`], [`Buffer::from_str`],
30/// [`Buffer::replace_all`], etc.) keep the same external signatures.
31/// Callers that do not need multi-window sharing see no behaviour change.
32/// Use [`Buffer::new_view`] to create a second window onto the same
33/// [`Content`].
34///
35/// ## Viewport
36///
37/// The `lines` invariant — at least one entry, never empty — is
38/// preserved by every mutation. The viewport itself (top_row, top_col,
39/// width, height, wrap, text_width) lives on the engine `Host` adapter;
40/// methods that need it take a `&Viewport` / `&mut Viewport` parameter
41/// so the rope-walking math stays here while runtime state lives there.
42pub struct Buffer {
43    /// Shared per-document state (text, dirty gen, folds).
44    pub(crate) content: Arc<Mutex<Content>>,
45    /// Charwise cursor. `col` is bound by `lines[row].chars().count()`
46    /// in normal mode, one past it in operator-pending / insert.
47    cursor: Position,
48}
49
50impl Default for Buffer {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl Buffer {
57    // ── Constructors ──────────────────────────────────────────────
58
59    /// Construct an empty buffer with one empty row + cursor at `(0, 0)`.
60    pub fn new() -> Self {
61        Self {
62            content: Arc::new(Mutex::new(Content::new())),
63            cursor: Position::default(),
64        }
65    }
66
67    /// Build a buffer from a flat string. Splits on `\n`; a trailing
68    /// `\n` produces a trailing empty line (matches every text
69    /// editor's behaviour and keeps `from_text(buf.as_string())` an
70    /// identity round-trip in the common case).
71    #[allow(clippy::should_implement_trait)]
72    pub fn from_str(text: &str) -> Self {
73        Self {
74            content: Arc::new(Mutex::new(Content::from_str(text))),
75            cursor: Position::default(),
76        }
77    }
78
79    /// Create a second per-window view onto existing [`Content`].
80    ///
81    /// The new `Buffer` shares text + folds with every other view on the
82    /// same `Arc`. Its cursor starts at `(0, 0)` independently. This is
83    /// the primary entry point for split-window features.
84    ///
85    /// ```rust
86    /// # use hjkl_buffer::{Buffer, Content, Position};
87    /// # use std::sync::Arc;
88    /// # use std::sync::Mutex;
89    /// let a = Buffer::from_str("hello\nworld");
90    /// let content = a.content_arc();
91    /// let mut b = Buffer::new_view(Arc::clone(&content));
92    ///
93    /// // Cursors are independent.
94    /// let mut a = Buffer::new_view(Arc::clone(&content));
95    /// a.set_cursor(Position::new(1, 0));
96    /// assert_eq!(b.cursor(), Position::new(0, 0));
97    /// ```
98    pub fn new_view(content: Arc<Mutex<Content>>) -> Self {
99        Self {
100            content,
101            cursor: Position::default(),
102        }
103    }
104
105    /// Return a clone of the `Arc<Mutex<Content>>` so callers can
106    /// create additional views with [`Buffer::new_view`].
107    pub fn content_arc(&self) -> Arc<Mutex<Content>> {
108        Arc::clone(&self.content)
109    }
110
111    // ── Read-only accessors (delegate to Content) ─────────────────
112
113    pub fn lines(&self) -> &[String] {
114        // SAFETY: We extend the lifetime of the slice to &self.
115        //
116        // This is sound because:
117        //   1. The caller holds &self, which prevents any &mut self method
118        //      from running while this reference is live (Rust borrow rules).
119        //   2. No &mut self method can mutate the Vec (or reallocate it)
120        //      while &self is held — that would require &mut self.
121        //   3. The Mutex is uncontested in this single-threaded call path;
122        //      we lock, grab the ptr+len, then immediately unlock. No thread
123        //      can enter an &mut self method while the caller holds &self.
124        //   4. Content is kept alive by the Arc while Buffer is alive.
125        //
126        // The ptr+len snapshot is taken under two separate lock() calls.
127        // The len cannot change between them because &self prevents mutation.
128        let c = self.content.lock().unwrap();
129        let ptr = c.lines.as_ptr();
130        let len = c.lines.len();
131        drop(c);
132        // SAFETY: ptr and len are valid; Vec cannot be reallocated while
133        // &self is held (no &mut self can run concurrently).
134        unsafe { std::slice::from_raw_parts(ptr, len) }
135    }
136
137    pub fn line(&self, row: usize) -> Option<&str> {
138        // SAFETY: same reasoning as lines(). Caller holds &self.
139        let c = self.content.lock().unwrap();
140        let s_ptr = c.lines.get(row)?.as_str() as *const str;
141        drop(c);
142        // SAFETY: the String is inside the Arc<Mutex<Content>>; the Arc
143        // keeps it alive and &self prevents deallocation.
144        Some(unsafe { &*s_ptr })
145    }
146
147    pub fn cursor(&self) -> Position {
148        self.cursor
149    }
150
151    pub fn dirty_gen(&self) -> u64 {
152        self.content.lock().unwrap().dirty_gen
153    }
154
155    /// Number of rows in the buffer. Always `>= 1`.
156    pub fn row_count(&self) -> usize {
157        self.content.lock().unwrap().lines.len()
158    }
159
160    /// Concatenate the rows into a single `String` joined by `\n`.
161    pub fn as_string(&self) -> String {
162        self.content.lock().unwrap().lines.join("\n")
163    }
164
165    // ── Cursor ops ────────────────────────────────────────────────
166
167    /// Set cursor without scrolling. Clamps to valid positions.
168    ///
169    /// The optional sticky column for `j`/`k` motions is **not** reset
170    /// by this call — it survives `set_cursor` intentionally.
171    pub fn set_cursor(&mut self, pos: Position) {
172        let c = self.content.lock().unwrap();
173        let last_row = c.lines.len().saturating_sub(1);
174        let row = pos.row.min(last_row);
175        let line_chars = c.lines[row].chars().count();
176        let col = pos.col.min(line_chars);
177        drop(c);
178        self.cursor = Position::new(row, col);
179    }
180
181    /// Bring the cursor into the visible [`Viewport`], scrolling by the
182    /// minimum amount needed.
183    pub fn ensure_cursor_visible(&mut self, viewport: &mut Viewport) {
184        let cursor = self.cursor;
185        let v = *viewport;
186        let wrap_active = !matches!(v.wrap, crate::Wrap::None) && v.text_width > 0;
187        if !wrap_active {
188            viewport.ensure_visible(cursor);
189            return;
190        }
191        if v.height == 0 {
192            return;
193        }
194        // Cursor above the visible region: snap top_row to it.
195        if cursor.row < v.top_row {
196            viewport.top_row = cursor.row;
197            viewport.top_col = 0;
198            return;
199        }
200        let height = v.height as usize;
201        // Push top_row forward until cursor lands inside [0, height).
202        loop {
203            let csr = self.cursor_screen_row_from(viewport, viewport.top_row);
204            match csr {
205                Some(row) if row < height => break,
206                _ => {}
207            }
208            let next = {
209                let c = self.content.lock().unwrap();
210                let mut n = viewport.top_row + 1;
211                while n <= cursor.row && c.folds.iter().any(|f| f.hides(n)) {
212                    n += 1;
213                }
214                n
215            };
216            if next > cursor.row {
217                viewport.top_row = cursor.row;
218                break;
219            }
220            viewport.top_row = next;
221        }
222        viewport.top_col = 0;
223    }
224
225    /// Cursor's screen row offset (0-based) from `viewport.top_row`.
226    pub fn cursor_screen_row(&self, viewport: &Viewport) -> Option<usize> {
227        if matches!(viewport.wrap, crate::Wrap::None) || viewport.text_width == 0 {
228            return None;
229        }
230        self.cursor_screen_row_from(viewport, viewport.top_row)
231    }
232
233    /// Number of screen rows the doc range `start..=end` occupies.
234    pub fn screen_rows_between(&self, viewport: &Viewport, start: usize, end: usize) -> usize {
235        if start > end {
236            return 0;
237        }
238        let c = self.content.lock().unwrap();
239        let last = c.lines.len().saturating_sub(1);
240        let end = end.min(last);
241        let v = *viewport;
242        let mut total = 0usize;
243        for r in start..=end {
244            if c.folds.iter().any(|f| f.hides(r)) {
245                continue;
246            }
247            if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
248                total += 1;
249            } else {
250                let line = c.lines.get(r).map(String::as_str).unwrap_or("");
251                total += crate::wrap::wrap_segments(line, v.text_width, v.wrap).len();
252            }
253        }
254        total
255    }
256
257    /// Earliest `top_row` such that `screen_rows_between(top, last)`
258    /// is at least `height`.
259    pub fn max_top_for_height(&self, viewport: &Viewport, height: usize) -> usize {
260        if height == 0 {
261            return 0;
262        }
263        let c = self.content.lock().unwrap();
264        let last = c.lines.len().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 = c.lines.get(row).map(String::as_str).unwrap_or("");
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 last_row = c.lines.len().saturating_sub(1);
291        let row = pos.row.min(last_row);
292        let line_chars = c.lines[row].chars().count();
293        let col = pos.col.min(line_chars);
294        Position::new(row, col)
295    }
296
297    /// Replace the buffer's full text in place. Cursor is clamped to
298    /// the new content.
299    pub fn replace_all(&mut self, text: &str) {
300        let new_cursor = {
301            let mut c = self.content.lock().unwrap();
302            let mut lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
303            if lines.is_empty() {
304                lines.push(String::new());
305            }
306            c.lines = lines;
307            let last_row = c.lines.len().saturating_sub(1);
308            let row = self.cursor.row.min(last_row);
309            let line_chars = c.lines[row].chars().count();
310            let col = self.cursor.col.min(line_chars);
311            c.dirty_gen = c.dirty_gen.wrapping_add(1);
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    }
324
325    /// Shared access to the folds vec. Crate-internal.
326    pub(crate) fn content_lock(&self) -> MutexGuard<'_, Content> {
327        self.content.lock().unwrap()
328    }
329
330    /// Exclusive access to Content. Crate-internal.
331    pub(crate) fn content_lock_mut(&mut self) -> MutexGuard<'_, Content> {
332        self.content.lock().unwrap()
333    }
334
335    // ── Screen-row helpers (private) ──────────────────────────────
336
337    fn cursor_screen_row_from(&self, viewport: &Viewport, top: usize) -> Option<usize> {
338        let cursor = self.cursor;
339        if cursor.row < top {
340            return None;
341        }
342        let c = self.content.lock().unwrap();
343        let v = *viewport;
344        let mut screen = 0usize;
345        for r in top..=cursor.row {
346            if c.folds.iter().any(|f| f.hides(r)) {
347                continue;
348            }
349            let line = c.lines.get(r).map(String::as_str).unwrap_or("");
350            let segs = crate::wrap::wrap_segments(line, v.text_width, v.wrap);
351            if r == cursor.row {
352                let seg_idx = crate::wrap::segment_for_col(&segs, cursor.col);
353                return Some(screen + seg_idx);
354            }
355            screen += segs.len();
356        }
357        None
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn new_has_one_empty_row() {
367        let b = Buffer::new();
368        assert_eq!(b.row_count(), 1);
369        assert_eq!(b.line(0), Some(""));
370        assert_eq!(b.cursor(), Position::default());
371    }
372
373    #[test]
374    fn from_str_splits_on_newline() {
375        let b = Buffer::from_str("foo\nbar\nbaz");
376        assert_eq!(b.row_count(), 3);
377        assert_eq!(b.line(0), Some("foo"));
378        assert_eq!(b.line(2), Some("baz"));
379    }
380
381    #[test]
382    fn from_str_trailing_newline_keeps_empty_row() {
383        let b = Buffer::from_str("foo\n");
384        assert_eq!(b.row_count(), 2);
385        assert_eq!(b.line(1), Some(""));
386    }
387
388    #[test]
389    fn from_str_empty_input_keeps_one_row() {
390        let b = Buffer::from_str("");
391        assert_eq!(b.row_count(), 1);
392        assert_eq!(b.line(0), Some(""));
393    }
394
395    #[test]
396    fn as_string_round_trips() {
397        let b = Buffer::from_str("a\nb\nc");
398        assert_eq!(b.as_string(), "a\nb\nc");
399    }
400
401    #[test]
402    fn dirty_gen_starts_at_zero() {
403        assert_eq!(Buffer::new().dirty_gen(), 0);
404    }
405
406    fn vp_wrap(width: u16, height: u16) -> Viewport {
407        Viewport {
408            top_row: 0,
409            top_col: 0,
410            width,
411            height,
412            wrap: crate::Wrap::Char,
413            text_width: width,
414            tab_width: 0,
415        }
416    }
417
418    #[test]
419    fn ensure_cursor_visible_wrap_scrolls_when_cursor_below_screen() {
420        let mut b = Buffer::from_str("aaaaaaaaaa\nb\nc");
421        let mut v = vp_wrap(4, 3);
422        b.set_cursor(Position::new(2, 0));
423        b.ensure_cursor_visible(&mut v);
424        assert_eq!(v.top_row, 1);
425    }
426
427    #[test]
428    fn ensure_cursor_visible_wrap_no_scroll_when_visible() {
429        let mut b = Buffer::from_str("aaaaaaaaaa\nb");
430        let mut v = vp_wrap(4, 4);
431        b.set_cursor(Position::new(0, 5));
432        b.ensure_cursor_visible(&mut v);
433        assert_eq!(v.top_row, 0);
434    }
435
436    #[test]
437    fn ensure_cursor_visible_wrap_snaps_top_when_cursor_above() {
438        let mut b = Buffer::from_str("a\nb\nc\nd\ne");
439        let mut v = vp_wrap(4, 2);
440        v.top_row = 3;
441        b.set_cursor(Position::new(1, 0));
442        b.ensure_cursor_visible(&mut v);
443        assert_eq!(v.top_row, 1);
444    }
445
446    #[test]
447    fn screen_rows_between_sums_segments_under_wrap() {
448        let b = Buffer::from_str("aaaaaaaaa\nb\n");
449        let v = vp_wrap(4, 0);
450        assert_eq!(b.screen_rows_between(&v, 0, 0), 3);
451        assert_eq!(b.screen_rows_between(&v, 0, 1), 4);
452        assert_eq!(b.screen_rows_between(&v, 0, 2), 5);
453        assert_eq!(b.screen_rows_between(&v, 1, 2), 2);
454    }
455
456    #[test]
457    fn screen_rows_between_one_per_doc_row_when_wrap_off() {
458        let b = Buffer::from_str("aaaaa\nb\nc");
459        let v = Viewport::default();
460        assert_eq!(b.screen_rows_between(&v, 0, 2), 3);
461    }
462
463    #[test]
464    fn max_top_for_height_walks_back_until_height_reached() {
465        let b = Buffer::from_str("a\nb\nc\nd\neeeeeeee");
466        let v = vp_wrap(4, 0);
467        assert_eq!(b.max_top_for_height(&v, 4), 2);
468        assert_eq!(b.max_top_for_height(&v, 99), 0);
469    }
470
471    #[test]
472    fn cursor_screen_row_returns_none_when_wrap_off() {
473        let b = Buffer::from_str("a");
474        let v = Viewport::default();
475        assert!(b.cursor_screen_row(&v).is_none());
476    }
477
478    #[test]
479    fn cursor_screen_row_under_wrap() {
480        let mut b = Buffer::from_str("aaaaaaaaaa\nb");
481        let v = vp_wrap(4, 0);
482        b.set_cursor(Position::new(0, 5));
483        assert_eq!(b.cursor_screen_row(&v), Some(1));
484        b.set_cursor(Position::new(1, 0));
485        assert_eq!(b.cursor_screen_row(&v), Some(3));
486    }
487
488    #[test]
489    fn ensure_cursor_visible_falls_back_when_wrap_disabled() {
490        let mut b = Buffer::from_str("a\nb\nc\nd\ne");
491        let mut v = Viewport {
492            top_row: 0,
493            top_col: 0,
494            width: 4,
495            height: 2,
496            wrap: crate::Wrap::None,
497            text_width: 4,
498            tab_width: 0,
499        };
500        b.set_cursor(Position::new(4, 0));
501        b.ensure_cursor_visible(&mut v);
502        assert_eq!(v.top_row, 3);
503    }
504
505    // ── View-split tests (new in 0.8.0) ──────────────────────────
506
507    /// Two `Buffer` views sharing one `Content` must have independent
508    /// cursors.
509    #[test]
510    fn buffer_views_independent_cursors() {
511        let a = Buffer::from_str("hello\nworld");
512        let arc = a.content_arc();
513        let mut view_a = Buffer::new_view(Arc::clone(&arc));
514        let mut view_b = Buffer::new_view(Arc::clone(&arc));
515
516        view_a.set_cursor(Position::new(1, 3));
517        // view_b cursor must remain at (0, 0).
518        assert_eq!(view_b.cursor(), Position::new(0, 0));
519
520        view_b.set_cursor(Position::new(0, 2));
521        // view_a cursor must remain at (1, 3).
522        assert_eq!(view_a.cursor(), Position::new(1, 3));
523    }
524
525    /// An edit applied via one view must be visible via the other.
526    #[test]
527    fn buffer_views_share_content() {
528        use crate::edit::Edit;
529
530        let a = Buffer::from_str("foo");
531        let arc = a.content_arc();
532        let mut view_a = Buffer::new_view(Arc::clone(&arc));
533        let view_b = Buffer::new_view(Arc::clone(&arc));
534
535        view_a.apply_edit(Edit::InsertStr {
536            at: Position::new(0, 3),
537            text: "bar".into(),
538        });
539
540        assert_eq!(view_a.line(0), Some("foobar"));
541        assert_eq!(view_b.line(0), Some("foobar"));
542    }
543}