Skip to main content

binocular/preview/rich_text/
utils.rs

1use 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}