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//!   those can be layered on later (eg. a view layer that maps `Pos` <-> screen).
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    /// NOTE: This is a simple version with no goal/preferred column tracking.
83    /// If I decide I want Vim-like vertical motion that remembers a preferred column,
84    /// I should store that in higher-level state and clamp it using `line_len_chars(...)`.
85    #[inline]
86    pub fn move_up(&self, pos: Pos) -> Pos {
87        let pos = self.clamp_pos(pos);
88        if pos.line == 0 {
89            return pos;
90        }
91        let new_line = pos.line - 1;
92        let new_col = min(pos.col, self.line_len_chars(new_line));
93        Pos::new(new_line, new_col)
94    }
95
96    /// Move down one line, preserving column as much as possible.
97    ///
98    /// This is a simple version (no goal/preferred column tracking).
99    /// NOTE: Same as above :)
100    #[inline]
101    pub fn move_down(&self, pos: Pos) -> Pos {
102        let pos = self.clamp_pos(pos);
103        let last = self.len_lines().saturating_sub(1);
104        if pos.line >= last {
105            return pos;
106        }
107        let new_line = pos.line + 1;
108        let new_col = min(pos.col, self.line_len_chars(new_line));
109        Pos::new(new_line, new_col)
110    }
111
112    /// Get the char at a position, if it's within the line's content (not including newline).
113    #[inline]
114    pub fn char_at(&self, pos: Pos) -> Option<char> {
115        let pos = self.clamp_pos(pos);
116        let line_len = self.line_len_chars(pos.line);
117        if pos.col >= line_len {
118            return None;
119        }
120        let idx = self.pos_to_char(pos);
121        Some(self.rope.char(idx))
122    }
123
124    /// Get the char before a position, if one exists.
125    #[inline]
126    pub fn char_before(&self, pos: Pos) -> Option<char> {
127        let c = self.pos_to_char(pos);
128        if c == 0 {
129            None
130        } else {
131            Some(self.rope.char(c - 1))
132        }
133    }
134}