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    /// Returns a snapshot of every line as an owned `Vec<String>`.
114    ///
115    /// Owned rather than `&[String]` because a `Buffer` is a per-window
116    /// view onto a shared `Content`; another view could mutate the rope
117    /// between when this returns and when the caller reads the slice,
118    /// invalidating any borrowed reference.
119    pub fn lines(&self) -> Vec<String> {
120        self.content_lock().lines.clone()
121    }
122
123    /// Returns a clone of the line at `row`, or `None` if out of bounds.
124    ///
125    /// Owned rather than `Option<&str>` for the same reason as [`Buffer::lines`]:
126    /// another view sharing the same `Content` could reallocate the backing `Vec`
127    /// between the lock release and the caller's use of the reference.
128    pub fn line(&self, row: usize) -> Option<String> {
129        self.content_lock().lines.get(row).cloned()
130    }
131
132    pub fn cursor(&self) -> Position {
133        self.cursor
134    }
135
136    pub fn dirty_gen(&self) -> u64 {
137        self.content.lock().unwrap().dirty_gen
138    }
139
140    /// Number of rows in the buffer. Always `>= 1`.
141    pub fn row_count(&self) -> usize {
142        self.content.lock().unwrap().lines.len()
143    }
144
145    /// Concatenate the rows into a single `String` joined by `\n`.
146    pub fn as_string(&self) -> String {
147        self.content.lock().unwrap().lines.join("\n")
148    }
149
150    // ── Cursor ops ────────────────────────────────────────────────
151
152    /// Set cursor without scrolling. Clamps to valid positions.
153    ///
154    /// The optional sticky column for `j`/`k` motions is **not** reset
155    /// by this call — it survives `set_cursor` intentionally.
156    pub fn set_cursor(&mut self, pos: Position) {
157        let c = self.content.lock().unwrap();
158        let last_row = c.lines.len().saturating_sub(1);
159        let row = pos.row.min(last_row);
160        let line_chars = c.lines[row].chars().count();
161        let col = pos.col.min(line_chars);
162        drop(c);
163        self.cursor = Position::new(row, col);
164    }
165
166    /// Bring the cursor into the visible [`Viewport`], scrolling by the
167    /// minimum amount needed.
168    pub fn ensure_cursor_visible(&mut self, viewport: &mut Viewport) {
169        let cursor = self.cursor;
170        let v = *viewport;
171        let wrap_active = !matches!(v.wrap, crate::Wrap::None) && v.text_width > 0;
172        if !wrap_active {
173            viewport.ensure_visible(cursor);
174            return;
175        }
176        if v.height == 0 {
177            return;
178        }
179        // Cursor above the visible region: snap top_row to it.
180        if cursor.row < v.top_row {
181            viewport.top_row = cursor.row;
182            viewport.top_col = 0;
183            return;
184        }
185        let height = v.height as usize;
186        // Push top_row forward until cursor lands inside [0, height).
187        loop {
188            let csr = self.cursor_screen_row_from(viewport, viewport.top_row);
189            match csr {
190                Some(row) if row < height => break,
191                _ => {}
192            }
193            let next = {
194                let c = self.content.lock().unwrap();
195                let mut n = viewport.top_row + 1;
196                while n <= cursor.row && c.folds.iter().any(|f| f.hides(n)) {
197                    n += 1;
198                }
199                n
200            };
201            if next > cursor.row {
202                viewport.top_row = cursor.row;
203                break;
204            }
205            viewport.top_row = next;
206        }
207        viewport.top_col = 0;
208    }
209
210    /// Cursor's screen row offset (0-based) from `viewport.top_row`.
211    pub fn cursor_screen_row(&self, viewport: &Viewport) -> Option<usize> {
212        if matches!(viewport.wrap, crate::Wrap::None) || viewport.text_width == 0 {
213            return None;
214        }
215        self.cursor_screen_row_from(viewport, viewport.top_row)
216    }
217
218    /// Number of screen rows the doc range `start..=end` occupies.
219    pub fn screen_rows_between(&self, viewport: &Viewport, start: usize, end: usize) -> usize {
220        if start > end {
221            return 0;
222        }
223        let c = self.content.lock().unwrap();
224        let last = c.lines.len().saturating_sub(1);
225        let end = end.min(last);
226        let v = *viewport;
227        let mut total = 0usize;
228        for r in start..=end {
229            if c.folds.iter().any(|f| f.hides(r)) {
230                continue;
231            }
232            if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
233                total += 1;
234            } else {
235                let line = c.lines.get(r).map(String::as_str).unwrap_or("");
236                total += crate::wrap::wrap_segments(line, v.text_width, v.wrap).len();
237            }
238        }
239        total
240    }
241
242    /// Earliest `top_row` such that `screen_rows_between(top, last)`
243    /// is at least `height`.
244    pub fn max_top_for_height(&self, viewport: &Viewport, height: usize) -> usize {
245        if height == 0 {
246            return 0;
247        }
248        let c = self.content.lock().unwrap();
249        let last = c.lines.len().saturating_sub(1);
250        let mut total = 0usize;
251        let mut row = last;
252        loop {
253            if !c.folds.iter().any(|f| f.hides(row)) {
254                let v = *viewport;
255                total += if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
256                    1
257                } else {
258                    let line = c.lines.get(row).map(String::as_str).unwrap_or("");
259                    crate::wrap::wrap_segments(line, v.text_width, v.wrap).len()
260                };
261            }
262            if total >= height {
263                return row;
264            }
265            if row == 0 {
266                return 0;
267            }
268            row -= 1;
269        }
270    }
271
272    /// Clamp `pos` to the buffer's content.
273    pub fn clamp_position(&self, pos: Position) -> Position {
274        let c = self.content.lock().unwrap();
275        let last_row = c.lines.len().saturating_sub(1);
276        let row = pos.row.min(last_row);
277        let line_chars = c.lines[row].chars().count();
278        let col = pos.col.min(line_chars);
279        Position::new(row, col)
280    }
281
282    /// Replace the buffer's full text in place. Cursor is clamped to
283    /// the new content.
284    pub fn replace_all(&mut self, text: &str) {
285        let new_cursor = {
286            let mut c = self.content.lock().unwrap();
287            let mut lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
288            if lines.is_empty() {
289                lines.push(String::new());
290            }
291            c.lines = lines;
292            let last_row = c.lines.len().saturating_sub(1);
293            let row = self.cursor.row.min(last_row);
294            let line_chars = c.lines[row].chars().count();
295            let col = self.cursor.col.min(line_chars);
296            c.dirty_gen = c.dirty_gen.wrapping_add(1);
297            Position::new(row, col)
298        };
299        self.cursor = new_cursor;
300    }
301
302    // ── Crate-internal accessors (used by folds.rs) ───────────────
303
304    /// Bump the render-cache generation. Crate-internal.
305    pub(crate) fn dirty_gen_bump(&mut self) {
306        let mut c = self.content.lock().unwrap();
307        c.dirty_gen = c.dirty_gen.wrapping_add(1);
308    }
309
310    /// Shared access to the folds vec. Crate-internal.
311    pub(crate) fn content_lock(&self) -> MutexGuard<'_, Content> {
312        self.content.lock().unwrap()
313    }
314
315    /// Exclusive access to Content. Crate-internal.
316    pub(crate) fn content_lock_mut(&mut self) -> MutexGuard<'_, Content> {
317        self.content.lock().unwrap()
318    }
319
320    // ── Screen-row helpers (private) ──────────────────────────────
321
322    fn cursor_screen_row_from(&self, viewport: &Viewport, top: usize) -> Option<usize> {
323        let cursor = self.cursor;
324        if cursor.row < top {
325            return None;
326        }
327        let c = self.content.lock().unwrap();
328        let v = *viewport;
329        let mut screen = 0usize;
330        for r in top..=cursor.row {
331            if c.folds.iter().any(|f| f.hides(r)) {
332                continue;
333            }
334            let line = c.lines.get(r).map(String::as_str).unwrap_or("");
335            let segs = crate::wrap::wrap_segments(line, v.text_width, v.wrap);
336            if r == cursor.row {
337                let seg_idx = crate::wrap::segment_for_col(&segs, cursor.col);
338                return Some(screen + seg_idx);
339            }
340            screen += segs.len();
341        }
342        None
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn new_has_one_empty_row() {
352        let b = Buffer::new();
353        assert_eq!(b.row_count(), 1);
354        assert_eq!(b.line(0).as_deref(), Some(""));
355        assert_eq!(b.cursor(), Position::default());
356    }
357
358    #[test]
359    fn from_str_splits_on_newline() {
360        let b = Buffer::from_str("foo\nbar\nbaz");
361        assert_eq!(b.row_count(), 3);
362        assert_eq!(b.line(0).as_deref(), Some("foo"));
363        assert_eq!(b.line(2).as_deref(), Some("baz"));
364    }
365
366    #[test]
367    fn from_str_trailing_newline_keeps_empty_row() {
368        let b = Buffer::from_str("foo\n");
369        assert_eq!(b.row_count(), 2);
370        assert_eq!(b.line(1).as_deref(), Some(""));
371    }
372
373    #[test]
374    fn from_str_empty_input_keeps_one_row() {
375        let b = Buffer::from_str("");
376        assert_eq!(b.row_count(), 1);
377        assert_eq!(b.line(0).as_deref(), Some(""));
378    }
379
380    #[test]
381    fn as_string_round_trips() {
382        let b = Buffer::from_str("a\nb\nc");
383        assert_eq!(b.as_string(), "a\nb\nc");
384    }
385
386    #[test]
387    fn dirty_gen_starts_at_zero() {
388        assert_eq!(Buffer::new().dirty_gen(), 0);
389    }
390
391    fn vp_wrap(width: u16, height: u16) -> Viewport {
392        Viewport {
393            top_row: 0,
394            top_col: 0,
395            width,
396            height,
397            wrap: crate::Wrap::Char,
398            text_width: width,
399            tab_width: 0,
400        }
401    }
402
403    #[test]
404    fn ensure_cursor_visible_wrap_scrolls_when_cursor_below_screen() {
405        let mut b = Buffer::from_str("aaaaaaaaaa\nb\nc");
406        let mut v = vp_wrap(4, 3);
407        b.set_cursor(Position::new(2, 0));
408        b.ensure_cursor_visible(&mut v);
409        assert_eq!(v.top_row, 1);
410    }
411
412    #[test]
413    fn ensure_cursor_visible_wrap_no_scroll_when_visible() {
414        let mut b = Buffer::from_str("aaaaaaaaaa\nb");
415        let mut v = vp_wrap(4, 4);
416        b.set_cursor(Position::new(0, 5));
417        b.ensure_cursor_visible(&mut v);
418        assert_eq!(v.top_row, 0);
419    }
420
421    #[test]
422    fn ensure_cursor_visible_wrap_snaps_top_when_cursor_above() {
423        let mut b = Buffer::from_str("a\nb\nc\nd\ne");
424        let mut v = vp_wrap(4, 2);
425        v.top_row = 3;
426        b.set_cursor(Position::new(1, 0));
427        b.ensure_cursor_visible(&mut v);
428        assert_eq!(v.top_row, 1);
429    }
430
431    #[test]
432    fn screen_rows_between_sums_segments_under_wrap() {
433        let b = Buffer::from_str("aaaaaaaaa\nb\n");
434        let v = vp_wrap(4, 0);
435        assert_eq!(b.screen_rows_between(&v, 0, 0), 3);
436        assert_eq!(b.screen_rows_between(&v, 0, 1), 4);
437        assert_eq!(b.screen_rows_between(&v, 0, 2), 5);
438        assert_eq!(b.screen_rows_between(&v, 1, 2), 2);
439    }
440
441    #[test]
442    fn screen_rows_between_one_per_doc_row_when_wrap_off() {
443        let b = Buffer::from_str("aaaaa\nb\nc");
444        let v = Viewport::default();
445        assert_eq!(b.screen_rows_between(&v, 0, 2), 3);
446    }
447
448    #[test]
449    fn max_top_for_height_walks_back_until_height_reached() {
450        let b = Buffer::from_str("a\nb\nc\nd\neeeeeeee");
451        let v = vp_wrap(4, 0);
452        assert_eq!(b.max_top_for_height(&v, 4), 2);
453        assert_eq!(b.max_top_for_height(&v, 99), 0);
454    }
455
456    #[test]
457    fn cursor_screen_row_returns_none_when_wrap_off() {
458        let b = Buffer::from_str("a");
459        let v = Viewport::default();
460        assert!(b.cursor_screen_row(&v).is_none());
461    }
462
463    #[test]
464    fn cursor_screen_row_under_wrap() {
465        let mut b = Buffer::from_str("aaaaaaaaaa\nb");
466        let v = vp_wrap(4, 0);
467        b.set_cursor(Position::new(0, 5));
468        assert_eq!(b.cursor_screen_row(&v), Some(1));
469        b.set_cursor(Position::new(1, 0));
470        assert_eq!(b.cursor_screen_row(&v), Some(3));
471    }
472
473    #[test]
474    fn ensure_cursor_visible_falls_back_when_wrap_disabled() {
475        let mut b = Buffer::from_str("a\nb\nc\nd\ne");
476        let mut v = Viewport {
477            top_row: 0,
478            top_col: 0,
479            width: 4,
480            height: 2,
481            wrap: crate::Wrap::None,
482            text_width: 4,
483            tab_width: 0,
484        };
485        b.set_cursor(Position::new(4, 0));
486        b.ensure_cursor_visible(&mut v);
487        assert_eq!(v.top_row, 3);
488    }
489
490    // ── View-split tests (new in 0.8.0) ──────────────────────────
491
492    /// Two `Buffer` views sharing one `Content` must have independent
493    /// cursors.
494    #[test]
495    fn buffer_views_independent_cursors() {
496        let a = Buffer::from_str("hello\nworld");
497        let arc = a.content_arc();
498        let mut view_a = Buffer::new_view(Arc::clone(&arc));
499        let mut view_b = Buffer::new_view(Arc::clone(&arc));
500
501        view_a.set_cursor(Position::new(1, 3));
502        // view_b cursor must remain at (0, 0).
503        assert_eq!(view_b.cursor(), Position::new(0, 0));
504
505        view_b.set_cursor(Position::new(0, 2));
506        // view_a cursor must remain at (1, 3).
507        assert_eq!(view_a.cursor(), Position::new(1, 3));
508    }
509
510    /// An edit applied via one view must be visible via the other.
511    #[test]
512    fn buffer_views_share_content() {
513        use crate::edit::Edit;
514
515        let a = Buffer::from_str("foo");
516        let arc = a.content_arc();
517        let mut view_a = Buffer::new_view(Arc::clone(&arc));
518        let view_b = Buffer::new_view(Arc::clone(&arc));
519
520        view_a.apply_edit(Edit::InsertStr {
521            at: Position::new(0, 3),
522            text: "bar".into(),
523        });
524
525        assert_eq!(view_a.line(0).as_deref(), Some("foobar"));
526        assert_eq!(view_b.line(0).as_deref(), Some("foobar"));
527    }
528}