xi_core_lib/
movement.rs

1// Copyright 2017 The xi-editor Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Representation and calculation of movement within a view.
16
17use std::cmp::max;
18
19use crate::selection::{HorizPos, SelRegion, Selection};
20use crate::view::View;
21use crate::word_boundaries::WordCursor;
22use xi_rope::{Cursor, LinesMetric, Rope};
23
24/// The specification of a movement.
25#[derive(Debug, PartialEq, Clone, Copy)]
26pub enum Movement {
27    /// Move to the left by one grapheme cluster.
28    Left,
29    /// Move to the right by one grapheme cluster.
30    Right,
31    /// Move to the left by one word.
32    LeftWord,
33    /// Move to the right by one word.
34    RightWord,
35    /// Move to left end of visible line.
36    LeftOfLine,
37    /// Move to right end of visible line.
38    RightOfLine,
39    /// Move up one visible line.
40    Up,
41    /// Move down one visible line.
42    Down,
43    /// Move up one viewport height.
44    UpPage,
45    /// Move down one viewport height.
46    DownPage,
47    /// Move up to the next line that can preserve the cursor position.
48    UpExactPosition,
49    /// Move down to the next line that can preserve the cursor position.
50    DownExactPosition,
51    /// Move to the start of the text line.
52    StartOfParagraph,
53    /// Move to the end of the text line.
54    EndOfParagraph,
55    /// Move to the end of the text line, or next line if already at end.
56    EndOfParagraphKill,
57    /// Move to the start of the document.
58    StartOfDocument,
59    /// Move to the end of the document
60    EndOfDocument,
61}
62
63/// Compute movement based on vertical motion by the given number of lines.
64///
65/// Note: in non-exceptional cases, this function preserves the `horiz`
66/// field of the selection region.
67fn vertical_motion(
68    r: SelRegion,
69    view: &View,
70    text: &Rope,
71    line_delta: isize,
72    modify: bool,
73) -> (usize, Option<HorizPos>) {
74    let (col, line) = selection_position(r, view, text, line_delta < 0, modify);
75    let n_lines = view.line_of_offset(text, text.len());
76
77    // This code is quite careful to avoid integer overflow.
78    // TODO: write tests to verify
79    if line_delta < 0 && (-line_delta as usize) > line {
80        return (0, Some(col));
81    }
82    let line = if line_delta < 0 {
83        line - (-line_delta as usize)
84    } else {
85        line.saturating_add(line_delta as usize)
86    };
87    if line > n_lines {
88        return (text.len(), Some(col));
89    }
90    let new_offset = view.line_col_to_offset(text, line, col);
91    (new_offset, Some(col))
92}
93
94/// Compute movement based on vertical motion by the given number of lines skipping
95/// any line that is shorter than the current cursor position.
96fn vertical_motion_exact_pos(
97    r: SelRegion,
98    view: &View,
99    text: &Rope,
100    move_up: bool,
101    modify: bool,
102) -> (usize, Option<HorizPos>) {
103    let (col, init_line) = selection_position(r, view, text, move_up, modify);
104    let n_lines = view.line_of_offset(text, text.len());
105
106    let mut line_length = view.offset_of_line(text, init_line.saturating_add(1))
107        - view.offset_of_line(text, init_line);
108    if move_up && init_line == 0 {
109        return (view.line_col_to_offset(text, init_line, col), Some(col));
110    }
111    let mut line = if move_up { init_line - 1 } else { init_line.saturating_add(1) };
112
113    // If the active columns is longer than the current line, use the current line length.
114    let col = if line_length < col { line_length - 1 } else { col };
115
116    loop {
117        line_length = view.offset_of_line(text, line + 1) - view.offset_of_line(text, line);
118
119        // If the line is longer than the current cursor position, break.
120        // We use > instead of >= because line_length includes newline.
121        if line_length > col {
122            break;
123        }
124
125        // If you are trying to add a selection past the end of the file or before the first line, return original selection
126        if line >= n_lines || (line == 0 && move_up) {
127            line = init_line;
128            break;
129        }
130
131        line = if move_up { line - 1 } else { line.saturating_add(1) };
132    }
133
134    (view.line_col_to_offset(text, line, col), Some(col))
135}
136
137/// Based on the current selection position this will return the cursor position, the current line, and the
138/// total number of lines of the file.
139fn selection_position(
140    r: SelRegion,
141    view: &View,
142    text: &Rope,
143    move_up: bool,
144    modify: bool,
145) -> (HorizPos, usize) {
146    // The active point of the selection
147    let active = if modify {
148        r.end
149    } else if move_up {
150        r.min()
151    } else {
152        r.max()
153    };
154    let col = if let Some(col) = r.horiz { col } else { view.offset_to_line_col(text, active).1 };
155    let line = view.line_of_offset(text, active);
156
157    (col, line)
158}
159
160/// When paging through a file, the number of lines from the previous page
161/// that will also be visible in the next.
162const SCROLL_OVERLAP: isize = 2;
163
164/// Computes the actual desired amount of scrolling (generally slightly
165/// less than the height of the viewport, to allow overlap).
166fn scroll_height(view: &View) -> isize {
167    max(view.scroll_height() as isize - SCROLL_OVERLAP, 1)
168}
169
170/// Compute the result of movement on one selection region.
171pub fn region_movement(
172    m: Movement,
173    r: SelRegion,
174    view: &View,
175    text: &Rope,
176    modify: bool,
177) -> SelRegion {
178    let (offset, horiz) = match m {
179        Movement::Left => {
180            if r.is_caret() || modify {
181                if let Some(offset) = text.prev_grapheme_offset(r.end) {
182                    (offset, None)
183                } else {
184                    (0, r.horiz)
185                }
186            } else {
187                (r.min(), None)
188            }
189        }
190        Movement::Right => {
191            if r.is_caret() || modify {
192                if let Some(offset) = text.next_grapheme_offset(r.end) {
193                    (offset, None)
194                } else {
195                    (r.end, r.horiz)
196                }
197            } else {
198                (r.max(), None)
199            }
200        }
201        Movement::LeftWord => {
202            let mut word_cursor = WordCursor::new(text, r.end);
203            let offset = word_cursor.prev_boundary().unwrap_or(0);
204            (offset, None)
205        }
206        Movement::RightWord => {
207            let mut word_cursor = WordCursor::new(text, r.end);
208            let offset = word_cursor.next_boundary().unwrap_or_else(|| text.len());
209            (offset, None)
210        }
211        Movement::LeftOfLine => {
212            let line = view.line_of_offset(text, r.end);
213            let offset = view.offset_of_line(text, line);
214            (offset, None)
215        }
216        Movement::RightOfLine => {
217            let line = view.line_of_offset(text, r.end);
218            let mut offset = text.len();
219
220            // calculate end of line
221            let next_line_offset = view.offset_of_line(text, line + 1);
222            if line < view.line_of_offset(text, offset) {
223                if let Some(prev) = text.prev_grapheme_offset(next_line_offset) {
224                    offset = prev;
225                }
226            }
227            (offset, None)
228        }
229        Movement::Up => vertical_motion(r, view, text, -1, modify),
230        Movement::Down => vertical_motion(r, view, text, 1, modify),
231        Movement::UpExactPosition => vertical_motion_exact_pos(r, view, text, true, modify),
232        Movement::DownExactPosition => vertical_motion_exact_pos(r, view, text, false, modify),
233        Movement::StartOfParagraph => {
234            // Note: TextEdit would start at modify ? r.end : r.min()
235            let mut cursor = Cursor::new(&text, r.end);
236            let offset = cursor.prev::<LinesMetric>().unwrap_or(0);
237            (offset, None)
238        }
239        Movement::EndOfParagraph => {
240            // Note: TextEdit would start at modify ? r.end : r.max()
241            let mut offset = r.end;
242            let mut cursor = Cursor::new(&text, offset);
243            if let Some(next_para_offset) = cursor.next::<LinesMetric>() {
244                if cursor.is_boundary::<LinesMetric>() {
245                    if let Some(eol) = text.prev_grapheme_offset(next_para_offset) {
246                        offset = eol;
247                    }
248                } else if cursor.pos() == text.len() {
249                    offset = text.len();
250                }
251                (offset, None)
252            } else {
253                //in this case we are already on a last line so just moving to EOL
254                (text.len(), None)
255            }
256        }
257        Movement::EndOfParagraphKill => {
258            // Note: TextEdit would start at modify ? r.end : r.max()
259            let mut offset = r.end;
260            let mut cursor = Cursor::new(&text, offset);
261            if let Some(next_para_offset) = cursor.next::<LinesMetric>() {
262                offset = next_para_offset;
263                if cursor.is_boundary::<LinesMetric>() {
264                    if let Some(eol) = text.prev_grapheme_offset(next_para_offset) {
265                        if eol != r.end {
266                            offset = eol;
267                        }
268                    }
269                }
270            }
271            (offset, None)
272        }
273        Movement::UpPage => vertical_motion(r, view, text, -scroll_height(view), modify),
274        Movement::DownPage => vertical_motion(r, view, text, scroll_height(view), modify),
275        Movement::StartOfDocument => (0, None),
276        Movement::EndOfDocument => (text.len(), None),
277    };
278    SelRegion::new(if modify { r.start } else { offset }, offset).with_horiz(horiz)
279}
280
281/// Compute a new selection by applying a movement to an existing selection.
282///
283/// In a multi-region selection, this function applies the movement to each
284/// region in the selection, and returns the union of the results.
285///
286/// If `modify` is `true`, the selections are modified, otherwise the results
287/// of individual region movements become carets.
288pub fn selection_movement(
289    m: Movement,
290    s: &Selection,
291    view: &View,
292    text: &Rope,
293    modify: bool,
294) -> Selection {
295    let mut result = Selection::new();
296    for &r in s.iter() {
297        let new_region = region_movement(m, r, view, text, modify);
298        result.add_region(new_region);
299    }
300    result
301}