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 ropey::RopeSlice;
15
16use crate::buffer::TextBuffer;
17
18impl TextBuffer {
19    /// Number of lines in the buffer.
20    ///
21    /// Ropey counts lines by `'\n'` boundaries and always reports at least 1 line,
22    /// even for empty text.
23    #[inline]
24    pub fn len_lines(&self) -> usize {
25        self.rope.len_lines()
26    }
27
28    /// Clamp a line index to the valid range `[0, len_lines - 1]`.
29    ///
30    /// If the buffer is empty, Ropey still reports `len_lines() == 1`, so this
31    /// always returns a valid line index.
32    #[inline]
33    pub fn clamp_line(&self, line: usize) -> usize {
34        let last = self.len_lines().saturating_sub(1);
35        min(line, last)
36    }
37
38    /// Returns the absolute char index at the start of `line`.
39    ///
40    /// `line` is clamped into a valid range.
41    #[inline]
42    pub fn line_to_char(&self, line: usize) -> usize {
43        let line = self.clamp_line(line);
44        self.rope.line_to_char(line)
45    }
46
47    /// Returns the line index containing `char_idx`.
48    ///
49    /// `char_idx` is clamped to `[0, len_chars]`.
50    #[inline]
51    pub fn char_to_line(&self, char_idx: usize) -> usize {
52        let c = min(char_idx, self.len_chars());
53        self.rope.char_to_line(c)
54    }
55
56    /// Returns the length of `line` in chars, excluding a trailing `'\n'` if present.
57    ///
58    /// This corresponds to the number of valid "columns" for a `(line, col)` cursor
59    /// model where the newline is not considered part of the line.
60    pub fn line_len_chars(&self, line: usize) -> usize {
61        let line = self.clamp_line(line);
62        let slice = self.rope.line(line);
63
64        // Ropey line slices typically include the newline if present.
65        let mut len = slice.len_chars();
66        if len > 0 && slice.char(len - 1) == '\n' {
67            len -= 1;
68        }
69
70        len
71    }
72
73    /// Returns the first non-whitespace column on `line`.
74    ///
75    /// If the line is all whitespace or empty, this returns `line_len_chars(line)`.
76    pub fn line_first_non_whitespace_col(&self, line: usize) -> usize {
77        let line = self.clamp_line(line);
78        let slice = self.line_slice(line);
79
80        for (idx, ch) in slice.chars().enumerate() {
81            if !ch.is_whitespace() {
82                return idx;
83            }
84        }
85
86        self.line_len_chars(line)
87    }
88
89    /// Returns the line content as a `String`, excluding a trailing `'\n'` if present.
90    pub fn line_string(&self, line: usize) -> String {
91        self.line_slice(line).to_string()
92    }
93
94    /// Returns a non-allocating line slice excluding a trailing `'\n'` if present.
95    pub fn line_slice(&self, line: usize) -> RopeSlice<'_> {
96        let line = self.clamp_line(line);
97        let range = self.line_char_range(line);
98        self.rope.slice(range)
99    }
100
101    /// Returns the char range `[start, end)` for the line content, excluding a trailing `'\n'`.
102    ///
103    /// This will be useful for operations like "delete to end of line" or yanking the line
104    /// content without the newline.
105    pub fn line_char_range(&self, line: usize) -> std::ops::Range<usize> {
106        let line = self.clamp_line(line);
107        let start = self.rope.line_to_char(line);
108
109        // `line(line).len_chars()` includes the newline if present.
110        let end_including_newline = start + self.rope.line(line).len_chars();
111
112        // Drop exactly one trailing '\n' if present.
113        let end =
114            if end_including_newline > start && self.rope.char(end_including_newline - 1) == '\n' {
115                end_including_newline - 1
116            } else {
117                end_including_newline
118            };
119
120        start..end
121    }
122
123    /// Returns the absolute char index of the end of `line`, including a trailing
124    /// `'\n'` when one exists.
125    ///
126    /// This is useful for line-block transforms that need stable slice boundaries
127    /// across both newline-terminated and non-terminated final lines.
128    pub fn line_full_end_char(&self, line: usize) -> usize {
129        let line = self.clamp_line(line);
130        if line + 1 < self.len_lines() {
131            self.rope.line_to_char(line + 1)
132        } else {
133            self.rope.line_to_char(line) + self.line_len_chars(line)
134        }
135    }
136}