Skip to main content

redox_core/buffer/text_buffer/
lines.rs

1//! Line-oriented helpers for `TextBuffer`.
2//!
3//! This file is intended to be included by the parent `text_buffer` module, and
4//! adds line/indexing utilities as an inherent `impl` on `TextBuffer`.
5//!
6//! Design notes
7//! - These APIs use **char indices** (Unicode scalar values), matching `ropey`.
8//! - Treats the trailing `'\n'` as *not part of the editable line*, so
9//!   `line_len_chars()` excludes it when present.
10//! - All functions are defensive, meaning they clamp out-of-range inputs.
11
12use std::cmp::min;
13
14use crate::buffer::TextBuffer;
15
16impl TextBuffer {
17    /// Number of lines in the buffer.
18    ///
19    /// Ropey counts lines by `'\n'` boundaries and always reports at least 1 line,
20    /// even for empty text.
21    #[inline]
22    pub fn len_lines(&self) -> usize {
23        self.rope.len_lines()
24    }
25
26    /// Clamp a line index to the valid range `[0, len_lines - 1]`.
27    ///
28    /// If the buffer is empty, Ropey still reports `len_lines() == 1`, so this
29    /// always returns a valid line index.
30    #[inline]
31    pub fn clamp_line(&self, line: usize) -> usize {
32        let last = self.len_lines().saturating_sub(1);
33        min(line, last)
34    }
35
36    /// Returns the absolute char index at the start of `line`.
37    ///
38    /// `line` is clamped into a valid range.
39    #[inline]
40    pub fn line_to_char(&self, line: usize) -> usize {
41        let line = self.clamp_line(line);
42        self.rope.line_to_char(line)
43    }
44
45    /// Returns the line index containing `char_idx`.
46    ///
47    /// `char_idx` is clamped to `[0, len_chars]`.
48    #[inline]
49    pub fn char_to_line(&self, char_idx: usize) -> usize {
50        let c = min(char_idx, self.len_chars());
51        self.rope.char_to_line(c)
52    }
53
54    /// Returns the length of `line` in chars, excluding a trailing `'\n'` if present.
55    ///
56    /// This corresponds to the number of valid "columns" for a `(line, col)` cursor
57    /// model where the newline is not considered part of the line.
58    pub fn line_len_chars(&self, line: usize) -> usize {
59        let line = self.clamp_line(line);
60        let slice = self.rope.line(line);
61
62        // Ropey line slices typically include the newline if present.
63        let mut len = slice.len_chars();
64        if len > 0 && slice.char(len - 1) == '\n' {
65            len -= 1;
66        }
67
68        len
69    }
70
71    /// Returns the line content as a `String`, excluding a trailing `'\n'` if present.
72    pub fn line_string(&self, line: usize) -> String {
73        let line = self.clamp_line(line);
74        let slice = self.rope.line(line);
75        let s = slice.to_string();
76        s.strip_suffix('\n').unwrap_or(&s).to_string()
77    }
78
79    /// Returns the char range `[start, end)` for the line content, excluding a trailing `'\n'`.
80    ///
81    /// This will be useful for operations like "delete to end of line" or yanking the line
82    /// content without the newline.
83    pub fn line_char_range(&self, line: usize) -> std::ops::Range<usize> {
84        let line = self.clamp_line(line);
85        let start = self.rope.line_to_char(line);
86
87        // `line(line).len_chars()` includes the newline if present.
88        let end_including_newline = start + self.rope.line(line).len_chars();
89
90        // Drop exactly one trailing '\n' if present.
91        let end =
92            if end_including_newline > start && self.rope.char(end_including_newline - 1) == '\n' {
93                end_including_newline - 1
94            } else {
95                end_including_newline
96            };
97
98        start..end
99    }
100}