Skip to main content

redox_core/buffer/text_buffer/
positions.rs

1//! Position conversion and cursor movement helpers for `TextBuffer`.
2//!
3//! This file is intended to be included by `buffer::text_buffer`'s module wiring,
4//! so that `TextBuffer`'s implementation can be split into focused, maintainable
5//! chunks.
6//!
7//! Design notes on extensibility:
8//! - Positions are logical `(line, col)` in **char units** (Unicode scalar values),
9//!   matching Ropey's indexing model.
10//! - Methods clamp inputs defensively, so higher-level code can stay simpler.
11//! - Visual column/grapheme cluster concerns are deliberately out of scope here;
12//!   these are handled in higher layers that map `Pos` to screen coordinates.
13
14use std::cmp::min;
15
16use super::TextBuffer;
17use crate::buffer::Pos;
18
19impl TextBuffer {
20    /// Clamp a position to a valid location in the buffer.
21    ///
22    /// - Line is clamped to `[0, len_lines - 1]`
23    /// - Column is clamped to `[0, line_len_chars(line)]`
24    #[inline]
25    pub fn clamp_pos(&self, pos: Pos) -> Pos {
26        let line = self.clamp_line(pos.line);
27        let max_col = self.line_len_chars(line);
28        let col = min(pos.col, max_col);
29        Pos { line, col }
30    }
31
32    /// Convert `Pos` (line+col) to absolute char index in the rope.
33    ///
34    /// The position is clamped first.
35    #[inline]
36    pub fn pos_to_char(&self, pos: Pos) -> usize {
37        let pos = self.clamp_pos(pos);
38        self.rope.line_to_char(pos.line) + pos.col
39    }
40
41    /// Convert absolute char index to `Pos` (line+col).
42    ///
43    /// `char_idx` is clamped to `[0, len_chars]`.
44    #[inline]
45    pub fn char_to_pos(&self, char_idx: usize) -> Pos {
46        let c = min(char_idx, self.len_chars());
47        let line = self.rope.char_to_line(c);
48        let line_start = self.rope.line_to_char(line);
49        let col = c - line_start;
50
51        // If `c` points at a newline, clamp col to `line_len` (ie. end of the line).
52        let max_col = self.line_len_chars(line);
53        Pos {
54            line,
55            col: min(col, max_col),
56        }
57    }
58
59    /// Move position left by one char, staying within buffer.
60    #[inline]
61    pub fn move_left(&self, pos: Pos) -> Pos {
62        let c = self.pos_to_char(pos);
63        if c == 0 {
64            return Pos::zero();
65        }
66        self.char_to_pos(c - 1)
67    }
68
69    /// Move position right by one char, staying within buffer.
70    #[inline]
71    pub fn move_right(&self, pos: Pos) -> Pos {
72        let c = self.pos_to_char(pos);
73        let maxc = self.len_chars();
74        if c >= maxc {
75            return self.char_to_pos(maxc);
76        }
77        self.char_to_pos(c + 1)
78    }
79
80    /// Move up one line, preserving column as much as possible.
81    ///
82    /// This implementation does not track a preferred column.
83    #[inline]
84    pub fn move_up(&self, pos: Pos) -> Pos {
85        let pos = self.clamp_pos(pos);
86        if pos.line == 0 {
87            return pos;
88        }
89        let new_line = pos.line - 1;
90        let new_col = min(pos.col, self.line_len_chars(new_line));
91        Pos::new(new_line, new_col)
92    }
93
94    /// Move down one line, preserving column as much as possible.
95    ///
96    /// This implementation does not track a preferred column.
97    #[inline]
98    pub fn move_down(&self, pos: Pos) -> Pos {
99        let pos = self.clamp_pos(pos);
100        let last = self.len_lines().saturating_sub(1);
101        if pos.line >= last {
102            return pos;
103        }
104        let new_line = pos.line + 1;
105        let new_col = min(pos.col, self.line_len_chars(new_line));
106        Pos::new(new_line, new_col)
107    }
108
109    /// Get the char at a position, if it's within the line's content (not including newline).
110    #[inline]
111    pub fn char_at(&self, pos: Pos) -> Option<char> {
112        let pos = self.clamp_pos(pos);
113        let line_len = self.line_len_chars(pos.line);
114        if pos.col >= line_len {
115            return None;
116        }
117        let idx = self.pos_to_char(pos);
118        Some(self.rope.char(idx))
119    }
120
121    /// Get the char before a position, if one exists.
122    #[inline]
123    pub fn char_before(&self, pos: Pos) -> Option<char> {
124        let c = self.pos_to_char(pos);
125        if c == 0 {
126            None
127        } else {
128            Some(self.rope.char(c - 1))
129        }
130    }
131}