1
34
use crate::{Document, LineNumber, Position, Row, RowIndex, ViewportOffset};
2
use std::cmp;
3
use std::collections::HashMap;
4

            
5
38
fn matching_closing_symbols() -> HashMap<&'static str, &'static str> {
6
38
    [("'", "'"), ("\"", "\""), ("{", "}"), ("(", ")"), ("[", "]")]
7
        .iter()
8
        .copied()
9
        .collect()
10
38
}
11

            
12
41
fn matching_opening_symbols() -> HashMap<&'static str, &'static str> {
13
41
    [("'", "'"), ("\"", "\""), ("}", "{"), (")", "("), ("]", "[")]
14
        .iter()
15
        .copied()
16
        .collect()
17
41
}
18
4
#[derive(PartialEq)]
19
pub enum Boundary {
20
    Start,
21
    End,
22
}
23

            
24
#[derive(Debug)]
25
pub struct Navigator {}
26

            
27
impl Navigator {
28
    #[must_use]
29
3
    pub fn find_index_of_first_non_whitespace(row: &Row) -> Option<usize> {
30
5
        for (x, character) in row.string.chars().enumerate() {
31
10
            if !character.is_whitespace() {
32
3
                return Some(x);
33
            }
34
        }
35
        None
36
6
    }
37

            
38
    /// Return the index of the matching closing symbol (eg } for {, etc)
39
    /// # Panics
40
    /// TODO
41
    #[must_use]
42
4
    pub fn find_matching_closing_symbol(
43
        document: &Document,
44
        current_position: &Position,
45
        offset: &ViewportOffset,
46
    ) -> Option<Position> {
47
4
        let initial_col_position = current_position.x.saturating_add(offset.columns);
48
4
        let initial_row_position = current_position.y.saturating_add(offset.rows);
49
8
        let symbol = document
50
4
            .get_row(RowIndex::new(initial_row_position))
51
            .unwrap()
52
4
            .nth_grapheme(current_position.x.saturating_add(offset.columns));
53
4
        let mut stack = vec![symbol];
54
4
        let mut current_opening_symbol = symbol;
55
4
        matching_closing_symbols().get(&symbol)?;
56
7
        for y in initial_row_position..document.num_rows() {
57
12
            let current_row = document.get_row(RowIndex::new(y)).unwrap();
58
6
            let start_x = if y == initial_row_position {
59
4
                initial_col_position.saturating_add(1)
60
            } else {
61
2
                0
62
            };
63
22
            for index in start_x..current_row.len() {
64
38
                let c = current_row.nth_grapheme(index);
65
39
                if c == *matching_closing_symbols()
66
                    .get(&current_opening_symbol)
67
19
                    .unwrap()
68
                {
69
4
                    stack.pop();
70
4
                    if stack.is_empty() {
71
3
                        return Some(Position { x: index, y });
72
                    }
73
1
                    current_opening_symbol = *stack.last().unwrap();
74
16
                } else if matching_closing_symbols().contains_key(&c) {
75
1
                    stack.push(c);
76
1
                    current_opening_symbol = c;
77
                }
78
            }
79
        }
80
1
        None
81
4
    }
82

            
83
    /// Return the index of the matching opening symbol (eg } for {, etc)
84
    /// # Panics
85
    /// TODO
86
    #[must_use]
87
3
    pub fn find_matching_opening_symbol(
88
        document: &Document,
89
        current_position: &Position,
90
        offset: &ViewportOffset,
91
    ) -> Option<Position> {
92
3
        let initial_col_position = current_position.x.saturating_add(offset.columns);
93
3
        let initial_row_position = current_position.y.saturating_add(offset.rows);
94
6
        let symbol = document
95
3
            .get_row(RowIndex::new(initial_row_position))
96
            .unwrap()
97
3
            .nth_grapheme(current_position.x.saturating_add(offset.columns));
98
3
        let mut stack = vec![symbol];
99
3
        let mut current_closing_symbol = symbol;
100
3
        matching_opening_symbols().get(&symbol)?;
101
6
        for y in (0..=initial_row_position).rev() {
102
10
            let current_row = document.get_row(RowIndex::new(y)).unwrap();
103
5
            let start_x = if y == initial_row_position {
104
3
                initial_col_position
105
            } else {
106
2
                current_row.len()
107
            };
108
23
            for index in (0..start_x).rev() {
109
40
                let c = current_row.nth_grapheme(index);
110
40
                if c == *matching_opening_symbols()
111
                    .get(&current_closing_symbol)
112
20
                    .unwrap()
113
                {
114
2
                    stack.pop();
115
2
                    if stack.is_empty() {
116
2
                        return Some(Position { x: index, y });
117
                    }
118
                    current_closing_symbol = *stack.last().unwrap();
119
18
                } else if matching_opening_symbols().contains_key(&c) {
120
                    stack.push(c);
121
                    current_closing_symbol = c;
122
                }
123
            }
124
        }
125
1
        None
126
3
    }
127

            
128
    #[must_use]
129
6
    pub fn find_line_number_of_start_or_end_of_paragraph(
130
        document: &Document,
131
        current_line_number: LineNumber,
132
        boundary: &Boundary,
133
    ) -> LineNumber {
134
6
        let mut current_line_number = current_line_number;
135
12
        loop {
136
16
            current_line_number = match boundary {
137
4
                Boundary::Start => cmp::max(LineNumber::new(1), current_line_number.previous()),
138
4
                Boundary::End => cmp::min(document.last_line_number(), current_line_number.next()),
139
            };
140
14
            if (current_line_number == LineNumber::new(1) && boundary == &Boundary::Start)
141
8
                || (current_line_number == document.last_line_number()
142
2
                    && boundary == &Boundary::End)
143
            {
144
4
                return current_line_number;
145
            }
146

            
147
4
            let current_line = document.row_for_line_number(current_line_number);
148
4
            let previous_line = document.row_for_line_number(current_line_number.previous());
149

            
150
4
            if let Some(previous_line) = previous_line {
151
6
                if let Some(current_line) = current_line {
152
                    let current_line_followed_by_empty_line =
153
4
                        current_line.is_whitespace() && !previous_line.is_whitespace();
154
4
                    if current_line_followed_by_empty_line {
155
2
                        return current_line_number;
156
                    }
157
                }
158
            }
159
        }
160
6
    }
161

            
162
    #[allow(clippy::suspicious_operation_groupings)]
163
    #[must_use]
164
    // mirrorred over the look and feel of vim
165
    // Note: this assumes working on char, and I _think_ is is shaky at best
166
    // as we start supporting unicde, as an unicode is made of code points, each
167
    // of which is internally represented by a char, so this has no change of _really_ working well.
168
    // we should drop that function and try to rely on the string.split_word_bounds
169
    // method implemented in the unicode-segmentation crate. However, that crate seems
170
    // to drop all characters (eg: heart) that isn't alphabetic.
171
123
    pub fn is_word_delimiter(char1: char, char2: char) -> bool {
172
123
        if char2.is_whitespace() || char1 == '_' || char2 == '_' {
173
24
            return false;
174
        }
175
352
        (char1.is_alphabetic() && !char2.is_alphabetic())
176
93
            || (!char1.is_alphabetic() && char2.is_alphabetic())
177
80
            || (char1.is_alphanumeric() && char2.is_ascii_punctuation())
178
80
            || (char1.is_whitespace() && char2.is_alphanumeric())
179
147
    }
180

            
181
    #[must_use]
182
24
    pub fn find_index_of_next_or_previous_word(
183
        current_row: &Row,
184
        current_x_position: usize,
185
        boundary: &Boundary,
186
    ) -> usize {
187
24
        let current_x_index = current_x_position.saturating_add(1);
188
24
        match boundary {
189
12
            Boundary::End => {
190
12
                let mut current_char = current_row.nth_char(current_x_position);
191
63
                for (i, next_char) in current_row.chars().skip(current_x_index).enumerate() {
192
122
                    if Self::is_word_delimiter(current_char, next_char) {
193
10
                        return current_x_index.saturating_add(i);
194
                    }
195
51
                    current_char = next_char;
196
                }
197
2
                current_row.len().saturating_sub(1)
198
            }
199
            Boundary::Start => {
200
57
                for i in (1..current_x_index.saturating_sub(1)).rev() {
201
102
                    let current_char = current_row.nth_char(i);
202
51
                    let prev_char = current_row.nth_char(i.saturating_sub(1));
203
51
                    if Self::is_word_delimiter(prev_char, current_char) {
204
6
                        return i;
205
                    }
206
                }
207
6
                0
208
            }
209
        }
210
24
    }
211
}
212

            
213
#[cfg(test)]
214
#[path = "./navigator_test.rs"]
215
mod navigator_test;