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}