Skip to main content

idet_core/
lineops.rs

1//! Moving the current line up or down.
2
3/// Direction in which [`move_line`] shifts the current line.
4#[derive(Clone, Copy)]
5pub enum LineMove {
6    /// Move the line towards the start of the text.
7    Up,
8    /// Move the line towards the end of the text.
9    Down,
10}
11
12/// Swaps the line at `cursor` with its neighbour in `direction`.
13///
14/// `cursor` is a character index; the cursor stays on the moved line. Returns
15/// the new text and cursor, or `None` when there is no neighbouring line.
16#[must_use]
17pub fn move_line(text: &str, cursor: usize, direction: LineMove) -> Option<(String, usize)> {
18    let mut lines: Vec<&str> = text.split('\n').collect();
19    let (line, column) = line_column(text, cursor);
20    let target = match direction {
21        LineMove::Up => line.checked_sub(1)?,
22        LineMove::Down => (line + 1 < lines.len()).then_some(line + 1)?,
23    };
24    lines.swap(line, target);
25    let moved_cursor = cursor_at(&lines, target, column);
26    Some((lines.join("\n"), moved_cursor))
27}
28
29/// Locates the zero-indexed `(line, column)` of the character at `cursor`.
30#[must_use]
31pub fn line_column(text: &str, cursor: usize) -> (usize, usize) {
32    let mut line = 0;
33    let mut column = 0;
34    for (index, character) in text.chars().enumerate() {
35        if index == cursor {
36            break;
37        }
38        if character == '\n' {
39            line += 1;
40            column = 0;
41        } else {
42            column += 1;
43        }
44    }
45    (line, column)
46}
47
48/// Converts a `(line, column)` position back into a character index, given
49/// `text` already split on `\n`.
50#[must_use]
51pub fn cursor_at(lines: &[&str], line: usize, column: usize) -> usize {
52    let preceding: usize = lines[..line].iter().map(|l| l.chars().count() + 1).sum();
53    preceding + column
54}