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}