binocular/preview/rich_text/
utils.rs1use crate::app::App;
2use crate::preview::PreviewContent;
3
4pub fn get_line_content(app: &App, line_idx: usize) -> Option<String> {
5 if let Some(PreviewContent::RichText(text)) = &app.preview_session.preview.content {
6 return text.line_slice(line_idx).map(ToString::to_string);
7 }
8 None
9}
10
11pub fn get_line_count(app: &App) -> usize {
12 if let Some(PreviewContent::RichText(text)) = &app.preview_session.preview.content {
13 text.line_count()
14 } else {
15 0
16 }
17}
18
19pub fn get_byte_index(content: &str, line: usize, char_idx: usize) -> usize {
20 let mut current_line = 0;
21 let mut current_char = 0;
22 let mut byte_idx = 0;
23
24 for (idx, c) in content.char_indices() {
25 if current_line == line && current_char == char_idx {
26 return idx;
27 }
28
29 if c == '\n' {
30 current_line += 1;
31 current_char = 0;
32 } else {
33 current_char += 1;
34 }
35 byte_idx = idx + c.len_utf8();
36 }
37
38 if current_line == line && current_char == char_idx {
39 return byte_idx;
40 }
41
42 byte_idx
43}
44
45pub fn get_line_char_from_byte(content: &str, byte_idx: usize) -> (usize, usize) {
46 let mut line = 0;
47 let mut char_idx = 0;
48 let mut current_byte = 0;
49
50 for (idx, c) in content.char_indices() {
51 if idx == byte_idx {
52 return (line, char_idx);
53 }
54 if c == '\n' {
55 line += 1;
56 char_idx = 0;
57 } else {
58 char_idx += 1;
59 }
60 current_byte = idx + c.len_utf8();
61 }
62
63 if current_byte == byte_idx {
64 return (line, char_idx);
65 }
66
67 (line, char_idx)
68}
69
70pub fn line_len(app: &App, line_idx: usize) -> usize {
71 if let Some(content) = get_line_content(app, line_idx) {
72 let chars: Vec<char> = content.chars().collect();
73 let mut len = chars.len();
74 while len > 0 && (chars[len - 1] == '\n' || chars[len - 1] == '\r') {
75 len -= 1;
76 }
77 len
78 } else {
79 0
80 }
81}
82
83pub fn char_type(c: char) -> u8 {
84 if c.is_alphanumeric() || c == '_' {
85 2
86 } else if c.is_whitespace() {
87 0
88 } else {
89 1
90 }
91}
92
93pub struct DocIter {
94 pub line: usize,
95 pub col: usize,
96 line_count: usize,
97}
98
99impl DocIter {
100 pub fn new(app: &App, line: usize, col: usize) -> Self {
101 Self {
102 line,
103 col,
104 line_count: get_line_count(app),
105 }
106 }
107
108 pub fn from_cursor(app: &App) -> Self {
109 Self::new(
110 app,
111 app.preview_session.preview.state.cursor_line,
112 app.preview_session.preview.state.cursor_char,
113 )
114 }
115
116 pub fn char_at(&self, app: &App) -> Option<char> {
117 get_line_content(app, self.line).and_then(|s| s.chars().nth(self.col))
118 }
119
120 pub fn char_type_at(&self, app: &App) -> u8 {
121 self.char_at(app).map(char_type).unwrap_or(0)
122 }
123
124 pub fn advance(&mut self, app: &App) -> bool {
125 let len = get_line_content(app, self.line)
126 .map(|s| s.chars().count())
127 .unwrap_or(0);
128 if self.col + 1 < len {
129 self.col += 1;
130 true
131 } else if self.line + 1 < self.line_count {
132 self.line += 1;
133 self.col = 0;
134 true
135 } else {
136 false
137 }
138 }
139
140 pub fn retreat(&mut self, app: &App) -> bool {
141 if self.col > 0 {
142 self.col -= 1;
143 true
144 } else if self.line > 0 {
145 self.line -= 1;
146 let len = get_line_content(app, self.line)
147 .map(|s| s.chars().count())
148 .unwrap_or(0);
149 self.col = if len > 0 { len - 1 } else { 0 };
150 true
151 } else {
152 false
153 }
154 }
155
156 pub fn apply(&self, app: &mut App) {
157 app.preview_session.preview.state.cursor_line = self.line;
158 app.preview_session.preview.state.cursor_char = self.col;
159 }
160}
161
162pub fn ensure_cursor_in_bounds(app: &mut App) {
163 use crate::app::InputMode;
164
165 let line_count = get_line_count(app);
166 if line_count == 0 {
167 app.preview_session.preview.state.cursor_line = 0;
168 app.preview_session.preview.state.cursor_char = 0;
169 return;
170 }
171
172 if app.preview_session.preview.state.cursor_line >= line_count {
173 app.preview_session.preview.state.cursor_line = line_count - 1;
174 }
175
176 let len = line_len(app, app.preview_session.preview.state.cursor_line);
177 if len == 0 {
178 app.preview_session.preview.state.cursor_char = 0;
179 } else if app.preview_session.preview.state.mode == InputMode::Insert {
180 if app.preview_session.preview.state.cursor_char > len {
181 app.preview_session.preview.state.cursor_char = len;
182 }
183 } else if app.preview_session.preview.state.cursor_char >= len {
184 app.preview_session.preview.state.cursor_char = len - 1;
185 }
186}
187
188pub fn adjust_scroll(app: &mut App) {
189 let viewport_height = if app.preview_height() > 2 {
190 app.preview_height() - 2
191 } else {
192 1
193 };
194 let viewport_width = if app.preview_width() > 2 {
195 app.preview_width() - 2
196 } else {
197 1
198 };
199
200 const LINE_NUM_WIDTH: usize = 5;
201
202 if app.preview_session.preview.state.cursor_line < app.preview_session.preview.state.scroll {
203 app.preview_session.preview.state.scroll = app.preview_session.preview.state.cursor_line;
204 } else if app.preview_session.preview.state.cursor_line
205 >= (app.preview_session.preview.state.scroll + viewport_height as usize)
206 {
207 app.preview_session.preview.state.scroll = (app.preview_session.preview.state.cursor_line
208 + 1)
209 .saturating_sub(viewport_height as usize);
210 }
211
212 let content_viewport = (viewport_width as usize).saturating_sub(LINE_NUM_WIDTH);
213
214 if app.preview_session.preview.state.cursor_char < app.preview_session.preview.state.scroll_char
215 {
216 app.preview_session.preview.state.scroll_char =
217 app.preview_session.preview.state.cursor_char;
218 }
219
220 let visible_end = app.preview_session.preview.state.scroll_char + content_viewport;
221 if app.preview_session.preview.state.cursor_char >= visible_end {
222 app.preview_session.preview.state.scroll_char =
223 (app.preview_session.preview.state.cursor_char + 1).saturating_sub(content_viewport);
224 }
225}